fix(tasks): match legacy_ref_json by extracted fields for idempotent migrate
Memind CI / Test, build, and release guards (push) Successful in 3m32s

MySQL JSON key order differs from JS stringify, which caused duplicate h5_tasks rows on re-run migrate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-24 16:07:46 +08:00
parent d58dc2a251
commit a512fecdc5
+21 -10
View File
@@ -37,6 +37,17 @@ function legacyKey(ref) {
return `${ref.table}:${ref.id}`; 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) { function formatTriggerLabel(task, timezone = DEFAULT_TIMEZONE) {
const trigger = task.trigger ?? {}; const trigger = task.trigger ?? {};
if (trigger.at) return trigger.at; 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) { if (mapped.legacyRef?.table && mapped.legacyRef?.id) {
const [existing] = await pool.query( const [existing] = await pool.query(
`SELECT * FROM h5_tasks `SELECT * FROM h5_tasks
WHERE user_id = ? AND legacy_ref_json = ? WHERE user_id = ? AND ${legacyRefMatchSql()}
LIMIT 1`, LIMIT 1`,
[mapped.userId, JSON.stringify(mapped.legacyRef)], [mapped.userId, ...legacyRefMatchParams(mapped.legacyRef)],
); );
if (existing?.[0]) return rowToUnifiedTask(existing[0]); if (existing?.[0]) return rowToUnifiedTask(existing[0]);
} }
@@ -373,8 +384,8 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now()
const [result] = await pool.query( const [result] = await pool.query(
`UPDATE h5_tasks `UPDATE h5_tasks
SET status = 'cancelled', updated_at = ? SET status = 'cancelled', updated_at = ?
WHERE user_id = ? AND legacy_ref_json = ?`, WHERE user_id = ? AND ${legacyRefMatchSql()}`,
[clock.now(), userId, JSON.stringify(legacyRef)], [clock.now(), userId, ...legacyRefMatchParams(legacyRef)],
); );
return Number(result?.affectedRows ?? 0); return Number(result?.affectedRows ?? 0);
}; };
@@ -400,17 +411,17 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now()
} }
if (lastError !== undefined) { if (lastError !== undefined) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT spec_json FROM h5_tasks WHERE user_id = ? AND legacy_ref_json = ? LIMIT 1`, `SELECT spec_json FROM h5_tasks WHERE user_id = ? AND ${legacyRefMatchSql()} LIMIT 1`,
[userId, JSON.stringify(legacyRef)], [userId, ...legacyRefMatchParams(legacyRef)],
); );
const spec = parseJson(rows?.[0]?.spec_json) ?? {}; const spec = parseJson(rows?.[0]?.spec_json) ?? {};
spec.lastError = lastError ?? null; spec.lastError = lastError ?? null;
sets.push('spec_json = ?'); sets.push('spec_json = ?');
params.push(JSON.stringify(spec)); params.push(JSON.stringify(spec));
} }
params.push(userId, JSON.stringify(legacyRef)); params.push(userId, ...legacyRefMatchParams(legacyRef));
const [result] = await pool.query( 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, params,
); );
return Number(result?.affectedRows ?? 0); return Number(result?.affectedRows ?? 0);
@@ -568,8 +579,8 @@ export function createTaskUnifiedService(pool, { clock = { now: () => Date.now()
for (const candidate of candidates) { for (const candidate of candidates) {
try { try {
const before = await pool.query( const before = await pool.query(
`SELECT id FROM h5_tasks WHERE user_id = ? AND legacy_ref_json = ? LIMIT 1`, `SELECT id FROM h5_tasks WHERE user_id = ? AND ${legacyRefMatchSql()} LIMIT 1`,
[candidate.userId, JSON.stringify(candidate.legacyRef)], [candidate.userId, ...legacyRefMatchParams(candidate.legacyRef)],
); );
if (before?.[0]?.[0]) { if (before?.[0]?.[0]) {
stats.skipped += 1; stats.skipped += 1;