snake-orm 0.1.0b1__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.
- snake_orm-0.1.0b1.dist-info/METADATA +366 -0
- snake_orm-0.1.0b1.dist-info/RECORD +198 -0
- snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
- snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
- snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
- snakeorm/__init__.py +214 -0
- snakeorm/advisor.py +109 -0
- snakeorm/cli/__init__.py +3 -0
- snakeorm/cli/app.py +992 -0
- snakeorm/cli/discovery.py +179 -0
- snakeorm/cli/hooks.py +86 -0
- snakeorm/compiler/__init__.py +3 -0
- snakeorm/compiler/compiler.py +330 -0
- snakeorm/connection.py +228 -0
- snakeorm/contrib/__init__.py +17 -0
- snakeorm/contrib/asgi.py +192 -0
- snakeorm/contrib/config.py +264 -0
- snakeorm/contrib/deliver.py +256 -0
- snakeorm/contrib/django.py +245 -0
- snakeorm/contrib/sidecar.py +44 -0
- snakeorm/contrib/wsgi.py +176 -0
- snakeorm/core/__init__.py +1 -0
- snakeorm/core/config.py +173 -0
- snakeorm/core/converters.py +172 -0
- snakeorm/core/exceptions.py +175 -0
- snakeorm/core/placement.py +15 -0
- snakeorm/core/sentinels.py +37 -0
- snakeorm/core/signals.py +114 -0
- snakeorm/debug/__init__.py +42 -0
- snakeorm/debug/assets/css/panel.css +616 -0
- snakeorm/debug/assets/css/snake_orm_app.css +293 -0
- snakeorm/debug/assets/img/icon.svg +5 -0
- snakeorm/debug/assets/js/history.js +425 -0
- snakeorm/debug/assets/js/language.js +283 -0
- snakeorm/debug/assets/js/panel.js +241 -0
- snakeorm/debug/assets/js/snake_orm_app.js +82 -0
- snakeorm/debug/assets/pages/cli.html +28 -0
- snakeorm/debug/assets/pages/config.html +34 -0
- snakeorm/debug/assets/pages/database_first.html +29 -0
- snakeorm/debug/assets/pages/help.html +21 -0
- snakeorm/debug/assets/pages/history.html +27 -0
- snakeorm/debug/capture.py +234 -0
- snakeorm/debug/channel.py +131 -0
- snakeorm/debug/collector.py +144 -0
- snakeorm/debug/config.py +149 -0
- snakeorm/debug/html.py +578 -0
- snakeorm/debug/matrix.py +60 -0
- snakeorm/debug/origin.py +42 -0
- snakeorm/debug/otel/__init__.py +68 -0
- snakeorm/debug/otel/context.py +43 -0
- snakeorm/debug/otel/exporter.py +277 -0
- snakeorm/debug/otel/payload.py +97 -0
- snakeorm/debug/otel/report.py +29 -0
- snakeorm/debug/otel/spans.py +393 -0
- snakeorm/debug/otel/summary.py +59 -0
- snakeorm/debug/record.py +54 -0
- snakeorm/debug/report.py +311 -0
- snakeorm/debug/testing.py +23 -0
- snakeorm/decorators/__init__.py +13 -0
- snakeorm/decorators/abstract.py +26 -0
- snakeorm/decorators/db_first.py +104 -0
- snakeorm/decorators/function.py +28 -0
- snakeorm/decorators/model.py +268 -0
- snakeorm/decorators/polymorphic.py +140 -0
- snakeorm/decorators/result.py +167 -0
- snakeorm/decorators/row.py +90 -0
- snakeorm/decorators/trigger.py +47 -0
- snakeorm/decorators/view.py +149 -0
- snakeorm/dialects/__init__.py +6 -0
- snakeorm/dialects/base.py +373 -0
- snakeorm/dialects/capabilities.py +571 -0
- snakeorm/dialects/literals.py +46 -0
- snakeorm/dialects/matrix.py +384 -0
- snakeorm/dialects/mysql.py +519 -0
- snakeorm/dialects/postgres.py +504 -0
- snakeorm/dialects/sqlite.py +417 -0
- snakeorm/drivers/__init__.py +18 -0
- snakeorm/drivers/asyncbase.py +73 -0
- snakeorm/drivers/asyncdecorators.py +244 -0
- snakeorm/drivers/asyncpool.py +228 -0
- snakeorm/drivers/asyncpsycopg.py +124 -0
- snakeorm/drivers/asyncpymysql.py +50 -0
- snakeorm/drivers/asyncsqlite.py +30 -0
- snakeorm/drivers/base.py +76 -0
- snakeorm/drivers/failures.py +181 -0
- snakeorm/drivers/logging.py +167 -0
- snakeorm/drivers/pool.py +352 -0
- snakeorm/drivers/psycopg.py +188 -0
- snakeorm/drivers/pymysql.py +176 -0
- snakeorm/drivers/savepoints.py +46 -0
- snakeorm/drivers/sqlite.py +170 -0
- snakeorm/drivers/threaded.py +143 -0
- snakeorm/drivers/timeout.py +87 -0
- snakeorm/dto/__init__.py +55 -0
- snakeorm/dto/read.py +231 -0
- snakeorm/dto/region.py +326 -0
- snakeorm/dto/resolve.py +354 -0
- snakeorm/dto/spec.py +201 -0
- snakeorm/expressions/__init__.py +88 -0
- snakeorm/expressions/conditional.py +178 -0
- snakeorm/expressions/expression.py +810 -0
- snakeorm/expressions/functions.py +71 -0
- snakeorm/expressions/keys.py +299 -0
- snakeorm/expressions/paths.py +43 -0
- snakeorm/expressions/scalar.py +293 -0
- snakeorm/expressions/window.py +248 -0
- snakeorm/fields/__init__.py +68 -0
- snakeorm/fields/check.py +79 -0
- snakeorm/fields/column.py +316 -0
- snakeorm/fields/enum.py +64 -0
- snakeorm/fields/index.py +46 -0
- snakeorm/fields/relationship.py +738 -0
- snakeorm/fields/typed.py +736 -0
- snakeorm/helpers/__init__.py +1 -0
- snakeorm/helpers/annotations.py +21 -0
- snakeorm/helpers/inheritance.py +41 -0
- snakeorm/helpers/pyliteral.py +52 -0
- snakeorm/helpers/pytype.py +50 -0
- snakeorm/introspection/__init__.py +13 -0
- snakeorm/introspection/base.py +30 -0
- snakeorm/introspection/drift.py +78 -0
- snakeorm/introspection/models.py +633 -0
- snakeorm/introspection/mysql.py +367 -0
- snakeorm/introspection/postgres.py +397 -0
- snakeorm/introspection/sqlite.py +217 -0
- snakeorm/introspection/unsupported.py +94 -0
- snakeorm/linker/__init__.py +3 -0
- snakeorm/linker/linker.py +521 -0
- snakeorm/metadata/__init__.py +41 -0
- snakeorm/metadata/check.py +37 -0
- snakeorm/metadata/column.py +201 -0
- snakeorm/metadata/enum_storage.py +33 -0
- snakeorm/metadata/fk_action.py +19 -0
- snakeorm/metadata/foreign_key.py +26 -0
- snakeorm/metadata/index.py +43 -0
- snakeorm/metadata/index_method.py +19 -0
- snakeorm/metadata/int_size.py +20 -0
- snakeorm/metadata/json_storage.py +21 -0
- snakeorm/metadata/polymorphic.py +30 -0
- snakeorm/metadata/primary_key.py +23 -0
- snakeorm/metadata/relationship.py +60 -0
- snakeorm/metadata/relationship_kind.py +43 -0
- snakeorm/metadata/routine.py +26 -0
- snakeorm/metadata/server_default.py +20 -0
- snakeorm/metadata/table.py +97 -0
- snakeorm/metadata/table_kind.py +23 -0
- snakeorm/metadata/trigger.py +62 -0
- snakeorm/metadata/type_params.py +348 -0
- snakeorm/migration/__init__.py +88 -0
- snakeorm/migration/asyncrunner.py +168 -0
- snakeorm/migration/autodetect.py +96 -0
- snakeorm/migration/ddl.py +1172 -0
- snakeorm/migration/diff.py +701 -0
- snakeorm/migration/loader.py +77 -0
- snakeorm/migration/operations.py +952 -0
- snakeorm/migration/planner.py +78 -0
- snakeorm/migration/realize.py +420 -0
- snakeorm/migration/renames.py +153 -0
- snakeorm/migration/render.py +1095 -0
- snakeorm/migration/runner.py +314 -0
- snakeorm/migration/squash.py +60 -0
- snakeorm/migration/state.py +107 -0
- snakeorm/model.py +162 -0
- snakeorm/py.typed +0 -0
- snakeorm/query/__init__.py +9 -0
- snakeorm/query/compound.py +288 -0
- snakeorm/query/join_kind.py +19 -0
- snakeorm/query/joined.py +143 -0
- snakeorm/query/query.py +1001 -0
- snakeorm/query/recursive.py +244 -0
- snakeorm/registry/__init__.py +5 -0
- snakeorm/registry/by_module.py +163 -0
- snakeorm/registry/registry.py +166 -0
- snakeorm/session/__init__.py +8 -0
- snakeorm/session/asyncsession.py +638 -0
- snakeorm/session/coercion.py +306 -0
- snakeorm/session/factory.py +29 -0
- snakeorm/session/guards.py +269 -0
- snakeorm/session/isolation.py +23 -0
- snakeorm/session/mapper.py +213 -0
- snakeorm/session/planning.py +815 -0
- snakeorm/session/retry.py +96 -0
- snakeorm/session/session.py +871 -0
- snakeorm/session/shared.py +355 -0
- snakeorm/sql/__init__.py +18 -0
- snakeorm/sql/adapt.py +104 -0
- snakeorm/sql/aggregate.py +232 -0
- snakeorm/sql/condition.py +359 -0
- snakeorm/sql/delete.py +46 -0
- snakeorm/sql/insert.py +149 -0
- snakeorm/sql/joins.py +158 -0
- snakeorm/sql/pk_subquery.py +54 -0
- snakeorm/sql/refs.py +19 -0
- snakeorm/sql/resolver.py +28 -0
- snakeorm/sql/select.py +175 -0
- snakeorm/sql/update.py +82 -0
- snakeorm/sql/value.py +552 -0
- snakeorm/times.py +276 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Declaring TRIGGERS: the signal that lives inside the database.
|
|
2
|
+
|
|
3
|
+
It mirrors `snake_function`: you declare it, it gets registered and the autodetect compares it
|
|
4
|
+
against the history (source of truth: the registry). It does NOT accept a Python callable on
|
|
5
|
+
purpose: the trigger holds even if another process or a `psql` does the writing, because the rule
|
|
6
|
+
lives in the engine. For Python code there is `snake_on`, which only fires if the write goes
|
|
7
|
+
through the session.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from snakeorm.core.placement import DEFAULT_SCHEMA
|
|
15
|
+
from snakeorm.metadata import SnakeTriggerEvent, SnakeTriggerInfo, SnakeTriggerTiming
|
|
16
|
+
from snakeorm.registry import SnakeRegistry
|
|
17
|
+
from snakeorm.registry import registry as default_registry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def snake_trigger(
|
|
21
|
+
*,
|
|
22
|
+
name: str,
|
|
23
|
+
table: str,
|
|
24
|
+
timing: SnakeTriggerTiming,
|
|
25
|
+
events: Sequence[SnakeTriggerEvent],
|
|
26
|
+
body: str,
|
|
27
|
+
schema: str = DEFAULT_SCHEMA,
|
|
28
|
+
for_each_row: bool = True,
|
|
29
|
+
registry: SnakeRegistry = default_registry,
|
|
30
|
+
) -> SnakeTriggerInfo:
|
|
31
|
+
"""Declare and register a desired trigger; return its `SnakeTriggerInfo`.
|
|
32
|
+
|
|
33
|
+
`body` is raw, opaque SQL: the diff only compares it as a string. Redeclaring the same
|
|
34
|
+
`(table, name)` replaces the previous one; the key carries the table because in Postgres the
|
|
35
|
+
name of a trigger is not unique on its own.
|
|
36
|
+
"""
|
|
37
|
+
trigger = SnakeTriggerInfo(
|
|
38
|
+
name=name,
|
|
39
|
+
table=table,
|
|
40
|
+
timing=timing,
|
|
41
|
+
events=tuple(events),
|
|
42
|
+
body=body,
|
|
43
|
+
schema=schema,
|
|
44
|
+
for_each_row=for_each_row,
|
|
45
|
+
)
|
|
46
|
+
registry.register_trigger(trigger)
|
|
47
|
+
return trigger
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""The @snake_view decorator: it maps a database VIEW as a READ-ONLY model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Any, TypeAlias, TypeVar, dataclass_transform
|
|
8
|
+
|
|
9
|
+
from snakeorm.compiler import compile_model
|
|
10
|
+
from snakeorm.decorators.model import _install_dunders, _make_init
|
|
11
|
+
from snakeorm.core.placement import DEFAULT_SCHEMA
|
|
12
|
+
from snakeorm.core.exceptions import SnakeModelDefinitionError
|
|
13
|
+
from snakeorm.fields import (
|
|
14
|
+
snake_auto,
|
|
15
|
+
snake_column,
|
|
16
|
+
snake_datetime,
|
|
17
|
+
snake_datetimetz,
|
|
18
|
+
snake_float,
|
|
19
|
+
snake_time,
|
|
20
|
+
snake_timetz,
|
|
21
|
+
snake_decimal,
|
|
22
|
+
snake_discriminator,
|
|
23
|
+
snake_enum,
|
|
24
|
+
snake_int,
|
|
25
|
+
snake_json,
|
|
26
|
+
snake_str,
|
|
27
|
+
snake_to_many,
|
|
28
|
+
snake_to_many_through,
|
|
29
|
+
snake_to_one,
|
|
30
|
+
)
|
|
31
|
+
from snakeorm.metadata import SnakeTableKind
|
|
32
|
+
from snakeorm.query import SnakeCompound, SnakeQuery
|
|
33
|
+
from snakeorm.registry import SnakeRegistry
|
|
34
|
+
from snakeorm.registry import registry as default_registry
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
|
|
38
|
+
SnakeViewBody: TypeAlias = "SnakeQuery[Any] | SnakeCompound[Any]"
|
|
39
|
+
"""What may DEFINE a view: one SELECT, or a set operation over several.
|
|
40
|
+
|
|
41
|
+
A compound was always executed correctly here —`view_body` only asks the object for `to_sql`, and
|
|
42
|
+
it was measured creating and querying the view on the three engines— and the annotation still said
|
|
43
|
+
`SnakeQuery`, which sent the caller to a `# type: ignore` over something the ORM does properly. A
|
|
44
|
+
view over a `UNION` is half the views there are.
|
|
45
|
+
|
|
46
|
+
`SnakeRecursive` is deliberately NOT here: a `WITH` inside a `CREATE VIEW` is its own question per
|
|
47
|
+
engine, and it has not been measured. What is declared is what has been.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _view_source(sql: str | None, query: SnakeViewBody | None) -> None:
|
|
52
|
+
"""Demand EXACTLY one source for the view definition (both or neither is an error)."""
|
|
53
|
+
if (sql is None) == (query is None):
|
|
54
|
+
raise SnakeModelDefinitionError(
|
|
55
|
+
"@snake_view requires EXACTLY one source for the definition: `sql=` (a raw SELECT) "
|
|
56
|
+
"or `query=` (a SnakeQuery, or a UNION/EXCEPT/INTERSECT of them) — never both, and "
|
|
57
|
+
"never neither."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dependency_name(dep: type, reg: SnakeRegistry) -> str:
|
|
62
|
+
"""Resolve a `depends_on` class to the NAME of its view (we store names, not classes).
|
|
63
|
+
|
|
64
|
+
It only admits OTHER `@snake_view` views (already registered). If it is not registered or is
|
|
65
|
+
not a view, that is an error: a view does not depend on a table through `depends_on` (tables
|
|
66
|
+
are always created first).
|
|
67
|
+
"""
|
|
68
|
+
# It is looked up in the SAME registry where the view is declared: an isolated view of that
|
|
69
|
+
# same store would not be found in the global one, and the error would say "it is not a view"
|
|
70
|
+
# when it very much is.
|
|
71
|
+
table = reg.table_of(dep)
|
|
72
|
+
if table is None or not table.is_view:
|
|
73
|
+
raise SnakeModelDefinitionError(
|
|
74
|
+
f"depends_on on @snake_view only admits OTHER @snake_view views; "
|
|
75
|
+
f"{getattr(dep, '__name__', dep)!r} is not a registered view. A view is always created "
|
|
76
|
+
f"after ALL the tables, so tables are never declared in depends_on."
|
|
77
|
+
)
|
|
78
|
+
return table.name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# A LITERAL tuple as PEP 681 demands; tied to SNAKE_FIELD_SPECIFIERS by a test.
|
|
82
|
+
@dataclass_transform(
|
|
83
|
+
kw_only_default=True,
|
|
84
|
+
field_specifiers=(
|
|
85
|
+
snake_column,
|
|
86
|
+
snake_auto,
|
|
87
|
+
snake_enum,
|
|
88
|
+
snake_int,
|
|
89
|
+
snake_str,
|
|
90
|
+
snake_decimal,
|
|
91
|
+
snake_datetime,
|
|
92
|
+
snake_datetimetz,
|
|
93
|
+
snake_float,
|
|
94
|
+
snake_time,
|
|
95
|
+
snake_timetz,
|
|
96
|
+
snake_json,
|
|
97
|
+
snake_to_one,
|
|
98
|
+
snake_to_many,
|
|
99
|
+
snake_to_many_through,
|
|
100
|
+
snake_discriminator,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
def snake_view(
|
|
104
|
+
*,
|
|
105
|
+
sql: str | None = None,
|
|
106
|
+
query: SnakeViewBody | None = None,
|
|
107
|
+
name: str | None = None,
|
|
108
|
+
schema: str = DEFAULT_SCHEMA,
|
|
109
|
+
depends_on: Sequence[type] = (),
|
|
110
|
+
registry: SnakeRegistry = default_registry,
|
|
111
|
+
) -> Callable[[type[T]], type[T]]:
|
|
112
|
+
"""Compile a `SnakeView` class as a READ-ONLY database VIEW.
|
|
113
|
+
|
|
114
|
+
The same TYPED columns as a model, but the node is marked `kind=SnakeTableKind.VIEW` and it
|
|
115
|
+
holds the SELECT (`view_definition`). Creating/editing/dropping it lives in the migrations, not
|
|
116
|
+
in the session. Relation navigation works in both directions as pure SQL generation: the DB
|
|
117
|
+
does not guarantee the FK of a view.
|
|
118
|
+
|
|
119
|
+
`depends_on` lists the OTHER `@snake_view` views that THIS one reads: the migration creates it
|
|
120
|
+
AFTER them (topological order) and drops it BEFORE. Only between views (a view gets created
|
|
121
|
+
after ALL the tables).
|
|
122
|
+
|
|
123
|
+
`registry` lets you declare it in an ISOLATED store, just like `@snake_model` and
|
|
124
|
+
`@snake_db_first`.
|
|
125
|
+
"""
|
|
126
|
+
_view_source(sql, query)
|
|
127
|
+
|
|
128
|
+
def wrap(klass: type[T]) -> type[T]:
|
|
129
|
+
dependencies = tuple(_dependency_name(dep, registry) for dep in depends_on)
|
|
130
|
+
table = compile_model(
|
|
131
|
+
klass, table=name, schema=schema, kind=SnakeTableKind.VIEW
|
|
132
|
+
)
|
|
133
|
+
registry.register(
|
|
134
|
+
klass,
|
|
135
|
+
replace(
|
|
136
|
+
table,
|
|
137
|
+
# The query is stored UNCOMPILED: the body of a view is written differently on
|
|
138
|
+
# each engine, and compiling it here froze it into one of them. The emitter
|
|
139
|
+
# compiles it.
|
|
140
|
+
view_definition=sql,
|
|
141
|
+
view_query=query,
|
|
142
|
+
depends_on=dependencies,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
setattr(klass, "__init__", _make_init(klass))
|
|
146
|
+
_install_dunders(klass, registry)
|
|
147
|
+
return klass
|
|
148
|
+
|
|
149
|
+
return wrap
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""SnakeORM's SQL dialects: how the SQL is written for each engine."""
|
|
2
|
+
|
|
3
|
+
from snakeorm.dialects.base import SnakeDialect as SnakeDialect
|
|
4
|
+
from snakeorm.dialects.mysql import MySQLDialect as MySQLDialect
|
|
5
|
+
from snakeorm.dialects.postgres import PostgresDialect as PostgresDialect
|
|
6
|
+
from snakeorm.dialects.sqlite import SQLiteDialect as SQLiteDialect
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""The Protocol that defines a SQL dialect (how the SQL is WRITTEN)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from snakeorm.dialects.capabilities import (
|
|
9
|
+
SnakeCapabilities,
|
|
10
|
+
SnakeLimits,
|
|
11
|
+
SnakeSyntax,
|
|
12
|
+
)
|
|
13
|
+
from snakeorm.expressions.scalar import SnakeFunc
|
|
14
|
+
from snakeorm.metadata import (
|
|
15
|
+
SnakeIndexMethod,
|
|
16
|
+
SnakeServerDefault,
|
|
17
|
+
SnakeTypeParams,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class SnakeDialect(Protocol):
|
|
23
|
+
"""How the SQL is written for one specific engine. It executes NOTHING.
|
|
24
|
+
|
|
25
|
+
It only enters `sql/` (emission). The graph and the models are 100% engine-agnostic.
|
|
26
|
+
Adding a new engine = one implementation of this Protocol, without touching the core.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
capabilities: SnakeCapabilities
|
|
30
|
+
"""What the engine KNOWS how to do, answered against the WHOLE `Cap` catalogue. The source of truth.
|
|
31
|
+
|
|
32
|
+
It is one object and not twenty loose attributes for two reasons the attributes could not give:
|
|
33
|
+
it can be WALKED (that is where the startup warning comes from, one for each thing the engine
|
|
34
|
+
does not give) and it can say "halfway" (SQLite stores an exact `Decimal` and sorts it as text:
|
|
35
|
+
neither absent nor full).
|
|
36
|
+
"""
|
|
37
|
+
syntax: SnakeSyntax
|
|
38
|
+
"""Differences in the SHAPE of the statement. They are translated in the emitter; they never stop the plan."""
|
|
39
|
+
limits: SnakeLimits
|
|
40
|
+
"""The engine's numeric ceilings. `None` is not "no ceiling": it is "it ignores the declared parameter"."""
|
|
41
|
+
|
|
42
|
+
# The same old `supports_*`, `triggers_are_table_scoped` and `max_*` still exist as READ-ONLY
|
|
43
|
+
# properties derived from the three objects above, so the reads already written all over the
|
|
44
|
+
# package keep working while they migrate. `DerivedFlags` implements them. Declared as
|
|
45
|
+
# properties and not as attributes on purpose: a plain attribute DOES satisfy a read-only
|
|
46
|
+
# property in a Protocol, so a dialect can use either of the two forms; the other way round it
|
|
47
|
+
# cannot.
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def supports_returning(self) -> bool:
|
|
51
|
+
"""Whether it can return the rows it wrote (`INSERT ... RETURNING`)."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def supports_row_constructor(self) -> bool:
|
|
56
|
+
"""Whether it understands `(a, b) IN ((...), (...))`. If not, the emitter uses the equivalent OR-of-ANDs."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def supports_transactional_ddl(self) -> bool:
|
|
61
|
+
"""Whether DDL goes inside the transaction. With it, an N-step migration is all-or-nothing."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def supports_upsert(self) -> bool:
|
|
66
|
+
"""Whether it can do an INSERT that is idempotent on conflict. Without it, `session.upsert()` raises (it does not emulate: there is a race)."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def supports_add_constraint(self) -> bool:
|
|
71
|
+
"""Whether it accepts `ALTER TABLE ... ADD CONSTRAINT`. It decides the SHAPE of the plan: with
|
|
72
|
+
it the FKs go AT THE END with no ordering between tables; without it (SQLite) they go INSIDE the CREATE TABLE and force a topological order."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def supports_alter_column(self) -> bool:
|
|
77
|
+
"""Whether it can change a column's type/nullability/default. SQLite cannot: it would require rebuilding the table."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def supports_schemas(self) -> bool:
|
|
82
|
+
"""Whether it has named schemas (`CREATE SCHEMA`). On SQLite the "schemas" are ATTACHed databases (`ATTACH`)."""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def supports_stored_functions(self) -> bool:
|
|
87
|
+
"""Whether it stores named functions that a migration can create.
|
|
88
|
+
|
|
89
|
+
A capability of its OWN ever since the catalogue exists. The plan used to ask about
|
|
90
|
+
`supports_schemas` to decide whether it could create a function: it matched on all three
|
|
91
|
+
engines, so it worked and nobody saw it, but they are different things and a new engine
|
|
92
|
+
would have inherited the confusion.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def supports_row_locking(self) -> bool:
|
|
98
|
+
"""Whether it can lock ROWS (`SELECT ... FOR UPDATE`). SQLite locks the file: asking for it fails at compile time."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def supports_comments(self) -> bool:
|
|
103
|
+
"""Whether it STORES table and column comments. Only SQLite does not.
|
|
104
|
+
|
|
105
|
+
Not "whether it has `COMMENT ON`": MySQL has no such statement and stores comments all the
|
|
106
|
+
same, as a clause. The spelling lives in `syntax.comment_style`.
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def supports_replace_view(self) -> bool:
|
|
112
|
+
"""Whether it can do `CREATE OR REPLACE VIEW`. Without it, altering a view is drop+create, and `realize()` rewrites it."""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def supports_parenthesised_compound(self) -> bool:
|
|
117
|
+
"""Whether the branches of a UNION/EXCEPT/INTERSECT may go in PARENTHESES. It is not cosmetic:
|
|
118
|
+
the parentheses make a `LIMIT` belong to the branch and not to the whole set. SQLite rejects them."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def supports_cte_in_compound_branch(self) -> bool:
|
|
123
|
+
"""Whether a `WITH [RECURSIVE] ...` may be a BRANCH of a UNION/EXCEPT/INTERSECT.
|
|
124
|
+
|
|
125
|
+
A different question from `supports_parenthesised_compound`, and the two only look alike from
|
|
126
|
+
Postgres: MySQL parenthesises branches and still refuses a CTE inside one."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def supports_ilike(self) -> bool:
|
|
131
|
+
"""Whether it has `ILIKE`. Without it (SQLite) the emission falls back to `LOWER(a) LIKE LOWER(b)`, with ASCII-only folding."""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def triggers_are_table_scoped(self) -> bool:
|
|
136
|
+
"""Whether a trigger belongs to a TABLE (`DROP TRIGGER x ON t`) or is global (SQLite: `DROP TRIGGER x`).
|
|
137
|
+
Different syntax, not an absent capability: it is translated in the emitter, the plan does not stop."""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def max_bind_params(self) -> int:
|
|
142
|
+
"""Ceiling of placeholders per statement; the bulk INSERT slices into batches with it. Postgres: 65535."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def max_numeric_precision(self) -> int | None:
|
|
147
|
+
"""TOTAL digits a NUMERIC/DECIMAL accepts. Postgres: 1000. MySQL: 65.
|
|
148
|
+
`None` if the engine does not restrict it because it IGNORES the declared parameter (SQLite,
|
|
149
|
+
which has per-column affinity): there, any number would assert a limit that does not exist."""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def max_numeric_scale(self) -> int | None:
|
|
154
|
+
"""Decimal places a NUMERIC/DECIMAL accepts. It is NOT the same number as the precision: MySQL
|
|
155
|
+
stops at 30 with a precision of 65, so `DECIMAL(40,35)` has one valid half and one that is not."""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def max_fractional_seconds(self) -> int | None:
|
|
160
|
+
"""Fractional-second digits of a date column. Postgres and MySQL: 6 (microseconds, the
|
|
161
|
+
resolution of Python's `datetime`). SQL Server reaches 7 and Oracle 9 — that is why the
|
|
162
|
+
number belongs to the engine and not to the model."""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def placeholder(self, index: int) -> str:
|
|
166
|
+
"""Returns the parameter marker for the given position (e.g. '%s', '$1', '?')."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
def trigger_statements(self, name: str, body: str) -> tuple[list[str], str]:
|
|
170
|
+
"""How this engine spells a trigger body: `(statements to run first, the body to inline)`.
|
|
171
|
+
|
|
172
|
+
THE THREE ARE DIFFERENT FROM EACH OTHER, which is why this is a translation and not a flag.
|
|
173
|
+
Measured, all three:
|
|
174
|
+
|
|
175
|
+
PostgreSQL the body goes in a FUNCTION and the trigger calls it (`EXECUTE FUNCTION f()`)
|
|
176
|
+
SQLite the body goes between `BEGIN` and `END`; without them it is a syntax error
|
|
177
|
+
MySQL a single statement goes bare
|
|
178
|
+
|
|
179
|
+
Sending it through verbatim made a declaration fail on whichever engine was not the one it
|
|
180
|
+
had been written for, and the DRIVER is what said so — `syntax error at or near "UPDATE"` on
|
|
181
|
+
PostgreSQL, `near "UPDATE": syntax error` on SQLite. Two different complaints about the same
|
|
182
|
+
portable declaration.
|
|
183
|
+
|
|
184
|
+
The first return value is anything that must exist BEFORE the trigger (PostgreSQL's function,
|
|
185
|
+
nothing for the others); the second is what goes after `FOR EACH ROW`.
|
|
186
|
+
"""
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
def quote_ident(self, name: str) -> str:
|
|
190
|
+
"""Quotes an identifier (table or column) the way the engine wants it."""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
def json_get_sql(
|
|
194
|
+
self, source: str, key_path: tuple[str, ...], as_type: type
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Emits a read INSIDE a JSON document, cast to the declared type.
|
|
197
|
+
|
|
198
|
+
`source` arrives already emitted (a quoted column, or an expression). The three engines spell
|
|
199
|
+
this so differently —`->>` and a `{a,b}` path, `JSON_EXTRACT` with `$.a` and an unquote, a
|
|
200
|
+
bare `json_extract`— that it belongs here for the same reason placeholders do: what the SQL
|
|
201
|
+
SAYS is the dialect's business, what it MEANS is the graph's.
|
|
202
|
+
|
|
203
|
+
The path is emitted INSIDE a literal because no engine takes a placeholder there. The keys
|
|
204
|
+
are validated when the expression is BUILT (`SnakeValue.json_get`), which is why this may
|
|
205
|
+
interpolate them.
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
def date_shift_sql(
|
|
210
|
+
self, source: str, amount_placeholder: str, unit: str, keeps_time: bool
|
|
211
|
+
) -> str:
|
|
212
|
+
"""Emits a date moved by a signed amount, with the amount as a PARAMETER.
|
|
213
|
+
|
|
214
|
+
The clearest case in the ORM for this seam: the three spellings have nothing in common
|
|
215
|
+
(`+ INTERVAL`, `DATE_ADD`, a modifier string), and all three were measured to accept the
|
|
216
|
+
amount as a placeholder, so the rule that values never touch the statement survives.
|
|
217
|
+
|
|
218
|
+
`unit` arrives as the agnostic lowercase name (`day`, `month`); each dialect shapes it.
|
|
219
|
+
`keeps_time` says whether the source carries a clock, which only SQLite needs — it has no
|
|
220
|
+
date type to inspect, and picking `date()` for a timestamp would silently drop the time.
|
|
221
|
+
"""
|
|
222
|
+
...
|
|
223
|
+
|
|
224
|
+
def string_agg_sql(
|
|
225
|
+
self, value: str, separator: str, order_by: str, params: list[object]
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Emits a group joined into one string, deciding how the SEPARATOR travels.
|
|
228
|
+
|
|
229
|
+
It takes `params` for the same reason `limit_offset` does: whether the separator can be a
|
|
230
|
+
placeholder is the ENGINE's answer, not the emitter's. Measured — PostgreSQL and SQLite take
|
|
231
|
+
it as a normal argument and parameterise it; MySQL makes it the `SEPARATOR` keyword and
|
|
232
|
+
rejects a placeholder there, so that dialect escapes it through `literal()`.
|
|
233
|
+
|
|
234
|
+
`order_by` arrives already emitted, or empty. It is worth passing whenever a person reads the
|
|
235
|
+
result: without it the order belongs to the engine and can change between runs.
|
|
236
|
+
"""
|
|
237
|
+
...
|
|
238
|
+
|
|
239
|
+
def integer_division_op(self) -> str:
|
|
240
|
+
"""How this engine spells division between two INTEGERS.
|
|
241
|
+
|
|
242
|
+
MEASURED, and the three do not agree: `SELECT 45/50` answers `0` on PostgreSQL and SQLite and
|
|
243
|
+
`0.9000` on MySQL, whose `/` is decimal division and which keeps `DIV` as a separate operator
|
|
244
|
+
for the integer one. The ORM declares `SnakeArith[int]` for two integer columns, so without
|
|
245
|
+
this the declared type is simply false on one engine of three.
|
|
246
|
+
|
|
247
|
+
It is asked ONLY when both operands are provably integers. Anything unproven keeps `/`.
|
|
248
|
+
"""
|
|
249
|
+
...
|
|
250
|
+
|
|
251
|
+
def cast_sql(self, source: str, as_type: type) -> str:
|
|
252
|
+
"""Emits an EXPLICIT cast of an already-emitted value to the named type.
|
|
253
|
+
|
|
254
|
+
It lives here for the same reason `json_get_sql` does, and the reason was MEASURED rather
|
|
255
|
+
than assumed: `CAST(x AS NUMERIC)` answers `0.9` on PostgreSQL and `0` on SQLite, whose
|
|
256
|
+
NUMERIC affinity collapses an integral value back to an integer. A single spelling shared by
|
|
257
|
+
the three engines would be right on two and silently wrong on the third.
|
|
258
|
+
|
|
259
|
+
The type is guaranteed to be in `CASTABLE`: `snake_cast` refuses anything else at the call
|
|
260
|
+
site, so an implementation never has to answer for a type it cannot spell.
|
|
261
|
+
"""
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
def map_type(
|
|
265
|
+
self,
|
|
266
|
+
python_type: object,
|
|
267
|
+
autoincrement: bool = False,
|
|
268
|
+
params: SnakeTypeParams | None = None,
|
|
269
|
+
) -> str:
|
|
270
|
+
"""Translates a Python type into its COMPLETE SQL type, with its family's parameters.
|
|
271
|
+
|
|
272
|
+
`params` arrives as ONE object per family (an int's width, a str's length, a Decimal's
|
|
273
|
+
precision, a dict's backing) and not as five loose knobs: loose, `precision` ended up
|
|
274
|
+
OUTSIDE this method —it was concatenated onto the result with an f-string— and that is why
|
|
275
|
+
it was the only parameter that was never validated. What this returns is the whole type,
|
|
276
|
+
not a fragment.
|
|
277
|
+
|
|
278
|
+
Returning the complete type is also what lets each engine genuinely decide: Postgres honours
|
|
279
|
+
every parameter, SQLite ignores the ones it does not distinguish.
|
|
280
|
+
"""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
def drop_all_sql(self, tables: Sequence[str]) -> tuple[str, ...]:
|
|
284
|
+
"""Statements that leave the schema empty of `tables`, in the order they must run.
|
|
285
|
+
|
|
286
|
+
Emptying a schema is DDL, so it is written here like the rest of it. The keyword was never
|
|
287
|
+
the hard part: what differs is how each engine is persuaded to ignore the foreign keys while
|
|
288
|
+
the tables come down — Postgres cascades per statement, MySQL has a session switch, SQLite
|
|
289
|
+
has a pragma. Three answers to one question is what a dialect is for.
|
|
290
|
+
|
|
291
|
+
It lives in the Protocol so a fourth engine cannot arrive without answering it. The CLI's
|
|
292
|
+
`fresh` used to write `DROP TABLE ... CASCADE` itself, which is Postgres and only Postgres,
|
|
293
|
+
so the one DESTRUCTIVE command failed halfway on the other two.
|
|
294
|
+
|
|
295
|
+
Empty in, empty out: with no tables there is nothing to bracket either.
|
|
296
|
+
"""
|
|
297
|
+
...
|
|
298
|
+
|
|
299
|
+
def explain_sql(self, sql: str) -> str:
|
|
300
|
+
"""Wraps a statement so the engine reports its PLAN instead of running it.
|
|
301
|
+
|
|
302
|
+
It lives here and not in the driver because the difference is grammar, not execution:
|
|
303
|
+
`EXPLAIN` on two engines, `EXPLAIN QUERY PLAN` on SQLite. The compiled `(sql, params)` goes
|
|
304
|
+
down the existing `fetch_all`, so nothing in the driver Protocol moves for this.
|
|
305
|
+
|
|
306
|
+
The ANSWER has no common shape and is not given one: Postgres returns one column, SQLite
|
|
307
|
+
four, MySQL about a dozen. The session hands back the engine's own lines.
|
|
308
|
+
"""
|
|
309
|
+
...
|
|
310
|
+
|
|
311
|
+
def statement_timeout_sql(self, milliseconds: int) -> str | None:
|
|
312
|
+
"""The statement that caps how long a query may run, or `None` if the engine has none.
|
|
313
|
+
|
|
314
|
+
One hung query drains a pool, so this is a production knob rather than a nicety — and it
|
|
315
|
+
was written INSIDE `TimeoutDriver` as `SET statement_timeout = <ms>`, which is Postgres and
|
|
316
|
+
only Postgres, under a class name that promises nothing about engines. Measured: MySQL
|
|
317
|
+
answers `1193 Unknown system variable` and SQLite a syntax error.
|
|
318
|
+
|
|
319
|
+
`None` is a legitimate answer and not a gap: SQLite has no server-side statement timeout at
|
|
320
|
+
all. The caller refuses out loud rather than pretending; inventing something plausible there
|
|
321
|
+
would be answering a different question.
|
|
322
|
+
|
|
323
|
+
The value is in MILLISECONDS because that is the unit the API is written in. An engine whose
|
|
324
|
+
variable expects another unit converts here, which is exactly the kind of thing a dialect is
|
|
325
|
+
for.
|
|
326
|
+
"""
|
|
327
|
+
...
|
|
328
|
+
|
|
329
|
+
def register_type(self, python_type: object, sql_type: str) -> None:
|
|
330
|
+
"""Adds (or rewrites) the SQL spelling of a Python type in THIS dialect.
|
|
331
|
+
|
|
332
|
+
The extension point of the type vocabulary: without it, declaring an `INET`, a `CITEXT` or a
|
|
333
|
+
domain type meant editing the dialect — the type system was the single source of truth but
|
|
334
|
+
you could not add words to it. It is per dialect because the same Python type is written
|
|
335
|
+
differently on each engine.
|
|
336
|
+
"""
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
def literal(self, value: object) -> str:
|
|
340
|
+
"""Formats a value as a SQL literal for DDL (DEFAULT), which takes no placeholders.
|
|
341
|
+
The formatting (TRUE vs 1, quoting) varies between engines, which is why it lives in the dialect.
|
|
342
|
+
"""
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
def function_name(self, func: SnakeFunc) -> str:
|
|
346
|
+
"""Translates the agnostic name of a scalar function into the engine's own."""
|
|
347
|
+
...
|
|
348
|
+
|
|
349
|
+
def index_method(self, method: SnakeIndexMethod) -> str:
|
|
350
|
+
"""Translates an index's access method (agnostic) into the engine's jargon."""
|
|
351
|
+
...
|
|
352
|
+
|
|
353
|
+
def server_default_sql(self, value: SnakeServerDefault) -> str:
|
|
354
|
+
"""Translates a SERVER-side default value (agnostic: `NOW`, `UUID_V4`...) into its SQL
|
|
355
|
+
expression on the engine. If it cannot translate it, it raises `SnakeDialectError`.
|
|
356
|
+
"""
|
|
357
|
+
...
|
|
358
|
+
|
|
359
|
+
def limit_offset(
|
|
360
|
+
self, limit: int | None, offset: int | None, params: list[object]
|
|
361
|
+
) -> str:
|
|
362
|
+
"""Emits the parametrised LIMIT/OFFSET clause (appending to `params`), or '' if there is neither.
|
|
363
|
+
Non-standard syntax: that is why the dialect decides it.
|
|
364
|
+
"""
|
|
365
|
+
...
|
|
366
|
+
|
|
367
|
+
def on_conflict_clause(
|
|
368
|
+
self, conflict_columns: Sequence[str], update_columns: Sequence[str]
|
|
369
|
+
) -> str:
|
|
370
|
+
"""An upsert's conflict-resolution clause. `conflict_columns` define the conflict (a
|
|
371
|
+
UNIQUE/PK constraint); `update_columns` are rewritten with the incoming value, or empty → leave alone.
|
|
372
|
+
"""
|
|
373
|
+
...
|