Skip to content

fix(mongo): inherit virtual/autoInc from legacy _fields entries - #145

Closed
undefined-moe wants to merge 1 commit into
cordiverse:mainfrom
undefined-moe:fix-mongo-meta-migration
Closed

undefined-moe wants to merge 1 commit into
cordiverse:mainfrom
undefined-moe:fix-mongo-meta-migration

Conversation

@undefined-moe

Copy link
Copy Markdown
Member

Background

While upgrading @minatojs/driver-mongo 3.6.1 → 3.7.0 on a production deployment, we hit a chain of failures that all trace back to one root cause in the _fields meta migration.

Root cause

_createFields initializes new-format _fields entries (keyed by _id: table) like this:

if (!found?.fields) {
  await metaTable.updateOne(meta, { $set: { fields: Object.keys(fields) } }, { upsert: true })
  return
}

For an existing deployment, the legacy (< 3.7.0) _fields collection contains per-table entries keyed by { table, field } with an ObjectId _id, carrying virtual and autoInc markers. When the new-format entry is created, only fields is persisted — virtual and autoInc are silently dropped.

Observed consequences

  1. autoInc loss → id reuse. _migratePrimary no longer short-circuits (if (!isNullable(found?.autoInc)) return) and rescans the table on every startup. If rows with the highest ids were ever deleted, the recalculated counter (max(remaining ids)) makes the next insert reuse the id of a deleted row, silently corrupting references from other tables. On our deployment (81k user rows) this caused E11000 duplicate key errors on the binding table on every message.

  2. Startup deadlock. The rescanned _migratePrimary runs inside prepare(), and every query awaits database.prepared() (selection.ts). With the user table carrying login-added migration hooks, the hook's own queries deadlocked the whole prepare chain in our case — every database query in the process hung forever, with zero log output.

  3. virtual loss → destructive full-table rewrite. If the recorded virtual flag disagrees with the table shape, _migrateVirtual rebuilds the entire collection via $out + drop + rename, with no validation against the actual data. In our incident this rewrote the numeric id column of 81k rows into ObjectIds (we recovered from backup + oplog, but a virtual/autoInc marker loss should never be able to trigger this path in the first place).

Fix

When initializing a new-format _fields entry, look up the legacy entry for the same table and inherit virtual / autoInc if present:

const legacy = await metaTable.findOne({ table, field: this.model(table).primary })
const $set: Partial<TableMeta> = { fields: Object.keys(fields) }
if (legacy?.virtual !== undefined) $set.virtual = legacy.virtual
if (legacy?.autoInc !== undefined) $set.autoInc = legacy.autoInc

Note: _migratePrimary reads autoInc regardless of the field value stored in the legacy entry, so inheriting by table lookup (with the primary field as query key) matches existing semantics.

Tests

Added packages/mongo/tests/meta-migration.spec.ts:

  • inherit test: seeds a legacy-format _fields entry (with a deleted max-id row), restarts the driver, and asserts virtual/autoInc are inherited and the deleted row's id is not reused. Fails on master (expected 2 to equal 3), passes with the fix.
  • cold re-detect test: drops _fields entirely and asserts the existing inference-from-data behavior still works.

Full mongo suite: 268 passed (266 existing + 2 new).

When migrating from the pre-3.7.0 `_fields` format (per-table entries
keyed by `{ table, field }` with ObjectId `_id`), the new-format
initialization in `_createFields` only persisted the `fields` list and
silently dropped the `virtual` and `autoInc` markers.

Consequences after upgrading:

- `_migratePrimary` no longer short-circuits on the stored `autoInc`
  and rescans the table on every startup. If rows were ever deleted,
  the recalculated counter reuses ids of deleted rows, silently
  corrupting references from other tables.
- With a stale/lost `virtual` marker, `_migrateVirtual` may consider
  the table shape mismatched and run its destructive full-table
  rewrite ($out to `_migrate_` + drop + rename), which can destroy
  primary-key columns when the actual data does not match the
  recorded flag.

Fix: when initializing a new-format entry, look up the legacy entry
for the same table and inherit `virtual` / `autoInc` if present.
@Hieuzest

Hieuzest commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Legacy _fields is ignored or dropped as designed, since the meta table is reconstruct-safe. Superseded by #146

@Hieuzest Hieuzest closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants