|
| 1 | +--- |
| 2 | +"@objectstack/types": minor |
| 3 | +"@objectstack/rest": patch |
| 4 | +--- |
| 5 | + |
| 6 | +fix(types,rest): one named answer for "which column conflicted" — an index name is never returned as one (#6544) |
| 7 | + |
| 8 | +#6250 retired four private "is this a unique violation?" vocabularies into |
| 9 | +`isUniqueViolationError`. It left the harder half of the question behind: the |
| 10 | +import runner's `sanitizeRowError` still carried its own three-dialect regex |
| 11 | +chain, because it does **more** than answer yes/no — it names the offending |
| 12 | +column so the importer can say *"A record with this `email` already exists."* |
| 13 | +This lands that second answer as a shared export and migrates the last private |
| 14 | +copy onto it. |
| 15 | + |
| 16 | +**New — `uniqueViolationColumn(error)` in `@objectstack/types`** (`string | |
| 17 | +undefined`), sibling to `isUniqueViolationError` and gated on it, reading the |
| 18 | +same channels one step down the same bounded `cause` chain, plus |
| 19 | +node-postgres' `detail` field. |
| 20 | + |
| 21 | +**Its contract, per the maintainer's 2026-08-08 ruling: a value comes back only |
| 22 | +when the identifier the driver printed is determinably a COLUMN.** When a |
| 23 | +dialect names an *index* instead — MySQL's `Duplicate entry … for key |
| 24 | +'idx_email_unique'`, Postgres' `violates unique constraint "sys_user_email_key"`, |
| 25 | +SQLite's `UNIQUE constraint failed: index 'x'` — the answer is `undefined`, |
| 26 | +never the index name. Callers render this into a form field, and an index name |
| 27 | +mistaken for a column points the user at a field that does not exist, whereas |
| 28 | +`undefined` degrades to generic copy. A **composite** key (`Key (tenant_id, |
| 29 | +email)=(…)`) is `undefined` for the same reason: there is no single offending |
| 30 | +column, and naming the first is the same class of wrong answer. |
| 31 | + |
| 32 | +**⚠️ User-visible change on MySQL imports.** MySQL's duplicate-entry message |
| 33 | +names the index and never the column, so the importer no longer names a column |
| 34 | +there: rows that used to read *"A record with this `idx_email_unique` already |
| 35 | +exists."* — or, on MySQL 8's table-qualified `for key 'sys_user.email'`, a |
| 36 | +plausible-looking *`email`* that was still an index name — now read **"A record |
| 37 | +with this value already exists."** That is deliberate and is the accepted cost |
| 38 | +of the ruling. The conflict is still recognised as a conflict; only the naming |
| 39 | +narrowed. |
| 40 | + |
| 41 | +Three smaller import messages improve in the same move, all previously wrong |
| 42 | +rather than merely vague: |
| 43 | + |
| 44 | +- SQLite's expression/partial-index form used to render as *"A record with this |
| 45 | + **index** already exists."* |
| 46 | +- Postgres' expression index used to render the truncated fragment *"A record |
| 47 | + with this **lower(email** already exists."* |
| 48 | +- A Postgres conflict with no `DETAIL:` line used to fall through to the SQL |
| 49 | + backstop and echo the driver's own sentence — index name included — at the |
| 50 | + importer. It now gets the same generic conflict copy, which is also the exact |
| 51 | + wording `mapDataError` puts in the 409 `UNIQUE_VIOLATION` body, so the |
| 52 | + importer and the API say one thing about one condition. |
| 53 | + |
| 54 | +Not changed: the NOT NULL branch, the raw-SQL backstop, and every non-conflict |
| 55 | +message, which pass through exactly as before. |
0 commit comments