v16 is a purely additive, guarded, atomic data migration (Bilan détail par
titre, Étape 2). It converts each existing single-security priced account into
a detailed account holding exactly one position, with zero data loss.
1. Mints one shared balance_securities row per priced account symbol
(normalized UPPER(TRIM), deduped via ON CONFLICT(symbol) DO NOTHING on the
COLLATE NOCASE UNIQUE), ONLY for accounts whose category carries a real
asset_type — balance_securities.asset_type is NOT NULL, and a priced
account whose category has asset_type IS NULL has no valid routing.
2. Mirrors each existing priced line into one holding (qty/unit_price/value/
price_source/price_fetched_at copied; book_cost stays NULL — no
retroactive acquisition cost). UNIQUE(line, security) + ON CONFLICT DO
NOTHING makes a re-run a strict no-op.
3. Collapses the now-redundant per-line qty/unit_price to NULL ONLY where a
holding now exists (the security fix — a line that got no holding, i.e.
priced-without-asset_type, is never NULLed, so no silent data loss).
NULLing both columns together preserves the lines' (both NULL | both NOT
NULL) CHECK.
A trailing TEMP-table CHECK(ok = 1) asserts the invariant 'qty NULLed ⇒ has a
holding' and ABORTS on breach, rolling back the whole v16 transaction (sqlx
wraps each migration in a transaction). Priced accounts without asset_type or
without a symbol are left fully intact.
Integration tests (in-memory SQLite, apply v10→v16 via execute_batch, mirroring
the #210 migration-test style): convertible account gains a security + holding
with values/history preserved and its line qty NULLed; non-convertible priced
account untouched (qty intact, no holding); re-run idempotent; injected-failure
mid-v16 aborts on the guard and a transaction rollback restores the pre-v16
state (zero securities/holdings, quantity intact).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>