Skip to content

Keep schema apply from dropping the tables of a plugin the host does not register - #3123

Merged
pournasserian merged 4 commits into
mainfrom
3072-schema-apply-plugin-tables
Nov 15, 2026
Merged

pournasserian merged 4 commits into
mainfrom
3072-schema-apply-plugin-tables

Conversation

@pournasserian

@pournasserian pournasserian commented Nov 15, 2026 •

Copy link
Copy Markdown
Member

Closes #3072

fluentcms schema apply --allow-destructive no longer drops the tables of a plugin that the host does not register. A history table cms_{plugin}__migrations in the database marks cms_{plugin}_ as a plugin's prefix. The tables under it are left alone and listed in the plan under Left alone:, with the history table as the reason, and --script lists them as comments. A stale content table is still dropped. Schema sync shares the comparison, so it no longer reports such a table as dropped.

Schema apply knows a removed type by the newest row of the schema history. For a host without content migrations, that row is now written only by what brings the content tables up to the schema: schema apply and schema sync, never migrate or a start. Before, migrate and a start recorded the new schema when no column was missing. Schema apply then lost sight of a removed type, and a block field renamed with formerNames passed as "Nothing to apply", with its values left under the old key.

The PR also fixes the two Low items of the review of #3084. On SQLite a renamed JSON field's check takes the new name, and a rename plus a column change now has a data test. From the review of this PR:

  • the plugin names system and content are refused;
  • the script escapes control characters in its comments.

Spec 4.3 and 6.1, docs/docker.md, docs/content-migrations.md and docs/plugin-migrations.md say what was built.

