Skip unchanged rows on import: diff cards, images, and set-rarities against DB state
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-15 21:59:26 +02:00
parent e82a50458b
commit 5e1c2558e7
+54 -31
View File
@@ -4,29 +4,31 @@ const db = require('../config/db');
const BATCH_SIZE = 500; const BATCH_SIZE = 500;
function cardFingerprint(c) {
return [c.name, c.type, c.frameType, c.level || null, c.race || null,
c.attribute || null, c.linkval || null, c.tcg_date || null, c.ocg_date || null].join('|');
}
async function importSetsInternal() { async function importSetsInternal() {
const sets = await fetchAllSets(); const sets = await fetchAllSets();
if (!sets.length) return { added: 0, total: 0 }; if (!sets.length) return { added: 0, total: sets.length };
const values = sets.map(() => '(?, ?, ?, ?)').join(', '); const [existing] = await db.execute('SELECT set_code FROM sets');
const params = sets.flatMap(s => [s.set_name, s.set_code, s.num_of_cards, s.tcg_date]); const existingCodes = new Set(existing.map(r => r.set_code));
await db.execute(` const newSets = sets.filter(s => !existingCodes.has(s.set_code));
INSERT INTO sets (set_name, set_code, num_of_cards, tcg_date) if (!newSets.length) return { added: 0, total: sets.length };
VALUES ${values}
ON DUPLICATE KEY UPDATE
set_name = VALUES(set_name),
num_of_cards = VALUES(num_of_cards),
tcg_date = VALUES(tcg_date)
`, params);
return { added: sets.length, total: sets.length }; const values = newSets.map(() => '(?, ?, ?, ?)').join(', ');
const params = newSets.flatMap(s => [s.set_name, s.set_code, s.num_of_cards, s.tcg_date]);
await db.execute(`INSERT INTO sets (set_name, set_code, num_of_cards, tcg_date) VALUES ${values}`, params);
return { added: newSets.length, total: sets.length };
} }
async function importSets(req, res) { async function importSets(req, res) {
try { try {
const result = await importSetsInternal(); res.json(await importSetsInternal());
res.json(result);
} catch (err) { } catch (err) {
console.error('Error importing sets:', err); console.error('Error importing sets:', err);
res.status(500).json({ error: 'Failed to import sets' }); res.status(500).json({ error: 'Failed to import sets' });
@@ -36,16 +38,31 @@ async function importSets(req, res) {
async function importCardsInternal() { async function importCardsInternal() {
const startTime = Date.now(); const startTime = Date.now();
const cards = await fetchAllCards(); const cards = await fetchAllCards();
const totalCards = cards.length;
// Pre-load set and rarity caches // Load full existing state for diffing
const [existingCards] = await db.execute(
'SELECT id, name, card_type, frame_type, level, race, attribute, link_val, tcg_date, ocg_date FROM cards'
);
const cardFingerprintMap = new Map(existingCards.map(r => [
r.id,
[r.name, r.card_type, r.frame_type, r.level, r.race,
r.attribute, r.link_val, r.tcg_date, r.ocg_date].join('|')
]));
const [existingImages] = await db.execute('SELECT image_id FROM card_images');
const existingImageIds = new Set(existingImages.map(r => r.image_id));
const [existingCSR] = await db.execute('SELECT card_id, set_id, rarity_id FROM card_sets_rarity');
const existingCSRKeys = new Set(existingCSR.map(r => `${r.card_id}-${r.set_id}-${r.rarity_id}`));
// Pre-load caches
const [setsRows] = await db.execute('SELECT id, set_name FROM sets'); const [setsRows] = await db.execute('SELECT id, set_name FROM sets');
const setCache = Object.fromEntries(setsRows.map(r => [r.set_name, r.id])); const setCache = Object.fromEntries(setsRows.map(r => [r.set_name, r.id]));
const [rarityRows] = await db.execute('SELECT id, rarity_name FROM rarities'); const [rarityRows] = await db.execute('SELECT id, rarity_name FROM rarities');
const rarityCache = Object.fromEntries(rarityRows.map(r => [r.rarity_name, r.id])); const rarityCache = Object.fromEntries(rarityRows.map(r => [r.rarity_name, r.id]));
// Collect all unique rarities from the full card list and upsert them before the main loop // Collect and upsert unseen rarities before the main loop
const unseenRarities = new Map(); const unseenRarities = new Map();
for (const card of cards) { for (const card of cards) {
if (!Array.isArray(card.card_sets)) continue; if (!Array.isArray(card.card_sets)) continue;
@@ -55,7 +72,6 @@ async function importCardsInternal() {
} }
} }
} }
if (unseenRarities.size) { if (unseenRarities.size) {
const rarityValues = Array.from(unseenRarities.keys()).map(() => '(?, ?)').join(', '); const rarityValues = Array.from(unseenRarities.keys()).map(() => '(?, ?)').join(', ');
const rarityParams = Array.from(unseenRarities.entries()).flatMap(([name, code]) => [name, code]); const rarityParams = Array.from(unseenRarities.entries()).flatMap(([name, code]) => [name, code]);
@@ -64,14 +80,13 @@ async function importCardsInternal() {
VALUES ${rarityValues} VALUES ${rarityValues}
ON DUPLICATE KEY UPDATE rarity_code = VALUES(rarity_code) ON DUPLICATE KEY UPDATE rarity_code = VALUES(rarity_code)
`, rarityParams); `, rarityParams);
const [newRarityRows] = await db.execute('SELECT id, rarity_name FROM rarities'); const [newRarityRows] = await db.execute('SELECT id, rarity_name FROM rarities');
for (const row of newRarityRows) rarityCache[row.rarity_name] = row.id; for (const row of newRarityRows) rarityCache[row.rarity_name] = row.id;
} }
let addedCards = 0, addedImages = 0, addedSetRarities = 0; let addedCards = 0, addedImages = 0, addedSetRarities = 0;
for (let i = 0; i < totalCards; i += BATCH_SIZE) { for (let i = 0; i < cards.length; i += BATCH_SIZE) {
const batch = cards.slice(i, i + BATCH_SIZE); const batch = cards.slice(i, i + BATCH_SIZE);
const cardValues = [], cardParams = []; const cardValues = [], cardParams = [];
@@ -79,17 +94,24 @@ async function importCardsInternal() {
const csrValues = [], csrParams = []; const csrValues = [], csrParams = [];
for (const card of batch) { for (const card of batch) {
const fp = cardFingerprint(card);
if (cardFingerprintMap.get(card.id) !== fp) {
cardValues.push('(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); cardValues.push('(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
cardParams.push( cardParams.push(
card.id, card.name, card.type, card.frameType, card.id, card.name, card.type, card.frameType,
card.level || null, card.race || null, card.attribute || null, card.level || null, card.race || null, card.attribute || null,
card.linkval || null, card.tcg_date || null, card.ocg_date || null card.linkval || null, card.tcg_date || null, card.ocg_date || null
); );
addedCards++;
}
if (Array.isArray(card.card_images)) { if (Array.isArray(card.card_images)) {
for (const img of card.card_images) { for (const img of card.card_images) {
if (!existingImageIds.has(img.id)) {
imageValues.push('(?, ?, ?)'); imageValues.push('(?, ?, ?)');
imageParams.push(img.id, card.id, img.image_url); imageParams.push(img.id, card.id, img.image_url);
addedImages++;
}
} }
} }
@@ -98,17 +120,24 @@ async function importCardsInternal() {
const set_id = setCache[set.set_name]; const set_id = setCache[set.set_name];
const rarity_id = rarityCache[set.set_rarity]; const rarity_id = rarityCache[set.set_rarity];
if (set_id && rarity_id) { if (set_id && rarity_id) {
const key = `${card.id}-${set_id}-${rarity_id}`;
if (!existingCSRKeys.has(key)) {
csrValues.push('(?, ?, ?, ?)'); csrValues.push('(?, ?, ?, ?)');
csrParams.push(card.id, set_id, rarity_id, set.set_code); csrParams.push(card.id, set_id, rarity_id, set.set_code);
addedSetRarities++;
} }
} }
} }
} }
}
if (!cardValues.length && !imageValues.length && !csrValues.length) continue;
const conn = await db.getConnection(); const conn = await db.getConnection();
try { try {
await conn.beginTransaction(); await conn.beginTransaction();
if (cardValues.length) {
await conn.execute(` await conn.execute(`
INSERT INTO cards (id, name, card_type, frame_type, level, race, attribute, link_val, tcg_date, ocg_date) INSERT INTO cards (id, name, card_type, frame_type, level, race, attribute, link_val, tcg_date, ocg_date)
VALUES ${cardValues.join(', ')} VALUES ${cardValues.join(', ')}
@@ -117,27 +146,22 @@ async function importCardsInternal() {
level = VALUES(level), race = VALUES(race), attribute = VALUES(attribute), level = VALUES(level), race = VALUES(race), attribute = VALUES(attribute),
link_val = VALUES(link_val), tcg_date = VALUES(tcg_date), ocg_date = VALUES(ocg_date) link_val = VALUES(link_val), tcg_date = VALUES(tcg_date), ocg_date = VALUES(ocg_date)
`, cardParams); `, cardParams);
}
if (imageValues.length) { if (imageValues.length) {
await conn.execute(` await conn.execute(`
INSERT INTO card_images (image_id, card_id, image_url) INSERT INTO card_images (image_id, card_id, image_url) VALUES ${imageValues.join(', ')}
VALUES ${imageValues.join(', ')}
ON DUPLICATE KEY UPDATE image_url = VALUES(image_url)
`, imageParams); `, imageParams);
} }
if (csrValues.length) { if (csrValues.length) {
await conn.execute(` await conn.execute(`
INSERT INTO card_sets_rarity (card_id, set_id, rarity_id, card_set_code) INSERT INTO card_sets_rarity (card_id, set_id, rarity_id, card_set_code) VALUES ${csrValues.join(', ')}
VALUES ${csrValues.join(', ')}
ON DUPLICATE KEY UPDATE card_set_code = VALUES(card_set_code) ON DUPLICATE KEY UPDATE card_set_code = VALUES(card_set_code)
`, csrParams); `, csrParams);
} }
await conn.commit(); await conn.commit();
addedCards += batch.length;
addedImages += imageValues.length;
addedSetRarities += csrValues.length;
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
throw err; throw err;
@@ -147,7 +171,7 @@ async function importCardsInternal() {
} }
return { return {
total_cards: totalCards, total_cards: cards.length,
cards_added: addedCards, cards_added: addedCards,
images_added: addedImages, images_added: addedImages,
set_rarities_added: addedSetRarities, set_rarities_added: addedSetRarities,
@@ -157,8 +181,7 @@ async function importCardsInternal() {
async function importCards(req, res) { async function importCards(req, res) {
try { try {
const result = await importCardsInternal(); res.json(await importCardsInternal());
res.json(result);
} catch (err) { } catch (err) {
console.error('Error importing cards:', err); console.error('Error importing cards:', err);
res.status(500).json({ error: 'Failed to import cards' }); res.status(500).json({ error: 'Failed to import cards' });