django-ormql 0.0.0__tar.gz → 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_ormql-0.0.0/django_ormql.egg-info → django_ormql-0.0.1}/PKG-INFO +2 -2
- django_ormql-0.0.1/django_ormql/__init__.py +1 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/columns.py +23 -14
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/db_func.py +37 -8
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/query.py +48 -37
- {django_ormql-0.0.0 → django_ormql-0.0.1/django_ormql.egg-info}/PKG-INFO +2 -2
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql.egg-info/requires.txt +1 -1
- {django_ormql-0.0.0 → django_ormql-0.0.1}/pyproject.toml +1 -1
- django_ormql-0.0.0/django_ormql/__init__.py +0 -1
- {django_ormql-0.0.0 → django_ormql-0.0.1}/LICENSE +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/MANIFEST.in +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/README.md +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/engine.py +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/exceptions.py +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/model_utils.py +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql/tables.py +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql.egg-info/SOURCES.txt +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql.egg-info/dependency_links.txt +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/django_ormql.egg-info/top_level.txt +0 -0
- {django_ormql-0.0.0 → django_ormql-0.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ormql
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.1
|
|
4
4
|
Summary: ORMQL is a library that allows adding a reporting DSL (that is loosely based on SQL) to your Django project.
|
|
5
5
|
Author-email: Raphael Michel <mail@raphaelmichel.de>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: sqlglot[c]==
|
|
20
|
+
Requires-Dist: sqlglot[c]==30.0.3
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: pytest==9.*; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.0.1"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import copy
|
|
2
1
|
import inspect
|
|
3
2
|
|
|
4
3
|
from django.db.models import F, Expression, OuterRef, Subquery
|
|
5
|
-
from django.db.models.expressions import ResolvedOuterRef
|
|
4
|
+
from django.db.models.expressions import ResolvedOuterRef, When
|
|
6
5
|
from django.utils import tree
|
|
7
6
|
from django.utils.module_loading import import_string
|
|
8
7
|
|
|
@@ -22,6 +21,10 @@ class BaseColumn:
|
|
|
22
21
|
self.source = field_name
|
|
23
22
|
|
|
24
23
|
def resolve_column_path(self, remaining_path):
|
|
24
|
+
if remaining_path:
|
|
25
|
+
raise QueryNotSupported(
|
|
26
|
+
f"Column '{self.field_name}' is not a related field"
|
|
27
|
+
)
|
|
25
28
|
return F(self.source)
|
|
26
29
|
|
|
27
30
|
@property
|
|
@@ -89,22 +92,31 @@ class ForeignKeyColumn(BaseColumn):
|
|
|
89
92
|
|
|
90
93
|
def _prefix_expression(self, expr, prefix):
|
|
91
94
|
if isinstance(expr, tree.Node):
|
|
92
|
-
new_expr = expr.create(connector=expr.connector, negated=expr.negated)
|
|
93
95
|
children = []
|
|
94
96
|
for e in expr.children:
|
|
95
97
|
e = self._prefix_expression(e, prefix)
|
|
96
98
|
if isinstance(e, F):
|
|
97
99
|
e = F(f"{prefix}__{expr}")
|
|
98
100
|
children.append(e)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
expr = expr.create(
|
|
102
|
+
children=children, connector=expr.connector, negated=expr.negated
|
|
103
|
+
)
|
|
104
|
+
elif isinstance(expr, When):
|
|
105
|
+
# Only django-native expression with Q-object-style kwargs
|
|
106
|
+
args, kwargs = expr._constructor_args
|
|
107
|
+
args = [self._prefix_expression(a, prefix) for a in args]
|
|
108
|
+
kwargs = {
|
|
109
|
+
f"{prefix}__{k}" if k != "then" else k: self._prefix_expression(
|
|
110
|
+
e, prefix
|
|
111
|
+
)
|
|
112
|
+
for k, e in kwargs.items()
|
|
113
|
+
}
|
|
114
|
+
expr = When(*args, **kwargs)
|
|
101
115
|
elif isinstance(expr, Expression):
|
|
102
|
-
|
|
103
|
-
for
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expr = copy.deepcopy(expr)
|
|
107
|
-
expr.set_source_expressions(source_expressions)
|
|
116
|
+
args, kwargs = expr._constructor_args
|
|
117
|
+
args = [self._prefix_expression(a, prefix) for a in args]
|
|
118
|
+
kwargs = {k: self._prefix_expression(e, prefix) for k, e in kwargs.items()}
|
|
119
|
+
expr = type(expr)(*args, **kwargs)
|
|
108
120
|
elif isinstance(expr, tuple) and len(expr) == 2:
|
|
109
121
|
# kwarg of Q()
|
|
110
122
|
return f"{prefix}__{expr[0]}", expr[1]
|
|
@@ -117,9 +129,6 @@ class ForeignKeyColumn(BaseColumn):
|
|
|
117
129
|
elif isinstance(expr, Subquery):
|
|
118
130
|
expr = expr.copy()
|
|
119
131
|
expr.query.where = self._prefix_expression(expr.query.where, self.source)
|
|
120
|
-
return expr
|
|
121
|
-
else:
|
|
122
|
-
raise TypeError(f"Unexpected type {expr!r}")
|
|
123
132
|
return expr
|
|
124
133
|
|
|
125
134
|
def bind(self, field_name, parent):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from django.core.exceptions import FieldError
|
|
2
2
|
from django.db.models import Func, fields, Value, ExpressionWrapper, Case, Subquery
|
|
3
|
+
from django.db.models.functions import ConcatPair, Concat
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Equal(Func):
|
|
@@ -52,14 +53,24 @@ class Like(Func):
|
|
|
52
53
|
function = ""
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
class
|
|
56
|
+
class TypeResolveMixin:
|
|
56
57
|
def _resolve_output_field(self):
|
|
57
|
-
# Auto-resolve of INT*DECIMAL to DECIMAL etc
|
|
58
|
+
# Auto-resolve of INT*DECIMAL to DECIMAL etc, TEXT and VARCHAR, etc.
|
|
58
59
|
source_types = set(
|
|
59
60
|
type(source) for source in self.get_source_fields() if source is not None
|
|
60
61
|
)
|
|
62
|
+
text_types = {
|
|
63
|
+
fields.CharField,
|
|
64
|
+
fields.TextField,
|
|
65
|
+
fields.URLField,
|
|
66
|
+
fields.EmailField,
|
|
67
|
+
fields.SlugField,
|
|
68
|
+
}
|
|
69
|
+
print("sources", source_types)
|
|
61
70
|
if len(source_types) == 1:
|
|
62
71
|
return list(source_types)[0]()
|
|
72
|
+
elif all(s in text_types for s in source_types):
|
|
73
|
+
return fields.TextField()
|
|
63
74
|
elif source_types == {fields.DecimalField, fields.IntegerField}:
|
|
64
75
|
return fields.DecimalField(
|
|
65
76
|
max_digits=max(
|
|
@@ -112,25 +123,25 @@ class NumericResolveMixin:
|
|
|
112
123
|
)
|
|
113
124
|
|
|
114
125
|
|
|
115
|
-
class Add(
|
|
126
|
+
class Add(TypeResolveMixin, Func):
|
|
116
127
|
arg_joiner = " + "
|
|
117
128
|
arity = 2
|
|
118
129
|
function = ""
|
|
119
130
|
|
|
120
131
|
|
|
121
|
-
class Sub(
|
|
132
|
+
class Sub(TypeResolveMixin, Func):
|
|
122
133
|
arg_joiner = " - "
|
|
123
134
|
arity = 2
|
|
124
135
|
function = ""
|
|
125
136
|
|
|
126
137
|
|
|
127
|
-
class Mul(
|
|
138
|
+
class Mul(TypeResolveMixin, Func):
|
|
128
139
|
arg_joiner = " * "
|
|
129
140
|
arity = 2
|
|
130
141
|
function = ""
|
|
131
142
|
|
|
132
143
|
|
|
133
|
-
class Div(
|
|
144
|
+
class Div(TypeResolveMixin, Func):
|
|
134
145
|
arg_joiner = " / "
|
|
135
146
|
arity = 2
|
|
136
147
|
function = ""
|
|
@@ -147,15 +158,33 @@ class Div(NumericResolveMixin, Func):
|
|
|
147
158
|
)
|
|
148
159
|
|
|
149
160
|
|
|
150
|
-
class Mod(
|
|
161
|
+
class Mod(TypeResolveMixin, Func):
|
|
151
162
|
arg_joiner = " %% "
|
|
152
163
|
arity = 2
|
|
153
164
|
function = ""
|
|
154
165
|
|
|
155
166
|
|
|
156
|
-
class NumericAwareCase(
|
|
167
|
+
class NumericAwareCase(TypeResolveMixin, Case):
|
|
157
168
|
pass
|
|
158
169
|
|
|
159
170
|
|
|
160
171
|
class AutoTypedSubquery(Subquery):
|
|
161
172
|
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PatchedConcatPair(TypeResolveMixin, ConcatPair):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class PatchedConcat(Concat):
|
|
180
|
+
def _paired(self, expressions):
|
|
181
|
+
# wrap pairs of expressions in successive concat functions
|
|
182
|
+
# exp = [a, b, c, d]
|
|
183
|
+
# -> ConcatPair(a, ConcatPair(b, ConcatPair(c, d))))
|
|
184
|
+
if len(expressions) == 2:
|
|
185
|
+
return PatchedConcatPair(*expressions)
|
|
186
|
+
return PatchedConcatPair(expressions[0], self._paired(expressions[1:]))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _patch_func(cls):
|
|
190
|
+
return type(f"Patched{cls.__name__}", (TypeResolveMixin, cls), {})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from django.conf import settings
|
|
4
|
+
from django.core.exceptions import FieldError
|
|
4
5
|
from django.db import models
|
|
5
6
|
from django.db.models import (
|
|
6
7
|
F,
|
|
@@ -18,9 +19,10 @@ from django.db.models.fields.json import KeyTransform
|
|
|
18
19
|
from django.db.models.functions import Cast
|
|
19
20
|
from sqlglot import parse_one, Dialect, Tokenizer, TokenType, Generator, ParseError
|
|
20
21
|
from sqlglot import expressions
|
|
22
|
+
from sqlglot.errors import ANSI_UNDERLINE, ANSI_RESET
|
|
21
23
|
|
|
22
24
|
from . import db_func
|
|
23
|
-
from .db_func import NumericAwareCase
|
|
25
|
+
from .db_func import NumericAwareCase, _patch_func
|
|
24
26
|
from .exceptions import QueryNotSupported, QueryError
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
@@ -134,20 +136,20 @@ aggregate_nodes = {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function_nodes = {
|
|
137
|
-
expressions.Coalesce: functions.Coalesce,
|
|
138
|
-
expressions.Concat:
|
|
139
|
-
expressions.Greatest: functions.Greatest,
|
|
140
|
-
expressions.Least: functions.Least,
|
|
141
|
-
expressions.Abs: functions.Abs,
|
|
142
|
-
expressions.Ceil: functions.Ceil,
|
|
143
|
-
expressions.Floor: functions.Floor,
|
|
144
|
-
expressions.Mod: functions.Mod,
|
|
145
|
-
expressions.Left: functions.Left,
|
|
146
|
-
expressions.Right: functions.Right,
|
|
147
|
-
expressions.Length: functions.Length,
|
|
148
|
-
expressions.Lower: functions.Lower,
|
|
149
|
-
expressions.Upper: functions.Upper,
|
|
150
|
-
expressions.SubstringIndex: functions.StrIndex,
|
|
139
|
+
expressions.Coalesce: _patch_func(functions.Coalesce),
|
|
140
|
+
expressions.Concat: db_func.PatchedConcatPair,
|
|
141
|
+
expressions.Greatest: _patch_func(functions.Greatest),
|
|
142
|
+
expressions.Least: _patch_func(functions.Least),
|
|
143
|
+
expressions.Abs: _patch_func(functions.Abs),
|
|
144
|
+
expressions.Ceil: _patch_func(functions.Ceil),
|
|
145
|
+
expressions.Floor: _patch_func(functions.Floor),
|
|
146
|
+
expressions.Mod: _patch_func(functions.Mod),
|
|
147
|
+
expressions.Left: _patch_func(functions.Left),
|
|
148
|
+
expressions.Right: _patch_func(functions.Right),
|
|
149
|
+
expressions.Length: _patch_func(functions.Length),
|
|
150
|
+
expressions.Lower: _patch_func(functions.Lower),
|
|
151
|
+
expressions.Upper: _patch_func(functions.Upper),
|
|
152
|
+
expressions.SubstringIndex: _patch_func(functions.StrIndex),
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
types = {
|
|
@@ -672,6 +674,9 @@ class Query:
|
|
|
672
674
|
|
|
673
675
|
qs = table.base_qs
|
|
674
676
|
|
|
677
|
+
if parent_table_stack:
|
|
678
|
+
qs = qs.order_by()
|
|
679
|
+
|
|
675
680
|
if root.args.get("where"):
|
|
676
681
|
qs = qs.filter(
|
|
677
682
|
self._where_to_django(
|
|
@@ -812,26 +817,24 @@ class Query:
|
|
|
812
817
|
if order_by:
|
|
813
818
|
qs = qs.order_by(*order_by)
|
|
814
819
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if not isinstance(
|
|
820
|
+
offset = root.args.get("offset")
|
|
821
|
+
limit = root.args.get("limit")
|
|
822
|
+
if offset:
|
|
823
|
+
if not isinstance(offset.expression, expressions.Literal):
|
|
819
824
|
raise QueryNotSupported("OFFSET may only contain literal numbers")
|
|
820
|
-
offset = int(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if not isinstance(root.args["limit"].expression, expressions.Literal):
|
|
825
|
+
offset = int(offset.expression.this)
|
|
826
|
+
|
|
827
|
+
if limit:
|
|
828
|
+
if not isinstance(limit.expression, expressions.Literal):
|
|
825
829
|
raise QueryNotSupported("LIMIT may only contain literal numbers")
|
|
826
830
|
limit = int(root.args["limit"].expression.this)
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
qs = qs[: self.default_limit]
|
|
831
|
+
else:
|
|
832
|
+
limit = self.default_limit
|
|
833
|
+
|
|
834
|
+
if offset is not None and limit is not None:
|
|
835
|
+
qs = qs[offset : offset + limit]
|
|
836
|
+
elif offset is not None or limit is not None:
|
|
837
|
+
qs = qs[offset:limit]
|
|
835
838
|
|
|
836
839
|
return qs, values_names
|
|
837
840
|
|
|
@@ -839,7 +842,8 @@ class Query:
|
|
|
839
842
|
try:
|
|
840
843
|
ast = parse_one(self.sql, dialect=OrmqlDialect)
|
|
841
844
|
except ParseError as e:
|
|
842
|
-
|
|
845
|
+
msg = str(e).replace(ANSI_UNDERLINE, "").replace(ANSI_RESET, "")
|
|
846
|
+
raise QueryNotSupported(msg) from e
|
|
843
847
|
|
|
844
848
|
if settings.DEBUG:
|
|
845
849
|
print(f"Parsed statement: {ast!r}")
|
|
@@ -851,13 +855,20 @@ class Query:
|
|
|
851
855
|
qs, values_names = self._select_to_qs(ast, [])
|
|
852
856
|
except QueryError:
|
|
853
857
|
raise
|
|
858
|
+
except FieldError as e:
|
|
859
|
+
raise QueryError("Invalid combination of types") from e
|
|
854
860
|
except Exception as e:
|
|
855
861
|
raise QueryError("Query parsing failed") from e
|
|
856
862
|
|
|
857
863
|
if isinstance(qs, dict):
|
|
858
864
|
yield {values_names[k]: v for k, v in qs.items()}
|
|
859
865
|
else:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
866
|
+
try:
|
|
867
|
+
if settings.DEBUG:
|
|
868
|
+
print(f"Generated statement: {qs.query!s}")
|
|
869
|
+
for row in qs:
|
|
870
|
+
yield {
|
|
871
|
+
values_names[k]: v for k, v in row.items() if k in values_names
|
|
872
|
+
}
|
|
873
|
+
except (FieldError, ValueError) as e:
|
|
874
|
+
raise QueryError("Invalid combination of types") from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ormql
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.1
|
|
4
4
|
Summary: ORMQL is a library that allows adding a reporting DSL (that is loosely based on SQL) to your Django project.
|
|
5
5
|
Author-email: Raphael Michel <mail@raphaelmichel.de>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: sqlglot[c]==
|
|
20
|
+
Requires-Dist: sqlglot[c]==30.0.3
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: pytest==9.*; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = "0.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|