Decisions

  • Which history tables name a plugin. Only cms_{name}__migrations where the name is a plugin name with underscores. The system's cms__migrations and the content stream's cms_content__migrations are left out, so cms_ and cms_content_ never spare a content table. The prefix keeps its trailing underscore, so cms_demonstration is not under cms_demo_. When several prefixes match, the plan names the longest.
  • A content table under a plugin's prefix. A content type's table can fall under a plugin's prefix: seo-meta gives cms_seo_meta under cms_seo_, and a localized seo gives cms_seo_i18n. Names cannot tell these apart, and neither can columns, since the search plugin's cms_search_entry has the content tables' id, document_id, site_id, state and locale. So schema apply reads the newest row of the schema history. The table of one of its collections or singletons is a content table under any prefix, and so is the locale table of a localized one, and a stale one is dropped. Only the newest row counts, so a plugin table that later took the name of an older type is not mistaken for content. With no row, or a row that cannot be read, the safer choice holds: every table under a plugin's prefix is left alone.
  • Registered plugins. The history rule covers the prefixes of registered plugins too. Until now, a removed type's table under such a prefix was kept without a word. A registered plugin's own tables stay its stream's, as before, and are not listed.
  • Who records the schema history.
    • A host with content migrations is unchanged: migrate and a start that migrates record when no column is missing.
    • A host without content migrations is the one schema apply serves. For it, migrate and a start that migrates no longer record. A start now logs the drift (9315) instead, and 9314 and 9315 ask such a host to run fluentcms schema apply.
    • For such a host, schema sync records only when it applied every change and the history counts nothing as lost, since sync moves no value. A removed type or a field renamed in JSON therefore keeps the row until schema apply records past it with --allow-destructive. A host with content migrations records after every sync that left nothing undone, as before: schema apply refuses that host, so nothing else would ever record past the loss.
    • schema diff --applied names fluentcms schema apply for an empty history of such a host (FCMS3323). When the history cannot be read (FCMS3324), it asks to run migrate and then schema apply. The migrate help says it records only for a host with content migrations.
    • The lookup of the host's content migrations runs inside the drift check's and sync's error handling, so a failure there never stops a start.
    • A new database gets its first row from the first schema apply, which records one even with nothing to apply, or from schema sync's first start.
    • The Fresh machine and template flows use content migrations, and the compose flow runs schema apply after migrate, so all three keep their first row.
    • The loss check that schema apply had is now shared, in SchemaHistory.Losses.
  • Reserved plugin names. A plugin's name is also its stream's. So system and content are refused with FCMS1502, both in PluginHost.Read and for a stream registered through IPluginMigrations, which MigrationRunner checks when it is created.
  • Database names. The plan's Operations: and Left alone: lines and the script's comments go through PrintableText.Escape (from CLI, backup and restore follow-ups from the review of M4 and M6 #3068, merged from main). A stale table whose name has a control character is left alone and listed, never dropped. No content table has such a name, and a GO line in one would split a SQL Server script.
  • SQLite check after a rename (Low item 1): fixed. The rename step now adds the model's check on the new column on every provider whose model has one. SQLite does that by rebuilding the table, so the check takes the new name, as in a migration. SQL Server is unchanged when its reader reports the old check. PostgreSQL has no JSON check.
  • No new error codes, log event ids, public API or migrations. The 9314 and 9315 messages take their advice as a parameter.

Declined

None. The second Low item, a rename plus a change of store type, now has a data test that applies it on every provider.

Dependencies

None.

Acceptance

  • A table under the prefix of a plugin that has a history table in the database and is not in the host is not dropped by schema apply --allow-destructive, and the plan says why
  • A stale content table is still dropped

Tests

  • SchemaApplyTests.TheTablesOfAPluginTheHostDoesNotRegisterAreLeftAloneAndARemovedTypesTablesAreDropped: the demo plugin is migrated, then left out of the host. Its table is kept and listed in the plan and in the script. The removed types' tables are dropped, including cms_demo_post and cms_demo_i18n under cms_demo_, and so are the hand-made cms_demonstration and cms_content_extra, the latter next to an empty content history. A host that registers the plugin again lists nothing.
  • SchemaApplyTests.MigrateAndAStartBeforeSchemaApplyLeaveItTheRowThatNamesARemovedType: migrate and a start that migrates run with the new schema before apply. demo-post, under the prefix of the registered plugin demo, is then still refused and is dropped with --allow-destructive. Fails on the previous code.
  • SchemaApplyTests.ABlockFieldRenamedBeforeMigrateAndAStartIsStillALoss: the same steps with a block field renamed; apply still stops with FCMS3337. Fails on the previous code.
  • SchemaSyncTests.ABlockFieldRenamedIsNotRecordedSinceItsValuesStayUnderTheFormerName: sync keeps the row and logs the drift, asking for fluentcms schema apply. Fails on the previous code.
  • SchemaApplyTests.WithoutAReadableHistoryRowEveryTableUnderAPluginsPrefixIsLeftAlone: with snapshot = '[]', and then with no row, --allow-destructive leaves cms_demo_post alone.
  • SchemaApplyTests.ATableWhoseNameHasAControlCharacterIsLeftAloneAndItsNameEscaped, with a table named cms_bad\nGO\nname.
  • SchemaSyncTests.AHostWithContentMigrationsRecordsASyncThatRemovesABlockField: 2 rows. Fails on the previous round's code.
  • SchemaSyncTests.ASyncWhoseHistoryCannotBeReadIsNotRecordedAndDoesNotFail.
  • SchemaHistoryTests.AStartOfAHostWithoutContentMigrationsRecordsNothingAndAsksForSchemaApply, for both start paths: the 9314 advice with an empty history, then the 9315 advice.
  • SchemaApplyTests.ADiffFromAnEmptyOrUnreadableHistoryNamesWhatRecordsItForTheHost (FCMS3323 for both kinds of host, and FCMS3324), and SchemaCommandTests checks the new advice.
  • MigrationRunnerTests.AStreamNamedAfterOneOfCoresIsRefused and PluginValidationTests.ABadManifestStopsTheHost for system and content.
  • SchemaApplyTests.ARenamedJsonFieldKeepsItsValuesAndItsCheckTakesTheNewName now checks the check's name on SQLite, and fails without the fix.
  • SchemaApplyTests.ARenamedFieldWhoseColumnAlsoChangesKeepsItsValues: a rename and then an AlterColumn, applied, with the values kept.
  • SchemaSyncTests.ATableOfAPluginTheHostNoLongerRegistersIsNotAStaleContentTable and SchemaSnapshotTests for ContentTypesOf.
  • Local: the migration data tests on SQLite, the plugin validation and contribution tests, the schema command and startup report integration tests, the SDK, core and architecture tests, a Release build with -warnaserror, and markdownlint. PostgreSQL and SQL Server ran in the full CI run.

Full CI run: 43731667867, passed, on 8419100 after merging main. The earlier runs were 43724851071, which passed after the rerun of a timing-bound job test on SQL Server 2019, and 43659302666.

@pournasserian pournasserian added type:bug Defect area:storage EF Core model, providers and migrations labels Nov 15, 2026
@pournasserian
pournasserian merged commit 53059cd into main Nov 15, 2026
31 checks passed
@pournasserian
pournasserian deleted the 3072-schema-apply-plugin-tables branch November 15, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:storage EF Core model, providers and migrations type:bug Defect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep schema apply from dropping the tables of a plugin the host does not register

1 participant