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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Dialect identifier quoting and qualification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import UnsupportedQueryError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class IdentifierQuoting:
|
|
12
|
+
dialect: str
|
|
13
|
+
delimiter: str = '"'
|
|
14
|
+
allow_catalog: bool = False
|
|
15
|
+
|
|
16
|
+
def quote(self, value: str) -> str:
|
|
17
|
+
escaped = value.replace(self.delimiter, self.delimiter * 2)
|
|
18
|
+
return f"{self.delimiter}{escaped}{self.delimiter}"
|
|
19
|
+
|
|
20
|
+
def qualified(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
/,
|
|
24
|
+
*,
|
|
25
|
+
table: str | None = None,
|
|
26
|
+
schema: str | None = None,
|
|
27
|
+
catalog: str | None = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
if catalog is not None and not self.allow_catalog:
|
|
30
|
+
message = f"{self.dialect} does not support catalog-qualified identifiers"
|
|
31
|
+
raise UnsupportedQueryError(message)
|
|
32
|
+
parts = tuple(
|
|
33
|
+
self.quote(part)
|
|
34
|
+
for part in (catalog, schema, table, name)
|
|
35
|
+
if part is not None
|
|
36
|
+
)
|
|
37
|
+
return ".".join(parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ("IdentifierQuoting",)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shared projection rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from pyoq.query.nodes import ExpressionNode, WindowSpecificationNode
|
|
8
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
9
|
+
from pyoq.query.rendering.sources import OrderTextRenderer, QueryTextRenderer
|
|
10
|
+
from pyoq.query.select_nodes import JsonElementsSourceNode, ProjectionNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExpressionText(Protocol):
|
|
14
|
+
def compile(self, node: ExpressionNode) -> str: ...
|
|
15
|
+
|
|
16
|
+
def compile_window_specification(self, node: WindowSpecificationNode) -> str:
|
|
17
|
+
"""How a window divides and orders the rows it covers."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def render_json_elements(self, node: JsonElementsSourceNode) -> str:
|
|
21
|
+
"""A JSON array read as rows."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def routine_name(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
schema: str | None = None,
|
|
28
|
+
catalog: str | None = None,
|
|
29
|
+
) -> str:
|
|
30
|
+
"""A routine's name as this dialect writes it."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def declares_windows(self, names: frozenset[str], /) -> frozenset[str]:
|
|
34
|
+
"""The windows named by the query being written, and the ones before it."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def reads_queries_with(self, render_query: QueryTextRenderer, /) -> None:
|
|
38
|
+
"""How to write a query that appears inside an expression."""
|
|
39
|
+
|
|
40
|
+
def orders_rows_with(self, render_order: OrderTextRenderer, /) -> None:
|
|
41
|
+
"""How to write one term of an ORDER BY, direction and nulls included."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compile_projection(
|
|
45
|
+
expressions: ExpressionText,
|
|
46
|
+
node: ProjectionNode,
|
|
47
|
+
quoting: IdentifierQuoting,
|
|
48
|
+
) -> str:
|
|
49
|
+
rendered = expressions.compile(node.expression)
|
|
50
|
+
if node.alias is None:
|
|
51
|
+
return rendered
|
|
52
|
+
return f"{rendered} AS {quoting.quote(node.alias)}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def compile_projections(
|
|
56
|
+
expressions: ExpressionText,
|
|
57
|
+
nodes: tuple[ProjectionNode, ...],
|
|
58
|
+
quoting: IdentifierQuoting,
|
|
59
|
+
) -> str:
|
|
60
|
+
return ", ".join(compile_projection(expressions, node, quoting) for node in nodes)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ("ExpressionText", "compile_projection", "compile_projections")
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Shared SELECT and compound query rendering with dialect hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from pyoq.errors import CompilationError, UnsupportedQueryError
|
|
9
|
+
from pyoq.query.nodes import FieldNode, NamedWindowNode
|
|
10
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
11
|
+
from pyoq.query.rendering.projections import ExpressionText, compile_projection
|
|
12
|
+
from pyoq.query.rendering.sources import SourceRenderer
|
|
13
|
+
from pyoq.query.select_nodes import (
|
|
14
|
+
CommonTableNode,
|
|
15
|
+
JoinKind,
|
|
16
|
+
JoinNode,
|
|
17
|
+
LockNode,
|
|
18
|
+
LockStrength,
|
|
19
|
+
LockWait,
|
|
20
|
+
NullOrder,
|
|
21
|
+
OrderDirection,
|
|
22
|
+
OrderNode,
|
|
23
|
+
QueryNode,
|
|
24
|
+
SelectNode,
|
|
25
|
+
SetNode,
|
|
26
|
+
SetOperator,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_LOCK_STRENGTHS: Mapping[LockStrength, str] = {
|
|
30
|
+
LockStrength.UPDATE: "UPDATE",
|
|
31
|
+
LockStrength.NO_KEY_UPDATE: "NO KEY UPDATE",
|
|
32
|
+
LockStrength.SHARE: "SHARE",
|
|
33
|
+
LockStrength.KEY_SHARE: "KEY SHARE",
|
|
34
|
+
}
|
|
35
|
+
"""Verified against a running PostgreSQL server, which has all four."""
|
|
36
|
+
|
|
37
|
+
_LOCK_WAITS: Mapping[LockWait, str] = {
|
|
38
|
+
LockWait.NOWAIT: "NOWAIT",
|
|
39
|
+
LockWait.SKIP_LOCKED: "SKIP LOCKED",
|
|
40
|
+
}
|
|
41
|
+
"""Waiting is what a lock does unless it is told otherwise, so it is unwritten."""
|
|
42
|
+
|
|
43
|
+
_ORDER_DIRECTIONS = {
|
|
44
|
+
OrderDirection.ASCENDING: "ASC",
|
|
45
|
+
OrderDirection.DESCENDING: "DESC",
|
|
46
|
+
}
|
|
47
|
+
_NULL_ORDERS = {
|
|
48
|
+
NullOrder.FIRST: "FIRST",
|
|
49
|
+
NullOrder.LAST: "LAST",
|
|
50
|
+
}
|
|
51
|
+
_SET_OPERATORS = {
|
|
52
|
+
SetOperator.UNION: "UNION",
|
|
53
|
+
SetOperator.UNION_ALL: "UNION ALL",
|
|
54
|
+
SetOperator.INTERSECT: "INTERSECT",
|
|
55
|
+
SetOperator.EXCEPT: "EXCEPT",
|
|
56
|
+
}
|
|
57
|
+
_JOIN_NAMES = {
|
|
58
|
+
JoinKind.INNER: "INNER JOIN",
|
|
59
|
+
JoinKind.LEFT: "LEFT JOIN",
|
|
60
|
+
JoinKind.RIGHT: "RIGHT JOIN",
|
|
61
|
+
JoinKind.FULL: "FULL JOIN",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_NATURAL_NAMES = {
|
|
65
|
+
JoinKind.INNER: "NATURAL JOIN",
|
|
66
|
+
JoinKind.LEFT: "NATURAL LEFT JOIN",
|
|
67
|
+
JoinKind.RIGHT: "NATURAL RIGHT JOIN",
|
|
68
|
+
JoinKind.FULL: "NATURAL FULL JOIN",
|
|
69
|
+
}
|
|
70
|
+
"""A natural join is inner unless it says otherwise, and reads that way."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class QueryRenderer(ABC):
|
|
74
|
+
__slots__ = ("_expressions", "_sources")
|
|
75
|
+
|
|
76
|
+
def __init__(self, expressions: ExpressionText) -> None:
|
|
77
|
+
self._expressions = expressions
|
|
78
|
+
self._sources = SourceRenderer(
|
|
79
|
+
self.compile,
|
|
80
|
+
self.quoting,
|
|
81
|
+
self.require_lateral_sources,
|
|
82
|
+
expressions.render_json_elements,
|
|
83
|
+
)
|
|
84
|
+
expressions.reads_queries_with(self.compile)
|
|
85
|
+
expressions.orders_rows_with(self._compile_order)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def dialect(self) -> str: ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def quoting(self) -> IdentifierQuoting: ...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def require_join_capability(self, kind: JoinKind) -> None: ...
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def require_recursive_common_tables(self) -> None: ...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def require_lateral_sources(self) -> None: ...
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def append_pagination(
|
|
106
|
+
self,
|
|
107
|
+
fragments: list[str],
|
|
108
|
+
limit: int | None,
|
|
109
|
+
offset: int | None,
|
|
110
|
+
) -> None: ...
|
|
111
|
+
|
|
112
|
+
def compile(self, node: QueryNode) -> str:
|
|
113
|
+
if isinstance(node, SelectNode):
|
|
114
|
+
return self._compile_select(node)
|
|
115
|
+
return self._compile_set(node)
|
|
116
|
+
|
|
117
|
+
def _compile_select(self, node: SelectNode) -> str:
|
|
118
|
+
previous = self._expressions.declares_windows(
|
|
119
|
+
frozenset(declared.name for declared in node.windows)
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
return self._compile_select_clauses(node)
|
|
123
|
+
finally:
|
|
124
|
+
self._expressions.declares_windows(previous)
|
|
125
|
+
|
|
126
|
+
def _compile_select_clauses(self, node: SelectNode) -> str:
|
|
127
|
+
fragments: list[str] = []
|
|
128
|
+
if node.common_tables:
|
|
129
|
+
fragments.append(self._compile_common_tables(node.common_tables))
|
|
130
|
+
projections = (
|
|
131
|
+
"*"
|
|
132
|
+
if node.every_column
|
|
133
|
+
else ", ".join(
|
|
134
|
+
compile_projection(self._expressions, projection, self.quoting)
|
|
135
|
+
for projection in node.projections
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
distinct = " DISTINCT" if node.distinct else ""
|
|
139
|
+
fragments.append(f"SELECT{distinct} {projections}")
|
|
140
|
+
if node.source is not None:
|
|
141
|
+
fragments.append(f"FROM {self._sources.compile(node.source)}")
|
|
142
|
+
fragments.extend(self._compile_join(join) for join in node.joins)
|
|
143
|
+
where = self._compile_where(node)
|
|
144
|
+
if where is not None:
|
|
145
|
+
fragments.append(where)
|
|
146
|
+
if node.groups:
|
|
147
|
+
groups = ", ".join(
|
|
148
|
+
self._expressions.compile(group) for group in node.groups
|
|
149
|
+
)
|
|
150
|
+
fragments.append(f"GROUP BY {groups}")
|
|
151
|
+
if node.having is not None:
|
|
152
|
+
fragments.append(f"HAVING {self._expressions.compile(node.having)}")
|
|
153
|
+
if node.windows:
|
|
154
|
+
fragments.append(self._compile_windows(node.windows))
|
|
155
|
+
self._append_ordering(fragments, node.orders)
|
|
156
|
+
self.append_pagination(fragments, node.limit, node.offset)
|
|
157
|
+
if node.lock is not None:
|
|
158
|
+
fragments.append(self.locking(node.lock))
|
|
159
|
+
return " ".join(fragments)
|
|
160
|
+
|
|
161
|
+
def _compile_windows(self, nodes: tuple[NamedWindowNode, ...]) -> str:
|
|
162
|
+
"""The windows this query names, written once for every reader."""
|
|
163
|
+
declarations = ", ".join(
|
|
164
|
+
f"{declared.name} AS "
|
|
165
|
+
f"({self._expressions.compile_window_specification(declared.specification)})"
|
|
166
|
+
for declared in nodes
|
|
167
|
+
)
|
|
168
|
+
return f"WINDOW {declarations}"
|
|
169
|
+
|
|
170
|
+
def _compile_set(self, node: SetNode) -> str:
|
|
171
|
+
left = self._compile_compound_operand(node.left)
|
|
172
|
+
right = self._compile_compound_operand(node.right)
|
|
173
|
+
operator = _SET_OPERATORS.get(node.operator)
|
|
174
|
+
if operator is None:
|
|
175
|
+
message = f"unsupported {self.dialect} set operator: {node.operator}"
|
|
176
|
+
raise UnsupportedQueryError(message)
|
|
177
|
+
fragments = [left, operator, right]
|
|
178
|
+
self._append_set_ordering(fragments, node.orders)
|
|
179
|
+
self.append_pagination(fragments, node.limit, node.offset)
|
|
180
|
+
return " ".join(fragments)
|
|
181
|
+
|
|
182
|
+
def _compile_compound_operand(self, node: QueryNode) -> str:
|
|
183
|
+
if isinstance(node, SelectNode) and _is_plain_compound_operand(node):
|
|
184
|
+
return self.compile(node)
|
|
185
|
+
nested = f"({self.compile(node)})"
|
|
186
|
+
return " ".join(("SELECT", "*", "FROM", nested))
|
|
187
|
+
|
|
188
|
+
def _compile_where(self, node: SelectNode) -> str | None:
|
|
189
|
+
"""The condition and whatever the semi joins ask, joined by AND."""
|
|
190
|
+
parts = [
|
|
191
|
+
self._expressions.compile(expression)
|
|
192
|
+
for expression in (node.condition, *node.matches)
|
|
193
|
+
if expression is not None
|
|
194
|
+
]
|
|
195
|
+
if not parts:
|
|
196
|
+
return None
|
|
197
|
+
return "WHERE " + " AND ".join(parts)
|
|
198
|
+
|
|
199
|
+
def _compile_join(self, node: JoinNode) -> str:
|
|
200
|
+
self.require_join_capability(node.kind)
|
|
201
|
+
source = self._sources.compile(node.source)
|
|
202
|
+
if node.kind is JoinKind.CROSS:
|
|
203
|
+
return f"CROSS JOIN {source}"
|
|
204
|
+
join_name = _JOIN_NAMES.get(node.kind)
|
|
205
|
+
if join_name is None:
|
|
206
|
+
message = f"unsupported {self.dialect} join kind: {node.kind}"
|
|
207
|
+
raise UnsupportedQueryError(message)
|
|
208
|
+
if node.natural:
|
|
209
|
+
return f"{_NATURAL_NAMES[node.kind]} {source}"
|
|
210
|
+
if node.using:
|
|
211
|
+
columns = ", ".join(self.quoting.quote(name) for name in node.using)
|
|
212
|
+
return f"{join_name} {source} USING ({columns})"
|
|
213
|
+
if node.condition is None:
|
|
214
|
+
message = f"qualified {self.dialect} join requires an ON condition"
|
|
215
|
+
raise CompilationError(message)
|
|
216
|
+
condition = self._expressions.compile(node.condition)
|
|
217
|
+
return f"{join_name} {source} ON {condition}"
|
|
218
|
+
|
|
219
|
+
def _compile_common_tables(
|
|
220
|
+
self,
|
|
221
|
+
common_tables: tuple[CommonTableNode, ...],
|
|
222
|
+
) -> str:
|
|
223
|
+
recursive = any(common_table.recursive for common_table in common_tables)
|
|
224
|
+
if recursive:
|
|
225
|
+
self.require_recursive_common_tables()
|
|
226
|
+
keyword = "WITH RECURSIVE" if recursive else "WITH"
|
|
227
|
+
definitions = ", ".join(
|
|
228
|
+
self._compile_common_table(common_table) for common_table in common_tables
|
|
229
|
+
)
|
|
230
|
+
return f"{keyword} {definitions}"
|
|
231
|
+
|
|
232
|
+
def _compile_common_table(self, node: CommonTableNode) -> str:
|
|
233
|
+
columns = ""
|
|
234
|
+
if node.columns:
|
|
235
|
+
rendered = ", ".join(self.quoting.quote(column) for column in node.columns)
|
|
236
|
+
columns = f" ({rendered})"
|
|
237
|
+
name = self.quoting.quote(node.name)
|
|
238
|
+
return f"{name}{columns} AS ({self.compile(node.query)})"
|
|
239
|
+
|
|
240
|
+
def _append_ordering(
|
|
241
|
+
self,
|
|
242
|
+
fragments: list[str],
|
|
243
|
+
orders: tuple[OrderNode, ...],
|
|
244
|
+
) -> None:
|
|
245
|
+
if not orders:
|
|
246
|
+
return
|
|
247
|
+
rendered = ", ".join(self._compile_order(order) for order in orders)
|
|
248
|
+
fragments.append(f"ORDER BY {rendered}")
|
|
249
|
+
|
|
250
|
+
def _append_set_ordering(
|
|
251
|
+
self,
|
|
252
|
+
fragments: list[str],
|
|
253
|
+
orders: tuple[OrderNode, ...],
|
|
254
|
+
) -> None:
|
|
255
|
+
if not orders:
|
|
256
|
+
return
|
|
257
|
+
rendered = ", ".join(self._compile_set_order(order) for order in orders)
|
|
258
|
+
fragments.append(f"ORDER BY {rendered}")
|
|
259
|
+
|
|
260
|
+
def locking(self, node: LockNode) -> str:
|
|
261
|
+
"""How this dialect holds the rows a statement reads.
|
|
262
|
+
|
|
263
|
+
A dialect that cannot hold rows at all says so, because returning
|
|
264
|
+
them unlocked would answer a question nobody asked.
|
|
265
|
+
"""
|
|
266
|
+
strength = self.lock_strength_sql(node.strength)
|
|
267
|
+
clauses = [f"FOR {strength}"]
|
|
268
|
+
if node.tables:
|
|
269
|
+
names = ", ".join(self.quoting.quote(table) for table in node.tables)
|
|
270
|
+
clauses.append(f"OF {names}")
|
|
271
|
+
wait = _LOCK_WAITS.get(node.wait)
|
|
272
|
+
if wait is not None:
|
|
273
|
+
clauses.append(wait)
|
|
274
|
+
return " ".join(clauses)
|
|
275
|
+
|
|
276
|
+
def lock_strength_sql(self, strength: LockStrength) -> str:
|
|
277
|
+
"""What this dialect calls a lock of this strength."""
|
|
278
|
+
name = _LOCK_STRENGTHS.get(strength)
|
|
279
|
+
if name is None:
|
|
280
|
+
message = f"unsupported {self.dialect} lock strength: {strength}"
|
|
281
|
+
raise UnsupportedQueryError(message)
|
|
282
|
+
return name
|
|
283
|
+
|
|
284
|
+
def _compile_order(self, node: OrderNode) -> str:
|
|
285
|
+
return self._ordered(self._expressions.compile(node.expression), node)
|
|
286
|
+
|
|
287
|
+
def _compile_set_order(self, node: OrderNode) -> str:
|
|
288
|
+
"""A set operation orders by what it returned, not by where it came from.
|
|
289
|
+
|
|
290
|
+
Its operands may read different tables, so a table-qualified name means
|
|
291
|
+
nothing once they are combined and a database refuses it. Only a column
|
|
292
|
+
the operation selected can be named here.
|
|
293
|
+
"""
|
|
294
|
+
if not isinstance(node.expression, FieldNode):
|
|
295
|
+
message = (
|
|
296
|
+
f"a {self.dialect} set operation can only be ordered by a "
|
|
297
|
+
f"column it selected"
|
|
298
|
+
)
|
|
299
|
+
raise UnsupportedQueryError(message)
|
|
300
|
+
return self._ordered(self.quoting.quote(node.expression.name), node)
|
|
301
|
+
|
|
302
|
+
def _ordered(self, expression: str, node: OrderNode) -> str:
|
|
303
|
+
direction = _ORDER_DIRECTIONS.get(node.direction)
|
|
304
|
+
if direction is None:
|
|
305
|
+
message = f"unsupported {self.dialect} order direction: {node.direction}"
|
|
306
|
+
raise UnsupportedQueryError(message)
|
|
307
|
+
if node.nulls is None:
|
|
308
|
+
return f"{expression} {direction}"
|
|
309
|
+
return self.null_ordered(expression, direction, node.nulls)
|
|
310
|
+
|
|
311
|
+
def null_ordered(
|
|
312
|
+
self,
|
|
313
|
+
expression: str,
|
|
314
|
+
direction: str,
|
|
315
|
+
nulls: NullOrder,
|
|
316
|
+
) -> str:
|
|
317
|
+
"""Where this dialect puts nulls when it is told where to put them."""
|
|
318
|
+
placement = _NULL_ORDERS.get(nulls)
|
|
319
|
+
if placement is None:
|
|
320
|
+
message = f"unsupported {self.dialect} null order: {nulls}"
|
|
321
|
+
raise UnsupportedQueryError(message)
|
|
322
|
+
return f"{expression} {direction} NULLS {placement}"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _is_plain_compound_operand(node: SelectNode) -> bool:
|
|
326
|
+
return not (
|
|
327
|
+
node.common_tables
|
|
328
|
+
or node.orders
|
|
329
|
+
or node.limit is not None
|
|
330
|
+
or node.offset is not None
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
__all__ = ("QueryRenderer",)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Shared query-source rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
8
|
+
from pyoq.query.select_nodes import (
|
|
9
|
+
JsonElementsSourceNode,
|
|
10
|
+
OrderNode,
|
|
11
|
+
QueryNode,
|
|
12
|
+
SourceNode,
|
|
13
|
+
SubquerySourceNode,
|
|
14
|
+
TableSourceNode,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
QueryTextRenderer = Callable[[QueryNode], str]
|
|
18
|
+
OrderTextRenderer = Callable[[OrderNode], str]
|
|
19
|
+
JsonElementsTextRenderer = Callable[[JsonElementsSourceNode], str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SourceRenderer:
|
|
23
|
+
__slots__ = (
|
|
24
|
+
"_quoting",
|
|
25
|
+
"_render_elements",
|
|
26
|
+
"_render_query",
|
|
27
|
+
"_require_lateral",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
render_query: QueryTextRenderer,
|
|
33
|
+
quoting: IdentifierQuoting,
|
|
34
|
+
require_lateral: Callable[[], None],
|
|
35
|
+
render_elements: JsonElementsTextRenderer,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._render_query = render_query
|
|
38
|
+
self._quoting = quoting
|
|
39
|
+
self._require_lateral = require_lateral
|
|
40
|
+
self._render_elements = render_elements
|
|
41
|
+
|
|
42
|
+
def compile(self, node: SourceNode) -> str:
|
|
43
|
+
if isinstance(node, TableSourceNode):
|
|
44
|
+
return self._compile_table(node)
|
|
45
|
+
if isinstance(node, JsonElementsSourceNode):
|
|
46
|
+
return self._render_elements(node)
|
|
47
|
+
if isinstance(node, SubquerySourceNode):
|
|
48
|
+
query = self._render_query(node.query)
|
|
49
|
+
if node.lateral:
|
|
50
|
+
self._require_lateral()
|
|
51
|
+
lateral = "LATERAL " if node.lateral else ""
|
|
52
|
+
return f"{lateral}({query}) AS {self._quoting.quote(node.alias)}"
|
|
53
|
+
return self._quoting.quote(node.name)
|
|
54
|
+
|
|
55
|
+
def _compile_table(self, node: TableSourceNode) -> str:
|
|
56
|
+
table = self._quoting.qualified(
|
|
57
|
+
node.name,
|
|
58
|
+
schema=node.schema,
|
|
59
|
+
catalog=node.catalog,
|
|
60
|
+
)
|
|
61
|
+
if node.alias is None:
|
|
62
|
+
return table
|
|
63
|
+
return f"{table} AS {self._quoting.quote(node.alias)}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ("JsonElementsTextRenderer", "QueryTextRenderer", "SourceRenderer")
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Shared write statement rendering with dialect hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from pyoq.errors import CompilationError
|
|
8
|
+
from pyoq.query.execution import StatementKind
|
|
9
|
+
from pyoq.query.nodes import ExpressionNode
|
|
10
|
+
from pyoq.query.rendering.identifiers import IdentifierQuoting
|
|
11
|
+
from pyoq.query.rendering.projections import ExpressionText, compile_projections
|
|
12
|
+
from pyoq.query.select_nodes import ProjectionNode
|
|
13
|
+
from pyoq.query.write_nodes import (
|
|
14
|
+
AssignmentNode,
|
|
15
|
+
CallNode,
|
|
16
|
+
ConflictAction,
|
|
17
|
+
ConflictNode,
|
|
18
|
+
DeleteNode,
|
|
19
|
+
InsertNode,
|
|
20
|
+
UpdateNode,
|
|
21
|
+
WriteNode,
|
|
22
|
+
WriteTargetNode,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WriteRenderer(ABC):
|
|
27
|
+
__slots__ = ("_expressions",)
|
|
28
|
+
|
|
29
|
+
def __init__(self, expressions: ExpressionText) -> None:
|
|
30
|
+
self._expressions = expressions
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def quoting(self) -> IdentifierQuoting: ...
|
|
35
|
+
|
|
36
|
+
def qualified_target(self, node: WriteTargetNode) -> str:
|
|
37
|
+
return self.quoting.qualified(
|
|
38
|
+
node.name,
|
|
39
|
+
schema=node.schema,
|
|
40
|
+
catalog=node.catalog,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def require_returning(self) -> None: ...
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def require_conflict_resolution(self) -> None: ...
|
|
48
|
+
|
|
49
|
+
def compile(self, node: WriteNode) -> tuple[str, StatementKind]:
|
|
50
|
+
if isinstance(node, InsertNode):
|
|
51
|
+
return self._compile_insert(node), StatementKind.INSERT
|
|
52
|
+
if isinstance(node, UpdateNode):
|
|
53
|
+
return self._compile_update(node), StatementKind.UPDATE
|
|
54
|
+
return self._compile_delete(node), StatementKind.DELETE
|
|
55
|
+
|
|
56
|
+
def compile_call(self, node: CallNode) -> str:
|
|
57
|
+
"""How this dialect runs a stored procedure.
|
|
58
|
+
|
|
59
|
+
A dialect that keeps no procedures says so, because there is nothing
|
|
60
|
+
for it to run and nothing to fall back on.
|
|
61
|
+
"""
|
|
62
|
+
arguments = ", ".join(
|
|
63
|
+
self._expressions.compile(argument) for argument in node.arguments
|
|
64
|
+
)
|
|
65
|
+
name = self._expressions.routine_name(node.name, node.schema, node.catalog)
|
|
66
|
+
return f"CALL {name}({arguments})"
|
|
67
|
+
|
|
68
|
+
def _compile_insert(self, node: InsertNode) -> str:
|
|
69
|
+
if not node.rows:
|
|
70
|
+
message = "INSERT requires at least one row of values"
|
|
71
|
+
raise CompilationError(message)
|
|
72
|
+
columns = ", ".join(self.quoting.quote(column) for column in node.columns)
|
|
73
|
+
fragments = [
|
|
74
|
+
"INSERT INTO",
|
|
75
|
+
self.qualified_target(node.target),
|
|
76
|
+
f"({columns})",
|
|
77
|
+
"VALUES",
|
|
78
|
+
", ".join(self._compile_row(row) for row in node.rows),
|
|
79
|
+
]
|
|
80
|
+
alias = self.insert_row_alias(node)
|
|
81
|
+
if alias is not None:
|
|
82
|
+
fragments.append(f"AS {self.quoting.quote(alias)}")
|
|
83
|
+
if node.conflict is not None:
|
|
84
|
+
fragments.append(self.render_conflict(node.conflict))
|
|
85
|
+
self._append_returning(fragments, node.returning)
|
|
86
|
+
return " ".join(fragments)
|
|
87
|
+
|
|
88
|
+
def _compile_update(self, node: UpdateNode) -> str:
|
|
89
|
+
if not node.assignments:
|
|
90
|
+
message = "UPDATE requires at least one assignment"
|
|
91
|
+
raise CompilationError(message)
|
|
92
|
+
assignments = ", ".join(
|
|
93
|
+
self._compile_assignment(assignment) for assignment in node.assignments
|
|
94
|
+
)
|
|
95
|
+
fragments = ["UPDATE", self.qualified_target(node.target), "SET", assignments]
|
|
96
|
+
self._append_scope(fragments, node.condition, node.unconditional, "UPDATE")
|
|
97
|
+
self._append_returning(fragments, node.returning)
|
|
98
|
+
return " ".join(fragments)
|
|
99
|
+
|
|
100
|
+
def _compile_delete(self, node: DeleteNode) -> str:
|
|
101
|
+
fragments = ["DELETE FROM", self.qualified_target(node.target)]
|
|
102
|
+
self._append_scope(fragments, node.condition, node.unconditional, "DELETE")
|
|
103
|
+
self._append_returning(fragments, node.returning)
|
|
104
|
+
return " ".join(fragments)
|
|
105
|
+
|
|
106
|
+
def insert_row_alias(self, node: InsertNode) -> str | None:
|
|
107
|
+
del node
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def compile_assignment(self, node: AssignmentNode) -> str:
|
|
111
|
+
return self._compile_assignment(node)
|
|
112
|
+
|
|
113
|
+
def render_conflict(self, node: ConflictNode) -> str:
|
|
114
|
+
self.require_conflict_resolution()
|
|
115
|
+
if node.action is ConflictAction.UPDATE and not node.targets:
|
|
116
|
+
message = "conflict resolution that updates requires a conflict target"
|
|
117
|
+
raise CompilationError(message)
|
|
118
|
+
fragments = ["ON CONFLICT"]
|
|
119
|
+
if node.targets:
|
|
120
|
+
targets = ", ".join(self.quoting.quote(target) for target in node.targets)
|
|
121
|
+
fragments.append(f"({targets})")
|
|
122
|
+
if node.action is ConflictAction.NOTHING:
|
|
123
|
+
fragments.append("DO NOTHING")
|
|
124
|
+
return " ".join(fragments)
|
|
125
|
+
if not node.assignments:
|
|
126
|
+
message = "conflict resolution that updates requires an assignment"
|
|
127
|
+
raise CompilationError(message)
|
|
128
|
+
assignments = ", ".join(
|
|
129
|
+
self._compile_assignment(assignment) for assignment in node.assignments
|
|
130
|
+
)
|
|
131
|
+
fragments.extend(("DO UPDATE SET", assignments))
|
|
132
|
+
if node.condition is not None:
|
|
133
|
+
fragments.append("WHERE")
|
|
134
|
+
fragments.append(self._expressions.compile(node.condition))
|
|
135
|
+
return " ".join(fragments)
|
|
136
|
+
|
|
137
|
+
def _compile_assignment(self, node: AssignmentNode) -> str:
|
|
138
|
+
value = self._expressions.compile(node.value)
|
|
139
|
+
return f"{self.quoting.quote(node.column)} = {value}"
|
|
140
|
+
|
|
141
|
+
def _compile_row(self, row: tuple[ExpressionNode, ...]) -> str:
|
|
142
|
+
rendered = ", ".join(self._expressions.compile(value) for value in row)
|
|
143
|
+
return f"({rendered})"
|
|
144
|
+
|
|
145
|
+
def _append_scope(
|
|
146
|
+
self,
|
|
147
|
+
fragments: list[str],
|
|
148
|
+
condition: ExpressionNode | None,
|
|
149
|
+
unconditional: bool,
|
|
150
|
+
clause: str,
|
|
151
|
+
) -> None:
|
|
152
|
+
if condition is not None:
|
|
153
|
+
fragments.append("WHERE")
|
|
154
|
+
fragments.append(self._expressions.compile(condition))
|
|
155
|
+
return
|
|
156
|
+
if not unconditional:
|
|
157
|
+
message = (
|
|
158
|
+
f"{clause} without a WHERE condition must opt in through all_rows()"
|
|
159
|
+
)
|
|
160
|
+
raise CompilationError(message)
|
|
161
|
+
|
|
162
|
+
def _append_returning(
|
|
163
|
+
self,
|
|
164
|
+
fragments: list[str],
|
|
165
|
+
returning: tuple[ProjectionNode, ...],
|
|
166
|
+
) -> None:
|
|
167
|
+
if not returning:
|
|
168
|
+
return
|
|
169
|
+
self.require_returning()
|
|
170
|
+
fragments.append("RETURNING")
|
|
171
|
+
fragments.append(
|
|
172
|
+
compile_projections(self._expressions, returning, self.quoting)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = ("WriteRenderer",)
|