fix(model): only carry scalar column defaults as server_default in migrations#6770
fix(model): only carry scalar column defaults as server_default in migrations#6770anxkhn wants to merge 1 commit into
Conversation
…grations The AddColumnOp rewriter used during autogeneration copied every column default into a server_default by wrapping op.column.default.arg in sqlalchemy.literal(). For a callable default (e.g. Field(default_factory=datetime.now) or Field(default=uuid.uuid4)), op.column.default.arg is the Python function object rather than a value, so rendering the migration script raised sqlalchemy.exc.CompileError: "No literal value renderer is available for literal value <function ...>". This broke autogenerated migrations for the common case of adding a timestamp or UUID column to an existing table. Gate the server_default rewrite on op.column.default.is_scalar so only scalar defaults are carried as a SQL literal; callable defaults keep alembic's default behavior, which is correct since a per-row Python callable cannot be a single SQL literal. Add a regression test that adds callable-default columns to an existing table and fails with CompileError before the fix. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Greptile SummaryThis PR fixes a
Confidence Score: 5/5Safe to merge — the change is a minimal, targeted guard on an existing code path with no behavioral change for scalar defaults and correct handling of callable defaults. The one-line guard adds No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "fix(model): only carry scalar column def..." | Re-trigger Greptile |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c68b001a8c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| # Python function itself, so leave server_default unset. | ||
| if ( | ||
| op.column.default is not None | ||
| and op.column.default.is_scalar |
There was a problem hiding this comment.
Handle existing rows for non-null callable defaults
When a callable-default column is non-nullable and the table already contains rows, this branch now leaves server_default unset, so Alembic emits an add_column with nullable=False and no default; migrate(autogenerate=True) then applies that migration immediately and databases such as SQLite/Postgres reject it because existing rows would receive NULL. The new regression test avoids this by adding created before inserting any rows, so adding a typical created: datetime = Field(default_factory=datetime.now) column to a populated table remains broken.
Useful? React with 👍 / 👎.
Type of change
What / why
reflex db makemigrationsandreflex db migrate --autogeneratecrash with asqlalchemy.exc.CompileErrorwhen the autogenerated migration adds a column thathas a callable default to an existing table. This is the single most common
real-world schema change: adding a
createdtimestamp or a UUID column, e.g.Root cause is in the
AddColumnOprewriter registered during autogeneration inreflex/model.py. To carry a sqlmodel default onto existing rows, it copies thecolumn default into a
server_defaultby wrappingop.column.default.arginsqlalchemy.literal(...):For a scalar default (
Field(default=7))op.column.default.argis the value(
7), which renders fine. For a callable default, SQLAlchemy stores aCallableColumnDefaultwhose.argis the Python function object itself, not avalue.
literal(<function>)becomes a bind parameter withNullType, and whenthe migration script is rendered with
literal_binds=TrueSQLAlchemy raises:so migration-script generation aborts.
Fix
Gate the
server_defaultrewrite onop.column.default.is_scalar, so only scalardefaults are carried as a SQL literal:
Callable defaults then keep alembic's default behavior (no
server_default), whichis correct: a per-row Python callable cannot be represented as a single SQL literal.
Scalar
server_defaultbehavior is unchanged.is_scalaris defined on the baseDefaultGenerator(class-levelFalse), so this is safe for every default typealembic can attach.
Tests
Added
tests/units/test_model.py::test_automigration_add_column_with_callable_default,which creates a table, migrates to head, then adds callable-default columns
(
default_factory=datetime.now, a nullabledefault_factory) and runs theautogenerate/migrate path. It fails with the
CompileErrorabove onmainandpasses with the fix. It also asserts that a scalar default alongside the callable
one still works and that rows read back correctly.
Local checks (all green):