Fix JSON paths for arrays
This commit is contained in:
parent
abb1ce6a31
commit
ae8baa0339
2 changed files with 42 additions and 25 deletions
|
@ -140,10 +140,10 @@ class NegatedExpression:
|
||||||
return self.expression
|
return self.expression
|
||||||
|
|
||||||
def __and__(self, other):
|
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):
|
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
|
@property
|
||||||
def left(self):
|
def left(self):
|
||||||
|
@ -221,10 +221,20 @@ class ExpressionProxy:
|
||||||
return Expression(left=self.field, op=Operators.GE, right=other, parents=self.parents)
|
return Expression(left=self.field, op=Operators.GE, right=other, parents=self.parents)
|
||||||
|
|
||||||
def __getattr__(self, item):
|
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__):
|
if isinstance(attr, self.__class__):
|
||||||
attr.parents.insert(0, (self.field.name, self.field.outer_type_))
|
attr.parents.append((self.field.name, self.field.outer_type_))
|
||||||
attr.parents = attr.parents + self.parents
|
attr.parents = self.parents + attr.parents
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
|
|
||||||
|
@ -786,6 +796,8 @@ class ModelMeta(ModelMetaclass):
|
||||||
# Create proxies for each model field so that we can use the field
|
# Create proxies for each model field so that we can use the field
|
||||||
# in queries, like Model.get(Model.field_name == 1)
|
# in queries, like Model.get(Model.field_name == 1)
|
||||||
for field_name, field in new_class.__fields__.items():
|
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, []))
|
setattr(new_class, field_name, ExpressionProxy(field, []))
|
||||||
# Check if this is our FieldInfo version with extended ORM metadata.
|
# Check if this is our FieldInfo version with extended ORM metadata.
|
||||||
if isinstance(field.field_info, FieldInfo):
|
if isinstance(field.field_info, FieldInfo):
|
||||||
|
@ -1092,8 +1104,8 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def schema_for_type(cls, json_path: str, name: str, name_prefix: str, typ: Any,
|
def schema_for_type(cls, json_path: str, name: str, name_prefix: str, typ: Any,
|
||||||
field_info: PydanticFieldInfo) -> str:
|
field_info: PydanticFieldInfo,
|
||||||
print(json_path, name, name_prefix, typ)
|
parent_type: Optional[Any] = None) -> str:
|
||||||
should_index = getattr(field_info, 'index', False)
|
should_index = getattr(field_info, 'index', False)
|
||||||
field_type = get_origin(typ)
|
field_type = get_origin(typ)
|
||||||
try:
|
try:
|
||||||
|
@ -1111,28 +1123,28 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
log.warning("Model %s defined an empty list field: %s", cls, name)
|
log.warning("Model %s defined an empty list field: %s", cls, name)
|
||||||
return ""
|
return ""
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
return cls.schema_for_type(f"{json_path}.{name}[]", name, name_prefix,
|
return cls.schema_for_type(f"{json_path}.{name}[*]", name, name_prefix,
|
||||||
embedded_cls, field_info)
|
embedded_cls, field_info, parent_type=field_type)
|
||||||
elif field_is_model:
|
elif field_is_model:
|
||||||
name_prefix = f"{name_prefix}_{name}" if name_prefix else name
|
name_prefix = f"{name_prefix}_{name}" if name_prefix else name
|
||||||
sub_fields = []
|
sub_fields = []
|
||||||
for embedded_name, field in typ.__fields__.items():
|
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
|
# This is a list, so the correct JSONPath expression is to
|
||||||
# refer directly to attribute names after the list notation,
|
# refer directly to attribute names after the list notation,
|
||||||
# e.g. orders[].created_date.
|
# e.g. orders[*].created_date.
|
||||||
path = f"{json_path}.{embedded_name}"
|
path = json_path
|
||||||
else:
|
else:
|
||||||
# All other fields should use dot notation with both the
|
# All other fields should use dot notation with both the
|
||||||
# current field name and "embedded" field name, e.g.,
|
# current field name and "embedded" field name, e.g.,
|
||||||
# order.address.street_line_1.
|
# order.address.street_line_1.
|
||||||
path = f"{json_path}.{name}.{embedded_name}"
|
path = f"{json_path}.{name}"
|
||||||
print(path)
|
|
||||||
sub_fields.append(cls.schema_for_type(path,
|
sub_fields.append(cls.schema_for_type(path,
|
||||||
embedded_name,
|
embedded_name,
|
||||||
name_prefix,
|
name_prefix,
|
||||||
field.outer_type_,
|
field.outer_type_,
|
||||||
field.field_info))
|
field.field_info,
|
||||||
|
parent_type=field_type))
|
||||||
return " ".join(filter(None, sub_fields))
|
return " ".join(filter(None, sub_fields))
|
||||||
elif should_index:
|
elif should_index:
|
||||||
index_field_name = f"{name_prefix}_{name}" if name_prefix else name
|
index_field_name = f"{name_prefix}_{name}" if name_prefix else name
|
||||||
|
|
|
@ -45,7 +45,8 @@ class Address(EmbeddedJsonModel):
|
||||||
|
|
||||||
class Item(EmbeddedJsonModel):
|
class Item(EmbeddedJsonModel):
|
||||||
price: decimal.Decimal
|
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):
|
class Order(EmbeddedJsonModel):
|
||||||
|
@ -268,6 +269,18 @@ def test_exact_match_queries(members):
|
||||||
actual = Member.find(Member.address.city == "Portland").all()
|
actual = Member.find(Member.address.city == "Portland").all()
|
||||||
assert actual == [member1, member2, member3]
|
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",
|
member1.address.note = Note(description="Weird house",
|
||||||
created_on=datetime.datetime.now())
|
created_on=datetime.datetime.now())
|
||||||
member1.save()
|
member1.save()
|
||||||
|
@ -284,14 +297,6 @@ def test_exact_match_queries(members):
|
||||||
assert actual == [member1]
|
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):
|
def test_tag_queries_boolean_logic(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
@ -451,4 +456,4 @@ def test_not_found():
|
||||||
|
|
||||||
|
|
||||||
def test_schema():
|
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"
|
Loading…
Reference in a new issue