From ae8baa0339b5778b461f0012380471fe50e275ec Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 13 Oct 2021 08:12:22 -0700 Subject: [PATCH] Fix JSON paths for arrays --- redis_developer/orm/model.py | 42 +++++++++++++++++++++++------------- tests/test_json_model.py | 25 ++++++++++++--------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/redis_developer/orm/model.py b/redis_developer/orm/model.py index 344e7c6..5adbebe 100644 --- a/redis_developer/orm/model.py +++ b/redis_developer/orm/model.py @@ -140,10 +140,10 @@ class NegatedExpression: return self.expression def __and__(self, other): - return Expression(left=self, op=Operators.AND, right=other) + return Expression(left=self, op=Operators.AND, right=other, parents=self.expression.parents) def __or__(self, other): - return Expression(left=self, op=Operators.OR, right=other) + return Expression(left=self, op=Operators.OR, right=other, parents=self.expression.parents) @property def left(self): @@ -221,10 +221,20 @@ class ExpressionProxy: return Expression(left=self.field, op=Operators.GE, right=other, parents=self.parents) def __getattr__(self, item): - attr = getattr(self.field.outer_type_, item) + if get_origin(self.field.outer_type_) == list: + embedded_cls = get_args(self.field.outer_type_) + if not embedded_cls: + # TODO: Is this even possible? + raise QuerySyntaxError("In order to query on a list field, you must define " + "the contents of the list with a type annotation, like: " + "orders: List[Order]. Docs: TODO") + embedded_cls = embedded_cls[0] + attr = getattr(embedded_cls, item) + else: + attr = getattr(self.field.outer_type_, item) if isinstance(attr, self.__class__): - attr.parents.insert(0, (self.field.name, self.field.outer_type_)) - attr.parents = attr.parents + self.parents + attr.parents.append((self.field.name, self.field.outer_type_)) + attr.parents = self.parents + attr.parents return attr @@ -786,6 +796,8 @@ class ModelMeta(ModelMetaclass): # Create proxies for each model field so that we can use the field # in queries, like Model.get(Model.field_name == 1) for field_name, field in new_class.__fields__.items(): + if new_class.__name__ == "Order": + print(new_class.__fields__) setattr(new_class, field_name, ExpressionProxy(field, [])) # Check if this is our FieldInfo version with extended ORM metadata. if isinstance(field.field_info, FieldInfo): @@ -1092,8 +1104,8 @@ class JsonModel(RedisModel, abc.ABC): @classmethod def schema_for_type(cls, json_path: str, name: str, name_prefix: str, typ: Any, - field_info: PydanticFieldInfo) -> str: - print(json_path, name, name_prefix, typ) + field_info: PydanticFieldInfo, + parent_type: Optional[Any] = None) -> str: should_index = getattr(field_info, 'index', False) field_type = get_origin(typ) try: @@ -1111,28 +1123,28 @@ class JsonModel(RedisModel, abc.ABC): log.warning("Model %s defined an empty list field: %s", cls, name) return "" embedded_cls = embedded_cls[0] - return cls.schema_for_type(f"{json_path}.{name}[]", name, name_prefix, - embedded_cls, field_info) + return cls.schema_for_type(f"{json_path}.{name}[*]", name, name_prefix, + embedded_cls, field_info, parent_type=field_type) elif field_is_model: name_prefix = f"{name_prefix}_{name}" if name_prefix else name sub_fields = [] for embedded_name, field in typ.__fields__.items(): - if json_path.endswith("[]"): + if parent_type == list or isinstance(parent_type, RedisModel): # This is a list, so the correct JSONPath expression is to # refer directly to attribute names after the list notation, - # e.g. orders[].created_date. - path = f"{json_path}.{embedded_name}" + # e.g. orders[*].created_date. + path = json_path else: # All other fields should use dot notation with both the # current field name and "embedded" field name, e.g., # order.address.street_line_1. - path = f"{json_path}.{name}.{embedded_name}" - print(path) + path = f"{json_path}.{name}" sub_fields.append(cls.schema_for_type(path, embedded_name, name_prefix, field.outer_type_, - field.field_info)) + field.field_info, + parent_type=field_type)) return " ".join(filter(None, sub_fields)) elif should_index: index_field_name = f"{name_prefix}_{name}" if name_prefix else name diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 2e35c05..5c845ce 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -45,7 +45,8 @@ class Address(EmbeddedJsonModel): class Item(EmbeddedJsonModel): price: decimal.Decimal - name: str = Field(index=True, full_text_search=True) + # name: str = Field(index=True, full_text_search=True) + name: str = Field(index=True) class Order(EmbeddedJsonModel): @@ -268,6 +269,18 @@ def test_exact_match_queries(members): actual = Member.find(Member.address.city == "Portland").all() assert actual == [member1, member2, member3] + +def test_recursive_query_expression_resolution(members): + member1, member2, member3 = members + + actual = Member.find((Member.last_name == "Brookins") | ( + Member.age == 100 + ) & (Member.last_name == "Smith")).all() + assert actual == [member1, member2, member3] + + +def test_recursive_query_field_resolution(members): + member1, _, _ = members member1.address.note = Note(description="Weird house", created_on=datetime.datetime.now()) member1.save() @@ -284,14 +297,6 @@ def test_exact_match_queries(members): assert actual == [member1] -def test_recursive_query_resolution(members): - member1, member2, member3 = members - - actual = Member.find((Member.last_name == "Brookins") | ( - Member.age == 100 - ) & (Member.last_name == "Smith")).all() - assert actual == [member1, member2, member3] - def test_tag_queries_boolean_logic(members): member1, member2, member3 = members @@ -451,4 +456,4 @@ def test_not_found(): def test_schema(): - assert Member.redisearch_schema() == "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.address.pk AS address_pk TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address__pk TAG SEPARATOR | $.address.note.description AS address__description TAG SEPARATOR | $.orders[].pk AS orders_pk TAG SEPARATOR | $.orders[].items[].pk AS orders_items_pk TAG SEPARATOR | $.orders[].items[].name AS orders_items_name TAG SEPARATOR | $.orders[].items[].name AS orders_items_name_fts TEXT" + assert Member.redisearch_schema() == "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | SORTABLE $.first_name AS first_name TAG SEPARATOR | SORTABLE $.last_name AS last_name TAG SEPARATOR | SORTABLE $.email AS email TAG SEPARATOR | SORTABLE $.age AS age NUMERIC SORTABLE $.address.pk AS address_pk TAG SEPARATOR | SORTABLE $.address.city AS address_city TAG SEPARATOR | SORTABLE $.address.postal_code AS address_postal_code TAG SEPARATOR | SORTABLE $.address.note.pk AS address_note_pk TAG SEPARATOR | SORTABLE $.address.note.description AS address_note_description TAG SEPARATOR | SORTABLE $.orders[*].pk AS orders_pk TAG SEPARATOR | SORTABLE $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | SORTABLE $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR | SORTABLE" \ No newline at end of file