Skip to content

encrypt cutover mishandles multi-column pending configs (silent bookkeeping drift) #409

@coderdan

Description

@coderdan

Summary

Severity: high — silent bookkeeping drift.

When a pending EQL config contains more than one renamed column on a single table, `stash encrypt cutover --column X` physically renames all of the renamed columns (because `eql_v2.rename_encrypted_columns()` operates on the entire pending config in one go), but only emits a `cut_over` event in `cs_migrations` for the column named on the CLI. The other columns are renamed in the database but their lifecycle bookkeeping is left at `backfilled`.

Repro

  1. Register two encrypted twins on the same table (e.g. `users.email` and `users.first_name`).
  2. Backfill both → both reach phase `backfilled`.
  3. Update the schema to rename both encrypted columns into the primary names; `stash db push` writes the new shape as pending.
  4. `stash encrypt cutover --table users --column email`.

Expected: both `email` and `first_name` are physically renamed AND both have a `cut_over` event in `cs_migrations`.

Actual: both are physically renamed, the pending config is promoted to active, but only `email` has a `cut_over` row in `cs_migrations`. `first_name` stays at `backfilled`.

Downstream symptoms

  • `stash encrypt cutover --column first_name` fails with "No pending EQL configuration" (the pending was already promoted).
  • `stash encrypt drop --column first_name` refuses because `cs_migrations` still says `backfilled`.
  • The only workaround today is a manual `INSERT INTO cipherstash.cs_migrations` for the orphaned column.

Surfaced by the 2026-05-04 spike.

Root cause

`packages/cli/src/commands/encrypt/cutover.ts:99-108` calls `renameEncryptedColumns(client)` and then `appendEvent` for the single `{tableName, columnName}` pair from the CLI flags. The rename is config-wide; the event log entry is per-column.

Proposed fix (recommended)

After `renameEncryptedColumns()` runs, walk the just-promoted pending config and emit a `cut_over` event for every column whose `_encrypted` sibling was renamed — not just the one named on the CLI.

Concretely:

  1. Before the `BEGIN` block, snapshot the pending config's column set.
  2. After `renameEncryptedColumns` / `migrateConfig` / `activateConfig`, walk the pending columns and `appendEvent` for each.
  3. Validate that the explicitly-named `--column` is in that set; if not, fail loudly (current behaviour for an unknown column should be preserved).

Side benefit: lets users batch multi-column cutovers with `stash encrypt cutover --table T` and no `--column` flag (cuts over every pending column on that table).

Alternatives considered

  • Per-column rename: change `rename_encrypted_columns()` (or add a per-column variant in EQL) so cutover stays one-column-at-a-time. Bigger surface — touches EQL.
  • Refuse multi-column pending: `cutover` errors out unless `--all-pending` is passed. Conservative; introduces a new flag and a new failure mode.

The recommended fix matches the apparent intent of the existing code without a new flag or an EQL change.

Acceptance criteria

  • Multi-column pending → `cutover --column X` emits `cut_over` events for every column the rename touched.
  • Subsequent `encrypt drop --column Y` (Y ≠ X but same pending batch) succeeds without manual SQL fixes.
  • Single-column case continues to behave identically.
  • `cutover --table T` with no `--column` is supported as a multi-column shorthand.
  • Integration test (gated on `POSTGRES_URL_FOR_TESTS`) covers the two-column scenario end-to-end.

Related

  • Followups doc §3.6 — integration tests for the new pending/active flow (this fix should ship a new test in that suite).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions