← all branches

fix/bf-rollback

needs attentionviewing older commit
0fd71fb · fullPR #259reviewed 2026-07-03 20:49 UTC0H · 4M · 2L · 7I
The branch
Purpose
Provide a proven, reviewed rollback path before running the derived-concept backfill on production
Goal
Add a tested rollback script and fix the --rpu array-binding bug in the backfill
Sub-goals
  • SG-1: New rollback script removes only powerFactorAdjustment + billingFrequency keys (append-only inverse)
  • SG-2: Fix --rpu filter from ANY(jsArray) to sql.join IN-list to avoid postgres.js malformed array literal
  • SG-3: Mirror backfill's --expect-ref, --dry-run, --rpu guards in rollback for operational consistency
The changes (whole branch)
What
Added scripts/rollback-derived-bill-concepts.ts (76 lines) and fixed the --rpu sql.join pattern in scripts/backfill-derived-bill-concepts.ts (5 lines)
Why
The --rpu bug was silently mis-serializing during the scoped staging run; without a rollback tool, running on prod carried unrecoverable risk
Areas
scripts/backfill-derived-bill-concepts.ts+61scripts/rollback-derived-bill-concepts.ts+760
Blast
2 files, +82/-1; scripts/ only — no domain code, API, or infra touched
ops-script data-mutation staging-verified
ci· CI status unavailable via personal access tokencoderabbit· no .coderabbit.yaml in repostaging-round-trip· backfill → rollback → backfill verified on staging (361→359→361 line items, lossless)

Findings · 13

correctness2

medium

--rpu repeated-flag form silently drops all but the first value

scripts/rollback-derived-bill-concepts.ts:22

argValue(flag) uses process.argv.indexOf(flag) which returns only the first occurrence. Invoking --rpu R1 --rpu R2 results in RPUS=['R1'] — R2 is silently ignored. The comma-separated form --rpu R1,R2 works correctly. The same bug exists in the backfill script. An operator using the repeated-flag convention could run an incomplete rollback without any warning.

low

Before-count and UPDATE not wrapped in a transaction — log may report incorrect N

scripts/rollback-derived-bill-concepts.ts:54

The SELECT COUNT(*) for toRemove and the UPDATE run as separate statements with no transaction wrapper. A concurrent write between the two could cause '[done] stripped keys from N bills' to misreport actual affected rows. For a maintenance script on a quiesced DB this is cosmetic, but worth noting.

security3

info

RPUS values properly parameterized via sql template tag

scripts/rollback-derived-bill-concepts.ts:46

Each RPU is wrapped in sql`${r}` before joining, ensuring Drizzle treats them as bound parameters. No SQL injection risk.

info

KEYS are hardcoded constants — not user-input driven

scripts/rollback-derived-bill-concepts.ts:29

KEYS is a compile-time constant, never derived from argv or environment. No injection surface.

info

--expect-ref guard bypass depends on projectRefFromUrl correctness

scripts/rollback-derived-bill-concepts.ts:35

The production guard is contingent on projectRefFromUrl correctly parsing all URL forms in use (pooler vs direct vs custom domain). The function aborts on null (safe fallback); projectRefFromUrl is independently tested in packages/database/src/__tests__/preview-db.test.ts.

conventions1

low

Rollback uses db.execute() directly; backfill uses Database cast alias

scripts/rollback-derived-bill-concepts.ts:54

The backfill establishes const database = db as unknown as Database and calls database.execute(...) throughout. The rollback calls db.execute(...) with inline casts at call sites. Both work at runtime, but the divergence makes the two companion scripts harder to read side-by-side and may mask type errors under strict mode.

tests2

info

No automated regression test for the --rpu array-binding fix

scripts/backfill-derived-bill-concepts.ts:157

The ANY(jsArray) → sql.join IN(...) fix is validated only by staging round-trip. A unit test asserting the generated SQL contains 'IN ($1, $2)' rather than 'ANY' would protect against future Drizzle/postgres.js serialization regressions.

info

Rollback UPDATE logic tested only by staging round-trip

scripts/rollback-derived-bill-concepts.ts:61

The jsonb_agg/jsonb_array_elements strip expression is non-trivial but unautomated. Acceptable for an infrequently-run emergency tool — dry-run and post-update count provide runtime confidence.

improvement5

medium

hasKeys JSONB filter hardcoded — diverges from KEYS array driving the strip

scripts/rollback-derived-bill-concepts.ts:52

KEYS = ['powerFactorAdjustment', 'billingFrequency'] defines what to strip, but the hasKeys sql fragment hardcodes both names as separate @> containment checks. If a third key is added to KEYS, bills carrying only that key won't be found by the filter — the UPDATE silently skips them while the count under-reports scope. The UPDATE's NOT IN (${keyList}) correctly drives from KEYS; the filter should too.

medium

Duplicated boilerplate across backfill and rollback (argValue, RPUS, EXPECT_REF, rpuFilter)

scripts/rollback-derived-bill-concepts.ts:21

Both scripts contain identical argValue(), DRY_RUN, RPUS, EXPECT_REF parsing and the same rpuFilter sql.join construction — including the same block that just contained the ANY() bug fixed in this PR. Fixing in one script required a manual fix in the other. A scripts/lib/args.ts helper would make future fixes atomic.

medium

Rollback guard omits maskedUrl logging present in backfill guard

scripts/rollback-derived-bill-concepts.ts:38

The backfill logs the masked database URL on guard success, giving operators a visible confirmation of which host they're targeting. The rollback only logs the project ref. For a destructive UPDATE this is a meaningful auditability gap — the operator can't visually confirm the host from the log output.

info

rpuFilter SQL fragment duplicated verbatim between both scripts

scripts/rollback-derived-bill-concepts.ts:46

The sql.join rpuFilter construction is copy-pasted identically. This was the exact fragment containing the ANY() bug; a shared helper would make future fixes apply once.

info

Inconsistent db access: rollback uses db.execute(), backfill uses Database wrapper

scripts/rollback-derived-bill-concepts.ts:54

Minor inconsistency; both work. Worth standardizing if a shared script lib is extracted.

History · 5 commits

  1. cbdca09safeincremental0H · 0M · 0L2026-07-03 22:30
  2. baca7afsafeincremental0H · 0M · 3L2026-07-03 21:27
  3. efb8a4fneeds attentionincremental0H · 1M · 7L2026-07-03 21:20
  4. b62fb9aneeds attentionincremental3H · 4M · 5L2026-07-03 21:04
  5. 0fd71fbneeds attentionfull0H · 4M · 2L2026-07-03 20:49current