// Generated, do not edit. sha256:40ff653075fba536c72875e2aaa7a896d17bbea56edc4438b0f2dcada32718e2 import { CONFIG } from '../../shared/config.js'; import { run, one, all } from './conn.mjs'; export const BASELINE_VERSION = 60; const MIGRATIONS = { 61: () => { run(`CREATE TABLE AiToolCall ( id INTEGER PRIMARY KEY, sessionId TEXT NOT NULL, client TEXT, tool TEXT NOT NULL, arguments TEXT, fieldsShared TEXT, charactersOut INTEGER CHECK (charactersOut IS NULL OR charactersOut >= 0), outcome TEXT NOT NULL CHECK (outcome IN ('ok','error','refused')), detail TEXT, occurredAt TEXT NOT NULL ) STRICT;`); run(`CREATE INDEX IxAiToolCallOccurredAt ON AiToolCall(occurredAt);`); }, 62: () => { run(`ALTER TABLE AiToolCall ADD COLUMN serverVersion TEXT`); }, 63: () => { run(`CREATE TABLE AiToolCallMessage ( id INTEGER PRIMARY KEY, callId INTEGER NOT NULL REFERENCES AiToolCall(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('request','response')), text TEXT NOT NULL, occurredAt TEXT NOT NULL ) STRICT;`); run(`CREATE INDEX IxAiToolCallMessageCallId ON AiToolCallMessage(callId);`); }, 64: () => { run(`CREATE TABLE BusSignal ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, occurredAt TEXT NOT NULL ) STRICT;`); }, 65: () => { run(`CREATE TABLE Command ( id INTEGER PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('navigate')), payload TEXT, state TEXT NOT NULL CHECK (state IN ('open','claimed','done','rejected','expired')), claimedBy TEXT, claimedAt TEXT, expiresAt TEXT, detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL ) STRICT;`); run(`UPDATE Preference SET value = '5' WHERE scope = 'timers' AND key = 'bus_poll_seconds' AND value = '15'`); }, 66: () => { run(`ALTER TABLE Command RENAME TO CommandOud`); run(`CREATE TABLE Command ( id INTEGER PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('navigate','locate')), payload TEXT, state TEXT NOT NULL CHECK (state IN ('open','claimed','done','rejected','expired')), claimedBy TEXT, claimedAt TEXT, expiresAt TEXT, detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL ) STRICT;`); run(`INSERT INTO Command (id, kind, payload, state, claimedBy, claimedAt, expiresAt, detail, resolvedAt, createdAt) SELECT id, kind, payload, state, claimedBy, claimedAt, expiresAt, detail, resolvedAt, createdAt FROM CommandOud`); run(`DROP TABLE CommandOud`); run(`ALTER TABLE Notification RENAME TO NotificationOud`); run(`CREATE TABLE Notification ( id INTEGER PRIMARY KEY, count INTEGER NOT NULL CHECK (count >= 0), highWater INTEGER NOT NULL CHECK (highWater >= 0), kind TEXT NOT NULL CHECK (kind IN ('reconcile','interest','valuation','schedule_ended','source_proposal','ai_command')), dismissedAt TEXT, createdAt TEXT NOT NULL ) STRICT;`); run(`INSERT INTO Notification (id, count, highWater, kind, dismissedAt, createdAt) SELECT id, count, highWater, kind, dismissedAt, createdAt FROM NotificationOud`); run(`DROP TABLE NotificationOud`); run(`CREATE INDEX IxNotificationKind ON Notification(kind)`); }, 67: () => { run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point')), eur REAL CHECK (eur IS NULL OR eur > 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)) ) STRICT;`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); }, 68: () => { const telAsset = one(`SELECT COUNT(*) AS n FROM Asset WHERE decimals > 9`).n; if (telAsset > 0) { throw new Error( `Migration 68 cannot run: ${telAsset === 1 ? 'there is 1 asset' : `there are ${telAsset} assets`} with ` + `more than 9 decimals. Such an asset is unusable (an amount above 0.009 unit can no longer be read ` + `back) and the new schema forbids it. Change the decimals of that asset to at most 9, or remove it, ` + `and start again.`); } const telGeld = one(`SELECT COUNT(*) AS n FROM Event WHERE eurPrice < 0 OR eurFee < 0 OR unitFee < 0`).n; if (telGeld > 0) { throw new Error( `Migration 68 cannot run: ${telGeld === 1 ? 'there is 1 event' : `there are ${telGeld} events`} with a ` + `negative eurPrice, eurFee or unitFee. The new schema forbids that, because a negative fee raises ` + `the balance. Correct those events and start again.`); } run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Asset RENAME TO AssetOud`); run(`CREATE TABLE Asset ( id INTEGER PRIMARY KEY, symbol TEXT NOT NULL, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, unit TEXT NOT NULL, subunit TEXT, displayUnit TEXT NOT NULL CHECK (displayUnit IN ('unit','subunit')), decimals INTEGER NOT NULL CHECK (decimals >= 0 AND decimals <= 9), kind TEXT NOT NULL CHECK (kind IN ('crypto','metal')), UNIQUE (symbol), CHECK (displayUnit <> 'subunit' OR subunit IS NOT NULL) ) STRICT;`); run(`INSERT INTO Asset (id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind) SELECT id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind FROM AssetOud`); run(`DROP TABLE AssetOud`); run(`CREATE INDEX IxAssetSearchName ON Asset(searchName)`); run(`ALTER TABLE Event RENAME TO EventOud`); run(`CREATE TABLE Event ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, toAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, productId INTEGER REFERENCES MetalProduct(id) ON DELETE NO ACTION, productCount INTEGER CHECK (productCount IS NULL OR productCount > 0), importReference TEXT, externalAddress TEXT, note TEXT, amount INTEGER NOT NULL CHECK (amount > 0), eurPrice REAL CHECK (eurPrice IS NULL OR eurPrice >= 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), unitFee INTEGER CHECK (unitFee IS NULL OR unitFee >= 0), type TEXT NOT NULL CHECK (type IN ('buy','sell','transfer','interest')), source TEXT NOT NULL CHECK (source IN ('manual','import','reconcile','lightning')), occurredAt TEXT NOT NULL, createdAt TEXT NOT NULL, CHECK ((productId IS NULL) = (productCount IS NULL)), CHECK (type NOT IN ('buy','sell') OR eurPrice IS NOT NULL) ) STRICT;`); run(`INSERT INTO Event (id, assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress, note, amount, eurPrice, eurFee, unitFee, type, source, occurredAt, createdAt) SELECT id, assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress, note, amount, eurPrice, eurFee, unitFee, type, source, occurredAt, createdAt FROM EventOud`); run(`DROP TABLE EventOud`); run(`CREATE INDEX IxEventAssetId ON Event(assetId)`); run(`CREATE INDEX IxEventFromAccountId ON Event(fromAccountId)`); run(`CREATE INDEX IxEventToAccountId ON Event(toAccountId)`); run(`CREATE INDEX IxEventProductId ON Event(productId)`); run(`CREATE INDEX IxEventOccurredAt ON Event(occurredAt)`); run(`CREATE INDEX IxEventType ON Event(type)`); run(`CREATE INDEX IxEventExternalAddress ON Event(externalAddress)`); run(`CREATE INDEX IxEventImportReference ON Event(importReference)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AssetOud%' OR sql LIKE '%EventOud%'`); if (besmet.length) { throw new Error( `Migration 68 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The rebuild rewrote the foreign keys of other tables; the step was rolled back ` + `and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 69: () => { run(`DELETE FROM Notification WHERE kind = 'ai_command'`); run(`ALTER TABLE Notification RENAME TO NotificationOud`); run(`CREATE TABLE Notification ( id INTEGER PRIMARY KEY, count INTEGER NOT NULL CHECK (count >= 0), highWater INTEGER NOT NULL CHECK (highWater >= 0), kind TEXT NOT NULL CHECK (kind IN ('reconcile','interest','valuation','schedule_ended','source_proposal','ai_proposal')), dismissedAt TEXT, createdAt TEXT NOT NULL ) STRICT;`); run(`INSERT INTO Notification (id, count, highWater, kind, dismissedAt, createdAt) SELECT id, count, highWater, kind, dismissedAt, createdAt FROM NotificationOud`); run(`DROP TABLE NotificationOud`); run(`CREATE INDEX IxNotificationKind ON Notification(kind)`); }, 70: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Asset RENAME TO AssetOud`); run(`CREATE TABLE Asset ( id INTEGER PRIMARY KEY, symbol TEXT NOT NULL, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, unit TEXT NOT NULL, subunit TEXT, displayUnit TEXT NOT NULL CHECK (displayUnit IN ('unit','subunit')), decimals INTEGER NOT NULL CHECK (decimals >= 0 AND decimals <= 9), kind TEXT NOT NULL CHECK (kind IN ('crypto','metal','fiat')), UNIQUE (symbol), CHECK (displayUnit <> 'subunit' OR subunit IS NOT NULL) ) STRICT;`); run(`INSERT INTO Asset (id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind) SELECT id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind FROM AssetOud`); run(`DROP TABLE AssetOud`); run(`CREATE INDEX IxAssetSearchName ON Asset(searchName)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AssetOud%'`); if (besmet.length) { throw new Error( `Migration 70 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The rebuild rewrote the foreign keys of other tables; the step was rolled back ` + `and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 71: () => { run(`CREATE TABLE ProviderFixedRate ( providerId INTEGER PRIMARY KEY REFERENCES Provider(id) ON DELETE CASCADE, eur REAL NOT NULL CHECK (eur > 0) ) STRICT;`); const nu = new Date().toISOString(); if (!one(`SELECT 1 AS x FROM Provider WHERE key = 'fixed'`)) { run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationHeaderName, authenticationParameterName, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('fixed', 'Vaste koers', 'vaste koers', '', NULL, NULL, 'none', 0, 1, 1, 1, ?)`, [nu]); const providerId = one(`SELECT id FROM Provider WHERE key = 'fixed'`).id; run(`INSERT INTO ProviderFixedRate (providerId, eur) VALUES (?, 1.0)`, [providerId]); run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?, 'EUR')`, [providerId]); for (const capability of ['spot', 'history_point']) { run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate) VALUES (?, ?, 'GET', '', NULL)`, [providerId, capability]); } } if (!one(`SELECT 1 AS x FROM Asset WHERE symbol = 'EUR'`)) { run(`INSERT INTO Asset (symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind) VALUES ('EUR', 'Euro', 'euro', NULL, 'EUR', 'cent', 'unit', 2, 'fiat')`); } const assetId = one(`SELECT id FROM Asset WHERE symbol = 'EUR'`).id; const providerId = one(`SELECT id FROM Provider WHERE key = 'fixed'`).id; for (const [consumerKey, capability] of [ ['converter', 'spot'], ['fiat', 'spot'], ['fiat', 'history_point'], ['tax', 'spot'], ['tax', 'history_point'], ['valuations', 'history_point'], ]) { if (one(`SELECT 1 AS x FROM ConsumerSource WHERE consumerKey = ? AND assetId = ? AND capability = ?`, [consumerKey, assetId, capability])) continue; run(`INSERT INTO ConsumerSource (consumerKey, assetId, capability, providerId, rank, status) VALUES (?, ?, ?, ?, 1, 'confirmed')`, [consumerKey, assetId, capability, providerId]); } }, 72: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Asset RENAME TO AssetOud`); run(`CREATE TABLE Asset ( id INTEGER PRIMARY KEY, symbol TEXT NOT NULL, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, unit TEXT NOT NULL, subunit TEXT, displayUnit TEXT NOT NULL CHECK (displayUnit IN ('unit','subunit')), decimals INTEGER NOT NULL CHECK (decimals >= 0 AND decimals <= 9), kind TEXT NOT NULL CHECK (kind IN ('crypto','metal','fiat')), taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig','buiten')), UNIQUE (symbol), CHECK (displayUnit <> 'subunit' OR subunit IS NOT NULL) ) STRICT;`); run(`INSERT INTO Asset (id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind) SELECT id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind FROM AssetOud`); run(`DROP TABLE AssetOud`); run(`CREATE INDEX IxAssetSearchName ON Asset(searchName)`); run(`ALTER TABLE Account RENAME TO AccountOud`); run(`CREATE TABLE Account ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, kind TEXT NOT NULL CHECK (kind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER NOT NULL CHECK (isOwnCustody IN (0,1)), scanEnabled INTEGER NOT NULL CHECK (scanEnabled IN (0,1)), isActive INTEGER NOT NULL CHECK (isActive IN (0,1)), closedAt TEXT, taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig','buiten')), CHECK ((isActive = 1 AND closedAt IS NULL) OR (isActive = 0 AND closedAt IS NOT NULL)) ) STRICT;`); run(`INSERT INTO Account (id, name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt) SELECT id, name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt FROM AccountOud`); run(`DROP TABLE AccountOud`); run(`CREATE INDEX IxAccountSearchName ON Account(searchName)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AssetOud%' OR sql LIKE '%AccountOud%'`); if (besmet.length) { throw new Error( `Migration 72 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The rebuild rewrote the foreign keys of other tables; the step was rolled back ` + `and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 73: () => { run(`DELETE FROM DashboardWidget WHERE type = 'dbstatus'`); }, 74: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Valuation RENAME TO ValuationOud`); run(`CREATE TABLE Valuation ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, balance INTEGER NOT NULL, eurRate REAL NOT NULL, source TEXT NOT NULL CHECK (source IN ('onchain','manual','statement')), valuedOn TEXT NOT NULL, rateAt TEXT NOT NULL, gapAcceptedAt TEXT, UNIQUE (assetId, valuedOn, accountId) ) STRICT;`); run(`INSERT INTO Valuation (id, assetId, accountId, balance, eurRate, source, valuedOn, rateAt) SELECT id, assetId, accountId, balance, eurRate, source, valuedOn, rateAt FROM ValuationOud`); run(`DROP TABLE ValuationOud`); run(`CREATE INDEX IxValuationAccountId ON Valuation(accountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%ValuationOud%'`); if (besmet.length) { throw new Error( `Migration 74 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 75: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE InterestSchedule RENAME TO InterestScheduleOud`); run(`CREATE TABLE InterestSchedule ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, principalAssetId INTEGER REFERENCES Asset(id) ON DELETE NO ACTION, payoutAssetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, note TEXT, principalAmount INTEGER CHECK (principalAmount IS NULL OR principalAmount > 0), annualRate REAL CHECK (annualRate IS NULL OR annualRate >= 0), rateKind TEXT NOT NULL CHECK (rateKind IN ('fixed','variable')), creditCadence TEXT CHECK (creditCadence IS NULL OR creditCadence IN ('term_end','annual','quarterly','monthly','weekly')), creditWeekday INTEGER CHECK (creditWeekday IS NULL OR creditWeekday BETWEEN 0 AND 6), creditDayOfMonth INTEGER CHECK (creditDayOfMonth IS NULL OR creditDayOfMonth BETWEEN 1 AND 31), startsOn TEXT NOT NULL, endsOn TEXT, createdAt TEXT NOT NULL, CHECK (rateKind = 'fixed' OR (principalAmount IS NULL AND annualRate IS NULL)), CHECK (creditWeekday IS NULL OR creditCadence = 'weekly'), CHECK (creditDayOfMonth IS NULL OR creditCadence = 'monthly'), CHECK (endsOn IS NULL OR endsOn >= startsOn) ) STRICT;`); run(`INSERT INTO InterestSchedule (id, accountId, principalAssetId, payoutAssetId, note, principalAmount, annualRate, rateKind, creditCadence, creditWeekday, creditDayOfMonth, startsOn, endsOn, createdAt) SELECT id, accountId, principalAssetId, payoutAssetId, note, principalAmount, annualRate, rateKind, creditCadence, creditWeekday, creditDayOfMonth, startsOn, endsOn, createdAt FROM InterestScheduleOud`); run(`DROP TABLE InterestScheduleOud`); run(`CREATE INDEX IxInterestScheduleAccountId ON InterestSchedule(accountId)`); run(`CREATE INDEX IxInterestSchedulePrincipalAssetId ON InterestSchedule(principalAssetId)`); run(`CREATE INDEX IxInterestSchedulePayoutAssetId ON InterestSchedule(payoutAssetId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%InterestScheduleOud%'`); if (besmet.length) { throw new Error( `Migration 75 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 76: () => { run(`CREATE TABLE InterestPeriod ( id INTEGER PRIMARY KEY, scheduleId INTEGER NOT NULL REFERENCES InterestSchedule(id) ON DELETE CASCADE, startsOn TEXT NOT NULL, annualRate REAL NOT NULL CHECK (annualRate >= 0), UNIQUE (scheduleId, startsOn) ) STRICT;`); }, 77: () => { if (!tableExists('Preference')) return; const uit = one(`SELECT value AS v FROM Preference WHERE scope = 'ai' AND key = 'enabled'`); run(`UPDATE Preference SET key = 'allow_read' WHERE scope = 'ai' AND key = 'enabled'`); if (uit && Number(uit.v) === 0) { for (const sleutel of ['allow_write', 'allow_control']) { run(`INSERT INTO Preference (scope, key, value) VALUES ('ai', ?, '0') ON CONFLICT (scope, key) DO UPDATE SET value = '0'`, [sleutel]); } } }, 78: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, kind, eur, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, kind, eur, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 78 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 79: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 79 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 80: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 80 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 81: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 81 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 82: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR name IS NOT NULL) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 82 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 83: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio','preference')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, scope TEXT, key TEXT, value TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR name IS NOT NULL), CHECK (kind <> 'preference' OR (scope IS NOT NULL AND key IS NOT NULL AND value IS NOT NULL)), CHECK (kind = 'preference' OR (scope IS NULL AND key IS NULL AND value IS NULL)) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 83 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 84: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, createdEventId INTEGER REFERENCES Event(id) ON DELETE SET NULL, createdAccountId INTEGER REFERENCES Account(id) ON DELETE SET NULL, createdValuationId INTEGER REFERENCES Valuation(id) ON DELETE SET NULL, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio','preference')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, scope TEXT, key TEXT, value TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR name IS NOT NULL), CHECK (kind <> 'preference' OR (scope IS NOT NULL AND key IS NOT NULL AND value IS NOT NULL)), CHECK (kind = 'preference' OR (scope IS NULL AND key IS NULL AND value IS NULL)) ) STRICT;`); run(`INSERT INTO AiProposal (id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, scope, key, value, note, status, detail, resolvedAt, createdAt) SELECT id, assetId, accountId, fromAccountId, toAccountId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, scope, key, value, note, status, detail, resolvedAt, createdAt FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); run(`CREATE INDEX IxAiProposalCreatedEventId ON AiProposal(createdEventId)`); run(`CREATE INDEX IxAiProposalCreatedAccountId ON AiProposal(createdAccountId)`); run(`CREATE INDEX IxAiProposalCreatedValuationId ON AiProposal(createdValuationId)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`); if (besmet.length) { throw new Error( `Migration 84 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 85: () => { const TIJDSTEMPELS = { createdAt: [ 'Asset', 'Account', 'Label', 'MetalProduct', 'Valuation', 'InterestPeriod', 'WalletAddress', 'ConsumerSource', 'DashboardWidget', 'Preference', 'ProviderAuthenticationHmac', 'ProviderEndpoint', 'ProviderFixedRate', 'ProviderInputMap', 'ProviderOutputMap', 'ProviderCoverage', ], updatedAt: [ 'Account', 'Asset', 'Event', 'Valuation', 'Label', 'MetalProduct', 'InterestSchedule', 'InterestPeriod', 'InterestCredit', 'WalletSource', ], }; for (const [kolom, tabellen] of Object.entries(TIJDSTEMPELS)) { for (const tabel of tabellen) { if (!tableExists(tabel)) continue; if (columnExists(tabel, kolom)) continue; run(`ALTER TABLE ${tabel} ADD COLUMN ${kolom} TEXT`); } } }, 86: () => { if (!tableExists('PriceHistory')) return; if (columnExists('PriceHistory', 'createdAt')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE PriceHistory RENAME TO PriceHistoryOud`); run(`CREATE TABLE PriceHistory ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE RESTRICT, eur REAL, usd REAL, resolution TEXT NOT NULL CHECK (resolution IN ('day','timestamp')), observedAt TEXT NOT NULL, createdAt TEXT, updatedAt TEXT, UNIQUE (assetId, providerId, observedAt, resolution), CHECK (eur IS NOT NULL OR usd IS NOT NULL) ) STRICT`); run(`INSERT INTO PriceHistory (id, assetId, providerId, eur, usd, resolution, observedAt, updatedAt) SELECT id, assetId, providerId, eur, usd, resolution, observedAt, updatedAt FROM PriceHistoryOud`); run(`DROP TABLE PriceHistoryOud`); run(`CREATE INDEX IxPriceHistoryProviderId ON PriceHistory(providerId)`); run(`CREATE INDEX IxPriceHistoryAssetIdObservedAt ON PriceHistory(assetId, observedAt)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%PriceHistoryOud%'`); if (besmet.length) { throw new Error( `Migration 86 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 87: () => { if (tableExists('ChangeLog')) return; run(`CREATE TABLE ChangeLog ( id INTEGER PRIMARY KEY, tableName TEXT NOT NULL, rowId INTEGER NOT NULL, operation TEXT NOT NULL CHECK (operation IN ('update','delete')), origin TEXT NOT NULL CHECK (origin IN ('manual','import','reconcile','interest','lightning','ai_proposal','merge','system')), originId INTEGER, changes TEXT NOT NULL, occurredAt TEXT NOT NULL ) STRICT`); run(`CREATE INDEX IxChangeLogTableNameRowId ON ChangeLog(tableName, rowId)`); run(`CREATE INDEX IxChangeLogOccurredAt ON ChangeLog(occurredAt)`); }, 88: () => { if (!tableExists('Event')) return; if (columnExists('Event', 'priceOrigin')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Event RENAME TO EventOud`); run(`CREATE TABLE Event ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, toAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, productId INTEGER REFERENCES MetalProduct(id) ON DELETE NO ACTION, productCount INTEGER CHECK (productCount IS NULL OR productCount > 0), importReference TEXT, externalAddress TEXT, note TEXT, amount INTEGER NOT NULL CHECK (amount > 0), eurPrice REAL CHECK (eurPrice IS NULL OR eurPrice >= 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), unitFee INTEGER CHECK (unitFee IS NULL OR unitFee >= 0), priceOrigin TEXT CHECK (priceOrigin IS NULL OR priceOrigin IN ('paid','market_day','market_moment','none')), type TEXT NOT NULL CHECK (type IN ('buy','sell','transfer','interest')), source TEXT NOT NULL CHECK (source IN ('manual','import','reconcile','lightning')), occurredAt TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT, CHECK ((productId IS NULL) = (productCount IS NULL)), CHECK (type NOT IN ('buy','sell') OR eurPrice IS NOT NULL) ) STRICT`); run(`INSERT INTO Event (id, assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress, note, amount, eurPrice, eurFee, unitFee, type, source, occurredAt, createdAt, updatedAt) SELECT id, assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress, note, amount, eurPrice, eurFee, unitFee, type, source, occurredAt, createdAt, updatedAt FROM EventOud`); run(`DROP TABLE EventOud`); run(`CREATE INDEX IxEventAssetId ON Event(assetId)`); run(`CREATE INDEX IxEventFromAccountId ON Event(fromAccountId)`); run(`CREATE INDEX IxEventToAccountId ON Event(toAccountId)`); run(`CREATE INDEX IxEventProductId ON Event(productId)`); run(`CREATE INDEX IxEventOccurredAt ON Event(occurredAt)`); run(`CREATE INDEX IxEventType ON Event(type)`); run(`CREATE INDEX IxEventExternalAddress ON Event(externalAddress)`); run(`CREATE INDEX IxEventImportReference ON Event(importReference)`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%EventOud%'`); if (besmet.length) { throw new Error( `Migration 88 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 89: () => { if (tableExists('HygieneAcceptance')) return; run(`CREATE TABLE HygieneAcceptance ( id INTEGER PRIMARY KEY, kind TEXT NOT NULL, fingerprint TEXT NOT NULL, summary TEXT NOT NULL, note TEXT, createdAt TEXT NOT NULL ) STRICT`); run(`CREATE UNIQUE INDEX UxHygieneAcceptanceFingerprint ON HygieneAcceptance(fingerprint)`); run(`CREATE INDEX IxHygieneAcceptanceKind ON HygieneAcceptance(kind)`); }, 90: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (tableExists('ProviderEndpoint') && !columnExists('ProviderEndpoint', 'quoteCurrency')) { const metCreatedAt = columnExists('ProviderEndpoint', 'createdAt'); run(`ALTER TABLE ProviderEndpoint RENAME TO ProviderEndpointOud`); run(`CREATE TABLE ProviderEndpoint ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, pathTemplate TEXT NOT NULL, queryTemplate TEXT, capability TEXT NOT NULL CHECK (capability IN ('spot','daily_history','history_point')), method TEXT NOT NULL CHECK (method IN ('GET','POST')), quoteCurrency TEXT, createdAt TEXT, UNIQUE (providerId, capability) ) STRICT`); const kolommen = `id, providerId, pathTemplate, queryTemplate, capability, method${metCreatedAt ? ', createdAt' : ''}`; run(`INSERT INTO ProviderEndpoint (${kolommen}) SELECT ${kolommen} FROM ProviderEndpointOud`); run(`DROP TABLE ProviderEndpointOud`); } const outputMapSql = one(`SELECT sql AS s FROM sqlite_master WHERE type = 'table' AND name = 'ProviderOutputMap'`); if (outputMapSql && !String(outputMapSql.s).includes('iso_date_to_s')) { run(`ALTER TABLE ProviderOutputMap RENAME TO ProviderOutputMapOud`); run(`CREATE TABLE ProviderOutputMap ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, jsonPath TEXT NOT NULL, capability TEXT NOT NULL CHECK (capability IN ('spot','daily_history','history_point')), internalField TEXT NOT NULL CHECK (internalField IN ('EUR','USD','time','price')), transform TEXT NOT NULL CHECK (transform IN ('none','ms_to_s','to_number','last_per_day','troy_ounce_to_gram','iso_date_to_s')), createdAt TEXT ) STRICT`); run(`INSERT INTO ProviderOutputMap (id, providerId, jsonPath, capability, internalField, transform, createdAt) SELECT id, providerId, jsonPath, capability, internalField, transform, createdAt FROM ProviderOutputMapOud`); run(`DROP TABLE ProviderOutputMapOud`); run(`CREATE INDEX IxProviderOutputMapProviderIdCapability ON ProviderOutputMap(providerId, capability)`); } const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%ProviderEndpointOud%' OR sql LIKE '%ProviderOutputMapOud%'`); if (besmet.length) { throw new Error( `Migration 90 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 91: () => { for (const t of ['Provider', 'ProviderCoverage', 'ProviderEndpoint', 'ProviderOutputMap', 'Asset', 'ConsumerSource']) { if (!tableExists(t)) return; } const nu = new Date().toISOString(); if (!one(`SELECT 1 AS x FROM Provider WHERE key = 'frankfurter'`)) { run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationHeaderName, authenticationParameterName, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('frankfurter', 'ECB-wisselkoersen (Frankfurter)', 'ecb-wisselkoersen (frankfurter)', 'https://api.frankfurter.dev', NULL, NULL, 'none', 0, 1, 1, 0, ?)`, [nu]); const providerId = one(`SELECT id FROM Provider WHERE key = 'frankfurter'`).id; run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?, 'USD')`, [providerId]); run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate, quoteCurrency, createdAt) VALUES (?, 'spot', 'GET', '/v1/latest', 'base={symbol}&symbols=EUR', NULL, ?)`, [providerId, nu]); run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate, quoteCurrency, createdAt) VALUES (?, 'daily_history', 'GET', '/v1/1999-01-04..', 'base={symbol}&symbols=EUR', NULL, ?)`, [providerId, nu]); for (const [capability, internalField, jsonPath, transform] of [ ['spot', 'EUR', 'rates.EUR', 'to_number'], ['daily_history', 'time', 'rates{}.@key', 'iso_date_to_s'], ['daily_history', 'EUR', 'rates{}.EUR', 'to_number'], ]) { run(`INSERT INTO ProviderOutputMap (providerId, capability, internalField, jsonPath, transform, createdAt) VALUES (?, ?, ?, ?, ?, ?)`, [providerId, capability, internalField, jsonPath, transform, nu]); } } if (!one(`SELECT 1 AS x FROM Asset WHERE symbol = 'USD'`)) { run(`INSERT INTO Asset (symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind) VALUES ('USD', 'Amerikaanse dollar', 'amerikaanse dollar', NULL, 'USD', 'cent', 'unit', 2, 'fiat')`); } const assetId = one(`SELECT id FROM Asset WHERE symbol = 'USD'`).id; const providerId = one(`SELECT id FROM Provider WHERE key = 'frankfurter'`).id; for (const [consumerKey, capability] of [ ['converter', 'spot'], ['fiat', 'spot'], ['fiat', 'daily_history'], ['signals', 'daily_history'], ['tax', 'spot'], ['tax', 'daily_history'], ['valuations', 'daily_history'], ]) { if (one(`SELECT 1 AS x FROM ConsumerSource WHERE consumerKey = ? AND assetId = ? AND capability = ?`, [consumerKey, assetId, capability])) continue; run(`INSERT INTO ConsumerSource (consumerKey, assetId, capability, providerId, rank, status) VALUES (?, ?, ?, ?, 1, 'confirmed')`, [consumerKey, assetId, capability, providerId]); } }, 92: () => { for (const t of ['Provider', 'ProviderEndpoint', 'ProviderOutputMap', 'ConsumerSource']) { if (!tableExists(t)) return; } const nu = new Date().toISOString(); for (const [key, path, usdTransform] of [ ['eulerpool', '/api/1/crypto/quotes/{symbol}', 'last_per_day'], ['eulerpool_metal', '/api/1/commodity/quotes/{symbol}', 'troy_ounce_to_gram'], ]) { const p = one(`SELECT id FROM Provider WHERE key = ?`, [key]); if (!p) continue; run(`DELETE FROM ConsumerSource WHERE providerId = ? AND capability <> 'daily_history'`, [p.id]); run(`DELETE FROM ProviderEndpoint WHERE providerId = ?`, [p.id]); run(`DELETE FROM ProviderOutputMap WHERE providerId = ?`, [p.id]); run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate, quoteCurrency, createdAt) VALUES (?, 'daily_history', 'GET', ?, NULL, 'USD', ?)`, [p.id, path, nu]); run(`INSERT INTO ProviderOutputMap (providerId, capability, internalField, jsonPath, transform, createdAt) VALUES (?, 'daily_history', 'time', '[].timestamp', 'ms_to_s', ?)`, [p.id, nu]); run(`INSERT INTO ProviderOutputMap (providerId, capability, internalField, jsonPath, transform, createdAt) VALUES (?, 'daily_history', 'USD', '[].price', ?, ?)`, [p.id, usdTransform, nu]); } }, 93: () => { if (!tableExists('ProviderCallLimit')) { run(`CREATE TABLE ProviderCallLimit ( providerId INTEGER PRIMARY KEY REFERENCES Provider(id) ON DELETE CASCADE, callsPerMonth INTEGER NOT NULL CHECK (callsPerMonth > 0), createdAt TEXT ) STRICT`); } if (!tableExists('Provider') || !columnExists('Provider', 'key')) return; const nu = new Date().toISOString(); for (const [key, calls] of [['goldapi', 100], ['metalsdev', 100], ['eulerpool', 1000], ['eulerpool_metal', 1000], ['coingecko', 10000]]) { const p = one(`SELECT id FROM Provider WHERE key = ?`, [key]); if (!p || one(`SELECT 1 AS x FROM ProviderCallLimit WHERE providerId = ?`, [p.id])) continue; run(`INSERT INTO ProviderCallLimit (providerId, callsPerMonth, createdAt) VALUES (?, ?, ?)`, [p.id, calls, nu]); } }, 94: () => { if (tableExists('IgnoredPriceGap')) return; run(`CREATE TABLE IgnoredPriceGap ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, fromDay TEXT NOT NULL, untilDay TEXT NOT NULL, createdAt TEXT NOT NULL, CHECK (fromDay <= untilDay) ) STRICT`); run(`CREATE INDEX IxIgnoredPriceGapAssetId ON IgnoredPriceGap(assetId)`); }, 95: () => { if (!tableExists('ProviderCallLimit') || !tableExists('Provider') || !columnExists('Provider', 'key')) return; run(`UPDATE ProviderCallLimit SET callsPerMonth = 100000 WHERE callsPerMonth = 1000 AND providerId IN (SELECT id FROM Provider WHERE key IN ('eulerpool', 'eulerpool_metal'))`); }, 96: () => { if (!tableExists('ProviderEndpoint') || columnExists('ProviderEndpoint', 'minIntervalSeconds')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); const metCreatedAt = columnExists('ProviderEndpoint', 'createdAt'); const metMunt = columnExists('ProviderEndpoint', 'quoteCurrency'); run(`ALTER TABLE ProviderEndpoint RENAME TO ProviderEndpointOud`); run(`CREATE TABLE ProviderEndpoint ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, pathTemplate TEXT NOT NULL, queryTemplate TEXT, capability TEXT NOT NULL CHECK (capability IN ('spot','daily_history','history_point')), method TEXT NOT NULL CHECK (method IN ('GET','POST')), quoteCurrency TEXT, minIntervalSeconds INTEGER CHECK (minIntervalSeconds IS NULL OR minIntervalSeconds > 0), createdAt TEXT, UNIQUE (providerId, capability) ) STRICT`); const kolommen = `id, providerId, pathTemplate, queryTemplate, capability, method` + `${metMunt ? ', quoteCurrency' : ''}${metCreatedAt ? ', createdAt' : ''}`; run(`INSERT INTO ProviderEndpoint (${kolommen}) SELECT ${kolommen} FROM ProviderEndpointOud`); run(`DROP TABLE ProviderEndpointOud`); const besmet = all(`SELECT name FROM sqlite_master WHERE sql LIKE '%ProviderEndpointOud%'`); if (besmet.length) { throw new Error( `Migration 96 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 97: () => { if (!tableExists('ConsumerSource') || tableExists('PriceSource')) return; const KEUZE = { spot: 'spot', daily_history: 'history', history_point: 'history' }; const GROEP = { spot: 0, daily_history: 0, history_point: 1 }; const STERKTE = { proposed: 0, confirmed: 1, manual: 2 }; try { run(`BEGIN`); run(`CREATE TABLE PriceSource ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE RESTRICT, capability TEXT NOT NULL CHECK (capability IN ('spot','history')), rank INTEGER CHECK (rank IS NULL OR rank > 0), status TEXT NOT NULL CHECK (status IN ('proposed','confirmed','manual')), createdAt TEXT, UNIQUE (assetId, capability, providerId), UNIQUE (assetId, capability, rank) ) STRICT`); run(`CREATE INDEX IxPriceSourceProviderId ON PriceSource(providerId)`); const metCreatedAt = columnExists('ConsumerSource', 'createdAt'); const rijen = all(`SELECT assetId, capability, providerId, rank, status${metCreatedAt ? ', createdAt' : ''} FROM ConsumerSource ORDER BY id`); const cellen = new Map(); for (const r of rijen) { const keuze = KEUZE[r.capability]; if (!keuze) continue; const k = `${r.assetId}|${keuze}`; if (!cellen.has(k)) cellen.set(k, new Map()); const perBron = cellen.get(k); const c = perBron.get(r.providerId) || { assetId: r.assetId, keuze, providerId: r.providerId, groep: GROEP[r.capability], rank: null, status: r.status, createdAt: null }; const g = GROEP[r.capability]; if (r.rank != null && (c.rank == null || g < c.groep || (g === c.groep && r.rank < c.rank))) { c.groep = g; c.rank = r.rank; } if (STERKTE[r.status] > STERKTE[c.status]) c.status = r.status; const t = r.createdAt ?? null; if (t && (!c.createdAt || t < c.createdAt)) c.createdAt = t; perBron.set(r.providerId, c); } for (const perBron of cellen.values()) { const gerangschikt = [...perBron.values()].filter(c => c.rank != null) .sort((a, b) => a.groep - b.groep || a.rank - b.rank || STERKTE[b.status] - STERKTE[a.status] || a.providerId - b.providerId); gerangschikt.forEach((c, i) => { c.rank = i + 1; }); for (const c of perBron.values()) { run(`INSERT INTO PriceSource (assetId, capability, providerId, rank, status, createdAt) VALUES (?, ?, ?, ?, ?, ?)`, [c.assetId, c.keuze, c.providerId, c.rank, c.status, c.createdAt]); } } run(`DROP TABLE ConsumerSource`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%ConsumerSource%'`) .filter(r => /\bConsumerSource\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 97 aborted: ${besmet.map(r => r.name).join(', ')} still refers to ConsumerSource. ` + `The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } }, 98: () => { if (!tableExists('AiProposal')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, createdEventId INTEGER REFERENCES Event(id) ON DELETE SET NULL, createdAccountId INTEGER REFERENCES Account(id) ON DELETE SET NULL, createdValuationId INTEGER REFERENCES Valuation(id) ON DELETE SET NULL, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio','preference','source_choice')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), eur REAL CHECK (eur IS NULL OR eur > 0), eurFee REAL CHECK (eurFee IS NULL OR eurFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, scope TEXT, key TEXT, value TEXT, note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND eur IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR eur IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR name IS NOT NULL), CHECK (kind <> 'preference' OR (scope IS NOT NULL AND key IS NOT NULL AND value IS NOT NULL)), CHECK (kind <> 'source_choice' OR (assetId IS NOT NULL AND scope IN ('spot','history') AND key IS NOT NULL AND value IN ('first','remove'))), CHECK (kind IN ('preference','source_choice') OR (scope IS NULL AND key IS NULL AND value IS NULL)) ) STRICT;`); const kolommen = `id, assetId, accountId, fromAccountId, toAccountId, createdEventId, createdAccountId, createdValuationId, kind, name, accountKind, isOwnCustody, eventType, amount, eur, eurFee, balance, observedAt, scope, key, value, note, status, detail, resolvedAt, createdAt`; run(`INSERT INTO AiProposal (${kolommen}) SELECT ${kolommen} FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); run(`CREATE INDEX IxAiProposalCreatedEventId ON AiProposal(createdEventId)`); run(`CREATE INDEX IxAiProposalCreatedAccountId ON AiProposal(createdAccountId)`); run(`CREATE INDEX IxAiProposalCreatedValuationId ON AiProposal(createdValuationId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`) .filter(r => /\bAiProposalOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 98 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 99: () => { if (!tableExists('ProviderInputMap')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (!columnExists('ProviderInputMap', 'quoteUnit')) { const metCreatedAt = columnExists('ProviderInputMap', 'createdAt'); run(`ALTER TABLE ProviderInputMap RENAME TO ProviderInputMapOud`); run(`CREATE TABLE ProviderInputMap ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, symbol TEXT NOT NULL, externalId TEXT NOT NULL, quoteUnit TEXT CHECK (quoteUnit IS NULL OR quoteUnit IN ('ozt')), createdAt TEXT, UNIQUE (providerId, symbol) ) STRICT`); const kolommen = `id, providerId, symbol, externalId${metCreatedAt ? ', createdAt' : ''}`; run(`INSERT INTO ProviderInputMap (${kolommen}) SELECT ${kolommen} FROM ProviderInputMapOud`); run(`DROP TABLE ProviderInputMapOud`); } const bron = key => (tableExists('Provider') ? one(`SELECT id FROM Provider WHERE key = ?`, [key]) : null); const crypto = bron('eulerpool'), metaal = bron('eulerpool_metal'); if (crypto && metaal) { const e = crypto.id, m = metaal.id, nu = new Date().toISOString(); run(`UPDATE ProviderEndpoint SET pathTemplate = '/api/1/{external_id}/quotes/{symbol}' WHERE providerId = ? AND capability = 'daily_history'`, [e]); for (const r of all(`SELECT symbol FROM ProviderCoverage WHERE providerId = ?`, [e])) { run(`INSERT OR IGNORE INTO ProviderInputMap (providerId, symbol, externalId, quoteUnit, createdAt) VALUES (?, ?, 'crypto', NULL, ?)`, [e, r.symbol, nu]); } for (const r of all(`SELECT symbol FROM ProviderCoverage WHERE providerId = ?`, [m])) { run(`INSERT OR IGNORE INTO ProviderCoverage (providerId, symbol) VALUES (?, ?)`, [e, r.symbol]); run(`INSERT OR IGNORE INTO ProviderInputMap (providerId, symbol, externalId, quoteUnit, createdAt) VALUES (?, ?, 'commodity', 'ozt', ?)`, [e, r.symbol, nu]); } if (tableExists('PriceSource')) { run(`UPDATE OR IGNORE PriceSource SET providerId = ? WHERE providerId = ?`, [e, m]); run(`DELETE FROM PriceSource WHERE providerId = ?`, [m]); } if (tableExists('PriceHistory')) { run(`UPDATE OR IGNORE PriceHistory SET providerId = ? WHERE providerId = ?`, [e, m]); run(`DELETE FROM PriceHistory WHERE providerId = ?`, [m]); } if (tableExists('ApiCallMetric')) { run(`INSERT INTO ApiCallMetric (providerId, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs) SELECT ?, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs FROM ApiCallMetric WHERE providerId = ? ON CONFLICT (providerId, capability, bucketMinute) DO UPDATE SET okCount = okCount + excluded.okCount, failCount = failCount + excluded.failCount, sumLatencyMs = sumLatencyMs + excluded.sumLatencyMs, maxLatencyMs = MAX(maxLatencyMs, excluded.maxLatencyMs)`, [e, m]); run(`DELETE FROM ApiCallMetric WHERE providerId = ?`, [m]); } for (const t of ['ProviderEndpoint', 'ProviderCoverage', 'ProviderInputMap', 'ProviderOutputMap', 'ProviderCallLimit', 'ProviderAuthenticationHmac', 'ProviderFixedRate']) { if (tableExists(t)) run(`DELETE FROM ${t} WHERE providerId = ?`, [m]); } run(`DELETE FROM Provider WHERE id = ?`, [m]); } const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%ProviderInputMapOud%'`) .filter(r => /\bProviderInputMapOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 99 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 100: () => { if (!tableExists('PriceHistory')) return; const usd = columnExists('PriceHistory', 'usd'); const geenKoers = kolom => `(${kolom} IS NULL OR ${kolom} <= 0)`; run(`DELETE FROM PriceHistory WHERE ${geenKoers('eur')}${usd ? ` AND ${geenKoers('usd')}` : ''}`); run(`UPDATE PriceHistory SET eur = NULL WHERE eur <= 0`); if (usd) run(`UPDATE PriceHistory SET usd = NULL WHERE usd <= 0`); }, 101: () => { if (tableExists('AssetNote')) return; run(`CREATE TABLE AssetNote ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, body TEXT NOT NULL, createdAt TEXT NOT NULL ) STRICT`); run(`CREATE UNIQUE INDEX UxAssetNoteAssetId ON AssetNote(assetId)`); }, 102: () => { if (!tableExists('ProviderInputMap')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (!columnExists('ProviderInputMap', 'isCatchAll')) { const metQuoteUnit = columnExists('ProviderInputMap', 'quoteUnit'); const metCreatedAt = columnExists('ProviderInputMap', 'createdAt'); run(`ALTER TABLE ProviderInputMap RENAME TO ProviderInputMapOud`); run(`CREATE TABLE ProviderInputMap ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, symbol TEXT, externalId TEXT NOT NULL, quoteUnit TEXT CHECK (quoteUnit IS NULL OR quoteUnit IN ('ozt')), isCatchAll INTEGER NOT NULL CHECK (isCatchAll IN (0,1)), createdAt TEXT, UNIQUE (providerId, symbol), CHECK ((isCatchAll = 1) = (symbol IS NULL)) ) STRICT`); const kolommen = `id, providerId, symbol, externalId${metQuoteUnit ? ', quoteUnit' : ''}${metCreatedAt ? ', createdAt' : ''}`; run(`INSERT INTO ProviderInputMap (${kolommen}, isCatchAll) SELECT ${kolommen}, 0 FROM ProviderInputMapOud`); run(`DROP TABLE ProviderInputMapOud`); run(`CREATE UNIQUE INDEX UxProviderInputMapCatchAll ON ProviderInputMap(providerId) WHERE isCatchAll = 1`); } const euler = tableExists('Provider') ? one(`SELECT id FROM Provider WHERE key = 'eulerpool'`) : null; if (euler && !one(`SELECT id FROM ProviderInputMap WHERE providerId = ? AND isCatchAll = 1`, [euler.id])) { const crypto = all(`SELECT id FROM ProviderInputMap WHERE providerId = ? AND externalId = 'crypto' AND quoteUnit IS NULL`, [euler.id]); if (crypto.length) { run(`DELETE FROM ProviderInputMap WHERE providerId = ? AND externalId = 'crypto' AND quoteUnit IS NULL`, [euler.id]); run(`INSERT INTO ProviderInputMap (providerId, symbol, externalId, quoteUnit, isCatchAll, createdAt) VALUES (?, NULL, 'crypto', NULL, 1, ?)`, [euler.id, new Date().toISOString()]); } } const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%ProviderInputMapOud%'`) .filter(r => /\bProviderInputMapOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 102 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 103: () => { const outputMapSql = one(`SELECT sql AS s FROM sqlite_master WHERE type = 'table' AND name = 'ProviderOutputMap'`); if (!outputMapSql) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (columnExists('ProviderOutputMap', 'transform') && !String(outputMapSql.s).includes('ms_to_s_local_close')) { run(`ALTER TABLE ProviderOutputMap RENAME TO ProviderOutputMapOud`); run(`CREATE TABLE ProviderOutputMap ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, jsonPath TEXT NOT NULL, capability TEXT NOT NULL CHECK (capability IN ('spot','daily_history','history_point')), internalField TEXT NOT NULL CHECK (internalField IN ('EUR','USD','time','price')), transform TEXT NOT NULL CHECK (transform IN ('none','ms_to_s','to_number','last_per_day','troy_ounce_to_gram','iso_date_to_s','ms_to_s_local_close')), createdAt TEXT ) STRICT`); run(`INSERT INTO ProviderOutputMap (id, providerId, jsonPath, capability, internalField, transform, createdAt) SELECT id, providerId, jsonPath, capability, internalField, transform, createdAt FROM ProviderOutputMapOud`); run(`DROP TABLE ProviderOutputMapOud`); run(`CREATE INDEX IxProviderOutputMapProviderIdCapability ON ProviderOutputMap(providerId, capability)`); } const euler = tableExists('Provider') ? one(`SELECT id FROM Provider WHERE key = 'eulerpool'`) : null; if (euler && columnExists('ProviderOutputMap', 'transform')) { run(`UPDATE ProviderOutputMap SET transform = 'ms_to_s_local_close' WHERE providerId = ? AND capability = 'daily_history' AND internalField = 'time' AND transform = 'ms_to_s'`, [euler.id]); if (tableExists('PriceHistory') && tableExists('Asset')) { run(`DELETE FROM PriceHistory WHERE providerId = ? AND assetId IN (SELECT id FROM Asset WHERE kind = 'metal')`, [euler.id]); } } const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%ProviderOutputMapOud%'`) .filter(r => /\bProviderOutputMapOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 103 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 104: () => { if (tableExists('Portfolio')) return; run(`CREATE TABLE Portfolio ( id INTEGER PRIMARY KEY CHECK (id = 1), baseCurrency TEXT NOT NULL CHECK (baseCurrency GLOB '[A-Z][A-Z][A-Z]'), createdAt TEXT ) STRICT`); run(`INSERT INTO Portfolio (id, baseCurrency, createdAt) VALUES (1, 'EUR', NULL)`); run(`CREATE TRIGGER TrPortfolioUpdate BEFORE UPDATE ON Portfolio BEGIN SELECT RAISE(ABORT, 'Portfolio is fixed after creation'); END`); run(`CREATE TRIGGER TrPortfolioDelete BEFORE DELETE ON Portfolio BEGIN SELECT RAISE(ABORT, 'Portfolio is fixed after creation'); END`); }, 105: () => { for (const [table, oud, nieuw] of [ ['Event', 'eurPrice', 'fiatPrice'], ['Event', 'eurFee', 'fiatFee'], ['Valuation', 'eurRate', 'fiatRate'], ['PriceHistory', 'eur', 'fiatPrice'], ['ProviderFixedRate', 'eur', 'fiatPrice'], ['AiProposal', 'eur', 'fiatPrice'], ['AiProposal', 'eurFee', 'fiatFee'], ]) { if (columnExists(table, oud)) run(`ALTER TABLE ${table} RENAME COLUMN ${oud} TO ${nieuw}`); } if (!tableExists('Preference')) return; for (const [scope, oud, nieuw] of [ ['table.trades', 'eurPrice', 'fiatPrice'], ['table.trades', 'eurFee', 'fiatFee'], ['table.valuations', 'eurRate', 'fiatRate'], ['table.prijsinfo', 'eur', 'fiatPrice'], ]) { run(`UPDATE Preference SET value = json_set(value, '$.sort.key', ?) WHERE scope = ? AND key = 'view' AND CASE WHEN json_valid(value) THEN json_extract(value, '$.sort.key') END = ?`, [nieuw, scope, oud]); } }, 106: () => { if (tableExists('Preference')) run(`DELETE FROM Preference WHERE scope = 'app' AND key = 'currency'`); if (tableExists('AiProposal')) { run(`UPDATE AiProposal SET status = 'rejected', resolvedAt = ?, detail = 'The preference app.currency no longer exists: the app shows the unit of account.' WHERE kind = 'preference' AND scope = 'app' AND key = 'currency' AND status = 'pending'`, [new Date().toISOString()]); } }, 107: () => { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (tableExists('ProviderOutputMap') && columnExists('ProviderOutputMap', 'internalField') && !one(`SELECT sql AS s FROM sqlite_master WHERE name = 'ProviderOutputMap'`).s.includes(`IN ('time','price')`)) { const endpoints = tableExists('ProviderEndpoint') && columnExists('ProviderEndpoint', 'quoteCurrency'); const paren = all(`SELECT DISTINCT providerId, capability FROM ProviderOutputMap WHERE internalField IN ('EUR','USD')`); for (const { providerId, capability } of paren) { const e = endpoints ? one(`SELECT quoteCurrency AS q FROM ProviderEndpoint WHERE providerId = ? AND capability = ?`, [providerId, capability]) : null; const q = e ? e.q : null; const veld = !q || q === 'EUR' ? 'EUR' : q === 'USD' ? 'USD' : 'price'; const heeft = one(`SELECT id FROM ProviderOutputMap WHERE providerId = ? AND capability = ? AND internalField = ?`, [providerId, capability, veld]); if (heeft && veld !== 'price') { run(`DELETE FROM ProviderOutputMap WHERE providerId = ? AND capability = ? AND internalField = 'price'`, [providerId, capability]); run(`UPDATE ProviderOutputMap SET internalField = 'price' WHERE id = ?`, [heeft.id]); } run(`DELETE FROM ProviderOutputMap WHERE providerId = ? AND capability = ? AND internalField IN ('EUR','USD')`, [providerId, capability]); } const metCreatedAt = columnExists('ProviderOutputMap', 'createdAt'); run(`ALTER TABLE ProviderOutputMap RENAME TO ProviderOutputMapOud`); run(`CREATE TABLE ProviderOutputMap ( id INTEGER PRIMARY KEY, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, jsonPath TEXT NOT NULL, capability TEXT NOT NULL CHECK (capability IN ('spot','daily_history','history_point')), internalField TEXT NOT NULL CHECK (internalField IN ('time','price')), transform TEXT NOT NULL CHECK (transform IN ('none','ms_to_s','to_number','last_per_day','troy_ounce_to_gram','iso_date_to_s','ms_to_s_local_close')), createdAt TEXT ) STRICT`); const kolommen = `id, providerId, jsonPath, capability, internalField, transform${metCreatedAt ? ', createdAt' : ''}`; run(`INSERT INTO ProviderOutputMap (${kolommen}) SELECT ${kolommen} FROM ProviderOutputMapOud`); run(`DROP TABLE ProviderOutputMapOud`); run(`CREATE INDEX IxProviderOutputMapProviderIdCapability ON ProviderOutputMap(providerId, capability)`); } const volledig = ['assetId', 'providerId', 'resolution', 'observedAt'].every(k => columnExists('PriceHistory', k)); if (tableExists('PriceHistory') && columnExists('PriceHistory', 'usd') && volledig) { run(`DELETE FROM PriceHistory WHERE fiatPrice IS NULL`); const tijd = ['createdAt', 'updatedAt'].filter(k => columnExists('PriceHistory', k)); run(`ALTER TABLE PriceHistory RENAME TO PriceHistoryOud`); run(`CREATE TABLE PriceHistory ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE RESTRICT, fiatPrice REAL NOT NULL, resolution TEXT NOT NULL CHECK (resolution IN ('day','timestamp')), observedAt TEXT NOT NULL, createdAt TEXT, updatedAt TEXT, UNIQUE (assetId, providerId, observedAt, resolution) ) STRICT`); const kolommen = ['id', 'assetId', 'providerId', 'fiatPrice', 'resolution', 'observedAt', ...tijd].join(', '); run(`INSERT INTO PriceHistory (${kolommen}) SELECT ${kolommen} FROM PriceHistoryOud`); run(`DROP TABLE PriceHistoryOud`); run(`CREATE INDEX IxPriceHistoryProviderId ON PriceHistory(providerId)`); run(`CREATE INDEX IxPriceHistoryAssetIdObservedAt ON PriceHistory(assetId, observedAt)`); } const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%Oud%'`) .filter(r => /\b(ProviderOutputMapOud|PriceHistoryOud)\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 107 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 108: () => { if (!tableExists('AiProposal') || columnExists('AiProposal', 'currency') || !columnExists('AiProposal', 'fiatPrice')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, createdEventId INTEGER REFERENCES Event(id) ON DELETE SET NULL, createdAccountId INTEGER REFERENCES Account(id) ON DELETE SET NULL, createdValuationId INTEGER REFERENCES Valuation(id) ON DELETE SET NULL, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio','preference','source_choice')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), fiatPrice REAL CHECK (fiatPrice IS NULL OR fiatPrice > 0), fiatFee REAL CHECK (fiatFee IS NULL OR fiatFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, scope TEXT, key TEXT, value TEXT, currency TEXT CHECK (currency IS NULL OR currency GLOB '[A-Z][A-Z][A-Z]'), note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND fiatPrice IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR fiatPrice IS NOT NULL), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR (name IS NOT NULL AND currency IS NOT NULL)), CHECK (kind = 'portfolio' OR currency IS NULL), CHECK (kind <> 'preference' OR (scope IS NOT NULL AND key IS NOT NULL AND value IS NOT NULL)), CHECK (kind <> 'source_choice' OR (assetId IS NOT NULL AND scope IN ('spot','history') AND key IS NOT NULL AND value IN ('first','remove'))), CHECK (kind IN ('preference','source_choice') OR (scope IS NULL AND key IS NULL AND value IS NULL)) ) STRICT;`); const kolommen = `id, assetId, accountId, fromAccountId, toAccountId, createdEventId, createdAccountId, createdValuationId, kind, name, accountKind, isOwnCustody, eventType, amount, fiatPrice, fiatFee, balance, observedAt, scope, key, value, note, status, detail, resolvedAt, createdAt`; run(`INSERT INTO AiProposal (${kolommen}, currency) SELECT ${kolommen}, CASE kind WHEN 'portfolio' THEN 'EUR' END FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); run(`CREATE INDEX IxAiProposalCreatedEventId ON AiProposal(createdEventId)`); run(`CREATE INDEX IxAiProposalCreatedAccountId ON AiProposal(createdAccountId)`); run(`CREATE INDEX IxAiProposalCreatedValuationId ON AiProposal(createdValuationId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`) .filter(r => /\bAiProposalOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 108 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 109: () => { if (!tableExists('Preference')) return; run(`UPDATE Preference SET value = json_set(value, '$.sort.key', 'fiatTotal') WHERE scope = 'table.trades' AND key = 'view' AND CASE WHEN json_valid(value) THEN json_extract(value, '$.sort.key') END = 'eurTotal'`); }, 110: () => { if (tableExists('PriceAbsence')) return; run(`CREATE TABLE PriceAbsence ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE CASCADE, day TEXT NOT NULL, createdAt TEXT NOT NULL ) STRICT`); run(`CREATE UNIQUE INDEX UxPriceAbsenceAssetIdProviderIdDay ON PriceAbsence(assetId, providerId, day)`); run(`CREATE INDEX IxPriceAbsenceProviderId ON PriceAbsence(providerId)`); }, 111: () => { if (!tableExists('Asset') || columnExists('Asset', 'balanceCapture') || !columnExists('Asset', 'taxCategory')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Asset RENAME TO AssetOud`); run(`CREATE TABLE Asset ( id INTEGER PRIMARY KEY, symbol TEXT NOT NULL, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, unit TEXT NOT NULL, subunit TEXT, displayUnit TEXT NOT NULL CHECK (displayUnit IN ('unit','subunit')), decimals INTEGER NOT NULL CHECK (decimals >= 0 AND decimals <= 9), kind TEXT NOT NULL CHECK (kind IN ('crypto','metal','fiat')), taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig','buiten')), balanceCapture INTEGER CHECK (balanceCapture IS NULL OR balanceCapture IN (0,1)), createdAt TEXT, updatedAt TEXT, UNIQUE (symbol), CHECK (displayUnit <> 'subunit' OR subunit IS NOT NULL) ) STRICT;`); const kolommen = `id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind, taxCategory, createdAt, updatedAt`; run(`INSERT INTO Asset (${kolommen}) SELECT ${kolommen} FROM AssetOud`); run(`DROP TABLE AssetOud`); run(`CREATE INDEX IxAssetSearchName ON Asset(searchName)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AssetOud%'`) .filter(r => /\bAssetOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 111 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 112: () => { const asset = tableExists('Asset') && !columnExists('Asset', 'taxScope') && columnExists('Asset', 'balanceCapture'); const account = tableExists('Account') && !columnExists('Account', 'taxScope') && columnExists('Account', 'taxCategory') && columnExists('Account', 'createdAt'); if (!asset && !account) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); if (asset) { run(`ALTER TABLE Asset RENAME TO AssetOud`); run(`CREATE TABLE Asset ( id INTEGER PRIMARY KEY, symbol TEXT NOT NULL, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, unit TEXT NOT NULL, subunit TEXT, displayUnit TEXT NOT NULL CHECK (displayUnit IN ('unit','subunit')), decimals INTEGER NOT NULL CHECK (decimals >= 0 AND decimals <= 9), kind TEXT NOT NULL CHECK (kind IN ('crypto','metal','fiat')), taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig')), taxScope TEXT CHECK (taxScope IS NULL OR taxScope IN ('all','category','none')), balanceCapture INTEGER CHECK (balanceCapture IS NULL OR balanceCapture IN (0,1)), createdAt TEXT, updatedAt TEXT, UNIQUE (symbol), CHECK (displayUnit <> 'subunit' OR subunit IS NOT NULL) ) STRICT;`); const kolommen = 'id, symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind'; run(`INSERT INTO Asset (${kolommen}, taxCategory, taxScope, balanceCapture, createdAt, updatedAt) SELECT ${kolommen}, CASE WHEN taxCategory = 'buiten' THEN NULL ELSE taxCategory END, CASE taxCategory WHEN 'buiten' THEN 'none' END, balanceCapture, createdAt, updatedAt FROM AssetOud`); run(`DROP TABLE AssetOud`); run(`CREATE INDEX IxAssetSearchName ON Asset(searchName)`); } if (account) { run(`ALTER TABLE Account RENAME TO AccountOud`); run(`CREATE TABLE Account ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, kind TEXT NOT NULL CHECK (kind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER NOT NULL CHECK (isOwnCustody IN (0,1)), scanEnabled INTEGER NOT NULL CHECK (scanEnabled IN (0,1)), isActive INTEGER NOT NULL CHECK (isActive IN (0,1)), closedAt TEXT, taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig')), taxScope TEXT CHECK (taxScope IS NULL OR taxScope IN ('all','category','none')), createdAt TEXT, updatedAt TEXT, CHECK ((isActive = 1 AND closedAt IS NULL) OR (isActive = 0 AND closedAt IS NOT NULL)) ) STRICT;`); const kolommen = 'id, name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt'; run(`INSERT INTO Account (${kolommen}, taxCategory, taxScope, createdAt, updatedAt) SELECT ${kolommen}, CASE WHEN taxCategory = 'buiten' THEN NULL ELSE taxCategory END, CASE taxCategory WHEN 'buiten' THEN 'none' END, createdAt, updatedAt FROM AccountOud`); run(`DROP TABLE AccountOud`); run(`CREATE INDEX IxAccountSearchName ON Account(searchName)`); } const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AssetOud%' OR sql LIKE '%AccountOud%'`) .filter(r => /\b(AssetOud|AccountOud)\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 112 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 113: () => { if (!tableExists('InterestSchedule') || columnExists('InterestSchedule', 'compounding') || !columnExists('InterestSchedule', 'updatedAt')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE InterestSchedule RENAME TO InterestScheduleOud`); run(`CREATE TABLE InterestSchedule ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, principalAssetId INTEGER REFERENCES Asset(id) ON DELETE NO ACTION, payoutAssetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, note TEXT, principalAmount INTEGER CHECK (principalAmount IS NULL OR principalAmount > 0), annualRate REAL CHECK (annualRate IS NULL OR annualRate >= 0), rateKind TEXT NOT NULL CHECK (rateKind IN ('fixed','variable')), creditCadence TEXT CHECK (creditCadence IS NULL OR creditCadence IN ('term_end','annual','quarterly','monthly','weekly')), creditWeekday INTEGER CHECK (creditWeekday IS NULL OR creditWeekday BETWEEN 0 AND 6), creditDayOfMonth INTEGER CHECK (creditDayOfMonth IS NULL OR creditDayOfMonth BETWEEN 1 AND 31), startsOn TEXT NOT NULL, endsOn TEXT, compounding INTEGER NOT NULL CHECK (compounding IN (0,1)), createdAt TEXT NOT NULL, updatedAt TEXT, CHECK (rateKind = 'fixed' OR (principalAmount IS NULL AND annualRate IS NULL)), CHECK (creditWeekday IS NULL OR creditCadence = 'weekly'), CHECK (creditDayOfMonth IS NULL OR creditCadence = 'monthly'), CHECK (endsOn IS NULL OR endsOn >= startsOn), CHECK (compounding = 0 OR (rateKind = 'fixed' AND principalAmount IS NOT NULL AND principalAssetId = payoutAssetId)) ) STRICT;`); const kolommen = `id, accountId, principalAssetId, payoutAssetId, note, principalAmount, annualRate, rateKind, creditCadence, creditWeekday, creditDayOfMonth, startsOn, endsOn`; run(`INSERT INTO InterestSchedule (${kolommen}, compounding, createdAt, updatedAt) SELECT ${kolommen}, 0, createdAt, updatedAt FROM InterestScheduleOud`); run(`DROP TABLE InterestScheduleOud`); run(`CREATE INDEX IxInterestScheduleAccountId ON InterestSchedule(accountId)`); run(`CREATE INDEX IxInterestSchedulePrincipalAssetId ON InterestSchedule(principalAssetId)`); run(`CREATE INDEX IxInterestSchedulePayoutAssetId ON InterestSchedule(payoutAssetId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%InterestScheduleOud%'`) .filter(r => /\bInterestScheduleOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 113 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 114: () => { if (!tableExists('Account') || columnExists('Account', 'wealthScope') || !columnExists('Account', 'taxScope')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Account RENAME TO AccountOud`); run(`CREATE TABLE Account ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, searchName TEXT NOT NULL, description TEXT, kind TEXT NOT NULL CHECK (kind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER NOT NULL CHECK (isOwnCustody IN (0,1)), scanEnabled INTEGER NOT NULL CHECK (scanEnabled IN (0,1)), isActive INTEGER NOT NULL CHECK (isActive IN (0,1)), closedAt TEXT, taxCategory TEXT CHECK (taxCategory IS NULL OR taxCategory IN ('bank','overig')), taxScope TEXT CHECK (taxScope IS NULL OR taxScope IN ('all','category','none')), wealthScope TEXT CHECK (wealthScope IS NULL OR wealthScope IN ('managed','none')), createdAt TEXT, updatedAt TEXT, CHECK ((isActive = 1 AND closedAt IS NULL) OR (isActive = 0 AND closedAt IS NOT NULL)) ) STRICT;`); const kolommen = 'id, name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt, taxCategory, taxScope'; run(`INSERT INTO Account (${kolommen}, wealthScope, createdAt, updatedAt) SELECT ${kolommen}, NULL, createdAt, updatedAt FROM AccountOud`); run(`DROP TABLE AccountOud`); run(`CREATE INDEX IxAccountSearchName ON Account(searchName)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AccountOud%'`) .filter(r => /\bAccountOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 114 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 115: () => serviceProviderRow(), 116: () => serviceProviderRow(), 117: () => serviceProviderRow(), 118: () => { if (tableExists('IntegrationCall')) return; run(`CREATE TABLE IntegrationCall ( id INTEGER PRIMARY KEY, target TEXT NOT NULL CHECK (target IN ('service','provider')), providerKey TEXT, operation TEXT NOT NULL, subject TEXT, outcome TEXT NOT NULL CHECK (outcome IN ('fail','empty','refused')), httpStatus INTEGER CHECK (httpStatus IS NULL OR httpStatus BETWEEN 100 AND 599), errorKey TEXT, detail TEXT CHECK (detail IS NULL OR length(detail) <= 300), latencyMs INTEGER CHECK (latencyMs IS NULL OR latencyMs >= 0), requestId TEXT, occurredAt TEXT NOT NULL, CHECK ((target = 'service') = (providerKey IS NULL)), CHECK (requestId IS NULL OR target = 'service') ) STRICT`); run(`CREATE INDEX IxIntegrationCallOccurredAt ON IntegrationCall(occurredAt)`); }, 119: () => { if (!tableExists('WalletTransaction') || columnExists('WalletTransaction', 'layer') || !columnExists('WalletTransaction', 'blockTime')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE WalletTransaction RENAME TO WalletTransactionOud`); run(`CREATE TABLE WalletTransaction ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, sourceId INTEGER REFERENCES WalletSource(id) ON DELETE CASCADE, labelId INTEGER REFERENCES Label(id) ON DELETE SET NULL, layer TEXT NOT NULL CHECK (layer IN ('onchain','lightning')), txid TEXT NOT NULL, address TEXT, valueSats INTEGER NOT NULL, feeSats INTEGER CHECK (feeSats IS NULL OR feeSats >= 0), blockHeight INTEGER CHECK (blockHeight IS NULL OR blockHeight > 0), blockTime TEXT, direction TEXT NOT NULL CHECK (direction IN ('in','out')), createdAt TEXT NOT NULL, CHECK ((layer = 'onchain' AND (blockHeight IS NULL) = (blockTime IS NULL)) OR (layer = 'lightning' AND blockHeight IS NULL AND blockTime IS NOT NULL AND sourceId IS NOT NULL)) ) STRICT;`); const kolommen = 'id, accountId, sourceId, labelId, txid, address, valueSats, feeSats, blockHeight, blockTime, direction, createdAt'; run(`INSERT INTO WalletTransaction (${kolommen}, layer) SELECT ${kolommen}, 'onchain' FROM WalletTransactionOud`); run(`DROP TABLE WalletTransactionOud`); run(`CREATE UNIQUE INDEX UxWalletTransactionAccountIdTxid ON WalletTransaction(accountId, txid) WHERE layer = 'onchain'`); run(`CREATE UNIQUE INDEX UxWalletTransactionSourceIdTxid ON WalletTransaction(sourceId, txid) WHERE layer = 'lightning'`); run(`CREATE INDEX IxWalletTransactionAccountId ON WalletTransaction(accountId)`); run(`CREATE INDEX IxWalletTransactionSourceId ON WalletTransaction(sourceId)`); run(`CREATE INDEX IxWalletTransactionLabelId ON WalletTransaction(labelId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%WalletTransactionOud%'`) .filter(r => /\bWalletTransactionOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 119 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 120: () => { if (!tableExists('WalletSource') || columnExists('WalletSource', 'balanceSats') || !columnExists('WalletSource', 'scriptType')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE WalletSource RENAME TO WalletSourceOud`); run(`CREATE TABLE WalletSource ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, connectionId INTEGER REFERENCES Connection(id) ON DELETE RESTRICT, value TEXT NOT NULL, name TEXT, kind TEXT NOT NULL CHECK (kind IN ('xpub','address','nwc','lnd')), scriptType TEXT CHECK (scriptType IS NULL OR scriptType IN ('p2tr','p2wpkh','p2sh-p2wpkh','p2pkh')), balanceSats INTEGER CHECK (balanceSats IS NULL OR balanceSats >= 0), lastScannedAt TEXT, createdAt TEXT NOT NULL, updatedAt TEXT, CHECK (scriptType IS NULL OR kind = 'xpub'), CHECK ((kind = 'lnd') = (connectionId IS NOT NULL)), CHECK ((balanceSats IS NULL) = (lastScannedAt IS NULL)), CHECK (balanceSats IS NULL OR kind IN ('lnd','nwc')) ) STRICT;`); const kolommen = 'id, accountId, connectionId, value, name, kind, scriptType'; run(`INSERT INTO WalletSource (${kolommen}, balanceSats, lastScannedAt, createdAt, updatedAt) SELECT ${kolommen}, NULL, NULL, createdAt, updatedAt FROM WalletSourceOud`); run(`DROP TABLE WalletSourceOud`); run(`CREATE INDEX IxWalletSourceAccountId ON WalletSource(accountId)`); run(`CREATE INDEX IxWalletSourceConnectionId ON WalletSource(connectionId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%WalletSourceOud%'`) .filter(r => /\bWalletSourceOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 120 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 121: () => { if (!tableExists('WalletTransaction') || columnExists('WalletTransaction', 'kind') || !columnExists('WalletTransaction', 'layer')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE WalletTransaction RENAME TO WalletTransactionOud`); run(`CREATE TABLE WalletTransaction ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, sourceId INTEGER REFERENCES WalletSource(id) ON DELETE CASCADE, labelId INTEGER REFERENCES Label(id) ON DELETE SET NULL, layer TEXT NOT NULL CHECK (layer IN ('onchain','lightning')), kind TEXT CHECK (kind IS NULL OR kind IN ('channel','self')), txid TEXT NOT NULL, address TEXT, valueSats INTEGER NOT NULL, feeSats INTEGER CHECK (feeSats IS NULL OR feeSats >= 0), blockHeight INTEGER CHECK (blockHeight IS NULL OR blockHeight > 0), blockTime TEXT, direction TEXT NOT NULL CHECK (direction IN ('in','out')), createdAt TEXT NOT NULL, CHECK ((layer = 'onchain' AND (blockHeight IS NULL) = (blockTime IS NULL)) OR (layer = 'lightning' AND blockHeight IS NULL AND blockTime IS NOT NULL AND sourceId IS NOT NULL)), CHECK (kind IS NULL OR (kind = 'channel') = (layer = 'onchain')) ) STRICT;`); const kolommen = 'id, accountId, sourceId, labelId, layer, txid, address, valueSats, feeSats, blockHeight, blockTime, direction, createdAt'; run(`INSERT INTO WalletTransaction (${kolommen}, kind) SELECT ${kolommen}, NULL FROM WalletTransactionOud`); run(`DROP TABLE WalletTransactionOud`); run(`CREATE UNIQUE INDEX UxWalletTransactionAccountIdTxid ON WalletTransaction(accountId, txid) WHERE layer = 'onchain'`); run(`CREATE UNIQUE INDEX UxWalletTransactionSourceIdTxid ON WalletTransaction(sourceId, txid) WHERE layer = 'lightning'`); run(`CREATE INDEX IxWalletTransactionAccountId ON WalletTransaction(accountId)`); run(`CREATE INDEX IxWalletTransactionSourceId ON WalletTransaction(sourceId)`); run(`CREATE INDEX IxWalletTransactionLabelId ON WalletTransaction(labelId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%WalletTransactionOud%'`) .filter(r => /\bWalletTransactionOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 121 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 122: () => { if (!tableExists('ReconcileProposal')) return; const ddl = one(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'ReconcileProposal'`)?.sql || ''; if (ddl.includes('accepted_interest') || !ddl.includes('auto_linked')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE ReconcileProposal RENAME TO ReconcileProposalOud`); run(`CREATE TABLE ReconcileProposal ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, walletTransactionId INTEGER REFERENCES WalletTransaction(id) ON DELETE CASCADE, resolvedEventId INTEGER REFERENCES Event(id) ON DELETE SET NULL, transactionHash TEXT, amount INTEGER NOT NULL CHECK (amount > 0), direction TEXT NOT NULL CHECK (direction IN ('in','out')), suggestedType TEXT NOT NULL CHECK (suggestedType IN ('trade','transfer')), status TEXT NOT NULL CHECK (status IN ('pending','accepted_trade','accepted_transfer','accepted_interest','ignored','auto_linked')), detectedAt TEXT NOT NULL ) STRICT;`); const kolommen = 'id, accountId, assetId, walletTransactionId, resolvedEventId, transactionHash, amount, direction, suggestedType, status, detectedAt'; run(`INSERT INTO ReconcileProposal (${kolommen}) SELECT ${kolommen} FROM ReconcileProposalOud`); run(`DROP TABLE ReconcileProposalOud`); run(`CREATE INDEX IxReconcileProposalAccountId ON ReconcileProposal(accountId)`); run(`CREATE INDEX IxReconcileProposalAssetId ON ReconcileProposal(assetId)`); run(`CREATE INDEX IxReconcileProposalWalletTransactionId ON ReconcileProposal(walletTransactionId)`); run(`CREATE INDEX IxReconcileProposalResolvedEventId ON ReconcileProposal(resolvedEventId)`); run(`CREATE INDEX IxReconcileProposalStatus ON ReconcileProposal(status)`); run(`CREATE UNIQUE INDEX UxReconcileProposalLink ON ReconcileProposal(walletTransactionId, resolvedEventId) WHERE walletTransactionId IS NOT NULL`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%ReconcileProposalOud%'`) .filter(r => /\bReconcileProposalOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 122 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 123: () => { if (!tableExists('WalletTransaction') || columnExists('WalletTransaction', 'memo') || !columnExists('WalletTransaction', 'kind')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE WalletTransaction RENAME TO WalletTransactionOud`); run(`CREATE TABLE WalletTransaction ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, sourceId INTEGER REFERENCES WalletSource(id) ON DELETE CASCADE, labelId INTEGER REFERENCES Label(id) ON DELETE SET NULL, layer TEXT NOT NULL CHECK (layer IN ('onchain','lightning')), kind TEXT CHECK (kind IS NULL OR kind IN ('channel','self')), txid TEXT NOT NULL, address TEXT, memo TEXT CHECK (memo IS NULL OR length(memo) <= 2000), valueSats INTEGER NOT NULL, feeSats INTEGER CHECK (feeSats IS NULL OR feeSats >= 0), blockHeight INTEGER CHECK (blockHeight IS NULL OR blockHeight > 0), blockTime TEXT, direction TEXT NOT NULL CHECK (direction IN ('in','out')), createdAt TEXT NOT NULL, CHECK ((layer = 'onchain' AND (blockHeight IS NULL) = (blockTime IS NULL)) OR (layer = 'lightning' AND blockHeight IS NULL AND blockTime IS NOT NULL AND sourceId IS NOT NULL)), CHECK (kind IS NULL OR (kind = 'channel') = (layer = 'onchain')), CHECK (memo IS NULL OR layer = 'lightning') ) STRICT;`); const kolommen = 'id, accountId, sourceId, labelId, layer, kind, txid, address, valueSats, feeSats, blockHeight, blockTime, direction, createdAt'; run(`INSERT INTO WalletTransaction (${kolommen}, memo) SELECT ${kolommen}, NULL FROM WalletTransactionOud`); run(`DROP TABLE WalletTransactionOud`); run(`CREATE UNIQUE INDEX UxWalletTransactionAccountIdTxid ON WalletTransaction(accountId, txid) WHERE layer = 'onchain'`); run(`CREATE UNIQUE INDEX UxWalletTransactionSourceIdTxid ON WalletTransaction(sourceId, txid) WHERE layer = 'lightning'`); run(`CREATE INDEX IxWalletTransactionAccountId ON WalletTransaction(accountId)`); run(`CREATE INDEX IxWalletTransactionSourceId ON WalletTransaction(sourceId)`); run(`CREATE INDEX IxWalletTransactionLabelId ON WalletTransaction(labelId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%WalletTransactionOud%'`) .filter(r => /\bWalletTransactionOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 123 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 124: () => { if (!tableExists('WalletSource') || !columnExists('WalletSource', 'balanceSats')) return; const ddl = one(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'WalletSource'`)?.sql || ''; if (!ddl.includes(`(kind = 'lnd') = (connectionId IS NOT NULL)`)) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE WalletSource RENAME TO WalletSourceOud`); run(`CREATE TABLE WalletSource ( id INTEGER PRIMARY KEY, accountId INTEGER NOT NULL REFERENCES Account(id) ON DELETE CASCADE, connectionId INTEGER REFERENCES Connection(id) ON DELETE RESTRICT, value TEXT NOT NULL, name TEXT, kind TEXT NOT NULL CHECK (kind IN ('xpub','address','nwc','lnd')), scriptType TEXT CHECK (scriptType IS NULL OR scriptType IN ('p2tr','p2wpkh','p2sh-p2wpkh','p2pkh')), balanceSats INTEGER CHECK (balanceSats IS NULL OR balanceSats >= 0), lastScannedAt TEXT, createdAt TEXT NOT NULL, updatedAt TEXT, CHECK (scriptType IS NULL OR kind = 'xpub'), CHECK (kind <> 'lnd' OR connectionId IS NOT NULL), CHECK (connectionId IS NULL OR kind IN ('lnd','xpub','address')), CHECK ((balanceSats IS NULL) = (lastScannedAt IS NULL)), CHECK (balanceSats IS NULL OR kind IN ('lnd','nwc')) ) STRICT;`); const kolommen = 'id, accountId, connectionId, value, name, kind, scriptType, balanceSats, lastScannedAt, createdAt, updatedAt'; run(`INSERT INTO WalletSource (${kolommen}) SELECT ${kolommen} FROM WalletSourceOud`); run(`DROP TABLE WalletSourceOud`); run(`CREATE INDEX IxWalletSourceAccountId ON WalletSource(accountId)`); run(`CREATE INDEX IxWalletSourceConnectionId ON WalletSource(connectionId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%WalletSourceOud%'`) .filter(r => /\bWalletSourceOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 124 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 125: () => { if (!tableExists('AiProposal') || columnExists('AiProposal', 'unitFee') || !columnExists('AiProposal', 'currency')) return; run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE AiProposal RENAME TO AiProposalOud`); run(`CREATE TABLE AiProposal ( id INTEGER PRIMARY KEY, assetId INTEGER REFERENCES Asset(id) ON DELETE CASCADE, accountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, toAccountId INTEGER REFERENCES Account(id) ON DELETE CASCADE, createdEventId INTEGER REFERENCES Event(id) ON DELETE SET NULL, createdAccountId INTEGER REFERENCES Account(id) ON DELETE SET NULL, createdValuationId INTEGER REFERENCES Valuation(id) ON DELETE SET NULL, kind TEXT NOT NULL CHECK (kind IN ('price_point','account','event','account_delete','valuation','portfolio','preference','source_choice')), name TEXT, accountKind TEXT CHECK (accountKind IS NULL OR accountKind IN ('exchange','wallet','lightning','card','bank','vault')), isOwnCustody INTEGER CHECK (isOwnCustody IS NULL OR isOwnCustody IN (0,1)), eventType TEXT CHECK (eventType IS NULL OR eventType IN ('buy','sell','transfer','interest')), amount INTEGER CHECK (amount IS NULL OR amount > 0), fiatPrice REAL CHECK (fiatPrice IS NULL OR fiatPrice > 0), fiatFee REAL CHECK (fiatFee IS NULL OR fiatFee >= 0), unitFee INTEGER CHECK (unitFee IS NULL OR unitFee >= 0), balance INTEGER CHECK (balance IS NULL OR balance >= 0), observedAt TEXT, scope TEXT, key TEXT, value TEXT, currency TEXT CHECK (currency IS NULL OR currency GLOB '[A-Z][A-Z][A-Z]'), note TEXT, status TEXT NOT NULL CHECK (status IN ('pending','accepted','rejected')), detail TEXT, resolvedAt TEXT, createdAt TEXT NOT NULL, CHECK (kind <> 'price_point' OR (assetId IS NOT NULL AND fiatPrice IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind <> 'account' OR (name IS NOT NULL AND accountKind IS NOT NULL AND isOwnCustody IS NOT NULL)), CHECK (kind <> 'event' OR (assetId IS NOT NULL AND eventType IS NOT NULL AND amount IS NOT NULL AND observedAt IS NOT NULL AND (fromAccountId IS NOT NULL OR toAccountId IS NOT NULL))), CHECK (kind <> 'event' OR eventType NOT IN ('buy','sell') OR fiatPrice IS NOT NULL), CHECK (unitFee IS NULL OR (kind = 'event' AND eventType IN ('sell','transfer'))), CHECK (kind <> 'account_delete' OR accountId IS NOT NULL), CHECK (kind <> 'valuation' OR (assetId IS NOT NULL AND accountId IS NOT NULL AND balance IS NOT NULL AND observedAt IS NOT NULL)), CHECK (kind = 'valuation' OR balance IS NULL), CHECK (kind <> 'portfolio' OR (name IS NOT NULL AND currency IS NOT NULL)), CHECK (kind = 'portfolio' OR currency IS NULL), CHECK (kind <> 'preference' OR (scope IS NOT NULL AND key IS NOT NULL AND value IS NOT NULL)), CHECK (kind <> 'source_choice' OR (assetId IS NOT NULL AND scope IN ('spot','history') AND key IS NOT NULL AND value IN ('first','remove'))), CHECK (kind IN ('preference','source_choice') OR (scope IS NULL AND key IS NULL AND value IS NULL)) ) STRICT;`); const kolommen = `id, assetId, accountId, fromAccountId, toAccountId, createdEventId, createdAccountId, createdValuationId, kind, name, accountKind, isOwnCustody, eventType, amount, fiatPrice, fiatFee, balance, observedAt, scope, key, value, currency, note, status, detail, resolvedAt, createdAt`; run(`INSERT INTO AiProposal (${kolommen}) SELECT ${kolommen} FROM AiProposalOud`); run(`DROP TABLE AiProposalOud`); run(`CREATE INDEX IxAiProposalStatus ON AiProposal(status)`); run(`CREATE INDEX IxAiProposalAssetId ON AiProposal(assetId)`); run(`CREATE INDEX IxAiProposalAccountId ON AiProposal(accountId)`); run(`CREATE INDEX IxAiProposalFromAccountId ON AiProposal(fromAccountId)`); run(`CREATE INDEX IxAiProposalToAccountId ON AiProposal(toAccountId)`); run(`CREATE INDEX IxAiProposalCreatedEventId ON AiProposal(createdEventId)`); run(`CREATE INDEX IxAiProposalCreatedAccountId ON AiProposal(createdAccountId)`); run(`CREATE INDEX IxAiProposalCreatedValuationId ON AiProposal(createdValuationId)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%AiProposalOud%'`) .filter(r => /\bAiProposalOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 125 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } }, 126: () => { if (tableExists('Event')) { const ddl = one(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'Event'`)?.sql || ''; if (!ddl.includes(`'reservation'`) && columnExists('Event', 'priceOrigin') && columnExists('Event', 'fiatPrice')) rebuildEvent126(); } if (tableExists('Notification')) { const ddl = one(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'Notification'`)?.sql || ''; if (!ddl.includes(`'reservation'`)) { run(`ALTER TABLE Notification RENAME TO NotificationOud`); run(`CREATE TABLE Notification ( id INTEGER PRIMARY KEY, count INTEGER NOT NULL CHECK (count >= 0), highWater INTEGER NOT NULL CHECK (highWater >= 0), kind TEXT NOT NULL CHECK (kind IN ('reconcile','interest','valuation','schedule_ended','source_proposal','ai_proposal','reservation')), dismissedAt TEXT, createdAt TEXT NOT NULL ) STRICT;`); run(`INSERT INTO Notification (id, count, highWater, kind, dismissedAt, createdAt) SELECT id, count, highWater, kind, dismissedAt, createdAt FROM NotificationOud`); run(`DROP TABLE NotificationOud`); run(`CREATE INDEX IxNotificationKind ON Notification(kind)`); } } }, 127: () => { if (tableExists('Preference')) { run(`INSERT INTO Preference (scope, key, value) VALUES ('app', 'locale', '"nl"') ON CONFLICT (scope, key) DO NOTHING`); } }, 128: () => { if (!tableExists('PriceHistory') || columnExists('PriceHistory', 'sourceName')) return; const vorm = ['assetId', 'providerId', 'fiatPrice', 'resolution', 'observedAt'].every(k => columnExists('PriceHistory', k)); if (vorm) rebuildPriceHistory128(); else run(`ALTER TABLE PriceHistory ADD COLUMN sourceName TEXT`); }, }; function rebuildPriceHistory128() { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE PriceHistory RENAME TO PriceHistoryOud`); run(`CREATE TABLE PriceHistory ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, providerId INTEGER NOT NULL REFERENCES Provider(id) ON DELETE RESTRICT, fiatPrice REAL NOT NULL, resolution TEXT NOT NULL CHECK (resolution IN ('day','timestamp')), observedAt TEXT NOT NULL, sourceName TEXT, createdAt TEXT, updatedAt TEXT, UNIQUE (assetId, providerId, observedAt, resolution) ) STRICT`); const oud = new Set(all(`PRAGMA table_info(PriceHistoryOud)`).map(c => c.name)); const kolommen = ['id', 'assetId', 'providerId', 'fiatPrice', 'resolution', 'observedAt', 'createdAt', 'updatedAt'] .filter(k => oud.has(k)).join(', '); run(`INSERT INTO PriceHistory (${kolommen}) SELECT ${kolommen} FROM PriceHistoryOud`); run(`DROP TABLE PriceHistoryOud`); run(`CREATE INDEX IxPriceHistoryProviderId ON PriceHistory(providerId)`); run(`CREATE INDEX IxPriceHistoryAssetIdObservedAt ON PriceHistory(assetId, observedAt)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%PriceHistoryOud%'`) .filter(r => /\bPriceHistoryOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 128 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } } function rebuildEvent126() { run(`PRAGMA foreign_keys = OFF`); run(`PRAGMA legacy_alter_table = ON`); try { run(`BEGIN`); run(`ALTER TABLE Event RENAME TO EventOud`); run(`CREATE TABLE Event ( id INTEGER PRIMARY KEY, assetId INTEGER NOT NULL REFERENCES Asset(id) ON DELETE CASCADE, fromAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, toAccountId INTEGER REFERENCES Account(id) ON DELETE NO ACTION, productId INTEGER REFERENCES MetalProduct(id) ON DELETE NO ACTION, productCount INTEGER CHECK (productCount IS NULL OR productCount > 0), importReference TEXT, externalAddress TEXT, note TEXT, amount INTEGER NOT NULL CHECK (amount > 0), fiatPrice REAL CHECK (fiatPrice IS NULL OR fiatPrice >= 0), fiatFee REAL CHECK (fiatFee IS NULL OR fiatFee >= 0), unitFee INTEGER CHECK (unitFee IS NULL OR unitFee >= 0), priceOrigin TEXT CHECK (priceOrigin IS NULL OR priceOrigin IN ('paid','market_day','market_moment','none')), type TEXT NOT NULL CHECK (type IN ('buy','sell','transfer','interest','reservation')), source TEXT NOT NULL CHECK (source IN ('manual','import','reconcile','lightning')), occurredAt TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT, CHECK ((productId IS NULL) = (productCount IS NULL)), CHECK (type NOT IN ('buy','sell') OR fiatPrice IS NOT NULL), CHECK (type <> 'reservation' OR ((fromAccountId IS NULL) <> (toAccountId IS NULL) AND fiatPrice IS NULL AND fiatFee IS NULL AND unitFee IS NULL AND productId IS NULL AND priceOrigin IS NULL AND importReference IS NULL AND externalAddress IS NULL)) ) STRICT`); const kolommen = `id, assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress, note, amount, fiatPrice, fiatFee, unitFee, priceOrigin, type, source, occurredAt, createdAt, updatedAt`; run(`INSERT INTO Event (${kolommen}) SELECT ${kolommen} FROM EventOud`); run(`DROP TABLE EventOud`); run(`CREATE INDEX IxEventAssetId ON Event(assetId)`); run(`CREATE INDEX IxEventFromAccountId ON Event(fromAccountId)`); run(`CREATE INDEX IxEventToAccountId ON Event(toAccountId)`); run(`CREATE INDEX IxEventProductId ON Event(productId)`); run(`CREATE INDEX IxEventOccurredAt ON Event(occurredAt)`); run(`CREATE INDEX IxEventType ON Event(type)`); run(`CREATE INDEX IxEventExternalAddress ON Event(externalAddress)`); run(`CREATE INDEX IxEventImportReference ON Event(importReference)`); const besmet = all(`SELECT name, sql FROM sqlite_master WHERE sql LIKE '%EventOud%'`) .filter(r => /\bEventOud\b/.test(String(r.sql).replace(/--[^\n]*/g, ''))); if (besmet.length) { throw new Error( `Migration 126 aborted: ${besmet.map(r => r.name).join(', ')} still refers to a temporary ` + `table name. The step was rolled back and the database is unchanged.`); } run(`COMMIT`); } catch (e) { try { run(`ROLLBACK`); } catch { } throw e; } finally { run(`PRAGMA legacy_alter_table = OFF`); run(`PRAGMA foreign_keys = ON`); } } function serviceProviderRow() { const kolommen = ['key', 'name', 'searchName', 'baseUrl', 'authenticationKind', 'isSystem', 'createdAt']; const aanwezig = new Set(tableExists('Provider') ? all(`SELECT name FROM pragma_table_info('Provider')`).map(r => r.name) : []); if (!kolommen.every(k => aanwezig.has(k))) return; if (one(`SELECT 1 AS x FROM Provider WHERE key = 'atreides_services'`)) return; run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationHeaderName, authenticationParameterName, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('atreides_services', 'Atreides Services', 'atreides services', '', NULL, NULL, 'none', 0, 1, 1, 1, ?)`, [new Date().toISOString()]); } function tableExists(name) { return !!one(`SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?`, [name]); } function columnExists(table, column) { const r = one(`SELECT sql AS s FROM sqlite_master WHERE type = 'table' AND name = ?`, [table]); return !!r && new RegExp(`[(,]\\s*["\`\\[]?${column}["\`\\]]?\\s`, 'i').test(String(r.s)); } export function readVersion() { if (!tableExists('Schema')) { throw new Error( 'This database is from before the new layout and cannot be opened with this version of the ' + 'app. The conversion script was removed on 14-08-2026; it is still in the git history ' + '(git log --all -- scripts/baseline-convert.mjs), as is a checkout from before the baseline ' + 'commit.'); } const r = one(`SELECT version AS v FROM Schema`); if (!r) throw new Error('Cannot determine the schema version. The database is inconsistent.'); return r.v; } function writeVersion(v) { run(`UPDATE Schema SET version = ?`, [v]); } export function runMigrations() { let found = readVersion(); if (found > CONFIG.SCHEMA_VERSION) { throw new Error(`DB version ${found} is newer than the app (${CONFIG.SCHEMA_VERSION}).`); } if (found < BASELINE_VERSION) { throw new Error( `This database is at version ${found} and thus from before the new layout ` + `(version ${BASELINE_VERSION}). The migrations that would update it step by step no longer ` + `exist, and neither does the conversion script: see git log --all -- scripts/baseline-convert.mjs.`); } let migrated = false; while (found < CONFIG.SCHEMA_VERSION) { const next = found + 1; const m = MIGRATIONS[next]; if (!m) throw new Error(`The migration to version ${next} is missing.`); m(); writeVersion(next); found = next; migrated = true; } return migrated; }