pyoq-sql 1.0.2__py3-none-any.whl

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.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,172 @@
1
+ """Typed aggregate expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from decimal import Decimal
6
+ from typing import TypeAlias, TypeVar, cast, overload
7
+
8
+ from pyoq.errors import ExpressionTypeError
9
+ from pyoq.query.expressions import ComputedExpression, Expression
10
+ from pyoq.query.nodes import AggregateName, AggregateNode, ScalarFamily
11
+ from pyoq.query.raw import NodeProvider
12
+ from pyoq.query.windows import Window, WindowSpecification, looking_through
13
+ from pyoq.schema.models import JsonValue
14
+
15
+ Value = TypeVar("Value")
16
+
17
+
18
+ class AggregateExpression(ComputedExpression[Value]):
19
+ __slots__ = ()
20
+
21
+ def over(self, window: WindowSpecification | None = None, /) -> Window[Value]:
22
+ """This aggregate computed per row over its window, not per group.
23
+
24
+ The rows stay as they are and each one is given the aggregate of the
25
+ window around it, so a running total sits beside the row it counts.
26
+ """
27
+ return Window[Value](looking_through(self.node, window), self.family)
28
+
29
+
30
+ SumResult: TypeAlias = (
31
+ AggregateExpression[int | None]
32
+ | AggregateExpression[float | None]
33
+ | AggregateExpression[Decimal | None]
34
+ )
35
+ AverageResult: TypeAlias = (
36
+ AggregateExpression[float | None] | AggregateExpression[Decimal | None]
37
+ )
38
+
39
+
40
+ def count(
41
+ expression: Expression[Value] | None = None,
42
+ /,
43
+ *,
44
+ distinct: bool = False,
45
+ ) -> AggregateExpression[int]:
46
+ arguments = () if expression is None else (expression.node,)
47
+ if distinct and expression is None:
48
+ message = "distinct count requires an expression"
49
+ raise ValueError(message)
50
+ return AggregateExpression(
51
+ AggregateNode(AggregateName.COUNT, arguments, distinct=distinct),
52
+ ScalarFamily.NUMERIC,
53
+ )
54
+
55
+
56
+ @overload
57
+ def sum_(expression: Expression[int], /) -> AggregateExpression[int | None]: ...
58
+
59
+
60
+ @overload
61
+ def sum_(expression: Expression[float], /) -> AggregateExpression[float | None]: ...
62
+
63
+
64
+ @overload
65
+ def sum_(
66
+ expression: Expression[Decimal],
67
+ /,
68
+ ) -> AggregateExpression[Decimal | None]: ...
69
+
70
+
71
+ def sum_(expression: object, /) -> SumResult:
72
+ typed_expression = _require_expression(expression)
73
+ _require_numeric(typed_expression)
74
+ return cast(
75
+ SumResult,
76
+ AggregateExpression(
77
+ AggregateNode(AggregateName.SUM, (typed_expression.node,)),
78
+ ScalarFamily.NUMERIC,
79
+ ),
80
+ )
81
+
82
+
83
+ @overload
84
+ def average(expression: Expression[int], /) -> AggregateExpression[float | None]: ...
85
+
86
+
87
+ @overload
88
+ def average(expression: Expression[float], /) -> AggregateExpression[float | None]: ...
89
+
90
+
91
+ @overload
92
+ def average(
93
+ expression: Expression[Decimal],
94
+ /,
95
+ ) -> AggregateExpression[Decimal | None]: ...
96
+
97
+
98
+ def average(expression: object, /) -> AverageResult:
99
+ typed_expression = _require_expression(expression)
100
+ _require_numeric(typed_expression)
101
+ return cast(
102
+ AverageResult,
103
+ AggregateExpression(
104
+ AggregateNode(AggregateName.AVERAGE, (typed_expression.node,)),
105
+ ScalarFamily.NUMERIC,
106
+ ),
107
+ )
108
+
109
+
110
+ def minimum(expression: Expression[Value], /) -> AggregateExpression[Value | None]:
111
+ return AggregateExpression(
112
+ AggregateNode(AggregateName.MINIMUM, (expression.node,)),
113
+ expression.family,
114
+ )
115
+
116
+
117
+ def maximum(expression: Expression[Value], /) -> AggregateExpression[Value | None]:
118
+ return AggregateExpression(
119
+ AggregateNode(AggregateName.MAXIMUM, (expression.node,)),
120
+ expression.family,
121
+ )
122
+
123
+
124
+ def json_agg(expression: NodeProvider, /) -> AggregateExpression[JsonValue]:
125
+ """The rows of a group gathered into one JSON array.
126
+
127
+ Only PostgreSQL and SQLite keep the order the rows were read in. MySQL
128
+ gathers them in whatever order it reached them.
129
+ """
130
+ return AggregateExpression[JsonValue](
131
+ AggregateNode(AggregateName.JSON_ARRAY_AGG, (expression.node,)),
132
+ ScalarFamily.JSON,
133
+ )
134
+
135
+
136
+ def json_object_agg(
137
+ member: Expression[str],
138
+ value: NodeProvider,
139
+ /,
140
+ ) -> AggregateExpression[JsonValue]:
141
+ """The rows of a group gathered into one JSON object, named by a column."""
142
+ return AggregateExpression[JsonValue](
143
+ AggregateNode(AggregateName.JSON_OBJECT_AGG, (member.node, value.node)),
144
+ ScalarFamily.JSON,
145
+ )
146
+
147
+
148
+ def _require_numeric(expression: Expression[object]) -> None:
149
+ if expression.family is not ScalarFamily.NUMERIC:
150
+ message = "aggregate requires a numeric expression"
151
+ raise ExpressionTypeError(message)
152
+
153
+
154
+ def _require_expression(value: object) -> Expression[object]:
155
+ if isinstance(value, Expression):
156
+ return cast(Expression[object], value)
157
+ message = "aggregate requires a typed expression"
158
+ raise TypeError(message)
159
+
160
+
161
+ __all__ = (
162
+ "AggregateExpression",
163
+ "AggregateName",
164
+ "AggregateNode",
165
+ "average",
166
+ "count",
167
+ "json_agg",
168
+ "json_object_agg",
169
+ "maximum",
170
+ "minimum",
171
+ "sum_",
172
+ )
pyoq/query/arrays.py ADDED
@@ -0,0 +1,65 @@
1
+ """Building an array out of the values around it.
2
+
3
+ Asking about an array is asked of the array, so those questions are methods
4
+ on the value itself. Building one starts from values that are not an array
5
+ yet, so it starts here.
6
+
7
+ Only PostgreSQL keeps arrays as a type of their own. The other dialects
8
+ refuse an array before the statement reaches them rather than storing one
9
+ under a name that does not mean the same thing.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from types import GenericAlias
15
+ from typing import TypeVar, cast
16
+
17
+ from pyoq.errors import QueryValidationError
18
+ from pyoq.query.expressions import (
19
+ ComputedExpression,
20
+ Expression,
21
+ family_for_value,
22
+ operand,
23
+ )
24
+ from pyoq.query.nodes import ArrayConstructNode, ExpressionNode, ScalarFamily
25
+
26
+ Element = TypeVar("Element")
27
+
28
+
29
+ def array_of(*values: Element) -> ComputedExpression[tuple[Element, ...]]:
30
+ """An array holding these values, in the order they are written.
31
+
32
+ At least one value, because an array built from none has no element type
33
+ and a database will not store a value whose type it cannot name.
34
+ """
35
+ if not values:
36
+ message = "an array is built from at least one value, which gives it a type"
37
+ raise QueryValidationError(message)
38
+ elements: tuple[ExpressionNode, ...] = tuple(
39
+ operand(value, family_for_value(value)) for value in values
40
+ )
41
+ return ComputedExpression[tuple[Element, ...]](
42
+ ArrayConstructNode(elements, _element_type(values)), ScalarFamily.OTHER
43
+ )
44
+
45
+
46
+ def _element_type(values: tuple[object, ...]) -> type[object] | None:
47
+ """What an array of these values holds, where they all hold one thing.
48
+
49
+ A column of an array is declared as a tuple of its element type, and an
50
+ array built here is declared the same way, so both read back alike. The
51
+ tuple type is built rather than written, which is the one place a checker
52
+ cannot see the type being made, so it is named here.
53
+
54
+ Values of more than one type, or values that are expressions, say nothing
55
+ about the element type, and the family speaks for them instead.
56
+ """
57
+ if any(isinstance(value, Expression) for value in values):
58
+ return None
59
+ held = {type(value) for value in values}
60
+ if len(held) != 1:
61
+ return None
62
+ return cast("type[object]", GenericAlias(tuple, (held.pop(), Ellipsis)))
63
+
64
+
65
+ __all__ = ("array_of",)
pyoq/query/binding.py ADDED
@@ -0,0 +1,52 @@
1
+ """Bound parameter accounting for query and write structures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pyoq.query.nodes import (
6
+ AggregateNode,
7
+ BinaryNode,
8
+ BoundValueNode,
9
+ ExpressionNode,
10
+ ExtractNode,
11
+ FunctionNode,
12
+ RawNode,
13
+ UnaryNode,
14
+ VariadicNode,
15
+ )
16
+ from pyoq.query.select_nodes import ProjectionNode
17
+
18
+
19
+ def bound_value_count(node: ExpressionNode) -> int:
20
+ direct = 1 if isinstance(node, BoundValueNode) else 0
21
+ return direct + sum(bound_value_count(child) for child in _children(node))
22
+
23
+
24
+ def row_bound_value_count(row: tuple[ExpressionNode, ...]) -> int:
25
+ return sum(bound_value_count(value) for value in row)
26
+
27
+
28
+ def projection_bound_value_count(projections: tuple[ProjectionNode, ...]) -> int:
29
+ return sum(bound_value_count(node.expression) for node in projections)
30
+
31
+
32
+ def _children(node: ExpressionNode) -> tuple[ExpressionNode, ...]:
33
+ if isinstance(node, UnaryNode):
34
+ return (node.operand,)
35
+ if isinstance(node, BinaryNode):
36
+ return (node.left, node.right)
37
+ if isinstance(node, VariadicNode):
38
+ return node.operands
39
+ if isinstance(node, (FunctionNode, AggregateNode)):
40
+ return node.arguments
41
+ if isinstance(node, ExtractNode):
42
+ return (node.expression,)
43
+ if isinstance(node, RawNode):
44
+ return tuple(part for part in node.parts if not isinstance(part, str))
45
+ return ()
46
+
47
+
48
+ __all__ = (
49
+ "bound_value_count",
50
+ "projection_bound_value_count",
51
+ "row_bound_value_count",
52
+ )
@@ -0,0 +1,317 @@
1
+ """What each dialect can be asked for, one operation at a time.
2
+
3
+ A broad name like JSON or windows says nothing about which questions a
4
+ dialect will answer. This says it for each operation: written by the dialect
5
+ itself, written by PyOQ out of what the dialect has, or refused before the
6
+ statement reaches a server.
7
+
8
+ This is the one place that claim is made. The documentation is written from
9
+ it and a contract compiles every row against every dialect, so a claim here
10
+ that is not true stops the test suite rather than reaching a caller.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from enum import StrEnum
18
+ from types import MappingProxyType
19
+
20
+ from pyoq.config import DatabaseDialect
21
+ from pyoq.query.nodes import ScalarFamily
22
+
23
+
24
+ class Support(StrEnum):
25
+ """How a dialect answers one operation."""
26
+
27
+ NATIVE = "native"
28
+ """The dialect has it, and PyOQ writes what the dialect calls it."""
29
+
30
+ EMULATED = "emulated"
31
+ """The dialect lacks it, and PyOQ writes it out of what the dialect has."""
32
+
33
+ REJECTED = "rejected"
34
+ """No dialect support, refused while the query is compiled."""
35
+
36
+
37
+ class Operation(StrEnum):
38
+ """One thing a caller can ask for, named at the grain support is decided."""
39
+
40
+ WINDOW_ROW_NUMBER = "window: row number"
41
+ WINDOW_RANK = "window: rank"
42
+ WINDOW_DENSE_RANK = "window: dense rank"
43
+ WINDOW_PERCENT_RANK = "window: percent rank"
44
+ WINDOW_CUME_DIST = "window: cumulative distribution"
45
+ WINDOW_NTILE = "window: ntile"
46
+ WINDOW_LAG = "window: lag"
47
+ WINDOW_LEAD = "window: lead"
48
+ WINDOW_FIRST_VALUE = "window: first value"
49
+ WINDOW_LAST_VALUE = "window: last value"
50
+ WINDOW_NTH_VALUE = "window: nth value"
51
+ WINDOW_AGGREGATE = "window: an aggregate over a window"
52
+ WINDOW_PARTITION = "window: partition by"
53
+ WINDOW_ORDER = "window: order by"
54
+ WINDOW_FRAME_ROWS = "window frame: rows"
55
+ WINDOW_FRAME_RANGE = "window frame: range"
56
+ WINDOW_FRAME_GROUPS = "window frame: groups"
57
+ WINDOW_FRAME_EXCLUDE = "window frame: exclude"
58
+ WINDOW_NAMED = "window: declared by name"
59
+
60
+ JSON_VALUE = "json: read a value"
61
+ JSON_TEXT = "json: read text"
62
+ JSON_LENGTH = "json: how long an array is"
63
+ JSON_EXISTS = "json: whether a path is there"
64
+ JSON_CONTAINS = "json: whether it holds a value"
65
+ JSON_SET = "json: set"
66
+ JSON_INSERT = "json: insert"
67
+ JSON_REPLACE = "json: replace"
68
+ JSON_MERGE = "json: merge all the way down"
69
+ JSON_CONCAT = "json: write over members, one level"
70
+ JSON_REMOVE = "json: remove"
71
+ JSON_OBJECT = "json: build an object"
72
+ JSON_ARRAY = "json: build an array"
73
+ JSON_KEYS = "json: name the members"
74
+ JSON_AGG = "json: gather rows into an array"
75
+ JSON_OBJECT_AGG = "json: gather rows into an object"
76
+ JSON_ELEMENTS = "json: read an array as rows"
77
+
78
+ ARRAY_HAS = "array: holds a value"
79
+ ARRAY_LACKS = "array: holds a value nowhere"
80
+ ARRAY_CONTAINS_ALL = "array: holds every value"
81
+ ARRAY_CONTAINED_BY = "array: is among"
82
+ ARRAY_OVERLAPS = "array: shares a value"
83
+ ARRAY_APPEND = "array: append"
84
+ ARRAY_PREPEND = "array: prepend"
85
+ ARRAY_CONCAT = "array: concatenate"
86
+ ARRAY_REMOVE = "array: without a value"
87
+ ARRAY_REPLACE = "array: replace a value"
88
+ ARRAY_ELEMENT = "array: one element"
89
+ ARRAY_LENGTH = "array: how many elements"
90
+ ARRAY_LENGTH_ALONG = "array: how many along a dimension"
91
+ ARRAY_DIMENSIONS = "array: how many dimensions"
92
+ ARRAY_CONSTRUCT = "array: built from values"
93
+
94
+ LOCK_FOR_UPDATE = "lock: for update"
95
+ LOCK_FOR_SHARE = "lock: for share"
96
+ LOCK_NOWAIT = "lock: fail rather than wait"
97
+ LOCK_SKIP_LOCKED = "lock: skip what is locked"
98
+ LOCK_OF_TABLES = "lock: named tables"
99
+ LOCK_OF_FIELDS = "lock: named fields"
100
+
101
+ QUERY_LATERAL = "query: lateral source"
102
+ QUERY_RECURSIVE = "query: recursive common table"
103
+ QUERY_ROW_VALUE = "query: row value"
104
+ QUERY_CAST = "query: cast"
105
+ QUERY_NULL_ORDERING = "query: nulls first or last"
106
+ QUERY_RIGHT_JOIN = "query: right join"
107
+ QUERY_FULL_JOIN = "query: full join"
108
+
109
+ ROUTINE_PROCEDURE = "routine: run a stored procedure"
110
+ ROUTINE_FUNCTION = "routine: ask a function by name"
111
+ ROUTINE_MANY_RESULT_SETS = "routine: more than one result set"
112
+
113
+
114
+ _P = DatabaseDialect.POSTGRES
115
+ _M = DatabaseDialect.MYSQL
116
+ _S = DatabaseDialect.SQLITE
117
+
118
+ _N = Support.NATIVE
119
+ _E = Support.EMULATED
120
+ _R = Support.REJECTED
121
+
122
+
123
+ @dataclass(frozen=True, slots=True)
124
+ class OperationSupport:
125
+ """One operation: what each dialect does, what it reads back, what runs it.
126
+
127
+ ``reads_back_as`` is the kind of value the operation answers with, which
128
+ is what decides how a driver's answer is read back. It is none where the
129
+ operation answers with rows rather than with a value.
130
+
131
+ ``evidence`` names the dialect-neutral contract that executes it against
132
+ every server that claims it, so a row points at the run that proves it
133
+ rather than at a compiled string.
134
+ """
135
+
136
+ dialects: Mapping[DatabaseDialect, Support]
137
+ reads_back_as: ScalarFamily | None
138
+ evidence: str
139
+
140
+
141
+ def _row(
142
+ postgres: Support,
143
+ mysql: Support,
144
+ sqlite: Support,
145
+ reads_back_as: ScalarFamily | None,
146
+ evidence: str,
147
+ ) -> OperationSupport:
148
+ return OperationSupport(
149
+ MappingProxyType({_P: postgres, _M: mysql, _S: sqlite}),
150
+ reads_back_as,
151
+ evidence,
152
+ )
153
+
154
+
155
+ SUPPORT: Mapping[Operation, OperationSupport] = MappingProxyType(
156
+ {
157
+ Operation.WINDOW_ROW_NUMBER: _row(
158
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
159
+ ),
160
+ Operation.WINDOW_RANK: _row(
161
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
162
+ ),
163
+ Operation.WINDOW_DENSE_RANK: _row(
164
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
165
+ ),
166
+ Operation.WINDOW_PERCENT_RANK: _row(
167
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
168
+ ),
169
+ Operation.WINDOW_CUME_DIST: _row(
170
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
171
+ ),
172
+ Operation.WINDOW_NTILE: _row(
173
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
174
+ ),
175
+ Operation.WINDOW_LAG: _row(_N, _N, _N, ScalarFamily.NUMERIC, "window_contract"),
176
+ Operation.WINDOW_LEAD: _row(
177
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
178
+ ),
179
+ Operation.WINDOW_FIRST_VALUE: _row(
180
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
181
+ ),
182
+ Operation.WINDOW_LAST_VALUE: _row(
183
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
184
+ ),
185
+ Operation.WINDOW_NTH_VALUE: _row(
186
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
187
+ ),
188
+ Operation.WINDOW_AGGREGATE: _row(
189
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
190
+ ),
191
+ Operation.WINDOW_PARTITION: _row(
192
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
193
+ ),
194
+ Operation.WINDOW_ORDER: _row(
195
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
196
+ ),
197
+ Operation.WINDOW_FRAME_ROWS: _row(
198
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
199
+ ),
200
+ Operation.WINDOW_FRAME_RANGE: _row(
201
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
202
+ ),
203
+ Operation.WINDOW_FRAME_GROUPS: _row(
204
+ _N, _R, _N, ScalarFamily.NUMERIC, "window_contract"
205
+ ),
206
+ Operation.WINDOW_FRAME_EXCLUDE: _row(
207
+ _N, _R, _N, ScalarFamily.NUMERIC, "window_contract"
208
+ ),
209
+ Operation.WINDOW_NAMED: _row(
210
+ _N, _N, _N, ScalarFamily.NUMERIC, "window_contract"
211
+ ),
212
+ Operation.JSON_VALUE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
213
+ Operation.JSON_TEXT: _row(_N, _N, _N, ScalarFamily.STRING, "json_contract"),
214
+ Operation.JSON_LENGTH: _row(_N, _N, _N, ScalarFamily.NUMERIC, "json_contract"),
215
+ Operation.JSON_EXISTS: _row(_N, _N, _N, ScalarFamily.BOOLEAN, "json_contract"),
216
+ Operation.JSON_CONTAINS: _row(
217
+ _N, _N, _R, ScalarFamily.BOOLEAN, "json_contract"
218
+ ),
219
+ Operation.JSON_SET: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
220
+ Operation.JSON_INSERT: _row(_E, _N, _N, ScalarFamily.JSON, "json_contract"),
221
+ Operation.JSON_REPLACE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
222
+ Operation.JSON_MERGE: _row(_R, _N, _N, ScalarFamily.JSON, "json_contract"),
223
+ Operation.JSON_CONCAT: _row(_N, _R, _R, ScalarFamily.JSON, "json_contract"),
224
+ Operation.JSON_REMOVE: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
225
+ Operation.JSON_OBJECT: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
226
+ Operation.JSON_ARRAY: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
227
+ Operation.JSON_KEYS: _row(_E, _N, _E, ScalarFamily.JSON, "json_contract"),
228
+ Operation.JSON_AGG: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
229
+ Operation.JSON_OBJECT_AGG: _row(_N, _N, _N, ScalarFamily.JSON, "json_contract"),
230
+ Operation.JSON_ELEMENTS: _row(_N, _N, _N, ScalarFamily.STRING, "json_contract"),
231
+ Operation.ARRAY_HAS: _row(_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"),
232
+ Operation.ARRAY_LACKS: _row(_N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"),
233
+ Operation.ARRAY_CONTAINS_ALL: _row(
234
+ _N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
235
+ ),
236
+ Operation.ARRAY_CONTAINED_BY: _row(
237
+ _N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
238
+ ),
239
+ Operation.ARRAY_OVERLAPS: _row(
240
+ _N, _R, _R, ScalarFamily.BOOLEAN, "array_contract"
241
+ ),
242
+ Operation.ARRAY_APPEND: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
243
+ Operation.ARRAY_PREPEND: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
244
+ Operation.ARRAY_CONCAT: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
245
+ Operation.ARRAY_REMOVE: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
246
+ Operation.ARRAY_REPLACE: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
247
+ Operation.ARRAY_ELEMENT: _row(_N, _R, _R, ScalarFamily.OTHER, "array_contract"),
248
+ Operation.ARRAY_LENGTH: _row(
249
+ _N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
250
+ ),
251
+ Operation.ARRAY_LENGTH_ALONG: _row(
252
+ _N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
253
+ ),
254
+ Operation.ARRAY_DIMENSIONS: _row(
255
+ _N, _R, _R, ScalarFamily.NUMERIC, "array_contract"
256
+ ),
257
+ Operation.ARRAY_CONSTRUCT: _row(
258
+ _N, _R, _R, ScalarFamily.OTHER, "array_contract"
259
+ ),
260
+ Operation.LOCK_FOR_UPDATE: _row(
261
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
262
+ ),
263
+ Operation.LOCK_FOR_SHARE: _row(
264
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
265
+ ),
266
+ Operation.LOCK_NOWAIT: _row(
267
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
268
+ ),
269
+ Operation.LOCK_SKIP_LOCKED: _row(
270
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
271
+ ),
272
+ Operation.LOCK_OF_TABLES: _row(
273
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
274
+ ),
275
+ Operation.LOCK_OF_FIELDS: _row(
276
+ _N, _N, _R, ScalarFamily.NUMERIC, "locking_contract"
277
+ ),
278
+ Operation.QUERY_LATERAL: _row(
279
+ _N, _N, _R, ScalarFamily.NUMERIC, "lateral_contract"
280
+ ),
281
+ Operation.QUERY_RECURSIVE: _row(
282
+ _N, _N, _N, ScalarFamily.NUMERIC, "recursion_contract"
283
+ ),
284
+ Operation.QUERY_ROW_VALUE: _row(
285
+ _N, _N, _N, ScalarFamily.BOOLEAN, "row_value_contract"
286
+ ),
287
+ Operation.QUERY_CAST: _row(_N, _N, _N, ScalarFamily.STRING, "cast_contract"),
288
+ Operation.QUERY_NULL_ORDERING: _row(
289
+ _N, _E, _N, ScalarFamily.NUMERIC, "null_order_contract"
290
+ ),
291
+ Operation.QUERY_RIGHT_JOIN: _row(
292
+ _N, _N, _R, ScalarFamily.NUMERIC, "joined_contract"
293
+ ),
294
+ Operation.QUERY_FULL_JOIN: _row(
295
+ _N, _R, _R, ScalarFamily.NUMERIC, "joined_contract"
296
+ ),
297
+ Operation.ROUTINE_PROCEDURE: _row(_N, _N, _R, None, "routine_contract"),
298
+ Operation.ROUTINE_FUNCTION: _row(
299
+ _N, _N, _N, ScalarFamily.NUMERIC, "routine_contract"
300
+ ),
301
+ Operation.ROUTINE_MANY_RESULT_SETS: _row(_R, _R, _R, None, "routine_contract"),
302
+ }
303
+ )
304
+ """Every operation, and how each dialect answers it.
305
+
306
+ Measured against running servers rather than read from documentation. A row
307
+ that says rejected is refused while the query is compiled, so a caller finds
308
+ out while writing rather than in production.
309
+ """
310
+
311
+
312
+ def support_for(operation: Operation, dialect: DatabaseDialect, /) -> Support:
313
+ """How that dialect answers that operation."""
314
+ return SUPPORT[operation].dialects[dialect]
315
+
316
+
317
+ __all__ = ("SUPPORT", "Operation", "OperationSupport", "Support", "support_for")
pyoq/query/casts.py ADDED
@@ -0,0 +1,73 @@
1
+ """Asking the database for a value as another type.
2
+
3
+ A cast is the one computed expression that declares what it answers with, so
4
+ it is also the one the row decoder can honour. The Python type given here
5
+ decides three things at once: the SQL each dialect emits, the family the
6
+ expression carries, and what reads the value back.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import date, datetime, time, timedelta
12
+ from decimal import Decimal
13
+ from typing import TypeVar
14
+ from uuid import UUID
15
+
16
+ from pyoq.query.expressions import ComputedExpression
17
+ from pyoq.query.fields import family_for_type
18
+ from pyoq.query.nodes import CastNode
19
+ from pyoq.query.raw import NodeProvider
20
+ from pyoq.schema.models import TypeKind
21
+
22
+ Target = TypeVar("Target")
23
+
24
+ _TARGET_KINDS: dict[type[object], TypeKind] = {
25
+ bool: TypeKind.BOOLEAN,
26
+ int: TypeKind.INTEGER,
27
+ float: TypeKind.DOUBLE,
28
+ Decimal: TypeKind.DECIMAL,
29
+ str: TypeKind.STRING,
30
+ bytes: TypeKind.BINARY,
31
+ date: TypeKind.DATE,
32
+ time: TypeKind.TIME,
33
+ datetime: TypeKind.DATETIME,
34
+ timedelta: TypeKind.INTERVAL,
35
+ UUID: TypeKind.UUID,
36
+ dict: TypeKind.JSON,
37
+ list: TypeKind.JSON,
38
+ }
39
+ """The database type each Python type asks for, which a dialect then names."""
40
+
41
+
42
+ def cast(
43
+ expression: NodeProvider,
44
+ target: type[Target],
45
+ /,
46
+ ) -> ComputedExpression[Target]:
47
+ """`expression` as `target`, computed by the database.
48
+
49
+ A target no database type answers to is refused here rather than at
50
+ compilation, so it is reported where it was written.
51
+ """
52
+ kind_for_target(target)
53
+ return ComputedExpression[Target](
54
+ CastNode(expression.node, target), family_for_type(target)
55
+ )
56
+
57
+
58
+ def kind_for_target(target: type[object], /) -> TypeKind:
59
+ """The database type a Python type asks for, or a refusal naming it.
60
+
61
+ `datetime` is settled first because it is also a `date`, and asking for
62
+ one is not asking for the other.
63
+ """
64
+ if target is datetime:
65
+ return TypeKind.DATETIME
66
+ for value_type, kind in _TARGET_KINDS.items():
67
+ if issubclass(target, value_type):
68
+ return kind
69
+ message = f"{getattr(target, '__name__', target)} is not a cast target"
70
+ raise TypeError(message)
71
+
72
+
73
+ __all__ = ("cast", "kind_for_target")