'use strict'; const SHORT_ANSWER = 'Ich ruhe mich später kurz aus.'; const ALTERNATIVE_ANSWER = 'Ich ruhe mich später aus.'; const COMBINED_ANSWER = `${SHORT_ANSWER} / ${ALTERNATIVE_ANSWER}`; const TARGET = 'Magpahuway ko gamay unya.'; const NORMALIZED_COMBINED_ANSWER = 'ich ruhe mich später kurz aus ich ruhe mich später aus'; const NORMALIZED_TARGET = 'magpahuway ko gamay unya'; module.exports = { async up(queryInterface) { await queryInterface.sequelize.transaction(async (transaction) => { await queryInterface.sequelize.query(` UPDATE community.vocab_course_lesson SET core_patterns = ( SELECT JSONB_AGG( CASE WHEN pattern.value->>'target' = :target AND pattern.value->>'gloss' = :shortAnswer THEN pattern.value || JSONB_BUILD_OBJECT('alternatives', JSONB_BUILD_ARRAY(:alternativeAnswer)) ELSE pattern.value END ) FROM JSONB_ARRAY_ELEMENTS(core_patterns) AS pattern(value) ) WHERE JSONB_TYPEOF(core_patterns) = 'array' AND core_patterns @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('target', :target, 'gloss', :shortAnswer)); `, { replacements: { target: TARGET, shortAnswer: SHORT_ANSWER, alternativeAnswer: ALTERNATIVE_ANSWER }, transaction }); await queryInterface.sequelize.query(` WITH stale AS ( SELECT id, user_id, course_id, lesson_id, direction, 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 ) UPDATE community.vocab_srs_item AS canonical SET correct_count = canonical.correct_count + stale.correct_count, wrong_count = canonical.wrong_count + stale.wrong_count, lapse_count = canonical.lapse_count + stale.lapse_count, stage = GREATEST(canonical.stage, stale.stage), interval_days = GREATEST(canonical.interval_days, stale.interval_days), last_reviewed_at = GREATEST(canonical.last_reviewed_at, stale.last_reviewed_at), next_due_at = LEAST(canonical.next_due_at, stale.next_due_at), updated_at = NOW() FROM stale WHERE canonical.user_id = stale.user_id AND canonical.id <> stale.id AND canonical.item_key = ENCODE(DIGEST(CONCAT_WS( '|', stale.course_id::text, COALESCE(stale.lesson_id::text, 'course'), UPPER(stale.direction), :normalizedCombinedAnswer, :normalizedTarget ), 'sha1'), 'hex'); `, { replacements: { target: TARGET, shortAnswer: SHORT_ANSWER, normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER, normalizedTarget: NORMALIZED_TARGET }, transaction }); await queryInterface.sequelize.query(` DELETE FROM community.vocab_srs_item AS stale WHERE stale.learning = :shortAnswer AND stale.reference = :target AND EXISTS ( SELECT 1 FROM community.vocab_srs_item canonical WHERE canonical.user_id = stale.user_id AND canonical.id <> stale.id AND canonical.item_key = ENCODE(DIGEST(CONCAT_WS( '|', stale.course_id::text, COALESCE(stale.lesson_id::text, 'course'), UPPER(stale.direction), :normalizedCombinedAnswer, :normalizedTarget ), 'sha1'), 'hex') ); `, { 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, 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, item_key = ENCODE(DIGEST(CONCAT_WS( '|', course_id::text, COALESCE(lesson_id::text, 'course'), UPPER(direction), :normalizedCombinedAnswer, :normalizedTarget ), 'sha1'), 'hex'), updated_at = NOW() WHERE learning = :shortAnswer AND reference = :target AND NOT EXISTS ( SELECT 1 FROM community.vocab_srs_item duplicate WHERE duplicate.user_id = vocab_srs_item.user_id AND duplicate.item_key = ENCODE(DIGEST(CONCAT_WS( '|', vocab_srs_item.course_id::text, COALESCE(vocab_srs_item.lesson_id::text, 'course'), UPPER(vocab_srs_item.direction), :normalizedCombinedAnswer, :normalizedTarget ), 'sha1'), 'hex') AND duplicate.id <> vocab_srs_item.id ); `, { replacements: { target: TARGET, shortAnswer: SHORT_ANSWER, combinedAnswer: COMBINED_ANSWER, normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER, normalizedTarget: NORMALIZED_TARGET }, transaction }); }); }, async down() { // The alternative is valid course content and should not be removed. }, };