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/cli/migrations.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Migration commands (makemigrations, migrate, showmigrations, sqlmigrate)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from oxyde.cli.app import (
|
|
10
|
+
app,
|
|
11
|
+
ensure_migrations_dir,
|
|
12
|
+
init_databases,
|
|
13
|
+
load_config_or_exit,
|
|
14
|
+
require_databases,
|
|
15
|
+
)
|
|
16
|
+
from oxyde.migrations.config import import_models
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def makemigrations(
|
|
21
|
+
name: str | None = typer.Option(None, help="Migration name"),
|
|
22
|
+
dry_run: bool = typer.Option(
|
|
23
|
+
False, help="Show what would be created without actually creating"
|
|
24
|
+
),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Create migration files by comparing current models with replayed migrations.
|
|
28
|
+
|
|
29
|
+
Scans all Model subclasses, replays existing migrations,
|
|
30
|
+
computes diff, and generates a new migration file if changes detected.
|
|
31
|
+
"""
|
|
32
|
+
from oxyde.core import migration_compute_diff
|
|
33
|
+
from oxyde.migrations import (
|
|
34
|
+
extract_current_schema,
|
|
35
|
+
generate_migration_file,
|
|
36
|
+
replay_migrations,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Load config
|
|
40
|
+
config = load_config_or_exit()
|
|
41
|
+
|
|
42
|
+
typer.echo("📝 Creating migrations...")
|
|
43
|
+
typer.echo()
|
|
44
|
+
|
|
45
|
+
# Import models
|
|
46
|
+
typer.echo("0️⃣ Loading models...")
|
|
47
|
+
imported = import_models(config.models)
|
|
48
|
+
if imported == 0:
|
|
49
|
+
typer.secho(" ❌ No modules imported", fg=typer.colors.RED)
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
typer.echo(f" ✅ Imported {imported} module(s)")
|
|
52
|
+
|
|
53
|
+
# Step 1: Extract current schema from models
|
|
54
|
+
typer.echo()
|
|
55
|
+
typer.echo("1️⃣ Extracting schema from models...")
|
|
56
|
+
try:
|
|
57
|
+
current_schema = extract_current_schema(dialect=config.dialect)
|
|
58
|
+
table_count = len(current_schema["tables"])
|
|
59
|
+
tables = ", ".join(current_schema["tables"].keys())
|
|
60
|
+
if table_count > 0:
|
|
61
|
+
typer.echo(f" ✅ Found {table_count} table(s): {tables}")
|
|
62
|
+
else:
|
|
63
|
+
typer.secho(" ⚠️ No tables found", fg=typer.colors.YELLOW)
|
|
64
|
+
typer.echo(" Make sure your models have 'class Meta: is_table = True'")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
typer.secho(f" ❌ Error extracting schema: {e}", fg=typer.colors.RED)
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
# Step 2: Replay existing migrations
|
|
70
|
+
typer.echo()
|
|
71
|
+
typer.echo("2️⃣ Replaying existing migrations...")
|
|
72
|
+
migrations_path = ensure_migrations_dir(config.migrations_dir, dry_run=dry_run)
|
|
73
|
+
|
|
74
|
+
if not migrations_path.exists():
|
|
75
|
+
old_schema = {"version": 1, "tables": {}}
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
old_schema = replay_migrations(config.migrations_dir)
|
|
79
|
+
migration_count = len(list(migrations_path.glob("[0-9]*.py")))
|
|
80
|
+
typer.echo(f" ✅ Replayed {migration_count} migration(s)")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
typer.secho(f" ❌ Error replaying migrations: {e}", fg=typer.colors.RED)
|
|
83
|
+
typer.echo(" Fix the broken migration(s) before running makemigrations.")
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
# Step 3: Compute diff
|
|
87
|
+
typer.echo()
|
|
88
|
+
typer.echo("3️⃣ Computing diff...")
|
|
89
|
+
try:
|
|
90
|
+
operations_json = migration_compute_diff(
|
|
91
|
+
json.dumps(old_schema), json.dumps(current_schema)
|
|
92
|
+
)
|
|
93
|
+
operations = json.loads(operations_json)
|
|
94
|
+
|
|
95
|
+
if not operations:
|
|
96
|
+
typer.echo()
|
|
97
|
+
typer.secho(" ✨ No changes detected", fg=typer.colors.GREEN)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
typer.echo(f" ✅ Found {len(operations)} operation(s):")
|
|
101
|
+
for op in operations:
|
|
102
|
+
op_type = op.get("type", "unknown")
|
|
103
|
+
if op_type == "create_table":
|
|
104
|
+
typer.echo(f" - Create table: {op['table']['name']}")
|
|
105
|
+
elif op_type == "drop_table":
|
|
106
|
+
typer.echo(f" - Drop table: {op['name']}")
|
|
107
|
+
elif op_type == "add_column":
|
|
108
|
+
typer.echo(f" - Add column: {op['table']}.{op['field']['name']}")
|
|
109
|
+
elif op_type == "drop_column":
|
|
110
|
+
typer.echo(f" - Drop column: {op['table']}.{op['field']}")
|
|
111
|
+
elif op_type == "alter_enum_type":
|
|
112
|
+
typer.secho(
|
|
113
|
+
f" - Manual enum change: {op['name']} "
|
|
114
|
+
f"{op['old_values']} -> {op['new_values']}",
|
|
115
|
+
fg=typer.colors.YELLOW,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
typer.echo(f" - {op_type}")
|
|
119
|
+
|
|
120
|
+
if any(op.get("type") == "alter_enum_type" for op in operations):
|
|
121
|
+
typer.secho(
|
|
122
|
+
" ⚠️ One or more enum types changed in a way that requires "
|
|
123
|
+
"manual SQL. The migration file will include a ctx.require_manual(...) "
|
|
124
|
+
"guard; replace it with ctx.execute(...) and keep ctx.alter_enum_type(...).",
|
|
125
|
+
fg=typer.colors.YELLOW,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
typer.secho(f" ❌ Error computing diff: {e}", fg=typer.colors.RED)
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
# Step 4: Generate migration file
|
|
133
|
+
typer.echo()
|
|
134
|
+
if dry_run:
|
|
135
|
+
typer.secho(" [DRY RUN] Would create migration file", fg=typer.colors.YELLOW)
|
|
136
|
+
typer.echo(f" Migration name: {name or 'auto'}")
|
|
137
|
+
typer.echo(f" Operations: {len(operations)}")
|
|
138
|
+
else:
|
|
139
|
+
typer.echo("4️⃣ Generating migration file...")
|
|
140
|
+
try:
|
|
141
|
+
filepath = generate_migration_file(
|
|
142
|
+
operations,
|
|
143
|
+
migrations_dir=config.migrations_dir,
|
|
144
|
+
name=name,
|
|
145
|
+
)
|
|
146
|
+
typer.echo()
|
|
147
|
+
typer.secho(f" ✅ Created: {filepath}", fg=typer.colors.GREEN, bold=True)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
typer.secho(f" ❌ Error generating migration: {e}", fg=typer.colors.RED)
|
|
150
|
+
raise typer.Exit(1)
|
|
151
|
+
|
|
152
|
+
# Step 5: Generate type stubs
|
|
153
|
+
typer.echo()
|
|
154
|
+
typer.echo("5️⃣ Generating type stubs...")
|
|
155
|
+
try:
|
|
156
|
+
from oxyde.codegen import generate_stubs_for_models, write_stubs
|
|
157
|
+
from oxyde.models.registry import registered_tables
|
|
158
|
+
|
|
159
|
+
models = list(registered_tables().values())
|
|
160
|
+
if models:
|
|
161
|
+
stub_mapping = generate_stubs_for_models(models)
|
|
162
|
+
write_stubs(stub_mapping)
|
|
163
|
+
typer.secho(
|
|
164
|
+
f" ✅ Generated {len(stub_mapping)} stub file(s)",
|
|
165
|
+
fg=typer.colors.GREEN,
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
typer.echo(" ⚠️ No models to generate stubs for")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
typer.secho(
|
|
171
|
+
f" ⚠️ Warning: Could not generate stubs: {e}",
|
|
172
|
+
fg=typer.colors.YELLOW,
|
|
173
|
+
)
|
|
174
|
+
# Don't fail migration on stub generation errors
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def migrate(
|
|
179
|
+
target: str | None = typer.Argument(None, help="Target migration name (e.g. 0001)"),
|
|
180
|
+
fake: bool = typer.Option(
|
|
181
|
+
False, help="Mark migrations as applied without running SQL"
|
|
182
|
+
),
|
|
183
|
+
db_alias: str = typer.Option("default", help="Database connection alias"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Apply all pending migrations.
|
|
187
|
+
|
|
188
|
+
Runs all migrations that haven't been applied yet.
|
|
189
|
+
Specify migration name to migrate to a specific version.
|
|
190
|
+
"""
|
|
191
|
+
from oxyde.migrations import (
|
|
192
|
+
apply_migrations,
|
|
193
|
+
get_applied_migrations,
|
|
194
|
+
get_pending_migrations,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Load config
|
|
198
|
+
config = load_config_or_exit()
|
|
199
|
+
require_databases(config, db_alias)
|
|
200
|
+
|
|
201
|
+
typer.echo("⏳ Applying migrations...")
|
|
202
|
+
typer.echo()
|
|
203
|
+
|
|
204
|
+
if fake:
|
|
205
|
+
typer.secho(
|
|
206
|
+
"⚠️ [FAKE MODE] Marking migrations as applied without executing SQL",
|
|
207
|
+
fg=typer.colors.YELLOW,
|
|
208
|
+
)
|
|
209
|
+
typer.echo()
|
|
210
|
+
|
|
211
|
+
async def run_migrate() -> tuple[str | None, list]:
|
|
212
|
+
from oxyde.migrations import get_migration_files, rollback_migrations
|
|
213
|
+
|
|
214
|
+
# Initialize database connection
|
|
215
|
+
await init_databases({db_alias: config.databases[db_alias]})
|
|
216
|
+
|
|
217
|
+
# Get current state
|
|
218
|
+
applied = await get_applied_migrations(db_alias)
|
|
219
|
+
all_migrations = get_migration_files(config.migrations_dir)
|
|
220
|
+
|
|
221
|
+
# If target specified, check if we need to rollback
|
|
222
|
+
if target:
|
|
223
|
+
# Special case: "zero" means rollback all migrations
|
|
224
|
+
if target.lower() == "zero":
|
|
225
|
+
if not applied:
|
|
226
|
+
typer.secho("✨ No migrations to roll back", fg=typer.colors.GREEN)
|
|
227
|
+
return "rollback", []
|
|
228
|
+
|
|
229
|
+
typer.echo(f"Rolling back all {len(applied)} migration(s)...")
|
|
230
|
+
typer.echo()
|
|
231
|
+
|
|
232
|
+
rolled_back = await rollback_migrations(
|
|
233
|
+
steps=len(applied),
|
|
234
|
+
migrations_dir=config.migrations_dir,
|
|
235
|
+
db_alias=db_alias,
|
|
236
|
+
fake=fake,
|
|
237
|
+
)
|
|
238
|
+
return "rollback", rolled_back
|
|
239
|
+
|
|
240
|
+
# Find target migration index
|
|
241
|
+
target_idx = -1
|
|
242
|
+
for i, m in enumerate(all_migrations):
|
|
243
|
+
if m.stem == target or m.stem.startswith(target):
|
|
244
|
+
target_idx = i
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
if target_idx == -1:
|
|
248
|
+
typer.secho(f"❌ Migration '{target}' not found", fg=typer.colors.RED)
|
|
249
|
+
return None, []
|
|
250
|
+
|
|
251
|
+
# Find current position (last applied migration)
|
|
252
|
+
current_idx = -1
|
|
253
|
+
for i, m in enumerate(all_migrations):
|
|
254
|
+
if m.stem in applied:
|
|
255
|
+
current_idx = i
|
|
256
|
+
|
|
257
|
+
# If target is before current position, rollback
|
|
258
|
+
if target_idx < current_idx:
|
|
259
|
+
steps = current_idx - target_idx
|
|
260
|
+
typer.echo(f"Rolling back {steps} migration(s) to reach {target}...")
|
|
261
|
+
typer.echo()
|
|
262
|
+
|
|
263
|
+
rolled_back = await rollback_migrations(
|
|
264
|
+
steps=steps,
|
|
265
|
+
migrations_dir=config.migrations_dir,
|
|
266
|
+
db_alias=db_alias,
|
|
267
|
+
fake=fake,
|
|
268
|
+
)
|
|
269
|
+
return "rollback", rolled_back
|
|
270
|
+
|
|
271
|
+
# Forward migration
|
|
272
|
+
pending = get_pending_migrations(config.migrations_dir, applied)
|
|
273
|
+
|
|
274
|
+
if not pending:
|
|
275
|
+
typer.secho("✨ No pending migrations", fg=typer.colors.GREEN)
|
|
276
|
+
return "apply", []
|
|
277
|
+
|
|
278
|
+
# Filter pending if target specified
|
|
279
|
+
if target:
|
|
280
|
+
filtered = []
|
|
281
|
+
for m in pending:
|
|
282
|
+
filtered.append(m)
|
|
283
|
+
if m.stem == target or m.stem.startswith(target):
|
|
284
|
+
break
|
|
285
|
+
pending = filtered
|
|
286
|
+
|
|
287
|
+
# Show what will be applied
|
|
288
|
+
typer.echo(f"Found {len(pending)} pending migration(s):")
|
|
289
|
+
for migration_path in pending:
|
|
290
|
+
typer.echo(f" - {migration_path.stem}")
|
|
291
|
+
|
|
292
|
+
typer.echo()
|
|
293
|
+
|
|
294
|
+
# Apply migrations
|
|
295
|
+
if target:
|
|
296
|
+
typer.echo(f"Migrating to: {target}")
|
|
297
|
+
else:
|
|
298
|
+
typer.echo("Migrating to latest...")
|
|
299
|
+
|
|
300
|
+
applied_migrations = await apply_migrations(
|
|
301
|
+
migrations_dir=config.migrations_dir,
|
|
302
|
+
db_alias=db_alias,
|
|
303
|
+
target=target,
|
|
304
|
+
fake=fake,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return "apply", applied_migrations
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
result = asyncio.run(run_migrate())
|
|
311
|
+
if result is None or result[0] is None:
|
|
312
|
+
raise typer.Exit(1)
|
|
313
|
+
|
|
314
|
+
action, migrations = result
|
|
315
|
+
|
|
316
|
+
if migrations:
|
|
317
|
+
typer.echo()
|
|
318
|
+
if action == "rollback":
|
|
319
|
+
typer.secho(
|
|
320
|
+
f"✅ Rolled back {len(migrations)} migration(s)",
|
|
321
|
+
fg=typer.colors.GREEN,
|
|
322
|
+
bold=True,
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
typer.secho(
|
|
326
|
+
f"✅ Applied {len(migrations)} migration(s)",
|
|
327
|
+
fg=typer.colors.GREEN,
|
|
328
|
+
bold=True,
|
|
329
|
+
)
|
|
330
|
+
for name in migrations:
|
|
331
|
+
typer.echo(f" - {name}")
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
typer.secho(f"❌ Error applying migrations: {e}", fg=typer.colors.RED)
|
|
335
|
+
raise typer.Exit(1)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@app.command()
|
|
339
|
+
def showmigrations(
|
|
340
|
+
db_alias: str = typer.Option("default", help="Database connection alias"),
|
|
341
|
+
) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Show list of all migrations with their status (applied/pending).
|
|
344
|
+
"""
|
|
345
|
+
from oxyde.migrations import get_applied_migrations, get_migration_files
|
|
346
|
+
|
|
347
|
+
# Load config
|
|
348
|
+
config = load_config_or_exit()
|
|
349
|
+
require_databases(config, db_alias)
|
|
350
|
+
|
|
351
|
+
typer.echo("📋 Migrations status:")
|
|
352
|
+
typer.echo()
|
|
353
|
+
|
|
354
|
+
async def run_show() -> set[str]:
|
|
355
|
+
# Initialize database connection
|
|
356
|
+
await init_databases({db_alias: config.databases[db_alias]})
|
|
357
|
+
|
|
358
|
+
# Get applied migrations
|
|
359
|
+
applied = await get_applied_migrations(db_alias)
|
|
360
|
+
return set(applied)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
applied_set = asyncio.run(run_show())
|
|
364
|
+
|
|
365
|
+
# Get all migration files
|
|
366
|
+
all_migrations = get_migration_files(config.migrations_dir)
|
|
367
|
+
|
|
368
|
+
if not all_migrations:
|
|
369
|
+
typer.secho("No migrations found", fg=typer.colors.YELLOW)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
# Show status for each migration
|
|
373
|
+
for migration_path in all_migrations:
|
|
374
|
+
name = migration_path.stem
|
|
375
|
+
if name in applied_set:
|
|
376
|
+
typer.secho(f" [✓] {name}", fg=typer.colors.GREEN)
|
|
377
|
+
else:
|
|
378
|
+
typer.echo(f" [ ] {name}")
|
|
379
|
+
|
|
380
|
+
typer.echo()
|
|
381
|
+
typer.echo(f"Total: {len(all_migrations)} migration(s)")
|
|
382
|
+
typer.echo(f"Applied: {len(applied_set)}")
|
|
383
|
+
typer.echo(f"Pending: {len(all_migrations) - len(applied_set)}")
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
typer.secho(f"❌ Error reading migrations: {e}", fg=typer.colors.RED)
|
|
387
|
+
raise typer.Exit(1)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@app.command()
|
|
391
|
+
def sqlmigrate(
|
|
392
|
+
name: str,
|
|
393
|
+
) -> None:
|
|
394
|
+
"""
|
|
395
|
+
Print the SQL for a specific migration.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
name: Name of the migration file (e.g., 0001_initial)
|
|
399
|
+
"""
|
|
400
|
+
from oxyde.core import migration_to_sql
|
|
401
|
+
from oxyde.migrations.context import MigrationContext
|
|
402
|
+
from oxyde.migrations.utils import load_migration_module
|
|
403
|
+
|
|
404
|
+
# Load config
|
|
405
|
+
config = load_config_or_exit()
|
|
406
|
+
|
|
407
|
+
typer.echo(f"📝 SQL for migration: {name}")
|
|
408
|
+
typer.echo()
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
# Find migration file
|
|
412
|
+
migration_path = Path(config.migrations_dir) / f"{name}.py"
|
|
413
|
+
if not migration_path.exists():
|
|
414
|
+
typer.secho(f"❌ Migration not found: {name}", fg=typer.colors.RED)
|
|
415
|
+
raise typer.Exit(1)
|
|
416
|
+
|
|
417
|
+
# Import migration module
|
|
418
|
+
module = load_migration_module(migration_path)
|
|
419
|
+
|
|
420
|
+
if module is None or not hasattr(module, "upgrade"):
|
|
421
|
+
typer.secho("❌ Migration missing upgrade() function", fg=typer.colors.RED)
|
|
422
|
+
raise typer.Exit(1)
|
|
423
|
+
|
|
424
|
+
# Create context in collect mode to get operations
|
|
425
|
+
ctx = MigrationContext(mode="collect", dialect=config.dialect)
|
|
426
|
+
module.upgrade(ctx)
|
|
427
|
+
|
|
428
|
+
operations = ctx.get_collected_operations()
|
|
429
|
+
|
|
430
|
+
if not operations:
|
|
431
|
+
typer.echo("-- No operations (empty migration)")
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
# Convert operations to SQL
|
|
435
|
+
operations_json = json.dumps(operations)
|
|
436
|
+
sql_statements = migration_to_sql(operations_json, config.dialect)
|
|
437
|
+
|
|
438
|
+
# Print SQL
|
|
439
|
+
typer.secho(f"-- Generated SQL ({config.dialect}):", fg=typer.colors.CYAN)
|
|
440
|
+
typer.echo()
|
|
441
|
+
for sql in sql_statements:
|
|
442
|
+
typer.echo(sql)
|
|
443
|
+
typer.echo()
|
|
444
|
+
|
|
445
|
+
# Manual ops emit no SQL — surface them so the preview is not empty.
|
|
446
|
+
for op in operations:
|
|
447
|
+
if op.get("type") == "alter_enum_type":
|
|
448
|
+
typer.secho(
|
|
449
|
+
f"-- manual migration required: enum {op['name']} "
|
|
450
|
+
f"{op['old_values']} -> {op['new_values']} "
|
|
451
|
+
"(no automatic SQL; the migration file pairs this with "
|
|
452
|
+
"ctx.require_manual(...))",
|
|
453
|
+
fg=typer.colors.YELLOW,
|
|
454
|
+
)
|
|
455
|
+
typer.echo()
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
typer.secho(f"❌ Error generating SQL: {e}", fg=typer.colors.RED)
|
|
459
|
+
raise typer.Exit(1)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ── oxyde migrations <subcommand> ────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
migrations_app = typer.Typer(help="Migration maintenance commands")
|
|
465
|
+
app.add_typer(migrations_app, name="migrations")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@migrations_app.command("squash")
|
|
469
|
+
def squash(
|
|
470
|
+
name: str = typer.Option(
|
|
471
|
+
"squashed", help="Name suffix for the new initial migration"
|
|
472
|
+
),
|
|
473
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
474
|
+
) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Squash the whole migration history into a single initial migration.
|
|
477
|
+
|
|
478
|
+
Replays all migration files in memory, computes the final schema and
|
|
479
|
+
replaces the history with one 0001_<name>.py in the current format.
|
|
480
|
+
Old files are deleted (version control is your backup). Manual SQL
|
|
481
|
+
(ctx.execute) is NOT carried over — affected files are listed.
|
|
482
|
+
|
|
483
|
+
On already-deployed databases run `oxyde migrate --fake` afterwards to
|
|
484
|
+
record the new initial migration without executing it.
|
|
485
|
+
"""
|
|
486
|
+
from oxyde.migrations.squash import squash_migrations
|
|
487
|
+
|
|
488
|
+
config = load_config_or_exit()
|
|
489
|
+
migrations_path = Path(config.migrations_dir)
|
|
490
|
+
files = sorted(migrations_path.glob("[0-9]*.py"))
|
|
491
|
+
|
|
492
|
+
if not files:
|
|
493
|
+
typer.secho(
|
|
494
|
+
"Nothing to squash: no migration files found.", fg=typer.colors.YELLOW
|
|
495
|
+
)
|
|
496
|
+
raise typer.Exit(0)
|
|
497
|
+
|
|
498
|
+
typer.echo(f"Will squash {len(files)} migration file(s) into one:")
|
|
499
|
+
for f in files:
|
|
500
|
+
typer.echo(f" • {f.name}")
|
|
501
|
+
typer.echo()
|
|
502
|
+
|
|
503
|
+
if not yes and not typer.confirm(
|
|
504
|
+
"Delete these files and write a new 0001 migration?"
|
|
505
|
+
):
|
|
506
|
+
raise typer.Exit(0)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
result = squash_migrations(config.migrations_dir, name=name)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
typer.secho(f"❌ Squash failed: {e}", fg=typer.colors.RED)
|
|
512
|
+
raise typer.Exit(1) from None
|
|
513
|
+
|
|
514
|
+
assert result.new_file is not None
|
|
515
|
+
typer.secho(
|
|
516
|
+
f"✅ Created {result.new_file.name} ({result.table_count} table(s)), "
|
|
517
|
+
f"deleted {len(result.deleted_files)} file(s).",
|
|
518
|
+
fg=typer.colors.GREEN,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if result.raw_sql_files:
|
|
522
|
+
typer.echo()
|
|
523
|
+
typer.secho(
|
|
524
|
+
"⚠️ These files contained manual SQL (ctx.execute) that was NOT "
|
|
525
|
+
"carried over — move it manually if still needed:",
|
|
526
|
+
fg=typer.colors.YELLOW,
|
|
527
|
+
)
|
|
528
|
+
for fname in result.raw_sql_files:
|
|
529
|
+
typer.echo(f" • {fname}")
|
|
530
|
+
|
|
531
|
+
typer.echo()
|
|
532
|
+
typer.echo("Next steps:")
|
|
533
|
+
typer.echo(" • fresh databases: oxyde migrate")
|
|
534
|
+
typer.echo(
|
|
535
|
+
" • deployed databases: oxyde migrate --fake (records 0001 without executing)"
|
|
536
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Code generation utilities for Oxyde ORM."""
|
|
2
|
+
|
|
3
|
+
from oxyde.codegen.stub_generator import (
|
|
4
|
+
generate_model_stub,
|
|
5
|
+
generate_stub_for_file,
|
|
6
|
+
generate_stubs_for_models,
|
|
7
|
+
write_stubs,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"generate_model_stub",
|
|
12
|
+
"generate_stub_for_file",
|
|
13
|
+
"generate_stubs_for_models",
|
|
14
|
+
"write_stubs",
|
|
15
|
+
]
|