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}`;
}
/** 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;