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
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"""Migration context for upgrade/downgrade operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from oxyde._msgpack import msgpack
|
|
9
|
+
from oxyde.core import (
|
|
10
|
+
begin_transaction,
|
|
11
|
+
commit_transaction,
|
|
12
|
+
execute_in_transaction,
|
|
13
|
+
migration_to_sql,
|
|
14
|
+
rollback_transaction,
|
|
15
|
+
)
|
|
16
|
+
from oxyde.core.ir import build_raw_sql_ir
|
|
17
|
+
from oxyde.db.pool import _msgpack_encoder
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from oxyde.db.pool import AsyncDatabase as DatabaseConnection
|
|
21
|
+
from oxyde.migrations.replay import SchemaState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_postgres_enum_add_value_sql(sql: str) -> bool:
|
|
25
|
+
"""Detect ALTER TYPE ... ADD VALUE in raw SQL — used only for ctx.execute()."""
|
|
26
|
+
upper = sql.strip().upper()
|
|
27
|
+
return upper.startswith("ALTER TYPE ") and " ADD VALUE " in upper
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MigrationContext:
|
|
31
|
+
"""Context for executing migrations.
|
|
32
|
+
|
|
33
|
+
Works in two modes (user doesn't know about this):
|
|
34
|
+
- "collect": Collects operations for replay (used by makemigrations)
|
|
35
|
+
- "execute": Generates and executes SQL (used by migrate)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
mode: str = "execute",
|
|
41
|
+
db_alias: str | None = None,
|
|
42
|
+
dialect: str | None = None,
|
|
43
|
+
db_conn: DatabaseConnection | None = None,
|
|
44
|
+
schema_state: SchemaState | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Initialize migration context.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
mode: Internal mode - "collect" or "execute"
|
|
50
|
+
db_alias: Database alias for execute mode (deprecated, use db_conn)
|
|
51
|
+
dialect: Database dialect (postgres)
|
|
52
|
+
db_conn: Database connection (for execute mode)
|
|
53
|
+
schema_state: Current schema state for migration replay
|
|
54
|
+
"""
|
|
55
|
+
self._mode = mode
|
|
56
|
+
self._db_alias = db_alias
|
|
57
|
+
self._dialect = dialect or "postgres"
|
|
58
|
+
from oxyde.migrations.utils import validate_dialect
|
|
59
|
+
|
|
60
|
+
validate_dialect(self._dialect)
|
|
61
|
+
self._db_conn = db_conn
|
|
62
|
+
self._schema_state = schema_state
|
|
63
|
+
self._operations: list[dict[str, Any]] = []
|
|
64
|
+
self._raw_sql_seen = False
|
|
65
|
+
self._sql_statements: list[str] = []
|
|
66
|
+
self._non_transactional_ddl = False
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def dialect(self) -> str:
|
|
70
|
+
"""Get current database dialect.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Database dialect: "postgres"
|
|
74
|
+
"""
|
|
75
|
+
return self._dialect
|
|
76
|
+
|
|
77
|
+
def create_enum_type(self, name: str, values: list[str]) -> None:
|
|
78
|
+
op = {
|
|
79
|
+
"type": "create_enum_type",
|
|
80
|
+
"name": name,
|
|
81
|
+
"values": values,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if self._mode == "collect":
|
|
85
|
+
self._operations.append(op)
|
|
86
|
+
else:
|
|
87
|
+
self._execute_operation(op)
|
|
88
|
+
|
|
89
|
+
def drop_enum_type(self, name: str) -> None:
|
|
90
|
+
op = {
|
|
91
|
+
"type": "drop_enum_type",
|
|
92
|
+
"name": name,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if self._mode == "collect":
|
|
96
|
+
self._operations.append(op)
|
|
97
|
+
else:
|
|
98
|
+
self._execute_operation(op)
|
|
99
|
+
|
|
100
|
+
def add_enum_value(
|
|
101
|
+
self,
|
|
102
|
+
name: str,
|
|
103
|
+
value: str,
|
|
104
|
+
fields: list[dict[str, Any]] | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
op = {
|
|
107
|
+
"type": "add_enum_value",
|
|
108
|
+
"name": name,
|
|
109
|
+
"value": value,
|
|
110
|
+
"fields": fields or [],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if self._mode == "collect":
|
|
114
|
+
self._operations.append(op)
|
|
115
|
+
else:
|
|
116
|
+
self._non_transactional_ddl = True
|
|
117
|
+
self._execute_operation(op)
|
|
118
|
+
|
|
119
|
+
def alter_enum_type(
|
|
120
|
+
self,
|
|
121
|
+
name: str,
|
|
122
|
+
old_values: list[str],
|
|
123
|
+
new_values: list[str],
|
|
124
|
+
) -> None:
|
|
125
|
+
op = {
|
|
126
|
+
"type": "alter_enum_type",
|
|
127
|
+
"name": name,
|
|
128
|
+
"old_values": old_values,
|
|
129
|
+
"new_values": new_values,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if self._mode == "collect":
|
|
133
|
+
self._operations.append(op)
|
|
134
|
+
|
|
135
|
+
def require_manual(self, message: str) -> None:
|
|
136
|
+
if self._mode == "execute":
|
|
137
|
+
raise RuntimeError(message)
|
|
138
|
+
|
|
139
|
+
# ========================================================================
|
|
140
|
+
# Table operations
|
|
141
|
+
# ========================================================================
|
|
142
|
+
|
|
143
|
+
def create_table(
|
|
144
|
+
self,
|
|
145
|
+
name: str,
|
|
146
|
+
fields: list[dict[str, Any]],
|
|
147
|
+
indexes: list[dict[str, Any]] | None = None,
|
|
148
|
+
foreign_keys: list[dict[str, Any]] | None = None,
|
|
149
|
+
checks: list[dict[str, Any]] | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Create a table.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Table name
|
|
155
|
+
fields: List of field definitions
|
|
156
|
+
indexes: List of index definitions (optional)
|
|
157
|
+
foreign_keys: List of foreign key definitions (optional)
|
|
158
|
+
checks: List of check constraint definitions (optional)
|
|
159
|
+
"""
|
|
160
|
+
op = {
|
|
161
|
+
"type": "create_table",
|
|
162
|
+
"table": {
|
|
163
|
+
"name": name,
|
|
164
|
+
"fields": fields,
|
|
165
|
+
"indexes": indexes or [],
|
|
166
|
+
"foreign_keys": foreign_keys or [],
|
|
167
|
+
"checks": checks or [],
|
|
168
|
+
"comment": None,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if self._mode == "collect":
|
|
173
|
+
self._operations.append(op)
|
|
174
|
+
else:
|
|
175
|
+
self._execute_operation(op)
|
|
176
|
+
|
|
177
|
+
def drop_table(self, name: str) -> None:
|
|
178
|
+
"""Drop a table.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
name: Table name
|
|
182
|
+
"""
|
|
183
|
+
op = {"type": "drop_table", "name": name}
|
|
184
|
+
|
|
185
|
+
if self._mode == "collect":
|
|
186
|
+
self._operations.append(op)
|
|
187
|
+
else:
|
|
188
|
+
self._execute_operation(op)
|
|
189
|
+
|
|
190
|
+
def rename_table(self, old_name: str, new_name: str) -> None:
|
|
191
|
+
"""Rename a table.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
old_name: Current table name
|
|
195
|
+
new_name: New table name
|
|
196
|
+
"""
|
|
197
|
+
op = {
|
|
198
|
+
"type": "rename_table",
|
|
199
|
+
"old_name": old_name,
|
|
200
|
+
"new_name": new_name,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if self._mode == "collect":
|
|
204
|
+
self._operations.append(op)
|
|
205
|
+
else:
|
|
206
|
+
self._execute_operation(op)
|
|
207
|
+
|
|
208
|
+
# ========================================================================
|
|
209
|
+
# Column operations
|
|
210
|
+
# ========================================================================
|
|
211
|
+
|
|
212
|
+
def add_column(self, table: str, field: dict[str, Any]) -> None:
|
|
213
|
+
"""Add a column to a table.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
table: Table name
|
|
217
|
+
field: Field definition dict
|
|
218
|
+
"""
|
|
219
|
+
op = {"type": "add_column", "table": table, "field": field}
|
|
220
|
+
|
|
221
|
+
if self._mode == "collect":
|
|
222
|
+
self._operations.append(op)
|
|
223
|
+
else:
|
|
224
|
+
self._execute_operation(op)
|
|
225
|
+
|
|
226
|
+
def drop_column(self, table: str, field_name: str) -> None:
|
|
227
|
+
"""Drop a column from a table.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
table: Table name
|
|
231
|
+
field_name: Column name to drop
|
|
232
|
+
"""
|
|
233
|
+
op = {"type": "drop_column", "table": table, "field": field_name}
|
|
234
|
+
|
|
235
|
+
if self._mode == "collect":
|
|
236
|
+
self._operations.append(op)
|
|
237
|
+
else:
|
|
238
|
+
self._execute_operation(op)
|
|
239
|
+
|
|
240
|
+
def rename_column(self, table: str, old_name: str, new_name: str) -> None:
|
|
241
|
+
"""Rename a column.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
table: Table name
|
|
245
|
+
old_name: Current column name
|
|
246
|
+
new_name: New column name
|
|
247
|
+
"""
|
|
248
|
+
op = {
|
|
249
|
+
"type": "rename_column",
|
|
250
|
+
"table": table,
|
|
251
|
+
"old_name": old_name,
|
|
252
|
+
"new_name": new_name,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if self._mode == "collect":
|
|
256
|
+
self._operations.append(op)
|
|
257
|
+
else:
|
|
258
|
+
self._execute_operation(op)
|
|
259
|
+
|
|
260
|
+
def alter_column(self, table: str, field_name: str, **changes: Any) -> None:
|
|
261
|
+
"""Alter column properties (nullable, type, default, etc.).
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
table: Table name
|
|
265
|
+
field_name: Column name
|
|
266
|
+
**changes: Properties to change (nullable, type, default, etc.)
|
|
267
|
+
"""
|
|
268
|
+
# For collect mode, store simple format
|
|
269
|
+
if self._mode == "collect":
|
|
270
|
+
op: dict[str, Any] = {
|
|
271
|
+
"type": "alter_column",
|
|
272
|
+
"table": table,
|
|
273
|
+
"column": field_name,
|
|
274
|
+
"changes": changes,
|
|
275
|
+
}
|
|
276
|
+
self._operations.append(op)
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# For execute mode, build Rust-compatible format with old_field/new_field
|
|
280
|
+
old_field = None
|
|
281
|
+
|
|
282
|
+
if self._schema_state:
|
|
283
|
+
table_schema = self._schema_state.tables.get(table)
|
|
284
|
+
if table_schema:
|
|
285
|
+
# Find old field definition
|
|
286
|
+
for field in table_schema["fields"]:
|
|
287
|
+
if field["name"] == field_name:
|
|
288
|
+
old_field = dict(field)
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
if old_field is None:
|
|
292
|
+
# Fallback: create minimal field definition
|
|
293
|
+
old_field = {
|
|
294
|
+
"name": field_name,
|
|
295
|
+
"column_type": changes.get("column_type", {"kind": "unknown"}),
|
|
296
|
+
"db_type": None,
|
|
297
|
+
"nullable": True,
|
|
298
|
+
"primary_key": False,
|
|
299
|
+
"unique": False,
|
|
300
|
+
"default": None,
|
|
301
|
+
"auto_increment": False,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Build new_field by applying changes to old_field
|
|
305
|
+
new_field = dict(old_field)
|
|
306
|
+
if "column_type" in changes:
|
|
307
|
+
new_field["column_type"] = changes["column_type"]
|
|
308
|
+
# Legacy keys from old migration files — drop stale column_type so
|
|
309
|
+
# normalization re-derives it from the legacy name
|
|
310
|
+
if "type" in changes:
|
|
311
|
+
new_field["python_type"] = changes["type"]
|
|
312
|
+
new_field.pop("column_type", None)
|
|
313
|
+
if "python_type" in changes:
|
|
314
|
+
new_field["python_type"] = changes["python_type"]
|
|
315
|
+
new_field.pop("column_type", None)
|
|
316
|
+
if "db_type" in changes:
|
|
317
|
+
new_field["db_type"] = changes["db_type"]
|
|
318
|
+
if "nullable" in changes:
|
|
319
|
+
new_field["nullable"] = changes["nullable"]
|
|
320
|
+
if "default" in changes:
|
|
321
|
+
new_field["default"] = changes["default"]
|
|
322
|
+
if "unique" in changes:
|
|
323
|
+
new_field["unique"] = changes["unique"]
|
|
324
|
+
if "max_length" in changes:
|
|
325
|
+
new_field["max_length"] = changes["max_length"]
|
|
326
|
+
if "max_digits" in changes:
|
|
327
|
+
new_field["max_digits"] = changes["max_digits"]
|
|
328
|
+
if "decimal_places" in changes:
|
|
329
|
+
new_field["decimal_places"] = changes["decimal_places"]
|
|
330
|
+
|
|
331
|
+
# Build Rust-compatible operation
|
|
332
|
+
op = {
|
|
333
|
+
"type": "alter_column",
|
|
334
|
+
"table": table,
|
|
335
|
+
"old_field": old_field,
|
|
336
|
+
"new_field": new_field,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
self._execute_operation(op)
|
|
340
|
+
|
|
341
|
+
# ========================================================================
|
|
342
|
+
# Index operations
|
|
343
|
+
# ========================================================================
|
|
344
|
+
|
|
345
|
+
def create_index(self, table: str, index: dict[str, Any]) -> None:
|
|
346
|
+
"""Create an index.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
table: Table name
|
|
350
|
+
index: Index definition dict
|
|
351
|
+
"""
|
|
352
|
+
op = {"type": "create_index", "table": table, "index": index}
|
|
353
|
+
|
|
354
|
+
if self._mode == "collect":
|
|
355
|
+
self._operations.append(op)
|
|
356
|
+
else:
|
|
357
|
+
self._execute_operation(op)
|
|
358
|
+
|
|
359
|
+
def drop_index(self, table: str, index_name: str) -> None:
|
|
360
|
+
"""Drop an index.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
table: Table name
|
|
364
|
+
index_name: Index name to drop
|
|
365
|
+
"""
|
|
366
|
+
op = {"type": "drop_index", "table": table, "name": index_name}
|
|
367
|
+
|
|
368
|
+
if self._mode == "collect":
|
|
369
|
+
self._operations.append(op)
|
|
370
|
+
else:
|
|
371
|
+
self._execute_operation(op)
|
|
372
|
+
|
|
373
|
+
# ========================================================================
|
|
374
|
+
# Foreign key operations
|
|
375
|
+
# ========================================================================
|
|
376
|
+
|
|
377
|
+
def add_foreign_key(
|
|
378
|
+
self,
|
|
379
|
+
table: str,
|
|
380
|
+
name: str,
|
|
381
|
+
columns: list[str],
|
|
382
|
+
ref_table: str,
|
|
383
|
+
ref_columns: list[str],
|
|
384
|
+
on_delete: str = "NO ACTION",
|
|
385
|
+
on_update: str = "NO ACTION",
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Add a foreign key constraint.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
table: Table name
|
|
391
|
+
name: Constraint name
|
|
392
|
+
columns: Local columns
|
|
393
|
+
ref_table: Referenced table name
|
|
394
|
+
ref_columns: Referenced columns
|
|
395
|
+
on_delete: ON DELETE action (CASCADE, SET NULL, RESTRICT, NO ACTION)
|
|
396
|
+
on_update: ON UPDATE action
|
|
397
|
+
"""
|
|
398
|
+
op = {
|
|
399
|
+
"type": "add_foreign_key",
|
|
400
|
+
"table": table,
|
|
401
|
+
"fk": {
|
|
402
|
+
"name": name,
|
|
403
|
+
"columns": columns,
|
|
404
|
+
"ref_table": ref_table,
|
|
405
|
+
"ref_columns": ref_columns,
|
|
406
|
+
"on_delete": on_delete,
|
|
407
|
+
"on_update": on_update,
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if self._mode == "collect":
|
|
412
|
+
self._operations.append(op)
|
|
413
|
+
else:
|
|
414
|
+
self._execute_operation(op)
|
|
415
|
+
|
|
416
|
+
def drop_foreign_key(self, table: str, name: str) -> None:
|
|
417
|
+
"""Drop a foreign key constraint.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
table: Table name
|
|
421
|
+
name: Constraint name to drop
|
|
422
|
+
"""
|
|
423
|
+
op = {"type": "drop_foreign_key", "table": table, "name": name}
|
|
424
|
+
|
|
425
|
+
if self._mode == "collect":
|
|
426
|
+
self._operations.append(op)
|
|
427
|
+
else:
|
|
428
|
+
self._execute_operation(op)
|
|
429
|
+
|
|
430
|
+
# ========================================================================
|
|
431
|
+
# Check constraint operations
|
|
432
|
+
# ========================================================================
|
|
433
|
+
|
|
434
|
+
def add_check(self, table: str, name: str, expression: str) -> None:
|
|
435
|
+
"""Add a check constraint.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
table: Table name
|
|
439
|
+
name: Constraint name
|
|
440
|
+
expression: SQL expression for the check
|
|
441
|
+
"""
|
|
442
|
+
op = {
|
|
443
|
+
"type": "add_check",
|
|
444
|
+
"table": table,
|
|
445
|
+
"check": {
|
|
446
|
+
"name": name,
|
|
447
|
+
"expression": expression,
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if self._mode == "collect":
|
|
452
|
+
self._operations.append(op)
|
|
453
|
+
else:
|
|
454
|
+
self._execute_operation(op)
|
|
455
|
+
|
|
456
|
+
def drop_check(self, table: str, name: str) -> None:
|
|
457
|
+
"""Drop a check constraint.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
table: Table name
|
|
461
|
+
name: Constraint name to drop
|
|
462
|
+
"""
|
|
463
|
+
op = {"type": "drop_check", "table": table, "name": name}
|
|
464
|
+
|
|
465
|
+
if self._mode == "collect":
|
|
466
|
+
self._operations.append(op)
|
|
467
|
+
else:
|
|
468
|
+
self._execute_operation(op)
|
|
469
|
+
|
|
470
|
+
# ========================================================================
|
|
471
|
+
# Custom operations
|
|
472
|
+
# ========================================================================
|
|
473
|
+
|
|
474
|
+
def execute(self, sql: str) -> None:
|
|
475
|
+
"""Execute arbitrary SQL (for data migrations, added manually).
|
|
476
|
+
|
|
477
|
+
This is ignored in "collect" mode (doesn't affect schema structure),
|
|
478
|
+
but the fact is recorded so `oxyde migrations squash` can warn that
|
|
479
|
+
the file contains manual SQL not carried over by the squash.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
sql: SQL statement to execute
|
|
483
|
+
"""
|
|
484
|
+
if self._mode == "execute":
|
|
485
|
+
if self._dialect == "postgres" and _is_postgres_enum_add_value_sql(sql):
|
|
486
|
+
self._non_transactional_ddl = True
|
|
487
|
+
self._execute_raw_sql(sql)
|
|
488
|
+
else:
|
|
489
|
+
# Collect mode: schema-neutral, but squash must know about it
|
|
490
|
+
self._raw_sql_seen = True
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def has_raw_sql(self) -> bool:
|
|
494
|
+
"""True if upgrade() called execute() with manual SQL (collect mode)."""
|
|
495
|
+
return self._raw_sql_seen
|
|
496
|
+
|
|
497
|
+
# ========================================================================
|
|
498
|
+
# Internal methods
|
|
499
|
+
# ========================================================================
|
|
500
|
+
|
|
501
|
+
def get_collected_operations(self) -> list[dict[str, Any]]:
|
|
502
|
+
"""Get collected operations (for replay). Internal use only.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
List of collected operations
|
|
506
|
+
"""
|
|
507
|
+
return self._operations
|
|
508
|
+
|
|
509
|
+
def _execute_operation(self, op: dict[str, Any]) -> None:
|
|
510
|
+
"""Convert operation to SQL and execute in database.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
op: Operation dictionary
|
|
514
|
+
"""
|
|
515
|
+
# Normalize legacy field dicts (python_type form) — Rust requires
|
|
516
|
+
# column_type. Single legacy funnel for the execute path.
|
|
517
|
+
from oxyde.migrations.utils import normalize_op_fields
|
|
518
|
+
|
|
519
|
+
op = normalize_op_fields(op)
|
|
520
|
+
|
|
521
|
+
# Convert operation to SQL using Rust
|
|
522
|
+
operations_json = json.dumps([op])
|
|
523
|
+
sql_statements = migration_to_sql(operations_json, self._dialect)
|
|
524
|
+
|
|
525
|
+
# Execute each SQL statement
|
|
526
|
+
for sql in sql_statements:
|
|
527
|
+
self._execute_raw_sql(sql)
|
|
528
|
+
|
|
529
|
+
def _execute_raw_sql(self, sql: str) -> None:
|
|
530
|
+
"""Execute SQL in database (collects SQL for batch execution).
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
sql: SQL statement
|
|
534
|
+
"""
|
|
535
|
+
if self._mode != "execute":
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
# Collect SQL statements for batch execution
|
|
539
|
+
# They will be executed by the executor after upgrade() completes
|
|
540
|
+
self._sql_statements.append(sql)
|
|
541
|
+
|
|
542
|
+
async def _execute_collected_sql(self) -> None:
|
|
543
|
+
"""Execute all collected SQL statements wrapped in a transaction.
|
|
544
|
+
|
|
545
|
+
This is called by the executor after upgrade() completes.
|
|
546
|
+
|
|
547
|
+
Transaction behavior by dialect:
|
|
548
|
+
- PostgreSQL: DDL is transactional except ALTER TYPE ADD VALUE chains
|
|
549
|
+
|
|
550
|
+
IMPORTANT: We use the Rust transaction API (begin_transaction/execute_in_transaction)
|
|
551
|
+
instead of raw BEGIN/COMMIT SQL because the connection pool may assign different
|
|
552
|
+
connections to different queries. The Rust transaction API ensures all queries
|
|
553
|
+
in a transaction use the same connection.
|
|
554
|
+
"""
|
|
555
|
+
if not self._sql_statements:
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if self._db_conn is None:
|
|
559
|
+
raise RuntimeError("Cannot execute SQL: no database connection provided")
|
|
560
|
+
|
|
561
|
+
use_transaction = self._should_use_transaction()
|
|
562
|
+
|
|
563
|
+
tx_id = None
|
|
564
|
+
try:
|
|
565
|
+
if use_transaction:
|
|
566
|
+
tx_id = await begin_transaction(self._db_conn.name)
|
|
567
|
+
|
|
568
|
+
for sql in self._sql_statements:
|
|
569
|
+
sql_ir = build_raw_sql_ir(sql=sql)
|
|
570
|
+
if tx_id is not None:
|
|
571
|
+
ir_bytes = msgpack.packb(sql_ir, default=_msgpack_encoder)
|
|
572
|
+
await execute_in_transaction(self._db_conn.name, tx_id, ir_bytes)
|
|
573
|
+
else:
|
|
574
|
+
await self._db_conn.execute(sql_ir)
|
|
575
|
+
|
|
576
|
+
if tx_id is not None:
|
|
577
|
+
await commit_transaction(tx_id)
|
|
578
|
+
|
|
579
|
+
except Exception:
|
|
580
|
+
if tx_id is not None:
|
|
581
|
+
try:
|
|
582
|
+
await rollback_transaction(tx_id)
|
|
583
|
+
except Exception:
|
|
584
|
+
pass # Ignore rollback errors
|
|
585
|
+
raise
|
|
586
|
+
|
|
587
|
+
finally:
|
|
588
|
+
# Clear collected statements
|
|
589
|
+
self._sql_statements = []
|
|
590
|
+
self._non_transactional_ddl = False
|
|
591
|
+
|
|
592
|
+
def _should_use_transaction(self) -> bool:
|
|
593
|
+
if self._non_transactional_ddl:
|
|
594
|
+
return False
|
|
595
|
+
return True
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
__all__ = ["MigrationContext"]
|