parity(api): declare _goldbank_migrations as an ignored model; drift now zero
Prisma migrate diff proposed DROP TABLE "_goldbank_migrations" because the table exists in the database but was not declared in the schema. Declaring it with @@ignore removes the proposal without exposing it to Prisma Client, and the table and all its rows are kept. Correcting the earlier DEAD-07 note: this is not a leftover from the pre-Prisma port. It is the tracking table of the application's own migration runner (src/prisma/migrate.ts:5), which exists because the Docker deploy image is copy-only and ships no Prisma CLI, so migrate deploy cannot run in the container (migrate.ts:7-13). The project therefore keeps two ledgers deliberately: _prisma_migrations for the CLI in development, and _goldbank_migrations for the runtime runner in the container. That runner already filtered out any DROP of its own table (migrate.ts:190-196); the schema declaration removes the cause rather than relying on that guard. The practical consequence, recorded in DECISIONS.md D15: a new migration must be safe under both paths, so its SQL stays idempotent. The ledger grew 12 -> 14 rows while testing, with nothing deleted: starting the API ran prepareDatabase(), which appended the two migrations applied earlier today. Its $$-aware statement splitter handled the PL/pgSQL trigger body correctly — trigger and function are both present and the 9 trigger assertions pass against the live database. Also records the UTF-8 testing rule: Persian test input must come from a file, never a shell argument, because the Windows shell replaces Persian characters with '?' and a test can then pass while storing '????'. Verified: migrate diff -> "This is an empty migration" (zero drift); migrate status -> up to date; tsc clean; 32 jest; 9 trigger assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8133a2bdf4
commit
654b98520d
@@ -974,10 +974,10 @@ model CustomerOtp {
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// bcrypt hash of the 4-digit code. Never the code itself.
|
||||
codeHash String
|
||||
expiresAt DateTime
|
||||
codeHash String
|
||||
expiresAt DateTime
|
||||
/// Failed verify attempts against THIS challenge.
|
||||
attempts Int @default(0)
|
||||
attempts Int @default(0)
|
||||
/// Set when the code is accepted, so one challenge cannot be reused.
|
||||
consumedAt DateTime?
|
||||
|
||||
@@ -989,3 +989,25 @@ model CustomerOtp {
|
||||
@@index([customerId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
/// Bespoke migration ledger left by the EARLIER, pre-Prisma port.
|
||||
///
|
||||
/// It predates Prisma Migrate in this repo and holds 12 historical rows
|
||||
/// (`0001_init.sql`, `0002_admin_schema.sql`, `seed:initial`, and the early
|
||||
/// `2026091…` migrations). It is the only record of how that port was applied,
|
||||
/// so it is **kept and never written to or deleted from** (DEAD-07).
|
||||
///
|
||||
/// It is declared here purely so `prisma migrate diff` stops proposing
|
||||
/// `DROP TABLE "_goldbank_migrations"` — an undeclared table reads as drift.
|
||||
/// `@@ignore` keeps it out of Prisma Client: nothing in the application should
|
||||
/// ever read or write it, and Prisma Migrate will not manage its shape either.
|
||||
///
|
||||
/// Prisma's own ledger is `_prisma_migrations`, which is separate and is never
|
||||
/// edited by hand (D14).
|
||||
model goldbank_migrations {
|
||||
name String @id
|
||||
appliedAt DateTime @default(now()) @map("applied_at") @db.Timestamptz(6)
|
||||
|
||||
@@map("_goldbank_migrations")
|
||||
@@ignore
|
||||
}
|
||||
|
||||
@@ -777,3 +777,104 @@ operator actually typed into.
|
||||
Verified live by `apps/api/scripts/test-name-trigger.cjs` — 9 assertions across
|
||||
all three directions plus insert, a multi-token name, and a no-op update. It
|
||||
creates one temporary row and deletes it, reporting the deletion.
|
||||
|
||||
---
|
||||
|
||||
# D15 — `_goldbank_migrations` is kept, and the UTF-8 testing rule (2026-09-22)
|
||||
|
||||
## The table stays, declared and ignored
|
||||
|
||||
**Owner's ruling:** keep `_goldbank_migrations`, delete no row from it, declare
|
||||
it in `schema.prisma` with `@@ignore` so `migrate diff` stops proposing a drop,
|
||||
and explain why it is there.
|
||||
|
||||
Done — `model goldbank_migrations` at the end of `schema.prisma`, mapped to the
|
||||
real table name, with `@@ignore` so it never enters Prisma Client.
|
||||
|
||||
**Verification:**
|
||||
|
||||
```
|
||||
npx prisma migrate diff --from-schema-datasource --to-schema-datamodel --script
|
||||
→ -- This is an empty migration. (drift: ZERO)
|
||||
npx prisma migrate status
|
||||
→ 11 migrations found. Database schema is up to date!
|
||||
Prisma Client exposes it? no (@@ignore)
|
||||
```
|
||||
|
||||
## ⚠️ Correction to the earlier DEAD-07 assessment
|
||||
|
||||
The previous entry called this table a leftover from the pre-Prisma port. **That
|
||||
was wrong**, and investigating the ruling is what surfaced it.
|
||||
|
||||
`_goldbank_migrations` is the tracking table of **the application's own
|
||||
migration runner**, `apps/api/src/prisma/migrate.ts:5`. That runner exists for a
|
||||
real reason, stated at `migrate.ts:7-13`: the Docker deploy image is copy-only
|
||||
and ships **no Prisma CLI**, so `migrate deploy` cannot run in the container.
|
||||
The runner executes the same `.sql` files through Prisma Client instead and
|
||||
records them here.
|
||||
|
||||
So the project keeps **two ledgers, deliberately**:
|
||||
|
||||
| Ledger | Written by | Used |
|
||||
|---|---|---|
|
||||
| `_prisma_migrations` | the Prisma CLI | development (`migrate dev` / `migrate deploy`) |
|
||||
| `_goldbank_migrations` | `src/prisma/migrate.ts` | the deployed container, at boot |
|
||||
|
||||
That runner also **already defended this table**: `migrate.ts:190-196` filters
|
||||
out any `DROP TABLE "_goldbank_migrations"` statement, with a comment noting
|
||||
that `migrate diff` had once emitted exactly that. The `@@ignore` model now
|
||||
removes the cause rather than relying on that guard.
|
||||
|
||||
**Consequence for D14:** `migrate deploy` remains the rule for production *when
|
||||
run by hand*, but the container applies migrations itself at boot through
|
||||
`prepareDatabase()`. Any new migration must therefore be safe under **both**
|
||||
paths — which is why idempotent SQL (`IF NOT EXISTS`, `CREATE OR REPLACE`)
|
||||
matters here beyond ordinary caution.
|
||||
|
||||
### Row count went 12 → 14, and nothing was deleted
|
||||
|
||||
Starting the API for the round-trip test ran `prepareDatabase()`, which appended
|
||||
the two new migrations to this ledger:
|
||||
|
||||
```
|
||||
2026-09-22T04:29:14.085Z 20260922120000_customer_name_parts
|
||||
2026-09-22T04:29:14.101Z 20260922120100_customer_making_charge_offset
|
||||
```
|
||||
|
||||
All 12 original rows are present and unmodified. The runner's `$$`-aware
|
||||
statement splitter handled the PL/pgSQL trigger body correctly — the trigger and
|
||||
its function are both present in the database, and the 9 trigger assertions pass
|
||||
against it.
|
||||
|
||||
## Testing rule — Persian input always comes from a file
|
||||
|
||||
**Owner's ruling:** the Windows shell mangles Persian text, so any test with
|
||||
Persian input (names, addresses, messages, validation) must read its data from a
|
||||
**file** — the test file itself or a UTF-8 JSON fixture — never from a
|
||||
command-line argument. Any earlier result that was wrong because of this must be
|
||||
re-run.
|
||||
|
||||
### Why, concretely
|
||||
|
||||
A `curl -d '{"fname":"محمد علی"}'` issued through the shell arrived at the API
|
||||
with every Persian character replaced by `?` (codepoint `0x3f`). The first
|
||||
round-trip attempt therefore "passed" its internal consistency checks while
|
||||
storing `????` — the API and DB agreed with each other, and both were wrong.
|
||||
|
||||
### What was re-run
|
||||
|
||||
The customer round-trip test, with the payload written by Node as UTF-8 and sent
|
||||
with `curl --data-binary @file`. It then passed on real Persian text:
|
||||
create «محمد علی» + «احمدی» → edit form shows each part correctly → no-op save
|
||||
leaves the row byte-identical → single-field edits touch only that field. The
|
||||
three rows from both attempts were deleted and logged.
|
||||
|
||||
### The rule going forward
|
||||
|
||||
- Fixtures live in the test file or in a `.json` written with `utf8` encoding.
|
||||
- HTTP bodies are sent with `--data-binary @file`, not `-d '<inline>'`.
|
||||
- Assertions compare against literals **in the test file**, never against a
|
||||
string that has passed through the shell.
|
||||
- Console output of Persian is not evidence: the Windows console renders it as
|
||||
`?` even when the data is correct. Verify by comparing values or codepoints in
|
||||
Node, as `scripts/test-name-trigger.cjs` does.
|
||||
|
||||
@@ -117,4 +117,4 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
|
||||
|
||||
| ID | Severity | Where | Description | Evidence | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| DEAD-07 | Medium | TGT DB table `_goldbank_migrations` | A bespoke migration ledger left by the earlier (pre-Prisma) port, holding 12 rows (`0001_init.sql`, `0002_admin_schema.sql`, `seed:initial`, …). It is not declared in `schema.prisma`, so `prisma migrate diff` reports it as drift and proposes `DROP TABLE`. **It was NOT dropped** — that is destructive, unapproved, and these rows are the only record of how the pre-Prisma port was applied. Because of it, `migrate dev` on a fresh checkout may offer a reset; the answer is always no. | `prisma migrate diff --from-schema-datasource --to-schema-datamodel` | Open — owner decides: keep and add to the schema as an ignored model, or archive the rows and drop. Until then use `migrate deploy`. |
|
||||
| DEAD-07 | Low | TGT DB table `_goldbank_migrations` + `apps/api/src/prisma/migrate.ts` | **Re-assessed 2026-09-22 — not dead code.** It is the tracking table of the app's OWN migration runner (`src/prisma/migrate.ts:5`), which exists because the Docker deploy image is copy-only and ships no Prisma CLI, so `migrate deploy` cannot run there (`migrate.ts:7-13`). So the project has TWO ledgers on purpose: `_prisma_migrations` (CLI, used in development) and `_goldbank_migrations` (runtime runner, used in the container). `migrate diff` proposed `DROP TABLE` only because the table was not declared in the schema; the runner already refused to execute such a statement (`migrate.ts:190-196`). | Read `src/prisma/migrate.ts`. | ✅ Resolved: declared as an `@@ignore`d model, so drift is now **zero**. Table and all rows kept. |
|
||||
|
||||
+20
-1
@@ -51,7 +51,7 @@ while the id and the id-addressed routes remain.**
|
||||
|
||||
## ⚠️ Read `DECISIONS.md` before doing anything
|
||||
|
||||
All user rulings live there (D0–D14). Highlights that change how work is done:
|
||||
All user rulings live there (D0–D15). Highlights that change how work is done:
|
||||
|
||||
- **D12 — the matrix is FIELD-BY-FIELD, not page-by-page**, and a 4-step
|
||||
round-trip test per entity is mandatory. Both rules were added to the
|
||||
@@ -104,6 +104,7 @@ All user rulings live there (D0–D14). Highlights that change how work is done:
|
||||
| D12 | **Field-by-field matrix + mandatory round-trip test.** Added to the skill. |
|
||||
| D13 | **Retail storefront (wallet, cashback, `ConfigStatic['percent']`) preserved but OUT OF SCOPE.** Nothing deleted or disabled; no parity work; matrices list it ⏸️ with no gap/work row; ask before touching shared surfaces. |
|
||||
| D14 | **`migrate dev` in development, `migrate deploy` in production and nowhere else.** Never `migrate reset`; never edit `_prisma_migrations`. On drift + a reset offer: **stop and report**. |
|
||||
| D15 | **`_goldbank_migrations` is KEPT** — it is the tracking table of the app's own migration runner (`src/prisma/migrate.ts`), needed because the Docker image has no Prisma CLI. Declared `@@ignore`d; drift is zero. **Persian test input must come from a FILE, never a shell argument.** |
|
||||
|
||||
## Open — awaiting user approval
|
||||
|
||||
@@ -196,3 +197,21 @@ different from what you wrote:
|
||||
|
||||
Re-read after writing if you need the stored value. See `DECISIONS.md` §D14 and
|
||||
the doc comment on the `Customer` model in `schema.prisma`.
|
||||
|
||||
## ⚠️ Two migration ledgers, on purpose (D15)
|
||||
|
||||
| Ledger | Written by | Used |
|
||||
|---|---|---|
|
||||
| `_prisma_migrations` | the Prisma CLI | development |
|
||||
| `_goldbank_migrations` | `apps/api/src/prisma/migrate.ts` | the deployed container, at boot |
|
||||
|
||||
The Docker deploy image is copy-only and ships **no Prisma CLI**, so the app
|
||||
applies migrations itself at startup. **Every new migration must be safe under
|
||||
both paths** — keep the SQL idempotent (`IF NOT EXISTS`, `CREATE OR REPLACE`).
|
||||
|
||||
## ⚠️ Persian test input comes from a FILE (D15)
|
||||
|
||||
The Windows shell replaces Persian characters with `?`. A test that passes its
|
||||
data inline can "pass" while storing `????`. Write fixtures in the test file or
|
||||
a UTF-8 JSON file and send bodies with `curl --data-binary @file`. Console
|
||||
output of Persian is never evidence — compare values in Node.
|
||||
|
||||
Reference in New Issue
Block a user