diff --git a/data/create_player_db.sql b/data/create_player_db.sql index 0c61909..71beaed 100644 --- a/data/create_player_db.sql +++ b/data/create_player_db.sql @@ -1,6 +1,6 @@ -- OTIVM Per-Player Database Schema -- File: data/create_player_db.sql --- Version: OTIVM-IV (schema version 4) +-- Version: OTIVM-V (schema version 5) -- Status: Game schema — transitional, designed to be throw-away -- Replaces: data/saves/{session_id}.json (OTIVM-I/II) -- Parallel to: data/create_otivm_db.sql (TESSERA world substrate) @@ -27,20 +27,27 @@ PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; -PRAGMA user_version = 4; -- OTIVM-IV schema version +PRAGMA user_version = 5; -- OTIVM-V schema version --- ================================================================ +-- =================================================================== -- TABLE: actor_profile -- One row per actor. The static anchor. -- Created at session initialisation. Never updated — superseded -- by a new row if background is changed (not currently possible -- in OTIVM-IV but the structure supports it). -- +-- UNIQUE (actor_id): required so that child tables can hold a valid +-- foreign key to actor_id alone. The composite PRIMARY KEY +-- (actor_id, recorded_at) supports the append-only supersession +-- pattern; the UNIQUE constraint makes actor_id independently +-- referenceable. In practice, each player database file holds +-- exactly one actor, so actor_id is always unique within the file. +-- -- session_anchor_at: NULL until first dispatch. Set once at the -- moment the player fires their first venture. All simulation -- clock arithmetic is relative to this timestamp. -- See docs/architecture/simulation-clock.md Section 7. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS actor_profile ( actor_id TEXT NOT NULL, -- uuid, matches save file naming @@ -51,15 +58,16 @@ CREATE TABLE IF NOT EXISTS actor_profile ( actor_name TEXT NOT NULL, -- display name chosen by participant epoch TEXT NOT NULL -- roman_14bce (Layer 3 code token) DEFAULT 'roman_14bce', - schema_version INTEGER NOT NULL DEFAULT 4, + schema_version INTEGER NOT NULL DEFAULT 5, recorded_at TEXT NOT NULL, -- ISO 8601 UTC — row creation time session_anchor_at TEXT, -- ISO 8601 UTC — first dispatch time -- NULL until player fires first venture -- Never updated after it is set - PRIMARY KEY (actor_id, recorded_at) + PRIMARY KEY (actor_id, recorded_at), + UNIQUE (actor_id) -- required for FK references from child tables ); --- ================================================================ +-- =================================================================== -- TABLE: actor_parameters -- The core parameter table. -- One row per parameter per actor per recorded moment. @@ -82,7 +90,7 @@ CREATE TABLE IF NOT EXISTS actor_profile ( -- value columns are TEXT to support ordinal bands (low/medium/high), -- enums, floats, integers, and JSON structures without schema changes. -- The parameter_token identifies what type to expect. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS actor_parameters ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -106,14 +114,14 @@ CREATE TABLE IF NOT EXISTS actor_parameters ( CREATE INDEX IF NOT EXISTS idx_actor_parameters_current ON actor_parameters (actor_id, parameter_token, superseded_at); --- ================================================================ +-- =================================================================== -- TABLE: parameter_drift_log -- Append-only event log of every parameter change. -- What changed, why, what triggered it, old and new values. -- The behavioral record. Rows are never deleted. -- This is the source of truth for the Simulator when it -- reads this database as a read-only layer. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS parameter_drift_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -134,11 +142,11 @@ CREATE TABLE IF NOT EXISTS parameter_drift_log ( CREATE INDEX IF NOT EXISTS idx_drift_log_actor ON parameter_drift_log (actor_id, recorded_at); --- ================================================================ +-- =================================================================== -- TABLE: ventures -- One row per venture (NEGOTIVM in Roman layer). -- Status tracks the lifecycle. Outcome recorded on completion. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS ventures ( venture_id TEXT PRIMARY KEY, -- uuid @@ -162,7 +170,7 @@ CREATE TABLE IF NOT EXISTS ventures ( CREATE INDEX IF NOT EXISTS idx_ventures_actor ON ventures (actor_id, status); --- ================================================================ +-- =================================================================== -- TABLE: venture_legs -- One row per leg (ITER in Roman layer). -- Indivisible unit of movement within a venture. @@ -172,7 +180,7 @@ CREATE INDEX IF NOT EXISTS idx_ventures_actor -- Must be a positive integer. Never a decimal. -- Derived from duration_ms / MS_PER_SIM_DAY at write time. -- See docs/architecture/simulation-clock.md Section 2. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS venture_legs ( leg_id TEXT PRIMARY KEY, -- uuid @@ -185,7 +193,7 @@ CREATE TABLE IF NOT EXISTS venture_legs ( duration_days INTEGER, -- simulated days (INTEGER, never REAL) -- NULL until leg completes cost_vectura REAL, -- freight charge (VECTVRA) - cost_portoria REAL, -- customs duty (PORTORIVM) + cost_portoria REAL, -- customs duty (PORTORIUM) cost_other REAL, -- horreum, incidentals cost_total REAL, -- sum of above status TEXT NOT NULL -- planned | active | complete | failed @@ -201,13 +209,13 @@ CREATE TABLE IF NOT EXISTS venture_legs ( CREATE INDEX IF NOT EXISTS idx_legs_venture ON venture_legs (venture_id, leg_sequence); --- ================================================================ +-- =================================================================== -- TABLE: scenario_state -- Active and archived scenario parameters. -- Transient during active window, archived on close. -- Must not persist into actor_parameters as permanent values — -- scenario parameters are pressures, not conditions. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS scenario_state ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -226,7 +234,7 @@ CREATE TABLE IF NOT EXISTS scenario_state ( CREATE INDEX IF NOT EXISTS idx_scenario_active ON scenario_state (actor_id, scenario_id, archived_at); --- ================================================================ +-- =================================================================== -- TABLE: events -- Append-only chronological record of all simulation state -- changes. The atomic unit of history. @@ -234,7 +242,7 @@ CREATE INDEX IF NOT EXISTS idx_scenario_active -- This table is the substrate for the behavioral model — -- when this database becomes a read-only Simulator layer, -- this table is the primary read target. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -260,13 +268,13 @@ CREATE INDEX IF NOT EXISTS idx_events_actor_time CREATE INDEX IF NOT EXISTS idx_events_type ON events (event_type, recorded_at); --- ================================================================ +-- =================================================================== -- TABLE: background_starting_values -- Seed data — the canonical starting parameter values for -- each of the six backgrounds, per parameter-registry.md. -- Read at actor initialisation to populate actor_parameters. -- Never modified after initial insert. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS background_starting_values ( background_id TEXT NOT NULL, @@ -279,11 +287,11 @@ CREATE TABLE IF NOT EXISTS background_starting_values ( PRIMARY KEY (background_id, parameter_token) ); --- ================================================================ +-- =================================================================== -- SEED DATA: background_starting_values -- Source: parameter-registry.md, section 1 (Actor Parameters) -- Ordinal bands: low | medium | high | distinguished | extensive --- ================================================================ +-- =================================================================== INSERT OR IGNORE INTO background_starting_values VALUES -- FORMER LEGIONARY @@ -370,13 +378,13 @@ INSERT OR IGNORE INTO background_starting_values VALUES ('guild_scribe', 'litterae', 'high', 'high', 'indicated', 'full', NULL), ('guild_scribe', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL); --- ================================================================ +-- =================================================================== -- TABLE: schema_changelog -- Documents schema version history. -- Required: if this database becomes a read-only Simulator -- layer, the Simulator must be able to identify schema version -- and adapt its reader accordingly. --- ================================================================ +-- =================================================================== CREATE TABLE IF NOT EXISTS schema_changelog ( version INTEGER PRIMARY KEY, @@ -388,9 +396,10 @@ INSERT OR IGNORE INTO schema_changelog VALUES (1, 'OTIVM-I: JSON save files (not SQLite — recorded here for continuity)', '2026-04-25T00:00:00Z'), (2, 'OTIVM-II: JSON save files with TESSERA map integration', '2026-04-28T00:00:00Z'), (3, 'OTIVM-III: Per-player SQLite schema, parameter registry v1', '2026-05-02T00:00:00Z'), -(4, 'OTIVM-IV: Add session_anchor_at to actor_profile for simulation clock','2026-05-02T00:00:00Z'); +(4, 'OTIVM-IV: Add session_anchor_at to actor_profile for simulation clock','2026-05-02T00:00:00Z'), +(5, 'OTIVM-V: Add UNIQUE(actor_id) to actor_profile to fix FK references', '2026-05-02T00:00:00Z'); --- ================================================================ +-- =================================================================== -- END OF SCHEMA -- To initialise a player database: -- sqlite3 data/saves/{actor_id}.sqlite3 < data/create_player_db.sql @@ -399,9 +408,11 @@ INSERT OR IGNORE INTO schema_changelog VALUES -- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA integrity_check;" -- sqlite3 data/saves/{actor_id}.sqlite3 "SELECT COUNT(*) FROM background_starting_values;" -- Expected: 72 rows (12 parameters × 6 backgrounds) +-- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA foreign_key_check;" +-- Expected: (empty — no violations) -- -- Note on venture_legs.duration_days and delay_days: -- Declared INTEGER, not REAL. This enforces the integer clock -- constraint from docs/architecture/simulation-clock.md. -- A fractional value here is a schema violation. --- ================================================================ +-- =================================================================== diff --git a/data/repair_player_db_fk.sql b/data/repair_player_db_fk.sql new file mode 100644 index 0000000..6d7b907 --- /dev/null +++ b/data/repair_player_db_fk.sql @@ -0,0 +1,61 @@ +-- OTIVM player database FK repair script +-- File: data/repair_player_db_fk.sql +-- Applies to: player databases created under schema v3 or v4 +-- Problem: actor_profile used composite PRIMARY KEY (actor_id, recorded_at) +-- without UNIQUE(actor_id), causing SQLite to reject FK references +-- from child tables (actor_parameters, events, etc.) to actor_id alone. +-- Fix: recreate actor_profile with UNIQUE(actor_id) added. +-- Uses the SQLite rename-recreate-copy-drop pattern. +-- Safe to run on any v3/v4 player database. Idempotent for data. + +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = OFF; + +BEGIN; + +-- Clean up any partial previous repair attempt +DROP TABLE IF EXISTS actor_profile_old; + +-- Step 1: Rename existing broken table +ALTER TABLE actor_profile RENAME TO actor_profile_old; + +-- Step 2: Create corrected table with UNIQUE(actor_id) +CREATE TABLE actor_profile ( + actor_id TEXT NOT NULL, + session_id TEXT NOT NULL, + background_id TEXT NOT NULL, + actor_name TEXT NOT NULL, + epoch TEXT NOT NULL DEFAULT 'roman_14bce', + schema_version INTEGER NOT NULL DEFAULT 5, + recorded_at TEXT NOT NULL, + session_anchor_at TEXT, + PRIMARY KEY (actor_id, recorded_at), + UNIQUE (actor_id) +); + +-- Step 3: Copy all data from old table. +-- session_anchor_at is supplied as NULL for v3 databases that lack the column +-- (v3 predates session_anchor_at). For v4 databases that have the column, +-- the value would be NULL anyway (no player has dispatched on a broken save). +INSERT INTO actor_profile ( + actor_id, session_id, background_id, actor_name, + epoch, schema_version, recorded_at, session_anchor_at +) +SELECT + actor_id, session_id, background_id, actor_name, + epoch, schema_version, recorded_at, NULL +FROM actor_profile_old; + +-- Step 4: Drop the broken old table +DROP TABLE actor_profile_old; + +-- Step 5: Record the repair in schema_changelog +INSERT OR IGNORE INTO schema_changelog VALUES +(5, 'OTIVM-V: Add UNIQUE(actor_id) to actor_profile to fix FK references', '2026-05-02T00:00:00Z'); + +-- Step 6: Bump schema version +PRAGMA user_version = 5; + +COMMIT; + +PRAGMA foreign_keys = ON;