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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ormql
3
- Version: 0.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]==29.0.1
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
- new_expr.children = children
100
- return new_expr
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
- source_expressions = []
103
- for e in expr.get_source_expressions():
104
- e = self._prefix_expression(e, prefix)
105
- source_expressions.append(e)
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 NumericResolveMixin:
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(NumericResolveMixin, Func):
126
+ class Add(TypeResolveMixin, Func):
116
127
  arg_joiner = " + "
117
128
  arity = 2
118
129
  function = ""
119
130
 
120
131
 
121
- class Sub(NumericResolveMixin, Func):
132
+ class Sub(TypeResolveMixin, Func):
122
133
  arg_joiner = " - "
123
134
  arity = 2
124
135
  function = ""
125
136
 
126
137
 
127
- class Mul(NumericResolveMixin, Func):
138
+ class Mul(TypeResolveMixin, Func):
128
139
  arg_joiner = " * "
129
140
  arity = 2
130
141
  function = ""
131
142
 
132
143
 
133
- class Div(NumericResolveMixin, Func):
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(NumericResolveMixin, Func):
161
+ class Mod(TypeResolveMixin, Func):
151
162
  arg_joiner = " %% "
152
163
  arity = 2
153
164
  function = ""
154
165
 
155
166
 
156
- class NumericAwareCase(NumericResolveMixin, Case):
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: functions.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
- if root.args.get("offset") and root.args.get("limit"):
816
- if not isinstance(root.args["limit"].expression, expressions.Literal):
817
- raise QueryNotSupported("LIMIT may only contain literal numbers")
818
- if not isinstance(root.args["offset"].expression, expressions.Literal):
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(root.args["offset"].expression.this)
821
- limit = int(root.args["limit"].expression.this)
822
- qs = qs[offset : offset + limit]
823
- elif root.args.get("limit"):
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
- qs = qs[:limit]
828
- elif root.args.get("offset"):
829
- if not isinstance(root.args["offset"].expression, expressions.Literal):
830
- raise QueryNotSupported("OFFSET may only contain literal numbers")
831
- offset = int(root.args["offset"].expression.this)
832
- qs = qs[offset:]
833
- elif self.default_limit and not parent_table_stack:
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
- raise QueryNotSupported(str(e)) from e
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
- if settings.DEBUG:
861
- print(f"Generated statement: {qs.query!s}")
862
- for row in qs:
863
- yield {values_names[k]: v for k, v in row.items() if k in values_names}
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.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]==29.0.1
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,4 +1,4 @@
1
- sqlglot[c]==29.0.1
1
+ sqlglot[c]==30.0.3
2
2
 
3
3
  [dev]
4
4
  pytest==9.*
@@ -24,7 +24,7 @@ classifiers = [
24
24
  ]
25
25
  requires-python = ">=3.10"
26
26
  dependencies = [
27
- "sqlglot[c]==29.0.1",
27
+ "sqlglot[c]==30.0.3",
28
28
  ]
29
29
  dynamic = ["version"]
30
30
 
@@ -1 +0,0 @@
1
- version = "0.0.0"
File without changes
File without changes
File without changes
File without changes