traPtitech / traPtitech/Checkin
isDuplicateKeyError が 3 実装で並存し、そのうち 2 つは重複キーを検出できない
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 0
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 7
Description
症状
重複キー(ER_DUP_ENTRY / errno 1062)を判定する isDuplicateKeyError が 3 つある。
packages/api/src/mysql-result.tsの共有版。投げられた値からcauseの連鎖を最大 5 段まで辿る。値がnullでなく、型がobjectかfunctionであれば中を読む。packages/api/src/auth/identity.tsとpackages/api/src/ledger/store.tsがこれを使う。packages/api/src/stripe/connect.tsの export された版。投げられた値の最上位しか読まない。型の判定は共有版と同じで、functionも中を読む。packages/api/src/webhook/events.tsのモジュール内の版。同じく最上位しか読まないが、型の判定がtypeof err !== 'object'なので、functionを弾く。
drizzle は mysql2 の例外を包み、code と errno を持つ側を .cause に入れる。mysql-result.ts の isDuplicateKeyError に付いているコメントが、drizzle-orm 0.45.2 と mysql2 3.23.2 を MariaDB 11 に対して測った結果として、最上位の DrizzleQueryError は code も errno も持たず、1 段下の .cause が code: 'ER_DUP_ENTRY' と errno: 1062 を持つ、と記録している。
3 つの答えは 3 通りに分かれる。2026-09-19 に main の 74ee63c で、3 つに同じ値を渡して測った結果は次のとおりである。mysql-result.ts と connect.ts は export されているので import して呼び、events.ts の版はモジュール内で private なので、ソースの当該関数の記述をそのまま取り出して実行した。
| 渡した値 | mysql-result.ts |
connect.ts |
webhook/events.ts |
|---|---|---|---|
cause に code: 'ER_DUP_ENTRY' と errno: 1062 を持つ Error を包んだ Error |
true |
false |
false |
code: 'ER_DUP_ENTRY' と errno: 1062 を持たせた関数 |
true |
true |
false |
最上位に code: 'ER_DUP_ENTRY' と errno: 1062 を持つ Error |
true |
true |
true |
drizzle が投げるのは cause に包まれた形なので、実際に効いてくるのはその違いである。関数を渡した場合は 3 つが 3 通りであることを示すもので、現在の呼び出し側にこの形が届く経路は確かめていない。
影響
どちらの呼び出し側も、この分岐に入るのは競合したときだけである。
packages/api/src/webhook/events.tsのrecordStripeEventOnceは、呼び出し側が先にhasProcessedStripeEventで照会してから記録するので、通常の再配信はこの分岐に届かない。同時に届いた重複配信が競ったときだけ、falseを返す代わりに例外が外へ出て 500 になる。Stripe は再送し、再送時は照会で弾かれる。packages/api/src/stripe/connect.tsのgetOrCreateConnectedAccountは、claimConnectedAccountIdが返すfalseで通常の敗北を扱う。IS NULLのガード自体が競合に負けて一意制約まで届いた場合だけ、won = falseへ落ちる代わりに例外が外へ出て、作成済みの Connect アカウントが孤児として残る。
どちらの箇所にも、この隙間を説明するコメントが付いている。
誰が何を誤るか
新しく重複キーを扱う人が、3 つのうちどれを import すればよいか分からない。浅い 2 つを新しい箇所で使うと、drizzle 経由で投げられた重複キーを取り逃す。
3 つを「共有版と、それ以外の同じもの 2 つ」と読むことも誤りである。上の表のとおり、浅い 2 つも型の判定が揃っていない。片方に合わせて直すと、もう片方は直らない。
直さずに残した理由
浅い 2 つを共有版に差し替えると、今まで 500 になっていた経路が設計どおりの分岐に入るようになる。これは振る舞いの変更なので、変更の影響を確かめる単位を分けた。
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the three implementations in packages/api/src/mysql-result.ts, packages/api/src/stripe/connect.ts, and packages/api/src/webhook/events.ts, then read recordStripeEventOnce and getOrCreateConnectedAccount. Trace the wrapped drizzle error through each caller and verify both affected paths handle duplicate-key races without returning 500 or leaving an orphaned Connect account.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend, database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100