ADR 0009: Schema Migration Story¶
Status¶
Accepted
Context¶
ADR-0005 (docs/adr/0005-schema-php-single-source.md) made class/xion/SchemaDefinition.php the canonical schema source and SchemaCompiler the MySQL / SQLite DDL emitter. The ADR explicitly carved out migrations (ALTER TABLE) as out of scope, with the trigger condition: "future trial that drifts on this surface escalates the concern."
The trigger condition is now satisfied by an external evaluation surface — REPORT_commercial_feasibility.md (PR #401) — which named schema migrations as the single largest practical concern for commercial NeNe deployments:
マイグレーション機構がない(最大の実務懸念)— スキーマ変更は
ALTER TABLEを手動実行。ADR-0005 でも「マイグレーションは対象外」と明記されています。本番運用中にカラム追加・変更があるたびに、オペレータが手で SQL を叩く必要があります。
A small commercial deploy with quarterly schema changes hand-rolls the ALTER TABLE against production. That works once but does not scale to "many deploys" or "long-lived multi-version pipelines" — the operator-side cognitive load and rollback risk grow per migration.
The trial-friction surface is in flight too — FT11 found that the three-file schema duplication friction (DataMapperBase + docker/mysql/init + SQLite initializer) was real enough to merit ADR-0005. Migrations are the natural next surface to ADR.
Constraints¶
- NeNe philosophy: small framework, env-driven config, no large new abstractions.
- Backward compatibility: existing operators who run
cli/setupDatabase.php(MySQL) orcli/initSQLite.php(SQLite) must not regress. Bundled001_schema.sqlfor the Docker entrypoint must continue to work. - No graceful rollback fantasy: even Phinx / Liquibase ship with caveats around rollback; the framework should not pretend it can
ALTER TABLE DROP COLUMNlosslessly. - Single source preserved: ADR-0005's
SchemaDefinitionstays the source of truth. Whatever migration mechanism this ADR adopts must read fromSchemaDefinitionor extend it; it cannot grow a parallel schema source.
Options considered¶
| Option | What it is | Trade-off |
|---|---|---|
| A — External Phinx / Liquibase | Operator installs a separate tool, NeNe is unaware. | Smallest framework change, but the integrator must learn Phinx semantics, set up its config, and bridge its idea of "current schema" with NeNe's SchemaDefinition. Two sources of truth in practice. |
B — Versioned SchemaDefinition + auto-generated ALTER TABLE |
Add a version field to each table in SchemaDefinition; SchemaCompiler diffs the running DB introspection against the definition and emits ALTER TABLE for new columns. |
Real implementation work. dialect-specific edge cases (column rename, type change, NOT NULL with default backfill) need careful handling. |
C — cli/schemaDiff.php + operator-applied SQL |
A new CLI compares the live DB schema with SchemaDefinition::buildTables() and emits the ALTER TABLE statements needed to converge. The operator runs them by hand (or pipes to mysql / sqlite3). |
Smallest viable. NeNe stays out of "apply" so destructive operations remain the operator's call. The integrator gets a single command that says exactly what needs to change. |
Option C wins for FT15-era NeNe because:
- Aligns with NeNe's "operator-side concerns stay operator-side" stance (production-deployment env matrix philosophy, ADR-0004 redirect hook, ADR-0008 Bearer token).
- Single source preserved:
cli/schemaDiff.phpreadsSchemaDefinition::buildTables()(the same sourceSchemaCompileralready uses). No new schema-source class is introduced. - No false promise of rollback: the operator sees the diff, runs it, owns the consequence. NeNe never claims it can revert.
- Phinx isn't ruled out: an operator who wants Phinx-style ergonomics can still install Phinx and use it side-by-side. C does not preclude A.
Option B is the right shape for a future NeNe + ADR — but it requires diff resolution heuristics (rename detection, type-change semantics, default-value backfill) that grow the framework's surface significantly. Premature for the current scale of NeNe deployments.
Decision¶
- Introduce
cli/schemaDiff.php: a CLI that introspects the live database (MySQL viaINFORMATION_SCHEMA, SQLite viaPRAGMA table_info), compares againstNene\Xion\SchemaDefinition::buildTables(), and emits theALTER TABLEstatements that would converge the live DB to the definition. - Output is idempotent when the schemas match (no
ALTERemitted; exit code 0 with a "schema is in sync" notice). - The CLI does not apply statements automatically. Operators redirect the output to a file, review, and
mysql < schemaDiff.sql(or equivalent for SQLite). This keeps destructive operations explicit. - Diff coverage in the initial implementation:
- Add column —
ALTER TABLE ... ADD COLUMN ...with default value from the definition. - Add table —
CREATE TABLEfromSchemaCompiler::mysqlStatements()/sqliteStatements(). - Add index —
CREATE INDEX. - Diff coverage explicitly out of scope initially (CLI prints a warning when these are detected):
- Drop column / Drop table / Drop index (destructive — operator must hand-write).
- Column type change (semantics depend on data; operator must hand-write with backfill).
- Column rename (cannot disambiguate from "drop old + add new" without metadata).
- Constraint changes (UNIQUE / FOREIGN KEY / CHECK additions).
- Default-value-only changes.
- The Docker MySQL entrypoint
docker/mysql/init/001_schema.sqlcontinues to be regenerated bycomposer schema:generate(ADR-0005). For ongoing production migrations, operators runcli/schemaDiff.phpafter every release that touchesSchemaDefinition. - A short doc,
docs/development/schema-migrations.md, explains the workflow and the explicit "out of scope" list.
Consequences¶
Positive:
- Adding a column becomes: edit
SchemaDefinition.php→composer schema:generate(regenerates001_schema.sql) → on the next deploy, the operator runsphp cli/schemaDiff.php --dsn=mysql:host=...to see theALTER TABLEstatement → apply. - The diff output is small, reviewable, and version-control-friendly. Operators can paste it into release notes.
- No new dependency. No new abstraction surface beyond a single CLI file.
- ADR-0005's single source is preserved —
SchemaDefinitionis read by both the compiler and the diff CLI. - Operators who want Phinx ergonomics can still adopt it; C does not block A.
Negative / accepted trade-offs:
- Operator runs the SQL. No auto-apply means a forgotten step results in a runtime error (controller hits a column that doesn't exist). The trade-off is preferred over silent schema mutation in production.
- Destructive ops are hand-written. Drop column, drop table, type change, rename — operator must author the SQL with awareness of data semantics. The CLI surfaces these gaps clearly (warnings in the output) so they are not silently overlooked.
- No multi-version migration history. Phinx-style "migration 0042 ran on 2026-02-01" tracking is not provided. Operators who need that level can layer Phinx on top of NeNe (the schema source is still
SchemaDefinition); ADR-0009 does not preclude that. - Diff heuristics are conservative. When in doubt, the CLI warns and refuses to emit. Operator escalation is expected for non-trivial changes.
Neutral:
- The CLI runs offline against the operator's existing DB credentials. No network round-trips beyond the DB query.
- The output format is line-by-line SQL with
-- commentheaders explaining eachALTER. Operators candifftwo runs to see what changed between releases.
Implementation tracking¶
- Implemented by FT17 (
docs/field-trials/2026-05-field-trial-17.md, Trial Issue #417). The implementation PR is #419 (feat) — addsNene\Xion\SchemaDiffer,cli/schemaDiff.php, promotes fourSchemaCompilerhelpers topublic static, and shipscomposer schema:diffas the operator-facing entry point. - The docs / workflow side ships in #420 —
docs/development/schema-migrations.md. - The add-index path is the only ADR-0009 scope item not in #419; it ships as the small follow-up Issue #421 (no new trial needed).
- A future ADR may extend this surface to (a) include Phinx as an optional dep, or (b) move to Option B (auto-generated
ALTER TABLEvia versionedSchemaDefinition) if the operator-side friction surfaces again. - ADR-0005's Consequences section already cross-links to this ADR ("Schema migrations: addressed by ADR-0009").