fix(migration): make option_type migration self-sufficient - #3603
Merged
Conversation
Version050300Date20260716000000 guarded changeSchema() against a missing option_type column but ran an unguarded UPDATE on it in postSchemaChange(), aborting occ upgrade on any instance where the column was absent. Create the column when missing and guard the backfill in both migrations. Fixes #3562 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Chartman123
approved these changes
Aug 25, 2026
Chartman123
marked this pull request as ready for review
August 25, 2026 13:14
Chartman123
enabled auto-merge
August 25, 2026 13:14
Collaborator
|
/backport to stable5.3 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Version050300Date20260716000000 guarded changeSchema() against a missing option_type column but ran an unguarded UPDATE on it in postSchemaChange(), aborting occ upgrade on any instance where the column was absent. Create the column when missing and guard the backfill in both migrations.
Fixes #3562
Assisted-by: Claude Code:claude-opus-5
Forms 5.3.4 introduced a database migration that is internally inconsistent: it explicitly tolerates the option_type column being missing during the schema phase, then unconditionally writes to that same column during the data phase, so any instance lacking the column fails the upgrade outright. Because Nextcloud records a migration as executed only after it completes, the failure repeats on every retry and leaves the instance unreachable until an administrator disables the Forms app by hand — which is exactly the loop three independent reporters describe on both PostgreSQL and MySQL. Investigation ruled out the obvious suspects (missing file in the released tarball, packaging exclusions, migration ordering, branch divergence) and established that affected instances have the column-creating migration recorded in oc_migrations while the column itself is absent, a desynchronisation that Nextcloud's schema-only install path can produce because it marks migrations as executed without per-migration verification. The defect escaped CI because all test workflows install the app from scratch, which never exercises the incremental-upgrade code path where these hooks run. The fix makes the migration self-sufficient — it recreates the column when missing and guards the backfill — and should ship as 5.3.6 with a backport to stable5.3; adding a CI job that performs a real version-to-version upgrade would prevent this whole class of defect from recurring.
Details
Analysis: #3562 — upgrade aborts with "column option_type does not exist"
Date: 2026-08-25
Analysed by: Andy Scherzinger (Assisted by Claude Code)
Affected app versions: Forms 5.3.3 / 5.3.4 / 5.3.5 (migration
050300Date20260716000000shipped in 5.3.4)Reported upgrade paths: 5.2.7 → 5.3.5, 5.3.2 → 5.3.5
Affected databases: PostgreSQL (
SQLSTATE 42703) and MySQL/MariaDB (1054) — both reportedSeverity: High — aborts
occ upgrade, leaves the instance unreachable until the app is disabled1. Symptom
Reporters describe a loop: the upgrade fails, the reverse proxy returns bad gateway, restarting the
container and re-running
occ upgradereproduces the identical error. Recovery requiredocc app:disable forms.2. Root cause — the migration is self-contradictory
lib/Migration/Version050300Date20260716000000.php:The schema phase explicitly handles the case where
option_typeis missing and emits no DDL.The data phase then unconditionally runs an
UPDATEagainst that same column. On any instancewhere the column is absent, this is a guaranteed hard failure.
Two aggravating properties:
MigrationService::executeStep()callsmarkAsExecuted()last,so the version is never recorded and every subsequent
occ upgradere-runs and re-fails.forward without manual DBA intervention or disabling the app.
3. Key deduction —
oc_migrationsand the schema are out of syncoption_typeis created byVersion050300Date20250914000000, added in 5.3.0 by the Grid featurecommit
ff31614band unchanged since.MigrationService::sortMigrations()orders050300Date20250914000000strictly before050300Date20260716000000. Therefore, if the creating migration had been pending, it would haverun first and created the column. The crash surfacing in
050300Date20260716000000proves that onaffected instances:
Diagnostic to confirm with reporters
PostgreSQL:
\d oc_forms_v2_options— MySQL/MariaDB:SHOW COLUMNS FROM oc_forms_v2_options;Expected on an affected instance: the
050300Date20250914000000row is present, the column is not.Implication for the "5.2.7 → 5.3.5" report
A clean 5.2.7 instance has no
050300*rows at all (verified: v5.2.7 and v5.2.10 ship 34migrations, none of them
050300*). Such an instance would run the creating migration normally.Any instance reporting this failure must therefore have been on 5.3.x at some point, or otherwise
have acquired the
oc_migrationsrow without the schema change.4. Causes ruled out
forms-v5.3.2.tar.gzandforms-v5.3.5.tar.gzfromnextcloud-releases/forms;Version050300Date20250914000000.phpis present in bothlib/MigrationMakefileappstoretarget includeslib/wholesale; no.gitattributes/export-ignore, nokrankerl.tomlsortMigrations()parses(\d+)Date(\d+), compares version asintthen date viastrnatcmp; 20250914 sorts before 20260716lib/Migration/: v5.2.10 → v5.3.0 adds 4 migrations, v5.3.2 → v5.3.5 adds exactly20260713180000and20260716000000; nothing removed or renameddropColumn('option_type')and nodropTable('forms_v2_options')anywhere inlib/Migration/5. How instances reach the desynchronised state
Installer::installAppLastSteps()(Nextcloud 33):MigrationService::migrateSchemaOnly():Relevant consequences:
regardless of whether the batched
migrateToSchema()actually applied their changes. There is noper-migration verification.
postSchemaChangenever runs on a first install. The backfill insideVersion050300Date20250914000000is skipped by design on this path — which is precisely the gapVersion050300Date20260716000000was written to close, but it was written assuming the schemahalf had always succeeded.
$toSchema ?: new SchemaWrapper(...). Any migration that mutates the schema and returnsnullbefore
$toSchemahas been seeded has its changes silently discarded while still being markedexecuted. Forms does not trip this today (the first migration returns
$schema), but it is theclass of defect that produces exactly this symptom.
Realistic real-world paths to the desynchronised state: a schema-only install or re-enable where the
batched DDL did not materialise; an app remove/re-add or a database restore that leaves
oc_migrationsrows out of step with the tables; or a partially applied batch.6. Why CI did not catch it
The
phpunit-mysql/phpunit-pgsql/phpunit-mariadb/phpunit-oci/phpunit-sqliteworkflows install the app fresh, which is the
schemaOnly = truepath —postSchemaChangeisnever executed there. The incremental upgrade path (old version installed → files replaced →
occ upgrade) is the only one that runs these hooks and it is not covered by any workflow.7. Implemented fix
Version050300Date20260716000000is made self-sufficient rather than dependent on its predecessorhaving succeeded:
changeSchema()createsoption_typewhen it is missing, andpostSchemaChange()returns early when the column is still absent. The samepostSchemaChangeguard is added to
Version050300Date20250914000000, which also gains an explicit'length' => 255instead of relying on the DBAL default for a length-less
Types::STRING.Target
main, backport tostable5.3, release as 5.3.6.8. General rules to carry forward
postSchemaChangethat touches a column must never be reachable on a code path where its ownchangeSchemapermitted that column to be absent.the schema it depends on. "The earlier migration is recorded in
oc_migrations" is not aguarantee that its DDL landed.
the working tree, run
occ upgrade) sopre-/postSchemaChangehooks are covered.9. Workaround for affected administrators
Until a fixed release is available, add the column manually and re-run the upgrade (adjust the
oc_prefix to match
dbtableprefix):Then run
occ upgradefollowed byocc app:enable forms.🤖 AI (if applicable)