-- Balance sheet — détail par titre (Bilan Étape 2) — Migration v14 -- Created: 2026-06-06 -- Issue: #210 (Bilan détail #1 — schema & migrations v14/v15 + types) -- -- Purely additive: two new tables enabling a single account to hold many -- securities at a given snapshot date instead of one denormalized value. -- Conventions aligned with balance_schema.sql / consolidated_schema.sql: -- - INTEGER PRIMARY KEY AUTOINCREMENT -- - REAL for monetary amounts / quantities (matches transactions.amount) -- - snake_case -- - FK with explicit ON DELETE policies -- - DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP for timestamps -- -- Design notes (spec-decisions-bilan-detail-titres.md + review caveats): -- - balance_securities.symbol is the natural key, normalized upper/trim by -- the service layer; COLLATE NOCASE UNIQUE prevents case-duplicates -- ('aapl' vs 'AAPL') at the SQL level (SEC/ARCH review caveat). -- - balance_snapshot_holdings references balance_snapshot_lines(id) rather -- than (snapshot_id, account_id): the lines table already guarantees one -- row per (snapshot, account) via UNIQUE(snapshot_id, account_id), so the -- line id uniquely identifies that pair (ARCH review: keep the line FK). -- - value is denormalized (= quantity * unit_price) so reports stay -- reproducible without re-fetching prices — same rationale as -- balance_snapshot_lines.value. -- ========================================================================= -- balance_securities — catalogue of investable instruments (stock | crypto) -- ========================================================================= -- One row per security/coin the user holds in a detailed account. `symbol` is -- stored normalized (upper/trim) with COLLATE NOCASE so duplicates differing -- only by case are impossible. `asset_type` mirrors balance_categories so the -- price-fetch flow can route per security. CREATE TABLE IF NOT EXISTS balance_securities ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT NOT NULL COLLATE NOCASE UNIQUE, name TEXT, currency TEXT NOT NULL DEFAULT 'CAD', asset_type TEXT NOT NULL CHECK (asset_type IN ('stock','crypto')), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- ========================================================================= -- balance_snapshot_holdings — one row per (snapshot line, security) -- ========================================================================= -- The per-title breakdown of a detailed account's snapshot line. CASCADE on -- snapshot_line_id wipes holdings when the parent line is removed; RESTRICT on -- security_id blocks deleting a security still referenced by history. `value` -- is stored denormalized (= quantity * unit_price) for reproducible reports. CREATE TABLE IF NOT EXISTS balance_snapshot_holdings ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_line_id INTEGER NOT NULL REFERENCES balance_snapshot_lines(id) ON DELETE CASCADE, security_id INTEGER NOT NULL REFERENCES balance_securities(id) ON DELETE RESTRICT, quantity REAL NOT NULL, unit_price REAL NOT NULL, value REAL NOT NULL, book_cost REAL, price_source TEXT, price_fetched_at DATETIME, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (snapshot_line_id, security_id) ); CREATE INDEX IF NOT EXISTS idx_balance_snapshot_holdings_line ON balance_snapshot_holdings(snapshot_line_id); CREATE INDEX IF NOT EXISTS idx_balance_snapshot_holdings_security ON balance_snapshot_holdings(security_id);