feat(vocab): add logic to aggregate and clean up duplicate SRS items based on answer patterns

This commit is contained in:
Torsten Schulz (local)
2026-08-19 14:14:50 +02:00
parent 2703227a17
commit 4cacc3c7a7

View File

@@ -95,6 +95,90 @@ module.exports = {
transaction
});
await queryInterface.sequelize.query(`
WITH candidates AS (
SELECT id, user_id,
ENCODE(DIGEST(CONCAT_WS(
'|',
course_id::text,
COALESCE(lesson_id::text, 'course'),
UPPER(direction),
:normalizedCombinedAnswer,
:normalizedTarget
), 'sha1'), 'hex') AS new_item_key,
correct_count, wrong_count, lapse_count, stage, interval_days,
last_reviewed_at, next_due_at
FROM community.vocab_srs_item
WHERE learning = :shortAnswer
AND reference = :target
), ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id, new_item_key ORDER BY id) AS position
FROM candidates
), aggregates AS (
SELECT user_id, new_item_key, MIN(id) AS keeper_id,
SUM(correct_count) AS correct_count,
SUM(wrong_count) AS wrong_count,
SUM(lapse_count) AS lapse_count,
MAX(stage) AS stage,
MAX(interval_days) AS interval_days,
MAX(last_reviewed_at) AS last_reviewed_at,
MIN(next_due_at) AS next_due_at
FROM ranked
GROUP BY user_id, new_item_key
)
UPDATE community.vocab_srs_item AS keeper
SET correct_count = aggregates.correct_count,
wrong_count = aggregates.wrong_count,
lapse_count = aggregates.lapse_count,
stage = aggregates.stage,
interval_days = aggregates.interval_days,
last_reviewed_at = aggregates.last_reviewed_at,
next_due_at = aggregates.next_due_at,
updated_at = NOW()
FROM aggregates
WHERE keeper.id = aggregates.keeper_id;
`, {
replacements: {
target: TARGET,
shortAnswer: SHORT_ANSWER,
normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER,
normalizedTarget: NORMALIZED_TARGET
},
transaction
});
await queryInterface.sequelize.query(`
WITH candidates AS (
SELECT id, user_id,
ENCODE(DIGEST(CONCAT_WS(
'|',
course_id::text,
COALESCE(lesson_id::text, 'course'),
UPPER(direction),
:normalizedCombinedAnswer,
:normalizedTarget
), 'sha1'), 'hex') AS new_item_key
FROM community.vocab_srs_item
WHERE learning = :shortAnswer
AND reference = :target
), ranked AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id, new_item_key ORDER BY id) AS position
FROM candidates
)
DELETE FROM community.vocab_srs_item AS duplicate
USING ranked
WHERE duplicate.id = ranked.id
AND ranked.position > 1;
`, {
replacements: {
target: TARGET,
shortAnswer: SHORT_ANSWER,
normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER,
normalizedTarget: NORMALIZED_TARGET
},
transaction
});
await queryInterface.sequelize.query(`
UPDATE community.vocab_srs_item
SET learning = :combinedAnswer,