Files
yourpart3/backend/migrations-active/20260818000000-add-bisaya-rest-answer-alternative.cjs

227 lines
8.5 KiB
JavaScript

'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.
},
};