Skip to content

data-structures: a publish keeps a column its table no longer declares, drops it only when listed as dropped, and renames it in place when declared renamedFrom (#7635) - #7674

Merged
delchev merged 1 commit into
masterfrom
issue-7635-keep-undeclared-columns
Oct 5, 2026
Merged

delchev merged 1 commit into
masterfrom
issue-7635-keep-undeclared-columns

Conversation

@delchev

@delchev delchev commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Cause

TableAlterProcessor, the alter path of every .table and .schema (so of every intent-generated table), reconciled the live table in two passes. First it ADDed the columns the model declares, then it DROPped every live column the model no longer names. TablesSynchronizer / SchemasSynchronizer are multitenant, so the DROP ran in every tenant schema, and the only log line was the INFO Processing Alter Table. A renamed intent field emits a new column name, so the publish added the new column and dropped the old one with its data, on the first boot of the new image. An image-only rollback cannot undo that.

Change: expand/contract for undeclared columns

  • Kept by default. An undeclared column stays with its data. It is logged at WARN with the table, the column and the owning artefact, and counted as the orphanColumns detail of the artefacts health component (PlatformReadiness.recordOrphanColumns, keyed per catalog.schema.table so one tenant's copy does not overwrite another's). A kept NOT NULL column is relaxed to nullable, because the app no longer writes it and every insert would otherwise be refused (new ISqlDialect.dropNotNull). MySQL, MariaDB, MSSQL and HANA return null there, because they need the whole column type restated; they get a WARN saying inserts that omit the column will be refused.
  • Dropped only when declared. A table's dropped: [...] removes those columns. DIRIGIBLE_DATABASE_DROP_UNDECLARED_COLUMNS=true is an operator opt-in for dev instances.
  • Rename as a first-class change. A column's renamedFrom renames the live one in place before the ADD pass (new ISqlDialect.renameColumn: ANSI by default, HANA's RENAME COLUMN t.c TO n). On MSSQL and MongoDB it returns null, and the ADD pass then adds the column nullable, copies the values and keeps the old one. A rename runs only while the old name is live and the new one is not, so it is harmless to leave in the definition.
  • The INCOMPATIBLE_CHANGE_OF_TABLE refusals are unchanged.

Intent DSL. A field's renamedFrom: <old name> and an entity's dropped: [names] become the .model's dataRenamedFrom / dataDropped. These are the former column names, derived like dataName, and comma-separated so they survive the scalar-only .edm. template-application-schema emits them as the column's renamedFrom and the table's dropped. The parser refuses:

  • either declaration naming a column the entity still declares (compared by column, so a relation counts);
  • a rename from the field's own name or from a dropped name;
  • two fields renamed from one name.

The assistant guide, intent-dsl-features.md and synchronizer-model.md document it.

Verification

Run:

  • Unit: TableAlterProcessorTest (keep + count + relaxed NOT NULL with a later insert, declared drop, operator opt-in, in-place rename with a no-op second run), ColumnChangeStatementTest (default / MySQL / HANA), PlatformReadinessTest, SchemaEvolutionValidationTest, EdmIntentGeneratorTest. Every changed module's unit suite is green.
  • TableUndeclaredColumnIT (new, HTTP-level, untagged so it is in the PR smoke set) on H2 and on a local PostgreSQL 16. pg_stat_database confirmed PostgreSQL was written to. The run:
    • publish A, B NOT NULL with a row, then republish with A only: B and its value survive, the WARN is logged, the orphan is counted, and an insert without B passes;
    • republish with dropped: [B]: B is gone;
    • the schema the real template renders from a .model with dataRenamedFrom / dataDropped, published over the old table: the values moved to the new name, and the old and the dropped columns are gone.
  • SchemaRepublishTypeToleranceIT (H2 + PostgreSQL), SchemaTemplateUniqueConstraintIT, SchemaTwoUniqueConstraintsIT and IntentEngineIT (81 tests) on H2.
  • formatter:validate with the cache wiped, and the release-profile javadoc build of the touched modules.

Not run:

  • The ADD + copy fallback and the MySQL/MSSQL/HANA paths against a real database (only the statement shapes are unit-tested); none of those databases is a CI leg.
  • A second tenant: the IT runs in the default tenant. The per-tenant isolation of the orphan count is unit-tested only.
  • The full nightly suite.

Not in this PR: the intentfile.org spec/site and dirigible.io /help/intent/ pages for renamedFrom: / dropped: follow as separate PRs.

Fixes #7635

🤖 Generated with Claude Code

…s, drops it only when listed as dropped, and renames it in place when declared renamedFrom (#7635)

TableAlterProcessor reconciled a published .table / .schema against the live
table by ADDing missing columns and then DROPping every live column the
model no longer named - in every tenant schema, with nothing in the log
beyond the INFO "Processing Alter Table". A renamed intent field emits a new
column name, so its data was gone on the first boot of the new image, and a
rollback that only swaps the image could not bring it back.

The alter now follows an expand/contract contract:

- An undeclared column is KEPT with its data, logged at WARN with the table,
  the column and the artefact that owns the table, and counted as the
  orphanColumns detail of the artefacts health component
  (PlatformReadiness.recordOrphanColumns, keyed per catalog.schema.table so
  one tenant's copy does not overwrite another's). A kept NOT NULL column is
  relaxed to nullable, because the application no longer writes it and every
  insert would be refused otherwise; a dialect that cannot express that
  without the column's whole type (MySQL, MariaDB, MSSQL, HANA) leaves it
  and says so.
- A column leaves only when the table lists it under `dropped`, or on an
  instance with DIRIGIBLE_DATABASE_DROP_UNDECLARED_COLUMNS=true (dev opt-in).
- A column declaring `renamedFrom` renames the live one in place before the
  ADD pass (new ISqlDialect.renameColumn: ANSI by default, HANA's own form).
  Where a dialect has no in-place rename (MSSQL, MongoDB) the ADD pass adds
  the column nullable, copies the values and keeps the old one.
- The incompatible-change refusals are unchanged.

The intent carries both: a field's `renamedFrom: <old name>` and an entity's
`dropped: [names]` become the .model's dataRenamedFrom / dataDropped (the
former COLUMN names, derived like dataName; comma-separated so they survive
the scalar-only .edm), which the schema template emits as the column's
renamedFrom and the table's dropped. The parser refuses either one naming a
column the entity still declares (compared by column, so a relation counts),
a rename from its own name or from a dropped name, and two renames from one
name.

Verified: TableAlterProcessorTest (keep + count + relaxed NOT NULL, declared
drop, operator opt-in, in-place rename with a no-op second run),
ColumnChangeStatementTest (default / MySQL / HANA statement shapes),
PlatformReadinessTest, SchemaEvolutionValidationTest, EdmIntentGeneratorTest;
the unit suites of every changed module; TableUndeclaredColumnIT on H2 and
on a local PostgreSQL 16 (publish A,B + a row, republish A only: B and its
data survive, the WARN is logged, an insert without B passes; republish with
dropped: [B]: B is gone; the schema the template renders from a .model with
dataRenamedFrom / dataDropped moves the data and drops the column), plus
SchemaRepublishTypeToleranceIT, SchemaTemplateUniqueConstraintIT,
SchemaTwoUniqueConstraintsIT and IntentEngineIT on H2; formatter:validate
with the cache wiped; the release-profile javadoc build of the touched
modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant