Skip to content

Commit 88f00d4

Browse files
author
CloudLobster
committed
fix: handle upgrade without PRAGMA — insert-migrate-delete strategy
D1 does not reliably support PRAGMA defer_foreign_keys or PRAGMA foreign_keys = OFF (connection pooling, transaction scope). New approach that needs zero PRAGMA: 1. INSERT new account with newHandle + temp wallet 2. Migrate all child tables (newHandle exists, FK satisfied) 3. DELETE old account (no children reference it) 4. UPDATE new account wallet to real value Fully atomic within the batch. No PRAGMA dependency.
1 parent 8345b14 commit 88f00d4

2 files changed

Lines changed: 75 additions & 87 deletions

File tree

worker/src/routes/register.ts

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -302,56 +302,46 @@ registerRoutes.put('/upgrade', authMiddleware(), async (c) => {
302302
}
303303

304304
// 更新帳號 handle + 遷移所有 FK 子表
305-
// SQLite: PRAGMA defer_foreign_keys must be set BEFORE the transaction starts.
306-
// D1 batch() is an implicit transaction, so PRAGMA goes before it.
307-
await c.env.DB.prepare("PRAGMA defer_foreign_keys = ON").run();
305+
// Strategy: Insert new handle first → migrate children → delete old handle.
306+
// This avoids any PRAGMA hacks (D1 doesn't support FK deferral reliably).
307+
// Step 1: Get old account data for copying
308+
const oldAccount = await c.env.DB.prepare(
309+
'SELECT handle, wallet, basename, webhook_url, created_at, tx_hash FROM accounts WHERE wallet = ?'
310+
).bind(auth.wallet).first<{ handle: string; wallet: string; basename: string | null; webhook_url: string | null; created_at: number; tx_hash: string | null }>();
311+
312+
if (!oldAccount) {
313+
return c.json({ error: 'Account not found during upgrade' }, 500);
314+
}
315+
316+
// Step 2: Insert new account with new handle (use temp wallet to avoid UNIQUE conflict)
317+
const tempWallet = `UPGRADE_${Date.now()}`;
318+
await c.env.DB.prepare(
319+
'INSERT INTO accounts (handle, wallet, basename, webhook_url, created_at, tx_hash) VALUES (?, ?, ?, ?, ?, ?)'
320+
).bind(newHandle, tempWallet, basenames, oldAccount.webhook_url, oldAccount.created_at, oldAccount.tx_hash).run();
321+
322+
// Step 3: Migrate all child tables (newHandle now exists in accounts, so FK is satisfied)
323+
// Then delete old account and fix wallet on new account.
308324
const batchResults = await c.env.DB.batch([
309-
// 1. 更新主表
310-
c.env.DB.prepare(
311-
'UPDATE accounts SET handle = ?, basename = ? WHERE wallet = ?'
312-
).bind(newHandle, basenames, auth.wallet),
313-
// 2. 遷移所有有 FK(handle) REFERENCES accounts(handle) 的子表
314-
c.env.DB.prepare(
315-
'UPDATE emails SET handle = ? WHERE handle = ?'
316-
).bind(newHandle, oldHandle),
317-
c.env.DB.prepare(
318-
'UPDATE refresh_tokens SET handle = ? WHERE handle = ?'
319-
).bind(newHandle, oldHandle),
320-
c.env.DB.prepare(
321-
'UPDATE api_keys SET handle = ? WHERE handle = ?'
322-
).bind(newHandle, oldHandle),
323-
c.env.DB.prepare(
324-
'UPDATE attention_config SET handle = ? WHERE handle = ?'
325-
).bind(newHandle, oldHandle),
326-
c.env.DB.prepare(
327-
'UPDATE attention_bonds SET sender_handle = ? WHERE sender_handle = ?'
328-
).bind(newHandle, oldHandle),
329-
c.env.DB.prepare(
330-
'UPDATE attention_bonds SET recipient_handle = ? WHERE recipient_handle = ?'
331-
).bind(newHandle, oldHandle),
332-
c.env.DB.prepare(
333-
'UPDATE attention_whitelist SET recipient_handle = ? WHERE recipient_handle = ?'
334-
).bind(newHandle, oldHandle),
335-
c.env.DB.prepare(
336-
'UPDATE sender_reputation SET sender_handle = ? WHERE sender_handle = ?'
337-
).bind(newHandle, oldHandle),
338-
c.env.DB.prepare(
339-
'UPDATE sender_reputation SET recipient_handle = ? WHERE recipient_handle = ?'
340-
).bind(newHandle, oldHandle),
341-
c.env.DB.prepare(
342-
'UPDATE qaf_scores SET handle = ? WHERE handle = ?'
343-
).bind(newHandle, oldHandle),
344-
c.env.DB.prepare(
345-
'UPDATE world_id_verifications SET handle = ? WHERE handle = ?'
346-
).bind(newHandle, oldHandle),
347-
c.env.DB.prepare(
348-
'UPDATE escrow_claims SET sender_handle = ? WHERE sender_handle = ?'
349-
).bind(newHandle, oldHandle),
350-
c.env.DB.prepare(
351-
'UPDATE escrow_claims SET claimer_handle = ? WHERE claimer_handle = ?'
352-
).bind(newHandle, oldHandle),
325+
// Migrate child tables
326+
c.env.DB.prepare('UPDATE emails SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
327+
c.env.DB.prepare('UPDATE refresh_tokens SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
328+
c.env.DB.prepare('UPDATE api_keys SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
329+
c.env.DB.prepare('UPDATE attention_config SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
330+
c.env.DB.prepare('UPDATE attention_bonds SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
331+
c.env.DB.prepare('UPDATE attention_bonds SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
332+
c.env.DB.prepare('UPDATE attention_whitelist SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
333+
c.env.DB.prepare('UPDATE sender_reputation SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
334+
c.env.DB.prepare('UPDATE sender_reputation SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
335+
c.env.DB.prepare('UPDATE qaf_scores SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
336+
c.env.DB.prepare('UPDATE world_id_verifications SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
337+
c.env.DB.prepare('UPDATE escrow_claims SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
338+
c.env.DB.prepare('UPDATE escrow_claims SET claimer_handle = ? WHERE claimer_handle = ?').bind(newHandle, oldHandle),
339+
// Delete old account (no children reference it anymore)
340+
c.env.DB.prepare('DELETE FROM accounts WHERE handle = ?').bind(oldHandle),
341+
// Fix wallet on new account (now unique since old row is deleted)
342+
c.env.DB.prepare('UPDATE accounts SET wallet = ? WHERE handle = ?').bind(auth.wallet, newHandle),
353343
]);
354-
const migratedCount = batchResults[1]?.meta?.changes || 0;
344+
const migratedCount = batchResults[0]?.meta?.changes || 0;
355345

356346
// Insert into basename_aliases with is_primary=1
357347
if (basenames) {

worker/src/routes/settings.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -185,46 +185,44 @@ settingsRoutes.put('/primary', async (c) => {
185185
return c.json({ error: 'Handle already taken by another wallet' }, 409);
186186
}
187187

188-
// Batch update: switch primary(更新所有 FK 子表)
189-
// SQLite: PRAGMA defer_foreign_keys must be set BEFORE the transaction starts.
190-
await c.env.DB.prepare("PRAGMA defer_foreign_keys = ON").run();
188+
// Switch primary: Insert new handle → migrate children → delete old handle.
189+
// No PRAGMA needed — avoids D1's unreliable FK deferral.
190+
const oldAccount = await c.env.DB.prepare(
191+
'SELECT handle, wallet, basename, webhook_url, created_at, tx_hash FROM accounts WHERE wallet = ?'
192+
).bind(auth.wallet).first<{ handle: string; wallet: string; basename: string | null; webhook_url: string | null; created_at: number; tx_hash: string | null }>();
193+
194+
if (!oldAccount) {
195+
return c.json({ error: 'Account not found' }, 500);
196+
}
197+
198+
// Insert new handle with temp wallet to avoid UNIQUE conflict
199+
const tempWallet = `SWITCH_${Date.now()}`;
200+
await c.env.DB.prepare(
201+
'INSERT INTO accounts (handle, wallet, basename, webhook_url, created_at, tx_hash) VALUES (?, ?, ?, ?, ?, ?)'
202+
).bind(newHandle, tempWallet, alias.basename, oldAccount.webhook_url, oldAccount.created_at, oldAccount.tx_hash).run();
203+
204+
// Migrate children, delete old, fix wallet — all in one atomic batch
191205
await c.env.DB.batch([
192-
// Update account handle
193-
c.env.DB.prepare('UPDATE accounts SET handle = ?, basename = ? WHERE wallet = ?')
194-
.bind(newHandle, alias.basename, auth.wallet),
195-
// Migrate all child tables with FK on accounts(handle)
196-
c.env.DB.prepare('UPDATE emails SET handle = ? WHERE handle = ?')
197-
.bind(newHandle, oldHandle),
198-
c.env.DB.prepare('UPDATE refresh_tokens SET handle = ? WHERE handle = ?')
199-
.bind(newHandle, oldHandle),
200-
c.env.DB.prepare('UPDATE api_keys SET handle = ? WHERE handle = ?')
201-
.bind(newHandle, oldHandle),
202-
c.env.DB.prepare('UPDATE attention_config SET handle = ? WHERE handle = ?')
203-
.bind(newHandle, oldHandle),
204-
c.env.DB.prepare('UPDATE attention_bonds SET sender_handle = ? WHERE sender_handle = ?')
205-
.bind(newHandle, oldHandle),
206-
c.env.DB.prepare('UPDATE attention_bonds SET recipient_handle = ? WHERE recipient_handle = ?')
207-
.bind(newHandle, oldHandle),
208-
c.env.DB.prepare('UPDATE attention_whitelist SET recipient_handle = ? WHERE recipient_handle = ?')
209-
.bind(newHandle, oldHandle),
210-
c.env.DB.prepare('UPDATE sender_reputation SET sender_handle = ? WHERE sender_handle = ?')
211-
.bind(newHandle, oldHandle),
212-
c.env.DB.prepare('UPDATE sender_reputation SET recipient_handle = ? WHERE recipient_handle = ?')
213-
.bind(newHandle, oldHandle),
214-
c.env.DB.prepare('UPDATE qaf_scores SET handle = ? WHERE handle = ?')
215-
.bind(newHandle, oldHandle),
216-
c.env.DB.prepare('UPDATE world_id_verifications SET handle = ? WHERE handle = ?')
217-
.bind(newHandle, oldHandle),
218-
c.env.DB.prepare('UPDATE escrow_claims SET sender_handle = ? WHERE sender_handle = ?')
219-
.bind(newHandle, oldHandle),
220-
c.env.DB.prepare('UPDATE escrow_claims SET claimer_handle = ? WHERE claimer_handle = ?')
221-
.bind(newHandle, oldHandle),
222-
// Reset all is_primary flags for this wallet
223-
c.env.DB.prepare('UPDATE basename_aliases SET is_primary = 0 WHERE wallet = ?')
224-
.bind(auth.wallet),
225-
// Set new primary
226-
c.env.DB.prepare('UPDATE basename_aliases SET is_primary = 1 WHERE handle = ? AND wallet = ?')
227-
.bind(newHandle, auth.wallet),
206+
c.env.DB.prepare('UPDATE emails SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
207+
c.env.DB.prepare('UPDATE refresh_tokens SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
208+
c.env.DB.prepare('UPDATE api_keys SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
209+
c.env.DB.prepare('UPDATE attention_config SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
210+
c.env.DB.prepare('UPDATE attention_bonds SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
211+
c.env.DB.prepare('UPDATE attention_bonds SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
212+
c.env.DB.prepare('UPDATE attention_whitelist SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
213+
c.env.DB.prepare('UPDATE sender_reputation SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
214+
c.env.DB.prepare('UPDATE sender_reputation SET recipient_handle = ? WHERE recipient_handle = ?').bind(newHandle, oldHandle),
215+
c.env.DB.prepare('UPDATE qaf_scores SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
216+
c.env.DB.prepare('UPDATE world_id_verifications SET handle = ? WHERE handle = ?').bind(newHandle, oldHandle),
217+
c.env.DB.prepare('UPDATE escrow_claims SET sender_handle = ? WHERE sender_handle = ?').bind(newHandle, oldHandle),
218+
c.env.DB.prepare('UPDATE escrow_claims SET claimer_handle = ? WHERE claimer_handle = ?').bind(newHandle, oldHandle),
219+
// Delete old account (children already migrated)
220+
c.env.DB.prepare('DELETE FROM accounts WHERE handle = ?').bind(oldHandle),
221+
// Fix wallet on new account
222+
c.env.DB.prepare('UPDATE accounts SET wallet = ? WHERE handle = ?').bind(auth.wallet, newHandle),
223+
// Update basename_aliases
224+
c.env.DB.prepare('UPDATE basename_aliases SET is_primary = 0 WHERE wallet = ?').bind(auth.wallet),
225+
c.env.DB.prepare('UPDATE basename_aliases SET is_primary = 1 WHERE handle = ? AND wallet = ?').bind(newHandle, auth.wallet),
228226
]);
229227

230228
// Issue new token

0 commit comments

Comments
 (0)