Skip to content

Commit 71246d9

Browse files
committed
add support for text database functions
1 parent d9edb00 commit 71246d9

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
lines changed

.github/workflows/test-python.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ jobs:
7777
db_functions.comparison
7878
db_functions.datetime
7979
db_functions.math
80+
db_functions.text
8081
empty
8182
defer
8283
defer_regress

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,16 @@ DATABASES = {
6363
- `DateTimeField` doesn't support microsecond precision.
6464

6565
- The following database functions aren't supported:
66+
- `Chr`
6667
- `ExtractQuarter`
68+
- `MD5`
6769
- `Now`
70+
- `Ord`
71+
- `Pad`
72+
- `Repeat`
73+
- `Reverse`
74+
- `Right`
75+
- `SHA1`, `SHA224`, `SHA256`, `SHA384`, `SHA512`
6876
- `Sign`
6977
- `TruncDate`
7078
- `TruncTime`

django_mongodb/features.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3030
# Slicing with QuerySet.count() doesn't work.
3131
"lookup.tests.LookupTests.test_count",
3232
# Lookup in order_by() not supported:
33-
# unsupported operand type(s) for %: 'function' and 'str'
33+
# argument of type '<database function>' is not iterable
3434
"db_functions.comparison.test_coalesce.CoalesceTests.test_ordering",
35+
"db_functions.text.test_length.LengthTests.test_ordering",
36+
"db_functions.text.test_strindex.StrIndexTests.test_order_by",
3537
"lookup.tests.LookupQueryingTests.test_lookup_in_order_by",
3638
# annotate() after values() doesn't raise NotSupportedError.
3739
"lookup.tests.LookupTests.test_exact_query_rhs_with_selected_columns",
@@ -80,6 +82,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
8082
"annotations.tests.NonAggregateAnnotationTestCase.test_update_with_annotation",
8183
"db_functions.comparison.test_least.LeastTests.test_update",
8284
"db_functions.comparison.test_greatest.GreatestTests.test_update",
85+
"db_functions.text.test_left.LeftTests.test_basic",
86+
"db_functions.text.test_lower.LowerTests.test_basic",
87+
"db_functions.text.test_replace.ReplaceTests.test_update",
88+
"db_functions.text.test_substr.SubstrTests.test_basic",
89+
"db_functions.text.test_upper.UpperTests.test_basic",
8390
"model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values",
8491
"timezones.tests.NewDatabaseTests.test_update_with_timedelta",
8592
"update.tests.AdvancedTests.test_update_annotated_queryset",
@@ -182,8 +189,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
182189
"annotations.tests.NonAggregateAnnotationTestCase.test_mixed_type_annotation_numbers",
183190
"annotations.tests.NonAggregateAnnotationTestCase.test_q_expression_annotation_with_aggregation",
184191
"lookup.tests.LookupQueryingTests.test_filter_wrapped_lookup_lhs",
185-
# Length not implemented.
186-
"annotations.tests.NonAggregateAnnotationTestCase.test_chaining_transforms",
187192
# CombinedExpression not implemented.
188193
"annotations.tests.NonAggregateAnnotationTestCase.test_combined_annotation_commutative",
189194
"annotations.tests.NonAggregateAnnotationTestCase.test_decimal_annotation",
@@ -317,10 +322,25 @@ class DatabaseFeatures(BaseDatabaseFeatures):
317322
"db_functions.math.test_sin.SinTests.test_transform",
318323
"db_functions.math.test_sqrt.SqrtTests.test_transform",
319324
"db_functions.math.test_tan.TanTests.test_transform",
325+
"db_functions.text.test_strindex.StrIndexTests.test_filtering",
326+
"db_functions.text.test_length.LengthTests.test_basic",
327+
"db_functions.text.test_length.LengthTests.test_transform",
320328
},
321329
"MongoDB does not support this database function.": {
322330
"db_functions.datetime.test_now.NowTests",
323331
"db_functions.math.test_sign.SignTests",
332+
"db_functions.text.test_chr.ChrTests",
333+
"db_functions.text.test_md5.MD5Tests",
334+
"db_functions.text.test_ord.OrdTests",
335+
"db_functions.text.test_pad.PadTests",
336+
"db_functions.text.test_repeat.RepeatTests",
337+
"db_functions.text.test_reverse.ReverseTests",
338+
"db_functions.text.test_right.RightTests",
339+
"db_functions.text.test_sha1.SHA1Tests",
340+
"db_functions.text.test_sha224.SHA224Tests",
341+
"db_functions.text.test_sha256.SHA256Tests",
342+
"db_functions.text.test_sha384.SHA384Tests",
343+
"db_functions.text.test_sha512.SHA512Tests",
324344
},
325345
"ExtractQuarter database function not supported.": {
326346
"db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_quarter_func",

django_mongodb/functions.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,20 @@
1616
TruncBase,
1717
)
1818
from django.db.models.functions.math import Ceil, Cot, Degrees, Log, Power, Radians, Random, Round
19-
from django.db.models.functions.text import Upper
19+
from django.db.models.functions.text import (
20+
Concat,
21+
ConcatPair,
22+
Left,
23+
Length,
24+
Lower,
25+
LTrim,
26+
Replace,
27+
RTrim,
28+
StrIndex,
29+
Substr,
30+
Trim,
31+
Upper,
32+
)
2033

