fix: unique index words.titel_en als partial index + robuster Upsert ohne ON CONFLICT
- Migration: partiell WHERE IS NOT NULL, dedup vorher, kein silent-catch - Route: INSERT mit .catch(23505) → UPDATE statt ON CONFLICT (partial index inkompatibel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -861,8 +861,20 @@ async function migrate() {
|
|||||||
await query(`ALTER TABLE words ADD CONSTRAINT words_level_check CHECK (level IN ('A1', 'A2', 'B1'))`).catch(() => {});
|
await query(`ALTER TABLE words ADD CONSTRAINT words_level_check CHECK (level IN ('A1', 'A2', 'B1'))`).catch(() => {});
|
||||||
|
|
||||||
// Unique-Index auf titel_en — Voraussetzung für ON CONFLICT im CSV-Import.
|
// Unique-Index auf titel_en — Voraussetzung für ON CONFLICT im CSV-Import.
|
||||||
// Falls bestehende Duplikate den Index verhindern, muss erst bereinigt werden.
|
// Partiell (WHERE IS NOT NULL) damit bestehende NULL-Zeilen den Index nicht blockieren.
|
||||||
await query(`CREATE UNIQUE INDEX IF NOT EXISTS words_titel_en_key ON words (titel_en)`).catch(() => {});
|
// Doppelte non-null titel_en erst bereinigen, dann Index anlegen.
|
||||||
|
await query(`
|
||||||
|
DELETE FROM words w
|
||||||
|
USING (
|
||||||
|
SELECT titel_en, MAX(created_at) AS keep_at
|
||||||
|
FROM words WHERE titel_en IS NOT NULL
|
||||||
|
GROUP BY titel_en HAVING COUNT(*) > 1
|
||||||
|
) dup
|
||||||
|
WHERE w.titel_en = dup.titel_en AND w.created_at < dup.keep_at
|
||||||
|
`).catch(() => {});
|
||||||
|
await query(
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS words_titel_en_key ON words (titel_en) WHERE titel_en IS NOT NULL`
|
||||||
|
);
|
||||||
|
|
||||||
// enrich_batches — Status-Tracking für Wort-Anreicherungs-Batches (analog category_batches)
|
// enrich_batches — Status-Tracking für Wort-Anreicherungs-Batches (analog category_batches)
|
||||||
await query(`
|
await query(`
|
||||||
|
|||||||
@@ -77,17 +77,26 @@ router.post('/', async (req, res, next) => {
|
|||||||
// Auto: alle 3 Sprachen direkt mitgeliefert + kein expliziter Status → 'translated'
|
// Auto: alle 3 Sprachen direkt mitgeliefert + kein expliziter Status → 'translated'
|
||||||
const allLangs = titel_de && titel_en && titel_sv;
|
const allLangs = titel_de && titel_en && titel_sv;
|
||||||
const effectiveStatus = status || (allLangs ? 'translated' : 'requested');
|
const effectiveStatus = status || (allLangs ? 'translated' : 'requested');
|
||||||
const result = await query(
|
// Upsert: neu anlegen oder bei doppeltem titel_en nur conc_m aktualisieren
|
||||||
|
let result = await query(
|
||||||
`INSERT INTO words (titel_de, titel_en, titel_sv, difficulty_level, status, conc_m, requested_at)
|
`INSERT INTO words (titel_de, titel_en, titel_sv, difficulty_level, status, conc_m, requested_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING *, true AS is_insert`,
|
||||||
ON CONFLICT (titel_en) DO UPDATE SET conc_m = EXCLUDED.conc_m
|
|
||||||
RETURNING *, (xmax = 0) AS is_insert`,
|
|
||||||
[titel_de || null, titel_en || null, titel_sv || null,
|
[titel_de || null, titel_en || null, titel_sv || null,
|
||||||
difficulty_level || null, effectiveStatus, conc_m ?? null]
|
difficulty_level || null, effectiveStatus, conc_m ?? null]
|
||||||
|
).catch(async err => {
|
||||||
|
if (err.code === '23505' && titel_en) {
|
||||||
|
// Duplikat auf titel_en → conc_m aktualisieren und bestehende Zeile zurückgeben
|
||||||
|
const upd = await query(
|
||||||
|
`UPDATE words SET conc_m = $1 WHERE titel_en = $2 RETURNING *, false AS is_insert`,
|
||||||
|
[conc_m ?? null, titel_en]
|
||||||
);
|
);
|
||||||
|
return upd;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
const row = result.rows[0];
|
const row = result.rows[0];
|
||||||
const { is_insert: _, ...word } = row;
|
const { is_insert, ...word } = row;
|
||||||
res.status(row.is_insert ? 201 : 200).json({ ...word, picture_ids: [], category_ids: [] });
|
res.status(is_insert ? 201 : 200).json({ ...word, picture_ids: [], category_ids: [] });
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user