Repository navigation
ALTER COLLECTION ADD COLUMN zombifies existing rows — schema bumped without data migration #48
Copy link
Copy link
Closed
Labels
area:sqlParser, planner, SQL semanticsParser, planner, SQL semanticsengine:documentDocument engine (schemaless + strict)Document engine (schemaless + strict)sev:1-criticalData loss, corruption, security, or crash; no workaroundData loss, corruption, security, or crash; no workaroundtype:bugA defect — broken, incorrect, or lost dataA defect — broken, incorrect, or lost data
Description
Activity
- changed the title
[-]HIGH: ALTER COLLECTION ADD COLUMN zombifies existing rows — schema bumped without data migration[/-][+]ALTER COLLECTION ADD COLUMN zombifies existing rows — schema bumped without data migration[/+]on Apr 16, 2026 Confirmed. Root cause: the reader (
binary_tuple_to_valueindata/executor/strict_format.rs) calleddecoder.extract_all()against the current schema, but pre-ALTER tuples were encoded with a different physical layout (fewer/extra columns, different null-bitmap size, different variable-offset table positions)..ok()?swallowed theTruncatedTupleerror → row silently vanished or null-everywhere.schema.versionwas bumped but never consulted on the read path.Fix in #64:
ColumnDef.added_at_versiontracks when each column was added.StrictSchema.dropped_columns: Vec<DroppedColumn { def, position, dropped_at_version }>keeps tombstones so the physical layout at any prior version can be reconstructed.StrictSchema::schema_for_version(v)rebuilds the exact layout a tuple at versionvwas encoded against (excludes later adds, re-inserts later drops at their original positions).- Reader now reads the tuple's version, builds a sub-schema decoder when it lags the catalog, decodes old columns, and virtually fills new columns with their DEFAULT (Postgres
pg_attributestyle).
No row rewrite, no DROP COLLECTION recovery needed. 7 regression tests added in
sql_transactions.rscovering ADD, DROP, RENAME, ALTER TYPE, multi-ADD, and UPDATE on pre-ALTER rows — all passing.- added a commit that references this issue
on Apr 16, 2026 - addedtype:bugA defect — broken, incorrect, or lost dataA defect — broken, incorrect, or lost datasev:1-criticalData loss, corruption, security, or crash; no workaroundData loss, corruption, security, or crash; no workaroundengine:documentDocument engine (schemaless + strict)Document engine (schemaless + strict)area:sqlParser, planner, SQL semanticsParser, planner, SQL semantics
on Jul 8, 2026
Metadata
Metadata
Assignees
Labels
area:sqlParser, planner, SQL semanticsParser, planner, SQL semanticsengine:documentDocument engine (schemaless + strict)Document engine (schemaless + strict)sev:1-criticalData loss, corruption, security, or crash; no workaroundData loss, corruption, security, or crash; no workaroundtype:bugA defect — broken, incorrect, or lost dataA defect — broken, incorrect, or lost data
ALTER COLLECTION … ADD COLUMN …on aTYPE DOCUMENT STRICTcollection updates the catalog schema (schema.columns.push(...),schema.version += 1) but never touches existing row data. Pre-ALTER rows were written with the old column set; post-ALTER reads run against the new schema. The result depends on how the reader handles version drift, but in practice reads of the old rows return "null-everywhere" row objects (columns populated in the row are invisible because the schema-vs-data offset is wrong) or silently drop the new column from results.No error is raised. No warning is logged. The only known recovery is to
DROP COLLECTIONand recreate — which loses all data.Current code
nodedb/src/control/server/pgwire/ddl/collection/alter/add_column.rs:15-103:The function:
schema.columns+schema.versionin the catalog.There is no iteration over existing rows, no backfill of the DEFAULT, no rewrite of the on-disk row format, and no compatibility shim in the reader that knows to return the DEFAULT for rows serialized under an older schema version.
schema.versionis bumped (line 66) but I could not find any reader path that readsschema.versionand reconciles against a row's written-under version — the row format appears to assume the catalog schema always matches the bytes.Postgres
ALTER TABLE … ADD COLUMN … DEFAULT …either backfills (rewriting the table) or records a non-null default inpg_attributeand evaluates it virtually at read time. NodeDB does neither.Why this matters
ALTER COLLECTION t ADD COLUMN new_col …) is the documented way to evolve a schema. Users doing this against a collection with data will silently corrupt reads until they realise everySELECTreturns null-everywhere rows./query, native client).DROP COLLECTION+CREATE COLLECTION+ re-ingest — data loss.schema.version = schema.version.saturating_add(1)at line 66 suggests the data model expected versioning to be implemented, but the read path was never wired up; this is a half-built feature that in its current state silently loses data.Repro
Notes
CREATE COLLECTIONtime and neverALTER ADD COLUMNon a collection that has data — severely limiting live-migration patterns.