using System.Data; using Dapper; namespace QuantEngine.Infrastructure.Data { public class DbMigrator { private readonly IDbConnectionFactory _connectionFactory; public DbMigrator(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } public void Migrate() { using var conn = _connectionFactory.CreateConnection(); conn.Open(); // Create schema if not exists conn.Execute("CREATE SCHEMA IF NOT EXISTS quantengine;"); // 0. kis_tokens conn.Execute(@" CREATE TABLE IF NOT EXISTS kis_tokens ( account TEXT PRIMARY KEY, access_token TEXT NOT NULL, expires_at TEXT NOT NULL, updated_at TEXT NOT NULL ); "); // 1. collection_runs conn.Execute(@" CREATE TABLE IF NOT EXISTS collection_runs ( run_id TEXT PRIMARY KEY, collector_name TEXT NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, input_source TEXT, output_json_path TEXT, output_db_path TEXT, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); "); // 2. collection_snapshots conn.Execute(@" CREATE TABLE IF NOT EXISTS collection_snapshots ( run_id TEXT NOT NULL, dataset_name TEXT NOT NULL, ticker TEXT NOT NULL, name TEXT, sector TEXT, as_of_date TEXT, source_priority TEXT, source_status TEXT, payload_json TEXT NOT NULL, provenance_json TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (run_id, dataset_name, ticker) ); CREATE INDEX IF NOT EXISTS idx_collection_snapshots_ticker_time ON collection_snapshots(ticker, created_at DESC); "); // 3. collection_source_errors conn.Execute(@" CREATE TABLE IF NOT EXISTS collection_source_errors ( run_id TEXT NOT NULL, ticker TEXT, source_name TEXT NOT NULL, error_kind TEXT NOT NULL, error_message TEXT NOT NULL, payload_json TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_collection_source_errors_run ON collection_source_errors(run_id, source_name); "); // 4. settings conn.Execute(@" CREATE TABLE IF NOT EXISTS settings ( ordinal INT NOT NULL, key TEXT PRIMARY KEY, value_json TEXT NOT NULL, note TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL ); "); // 5. account_snapshot conn.Execute(@" CREATE TABLE IF NOT EXISTS account_snapshot ( ordinal INT NOT NULL, row_json TEXT NOT NULL, captured_at TEXT NOT NULL DEFAULT '', account TEXT NOT NULL DEFAULT '', account_type TEXT NOT NULL DEFAULT '', ticker TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', parse_status TEXT NOT NULL DEFAULT '', user_confirmed TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_account_snapshot_captured_at ON account_snapshot(captured_at); CREATE INDEX IF NOT EXISTS idx_account_snapshot_ticker ON account_snapshot(ticker); "); // 6. workspace_meta conn.Execute(@" CREATE TABLE IF NOT EXISTS workspace_meta ( key TEXT PRIMARY KEY, value_json TEXT NOT NULL ); "); // 7. workspace_change_log conn.Execute(@" CREATE TABLE IF NOT EXISTS workspace_change_log ( id SERIAL PRIMARY KEY, domain TEXT NOT NULL, action TEXT NOT NULL, target_ref TEXT NOT NULL DEFAULT '', actor TEXT NOT NULL DEFAULT 'system', note TEXT NOT NULL DEFAULT '', before_json TEXT NOT NULL DEFAULT 'null', after_json TEXT NOT NULL DEFAULT 'null', created_at TEXT NOT NULL ); "); // 8. workspace_approval_v2 conn.Execute(@" CREATE TABLE IF NOT EXISTS workspace_approval_v2 ( domain TEXT NOT NULL, target_ref TEXT NOT NULL DEFAULT '*', status TEXT NOT NULL, approved_by TEXT NOT NULL DEFAULT '', approved_at TEXT NOT NULL DEFAULT '', note TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL, PRIMARY KEY (domain, target_ref) ); "); // 9. workspace_lock conn.Execute(@" CREATE TABLE IF NOT EXISTS workspace_lock ( domain TEXT NOT NULL, target_ref TEXT NOT NULL DEFAULT '', locked_by TEXT NOT NULL DEFAULT '', reason TEXT NOT NULL DEFAULT '', locked_at TEXT NOT NULL, PRIMARY KEY (domain, target_ref) ); "); // 10. engine_history schema and tables conn.Execute(@" CREATE SCHEMA IF NOT EXISTS engine_history; "); conn.Execute(@" CREATE TABLE IF NOT EXISTS engine_history.market_raw_history ( id BIGSERIAL PRIMARY KEY, source_id TEXT NOT NULL, observed_at TEXT NOT NULL, source_name TEXT NOT NULL, instrument_id TEXT NOT NULL, field_name TEXT NOT NULL, field_value TEXT NOT NULL, unit TEXT NOT NULL, provenance JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_market_raw_history_created_at ON engine_history.market_raw_history (created_at DESC); "); conn.Execute(@" CREATE TABLE IF NOT EXISTS engine_history.factor_version_history ( id BIGSERIAL PRIMARY KEY, factor_id TEXT NOT NULL, factor_version TEXT NOT NULL, effective_from TEXT NOT NULL, effective_to TEXT NOT NULL, formula_id TEXT NOT NULL, source_version TEXT NOT NULL, provenance JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_factor_version_history_created_at ON engine_history.factor_version_history (created_at DESC); "); conn.Execute(@" CREATE TABLE IF NOT EXISTS engine_history.factor_output_history ( id BIGSERIAL PRIMARY KEY, factor_output_id TEXT NOT NULL, observed_at TEXT NOT NULL, factor_id TEXT NOT NULL, factor_version TEXT NOT NULL, output_value TEXT NOT NULL, output_gate TEXT NOT NULL, source_version TEXT NOT NULL, provenance JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_factor_output_history_created_at ON engine_history.factor_output_history (created_at DESC); "); conn.Execute(@" CREATE TABLE IF NOT EXISTS engine_history.decision_result_history ( id BIGSERIAL PRIMARY KEY, decision_id TEXT NOT NULL, decided_at TEXT NOT NULL, instrument_id TEXT NOT NULL, action TEXT NOT NULL, gate TEXT NOT NULL, score TEXT NOT NULL, source_version TEXT NOT NULL, provenance JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_decision_result_history_created_at ON engine_history.decision_result_history (created_at DESC); "); conn.Execute(@" CREATE TABLE IF NOT EXISTS engine_history.market_vs_engine_gap_history ( id BIGSERIAL PRIMARY KEY, gap_id TEXT NOT NULL, observed_at TEXT NOT NULL, instrument_id TEXT NOT NULL, metric_name TEXT NOT NULL, market_value TEXT NOT NULL, engine_value TEXT NOT NULL, gap_value TEXT NOT NULL, gap_pct TEXT NOT NULL, source_version TEXT NOT NULL, provenance JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_market_vs_engine_gap_history_created_at ON engine_history.market_vs_engine_gap_history (created_at DESC); "); } } }