-
Notifications
You must be signed in to change notification settings - Fork 28
INTPYTHON-751 Make query generation omit $expr unless required #396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4e4ed59
f87fac0
a035274
bc7e8b6
78cde50
8572489
8a8b441
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,8 @@ | |
import logging | ||
import os | ||
|
||
from django.core.exceptions import ImproperlyConfigured | ||
from bson import Decimal128 | ||
from django.core.exceptions import EmptyResultSet, FullResultSet, ImproperlyConfigured | ||
from django.db import DEFAULT_DB_ALIAS | ||
from django.db.backends.base.base import BaseDatabaseWrapper | ||
from django.db.backends.utils import debug_transaction | ||
|
@@ -20,7 +21,7 @@ | |
from .features import DatabaseFeatures | ||
from .introspection import DatabaseIntrospection | ||
from .operations import DatabaseOperations | ||
from .query_utils import regex_match | ||
from .query_utils import regex_expr, regex_match | ||
from .schema import DatabaseSchemaEditor | ||
from .utils import OperationDebugWrapper | ||
from .validation import DatabaseValidation | ||
|
@@ -97,40 +98,91 @@ class DatabaseWrapper(BaseDatabaseWrapper): | |
} | ||
_connection_pools = {} | ||
|
||
def _isnull_operator(a, b): | ||
is_null = { | ||
def _isnull_operator_expr(field, is_null): | ||
is_null_expr = { | ||
"$or": [ | ||
# The path does not exist (i.e. is "missing") | ||
{"$eq": [{"$type": a}, "missing"]}, | ||
{"$eq": [{"$type": field}, "missing"]}, | ||
# or the value is None. | ||
{"$eq": [a, None]}, | ||
{"$eq": [field, None]}, | ||
] | ||
} | ||
return is_null if b else {"$not": is_null} | ||
return is_null_expr if is_null else {"$not": is_null_expr} | ||
|
||
mongo_operators = { | ||
mongo_expr_operators = { | ||
"exact": lambda a, b: {"$eq": [a, b]}, | ||
"gt": lambda a, b: {"$gt": [a, b]}, | ||
"gte": lambda a, b: {"$gte": [a, b]}, | ||
# MongoDB considers null less than zero. Exclude null values to match | ||
# SQL behavior. | ||
"lt": lambda a, b: {"$and": [{"$lt": [a, b]}, DatabaseWrapper._isnull_operator(a, False)]}, | ||
"lt": lambda a, b: { | ||
"$and": [{"$lt": [a, b]}, DatabaseWrapper._isnull_operator_expr(a, False)] | ||
}, | ||
"lte": lambda a, b: { | ||
"$and": [{"$lte": [a, b]}, DatabaseWrapper._isnull_operator(a, False)] | ||
"$and": [{"$lte": [a, b]}, DatabaseWrapper._isnull_operator_expr(a, False)] | ||
}, | ||
"in": lambda a, b: {"$in": [a, b]}, | ||
"isnull": _isnull_operator, | ||
"in": lambda a, b: {"$in": (a, b)}, | ||
timgraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"isnull": _isnull_operator_expr, | ||
"range": lambda a, b: { | ||
"$and": [ | ||
{"$or": [DatabaseWrapper._isnull_operator(b[0], True), {"$gte": [a, b[0]]}]}, | ||
{"$or": [DatabaseWrapper._isnull_operator(b[1], True), {"$lte": [a, b[1]]}]}, | ||
{"$or": [DatabaseWrapper._isnull_operator_expr(b[0], True), {"$gte": [a, b[0]]}]}, | ||
{"$or": [DatabaseWrapper._isnull_operator_expr(b[1], True), {"$lte": [a, b[1]]}]}, | ||
] | ||
}, | ||
"iexact": lambda a, b: regex_match(a, ("^", b, {"$literal": "$"}), insensitive=True), | ||
"startswith": lambda a, b: regex_match(a, ("^", b)), | ||
"istartswith": lambda a, b: regex_match(a, ("^", b), insensitive=True), | ||
"endswith": lambda a, b: regex_match(a, (b, {"$literal": "$"})), | ||
"iendswith": lambda a, b: regex_match(a, (b, {"$literal": "$"}), insensitive=True), | ||
"iexact": lambda a, b: regex_expr(a, ("^", b, {"$literal": "$"}), insensitive=True), | ||
"startswith": lambda a, b: regex_expr(a, ("^", b)), | ||
"istartswith": lambda a, b: regex_expr(a, ("^", b), insensitive=True), | ||
"endswith": lambda a, b: regex_expr(a, (b, {"$literal": "$"})), | ||
"iendswith": lambda a, b: regex_expr(a, (b, {"$literal": "$"}), insensitive=True), | ||
"contains": lambda a, b: regex_expr(a, b), | ||
"icontains": lambda a, b: regex_expr(a, b, insensitive=True), | ||
"regex": lambda a, b: regex_expr(a, b), | ||
"iregex": lambda a, b: regex_expr(a, b, insensitive=True), | ||
} | ||
|
||
def range_match(a, b): | ||
conditions = [] | ||
start, end = b | ||
if start is not None: | ||
conditions.append({a: {"$gte": b[0]}}) | ||
if end is not None: | ||
conditions.append({a: {"$lte": b[1]}}) | ||
if not conditions: | ||
raise FullResultSet | ||
if start is not None and end is not None: | ||
if isinstance(start, Decimal128): | ||
start = start.to_decimal() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. potencial overflow risk here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a failing test without the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a BSON type held at the database and returned locally. I think it should be good. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, there is: |
||
if isinstance(end, Decimal128): | ||
end = end.to_decimal() | ||
if start > end: | ||
raise EmptyResultSet | ||
return {"$and": conditions} | ||
|
||
def _isnull_operator_match(field, is_null): | ||
if is_null: | ||
return {"$or": [{field: {"$exists": False}}, {field: None}]} | ||
return {"$and": [{field: {"$exists": True}}, {field: {"$ne": None}}]} | ||
|
||
mongo_operators = { | ||
"exact": lambda a, b: {a: b}, | ||
"gt": lambda a, b: {a: {"$gt": b}}, | ||
"gte": lambda a, b: {a: {"$gte": b}}, | ||
# MongoDB considers null less than zero. Exclude null values to match | ||
# SQL behavior. | ||
"lt": lambda a, b: { | ||
"$and": [{a: {"$lt": b}}, DatabaseWrapper._isnull_operator_match(a, False)] | ||
}, | ||
"lte": lambda a, b: { | ||
"$and": [{a: {"$lte": b}}, DatabaseWrapper._isnull_operator_match(a, False)] | ||
}, | ||
"in": lambda a, b: {a: {"$in": tuple(b)}}, | ||
"isnull": _isnull_operator_match, | ||
"range": range_match, | ||
"iexact": lambda a, b: regex_match(a, f"^{b}$", insensitive=True), | ||
"startswith": lambda a, b: regex_match(a, f"^{b}"), | ||
"istartswith": lambda a, b: regex_match(a, f"^{b}", insensitive=True), | ||
"endswith": lambda a, b: regex_match(a, f"{b}$"), | ||
"iendswith": lambda a, b: regex_match(a, f"{b}$", insensitive=True), | ||
"contains": lambda a, b: regex_match(a, b), | ||
"icontains": lambda a, b: regex_match(a, b, insensitive=True), | ||
"regex": lambda a, b: regex_match(a, b), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we have
as_mql_expr()
,as_mql_path()
, andas_mql(..., as_path=...)
. If this is the way we keep it, it would be good to explain in the design document which objects (aggregate, func, expression, etc.) get which.I wonder about renaming
as_mql_expr()
oras_mql_path()
toas_mql()
(i.e. treating one of paths as the default). Do you think it would be more or less confusing?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was the idea. I’ll explain it in the docs, and we might also consider renaming some methods. The core concept is:
as_mql
method.as_mql
directly, so those methods don’t follow the common expression flow.as_mql
is a composite function that delegates toas_path
oras_expr
when applied.base_expression.as_mql
method controls when these are called and performs boilerplate checks to prevent nesting anexpr
inside anotherexpr
(a MongoDB 6 restriction).In short: every object has
as_mql
. Some also defineas_path
andas_expr
. Thebase_expression
coordinates how these methods are used, except for cases whereas_mql
is defined directly.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc here: link
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timgraham I actually like the decoupling of
as_mql
andas_mql_(path | expr)
. I view it as, you need to define at least two:as_mql
andas_mql_expr
.Then if you want to have the more optimized function you define
as_mql_path
. It feels less confusing to me that way.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 An expression needs at least one of the methods, as_mql_expr is enough (not optimal but functional). The aggregation module is a good example. Because the as_mql is defined in baseExpression class. In this approach most of the expressions don't need as_mql (if as_mql is defined, expr or path aren't needed).
EDIT: Sorry, I rushed the answer before read all the text, you got the point well. 😬