-- 001_initial_schema.sql -- Phase 1: Complete tournament engine schema -- All financial columns use INTEGER (int64 cents, never REAL/FLOAT) -- All timestamps use INTEGER (Unix epoch seconds) -- All UUIDs stored as TEXT -- ============================================================================= -- Venue & Settings -- ============================================================================= CREATE TABLE IF NOT EXISTS venue_settings ( id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row venue_name TEXT NOT NULL DEFAULT '', currency_code TEXT NOT NULL DEFAULT 'DKK', currency_symbol TEXT NOT NULL DEFAULT 'kr', rounding_denomination INTEGER NOT NULL DEFAULT 5000, -- 50.00 kr in cents receipt_mode TEXT NOT NULL DEFAULT 'digital' CHECK (receipt_mode IN ('off', 'digital', 'print', 'both')), timezone TEXT NOT NULL DEFAULT 'Europe/Copenhagen', created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Building Blocks (venue-level reusable) -- ============================================================================= -- Chip Sets CREATE TABLE IF NOT EXISTS chip_sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS chip_denominations ( id INTEGER PRIMARY KEY AUTOINCREMENT, chip_set_id INTEGER NOT NULL REFERENCES chip_sets(id) ON DELETE CASCADE, value INTEGER NOT NULL, -- cents color_hex TEXT NOT NULL DEFAULT '#FFFFFF', label TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0, UNIQUE(chip_set_id, value) ); -- Blind Structures CREATE TABLE IF NOT EXISTS blind_structures ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, game_type_default TEXT NOT NULL DEFAULT 'nlhe', notes TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS blind_levels ( id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL REFERENCES blind_structures(id) ON DELETE CASCADE, position INTEGER NOT NULL, -- sort order level_type TEXT NOT NULL DEFAULT 'round' CHECK (level_type IN ('round', 'break')), game_type TEXT NOT NULL DEFAULT 'nlhe', small_blind INTEGER NOT NULL DEFAULT 0, big_blind INTEGER NOT NULL DEFAULT 0, ante INTEGER NOT NULL DEFAULT 0, bb_ante INTEGER NOT NULL DEFAULT 0, duration_seconds INTEGER NOT NULL DEFAULT 900, -- 15 min default chip_up_denomination_value INTEGER, -- nullable, cents notes TEXT NOT NULL DEFAULT '', UNIQUE(structure_id, position) ); -- Payout Structures CREATE TABLE IF NOT EXISTS payout_structures ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS payout_brackets ( id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL REFERENCES payout_structures(id) ON DELETE CASCADE, min_entries INTEGER NOT NULL, max_entries INTEGER NOT NULL, CHECK (min_entries <= max_entries) ); CREATE TABLE IF NOT EXISTS payout_tiers ( id INTEGER PRIMARY KEY AUTOINCREMENT, bracket_id INTEGER NOT NULL REFERENCES payout_brackets(id) ON DELETE CASCADE, position INTEGER NOT NULL, percentage_basis_points INTEGER NOT NULL, -- e.g. 5000 = 50.00% UNIQUE(bracket_id, position) ); -- Buy-in Configs CREATE TABLE IF NOT EXISTS buyin_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, buyin_amount INTEGER NOT NULL DEFAULT 0, -- cents starting_chips INTEGER NOT NULL DEFAULT 0, rake_total INTEGER NOT NULL DEFAULT 0, -- cents bounty_amount INTEGER NOT NULL DEFAULT 0, -- cents bounty_chip INTEGER NOT NULL DEFAULT 0, rebuy_allowed INTEGER NOT NULL DEFAULT 0, rebuy_cost INTEGER NOT NULL DEFAULT 0, -- cents rebuy_chips INTEGER NOT NULL DEFAULT 0, rebuy_rake INTEGER NOT NULL DEFAULT 0, -- cents rebuy_limit INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited rebuy_level_cutoff INTEGER, -- nullable rebuy_time_cutoff_seconds INTEGER, -- nullable rebuy_chip_threshold INTEGER, -- nullable addon_allowed INTEGER NOT NULL DEFAULT 0, addon_cost INTEGER NOT NULL DEFAULT 0, -- cents addon_chips INTEGER NOT NULL DEFAULT 0, addon_rake INTEGER NOT NULL DEFAULT 0, -- cents addon_level_start INTEGER, -- nullable addon_level_end INTEGER, -- nullable reentry_allowed INTEGER NOT NULL DEFAULT 0, reentry_limit INTEGER NOT NULL DEFAULT 0, late_reg_level_cutoff INTEGER, -- nullable late_reg_time_cutoff_seconds INTEGER, -- nullable created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS rake_splits ( id INTEGER PRIMARY KEY AUTOINCREMENT, buyin_config_id INTEGER NOT NULL REFERENCES buyin_configs(id) ON DELETE CASCADE, category TEXT NOT NULL CHECK (category IN ('house', 'staff', 'league', 'season_reserve')), amount INTEGER NOT NULL DEFAULT 0, -- cents UNIQUE(buyin_config_id, category) ); -- Points Formulas CREATE TABLE IF NOT EXISTS points_formulas ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, expression TEXT NOT NULL DEFAULT '', variables TEXT NOT NULL DEFAULT '{}', -- JSON is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Tournament Templates -- ============================================================================= CREATE TABLE IF NOT EXISTS tournament_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', chip_set_id INTEGER NOT NULL REFERENCES chip_sets(id), blind_structure_id INTEGER NOT NULL REFERENCES blind_structures(id), payout_structure_id INTEGER NOT NULL REFERENCES payout_structures(id), buyin_config_id INTEGER NOT NULL REFERENCES buyin_configs(id), points_formula_id INTEGER REFERENCES points_formulas(id), -- nullable min_players INTEGER NOT NULL DEFAULT 2, max_players INTEGER, -- nullable early_signup_bonus_chips INTEGER NOT NULL DEFAULT 0, early_signup_cutoff TEXT, -- nullable, datetime or player count punctuality_bonus_chips INTEGER NOT NULL DEFAULT 0, is_pko INTEGER NOT NULL DEFAULT 0, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Tournaments (runtime instances) -- ============================================================================= CREATE TABLE IF NOT EXISTS tournaments ( id TEXT PRIMARY KEY, -- UUID name TEXT NOT NULL, template_id INTEGER REFERENCES tournament_templates(id), -- nullable, reference only -- Copied config (local changes don't affect template) chip_set_id INTEGER NOT NULL REFERENCES chip_sets(id), blind_structure_id INTEGER NOT NULL REFERENCES blind_structures(id), payout_structure_id INTEGER NOT NULL REFERENCES payout_structures(id), buyin_config_id INTEGER NOT NULL REFERENCES buyin_configs(id), points_formula_id INTEGER REFERENCES points_formulas(id), -- nullable status TEXT NOT NULL DEFAULT 'created' CHECK (status IN ( 'created', 'registering', 'running', 'paused', 'final_table', 'completed', 'cancelled' )), min_players INTEGER NOT NULL DEFAULT 2, max_players INTEGER, -- nullable early_signup_bonus_chips INTEGER NOT NULL DEFAULT 0, early_signup_cutoff TEXT, -- nullable punctuality_bonus_chips INTEGER NOT NULL DEFAULT 0, is_pko INTEGER NOT NULL DEFAULT 0, -- Runtime state current_level INTEGER NOT NULL DEFAULT 0, clock_state TEXT NOT NULL DEFAULT 'stopped' CHECK (clock_state IN ('stopped', 'running', 'paused')), clock_remaining_ns INTEGER NOT NULL DEFAULT 0, total_elapsed_ns INTEGER NOT NULL DEFAULT 0, hand_for_hand INTEGER NOT NULL DEFAULT 0, started_at INTEGER, -- nullable ended_at INTEGER, -- nullable created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Players -- ============================================================================= CREATE TABLE IF NOT EXISTS players ( id TEXT PRIMARY KEY, -- UUID name TEXT NOT NULL, nickname TEXT, email TEXT, phone TEXT, photo_url TEXT, notes TEXT, custom_fields TEXT, -- JSON created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS tournament_players ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, player_id TEXT NOT NULL REFERENCES players(id), status TEXT NOT NULL DEFAULT 'registered' CHECK (status IN ( 'registered', 'active', 'busted', 'deal' )), seat_table_id INTEGER REFERENCES tables(id), -- nullable seat_position INTEGER, -- nullable buy_in_at INTEGER, -- nullable bust_out_at INTEGER, -- nullable bust_out_order INTEGER, -- nullable, position when busted finishing_position INTEGER, -- nullable, final position current_chips INTEGER NOT NULL DEFAULT 0, rebuys INTEGER NOT NULL DEFAULT 0, addons INTEGER NOT NULL DEFAULT 0, reentries INTEGER NOT NULL DEFAULT 0, bounty_value INTEGER NOT NULL DEFAULT 0, -- cents, for PKO bounties_collected INTEGER NOT NULL DEFAULT 0, prize_amount INTEGER NOT NULL DEFAULT 0, -- cents points_awarded INTEGER NOT NULL DEFAULT 0, early_signup_bonus_applied INTEGER NOT NULL DEFAULT 0, punctuality_bonus_applied INTEGER NOT NULL DEFAULT 0, hitman_player_id TEXT REFERENCES players(id), -- nullable, who busted them created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()), UNIQUE(tournament_id, player_id) ); -- ============================================================================= -- Tables & Seating -- ============================================================================= CREATE TABLE IF NOT EXISTS tables ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, name TEXT NOT NULL, seat_count INTEGER NOT NULL DEFAULT 9 CHECK (seat_count >= 6 AND seat_count <= 10), dealer_button_position INTEGER, -- nullable is_active INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS table_blueprints ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, table_configs TEXT NOT NULL DEFAULT '[]', -- JSON array of {name, seat_count} created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS balance_suggestions ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ( 'pending', 'accepted', 'cancelled', 'expired' )), from_table_id INTEGER NOT NULL REFERENCES tables(id), to_table_id INTEGER NOT NULL REFERENCES tables(id), player_id TEXT REFERENCES players(id), -- nullable from_seat INTEGER, -- nullable to_seat INTEGER, -- nullable reason TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT (unixepoch()), resolved_at INTEGER -- nullable ); -- ============================================================================= -- Financial Transactions -- ============================================================================= CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY, -- UUID tournament_id TEXT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, player_id TEXT NOT NULL REFERENCES players(id), type TEXT NOT NULL CHECK (type IN ( 'buyin', 'rebuy', 'addon', 'reentry', 'bounty_collected', 'bounty_paid', 'payout', 'rake', 'chop', 'bubble_prize' )), amount INTEGER NOT NULL DEFAULT 0, -- cents chips INTEGER NOT NULL DEFAULT 0, -- chips given/removed operator_id TEXT NOT NULL, -- who performed the action receipt_data TEXT, -- nullable, JSON undone INTEGER NOT NULL DEFAULT 0, undone_by TEXT, -- nullable, FK audit_entries.id metadata TEXT, -- nullable, JSON (bounty chain, chop details, etc) created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS bubble_prizes ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE, amount INTEGER NOT NULL DEFAULT 0, -- cents funded_from TEXT NOT NULL DEFAULT '[]', -- JSON array of {position, reduction_amount} status TEXT NOT NULL DEFAULT 'proposed' CHECK (status IN ( 'proposed', 'confirmed', 'cancelled' )), created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Audit Trail (append-only) -- ============================================================================= CREATE TABLE IF NOT EXISTS audit_entries ( id TEXT PRIMARY KEY, -- UUID tournament_id TEXT, -- nullable, some are venue-level timestamp INTEGER NOT NULL DEFAULT (unixepoch()), -- UnixNano in Go, epoch seconds in SQL default operator_id TEXT NOT NULL, action TEXT NOT NULL, -- e.g. 'player.bust', 'financial.buyin', 'clock.pause', 'seat.move' target_type TEXT NOT NULL DEFAULT '', target_id TEXT NOT NULL DEFAULT '', previous_state TEXT, -- JSON new_state TEXT, -- JSON metadata TEXT, -- nullable, JSON undone_by TEXT -- nullable, references audit_entries.id -- NO UPDATE OR DELETE -- enforced by triggers below ); -- Tamper protection: REJECT any UPDATE except setting undone_by on a row where undone_by is currently NULL CREATE TRIGGER IF NOT EXISTS audit_entries_no_update BEFORE UPDATE ON audit_entries WHEN OLD.undone_by IS NOT NULL OR NEW.undone_by IS NULL OR OLD.id != NEW.id OR OLD.timestamp != NEW.timestamp OR OLD.operator_id != NEW.operator_id OR OLD.action != NEW.action BEGIN SELECT RAISE(ABORT, 'audit_entries is append-only: only undone_by may be set once'); END; -- Tamper protection: REJECT any DELETE on audit_entries entirely CREATE TRIGGER IF NOT EXISTS audit_entries_no_delete BEFORE DELETE ON audit_entries BEGIN SELECT RAISE(ABORT, 'audit_entries is append-only: deletion is prohibited'); END; -- ============================================================================= -- Operators -- ============================================================================= CREATE TABLE IF NOT EXISTS operators ( id TEXT PRIMARY KEY, -- UUID name TEXT NOT NULL, pin_hash TEXT NOT NULL, -- bcrypt role TEXT NOT NULL DEFAULT 'floor' CHECK (role IN ('admin', 'floor', 'viewer')), created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); -- ============================================================================= -- Indexes -- ============================================================================= -- Tournament players: active player lookups and rankings CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament_status ON tournament_players(tournament_id, status); CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament_bust_order ON tournament_players(tournament_id, bust_out_order); -- Transactions: financial summaries and player history CREATE INDEX IF NOT EXISTS idx_transactions_tournament_type ON transactions(tournament_id, type); CREATE INDEX IF NOT EXISTS idx_transactions_tournament_player ON transactions(tournament_id, player_id); -- Audit entries: log browsing and action filtering CREATE INDEX IF NOT EXISTS idx_audit_entries_tournament_timestamp ON audit_entries(tournament_id, timestamp); CREATE INDEX IF NOT EXISTS idx_audit_entries_action ON audit_entries(action); -- Tables: active table lookups per tournament CREATE INDEX IF NOT EXISTS idx_tables_tournament_active ON tables(tournament_id, is_active); -- Foreign key indexes on child tables CREATE INDEX IF NOT EXISTS idx_chip_denominations_chip_set ON chip_denominations(chip_set_id); CREATE INDEX IF NOT EXISTS idx_blind_levels_structure ON blind_levels(structure_id); CREATE INDEX IF NOT EXISTS idx_payout_brackets_structure ON payout_brackets(structure_id); CREATE INDEX IF NOT EXISTS idx_payout_tiers_bracket ON payout_tiers(bracket_id); CREATE INDEX IF NOT EXISTS idx_rake_splits_buyin_config ON rake_splits(buyin_config_id); CREATE INDEX IF NOT EXISTS idx_balance_suggestions_tournament ON balance_suggestions(tournament_id); CREATE INDEX IF NOT EXISTS idx_bubble_prizes_tournament ON bubble_prizes(tournament_id); CREATE INDEX IF NOT EXISTS idx_tournament_players_player ON tournament_players(player_id);