Skip to content

Commit 129b378

Browse files
fix(types,rest): uniqueViolationColumn — 一个索引名永远不会被当成列名回答 (#6544) (#6717)
* wip(types,rest): uniqueViolationColumn export + import-runner migration * test(types,rest): one dialect table driven through both faces; changeset --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4856789 commit 129b378

5 files changed

Lines changed: 693 additions & 34 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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

Comments
 (0)