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
+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.