Found while merging #3953 (datetime canonical storage) against the deferred-DDL work from #3917. Filed rather than bundled, because fixing it means changing an exported type and the CLI renderer that #3917 just landed.
The gap
previewDeferredSchemaWork() builds the plan from PendingSchemaWork, whose kind is a closed union:
exportinterfacePendingSchemaWork{table: string;kind: 'create_table'|'add_columns';columns: string[];}#3953 adds two convergence steps that run inside initObjects alongside the create/alter path:
backfillCanonicalDatetimes — a row-rewriting UPDATE per SQLite Field.datetime column.migrateMysqlDatetimeColumns — ALTER TABLE … MODIFY … DATETIME(3) per legacy MySQL TIMESTAMP column.
Both correctly respect the deferral (they sit after the if (this.deferredDdl) continue gate, and flushDeferredSchemaDdl re-enters initObjects with it disarmed), so plan performs no work and apply performs all of it — the behaviour is right.
What is wrong is the reporting: neither step can be expressed in PendingSchemaWork, so os migrate plan shows nothing for them. An operator sees a plan listing, say, two added columns, confirms it, and apply additionally rewrites every row of a datetime column or rebuilds a column on a large table. That is exactly the kind of surprise #3917 exists to remove — the plan promises to show what apply will do.
Why it matters more than it looks
A DATETIME(3)ALTER … MODIFY on MySQL is a table rebuild: it takes a metadata lock and can run for a long time on a large table. Being told about it beforehand is the difference between a planned maintenance window and an unexplained stall.
The SQLite backfill is cheaper (one scan, and zero writes once converged) but still worth naming, if only so the operator can correlate the one-time boot cost.
Suggested direction
Widen the union and give each new kind the detail an operator needs to judge cost:
kind: 'create_table'|'add_columns'|'normalize_datetime_storage'|'widen_datetime_columns'
columns already carries the per-column list for both, so the shape may not need more than the new labels plus renderer cases. Worth checking whether the plan should also estimate row counts for the rewriting steps — that is the number that decides whether to run it now or in a window.
Anything that adds work to initObjects' physical path from here on has the same obligation, so it may be worth a short note next to PendingSchemaWork saying so.
Found while merging #3953 (datetime canonical storage) against the deferred-DDL work from #3917. Filed rather than bundled, because fixing it means changing an exported type and the CLI renderer that #3917 just landed.
The gap
previewDeferredSchemaWork()builds the plan fromPendingSchemaWork, whosekindis a closed union:#3953 adds two convergence steps that run inside
initObjectsalongside the create/alter path:backfillCanonicalDatetimes— a row-rewritingUPDATEper SQLiteField.datetimecolumn.migrateMysqlDatetimeColumns—ALTER TABLE … MODIFY … DATETIME(3)per legacy MySQLTIMESTAMPcolumn.Both correctly respect the deferral (they sit after the
if (this.deferredDdl) continuegate, andflushDeferredSchemaDdlre-entersinitObjectswith it disarmed), soplanperforms no work andapplyperforms all of it — the behaviour is right.What is wrong is the reporting: neither step can be expressed in
PendingSchemaWork, soos migrate planshows nothing for them. An operator sees a plan listing, say, two added columns, confirms it, andapplyadditionally rewrites every row of a datetime column or rebuilds a column on a large table. That is exactly the kind of surprise #3917 exists to remove — the plan promises to show what apply will do.Why it matters more than it looks
A
DATETIME(3)ALTER … MODIFYon MySQL is a table rebuild: it takes a metadata lock and can run for a long time on a large table. Being told about it beforehand is the difference between a planned maintenance window and an unexplained stall.The SQLite backfill is cheaper (one scan, and zero writes once converged) but still worth naming, if only so the operator can correlate the one-time boot cost.
Suggested direction
Widen the union and give each new kind the detail an operator needs to judge cost:
columnsalready carries the per-column list for both, so the shape may not need more than the new labels plus renderer cases. Worth checking whether the plan should also estimate row counts for the rewriting steps — that is the number that decides whether to run it now or in a window.Anything that adds work to
initObjects' physical path from here on has the same obligation, so it may be worth a short note next toPendingSchemaWorksaying so.