/** * SQL Query Constants * * All SQL queries used in the application. * Repositories reference these by name. * * All queries use parameterized statements (?) for security. * User input NEVER goes into the SQL string. */ // ── Holdings Table Queries ─────────────────────────────────────────────────── export const HOLDINGS_QUERIES = { // Check if any holdings exist for a user EXISTS: 'SELECT COUNT(*) AS n FROM holdings WHERE user_id = ?', // Get all holdings for a user, sorted by ticker SELECT_ALL: ` SELECT ticker, shares, cost_basis, type, source FROM holdings WHERE user_id = ? ORDER BY ticker ASC `, // Insert or update a holding scoped to a user UPSERT: ` INSERT INTO holdings (ticker, shares, cost_basis, type, source, user_id) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(ticker, user_id) DO UPDATE SET shares = excluded.shares, cost_basis = excluded.cost_basis, type = excluded.type, source = excluded.source `, // Delete a holding by ticker for a specific user DELETE_BY_TICKER: 'DELETE FROM holdings WHERE ticker = ? AND user_id = ?', // Migrate ownerless holdings to admin user (one-time) MIGRATE_TO_ADMIN: "UPDATE holdings SET user_id = ? WHERE user_id IS NULL OR user_id = ''", }; // ── Market Calls Table Queries ─────────────────────────────────────────────── export const MARKET_CALLS_QUERIES = { // Get all market calls, newest first SELECT_ALL: ` SELECT id, title, quarter, date, thesis, tickers, snapshot, created_at FROM market_calls ORDER BY created_at DESC `, // Get a single market call by ID SELECT_BY_ID: ` SELECT id, title, quarter, date, thesis, tickers, snapshot, created_at FROM market_calls WHERE id = ? `, // Insert a new market call INSERT: ` INSERT INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, // Delete a market call by ID DELETE_BY_ID: 'DELETE FROM market_calls WHERE id = ?', }; // ── Migration Queries (for DatabaseInitializer) ────────────────────────────── export const MIGRATION_QUERIES = { // Insert holdings during migration HOLDINGS_INSERT_OR_IGNORE: ` INSERT OR IGNORE INTO holdings (ticker, shares, cost_basis, type, source, user_id) VALUES (?, ?, ?, ?, ?, ?) `, // Insert market calls during migration MARKET_CALLS_INSERT_OR_IGNORE: ` INSERT OR IGNORE INTO market_calls (id, title, quarter, date, thesis, tickers, snapshot, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, }; // ── User Table Queries ─────────────────────────────────────────────────────── export const USER_QUERIES = { SELECT_BY_EMAIL: ` SELECT id, email, password_hash, role, created_at, last_login FROM users WHERE email = ? `, SELECT_BY_ID: ` SELECT id, email, role, created_at, last_login FROM users WHERE id = ? `, INSERT: ` INSERT INTO users (id, email, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?) `, UPDATE_LAST_LOGIN: ` UPDATE users SET last_login = ? WHERE id = ? `, }; // ── Password Reset Token Queries ───────────────────────────────────────────── export const RESET_TOKEN_QUERIES = { INSERT: ` INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?) `, FIND: ` SELECT token, user_id, expires_at, used FROM password_reset_tokens WHERE token = ? `, MARK_USED: ` UPDATE password_reset_tokens SET used = 1 WHERE token = ? `, // Clean up expired/used tokens older than 24h PURGE: ` DELETE FROM password_reset_tokens WHERE used = 1 OR expires_at < ? `, }; // ── Schema Definition (DDL) ────────────────────────────────────────────────── // ── Watchlist Queries ──────────────────────────────────────────────────────── export const WATCHLIST_QUERIES = { SELECT_ALL: ` SELECT ticker, pinned_at FROM watchlist WHERE user_id = ? ORDER BY pinned_at DESC `, INSERT: ` INSERT OR IGNORE INTO watchlist (ticker, user_id, pinned_at) VALUES (?, ?, ?) `, DELETE: ` DELETE FROM watchlist WHERE ticker = ? AND user_id = ? `, EXISTS: ` SELECT 1 FROM watchlist WHERE ticker = ? AND user_id = ? `, }; // ── Screening Universe Queries (bin/daily-screen.ts) ──────────────────────── export const UNIVERSE_QUERIES = { // Every ticker pinned by any user DISTINCT_WATCHLIST_TICKERS: 'SELECT DISTINCT ticker FROM watchlist ORDER BY ticker', // Every ticker held by any user (crypto excluded — not fundamentally scored) DISTINCT_HOLDING_TICKERS: ` SELECT DISTINCT ticker FROM holdings WHERE type != 'crypto' ORDER BY ticker `, // Every ticker screened recently (snapshot ledger) — part of the news universe DISTINCT_SNAPSHOT_TICKERS_SINCE: ` SELECT DISTINCT ticker FROM signal_snapshots WHERE snapshot_date >= ? ORDER BY ticker `, }; // ── News Queries (FREE-DATA-STACK §2–5 — free-tier news pipeline) ─────────── export const NEWS_QUERIES = { // INSERT OR IGNORE — url_hash PK is the first dedupe line (returns 0 changes on dup) INSERT_ARTICLE: ` INSERT OR IGNORE INTO news_articles (url_hash, title_hash, ticker_list, headline, body, source, catalyst, url, published_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, // Second dedupe line: same (normalized) title seen recently → syndicated copy TITLE_SEEN_SINCE: ` SELECT 1 FROM news_articles WHERE title_hash = ? AND published_at >= ? LIMIT 1 `, INSERT_CATALYST_LINK: ` INSERT OR IGNORE INTO ticker_catalysts (ticker, day, url_hash) VALUES (?, ?, ?) `, // Per-ticker daily cap check (FREE-DATA-STACK §4.4) COUNT_TICKER_DAY: ` SELECT COUNT(*) AS n FROM ticker_catalysts WHERE ticker = ? AND day = ? `, // Stories for one ticker since a given day — what the UI reads (never Yahoo live) SELECT_TICKER_NEWS: ` SELECT a.* FROM ticker_catalysts c JOIN news_articles a ON a.url_hash = c.url_hash WHERE c.ticker = ? AND c.day >= ? ORDER BY a.published_at DESC `, SELECT_RECENT: ` SELECT * FROM news_articles ORDER BY published_at DESC LIMIT ? `, // Retention (FREE-DATA-STACK §5): purge bodies after 90d, drop unreferenced after 18mo PURGE_BODIES_BEFORE: ` UPDATE news_articles SET body = NULL WHERE body IS NOT NULL AND published_at < ? `, DELETE_UNREFERENCED_BEFORE: ` DELETE FROM news_articles WHERE published_at < ? AND url_hash NOT IN (SELECT url_hash FROM ticker_catalysts) `, }; // ── Signal Snapshot Queries (P0.1 — signal track record) ──────────────────── export const SIGNAL_SNAPSHOT_QUERIES = { // One row per ticker per day — repeated screens the same day keep the latest UPSERT: ` INSERT INTO signal_snapshots ( ticker, snapshot_date, asset_type, price, signal, fundamental_tier, fundamental_score, fundamental_label, inflated_tier, inflated_score, inflated_label, coverage_active, coverage_total, risk_flags, rate_regime, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(ticker, snapshot_date) DO UPDATE SET asset_type = excluded.asset_type, price = excluded.price, signal = excluded.signal, fundamental_tier = excluded.fundamental_tier, fundamental_score = excluded.fundamental_score, fundamental_label = excluded.fundamental_label, inflated_tier = excluded.inflated_tier, inflated_score = excluded.inflated_score, inflated_label = excluded.inflated_label, coverage_active = excluded.coverage_active, coverage_total = excluded.coverage_total, risk_flags = excluded.risk_flags, rate_regime = excluded.rate_regime, created_at = excluded.created_at `, // Full history for one ticker, oldest first (for trend/backtest views) SELECT_BY_TICKER: ` SELECT * FROM signal_snapshots WHERE ticker = ? ORDER BY snapshot_date ASC `, // All snapshots for one day (for daily diff jobs) SELECT_BY_DATE: ` SELECT * FROM signal_snapshots WHERE snapshot_date = ? ORDER BY ticker ASC `, // Latest snapshot per ticker on or before a given date (for change detection) SELECT_LATEST_BEFORE: ` SELECT s.* FROM signal_snapshots s JOIN ( SELECT ticker, MAX(snapshot_date) AS d FROM signal_snapshots WHERE snapshot_date < ? GROUP BY ticker ) latest ON latest.ticker = s.ticker AND latest.d = s.snapshot_date `, }; export const DDL = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('trader', 'viewer', 'admin')), created_at TEXT NOT NULL, last_login TEXT ); CREATE TABLE IF NOT EXISTS holdings ( ticker TEXT NOT NULL, user_id TEXT NOT NULL REFERENCES users(id), shares REAL NOT NULL, cost_basis REAL NOT NULL DEFAULT 0, type TEXT NOT NULL DEFAULT 'stock', source TEXT NOT NULL DEFAULT 'Manual', PRIMARY KEY (ticker, user_id) ); CREATE TABLE IF NOT EXISTS password_reset_tokens ( token TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), expires_at TEXT NOT NULL, used INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS market_calls ( id TEXT PRIMARY KEY, title TEXT NOT NULL, quarter TEXT NOT NULL, date TEXT NOT NULL, thesis TEXT NOT NULL, tickers TEXT NOT NULL, -- JSON array snapshot TEXT NOT NULL, -- JSON object created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS watchlist ( ticker TEXT NOT NULL, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, pinned_at TEXT NOT NULL, PRIMARY KEY (ticker, user_id) ); CREATE TABLE IF NOT EXISTS signal_snapshots ( ticker TEXT NOT NULL, snapshot_date TEXT NOT NULL, -- YYYY-MM-DD asset_type TEXT NOT NULL, -- STOCK / ETF / BOND price REAL, signal TEXT NOT NULL, -- ✅ Strong Buy etc. fundamental_tier TEXT NOT NULL, -- PASS / HOLD / REJECT fundamental_score REAL, fundamental_label TEXT, inflated_tier TEXT NOT NULL, inflated_score REAL, inflated_label TEXT, coverage_active INTEGER, coverage_total INTEGER, risk_flags TEXT, -- JSON array rate_regime TEXT, created_at TEXT NOT NULL, PRIMARY KEY (ticker, snapshot_date) ); CREATE INDEX IF NOT EXISTS idx_snapshots_date ON signal_snapshots(snapshot_date); CREATE INDEX IF NOT EXISTS idx_snapshots_signal ON signal_snapshots(signal, snapshot_date); CREATE TABLE IF NOT EXISTS news_articles ( url_hash TEXT PRIMARY KEY, -- sha256(url) title_hash TEXT NOT NULL, -- sha256(normalized headline) — syndication dedupe ticker_list TEXT NOT NULL, -- JSON array of matched universe tickers headline TEXT NOT NULL, body TEXT, -- nullable; purged after 90 days (retention job) source TEXT NOT NULL, -- 'edgar' | 'prwire' | 'yahoo' catalyst TEXT, -- 'earnings'|'ma'|'guidance'|'regulatory'|'macro'|NULL url TEXT NOT NULL, published_at TEXT NOT NULL, -- ISO timestamp created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_news_published ON news_articles(published_at DESC); CREATE INDEX IF NOT EXISTS idx_news_title ON news_articles(title_hash, published_at); CREATE TABLE IF NOT EXISTS ticker_catalysts ( ticker TEXT NOT NULL, day TEXT NOT NULL, -- YYYY-MM-DD (published date) url_hash TEXT NOT NULL REFERENCES news_articles(url_hash), PRIMARY KEY (ticker, day, url_hash) ); CREATE INDEX IF NOT EXISTS idx_catalysts_ticker ON ticker_catalysts(ticker, day DESC); `; // ── Runtime migrations (ALTER TABLE for existing DBs) ──────────────────────── // These are safe to run repeatedly — they no-op if the column already exists. export const RUNTIME_MIGRATIONS = [ // Add user_id to holdings if upgrading from pre-auth schema `ALTER TABLE holdings ADD COLUMN user_id TEXT NOT NULL DEFAULT '' REFERENCES users(id)`, ];