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:
s.esmaeili
2026-09-22 08:25:03 +03:30
co-authored by Claude Opus 5
parent 8133a2bdf4
commit 654b98520d
4 changed files with 147 additions and 5 deletions
+25 -3
View File
@@ -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
}
+101
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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.