import Database from 'better-sqlite3'; const SCHEMA = ` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, token_hash TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, created_at TEXT DEFAULT current_timestamp ); CREATE TABLE IF NOT EXISTS trips ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, start_date TEXT NOT NULL, end_date TEXT NOT NULL, owner_id INTEGER NOT NULL REFERENCES users(id), currency TEXT NOT NULL DEFAULT 'USD', join_code TEXT UNIQUE NOT NULL, created_at TEXT DEFAULT current_timestamp ); CREATE TABLE IF NOT EXISTS trip_members ( trip_id INTEGER NOT NULL REFERENCES trips(id), user_id INTEGER NOT NULL REFERENCES users(id), role TEXT NOT NULL DEFAULT 'editor', sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (trip_id, user_id) ); CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY, trip_id INTEGER NOT NULL REFERENCES trips(id), date TEXT NOT NULL, end_date TEXT, type TEXT NOT NULL, title TEXT NOT NULL, details TEXT DEFAULT '', start_time TEXT, end_time TEXT, location_name TEXT, lat REAL, lng REAL, sort_order INTEGER NOT NULL DEFAULT 0, price REAL, paid_by INTEGER REFERENCES users(id), split_mode TEXT NOT NULL DEFAULT 'equal', segments TEXT, rental TEXT, transport_mode TEXT, created_at TEXT DEFAULT current_timestamp ); CREATE TABLE IF NOT EXISTS entry_participants ( entry_id INTEGER NOT NULL REFERENCES entries(id), user_id INTEGER NOT NULL REFERENCES users(id), PRIMARY KEY (entry_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_entries_trip ON entries(trip_id, date, sort_order, id); CREATE INDEX IF NOT EXISTS idx_members_user ON trip_members(user_id); CREATE INDEX IF NOT EXISTS idx_participants_entry ON entry_participants(entry_id); `; // Columns added after the initial release: CREATE TABLE IF NOT EXISTS never // alters existing tables, so databases created by an older version need them // backfilled with ALTER TABLE. const MIGRATIONS = [ { table: 'trips', column: 'currency', ddl: "ALTER TABLE trips ADD COLUMN currency TEXT NOT NULL DEFAULT 'USD'" }, { table: 'entries', column: 'price', ddl: 'ALTER TABLE entries ADD COLUMN price REAL' }, { table: 'entries', column: 'paid_by', ddl: 'ALTER TABLE entries ADD COLUMN paid_by INTEGER REFERENCES users(id)' }, { table: 'entries', column: 'split_mode', ddl: "ALTER TABLE entries ADD COLUMN split_mode TEXT NOT NULL DEFAULT 'equal'" }, { table: 'entries', column: 'segments', ddl: 'ALTER TABLE entries ADD COLUMN segments TEXT' }, { table: 'entries', column: 'rental', ddl: 'ALTER TABLE entries ADD COLUMN rental TEXT' }, { table: 'entries', column: 'end_date', ddl: 'ALTER TABLE entries ADD COLUMN end_date TEXT' }, { table: 'entries', column: 'transport_mode', ddl: 'ALTER TABLE entries ADD COLUMN transport_mode TEXT' }, { table: 'trip_members', column: 'sort_order', ddl: 'ALTER TABLE trip_members ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0' }, ]; function applyMigrations(db) { for (const { table, column, ddl } of MIGRATIONS) { const columns = db.prepare(`PRAGMA table_info(${table})`).all(); if (!columns.some((c) => c.name === column)) db.exec(ddl); } } // Data migrations for the 2026-07 entry-type rework (hotel/travel/immigration // removed in favour of stay/transport/activity). Naturally idempotent: after // the first run no rows of the legacy types remain, so re-running is a no-op. export function applyDataMigrations(db) { db.exec("UPDATE entries SET type = 'stay' WHERE type = 'hotel'"); db.exec("UPDATE entries SET type = 'transport' WHERE type = 'travel'"); db.exec("UPDATE entries SET type = 'activity', title = '🛂 ' || title WHERE type = 'immigration'"); } // Open (or create) the SQLite database at dbPath and ensure the schema exists. export function openDb(dbPath) { const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(SCHEMA); applyMigrations(db); applyDataMigrations(db); return db; }