oxyde-postgres 0.1.0__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.
- oxyde/__init__.py +151 -0
- oxyde/_msgpack.py +3 -0
- oxyde/cli/__init__.py +7 -0
- oxyde/cli/app.py +107 -0
- oxyde/cli/codegen.py +66 -0
- oxyde/cli/config.py +84 -0
- oxyde/cli/migrations.py +536 -0
- oxyde/codegen/__init__.py +15 -0
- oxyde/codegen/stub_generator.py +1229 -0
- oxyde/core/__init__.py +90 -0
- oxyde/core/column_types.py +367 -0
- oxyde/core/ir.py +380 -0
- oxyde/core/types.py +69 -0
- oxyde/core/wrapper.py +242 -0
- oxyde/db/__init__.py +230 -0
- oxyde/db/pool.py +305 -0
- oxyde/db/registry.py +113 -0
- oxyde/db/schema.py +51 -0
- oxyde/db/transaction.py +392 -0
- oxyde/exceptions.py +89 -0
- oxyde/migrations/__init__.py +49 -0
- oxyde/migrations/config.py +187 -0
- oxyde/migrations/context.py +598 -0
- oxyde/migrations/executor.py +456 -0
- oxyde/migrations/extract.py +316 -0
- oxyde/migrations/generator.py +534 -0
- oxyde/migrations/replay.py +367 -0
- oxyde/migrations/squash.py +109 -0
- oxyde/migrations/tracker.py +162 -0
- oxyde/migrations/utils.py +145 -0
- oxyde/models/__init__.py +83 -0
- oxyde/models/base.py +871 -0
- oxyde/models/decorators.py +111 -0
- oxyde/models/field.py +204 -0
- oxyde/models/lookups.py +667 -0
- oxyde/models/metadata.py +174 -0
- oxyde/models/registry.py +170 -0
- oxyde/models/serializers.py +144 -0
- oxyde/models/utils.py +116 -0
- oxyde/py.typed +0 -0
- oxyde/queries/__init__.py +79 -0
- oxyde/queries/aggregates.py +145 -0
- oxyde/queries/base.py +249 -0
- oxyde/queries/conditions.py +92 -0
- oxyde/queries/expressions.py +171 -0
- oxyde/queries/insert.py +124 -0
- oxyde/queries/joins.py +66 -0
- oxyde/queries/manager.py +409 -0
- oxyde/queries/mixins/__init__.py +68 -0
- oxyde/queries/mixins/aggregation.py +335 -0
- oxyde/queries/mixins/debug.py +154 -0
- oxyde/queries/mixins/execution.py +701 -0
- oxyde/queries/mixins/filtering.py +144 -0
- oxyde/queries/mixins/joining.py +160 -0
- oxyde/queries/mixins/mutation.py +508 -0
- oxyde/queries/mixins/pagination.py +145 -0
- oxyde/queries/q.py +250 -0
- oxyde/queries/raw.py +114 -0
- oxyde/queries/select.py +345 -0
- oxyde/queries/typed.py +213 -0
- oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
- oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
- oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
- oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
oxyde/core/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Python bindings to the Rust core (_oxyde_core extension module).
|
|
2
|
+
|
|
3
|
+
This module re-exports functions from the Rust PyO3 extension module and
|
|
4
|
+
provides the ir submodule for building query intermediate representations.
|
|
5
|
+
|
|
6
|
+
The Rust core provides:
|
|
7
|
+
- Connection pool management (init_pool, close_pool)
|
|
8
|
+
- Query execution (execute, execute_in_transaction)
|
|
9
|
+
- Transaction control (begin/commit/rollback, savepoints)
|
|
10
|
+
- Migration utilities (compute_diff, to_sql)
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
Python Query → IR dict → msgpack bytes → Rust → SQL → Database
|
|
14
|
+
Database → Rust → msgpack bytes → Python dict/models
|
|
15
|
+
|
|
16
|
+
Pool Functions:
|
|
17
|
+
init_pool(name, url, settings): Create connection pool in Rust registry.
|
|
18
|
+
init_pool_overwrite(name, url, settings): Replace existing pool.
|
|
19
|
+
close_pool(name): Close specific pool.
|
|
20
|
+
close_all_pools(): Close all pools (shutdown).
|
|
21
|
+
|
|
22
|
+
Execution Functions:
|
|
23
|
+
execute(pool_name, ir_bytes): Execute query, return msgpack result.
|
|
24
|
+
execute_in_transaction(pool_name, tx_id, ir_bytes): Execute in transaction.
|
|
25
|
+
|
|
26
|
+
Transaction Functions:
|
|
27
|
+
begin_transaction(pool_name) → tx_id: Start transaction.
|
|
28
|
+
commit_transaction(tx_id): Commit and release.
|
|
29
|
+
rollback_transaction(tx_id): Rollback and release.
|
|
30
|
+
create_savepoint(tx_id, name): Create savepoint for nested tx.
|
|
31
|
+
rollback_to_savepoint(tx_id, name): Rollback to savepoint.
|
|
32
|
+
release_savepoint(tx_id, name): Release savepoint.
|
|
33
|
+
|
|
34
|
+
Migration Functions:
|
|
35
|
+
migration_compute_diff(models_schema, db_schema): Compute schema diff.
|
|
36
|
+
migration_to_sql(diff, dialect): Generate SQL migration statements.
|
|
37
|
+
|
|
38
|
+
Validation:
|
|
39
|
+
register_validator(model_key, validator): Register Pydantic validator.
|
|
40
|
+
(Currently unused - validation happens in Python.)
|
|
41
|
+
|
|
42
|
+
Submodules:
|
|
43
|
+
ir: Query IR construction helpers (build_select_ir, filter_and, etc.)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from . import ir
|
|
47
|
+
from .wrapper import (
|
|
48
|
+
begin_transaction,
|
|
49
|
+
close_all_pools,
|
|
50
|
+
close_pool,
|
|
51
|
+
commit_transaction,
|
|
52
|
+
create_savepoint,
|
|
53
|
+
execute,
|
|
54
|
+
execute_in_transaction,
|
|
55
|
+
explain_query,
|
|
56
|
+
init_pool,
|
|
57
|
+
init_pool_overwrite,
|
|
58
|
+
migration_compute_diff,
|
|
59
|
+
migration_to_sql,
|
|
60
|
+
pool_backend,
|
|
61
|
+
register_validator,
|
|
62
|
+
release_savepoint,
|
|
63
|
+
render_sql,
|
|
64
|
+
render_sql_debug,
|
|
65
|
+
rollback_to_savepoint,
|
|
66
|
+
rollback_transaction,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"execute",
|
|
71
|
+
"execute_in_transaction",
|
|
72
|
+
"init_pool",
|
|
73
|
+
"init_pool_overwrite",
|
|
74
|
+
"close_pool",
|
|
75
|
+
"close_all_pools",
|
|
76
|
+
"begin_transaction",
|
|
77
|
+
"commit_transaction",
|
|
78
|
+
"rollback_transaction",
|
|
79
|
+
"create_savepoint",
|
|
80
|
+
"rollback_to_savepoint",
|
|
81
|
+
"release_savepoint",
|
|
82
|
+
"pool_backend",
|
|
83
|
+
"register_validator",
|
|
84
|
+
"render_sql",
|
|
85
|
+
"render_sql_debug",
|
|
86
|
+
"explain_query",
|
|
87
|
+
"migration_compute_diff",
|
|
88
|
+
"migration_to_sql",
|
|
89
|
+
"ir",
|
|
90
|
+
]
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""The single Python → ColumnTypeSpec mapping point.
|
|
2
|
+
|
|
3
|
+
Converts a field's Python annotation + user ``db_type`` + constraints into a
|
|
4
|
+
``ColumnTypeSpec`` dict — the tagged form the Rust core deserializes
|
|
5
|
+
(``{"kind": "decimal", "precision": 10, "scale": 2}``).
|
|
6
|
+
|
|
7
|
+
This module is the only place in the Python layer that classifies types.
|
|
8
|
+
Everything else (model finalization, migrations extract, F-expression
|
|
9
|
+
literals) delegates here. ``TYPE_REGISTRY`` keeps its other roles
|
|
10
|
+
(msgpack value serialization, lookup categories) untouched.
|
|
11
|
+
|
|
12
|
+
Two roles of ``db_type`` are split deliberately:
|
|
13
|
+
- its *semantic kind* (for binding/decoding) is derived here via
|
|
14
|
+
``_KNOWN_DB_TYPES``;
|
|
15
|
+
- its *verbatim DDL string* travels separately (``FieldDef.db_type``)
|
|
16
|
+
and is never classified in Rust.
|
|
17
|
+
|
|
18
|
+
Unknown annotations and unrecognized ``db_type`` strings return ``None``:
|
|
19
|
+
the column is simply omitted from ``column_types`` and Rust falls back to
|
|
20
|
+
native conversion / runtime DB type info — exactly the legacy behavior.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from datetime import date, datetime, time, timedelta
|
|
26
|
+
from decimal import Decimal
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any, get_args, get_origin
|
|
29
|
+
from uuid import UUID
|
|
30
|
+
|
|
31
|
+
ColumnSpec = dict[str, Any]
|
|
32
|
+
|
|
33
|
+
# Scalar Python type → spec kind (exact type() lookup, no issubclass).
|
|
34
|
+
_PY_SCALAR_KINDS: dict[type, str] = {
|
|
35
|
+
bool: "boolean",
|
|
36
|
+
int: "big_integer",
|
|
37
|
+
float: "double",
|
|
38
|
+
str: "string",
|
|
39
|
+
bytes: "blob",
|
|
40
|
+
bytearray: "blob",
|
|
41
|
+
datetime: "date_time",
|
|
42
|
+
date: "date",
|
|
43
|
+
time: "time",
|
|
44
|
+
timedelta: "timedelta",
|
|
45
|
+
UUID: "uuid",
|
|
46
|
+
Decimal: "decimal",
|
|
47
|
+
dict: "json",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Uppercased SQL type name (precision stripped) → spec kind.
|
|
51
|
+
# Mirrors the legacy Rust `classify_type` table: only names whose binding
|
|
52
|
+
# semantics are known. Anything else → None (native conversion).
|
|
53
|
+
_KNOWN_DB_TYPES: dict[str, str] = {
|
|
54
|
+
"INT": "big_integer",
|
|
55
|
+
"INTEGER": "big_integer",
|
|
56
|
+
"BIGINT": "big_integer",
|
|
57
|
+
"SMALLINT": "big_integer",
|
|
58
|
+
"TINYINT": "big_integer",
|
|
59
|
+
"SERIAL": "big_integer",
|
|
60
|
+
"BIGSERIAL": "big_integer",
|
|
61
|
+
"SMALLSERIAL": "big_integer",
|
|
62
|
+
"INT2": "big_integer",
|
|
63
|
+
"INT4": "big_integer",
|
|
64
|
+
"INT8": "big_integer",
|
|
65
|
+
"TIMEDELTA": "timedelta",
|
|
66
|
+
"INTERVAL": "timedelta",
|
|
67
|
+
"FLOAT": "double",
|
|
68
|
+
"DOUBLE": "double",
|
|
69
|
+
"REAL": "double",
|
|
70
|
+
"FLOAT4": "double",
|
|
71
|
+
"FLOAT8": "double",
|
|
72
|
+
"DOUBLE PRECISION": "double",
|
|
73
|
+
"BOOL": "boolean",
|
|
74
|
+
"BOOLEAN": "boolean",
|
|
75
|
+
"TEXT": "text",
|
|
76
|
+
"STR": "text",
|
|
77
|
+
"VARCHAR": "string",
|
|
78
|
+
"CHAR": "string",
|
|
79
|
+
"UUID": "uuid",
|
|
80
|
+
"JSON": "json",
|
|
81
|
+
"JSONB": "json_binary",
|
|
82
|
+
"DATETIME": "date_time",
|
|
83
|
+
"TIMESTAMP": "date_time",
|
|
84
|
+
"TIMESTAMPTZ": "date_time_utc",
|
|
85
|
+
"DATE": "date",
|
|
86
|
+
"TIME": "time",
|
|
87
|
+
"TIMETZ": "time",
|
|
88
|
+
"BYTES": "blob",
|
|
89
|
+
"BYTEA": "blob",
|
|
90
|
+
"BLOB": "blob",
|
|
91
|
+
"DECIMAL": "decimal",
|
|
92
|
+
"NUMERIC": "decimal",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def compute_column_type(
|
|
97
|
+
python_type: Any,
|
|
98
|
+
db_type: str | None = None,
|
|
99
|
+
*,
|
|
100
|
+
max_length: int | None = None,
|
|
101
|
+
max_digits: int | None = None,
|
|
102
|
+
decimal_places: int | None = None,
|
|
103
|
+
) -> ColumnSpec | None:
|
|
104
|
+
"""Compute the ColumnTypeSpec dict for a column.
|
|
105
|
+
|
|
106
|
+
Priority: explicit ``db_type`` (semantic kind via _KNOWN_DB_TYPES,
|
|
107
|
+
parameters parsed from e.g. ``NUMERIC(10,2)``/``VARCHAR(100)``),
|
|
108
|
+
otherwise the Python annotation. Returns None when nothing is known —
|
|
109
|
+
the column is then omitted from ``column_types``.
|
|
110
|
+
"""
|
|
111
|
+
enum_spec = _spec_from_enum_annotation(python_type, db_type)
|
|
112
|
+
if enum_spec is not None:
|
|
113
|
+
return enum_spec
|
|
114
|
+
if db_type:
|
|
115
|
+
return _spec_from_db_type(db_type)
|
|
116
|
+
return _spec_from_annotation(
|
|
117
|
+
python_type,
|
|
118
|
+
max_length=max_length,
|
|
119
|
+
max_digits=max_digits,
|
|
120
|
+
decimal_places=decimal_places,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def spec_for_literal(value_type: type) -> ColumnSpec | None:
|
|
125
|
+
"""Spec for an F-expression literal by its Python type.
|
|
126
|
+
|
|
127
|
+
Used to recover typed bindings (Decimal, UUID, datetime, ...) for values
|
|
128
|
+
that msgpack-encode as strings. Returns None for types msgpack carries
|
|
129
|
+
natively with correct binding.
|
|
130
|
+
"""
|
|
131
|
+
if _is_enum_type(value_type):
|
|
132
|
+
return _enum_spec(value_type)
|
|
133
|
+
kind = _PY_SCALAR_KINDS.get(value_type)
|
|
134
|
+
return {"kind": kind} if kind is not None else None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _spec_from_db_type(db_type: str) -> ColumnSpec | None:
|
|
138
|
+
upper = db_type.upper().strip()
|
|
139
|
+
|
|
140
|
+
if upper.endswith("[]"):
|
|
141
|
+
item = _spec_from_db_type(upper[:-2])
|
|
142
|
+
return {"kind": "array", "item": item} if item else None
|
|
143
|
+
|
|
144
|
+
base, params = _split_type_params(upper)
|
|
145
|
+
kind = _KNOWN_DB_TYPES.get(base)
|
|
146
|
+
if kind is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
spec: ColumnSpec = {"kind": kind}
|
|
150
|
+
if kind == "string" and params:
|
|
151
|
+
spec["length"] = params[0]
|
|
152
|
+
elif kind == "decimal" and params:
|
|
153
|
+
spec["precision"] = params[0]
|
|
154
|
+
if len(params) > 1:
|
|
155
|
+
spec["scale"] = params[1]
|
|
156
|
+
return spec
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _spec_from_annotation(
|
|
160
|
+
python_type: Any,
|
|
161
|
+
*,
|
|
162
|
+
max_length: int | None = None,
|
|
163
|
+
max_digits: int | None = None,
|
|
164
|
+
decimal_places: int | None = None,
|
|
165
|
+
) -> ColumnSpec | None:
|
|
166
|
+
origin = get_origin(python_type)
|
|
167
|
+
|
|
168
|
+
# dict[K, V] -> json
|
|
169
|
+
if origin is dict:
|
|
170
|
+
return {"kind": "json"}
|
|
171
|
+
|
|
172
|
+
# list[T] -> array of T; constraints apply to the element
|
|
173
|
+
# (legacy parity: max_length on a list field meant VARCHAR(n)[] elements)
|
|
174
|
+
if origin is list:
|
|
175
|
+
args = get_args(python_type)
|
|
176
|
+
if args:
|
|
177
|
+
item = _spec_from_annotation(
|
|
178
|
+
args[0],
|
|
179
|
+
max_length=max_length,
|
|
180
|
+
max_digits=max_digits,
|
|
181
|
+
decimal_places=decimal_places,
|
|
182
|
+
)
|
|
183
|
+
if item:
|
|
184
|
+
return {"kind": "array", "item": item}
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Union types (including Optional[T]) -> first non-None arg
|
|
188
|
+
if origin is not None:
|
|
189
|
+
for arg in get_args(python_type):
|
|
190
|
+
if arg is not type(None):
|
|
191
|
+
result = _spec_from_annotation(
|
|
192
|
+
arg,
|
|
193
|
+
max_length=max_length,
|
|
194
|
+
max_digits=max_digits,
|
|
195
|
+
decimal_places=decimal_places,
|
|
196
|
+
)
|
|
197
|
+
if result:
|
|
198
|
+
return result
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
kind = _PY_SCALAR_KINDS.get(python_type)
|
|
202
|
+
if kind is None:
|
|
203
|
+
if _is_enum_type(python_type):
|
|
204
|
+
return _enum_spec(python_type)
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
spec: ColumnSpec = {"kind": kind}
|
|
208
|
+
if kind == "string" and max_length is not None:
|
|
209
|
+
spec["length"] = max_length
|
|
210
|
+
elif kind == "decimal":
|
|
211
|
+
if max_digits is not None:
|
|
212
|
+
spec["precision"] = max_digits
|
|
213
|
+
if decimal_places is not None:
|
|
214
|
+
spec["scale"] = decimal_places
|
|
215
|
+
return spec
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _spec_from_enum_annotation(
|
|
219
|
+
python_type: Any,
|
|
220
|
+
db_type: str | None,
|
|
221
|
+
) -> ColumnSpec | None:
|
|
222
|
+
if db_type:
|
|
223
|
+
# Explicit db_type = plain verbatim column, enum machinery off.
|
|
224
|
+
return None
|
|
225
|
+
enum_info = _enum_annotation_info(python_type)
|
|
226
|
+
if enum_info is None:
|
|
227
|
+
return None
|
|
228
|
+
enum_type, is_array = enum_info
|
|
229
|
+
spec = _enum_spec(enum_type)
|
|
230
|
+
return {"kind": "array", "item": spec} if is_array else spec
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _enum_annotation_info(python_type: Any) -> tuple[type[Enum], bool] | None:
|
|
234
|
+
origin = get_origin(python_type)
|
|
235
|
+
if origin is list:
|
|
236
|
+
args = get_args(python_type)
|
|
237
|
+
if not args:
|
|
238
|
+
return None
|
|
239
|
+
inner = _enum_annotation_info(args[0])
|
|
240
|
+
return (inner[0], True) if inner is not None else None
|
|
241
|
+
if origin is not None:
|
|
242
|
+
for arg in get_args(python_type):
|
|
243
|
+
if arg is type(None):
|
|
244
|
+
continue
|
|
245
|
+
enum_info = _enum_annotation_info(arg)
|
|
246
|
+
if enum_info is not None:
|
|
247
|
+
return enum_info
|
|
248
|
+
return None
|
|
249
|
+
return (python_type, False) if _is_enum_type(python_type) else None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_enum_type(value: Any) -> bool:
|
|
253
|
+
return isinstance(value, type) and issubclass(value, Enum)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _enum_spec(enum_type: type[Enum]) -> ColumnSpec:
|
|
257
|
+
return {
|
|
258
|
+
"kind": "enum",
|
|
259
|
+
"name": _default_enum_type_name(enum_type),
|
|
260
|
+
"values": _enum_values(enum_type),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _enum_values(enum_type: type[Enum]) -> list[str]:
|
|
265
|
+
values = []
|
|
266
|
+
for member in enum_type:
|
|
267
|
+
value = member.value
|
|
268
|
+
if not isinstance(value, str):
|
|
269
|
+
raise TypeError(
|
|
270
|
+
f"Enum field '{enum_type.__name__}' must define string values. "
|
|
271
|
+
"Use a str-valued Enum for database enum columns, or annotate "
|
|
272
|
+
"the field as int for integer storage."
|
|
273
|
+
)
|
|
274
|
+
values.append(value)
|
|
275
|
+
return values
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _default_enum_type_name(enum_type: type[Enum]) -> str:
|
|
279
|
+
name = enum_type.__name__
|
|
280
|
+
parts: list[str] = []
|
|
281
|
+
for index, char in enumerate(name):
|
|
282
|
+
if char.isupper() and index > 0:
|
|
283
|
+
prev = name[index - 1]
|
|
284
|
+
next_char = name[index + 1] if index + 1 < len(name) else ""
|
|
285
|
+
if prev != "_" and (not prev.isupper() or next_char.islower()):
|
|
286
|
+
parts.append("_")
|
|
287
|
+
parts.append(char.lower())
|
|
288
|
+
return f"{''.join(parts)}_enum"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _split_type_params(upper: str) -> tuple[str, list[int]]:
|
|
292
|
+
"""Split "NUMERIC(10,2)" → ("NUMERIC", [10, 2]); no parens → (name, [])."""
|
|
293
|
+
if "(" not in upper:
|
|
294
|
+
return upper, []
|
|
295
|
+
base, _, rest = upper.partition("(")
|
|
296
|
+
params: list[int] = []
|
|
297
|
+
for part in rest.rstrip(")").split(","):
|
|
298
|
+
part = part.strip()
|
|
299
|
+
if part.isdigit():
|
|
300
|
+
params.append(int(part))
|
|
301
|
+
else:
|
|
302
|
+
return base.strip(), []
|
|
303
|
+
return base.strip(), params
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Legacy migration-file type names ("int", "str", "decimal", "custom_thing",
|
|
307
|
+
# "int[]") → spec kind. Used only by the legacy reader in migrations
|
|
308
|
+
# (to_snapshot normalization); becomes a hard error in 1.0.
|
|
309
|
+
_LEGACY_NAME_KINDS: dict[str, str] = {
|
|
310
|
+
"int": "big_integer",
|
|
311
|
+
"str": "string",
|
|
312
|
+
"float": "double",
|
|
313
|
+
"bool": "boolean",
|
|
314
|
+
"bytes": "blob",
|
|
315
|
+
"datetime": "date_time",
|
|
316
|
+
"date": "date",
|
|
317
|
+
"time": "time",
|
|
318
|
+
"timedelta": "timedelta",
|
|
319
|
+
"uuid": "uuid",
|
|
320
|
+
"decimal": "decimal",
|
|
321
|
+
"json": "json",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def spec_from_legacy_name(
|
|
326
|
+
name: str,
|
|
327
|
+
*,
|
|
328
|
+
max_length: int | None = None,
|
|
329
|
+
max_digits: int | None = None,
|
|
330
|
+
decimal_places: int | None = None,
|
|
331
|
+
) -> ColumnSpec:
|
|
332
|
+
"""Spec from a legacy migration-file type name.
|
|
333
|
+
|
|
334
|
+
Unknown names (custom annotation classes) collapse to ``unknown`` —
|
|
335
|
+
they always rendered as TEXT and bound natively. Never returns None:
|
|
336
|
+
``FieldDef.column_type`` is required on the Rust side.
|
|
337
|
+
"""
|
|
338
|
+
if name.endswith("[]"):
|
|
339
|
+
item = spec_from_legacy_name(
|
|
340
|
+
name[:-2],
|
|
341
|
+
max_length=max_length,
|
|
342
|
+
max_digits=max_digits,
|
|
343
|
+
decimal_places=decimal_places,
|
|
344
|
+
)
|
|
345
|
+
return {"kind": "array", "item": item}
|
|
346
|
+
|
|
347
|
+
kind = _LEGACY_NAME_KINDS.get(name)
|
|
348
|
+
if kind is None:
|
|
349
|
+
return {"kind": "unknown"}
|
|
350
|
+
|
|
351
|
+
spec: ColumnSpec = {"kind": kind}
|
|
352
|
+
if kind == "string" and max_length is not None:
|
|
353
|
+
spec["length"] = max_length
|
|
354
|
+
elif kind == "decimal":
|
|
355
|
+
if max_digits is not None:
|
|
356
|
+
spec["precision"] = max_digits
|
|
357
|
+
if decimal_places is not None:
|
|
358
|
+
spec["scale"] = decimal_places
|
|
359
|
+
return spec
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
__all__ = [
|
|
363
|
+
"ColumnSpec",
|
|
364
|
+
"compute_column_type",
|
|
365
|
+
"spec_for_literal",
|
|
366
|
+
"spec_from_legacy_name",
|
|
367
|
+
]
|