Skip to content

Fix SQLite ->change(): restore attribute preservation without the crash-prone rebuild - #239

Merged
LukeTowers merged 1 commit into
wip/1.3from
fix/sqlite-change-column-rebuild
Aug 11, 2026
Merged

Fix SQLite ->change(): restore attribute preservation without the crash-prone rebuild#239
LukeTowers merged 1 commit into
wip/1.3from
fix/sqlite-change-column-rebuild

Conversation

@LukeTowers

@LukeTowers LukeTowers commented Aug 11, 2026

Copy link
Copy Markdown
Member

Background — why the override exists

Laravel 11 rewrote the schema builder to drop its doctrine/dbal dependency and introspect schemas natively. A deliberate side effect (Laravel 11 upgrade guide — Modifying Columns): ->change() no longer preserves a column's existing attributes — you must now re-specify every modifier on a change or it is dropped. Laravel's recommended mitigations are to squash migrations into a current-schema snapshot and to always fully redefine changed columns.

Neither mitigation is viable for Winter. It's plugin-based, so any given install is an unbounded combination of plugins and migration states — there is no single migration history to squash, and plugin authors can't be expected to fully redefine columns they didn't author. To restore the pre-11 "keep unspecified attributes on change" behaviour, Storm added a compileChange() grammar override in #207 (Support Laravel 12; commit 8dcf119b, "restore migration behavior").

SQLite can't ALTER COLUMN — it has to rebuild the table — so on SQLite that override hand-rolled its own rebuild, re-emitting every column (including unchanged ones) through the grammar's getType() using SQLite's introspected type_name.

The problem

Hand-rolling a parallel rebuild fought Laravel 11's design and broke in three ways:

  1. Crash. getType() dispatches on the introspected type name. SQLite stores a decimal column's declared type as numeric and a binary column's as blob, so the rebuild called typeNumeric() / typeBlob() — which don't exist → BadMethodCallException: Method Winter\Storm\Database\Schema\Grammars\SQLiteGrammar::typeNumeric does not exist. This is the same failure class Implement typeTinyint for SQLite grammar #226 patched for booleans (tinyint → the missing typeTinyint) — a symptom, not the cause. Any table containing a decimal/binary column made ->change() unusable on SQLite.
  2. Double rebuild. Because SQLite's getAlterCommands() marks change as an alter command, base Blueprint::addAlterCommands() also emitted the implied alter command, so Laravel's own compileAlter() rebuilt the table a second time — two full create-temp → copy → drop → rename cycles per change.
  3. The feature didn't even work. Laravel's compileAlter() runs after the override and overwrote its result with L11 "drop unspecified attributes" semantics — so on SQLite the attribute preservation the override existed to provide was silently discarded. (It works on MySQL/PostgreSQL/SQL Server, which alter in place and never hit the second rebuild.)

Why this approach fixes it properly

Laravel 11 funnels all SQLite alterations through a single rebuild — BlueprintState + SQLiteGrammar::compileAlter() — which reproduces unchanged columns verbatim via $column->full_type_definition ?? $this->getType($column) and only routes changed/added columns through getType(). That's precisely what makes it faithful (no numeric/blob/tinyint round-trip) and single-pass. The old override duplicated and fought that machinery; the fix is to stop fighting it and re-add only the piece Winter needs, at the seam Laravel already uses:

  • Delete the SQLite compileChange() override and the now-dead typeTinyint/typeVarChar aliases (only reachable via the deleted round-trip). Faithful type reproduction comes for free from full_type_definition.
  • Add Winter\Storm\Database\Schema\BlueprintState (extends the base). Its update() merges the existing column's unspecified modifiers onto a change command before delegating to parent::update() — restoring the pre-11 behaviour and, unlike the old override, actually taking effect because it feeds Laravel's single rebuild.
  • Wire it in via Blueprint::addImpliedCommands(). Since Blueprint::addAlterCommands() only builds state if ($this->grammar instanceof SQLiteGrammar), this is SQLite-only — MySQL/PostgreSQL/SQL Server are untouched (their compileChange overrides remain the sole handler and continue to work).

