Avoid adding same parent to attr multiple times
This commit is contained in:
parent
ae8baa0339
commit
e59e210834
2 changed files with 50 additions and 14 deletions
|
@ -220,6 +220,9 @@ class ExpressionProxy:
|
||||||
def __ge__(self, other: Any) -> Expression: # type: ignore[override]
|
def __ge__(self, other: Any) -> Expression: # type: ignore[override]
|
||||||
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 __mod__(self, other: Any) -> Expression: # type: ignore[override]
|
||||||
|
return Expression(left=self.field, op=Operators.LIKE, right=other, parents=self.parents)
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if get_origin(self.field.outer_type_) == list:
|
if get_origin(self.field.outer_type_) == list:
|
||||||
embedded_cls = get_args(self.field.outer_type_)
|
embedded_cls = get_args(self.field.outer_type_)
|
||||||
|
@ -233,8 +236,12 @@ class ExpressionProxy:
|
||||||
else:
|
else:
|
||||||
attr = getattr(self.field.outer_type_, item)
|
attr = getattr(self.field.outer_type_, item)
|
||||||
if isinstance(attr, self.__class__):
|
if isinstance(attr, self.__class__):
|
||||||
attr.parents.append((self.field.name, self.field.outer_type_))
|
new_parent = (self.field.name, self.field.outer_type_)
|
||||||
attr.parents = self.parents + attr.parents
|
if not new_parent in attr.parents:
|
||||||
|
attr.parents.append(new_parent)
|
||||||
|
new_parents = list(set(self.parents) - set(attr.parents))
|
||||||
|
if new_parents:
|
||||||
|
attr.parents = new_parents + attr.parents
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
|
|
||||||
|
@ -315,10 +322,15 @@ class FindQuery:
|
||||||
return sort_fields
|
return sort_fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_field_type(field: ModelField) -> RediSearchFieldTypes:
|
def resolve_field_type(field: ModelField, operator: Operators) -> RediSearchFieldTypes:
|
||||||
if getattr(field.field_info, 'primary_key', None) is True:
|
if getattr(field.field_info, 'primary_key', None) is True:
|
||||||
return RediSearchFieldTypes.TAG
|
return RediSearchFieldTypes.TAG
|
||||||
elif getattr(field.field_info, 'full_text_search', None) is True:
|
elif operator is Operators.LIKE:
|
||||||
|
fts = getattr(field.field_info, 'full_text_search', None)
|
||||||
|
if fts is not True: # Could be PydanticUndefined
|
||||||
|
raise QuerySyntaxError(f"You tried to do a full-text search on the field '{field.name}', "
|
||||||
|
f"but the field is not indexed for full-text search. Use the "
|
||||||
|
f"full_text_search=True option. Docs: TODO")
|
||||||
return RediSearchFieldTypes.TEXT
|
return RediSearchFieldTypes.TEXT
|
||||||
|
|
||||||
field_type = field.outer_type_
|
field_type = field.outer_type_
|
||||||
|
@ -353,7 +365,7 @@ class FindQuery:
|
||||||
field_name = f"{prefix}_{field_name}"
|
field_name = f"{prefix}_{field_name}"
|
||||||
result = ""
|
result = ""
|
||||||
if field_type is RediSearchFieldTypes.TEXT:
|
if field_type is RediSearchFieldTypes.TEXT:
|
||||||
result = f"@{field_name}:"
|
result = f"@{field_name}_fts:"
|
||||||
if op is Operators.EQ:
|
if op is Operators.EQ:
|
||||||
result += f'"{value}"'
|
result += f'"{value}"'
|
||||||
elif op is Operators.NE:
|
elif op is Operators.NE:
|
||||||
|
@ -458,7 +470,7 @@ class FindQuery:
|
||||||
isinstance(expression.left, NegatedExpression):
|
isinstance(expression.left, NegatedExpression):
|
||||||
result += f"({cls.resolve_redisearch_query(expression.left)})"
|
result += f"({cls.resolve_redisearch_query(expression.left)})"
|
||||||
elif isinstance(expression.left, ModelField):
|
elif isinstance(expression.left, ModelField):
|
||||||
field_type = cls.resolve_field_type(expression.left)
|
field_type = cls.resolve_field_type(expression.left, expression.op)
|
||||||
field_name = expression.left.name
|
field_name = expression.left.name
|
||||||
field_info = expression.left.field_info
|
field_info = expression.left.field_info
|
||||||
if not field_info or not getattr(field_info, "index", None):
|
if not field_info or not getattr(field_info, "index", None):
|
||||||
|
@ -796,8 +808,6 @@ 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):
|
||||||
|
@ -1160,8 +1170,7 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
else:
|
else:
|
||||||
schema_part = f"{path} AS {index_field_name} TAG"
|
schema_part = f"{path} AS {index_field_name} TAG"
|
||||||
# TODO: GEO field
|
# TODO: GEO field
|
||||||
if should_index:
|
schema_part += " SORTABLE"
|
||||||
schema_part += " SORTABLE"
|
|
||||||
return schema_part
|
return schema_part
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -29,7 +29,7 @@ class EmbeddedJsonModel(BaseJsonModel, abc.ABC):
|
||||||
|
|
||||||
|
|
||||||
class Note(EmbeddedJsonModel):
|
class Note(EmbeddedJsonModel):
|
||||||
description: str = Field(index=True)
|
description: str = Field(index=True, full_text_search=True)
|
||||||
created_on: datetime.datetime
|
created_on: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,8 +45,7 @@ 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):
|
||||||
|
@ -297,6 +296,34 @@ def test_recursive_query_field_resolution(members):
|
||||||
assert actual == [member1]
|
assert actual == [member1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_text_search(members):
|
||||||
|
member1, member2, _ = members
|
||||||
|
member1.address.note = Note(description="white house",
|
||||||
|
created_on=datetime.datetime.now())
|
||||||
|
member2.address.note = Note(description="blue house",
|
||||||
|
created_on=datetime.datetime.now())
|
||||||
|
member1.save()
|
||||||
|
member2.save()
|
||||||
|
|
||||||
|
actual = Member.find(Member.address.note.description % "white").all()
|
||||||
|
assert actual == [member1]
|
||||||
|
|
||||||
|
member1.orders = [
|
||||||
|
Order(items=[Item(price=10.99, name="balls")],
|
||||||
|
total=10.99,
|
||||||
|
created_on=datetime.datetime.now())
|
||||||
|
]
|
||||||
|
member2.orders = [
|
||||||
|
Order(items=[Item(price=10.99, name="white ball")],
|
||||||
|
total=10.99,
|
||||||
|
created_on=datetime.datetime.now())
|
||||||
|
]
|
||||||
|
|
||||||
|
member1.save()
|
||||||
|
member2.save()
|
||||||
|
actual = Member.find(Member.orders.items.name % "ball").all()
|
||||||
|
assert actual == [member1, member2]
|
||||||
|
|
||||||
|
|
||||||
def test_tag_queries_boolean_logic(members):
|
def test_tag_queries_boolean_logic(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
@ -456,4 +483,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 | 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"
|
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 | $.address.note.description AS address_note_description_fts TEXT 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 | $.orders[*].items[*].name AS orders_items_name_fts TEXT SORTABLE"
|
Loading…
Reference in a new issue