schema: fix actor_profile FK by adding UNIQUE(actor_id), bump to v5
This commit is contained in:
@@ -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.
|
||||
-- ================================================================
|
||||
-- ===================================================================
|
||||
|
||||
61
data/repair_player_db_fk.sql
Normal file
61
data/repair_player_db_fk.sql
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user