Repository navigation
Conversation
An app whose models use a SQLAlchemy DeclarativeBase, the style the docs recommend, had to call rx.ModelRegistry.register(Base). Without it the alembic target metadata held none of its tables, so `reflex db makemigrations` wrote a revision dropping every one of them and `reflex db migrate` deleted their data, while `alembic check` and the startup hint pointed at makemigrations. ModelRegistry.get_models now adds every module-level DeclarativeBase subclass with its own metadata until a base other than SQLModel's is registered, so registering bases still restricts migrations to them, e.g. for an app with a second database. A base is detected only while its module holds it under its name, so one replaced by a reload of its module is not. The walk runs when the metadata is built, which is cached as before. The env.py template also warns when an autogenerated revision drops tables, naming them, so that the remaining ways to drop a table by accident (a base that is not detected, such as a legacy declarative_base(), or a base left unregistered beside a registered one) are visible before `reflex db migrate`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Say that detection applies only until a base is registered, that it covers module-level bases the app or its pages import, a library's included, and not one created inside a function, and update the key-takeaways bullet that still required registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
[High impact] The PR appears safe to merge. SummaryThe PR refuses unsafe column additions when a dataclass
Reviews (5) · Last reviewed commit: "Look up the added column by key and its ..." · Reviewed by Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 14 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
|
Generated by Claude Code |
Merging this PR will degrade performance by 5.05%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
|
CodSpeed is red here ( Generated by Claude Code |
3faf080 to
342353a
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
…e the base is made A factory-made base bound under its own class name is detected; only a base its module holds under a different name, or one made with declarative_base(), is not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
342353a to
fcfb9ff
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
`mapped_column(default_factory=...)` on a MappedAsDataclass model is the dataclass's default alone: the Column has no default, so the env.py add-column hook gave the revision no server_default and adding the column to a table with rows failed with "NOT NULL constraint failed" (on sqlite batch mode leaving an `_alembic_tmp_*` table behind). The hook now looks the column up in the mapped dataclasses and, for a NOT NULL column whose only default is a default_factory, raises a CommandError naming the table and column before any revision is written (and in `alembic check`). Column defaults, literal or callable, still become the server default as before; a nullable column needs none. Fixes FINDING-012 of the 0.11.0a1 pre-release testing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
`Table.c` is keyed by `Column.key`, so a column with an explicit key other than its name was missed and its default_factory not refused. The property is now found with `mapper.get_property_by_column` and the dataclass field with `__dataclass_fields__`, instead of scanning `mapper.columns` and `dataclasses.fields`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
fcfb9ff to
3e9f0a8
Compare
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
All Submissions:
Type of change
Changes To Core Features:
Fixes FINDING-012 from the reflex 0.11.0a1 pre-release testing (artifacts on branch
claude/vibrant-pasteur-3921g3,prerelease-testing/runs/2026-11-11-v0.11.0a1/).Defect
Take a non-empty table and add
score: Mapped[int] = mapped_column(default_factory=lambda: 7)on aMappedAsDataclassmodel. Autogenerate wroteadd_column(... nullable=False)with noserver_default, soreflex db migratefailed withNOT NULL constraint failed. When the revision rebuilt the table in sqlite batch mode, alembic also left an_alembic_tmp_*table behind that broke the next migrate.The cause is the env.py template's add-column hook, which only reads
Column.default. A dataclassdefault_factoryis stored on the dataclass field only, soColumn.defaultisNone.Fix
The hook now handles a NOT NULL added column that has no
server_defaultand noColumn.defaultlike this:It looks the column up in the mapped dataclasses of the registered or detected model bases. It uses
table.c[op.column.key](keyed byColumn.key, which can differ from the name), thenmapper.get_property_by_column(column).key, then__dataclass_fields__. A mapper that doesn't map the column, as with a single-table-inheritance parent, is skipped.If the column's attribute has a
default_factory, the hook raises aCommandErrorrather than guess a value:I put the refusal at autogenerate time because it fails before a broken revision is written, so no
_alembic_tmp_*leftover can come from this path. It also fires inalembic check, so the startup schema check reports it.Docs:
docs/database/tables.mdgets a sentence aboutdefault_factoryand migrations.Known behaviour
This was confirmed by the release owner.
Column.defaultis still evaluated once, not refused. Examples are sqlmodelField(default_factory=...)andmapped_column(insert_default=callable). The value is taken when the revision is generated and becomes the server default for existing rows, as in 0.10.1.test_callable_default_is_evaluated_for_existing_rowscovers it. Only a dataclassdefault_factorythat leavesColumn.defaultempty is refused.Column.defaultstill becomes the server default, and a nullable column is never refused.Test plan
New test
test_default_factory_column_is_refusedintests/units/utils/test_migrations.py(real alembic and sqlite, MappedAsDataclass app). It failed before the fix:makemigrationsexited 0 and wrote..._factory.py. After the fix it checks:makemigrationsexits 1 with the message naming'thing.score', and no revision is written.alembic checkfails with the same message.mapped_column('points', key='points_key', default_factory=...), is refused too, naming'thing.points'. This case was added for the review thread and failed before the key fix.default=7getsserver_default=sa.text('7')and migrates the existing row.default_factorycolumn is accepted.uv run pytest tests/units --cov(withouttests/units/reflex_bench/drivers/test_browser.py, since there is no Chromium in this sandbox): 15645 passed, 66 skipped, coverage 81.44%. After the rebase onto 93beac9, the migration and model tests pass again: 43/43 locally, and test_migrations 30/30 on SQLite 3.45.uv run ruff check .anduv run ruff format --check .are clean.uv run pyright reflex testsgives 0 errors.End to end with the verifier's
verify_dbm/v3_default_factory.sh, run on an editable install:dfmakemigrationsrc 1 with the message, no revision;migraterc 0 and its schema check logs the refusal; row untouched; no_alembic_tmp_*tabledfbatchdfdfltserver_default=sa.text('7'), rows[(1, 'a', 7)]insserver_default=sa.text('7'), rows[(1, 'a', 7)]rxdfserver_default=sa.text('7'), rows[(1, 'a', 7)]Notes
reflex db initwrote keeps the old hook until it is moved aside and re-initialized._alembic_tmp_*table left by an already-written broken revision.🤖 Generated with Claude Code
https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi