From 654b98520d76f7fa25e8e5f3410daa781c43e184 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Tue, 22 Sep 2026 08:25:03 +0330 Subject: [PATCH] parity(api): declare _goldbank_migrations as an ignored model; drift now zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/prisma/schema.prisma | 28 +++++++++- docs/parity/DECISIONS.md | 101 ++++++++++++++++++++++++++++++++++ docs/parity/ISSUES.md | 2 +- docs/parity/PROGRESS.md | 21 ++++++- 4 files changed, 147 insertions(+), 5 deletions(-) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index dcf979a..1138bc6 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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 +} diff --git a/docs/parity/DECISIONS.md b/docs/parity/DECISIONS.md index 968d664..03de954 100644 --- a/docs/parity/DECISIONS.md +++ b/docs/parity/DECISIONS.md @@ -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 ''`. +- 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. diff --git a/docs/parity/ISSUES.md b/docs/parity/ISSUES.md index b86935d..ab4cf9e 100644 --- a/docs/parity/ISSUES.md +++ b/docs/parity/ISSUES.md @@ -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. | diff --git a/docs/parity/PROGRESS.md b/docs/parity/PROGRESS.md index 9d656be..6c36c9a 100644 --- a/docs/parity/PROGRESS.md +++ b/docs/parity/PROGRESS.md @@ -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.