Skip to content

Commit e23b939

Browse files
authored
Merge pull request #602 from lbedner/v0.6.8-rc5
v0.6.8-rc5
2 parents b18b1cd + e9c7ffc commit e23b939

9 files changed

Lines changed: 122 additions & 47 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Each generated project includes:
3232

3333
## Installation
3434

35-
**Current Version**: 0.6.8rc4
35+
**Current Version**: 0.6.8rc5
3636

3737
```bash
3838
pip install aegis-stack

aegis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Aegis Stack CLI - Component generation and project management tools.
33
"""
44

5-
__version__ = "0.6.8rc4"
5+
__version__ = "0.6.8rc5"

aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/migrate_fix.py.jinja

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ def _generate_fix_migration(diffs: list[tuple]) -> Path | None:
121121

122122
if op_type == "add_table":
123123
table = diff[1]
124-
# Build CREATE TABLE with columns
125-
cols = []
124+
# Build CREATE TABLE with columns + inline FK constraints
125+
items = []
126126
for col in table.columns:
127127
col_str = f"sa.Column('{col.name}', {_sa_type_str(col.type)}"
128128
col_str += f", nullable={col.nullable}"
@@ -131,23 +131,27 @@ def _generate_fix_migration(diffs: list[tuple]) -> Path | None:
131131
if col.server_default is not None:
132132
col_str += f", server_default=sa.text('{col.server_default.arg}')"
133133
col_str += ")"
134-
cols.append(f" {col_str},")
135-
cols_block = "\n".join(cols)
136-
upgrade_lines.append(
137-
f" op.create_table(\n '{table.name}',\n{cols_block}\n )"
138-
)
139-
# Add foreign keys
134+
items.append(f" {col_str},")
135+
# Inline FK constraints (works with SQLite — created with table)
140136
for fk in table.foreign_key_constraints:
141137
local_cols = [c.name for c in fk.columns]
142138
ref_table = fk.referred_table.name
143-
ref_cols = [c.name for c in fk.referred_table.primary_key.columns]
144-
fk_name = fk.name or f"fk_{table.name}_{'_'.join(local_cols)}_{ref_table}"
145-
upgrade_lines.append(
146-
f" op.create_foreign_key('{fk_name}', '{table.name}', '{ref_table}', {local_cols}, {ref_cols})"
147-
)
148-
downgrade_lines.append(
149-
f" op.drop_constraint('{fk_name}', '{table.name}', type_='foreignkey')"
139+
ref_cols = [
140+
el.column.name
141+
for el in fk.elements
142+
if hasattr(el, "column") and el.column is not None
143+
]
144+
if not ref_cols:
145+
ref_cols = [c.name for c in fk.referred_table.primary_key.columns]
146+
ref_col_strs = [f"'{ref_table}.{rc}'" for rc in ref_cols]
147+
local_col_strs = [f"'{lc}'" for lc in local_cols]
148+
items.append(
149+
f" sa.ForeignKeyConstraint([{', '.join(local_col_strs)}], [{', '.join(ref_col_strs)}]),"
150150
)
151+
items_block = "\n".join(items)
152+
upgrade_lines.append(
153+
f" op.create_table(\n '{table.name}',\n{items_block}\n )"
154+
)
151155
# Add indexes
152156
for idx in table.indexes:
153157
idx_cols = [c.name for c in idx.columns]
@@ -237,6 +241,14 @@ def main() -> None:
237241
"""Check for schema mismatches and generate fix migration."""
238242
print(t("migrate.checking_schema"))
239243

244+
# First, apply any pending migrations (e.g., from an aegis upgrade)
245+
from alembic import command as alembic_command
246+
247+
alembic_cfg = Config("alembic/alembic.ini")
248+
print(t("migrate.applying_pending"))
249+
alembic_command.upgrade(alembic_cfg, "head")
250+
251+
# Now diff against the updated DB — only truly missing items remain
240252
diffs = _get_additive_diffs()
241253

242254
if not diffs:

aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/en.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@
470470
),
471471
"migrate.no_existing_migrations": "No existing migrations found. Run 'make migrate' first.",
472472
"migrate.checking_schema": "Checking schema against models...",
473+
"migrate.applying_pending": "Applying pending migrations first...",
473474
"migrate.schema_up_to_date": "Schema is up to date. No fix needed.",
474475
"migrate.found_differences": "Found {count} schema differences:",
475476
"migrate.add_column": "ADD COLUMN",

aegis/templates/copier-aegis-project/{{ project_slug }}/app/i18n/locales/zh.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@
469469
),
470470
"migrate.no_existing_migrations": "未找到现有迁移记录,请先运行 'make migrate'。",
471471
"migrate.checking_schema": "正在检查数据库结构与模型的一致性...",
472+
"migrate.applying_pending": "正在先执行待处理的迁移...",
472473
"migrate.schema_up_to_date": "数据库结构已是最新,无需修复。",
473474
"migrate.found_differences": "发现 {count} 处结构差异:",
474475
"migrate.add_column": "新增列",

copier.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# - Update support
77

88
_min_copier_version: "9.0.0"
9-
_version: "0.6.8rc4"
9+
_version: "0.6.8rc5"
1010

1111
# IMPORTANT: Template content is in subdirectory
1212
# This allows the template to be recognized as git-tracked (aegis-stack repo root has .git)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "aegis-stack"
3-
version = "0.6.8rc4"
3+
version = "0.6.8rc5"
44
description = "A production-ready FastAPI platform with modular components and a built-in control plane. Try: uvx aegis-stack init my-project"
55
readme = "README.md"
66
requires-python = ">=3.11,<3.15"

tests/core/test_migrate_fix.py

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,65 @@ def test_upgrade_scenario_basic_to_current(self) -> None:
196196
assert "password_reset_token" in added_table_names
197197
assert "email_verification_token" in added_table_names
198198

199+
def test_fk_ref_columns_resolved_in_diff_context(self) -> None:
200+
"""FK referenced columns are accessible from add_table diffs."""
201+
engine = create_engine("sqlite:///:memory:")
202+
203+
# Create only the user table (referenced by FK)
204+
old_metadata = sa.MetaData()
205+
sa.Table(
206+
"user",
207+
old_metadata,
208+
sa.Column("id", sa.Integer(), primary_key=True),
209+
sa.Column("email", sa.String(), nullable=False),
210+
)
211+
old_metadata.create_all(engine)
212+
213+
# New metadata adds a table with FK to user
214+
new_metadata = sa.MetaData()
215+
sa.Table(
216+
"user",
217+
new_metadata,
218+
sa.Column("id", sa.Integer(), primary_key=True),
219+
sa.Column("email", sa.String(), nullable=False),
220+
)
221+
sa.Table(
222+
"password_reset_token",
223+
new_metadata,
224+
sa.Column("id", sa.Integer(), primary_key=True),
225+
sa.Column(
226+
"user_id",
227+
sa.Integer(),
228+
sa.ForeignKey("user.id"),
229+
nullable=False,
230+
),
231+
sa.Column("token", sa.String(), nullable=False),
232+
)
233+
234+
with engine.connect() as conn:
235+
migration_ctx = MigrationContext.configure(conn)
236+
diffs = compare_metadata(migration_ctx, new_metadata)
237+
238+
# Get the add_table diff
239+
add_table_diffs = [d for d in diffs if d[0] == "add_table"]
240+
assert len(add_table_diffs) == 1
241+
242+
table = add_table_diffs[0][1]
243+
assert table.name == "password_reset_token"
244+
245+
# Verify FK ref columns are resolvable (the bug was empty ref_cols)
246+
fks = list(table.foreign_key_constraints)
247+
assert len(fks) == 1
248+
fk = fks[0]
249+
250+
# Try element-based extraction (our fix)
251+
ref_cols = [
252+
el.column.name
253+
for el in fk.elements
254+
if hasattr(el, "column") and el.column is not None
255+
]
256+
assert ref_cols == ["id"], f"FK ref_cols should be ['id'], got {ref_cols}"
257+
199258

200259
class TestMigrateFixMigrationRendering:
201260
"""Test that add_table diffs render correct migration content."""
@@ -210,7 +269,7 @@ def _sa_type_str(col_type: object) -> str:
210269
upgrade_lines: list[str] = []
211270
downgrade_lines: list[str] = []
212271

213-
cols = []
272+
items = []
214273
for col in table.columns:
215274
col_str = f"sa.Column('{col.name}', {_sa_type_str(col.type)}"
216275
col_str += f", nullable={col.nullable}"
@@ -219,23 +278,28 @@ def _sa_type_str(col_type: object) -> str:
219278
if col.server_default is not None:
220279
col_str += f", server_default=sa.text('{col.server_default.arg}')" # type: ignore[union-attr]
221280
col_str += ")"
222-
cols.append(f" {col_str},")
223-
cols_block = "\n".join(cols)
224-
upgrade_lines.append(
225-
f" op.create_table(\n '{table.name}',\n{cols_block}\n )"
226-
)
227-
281+
items.append(f" {col_str},")
282+
# Inline FK constraints
228283
for fk in table.foreign_key_constraints:
229284
local_cols = [c.name for c in fk.columns]
230285
ref_table = fk.referred_table.name
231-
ref_cols = [c.name for c in fk.referred_table.primary_key.columns]
232-
fk_name = fk.name or f"fk_{table.name}_{'_'.join(local_cols)}_{ref_table}"
233-
upgrade_lines.append(
234-
f" op.create_foreign_key('{fk_name}', '{table.name}', '{ref_table}', {local_cols}, {ref_cols})"
235-
)
236-
downgrade_lines.append(
237-
f" op.drop_constraint('{fk_name}', '{table.name}', type_='foreignkey')"
286+
# Match production: prefer fk.elements, fall back to PK
287+
ref_cols = [
288+
el.column.name
289+
for el in fk.elements
290+
if hasattr(el, "column") and el.column is not None
291+
]
292+
if not ref_cols:
293+
ref_cols = [c.name for c in fk.referred_table.primary_key.columns]
294+
ref_col_strs = [f"'{ref_table}.{rc}'" for rc in ref_cols]
295+
local_col_strs = [f"'{lc}'" for lc in local_cols]
296+
items.append(
297+
f" sa.ForeignKeyConstraint([{', '.join(local_col_strs)}], [{', '.join(ref_col_strs)}]),"
238298
)
299+
items_block = "\n".join(items)
300+
upgrade_lines.append(
301+
f" op.create_table(\n '{table.name}',\n{items_block}\n )"
302+
)
239303

240304
for idx in table.indexes:
241305
idx_cols = [c.name for c in idx.columns]
@@ -278,8 +342,8 @@ def test_renders_create_table(self) -> None:
278342
# Downgrade drops the table
279343
assert "op.drop_table('password_reset_token')" in downgrade[-1]
280344

281-
def test_renders_foreign_keys(self) -> None:
282-
"""add_table diff with FKs renders op.create_foreign_key."""
345+
def test_renders_foreign_keys_inline(self) -> None:
346+
"""add_table diff with FKs renders inline sa.ForeignKeyConstraint."""
283347
metadata = sa.MetaData()
284348
sa.Table(
285349
"user",
@@ -302,18 +366,15 @@ def test_renders_foreign_keys(self) -> None:
302366
token_table = metadata.tables["password_reset_token"]
303367
upgrade, downgrade = self._render_add_table_upgrade(token_table)
304368

305-
# Should have create_foreign_key
369+
# FK should be inline in create_table, not separate
370+
create_line = upgrade[0]
371+
assert "ForeignKeyConstraint" in create_line
372+
assert "'user_id'" in create_line
373+
assert "'user.id'" in create_line
374+
375+
# No separate create_foreign_key calls
306376
fk_lines = [line for line in upgrade if "create_foreign_key" in line]
307-
assert len(fk_lines) == 1
308-
assert "'password_reset_token'" in fk_lines[0]
309-
assert "'user'" in fk_lines[0]
310-
assert "['user_id']" in fk_lines[0]
311-
assert "['id']" in fk_lines[0]
312-
313-
# Downgrade should drop the constraint
314-
drop_fk_lines = [line for line in downgrade if "drop_constraint" in line]
315-
assert len(drop_fk_lines) == 1
316-
assert "type_='foreignkey'" in drop_fk_lines[0]
377+
assert len(fk_lines) == 0
317378

318379
def test_renders_indexes(self) -> None:
319380
"""add_table diff with indexes renders op.create_index."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)