2134
from .query_utils import process_lhs
2235

@@ -26,6 +39,7 @@
2639
Degrees: "radiansToDegrees",
2740
Greatest: "max",
2841
Least: "min",
42+
Lower: "toLower",
2943
Power: "pow",
3044
Radians: "degreesToRadians",
3145
Random: "rand",
@@ -56,6 +70,16 @@ def cast(self, compiler, connection):
5670
return lhs_mql
5771

5872

73+
def concat(self, compiler, connection):
74+
return self.get_source_expressions()[0].as_mql(compiler, connection)
75+
76+
77+
def concat_pair(self, compiler, connection):
78+
# null on either side results in null for expression, wrap with coalesce.
79+
coalesced = self.coalesce()
80+
return super(ConcatPair, coalesced).as_mql(compiler, connection)
81+
82+
5983
def cot(self, compiler, connection):
6084
lhs_mql = process_lhs(self, compiler, connection)
6185
return {"$divide": [1, {"$tan": lhs_mql}]}
@@ -77,6 +101,16 @@ def func(self, compiler, connection):
77101
return {f"${operator}": lhs_mql}
78102

79103

104+
def left(self, compiler, connection):
105+
return self.get_substr().as_mql(compiler, connection)
106+
107+
108+
def length(self, compiler, connection):
109+
# Check for null first since $strLenCP only accepts strings.
110+
lhs_mql = process_lhs(self, compiler, connection)
111+
return {"$cond": {"if": {"$eq": [lhs_mql, None]}, "then": None, "else": {"$strLenCP": lhs_mql}}}
112+
113+
80114
def log(self, compiler, connection):
81115
# This function is usually log(base, num) but on MongoDB it's log(num, base).
82116
clone = self.copy()
@@ -90,12 +124,42 @@ def null_if(self, compiler, connection):
90124
return {"$cond": {"if": {"$eq": [expr1, expr2]}, "then": None, "else": expr1}}
91125

92126

127+
def replace(self, compiler, connection):
128+
expression, text, replacement = process_lhs(self, compiler, connection)
129+
return {"$replaceAll": {"input": expression, "find": text, "replacement": replacement}}
130+
131+
93132
def round_(self, compiler, connection):
94133
# Round needs its own function because it's a special case that inherits
95134
# from Transform but has two arguments.
96135
return {"$round": [expr.as_mql(compiler, connection) for expr in self.get_source_expressions()]}
97136

98137

138+
def str_index(self, compiler, connection):
139+
lhs = process_lhs(self, compiler, connection)
140+
# StrIndex should be 0-indexed (not found) but it's -1-indexed on MongoDB.
141+
return {"$add": [{"$indexOfCP": lhs}, 1]}
142+
143+
144+
def substr(self, compiler, connection):
145+
lhs = process_lhs(self, compiler, connection)
146+
# The starting index is zero-indexed on MongoDB rather than one-indexed.
147+
lhs[1] = {"$add": [lhs[1], -1]}
148+
# If no limit is specified, use the length of the string since $substrCP
149+
# requires one.
150+
if len(lhs) == 2:
151+
lhs.append({"$strLenCP": lhs[0]})
152+
return {"$substrCP": lhs}
153+
154+
155+
def trim(operator):
156+
def wrapped(self, compiler, connection):
157+
lhs = process_lhs(self, compiler, connection)
158+
return {f"${operator}": {"input": lhs}}
159+
160+
return wrapped
161+
162+
99163
def trunc(self, compiler, connection):
100164
lhs_mql = process_lhs(self, compiler, connection)
101165
lhs_mql = {"date": lhs_mql, "unit": self.kind, "startOfWeek": "mon"}
@@ -106,10 +170,20 @@ def trunc(self, compiler, connection):
106170

107171
def register_functions():
108172
Cast.as_mql = cast
173+
Concat.as_mql = concat
174+
ConcatPair.as_mql = concat_pair
109175
Cot.as_mql = cot
110176
Extract.as_mql = extract
111177
Func.as_mql = func
178+
Left.as_mql = left
179+
Length.as_mql = length
112180
Log.as_mql = log
181+
LTrim.as_mql = trim("ltrim")
113182
NullIf.as_mql = null_if
183+
Replace.as_mql = replace
114184
Round.as_mql = round_
185+
RTrim.as_mql = trim("rtrim")
186+
StrIndex.as_mql = str_index
187+
Substr.as_mql = substr
188+
Trim.as_mql = trim("trim")
115189
TruncBase.as_mql = trunc

0 commit comments

Comments
 (0)