diff --git a/task-unified-service.mjs b/task-unified-service.mjs index ba85fca..e6d7779 100644 --- a/task-unified-service.mjs +++ b/task-unified-service.mjs @@ -37,6 +37,17 @@ function legacyKey(ref) { return `${ref.table}:${ref.id}`; } +/** MySQL JSON column key order is not stable; match by extracted fields. */ +function legacyRefMatchSql(column = 'legacy_ref_json') { + return `${column} IS NOT NULL + AND JSON_UNQUOTE(JSON_EXTRACT(${column}, '$.table')) = ? + AND JSON_UNQUOTE(JSON_EXTRACT(${column}, '$.id')) = ?`; +} + +function legacyRefMatchParams(legacyRef) { + return [legacyRef.table, legacyRef.id]; +} + function formatTriggerLabel(task, timezone = DEFAULT_TIMEZONE) { const trigger = task.trigger ?? {}; if (trigger.at) return trigger.at; @@ -352,9 +363,9 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now() if (mapped.legacyRef?.table && mapped.legacyRef?.id) { const [existing] = await pool.query( `SELECT * FROM h5_tasks - WHERE user_id = ? AND legacy_ref_json = ? + WHERE user_id = ? AND ${legacyRefMatchSql()} LIMIT 1`, - [mapped.userId, JSON.stringify(mapped.legacyRef)], + [mapped.userId, ...legacyRefMatchParams(mapped.legacyRef)], ); if (existing?.[0]) return rowToUnifiedTask(existing[0]); } @@ -373,8 +384,8 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now() const [result] = await pool.query( `UPDATE h5_tasks SET status = 'cancelled', updated_at = ? - WHERE user_id = ? AND legacy_ref_json = ?`, - [clock.now(), userId, JSON.stringify(legacyRef)], + WHERE user_id = ? AND ${legacyRefMatchSql()}`, + [clock.now(), userId, ...legacyRefMatchParams(legacyRef)], ); return Number(result?.affectedRows ?? 0); }; @@ -400,17 +411,17 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now() } if (lastError !== undefined) { const [rows] = await pool.query( - `SELECT spec_json FROM h5_tasks WHERE user_id = ? AND legacy_ref_json = ? LIMIT 1`, - [userId, JSON.stringify(legacyRef)], + `SELECT spec_json FROM h5_tasks WHERE user_id = ? AND ${legacyRefMatchSql()} LIMIT 1`, + [userId, ...legacyRefMatchParams(legacyRef)], ); const spec = parseJson(rows?.[0]?.spec_json) ?? {}; spec.lastError = lastError ?? null; sets.push('spec_json = ?'); params.push(JSON.stringify(spec)); } - params.push(userId, JSON.stringify(legacyRef)); + params.push(userId, ...legacyRefMatchParams(legacyRef)); const [result] = await pool.query( - `UPDATE h5_tasks SET ${sets.join(', ')} WHERE user_id = ? AND legacy_ref_json = ?`, + `UPDATE h5_tasks SET ${sets.join(', ')} WHERE user_id = ? AND ${legacyRefMatchSql()}`, params, ); return Number(result?.affectedRows ?? 0); @@ -568,8 +579,8 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now() for (const candidate of candidates) { try { const before = await pool.query( - `SELECT id FROM h5_tasks WHERE user_id = ? AND legacy_ref_json = ? LIMIT 1`, - [candidate.userId, JSON.stringify(candidate.legacyRef)], + `SELECT id FROM h5_tasks WHERE user_id = ? AND ${legacyRefMatchSql()} LIMIT 1`, + [candidate.userId, ...legacyRefMatchParams(candidate.legacyRef)], ); if (before?.[0]?.[0]) { stats.skipped += 1;