Skip to content

import-runner's sanitizeRowError keeps its own three-dialect unique regexes — deferred from #6250 to avoid a merge race with #4633 #6544

Closed

Description

@os-project-manager

Blocked-by: #6250 (the shared predicate must land first — PR #6541)

Filed by the #6250 developer agent. This consumer was in #6250's declared scope and was removed mid-task by a PM scope narrowing, so it is recorded here rather than silently dropped.

Why it was deferred (not forgotten)

#4633 was dispatched in parallel and its consuming half lands squarely in the import path, packages/rest/src/import-runner.ts included. Two agents editing that file is a merge race that costs more than serialising. The migration was always the incidental half of #6250, never the user-visible fix — that half (MySQL conflicts returning 409 UNIQUE_VIOLATION) shipped in PR #6541.

What is left

sanitizeRowError in import-runner.ts:

const unique =
  /unique constraint failed:\s*([^\s,)]+)/i.exec(msg) ??          // sqlite
  /duplicate entry .* for key '([^']+)'/i.exec(msg) ??            // mysql
  /duplicate key value violates unique constraint.*?[Kk]ey \(([^)]+)\)/is.exec(msg); // postgres

Correction to #6250's inventory, for whoever picks this up: that issue's table records this row as "一条 Postgres 专用正则 ... 仅 Postgres". That has drifted. All three dialect limbs have been present since #3572 (git log -S "duplicate entry .* for key" confirms) — the line number the issue cites is the third limb of a three-way chain, not the whole of it. The row is still a private vocabulary; it is just not the narrowest one.

Why this is not a mechanical swap

This site does more than answer yes/no: it extracts the offending column so the importer can say "A record with this email already exists." isUniqueViolationError deliberately answers the boolean only — #6250's ruling put the structured conflict-column export out of scope as a new contract surface.

So this migration needs one of:

  • (a) keep the column-extracting regexes and use the predicate only as the gate — buys little, and leaves the vocabulary forked; or
  • (b) land a uniqueViolationColumn(error) sibling in @objectstack/types first, which is the actual convergence. PR fix(types,rest): one named unique-violation predicate — MySQL conflicts return 409 UNIQUE_VIOLATION, not 500 (#6250) #6541's structure already separates the channels per dialect, so the shape slots in without restructuring — but it is a contract decision, not a refactor: when a dialect names an index (for key 'idx_email_unique') rather than a column (Postgres' Key (email)=(...)), which does the export promise? A caller that renders "already exists" into a form field needs the column; an index name mis-rendered there is worse than no answer.

Recommend (b), and note that #5495's autonumber-retry branch wants the same export for a different reason (retry only when the conflicting column IS the autonumber field), so the two callers should agree on the answer before either is built. That decision is the maintainer's, per #6250's ruling — this card should not settle it unilaterally.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  2. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Findings round: held — finding stays, with the blocker note updated.

    Stale-premise check: the body's Blocked-by: #6250 is now satisfied — #6250 closed as completed at 04:55Z via merged PR #6541, which ships isUniqueViolationError in @objectstack/types and explicitly names this migration "a follow-up per lane, now unblocked". The remaining private vocabulary is confirmed still on origin/main @ a36db28 (import-runner.ts's sanitizeRowError three-limb chain).

    Why held rather than queued or inboxed: the card's own analysis is that the honest route is (b) — a uniqueViolationColumn(error) sibling — and that this is a contract decision #6250's ruling explicitly reserved to the maintainer (column vs index-name promise), with #5495's autonumber-retry branch needing the same answer. Route (a) is mechanical but "buys little" by the card's own words. No user-visible defect remains on this path (the 409 fix shipped), so parking a low-urgency contract question in an inbox that already holds five pending rulings would be queue-jumping.

    Restart condition: #5495's autonumber-retry branch is picked up, or any consumer concretely needs the conflicting-column export — at that moment this promotes to needs-user-decision with the column-vs-index question as the single ask, and the two callers get one answer.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    维护者裁定(2026-08-08,cli 席存档):取路线 (b) —— 先在 @objectstack/types 落 uniqueViolationColumn 导出,再迁 import-runner。Blocked-by: #6250 已解除(PR #6541 已合并 88f9d94bb)。卡转可派。

    语义裁定(本卡此前最大的未决点):当方言报的是索引名(MySQL 的 for key 'idx_email_unique')而非列名(Postgres 的 Key (email)=(...))时,uniqueViolationColumn 返回 undefined —— 只在能确定是列名时才给值。

    理由:调用方要把它渲染到表单字段上,一个被当成列名的索引名比没有答案更糟 —— 前者会把用户指向一个不存在的字段,后者只是退回通用文案。#5495 的 autonumber 重试分支同理:它要判断「冲突列是否就是 autonumber 字段」,一个索引名在那里会给出错误的重试决策。代价是 MySQL 部署上常常拿不到列名,这是明知并接受的。

    ⚠️ 对本卡结论的一处更正,实施者请注意:本卡正文称 #6250 的清单把这一行记为「仅 Postgres」已经漂移 —— 三条方言肢自 #3572 起就都在。该更正成立,已由 #6250 的 dev 在 PR #6541 正文独立复核过。请按三方言的真实现状迁移,不要照抄「仅 Postgres」的旧描述。


    Generated by Claude Code

  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    CLAIM — session session_017uFVNMmTxLpmfQYiuKM1Yx, branch claude/issue-6544-unique-violation-column.

    按维护者 2026-08-08 裁定(comment 5226102517)实施路线 (b):先在 @objectstack/types 落 uniqueViolationColumn 导出(索引名一律返回 undefined),再把 packages/rest/src/import-runner.ts 的 sanitizeRowError 迁移到该导出上。不触碰 rest-server.ts / rest-api-plugin.ts(#6633 在改),也不迁 service-messaging / driver-sql 的副本(按 lane 另行跟进)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions