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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
pyoq/query/vendor.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Calling a function this database has that PyOQ does not name itself.
|
|
2
|
+
|
|
3
|
+
Every database grows functions the others have not got, and a toolkit that
|
|
4
|
+
only offered what all three share would be a smaller toolkit than any of
|
|
5
|
+
them. A vendor function is declared once with the type it answers with, and
|
|
6
|
+
called like any other expression.
|
|
7
|
+
|
|
8
|
+
This is the manual way, and what it promises is what the caller asserted: the
|
|
9
|
+
result type is taken on trust, and the arguments are whatever was passed. A
|
|
10
|
+
function the database itself declares is reflected and generated instead, and
|
|
11
|
+
that one carries the database's own signature, so its arity and parameter
|
|
12
|
+
types are checked rather than assumed. Reach for this when the function is not
|
|
13
|
+
in the catalog, or when generation is not in use.
|
|
14
|
+
|
|
15
|
+
The declaration can say which databases the function exists on. That turns a
|
|
16
|
+
query nobody could run into one that is refused while it is being written,
|
|
17
|
+
rather than one that fails at the server.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
24
|
+
|
|
25
|
+
from pyoq.errors import ExpressionTypeError
|
|
26
|
+
from pyoq.query.expressions import ComputedExpression, Condition
|
|
27
|
+
from pyoq.query.fields import family_for_type
|
|
28
|
+
from pyoq.query.nodes import ScalarFamily, VendorFunctionNode
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Collection
|
|
32
|
+
|
|
33
|
+
from pyoq.config import DatabaseDialect
|
|
34
|
+
from pyoq.query.raw import NodeProvider
|
|
35
|
+
|
|
36
|
+
Value = TypeVar("Value")
|
|
37
|
+
|
|
38
|
+
_FORBIDDEN = ("\x00", "\n", "\r")
|
|
39
|
+
"""What no identifier may hold, quoted or not."""
|
|
40
|
+
|
|
41
|
+
PLAIN_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$")
|
|
42
|
+
"""A name a database reads as written, needing no quoting to be safe."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VendorFunction(Generic[Value]):
|
|
46
|
+
"""A declared function, callable wherever an expression is."""
|
|
47
|
+
|
|
48
|
+
__slots__ = ("_catalog", "_dialects", "_family", "_name", "_schema", "_value_type")
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
name: str,
|
|
53
|
+
family: ScalarFamily,
|
|
54
|
+
dialects: frozenset[str],
|
|
55
|
+
value_type: type[object] | None = None,
|
|
56
|
+
schema: str | None = None,
|
|
57
|
+
catalog: str | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._name = name
|
|
60
|
+
self._family = family
|
|
61
|
+
self._dialects = dialects
|
|
62
|
+
self._value_type = value_type
|
|
63
|
+
self._schema = schema
|
|
64
|
+
self._catalog = catalog
|
|
65
|
+
|
|
66
|
+
def __call__(self, *arguments: NodeProvider) -> ComputedExpression[Value]:
|
|
67
|
+
return ComputedExpression[Value](self._node(arguments), self._family)
|
|
68
|
+
|
|
69
|
+
def _node(self, arguments: tuple[NodeProvider, ...]) -> VendorFunctionNode:
|
|
70
|
+
return VendorFunctionNode(
|
|
71
|
+
self._name,
|
|
72
|
+
tuple(argument.node for argument in arguments),
|
|
73
|
+
self._family,
|
|
74
|
+
self._dialects,
|
|
75
|
+
value_type=self._value_type,
|
|
76
|
+
schema=self._schema,
|
|
77
|
+
catalog=self._catalog,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class VendorPredicate(VendorFunction[bool]):
|
|
82
|
+
"""A declared function that answers true or false."""
|
|
83
|
+
|
|
84
|
+
__slots__ = ()
|
|
85
|
+
|
|
86
|
+
def __call__(self, *arguments: NodeProvider) -> Condition: # type: ignore[override]
|
|
87
|
+
return Condition(self._node(arguments))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def vendor_function(
|
|
91
|
+
value_type: type[Value],
|
|
92
|
+
name: str,
|
|
93
|
+
/,
|
|
94
|
+
*,
|
|
95
|
+
dialects: Collection[DatabaseDialect] = (),
|
|
96
|
+
schema: str | None = None,
|
|
97
|
+
catalog: str | None = None,
|
|
98
|
+
) -> VendorFunction[Value]:
|
|
99
|
+
"""Declare a function this database has, and what it answers with.
|
|
100
|
+
|
|
101
|
+
Saying which schema keeps it is what makes a call reach that one rather
|
|
102
|
+
than whichever the search path happens to find first.
|
|
103
|
+
"""
|
|
104
|
+
return VendorFunction[Value](
|
|
105
|
+
checked_routine_name(name),
|
|
106
|
+
family_for_type(value_type),
|
|
107
|
+
_named(dialects),
|
|
108
|
+
value_type,
|
|
109
|
+
checked_routine_qualifier(schema, "routine schema"),
|
|
110
|
+
checked_routine_qualifier(catalog, "routine catalog"),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def checked_routine_qualifier(value: str | None, label: str) -> str | None:
|
|
115
|
+
"""Where a routine is kept, held to being a name a database can hold."""
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
if not value or "\x00" in value or "\n" in value or "\r" in value:
|
|
119
|
+
message = f"{label} cannot be empty or hold a line break"
|
|
120
|
+
raise ExpressionTypeError(message)
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def vendor_predicate(
|
|
125
|
+
name: str,
|
|
126
|
+
/,
|
|
127
|
+
*,
|
|
128
|
+
dialects: Collection[DatabaseDialect] = (),
|
|
129
|
+
) -> VendorPredicate:
|
|
130
|
+
"""Declare a function that answers true or false."""
|
|
131
|
+
return VendorPredicate(
|
|
132
|
+
checked_routine_name(name), ScalarFamily.BOOLEAN, _named(dialects)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def checked_routine_name(name: str) -> str:
|
|
137
|
+
"""A routine name, checked because it is written into the SQL, not bound.
|
|
138
|
+
|
|
139
|
+
A database takes no routine name as a parameter, so the name is written
|
|
140
|
+
out. One that reads as an identifier is written as it stands; any other
|
|
141
|
+
is quoted by the dialect, which is what makes a name with a space in it
|
|
142
|
+
usable and a name with anything else in it harmless.
|
|
143
|
+
"""
|
|
144
|
+
if not name or any(character in name for character in _FORBIDDEN):
|
|
145
|
+
message = f"{name!r} is not a routine name"
|
|
146
|
+
raise ExpressionTypeError(message)
|
|
147
|
+
return name
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _named(dialects: Collection[DatabaseDialect]) -> frozenset[str]:
|
|
151
|
+
return frozenset(dialect.value for dialect in dialects)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = (
|
|
155
|
+
"PLAIN_NAME",
|
|
156
|
+
"VendorFunction",
|
|
157
|
+
"VendorPredicate",
|
|
158
|
+
"checked_routine_name",
|
|
159
|
+
"checked_routine_qualifier",
|
|
160
|
+
"vendor_function",
|
|
161
|
+
"vendor_predicate",
|
|
162
|
+
)
|
pyoq/query/windows.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Measuring a row against the rows around it.
|
|
2
|
+
|
|
3
|
+
An aggregate collapses a group into one row. A window leaves the rows alone
|
|
4
|
+
and gives each one an answer computed from its neighbours, so a running total,
|
|
5
|
+
a rank, or the previous row's value can be selected beside the row itself.
|
|
6
|
+
|
|
7
|
+
The clauses read in the order SQL writes them: the function, then the window
|
|
8
|
+
it looks through, then how that window is divided and ordered.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, replace
|
|
15
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
16
|
+
|
|
17
|
+
from pyoq.errors import QueryStateError, QueryValidationError
|
|
18
|
+
from pyoq.query.expressions import ComputedExpression, Expression, operand
|
|
19
|
+
from pyoq.query.nodes import (
|
|
20
|
+
EVERY_ROW,
|
|
21
|
+
FrameBound,
|
|
22
|
+
FrameBoundNode,
|
|
23
|
+
FrameExclusion,
|
|
24
|
+
FrameKind,
|
|
25
|
+
FrameNode,
|
|
26
|
+
NamedWindowNode,
|
|
27
|
+
ScalarFamily,
|
|
28
|
+
WindowFunctionNode,
|
|
29
|
+
WindowName,
|
|
30
|
+
WindowNode,
|
|
31
|
+
WindowReferenceNode,
|
|
32
|
+
WindowSpecificationNode,
|
|
33
|
+
)
|
|
34
|
+
from pyoq.query.projections import order_node
|
|
35
|
+
from pyoq.query.raw import NodeProvider
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from pyoq.query.nodes import ExpressionNode
|
|
39
|
+
from pyoq.query.select_nodes import OrderNode
|
|
40
|
+
|
|
41
|
+
Value = TypeVar("Value")
|
|
42
|
+
|
|
43
|
+
_WINDOW_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
44
|
+
"""A window is named by one identifier, and it is written as it is given."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class WindowSpecification:
|
|
49
|
+
"""How a window divides and orders the rows it covers.
|
|
50
|
+
|
|
51
|
+
The same vocabulary builds the window a single function looks through and
|
|
52
|
+
the one a query declares by name, so a frame means the same thing in both.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
node: WindowSpecificationNode = EVERY_ROW
|
|
56
|
+
name: str | None = None
|
|
57
|
+
|
|
58
|
+
def partition_by(self, *expressions: NodeProvider) -> WindowSpecification:
|
|
59
|
+
"""The groups the rows are divided into before they are measured."""
|
|
60
|
+
if not expressions:
|
|
61
|
+
message = "partition by requires at least one expression"
|
|
62
|
+
raise ValueError(message)
|
|
63
|
+
return self._with(
|
|
64
|
+
partition_by=tuple(expression.node for expression in expressions)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def order_by(self, *terms: object) -> WindowSpecification:
|
|
68
|
+
"""The order the rows are read in, which is what makes a rank a rank.
|
|
69
|
+
|
|
70
|
+
A window orders rows the way a query does, so a bare column and a
|
|
71
|
+
term that names a direction are both accepted here.
|
|
72
|
+
"""
|
|
73
|
+
if not terms:
|
|
74
|
+
message = "order by requires at least one term"
|
|
75
|
+
raise ValueError(message)
|
|
76
|
+
return self._with(order_by=tuple(order_node(term) for term in terms))
|
|
77
|
+
|
|
78
|
+
def rows(
|
|
79
|
+
self, start: FrameEdge, end: FrameEdge | None = None
|
|
80
|
+
) -> WindowSpecification:
|
|
81
|
+
"""Narrow the window by counting rows around the one being measured."""
|
|
82
|
+
return self._framed(FrameKind.ROWS, start, end)
|
|
83
|
+
|
|
84
|
+
def range_(
|
|
85
|
+
self, start: FrameEdge, end: FrameEdge | None = None
|
|
86
|
+
) -> WindowSpecification:
|
|
87
|
+
"""Narrow the window by value, so rows of equal value come together."""
|
|
88
|
+
return self._framed(FrameKind.RANGE, start, end)
|
|
89
|
+
|
|
90
|
+
def groups(
|
|
91
|
+
self, start: FrameEdge, end: FrameEdge | None = None
|
|
92
|
+
) -> WindowSpecification:
|
|
93
|
+
"""Narrow the window by counting peer groups rather than rows."""
|
|
94
|
+
return self._framed(FrameKind.GROUPS, start, end)
|
|
95
|
+
|
|
96
|
+
def exclude(self, exclusion: FrameExclusion, /) -> WindowSpecification:
|
|
97
|
+
"""Leave rows out of a frame that it would otherwise reach."""
|
|
98
|
+
frame = self.node.frame
|
|
99
|
+
if frame is None:
|
|
100
|
+
message = "what a frame excludes is said after what it covers"
|
|
101
|
+
raise QueryStateError(message)
|
|
102
|
+
return self._replace_frame(replace(frame, exclusion=exclusion))
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def declaration(self) -> NamedWindowNode:
|
|
106
|
+
"""This window as a query declares it, which needs it to be named."""
|
|
107
|
+
if self.name is None:
|
|
108
|
+
message = "only a named window is declared by a query"
|
|
109
|
+
raise QueryStateError(message)
|
|
110
|
+
return NamedWindowNode(self.name, self.node)
|
|
111
|
+
|
|
112
|
+
def _framed(
|
|
113
|
+
self,
|
|
114
|
+
kind: FrameKind,
|
|
115
|
+
start: FrameEdge,
|
|
116
|
+
end: FrameEdge | None,
|
|
117
|
+
) -> WindowSpecification:
|
|
118
|
+
return self._replace_frame(
|
|
119
|
+
FrameNode(kind, start.node, None if end is None else end.node)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _replace_frame(self, frame: FrameNode) -> WindowSpecification:
|
|
123
|
+
return replace(self, node=replace(self.node, frame=frame))
|
|
124
|
+
|
|
125
|
+
def _with(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
partition_by: tuple[ExpressionNode, ...] | None = None,
|
|
129
|
+
order_by: tuple[OrderNode, ...] | None = None,
|
|
130
|
+
) -> WindowSpecification:
|
|
131
|
+
current = self.node
|
|
132
|
+
return replace(
|
|
133
|
+
self,
|
|
134
|
+
node=WindowSpecificationNode(
|
|
135
|
+
current.partition_by if partition_by is None else partition_by,
|
|
136
|
+
current.order_by if order_by is None else order_by,
|
|
137
|
+
current.frame,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def named_window(name: str, /) -> WindowSpecification:
|
|
143
|
+
"""A window written once in a query and looked through by several rows.
|
|
144
|
+
|
|
145
|
+
Repeating a partition and an order beside every function is how they come
|
|
146
|
+
to disagree. A named window is declared once, and each function says which
|
|
147
|
+
window it reads through.
|
|
148
|
+
|
|
149
|
+
The name is written into the SQL rather than bound, because no database
|
|
150
|
+
takes a window name as a parameter, so it is held to being a plain name.
|
|
151
|
+
"""
|
|
152
|
+
if _WINDOW_NAME.match(name) is None:
|
|
153
|
+
message = "a window name is a plain identifier"
|
|
154
|
+
raise QueryValidationError(message)
|
|
155
|
+
return WindowSpecification(name=name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def looking_through(
|
|
159
|
+
node: ExpressionNode,
|
|
160
|
+
window: WindowSpecification | None,
|
|
161
|
+
) -> WindowNode:
|
|
162
|
+
"""One function and the window it reads, named by the query or written here.
|
|
163
|
+
|
|
164
|
+
The value the window answers with is the one the function already had, so
|
|
165
|
+
the caller keeps that and only the window is decided here.
|
|
166
|
+
"""
|
|
167
|
+
if window is None:
|
|
168
|
+
return WindowNode(node)
|
|
169
|
+
if window.name is None:
|
|
170
|
+
message = "a window is declared with a name before it is read through"
|
|
171
|
+
raise QueryStateError(message)
|
|
172
|
+
return WindowNode(node, WindowReferenceNode(window.name))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Window(ComputedExpression[Value], Generic[Value]):
|
|
176
|
+
"""A value already usable, which naming partitions or an order refines."""
|
|
177
|
+
|
|
178
|
+
__slots__ = ("_window_node",)
|
|
179
|
+
_window_node: WindowNode
|
|
180
|
+
|
|
181
|
+
def __init__(self, node: WindowNode, family: ScalarFamily) -> None:
|
|
182
|
+
super().__init__(node, family)
|
|
183
|
+
object.__setattr__(self, "_window_node", node)
|
|
184
|
+
|
|
185
|
+
def partition_by(self, *expressions: NodeProvider) -> Window[Value]:
|
|
186
|
+
"""The groups the rows are divided into before they are measured."""
|
|
187
|
+
return self._refined(self._specification.partition_by(*expressions))
|
|
188
|
+
|
|
189
|
+
def order_by(self, *terms: object) -> Window[Value]:
|
|
190
|
+
"""The order the rows are read in, which is what makes a rank a rank."""
|
|
191
|
+
return self._refined(self._specification.order_by(*terms))
|
|
192
|
+
|
|
193
|
+
def rows(self, start: FrameEdge, end: FrameEdge | None = None) -> Window[Value]:
|
|
194
|
+
"""Narrow the window by counting rows around the one being measured."""
|
|
195
|
+
return self._refined(self._specification.rows(start, end))
|
|
196
|
+
|
|
197
|
+
def range_(self, start: FrameEdge, end: FrameEdge | None = None) -> Window[Value]:
|
|
198
|
+
"""Narrow the window by value, so rows of equal value come together."""
|
|
199
|
+
return self._refined(self._specification.range_(start, end))
|
|
200
|
+
|
|
201
|
+
def groups(self, start: FrameEdge, end: FrameEdge | None = None) -> Window[Value]:
|
|
202
|
+
"""Narrow the window by counting peer groups rather than rows."""
|
|
203
|
+
return self._refined(self._specification.groups(start, end))
|
|
204
|
+
|
|
205
|
+
def exclude(self, exclusion: FrameExclusion, /) -> Window[Value]:
|
|
206
|
+
"""Leave rows out of a frame that it would otherwise reach."""
|
|
207
|
+
return self._refined(self._specification.exclude(exclusion))
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def _specification(self) -> WindowSpecification:
|
|
211
|
+
window = self._window_node.window
|
|
212
|
+
if isinstance(window, WindowReferenceNode):
|
|
213
|
+
message = "a window declared by the query is refined where it is declared"
|
|
214
|
+
raise QueryStateError(message)
|
|
215
|
+
return WindowSpecification(window)
|
|
216
|
+
|
|
217
|
+
def _refined(self, specification: WindowSpecification) -> Window[Value]:
|
|
218
|
+
return Window[Value](
|
|
219
|
+
replace(self._window_node, window=specification.node), self.family
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class WindowFunction(ComputedExpression[Value], Generic[Value]):
|
|
224
|
+
"""A function that means nothing until it is given a window to look through."""
|
|
225
|
+
|
|
226
|
+
__slots__ = ()
|
|
227
|
+
|
|
228
|
+
def over(self, window: WindowSpecification | None = None, /) -> Window[Value]:
|
|
229
|
+
"""The rows this reads, which is every row until it is partitioned.
|
|
230
|
+
|
|
231
|
+
Given a window the query declares, this reads through that one by
|
|
232
|
+
name and is not refined further here.
|
|
233
|
+
"""
|
|
234
|
+
return Window[Value](looking_through(self.node, window), self.family)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(frozen=True, slots=True)
|
|
238
|
+
class FrameEdge:
|
|
239
|
+
"""One edge of a frame, built by the helpers below rather than by hand."""
|
|
240
|
+
|
|
241
|
+
node: FrameBoundNode
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def unbounded_preceding() -> FrameEdge:
|
|
245
|
+
"""Every row before this one, however many there are."""
|
|
246
|
+
return FrameEdge(FrameBoundNode(FrameBound.UNBOUNDED_PRECEDING))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def preceding(distance: int, /) -> FrameEdge:
|
|
250
|
+
"""This far back from the row being measured."""
|
|
251
|
+
return FrameEdge(
|
|
252
|
+
FrameBoundNode(
|
|
253
|
+
FrameBound.PRECEDING, operand(_counted(distance), ScalarFamily.NUMERIC)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def current_row() -> FrameEdge:
|
|
259
|
+
"""The row being measured."""
|
|
260
|
+
return FrameEdge(FrameBoundNode(FrameBound.CURRENT_ROW))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def following(distance: int, /) -> FrameEdge:
|
|
264
|
+
"""This far ahead of the row being measured."""
|
|
265
|
+
return FrameEdge(
|
|
266
|
+
FrameBoundNode(
|
|
267
|
+
FrameBound.FOLLOWING, operand(_counted(distance), ScalarFamily.NUMERIC)
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def unbounded_following() -> FrameEdge:
|
|
273
|
+
"""Every row after this one, however many there are."""
|
|
274
|
+
return FrameEdge(FrameBoundNode(FrameBound.UNBOUNDED_FOLLOWING))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _counted(distance: int) -> int:
|
|
278
|
+
if distance < 0:
|
|
279
|
+
message = "a frame edge counts forwards from the row it is measured against"
|
|
280
|
+
raise ValueError(message)
|
|
281
|
+
return distance
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def row_number() -> WindowFunction[int]:
|
|
285
|
+
"""Which row this is in its window, counting from one, ties broken."""
|
|
286
|
+
return _ranking(WindowName.ROW_NUMBER)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def rank() -> WindowFunction[int]:
|
|
290
|
+
"""The position of this row, where ties share a position and leave a gap."""
|
|
291
|
+
return _ranking(WindowName.RANK)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def dense_rank() -> WindowFunction[int]:
|
|
295
|
+
"""The position of this row, where ties share a position and leave no gap."""
|
|
296
|
+
return _ranking(WindowName.DENSE_RANK)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def ntile(buckets: int, /) -> WindowFunction[int]:
|
|
300
|
+
"""Which of this many equal buckets the row falls in."""
|
|
301
|
+
if buckets < 1:
|
|
302
|
+
message = "ntile requires at least one bucket"
|
|
303
|
+
raise ValueError(message)
|
|
304
|
+
return WindowFunction[int](
|
|
305
|
+
WindowFunctionNode(WindowName.NTILE, (operand(buckets, ScalarFamily.NUMERIC),)),
|
|
306
|
+
ScalarFamily.NUMERIC,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def lag(
|
|
311
|
+
expression: Expression[Value],
|
|
312
|
+
offset: int = 1,
|
|
313
|
+
/,
|
|
314
|
+
) -> WindowFunction[Value | None]:
|
|
315
|
+
"""The value this far back in the window, or null before its start."""
|
|
316
|
+
return _offset(WindowName.LAG, expression, offset)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def lead(
|
|
320
|
+
expression: Expression[Value],
|
|
321
|
+
offset: int = 1,
|
|
322
|
+
/,
|
|
323
|
+
) -> WindowFunction[Value | None]:
|
|
324
|
+
"""The value this far ahead in the window, or null past its end."""
|
|
325
|
+
return _offset(WindowName.LEAD, expression, offset)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def percent_rank() -> WindowFunction[float]:
|
|
329
|
+
"""How far through its window this row sits, from zero to one."""
|
|
330
|
+
return WindowFunction[float](
|
|
331
|
+
WindowFunctionNode(WindowName.PERCENT_RANK), ScalarFamily.NUMERIC
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def cume_dist() -> WindowFunction[float]:
|
|
336
|
+
"""The share of the window at or before this row, above zero to one."""
|
|
337
|
+
return WindowFunction[float](
|
|
338
|
+
WindowFunctionNode(WindowName.CUME_DIST), ScalarFamily.NUMERIC
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def first_value(expression: Expression[Value], /) -> WindowFunction[Value | None]:
|
|
343
|
+
"""The value in the first row of the window, or null when it is empty."""
|
|
344
|
+
return _navigating(WindowName.FIRST_VALUE, expression)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def last_value(expression: Expression[Value], /) -> WindowFunction[Value | None]:
|
|
348
|
+
"""The value in the last row the frame reaches, which the frame decides.
|
|
349
|
+
|
|
350
|
+
Without a frame a window ends at the current row, so this answers with the
|
|
351
|
+
current row's value until the frame is widened to the whole partition.
|
|
352
|
+
"""
|
|
353
|
+
return _navigating(WindowName.LAST_VALUE, expression)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def nth_value(
|
|
357
|
+
expression: Expression[Value],
|
|
358
|
+
position: int,
|
|
359
|
+
/,
|
|
360
|
+
) -> WindowFunction[Value | None]:
|
|
361
|
+
"""The value this far into the window, counting from one."""
|
|
362
|
+
if position < 1:
|
|
363
|
+
message = "nth value counts from one"
|
|
364
|
+
raise ValueError(message)
|
|
365
|
+
return WindowFunction[Value | None](
|
|
366
|
+
WindowFunctionNode(
|
|
367
|
+
WindowName.NTH_VALUE,
|
|
368
|
+
(expression.node, operand(position, ScalarFamily.NUMERIC)),
|
|
369
|
+
),
|
|
370
|
+
expression.family,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _navigating(
|
|
375
|
+
name: WindowName,
|
|
376
|
+
expression: Expression[Value],
|
|
377
|
+
) -> WindowFunction[Value | None]:
|
|
378
|
+
return WindowFunction[Value | None](
|
|
379
|
+
WindowFunctionNode(name, (expression.node,)), expression.family
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _offset(
|
|
384
|
+
name: WindowName,
|
|
385
|
+
expression: Expression[Value],
|
|
386
|
+
offset: int,
|
|
387
|
+
) -> WindowFunction[Value | None]:
|
|
388
|
+
if offset < 1:
|
|
389
|
+
message = f"{name.value.replace('-', ' ')} requires a positive offset"
|
|
390
|
+
raise ValueError(message)
|
|
391
|
+
arguments = (expression.node, operand(offset, ScalarFamily.NUMERIC))
|
|
392
|
+
return WindowFunction[Value | None](
|
|
393
|
+
WindowFunctionNode(name, arguments), expression.family
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _ranking(name: WindowName) -> WindowFunction[int]:
|
|
398
|
+
return WindowFunction[int](WindowFunctionNode(name), ScalarFamily.NUMERIC)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
__all__ = (
|
|
402
|
+
"FrameEdge",
|
|
403
|
+
"FrameExclusion",
|
|
404
|
+
"Window",
|
|
405
|
+
"WindowFunction",
|
|
406
|
+
"WindowSpecification",
|
|
407
|
+
"cume_dist",
|
|
408
|
+
"current_row",
|
|
409
|
+
"dense_rank",
|
|
410
|
+
"first_value",
|
|
411
|
+
"following",
|
|
412
|
+
"lag",
|
|
413
|
+
"last_value",
|
|
414
|
+
"lead",
|
|
415
|
+
"named_window",
|
|
416
|
+
"nth_value",
|
|
417
|
+
"ntile",
|
|
418
|
+
"percent_rank",
|
|
419
|
+
"preceding",
|
|
420
|
+
"rank",
|
|
421
|
+
"row_number",
|
|
422
|
+
"unbounded_following",
|
|
423
|
+
"unbounded_preceding",
|
|
424
|
+
)
|