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,534 @@
|
|
|
1
|
+
"""Generate Python migration files from operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _python_repr(obj: Any, indent: int = 0, base_indent: int = 4) -> str:
|
|
11
|
+
"""Convert Python object to proper Python repr string.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
obj: Object to convert
|
|
15
|
+
indent: Current indentation level (in spaces)
|
|
16
|
+
base_indent: Base indentation increment
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Python code string
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(obj, dict):
|
|
22
|
+
if not obj:
|
|
23
|
+
return "{}"
|
|
24
|
+
|
|
25
|
+
items = []
|
|
26
|
+
for key, value in obj.items():
|
|
27
|
+
key_repr = repr(key) if isinstance(key, str) else str(key)
|
|
28
|
+
value_repr = _python_repr(value, indent + base_indent, base_indent)
|
|
29
|
+
items.append(f"{' ' * (indent + base_indent)}{key_repr}: {value_repr}")
|
|
30
|
+
|
|
31
|
+
return "{\n" + ",\n".join(items) + f"\n{' ' * indent}" + "}"
|
|
32
|
+
|
|
33
|
+
elif isinstance(obj, list):
|
|
34
|
+
if not obj:
|
|
35
|
+
return "[]"
|
|
36
|
+
|
|
37
|
+
items = []
|
|
38
|
+
for item in obj:
|
|
39
|
+
item_repr = _python_repr(item, indent + base_indent, base_indent)
|
|
40
|
+
items.append(f"{' ' * (indent + base_indent)}{item_repr}")
|
|
41
|
+
|
|
42
|
+
return "[\n" + ",\n".join(items) + f"\n{' ' * indent}" + "]"
|
|
43
|
+
|
|
44
|
+
elif isinstance(obj, str):
|
|
45
|
+
return repr(obj)
|
|
46
|
+
|
|
47
|
+
elif isinstance(obj, bool):
|
|
48
|
+
return "True" if obj else "False"
|
|
49
|
+
|
|
50
|
+
elif obj is None:
|
|
51
|
+
return "None"
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
return repr(obj)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _operation_to_python(op: dict[str, Any], indent: str = " ") -> str:
|
|
58
|
+
"""Convert a single operation to Python code.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
op: Operation dictionary
|
|
62
|
+
indent: Indentation string
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Python code string for this operation
|
|
66
|
+
"""
|
|
67
|
+
op_type = op.get("type")
|
|
68
|
+
|
|
69
|
+
if op_type == "create_enum_type":
|
|
70
|
+
values_repr = _python_repr(op["values"])
|
|
71
|
+
return (
|
|
72
|
+
f"{indent}ctx.create_enum_type({_python_repr(op['name'])}, {values_repr})"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
elif op_type == "drop_enum_type":
|
|
76
|
+
return f"{indent}ctx.drop_enum_type({_python_repr(op['name'])})"
|
|
77
|
+
|
|
78
|
+
elif op_type == "add_enum_value":
|
|
79
|
+
args = f"{_python_repr(op['name'])}, {_python_repr(op['value'])}"
|
|
80
|
+
if fields := op.get("fields"):
|
|
81
|
+
return (
|
|
82
|
+
f"{indent}ctx.add_enum_value(\n"
|
|
83
|
+
f"{indent} {args},\n"
|
|
84
|
+
f"{indent} fields={_python_repr(fields, indent=len(indent) + 11)},\n"
|
|
85
|
+
f"{indent})"
|
|
86
|
+
)
|
|
87
|
+
return f"{indent}ctx.add_enum_value({args})"
|
|
88
|
+
|
|
89
|
+
elif op_type == "alter_enum_type":
|
|
90
|
+
message = (
|
|
91
|
+
f"Manual enum migration required for {op['name']}: "
|
|
92
|
+
f"{op['old_values']!r} -> {op['new_values']!r}. "
|
|
93
|
+
"Replace ctx.require_manual(...) with ctx.execute(...) statements and keep "
|
|
94
|
+
"ctx.alter_enum_type(...) to update migration replay state."
|
|
95
|
+
)
|
|
96
|
+
return (
|
|
97
|
+
f"{indent}ctx.alter_enum_type(\n"
|
|
98
|
+
f"{indent} {_python_repr(op['name'])},\n"
|
|
99
|
+
f"{indent} old_values={_python_repr(op['old_values'], indent=len(indent) + 15)},\n"
|
|
100
|
+
f"{indent} new_values={_python_repr(op['new_values'], indent=len(indent) + 15)},\n"
|
|
101
|
+
f"{indent})\n"
|
|
102
|
+
f"{indent}ctx.require_manual({_python_repr(message)})"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
elif op_type == "create_table":
|
|
106
|
+
table = op["table"]
|
|
107
|
+
tname = table["name"]
|
|
108
|
+
# Use consistent indentation for all kwargs
|
|
109
|
+
inner_indent = len(indent) + 4
|
|
110
|
+
|
|
111
|
+
lines = [f"{indent}ctx.create_table(", f'{indent} "{tname}",']
|
|
112
|
+
|
|
113
|
+
fields_repr = _python_repr(table["fields"], indent=inner_indent)
|
|
114
|
+
lines.append(f"{indent} fields={fields_repr},")
|
|
115
|
+
|
|
116
|
+
if indexes := table.get("indexes", []):
|
|
117
|
+
indexes_repr = _python_repr(indexes, indent=inner_indent)
|
|
118
|
+
lines.append(f"{indent} indexes={indexes_repr},")
|
|
119
|
+
|
|
120
|
+
if foreign_keys := table.get("foreign_keys", []):
|
|
121
|
+
fk_repr = _python_repr(foreign_keys, indent=inner_indent)
|
|
122
|
+
lines.append(f"{indent} foreign_keys={fk_repr},")
|
|
123
|
+
|
|
124
|
+
if checks := table.get("checks", []):
|
|
125
|
+
checks_repr = _python_repr(checks, indent=inner_indent)
|
|
126
|
+
lines.append(f"{indent} checks={checks_repr},")
|
|
127
|
+
|
|
128
|
+
lines.append(f"{indent})")
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
elif op_type == "drop_table":
|
|
132
|
+
return f'{indent}ctx.drop_table("{op["name"]}")'
|
|
133
|
+
|
|
134
|
+
elif op_type == "rename_table":
|
|
135
|
+
return f'{indent}ctx.rename_table("{op["old_name"]}", "{op["new_name"]}")'
|
|
136
|
+
|
|
137
|
+
elif op_type == "add_column":
|
|
138
|
+
field_repr = _python_repr(op["field"])
|
|
139
|
+
return f'{indent}ctx.add_column("{op["table"]}", {field_repr})'
|
|
140
|
+
|
|
141
|
+
elif op_type == "drop_column":
|
|
142
|
+
return f'{indent}ctx.drop_column("{op["table"]}", "{op["field"]}")'
|
|
143
|
+
|
|
144
|
+
elif op_type == "rename_column":
|
|
145
|
+
t, old, new = op["table"], op["old_name"], op["new_name"]
|
|
146
|
+
return f'{indent}ctx.rename_column("{t}", "{old}", "{new}")'
|
|
147
|
+
|
|
148
|
+
elif op_type == "alter_column":
|
|
149
|
+
# Handle both formats:
|
|
150
|
+
# 1. Rust format: old_field/new_field FieldDef dicts
|
|
151
|
+
# 2. Python format: column + changes dict
|
|
152
|
+
if "old_field" in op and "new_field" in op:
|
|
153
|
+
# Rust format - extract column and build changes
|
|
154
|
+
old_field = op["old_field"]
|
|
155
|
+
new_field = op["new_field"]
|
|
156
|
+
column = new_field["name"]
|
|
157
|
+
changes = {}
|
|
158
|
+
|
|
159
|
+
# Compare fields and build changes dict
|
|
160
|
+
if old_field.get("column_type") != new_field.get("column_type"):
|
|
161
|
+
changes["column_type"] = new_field.get("column_type")
|
|
162
|
+
if old_field.get("db_type") != new_field.get("db_type"):
|
|
163
|
+
changes["db_type"] = new_field.get("db_type")
|
|
164
|
+
if old_field.get("nullable") != new_field.get("nullable"):
|
|
165
|
+
changes["nullable"] = new_field["nullable"]
|
|
166
|
+
if old_field.get("default") != new_field.get("default"):
|
|
167
|
+
changes["default"] = new_field.get("default")
|
|
168
|
+
if old_field.get("unique") != new_field.get("unique"):
|
|
169
|
+
changes["unique"] = new_field["unique"]
|
|
170
|
+
if old_field.get("max_length") != new_field.get("max_length"):
|
|
171
|
+
changes["max_length"] = new_field.get("max_length")
|
|
172
|
+
if old_field.get("max_digits") != new_field.get("max_digits"):
|
|
173
|
+
changes["max_digits"] = new_field.get("max_digits")
|
|
174
|
+
if old_field.get("decimal_places") != new_field.get("decimal_places"):
|
|
175
|
+
changes["decimal_places"] = new_field.get("decimal_places")
|
|
176
|
+
else:
|
|
177
|
+
# Python format - use as-is
|
|
178
|
+
column = op["column"]
|
|
179
|
+
changes = op.get("changes", {})
|
|
180
|
+
|
|
181
|
+
ch_repr = ", ".join(f"{k}={_python_repr(v)}" for k, v in changes.items())
|
|
182
|
+
return f'{indent}ctx.alter_column("{op["table"]}", "{column}", {ch_repr})'
|
|
183
|
+
|
|
184
|
+
elif op_type == "create_index":
|
|
185
|
+
index_repr = _python_repr(op["index"])
|
|
186
|
+
return f'{indent}ctx.create_index("{op["table"]}", {index_repr})'
|
|
187
|
+
|
|
188
|
+
elif op_type == "drop_index":
|
|
189
|
+
return f'{indent}ctx.drop_index("{op["table"]}", "{op["name"]}")'
|
|
190
|
+
|
|
191
|
+
elif op_type == "add_foreign_key":
|
|
192
|
+
fk = op["fk"]
|
|
193
|
+
columns_repr = repr(fk["columns"])
|
|
194
|
+
ref_columns_repr = repr(fk["ref_columns"])
|
|
195
|
+
on_delete = fk.get("on_delete", "NO ACTION")
|
|
196
|
+
on_update = fk.get("on_update", "NO ACTION")
|
|
197
|
+
return (
|
|
198
|
+
f"{indent}ctx.add_foreign_key(\n"
|
|
199
|
+
f'{indent} "{op["table"]}",\n'
|
|
200
|
+
f'{indent} "{fk["name"]}",\n'
|
|
201
|
+
f"{indent} {columns_repr},\n"
|
|
202
|
+
f'{indent} "{fk["ref_table"]}",\n'
|
|
203
|
+
f"{indent} {ref_columns_repr},\n"
|
|
204
|
+
f'{indent} on_delete="{on_delete}",\n'
|
|
205
|
+
f'{indent} on_update="{on_update}",\n'
|
|
206
|
+
f"{indent})"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
elif op_type == "drop_foreign_key":
|
|
210
|
+
return f'{indent}ctx.drop_foreign_key("{op["table"]}", "{op["name"]}")'
|
|
211
|
+
|
|
212
|
+
elif op_type == "add_check":
|
|
213
|
+
check = op["check"]
|
|
214
|
+
# Escape quotes in expression
|
|
215
|
+
expr = check["expression"].replace('"', '\\"')
|
|
216
|
+
return f'{indent}ctx.add_check("{op["table"]}", "{check["name"]}", "{expr}")'
|
|
217
|
+
|
|
218
|
+
elif op_type == "drop_check":
|
|
219
|
+
return f'{indent}ctx.drop_check("{op["table"]}", "{op["name"]}")'
|
|
220
|
+
|
|
221
|
+
return f"{indent}# Unknown operation: {op_type}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _infer_migration_name(operations: list[dict[str, Any]]) -> str:
|
|
225
|
+
"""Infer migration name from operations.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
operations: List of migration operations
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Migration name (e.g., "create_user_table")
|
|
232
|
+
"""
|
|
233
|
+
if not operations:
|
|
234
|
+
return "empty"
|
|
235
|
+
|
|
236
|
+
# Take first operation as basis
|
|
237
|
+
first_op = operations[0]
|
|
238
|
+
op_type = first_op.get("type")
|
|
239
|
+
|
|
240
|
+
if op_type == "create_enum_type":
|
|
241
|
+
return f"create_{first_op['name']}_enum"
|
|
242
|
+
elif op_type == "drop_enum_type":
|
|
243
|
+
return f"drop_{first_op['name']}_enum"
|
|
244
|
+
elif op_type == "add_enum_value":
|
|
245
|
+
return f"add_{first_op['value']}_to_{first_op['name']}_enum"
|
|
246
|
+
elif op_type == "alter_enum_type":
|
|
247
|
+
return f"alter_{first_op['name']}_enum"
|
|
248
|
+
elif op_type == "create_table":
|
|
249
|
+
table_name = first_op["table"]["name"]
|
|
250
|
+
return f"create_{table_name}_table"
|
|
251
|
+
elif op_type == "drop_table":
|
|
252
|
+
return f"drop_{first_op['name']}_table"
|
|
253
|
+
elif op_type == "add_column":
|
|
254
|
+
return f"add_{first_op['field']['name']}_to_{first_op['table']}"
|
|
255
|
+
elif op_type == "drop_column":
|
|
256
|
+
return f"drop_{first_op['field']}_from_{first_op['table']}"
|
|
257
|
+
elif op_type == "rename_table":
|
|
258
|
+
return f"rename_{first_op['old_name']}_to_{first_op['new_name']}"
|
|
259
|
+
elif op_type == "rename_column":
|
|
260
|
+
return f"rename_column_{first_op['old_name']}_to_{first_op['new_name']}"
|
|
261
|
+
|
|
262
|
+
# Multiple operations - use generic name
|
|
263
|
+
if len(operations) > 1:
|
|
264
|
+
return "auto"
|
|
265
|
+
|
|
266
|
+
return "migration"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_next_migration_number(migrations_dir: str | Path) -> str:
|
|
270
|
+
"""Get next migration number.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
migrations_dir: Path to migrations directory
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Next migration number as 4-digit string (e.g., "0001")
|
|
277
|
+
"""
|
|
278
|
+
migrations_path = Path(migrations_dir)
|
|
279
|
+
if not migrations_path.exists():
|
|
280
|
+
return "0001"
|
|
281
|
+
|
|
282
|
+
# Find existing migrations
|
|
283
|
+
existing = sorted(migrations_path.glob("[0-9]*.py"))
|
|
284
|
+
if not existing:
|
|
285
|
+
return "0001"
|
|
286
|
+
|
|
287
|
+
# Extract number from last migration
|
|
288
|
+
last_file = existing[-1].stem
|
|
289
|
+
try:
|
|
290
|
+
last_num = int(last_file.split("_")[0])
|
|
291
|
+
return f"{last_num + 1:04d}"
|
|
292
|
+
except (ValueError, IndexError):
|
|
293
|
+
return "0001"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _get_previous_migration(migrations_dir: str | Path) -> str | None:
|
|
297
|
+
"""Get the name of the previous (latest) migration.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
migrations_dir: Path to migrations directory
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Name of the previous migration (without .py) or None if no migrations exist
|
|
304
|
+
"""
|
|
305
|
+
migrations_path = Path(migrations_dir)
|
|
306
|
+
if not migrations_path.exists():
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
existing = sorted(migrations_path.glob("[0-9]*.py"))
|
|
310
|
+
if not existing:
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
return existing[-1].stem
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def generate_migration_file(
|
|
317
|
+
operations: list[dict[str, Any]],
|
|
318
|
+
migrations_dir: str | Path = "migrations",
|
|
319
|
+
name: str | None = None,
|
|
320
|
+
) -> Path:
|
|
321
|
+
"""Generate a Python migration file.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
operations: List of migration operations (from compute_diff)
|
|
325
|
+
migrations_dir: Directory to write migration file
|
|
326
|
+
name: Optional migration name (auto-inferred if not provided)
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Path to generated migration file
|
|
330
|
+
"""
|
|
331
|
+
migrations_path = Path(migrations_dir)
|
|
332
|
+
migrations_path.mkdir(parents=True, exist_ok=True)
|
|
333
|
+
|
|
334
|
+
# Determine migration name and number
|
|
335
|
+
migration_name = name or _infer_migration_name(operations)
|
|
336
|
+
migration_number = _get_next_migration_number(migrations_path)
|
|
337
|
+
filename = f"{migration_number}_{migration_name}.py"
|
|
338
|
+
filepath = migrations_path / filename
|
|
339
|
+
|
|
340
|
+
# Generate upgrade code
|
|
341
|
+
if operations:
|
|
342
|
+
upgrade_lines = [_operation_to_python(op) for op in operations]
|
|
343
|
+
upgrade_body = "\n".join(upgrade_lines)
|
|
344
|
+
else:
|
|
345
|
+
upgrade_body = " pass # No operations"
|
|
346
|
+
|
|
347
|
+
# Generate downgrade code (reverse operations)
|
|
348
|
+
downgrade_lines = []
|
|
349
|
+
for op in reversed(operations):
|
|
350
|
+
op_type = op.get("type")
|
|
351
|
+
|
|
352
|
+
# Generate reverse operation
|
|
353
|
+
if op_type == "create_enum_type":
|
|
354
|
+
downgrade_lines.append(
|
|
355
|
+
f" ctx.drop_enum_type({_python_repr(op['name'])})"
|
|
356
|
+
)
|
|
357
|
+
elif op_type == "drop_enum_type":
|
|
358
|
+
values = op.get("values")
|
|
359
|
+
if values:
|
|
360
|
+
values_repr = _python_repr(values)
|
|
361
|
+
downgrade_lines.append(
|
|
362
|
+
f" ctx.create_enum_type({_python_repr(op['name'])}, {values_repr})"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
message = f"Cannot recreate enum type {op['name']} without its values"
|
|
366
|
+
downgrade_lines.append(
|
|
367
|
+
f" raise RuntimeError({_python_repr(message)})"
|
|
368
|
+
)
|
|
369
|
+
elif op_type == "add_enum_value":
|
|
370
|
+
message = (
|
|
371
|
+
f"Cannot automatically remove enum value {op['value']} "
|
|
372
|
+
f"from {op['name']}"
|
|
373
|
+
)
|
|
374
|
+
downgrade_lines.append(f" raise RuntimeError({_python_repr(message)})")
|
|
375
|
+
elif op_type == "alter_enum_type":
|
|
376
|
+
message = (
|
|
377
|
+
f"Manual enum migration required for {op['name']}: "
|
|
378
|
+
f"{op['new_values']!r} -> {op['old_values']!r}. "
|
|
379
|
+
"Replace ctx.require_manual(...) with ctx.execute(...) statements and keep "
|
|
380
|
+
"ctx.alter_enum_type(...) to update migration replay state."
|
|
381
|
+
)
|
|
382
|
+
downgrade_lines.append(
|
|
383
|
+
f" ctx.alter_enum_type(\n"
|
|
384
|
+
f" {_python_repr(op['name'])},\n"
|
|
385
|
+
f" old_values={_python_repr(op['new_values'], indent=19)},\n"
|
|
386
|
+
f" new_values={_python_repr(op['old_values'], indent=19)},\n"
|
|
387
|
+
f" )\n"
|
|
388
|
+
f" ctx.require_manual({_python_repr(message)})"
|
|
389
|
+
)
|
|
390
|
+
elif op_type == "create_table":
|
|
391
|
+
downgrade_lines.append(f' ctx.drop_table("{op["table"]["name"]}")')
|
|
392
|
+
elif op_type == "drop_table":
|
|
393
|
+
# Reverse drop_table by recreating the table from stored structure
|
|
394
|
+
table_def = op.get("table")
|
|
395
|
+
if table_def:
|
|
396
|
+
fields_repr = _python_repr(table_def["fields"], indent=8 + 11)
|
|
397
|
+
indexes = table_def.get("indexes", [])
|
|
398
|
+
tname = table_def["name"]
|
|
399
|
+
if indexes:
|
|
400
|
+
indexes_repr = _python_repr(indexes, indent=8 + 13)
|
|
401
|
+
downgrade_lines.append(
|
|
402
|
+
f' ctx.create_table(\n "{tname}",\n'
|
|
403
|
+
f" fields={fields_repr},\n"
|
|
404
|
+
f" indexes={indexes_repr},\n )"
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
downgrade_lines.append(
|
|
408
|
+
f' ctx.create_table(\n "{tname}",\n'
|
|
409
|
+
f" fields={fields_repr},\n )"
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
downgrade_lines.append(
|
|
413
|
+
f" # TODO: Reverse drop_table for {op['name']}"
|
|
414
|
+
)
|
|
415
|
+
elif op_type == "add_column":
|
|
416
|
+
downgrade_lines.append(
|
|
417
|
+
f' ctx.drop_column("{op["table"]}", "{op["field"]["name"]}")'
|
|
418
|
+
)
|
|
419
|
+
elif op_type == "drop_column":
|
|
420
|
+
# Reverse drop_column by adding the column back from stored definition
|
|
421
|
+
field_def = op.get("field_def")
|
|
422
|
+
if field_def:
|
|
423
|
+
field_repr = _python_repr(field_def)
|
|
424
|
+
downgrade_lines.append(
|
|
425
|
+
f' ctx.add_column("{op["table"]}", {field_repr})'
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
downgrade_lines.append(
|
|
429
|
+
f" # TODO: Reverse drop_column for {op['table']}.{op['field']}"
|
|
430
|
+
)
|
|
431
|
+
elif op_type == "rename_table":
|
|
432
|
+
downgrade_lines.append(
|
|
433
|
+
f' ctx.rename_table("{op["new_name"]}", "{op["old_name"]}")'
|
|
434
|
+
)
|
|
435
|
+
elif op_type == "rename_column":
|
|
436
|
+
t, new, old = op["table"], op["new_name"], op["old_name"]
|
|
437
|
+
downgrade_lines.append(f' ctx.rename_column("{t}", "{new}", "{old}")')
|
|
438
|
+
elif op_type == "create_index":
|
|
439
|
+
downgrade_lines.append(
|
|
440
|
+
f' ctx.drop_index("{op["table"]}", "{op["index"]["name"]}")'
|
|
441
|
+
)
|
|
442
|
+
elif op_type == "drop_index":
|
|
443
|
+
# Reverse drop_index by recreating the index from stored definition
|
|
444
|
+
index_def = op.get("index_def")
|
|
445
|
+
if index_def:
|
|
446
|
+
index_repr = _python_repr(index_def)
|
|
447
|
+
downgrade_lines.append(
|
|
448
|
+
f' ctx.create_index("{op["table"]}", {index_repr})'
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
downgrade_lines.append(
|
|
452
|
+
f" # TODO: Reverse drop_index for {op['table']}.{op['index']}"
|
|
453
|
+
)
|
|
454
|
+
elif op_type == "add_foreign_key":
|
|
455
|
+
downgrade_lines.append(
|
|
456
|
+
f' ctx.drop_foreign_key("{op["table"]}", "{op["fk"]["name"]}")'
|
|
457
|
+
)
|
|
458
|
+
elif op_type == "drop_foreign_key":
|
|
459
|
+
# Reverse drop_foreign_key by recreating the FK from stored definition
|
|
460
|
+
fk_def = op.get("fk_def")
|
|
461
|
+
if fk_def:
|
|
462
|
+
columns_repr = repr(fk_def["columns"])
|
|
463
|
+
ref_columns_repr = repr(fk_def["ref_columns"])
|
|
464
|
+
on_delete = fk_def.get("on_delete", "NO ACTION")
|
|
465
|
+
on_update = fk_def.get("on_update", "NO ACTION")
|
|
466
|
+
downgrade_lines.append(
|
|
467
|
+
f" ctx.add_foreign_key(\n"
|
|
468
|
+
f' "{op["table"]}",\n'
|
|
469
|
+
f' "{fk_def["name"]}",\n'
|
|
470
|
+
f" {columns_repr},\n"
|
|
471
|
+
f' "{fk_def["ref_table"]}",\n'
|
|
472
|
+
f" {ref_columns_repr},\n"
|
|
473
|
+
f' on_delete="{on_delete}",\n'
|
|
474
|
+
f' on_update="{on_update}",\n'
|
|
475
|
+
f" )"
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
t, n = op["table"], op["name"]
|
|
479
|
+
downgrade_lines.append(
|
|
480
|
+
f" # TODO: Reverse drop_foreign_key for {t}.{n}"
|
|
481
|
+
)
|
|
482
|
+
elif op_type == "add_check":
|
|
483
|
+
downgrade_lines.append(
|
|
484
|
+
f' ctx.drop_check("{op["table"]}", "{op["check"]["name"]}")'
|
|
485
|
+
)
|
|
486
|
+
elif op_type == "drop_check":
|
|
487
|
+
# Reverse drop_check by recreating the check from stored definition
|
|
488
|
+
check_def = op.get("check_def")
|
|
489
|
+
if check_def:
|
|
490
|
+
# Escape quotes in expression
|
|
491
|
+
expr = check_def["expression"].replace('"', '\\"')
|
|
492
|
+
t, name = op["table"], check_def["name"]
|
|
493
|
+
downgrade_lines.append(f' ctx.add_check("{t}", "{name}", "{expr}")')
|
|
494
|
+
else:
|
|
495
|
+
downgrade_lines.append(
|
|
496
|
+
f" # TODO: Reverse drop_check for {op['table']}.{op['name']}"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
downgrade_body = "\n".join(downgrade_lines) if downgrade_lines else " pass"
|
|
500
|
+
|
|
501
|
+
# Get previous migration for dependency
|
|
502
|
+
previous_migration = _get_previous_migration(migrations_path)
|
|
503
|
+
depends_on_line = (
|
|
504
|
+
f'depends_on = "{previous_migration}"'
|
|
505
|
+
if previous_migration
|
|
506
|
+
else "depends_on = None"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Generate file content
|
|
510
|
+
content = f'''"""Auto-generated migration.
|
|
511
|
+
|
|
512
|
+
Created: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
{depends_on_line}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def upgrade(ctx):
|
|
519
|
+
"""Apply migration."""
|
|
520
|
+
{upgrade_body}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def downgrade(ctx):
|
|
524
|
+
"""Revert migration."""
|
|
525
|
+
{downgrade_body}
|
|
526
|
+
'''
|
|
527
|
+
|
|
528
|
+
# Write file
|
|
529
|
+
filepath.write_text(content)
|
|
530
|
+
|
|
531
|
+
return filepath
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
__all__ = ["generate_migration_file"]
|