Net effect: the crash, the double rebuild, and the silent tinyint(1)integer type drift all disappear; the #207 intent (preserve attributes across a change) finally works on SQLite; and #226's typeTinyint band-aid is superseded. −243/+199 across five files.

Changes

  • src/Database/Schema/BlueprintState.phpnew; merges unspecified column modifiers on change, then delegates to the base.
  • src/Database/Schema/Blueprint.phpaddImpliedCommands() override swaps in Winter's BlueprintState (SQLite only).
  • src/Database/Schema/Grammars/SQLiteGrammar.php — remove compileChange, typeTinyint, typeVarChar (leaves only getDefaultValue).
  • tests/Database/Schema/Grammars/SQLiteSchemaGrammarTest.php — mock tests → real in-memory-SQLite integration tests + crash/faithfulness/single-rebuild coverage.
  • phpstan-baseline.neon — drop the 6 entries pinned to the removed grammar code.

Verification

  • Full suite: 846 pass, no regressions (rebased on wip/1.3 after Fix CI: regenerate stale PHPStan baseline and code-quality drift #240).
  • 7 new SQLite integration tests: make-nullable; add-default-preserves-nullable; add-default+drop-nullable; preserves-unspecified-attributes; preserves-tinyint-type-of-other-columns (was silently rewritten to integer); decimal+binary faithful & no-crash; rebuilds-exactly-once.
  • Driver isolation: state is built for SQLite only; MySQL/PG/SqlServer grammar tests pass unchanged.
  • php -l / phpcs clean; PHPStan [OK] No errors against the regenerated (Fix CI: regenerate stale PHPStan baseline and code-quality drift #240) baseline.
  • Laravel 13 checked: every relied-upon Blueprint / BlueprintState / SQLiteGrammar seam is byte-identical on 13.x — no change required.

References

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9b0723c-d487-42a0-8d24-9ab09181e037

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…utes

On SQLite, changing a column routed through Storm's own compileChange()
override, which hand-rolled a full table rebuild in parallel with
Laravel's. This caused three defects:

- Tables containing decimal/binary columns threw "Method SQLiteGrammar::
  typeNumeric/typeBlob does not exist", because every column was
  re-emitted through getType() using the introspected type name instead
  of Laravel's full_type_definition.
- Every ->change() rebuilt the table twice (Storm's rebuild plus the base
  compileAlter rebuild triggered by the implied alter command).
- The override's attribute preservation was silently discarded: the base
  compileAlter ran last and applied Laravel 11's "drop unspecified
  attributes" semantics.

Instead of maintaining a parallel rebuild, defer to Laravel's single
compileAlter/BlueprintState rebuild - which reproduces unchanged columns
faithfully via full_type_definition - and re-add only attribute
preservation via a Winter BlueprintState whose update() merges the
existing column's unspecified modifiers onto a change command. It is
wired in through Blueprint::addImpliedCommands(); because
Blueprint::addAlterCommands() only builds state for SQLite, this affects
SQLite exclusively (verified against Laravel 12.x and 13.x).

Removes the redundant compileChange() override and its typeTinyint/
typeVarChar aliases (only reachable via the deleted round-trip), and
rewrites the SQLite grammar tests as real integration tests covering
attribute preservation, faithful type reproduction, the decimal/binary
crash, and single-rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LukeTowers
LukeTowers force-pushed the fix/sqlite-change-column-rebuild branch from cf48eed to 57d41e8 Compare August 11, 2026 16:21
@LukeTowers LukeTowers changed the title Fix SQLite ->change() rebuild: crash, double-rebuild, and lost column attributes Fix SQLite ->change(): restore attribute preservation without the crash-prone rebuild Aug 11, 2026
@LukeTowers
LukeTowers merged commit e842470 into wip/1.3 Aug 11, 2026
18 checks passed
@LukeTowers
LukeTowers deleted the fix/sqlite-change-column-rebuild branch August 11, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant