Skip to content

Commit e4e3583

Browse files
author
Andrew Brookins
committed
Document some possible error messages
1 parent 60fbd09 commit e4e3583

File tree

5 files changed

+262
-25
lines changed

5 files changed

+262
-25
lines changed

aredis_om/model/encoders.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,6 @@ def jsonable_encoder(
6969
if exclude is not None and not isinstance(exclude, (set, dict)):
7070
exclude = set(exclude)
7171

72-
if custom_encoder:
73-
if type(obj) in custom_encoder:
74-
return custom_encoder[type(obj)](obj)
75-
else:
76-
for encoder_type, encoder in custom_encoder.items():
77-
if isinstance(obj, encoder_type):
78-
return encoder(obj)
79-
8072
if isinstance(obj, BaseModel):
8173
encoder = getattr(obj.__config__, "json_encoders", {})
8274
if custom_encoder:
@@ -154,9 +146,13 @@ def jsonable_encoder(
154146
)
155147
return encoded_list
156148

157-
# This function originally called custom encoders here,
158-
# which meant we couldn't override the encoder for many
159-
# types hard-coded into this function (lists, etc.).
149+
if custom_encoder:
150+
if type(obj) in custom_encoder:
151+
return custom_encoder[type(obj)](obj)
152+
else:
153+
for encoder_type, encoder in custom_encoder.items():
154+
if isinstance(obj, encoder_type):
155+
return encoder(obj)
160156

161157
if type(obj) in ENCODERS_BY_TYPE:
162158
return ENCODERS_BY_TYPE[type(obj)](obj)

aredis_om/model/model.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
# multi-value field lookup, like a IN or NOT_IN.
6464
DEFAULT_REDISEARCH_FIELD_SEPARATOR = ","
6565

66+
ERRORS_URL = "https://github.com/redis/redis-om-python/blob/main/docs/errors.md"
67+
6668

6769
class RedisModelError(Exception):
6870
"""Raised when a problem exists in the definition of a RedisModel."""
@@ -288,14 +290,19 @@ def __lshift__(self, other: Any) -> Expression:
288290
left=self.field, op=Operators.IN, right=other, parents=self.parents
289291
)
290292

293+
def __rshift__(self, other: Any) -> Expression:
294+
return Expression(
295+
left=self.field, op=Operators.NOT_IN, right=other, parents=self.parents
296+
)
297+
291298
def __getattr__(self, item):
292299
if is_supported_container_type(self.field.outer_type_):
293300
embedded_cls = get_args(self.field.outer_type_)
294301
if not embedded_cls:
295302
raise QuerySyntaxError(
296303
"In order to query on a list field, you must define "
297304
"the contents of the list with a type annotation, like: "
298-
"orders: List[Order]. Docs: TODO"
305+
f"orders: List[Order]. Docs: {ERRORS_URL}#E1"
299306
)
300307
embedded_cls = embedded_cls[0]
301308
attr = getattr(embedded_cls, item)
@@ -419,7 +426,7 @@ def validate_sort_fields(self, sort_fields: List[str]):
419426
if not getattr(field_proxy.field.field_info, "sortable", False):
420427
raise QueryNotSupportedError(
421428
f"You tried sort by {field_name}, but {self.model} does "
422-
"not define that field as sortable. See docs: XXX"
429+
f"not define that field as sortable. Docs: {ERRORS_URL}#E2"
423430
)
424431
return sort_fields
425432

@@ -433,7 +440,7 @@ def resolve_field_type(field: ModelField, op: Operators) -> RediSearchFieldTypes
433440
raise QuerySyntaxError(
434441
f"You tried to do a full-text search on the field '{field.name}', "
435442
f"but the field is not indexed for full-text search. Use the "
436-
f"full_text_search=True option. Docs: TODO"
443+
f"full_text_search=True option. Docs: {ERRORS_URL}#E3"
437444
)
438445
return RediSearchFieldTypes.TEXT
439446

@@ -460,7 +467,7 @@ def resolve_field_type(field: ModelField, op: Operators) -> RediSearchFieldTypes
460467
elif container_type is not None:
461468
raise QuerySyntaxError(
462469
"Only lists and tuples are supported for multi-value fields. "
463-
"See docs: TODO"
470+
f"Docs: {ERRORS_URL}#E4"
464471
)
465472
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
466473
# Index numeric Python types as NUMERIC fields, so we can support
@@ -519,8 +526,8 @@ def resolve_value(
519526
else:
520527
raise QueryNotSupportedError(
521528
"Only equals (=), not-equals (!=), and like() "
522-
"comparisons are supported for TEXT fields. See "
523-
"docs: TODO."
529+
"comparisons are supported for TEXT fields. "
530+
f"Docs: {ERRORS_URL}#E5"
524531
)
525532
elif field_type is RediSearchFieldTypes.NUMERIC:
526533
if op is Operators.EQ:
@@ -571,7 +578,6 @@ def resolve_value(
571578
value = escaper.escape(value)
572579
result += f"-(@{field_name}:{{{value}}})"
573580
elif op is Operators.IN:
574-
# TODO: Implement IN, test this...
575581
expanded_value = cls.expand_tag_value(value)
576582
result += f"(@{field_name}:{{{expanded_value}}})"
577583
elif op is Operators.NOT_IN:
@@ -651,13 +657,12 @@ def resolve_redisearch_query(cls, expression: ExpressionOrNegated) -> str:
651657
if not field_info or not getattr(field_info, "index", None):
652658
raise QueryNotSupportedError(
653659
f"You tried to query by a field ({field_name}) "
654-
f"that isn't indexed. See docs: TODO"
660+
f"that isn't indexed. Docs: {ERRORS_URL}#E6"
655661
)
656662
else:
657663
raise QueryNotSupportedError(
658664
"A query expression should start with either a field "
659-
"or an expression enclosed in parenthesis. See docs: "
660-
"TODO"
665+
f"or an expression enclosed in parentheses. Docs: {ERRORS_URL}#E7"
661666
)
662667

663668
right = expression.right
@@ -670,7 +675,7 @@ def resolve_redisearch_query(cls, expression: ExpressionOrNegated) -> str:
670675
else:
671676
raise QueryNotSupportedError(
672677
"You can only combine two query expressions with"
673-
"AND (&) or OR (|). See docs: TODO"
678+
f"AND (&) or OR (|). Docs: {ERRORS_URL}#E8"
674679
)
675680

676681
if isinstance(right, NegatedExpression):

docs/errors.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Errors
2+
3+
This page lists errors that Redis OM might generate while you're using it, with more context about the error.
4+
5+
## E1
6+
7+
> In order to query on a list field, you must define the contents of the list with a type annotation, like: orders: List[Order].
8+
9+
You will see this error if you try to use an "IN" query, e.g., `await TarotWitch.find(TarotWitch.tarot_cards << "death").all()`, on a field that is not a list.
10+
11+
In this example, `TarotWitch.tarot_cards` is a list, so the query works:
12+
13+
```python
14+
from typing import List
15+
16+
from redis_om import JsonModel, Field
17+
18+
class TarotWitch(JsonModel):
19+
tarot_cards: List[str] = Field(index=True)
20+
```
21+
22+
But if `tarot_cards` was _not_ a list, trying to query with `<<` would have resulted in this error.
23+
24+
## E2
25+
26+
> You tried sort by {field_name}, but {self.model} does not define that field as sortable.
27+
28+
You tried to sort query results by a field that is not sortable. Here is how you mark a field as sortable:
29+
30+
```python
31+
from typing import List
32+
33+
from redis_om import JsonModel, Field
34+
35+
class Member(JsonModel):
36+
age: int = Field(index=True, sortable=True)
37+
```
38+
39+
**NOTE:** Only an indexed field can be sortable.
40+
41+
## E3
42+
43+
>You tried to do a full-text search on the field '{field.name}', but the field is not indexed for full-text search. Use the full_text_search=True option.
44+
45+
You can make a full-text search with the module (`%`) operator. Such a query looks like this:
46+
47+
```python
48+
from redis_om import JsonModel, Field
49+
50+
class Member(JsonModel):
51+
bio: str = Field(index=True, full_text_search=True, default="")
52+
53+
Member.find(Member.bio % "beaches").all()
54+
```
55+
56+
If you see this error, it means that the field you are querying (`bio` in the example) is not indexed for full-text search. Make sure you're marking the field both `index=True` and `full_text_search=True`, as in the example.
57+
58+
## E4
59+
60+
> Only lists and tuples are supported for multi-value fields.
61+
62+
This means that you marked a field as `index=True`, but the field is not a type that Redis OM can actually index.
63+
64+
Specifically, you probably used a _subscripted_ annotation, like `Dict[str, str]`. The only subscripted types that OM can index are `List` and `Tuple`.
65+
66+
## E5
67+
68+
> Only equals (=), not-equals (!=), and like() comparisons are supported for TEXT fields.
69+
70+
You are querying a field you marked as indexed for full-text search. You can only query such fields with the operators for equality (==), non-equality (!=), and like `(%)`.
71+
72+
```python
73+
from redis_om import JsonModel, Field
74+
75+
class Member(JsonModel):
76+
bio: str = Field(index=True, full_text_search=True, default="")
77+
78+
# Equality
79+
Member.find(Member.bio == "Programmer").all()
80+
81+
# Non-equality
82+
Member.find(Member.bio != "Programmer").all()
83+
84+
# Like (full-text search). This stems "programming"
85+
# to find any matching terms with the same stem,
86+
# "program".
87+
Member.find(Member.bio % "programming").all()
88+
```
89+
90+
## E6
91+
92+
> You tried to query by a field ({field_name}) that isn't indexed.
93+
94+
You wrote a query using a model field that you did not make indexed. You can only query indexed fields. Here is example code that would generate this error:
95+
96+
```python
97+
from redis_om import JsonModel, Field
98+
99+
class Member(JsonModel):
100+
first_name: str
101+
bio: str = Field(index=True, full_text_search=True, default="")
102+
103+
# Raises a QueryNotSupportedError because we did not make
104+
# `first_name` indexed!
105+
Member.find(Member.first_name == "Andrew").all()
106+
```
107+
108+
Fix this by making the field indexed:
109+
110+
```python
111+
from redis_om import JsonModel, Field
112+
113+
class Member(JsonModel):
114+
first_name: str = Field(index=True)
115+
bio: str = Field(index=True, full_text_search=True, default="")
116+
117+
# Raises a QueryNotSupportedError because we did not make
118+
# `first_name` indexed!
119+
Member.find(Member.first_name == "Andrew").all()
120+
```
121+
122+
## E7
123+
124+
> A query expression should start with either a field or an expression enclosed in parentheses.
125+
126+
We got confused trying to parse your query expression. It's not you, it's us! Some code examples might help...
127+
128+
```python
129+
from redis_om import JsonModel, Field
130+
131+
class Member(JsonModel):
132+
first_name: str = Field(index=True)
133+
last_name: str = Field(index=True)
134+
135+
136+
# Queries with a single operator are usually simple:
137+
Member.find(Member.first_name == "Andrew").all()
138+
139+
# If you want to add multiple conditions, you can AND
140+
# them together by including the conditions one after
141+
# another as arguments.
142+
Member.find(Member.first_name=="Andrew",
143+
Member.last_name=="Brookins").all()
144+
145+
# Alternatively, you can separate the conditions with
146+
# parenthesis and use an explicit AND.
147+
Member.find(
148+
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
149+
).all()
150+
151+
# You can't use `!` to say NOT. Instead, use `~`.
152+
Member.find(
153+
(Member.first_name == "Andrew") &
154+
~(Member.last_name == "Brookins") # <- Notice, this one is NOT now!
155+
).all()
156+
157+
# Parenthesis are key to building more complex queries,
158+
# like this one.
159+
Member.find(
160+
~(Member.first_name == "Andrew")
161+
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
162+
).all()
163+
164+
# If you're confused about how Redis OM interprets a query,
165+
# use the `tree()` method to visualize the expression tree
166+
# for a `FindQuery`.
167+
query = Member.find(
168+
~(Member.first_name == "Andrew")
169+
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
170+
)
171+
print(query.expression.tree)
172+
"""
173+
┌first_name
174+
┌NOT EQ┤
175+
| └Andrew
176+
AND┤
177+
| ┌last_name
178+
| ┌EQ┤
179+
| | └Brookins
180+
└OR┤
181+
| ┌last_name
182+
└EQ┤
183+
└Smith
184+
"""
185+
```
186+
187+
## E8
188+
189+
> You can only combine two query expressions with AND (&) or OR (|).
190+
191+
The only two operators you can use to combine expressions in a query
192+
are `&` and `|`. You may have accidentally used another operator,
193+
or Redis OM might be confused. Make sure you are using parentheses
194+
to organize your query expressions.
195+
196+
If you are trying to use "NOT," you can do that by prefixing a query
197+
with the `~` operator, like this:
198+
199+
```python
200+
from redis_om import JsonModel, Field
201+
202+
class Member(JsonModel):
203+
first_name: str = Field(index=True)
204+
last_name: str = Field(index=True)
205+
206+
207+
# Find people who are not named Andrew.
208+
Member.find(~(Member.first_name == "Andrew")).all()
209+
```
210+
211+
Note that this form requires parenthesis around the expression
212+
that you are "negating." Of course, this example makes more sense
213+
with `!=`:
214+
215+
```python
216+
from redis_om import JsonModel, Field
217+
218+
class Member(JsonModel):
219+
first_name: str = Field(index=True)
220+
last_name: str = Field(index=True)
221+
222+
223+
# Find people who are not named Andrew.
224+
Member.find(Member.first_name != "Andrew").all()
225+
```
226+
227+
Still, `~` is useful to negate groups of expressions
228+
surrounded by parentheses.

0 commit comments

Comments
 (0)