diff --git a/backend/docs/falukant-character-development.md b/backend/docs/falukant-character-development.md index 7472c61..8a22aaf 100644 --- a/backend/docs/falukant-character-development.md +++ b/backend/docs/falukant-character-development.md @@ -11,6 +11,8 @@ Werte für bestehende Charaktere. Ein Datenbanktrigger initialisiert sie auch f neue Charaktere, einschließlich der direkten SQL-Inserts des Daemons für NPCs und Neugeborene. Normale Updates, auch das Übergeben von NULL, würfeln bereits gespeicherte Werte nicht neu aus. Explizite gültige Änderungen bleiben möglich. +Die Migration kann auch nach manueller Vorbereitung der Spalten erneut laufen: +Vorhandene Werte bleiben erhalten, fehlende Spalten und Werte werden ergänzt. Das vereinfachte Spielmodell verwendet folgende Verteilung: diff --git a/backend/migrations-active/20261104000000-add-character-development-ages.cjs b/backend/migrations-active/20261104000000-add-character-development-ages.cjs index 665ba61..6573486 100644 --- a/backend/migrations-active/20261104000000-add-character-development-ages.cjs +++ b/backend/migrations-active/20261104000000-add-character-development-ages.cjs @@ -5,13 +5,13 @@ module.exports = { await queryInterface.sequelize.transaction(async (transaction) => { await queryInterface.sequelize.query(` ALTER TABLE falukant_data.character - ADD COLUMN puberty_start_age SMALLINT, - ADD COLUMN fertile_from_age SMALLINT; + ADD COLUMN IF NOT EXISTS puberty_start_age SMALLINT, + ADD COLUMN IF NOT EXISTS fertile_from_age SMALLINT; -- Shared by Sequelize and raw daemon INSERTs, including newborns and NPCs. -- One real calendar day is one game year. These are game distributions, -- not medical predictions or reconstructed medieval population data. - CREATE FUNCTION falukant_data.initialize_character_development() + CREATE OR REPLACE FUNCTION falukant_data.initialize_character_development() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'UPDATE' THEN @@ -30,21 +30,35 @@ module.exports = { END; $$; + DROP TRIGGER IF EXISTS initialize_character_development ON falukant_data.character; CREATE TRIGGER initialize_character_development BEFORE INSERT OR UPDATE ON falukant_data.character FOR EACH ROW EXECUTE FUNCTION falukant_data.initialize_character_development(); -- Existing pregnancies and relationships are deliberately untouched. - UPDATE falukant_data.character SET puberty_start_age = puberty_start_age; + UPDATE falukant_data.character SET puberty_start_age = puberty_start_age + WHERE puberty_start_age IS NULL OR fertile_from_age IS NULL; ALTER TABLE falukant_data.character ALTER COLUMN puberty_start_age SET NOT NULL, - ALTER COLUMN fertile_from_age SET NOT NULL, - ADD CONSTRAINT character_development_ages_valid CHECK ( - puberty_start_age BETWEEN 8 AND 18 - AND fertile_from_age > puberty_start_age - AND fertile_from_age <= 21 - ); + ALTER COLUMN fertile_from_age SET NOT NULL; + + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conrelid = 'falukant_data.character'::regclass + AND conname = 'character_development_ages_valid' + ) THEN + ALTER TABLE falukant_data.character + ADD CONSTRAINT character_development_ages_valid CHECK ( + puberty_start_age BETWEEN 8 AND 18 + AND fertile_from_age > puberty_start_age + AND fertile_from_age <= 21 + ); + END IF; + END; + $$; `, { transaction }); }); }, diff --git a/backend/tools/characterDevelopment.test.mjs b/backend/tools/characterDevelopment.test.mjs index 895b62b..4390616 100644 --- a/backend/tools/characterDevelopment.test.mjs +++ b/backend/tools/characterDevelopment.test.mjs @@ -71,6 +71,31 @@ test('database migration, persistence and daemon conception paths', { await assert.rejects(db.query(`INSERT INTO falukant_data.character (puberty_start_age, fertile_from_age) VALUES (12, 11)`)); + await t.test('repeating the migration preserves all existing character data', async () => { + const [saved] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); + await migration.up(qi); + const [repeated] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); + assert.deepEqual(repeated, saved); + }); + + for (const bothColumns of [true, false]) { + await t.test(`manually prepared schema with ${bothColumns ? 'both columns' : 'one column'}`, async () => { + await migration.down(qi); + await db.query(`ALTER TABLE falukant_data.character ADD COLUMN puberty_start_age SMALLINT`); + if (bothColumns) { + await db.query(`ALTER TABLE falukant_data.character ADD COLUMN fertile_from_age SMALLINT; + UPDATE falukant_data.character SET fertile_from_age = 13 WHERE id = 1`); + } + await db.query(`UPDATE falukant_data.character SET puberty_start_age = 10 WHERE id = 1`); + await migration.up(qi); + const [characters] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); + assert.equal(characters[0].puberty_start_age, 10); + if (bothColumns) assert.equal(characters[0].fertile_from_age, 13); + assert.deepEqual(characters[0].pregnancy_due_at, before[0].pregnancy_due_at); + assert.ok(characters.every(c => c.fertile_from_age > c.puberty_start_age)); + }); + } + await t.test('daemon checks both parents and preserves already scheduled births', { skip: !process.env.DAEMON_SQL_PATH, }, async () => {