Integrate crabrl parser into taxonomy hydration
This commit is contained in:
@@ -1,96 +1,239 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { __dbInternals } from './index';
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { __dbInternals } from "./index";
|
||||
|
||||
function applyMigration(client: Database, fileName: string) {
|
||||
const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8');
|
||||
const sql = readFileSync(join(process.cwd(), "drizzle", fileName), "utf8");
|
||||
client.exec(sql);
|
||||
}
|
||||
|
||||
describe('sqlite schema compatibility bootstrap', () => {
|
||||
it('adds missing watchlist columns and taxonomy tables for older local databases', () => {
|
||||
const client = new Database(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON;');
|
||||
describe("sqlite schema compatibility bootstrap", () => {
|
||||
it("adds missing watchlist columns and taxonomy tables for older local databases", () => {
|
||||
const client = new Database(":memory:");
|
||||
client.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
applyMigration(client, '0000_cold_silver_centurion.sql');
|
||||
applyMigration(client, '0001_glossy_statement_snapshots.sql');
|
||||
applyMigration(client, '0002_workflow_task_projection_metadata.sql');
|
||||
applyMigration(client, '0003_task_stage_event_timeline.sql');
|
||||
applyMigration(client, '0009_task_notification_context.sql');
|
||||
applyMigration(client, "0000_cold_silver_centurion.sql");
|
||||
applyMigration(client, "0001_glossy_statement_snapshots.sql");
|
||||
applyMigration(client, "0002_workflow_task_projection_metadata.sql");
|
||||
applyMigration(client, "0003_task_stage_event_timeline.sql");
|
||||
applyMigration(client, "0009_task_notification_context.sql");
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'category')).toBe(false);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'status')).toBe(false);
|
||||
expect(__dbInternals.hasColumn(client, 'holding', 'company_name')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_snapshot')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'research_journal_entry')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'research_artifact')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'research_memo')).toBe(false);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "category")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "status")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "holding", "company_name")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_snapshot")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(__dbInternals.hasTable(client, "research_journal_entry")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(__dbInternals.hasTable(client, "research_artifact")).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, "research_memo")).toBe(false);
|
||||
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'category')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'tags')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'status')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'priority')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'updated_at')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'watchlist_item', 'last_reviewed_at')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'holding', 'company_name')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_snapshot')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'parser_engine')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'parser_version')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'taxonomy_regime')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'faithful_rows')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'surface_rows')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'detail_rows')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'kpi_rows')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'normalization_summary')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_context')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_fact')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'balance')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'period_type')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'data_type')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'authoritative_concept_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'mapping_method')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'surface_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'detail_parent_surface_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'kpi_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_concept', 'residual_flag')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'data_type')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'authoritative_concept_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'mapping_method')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'surface_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'detail_parent_surface_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'kpi_key')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'residual_flag')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'precision')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_fact', 'nil')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'task_run', 'stage_context')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'task_stage_event', 'stage_context')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'research_journal_entry')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'search_document')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'search_chunk')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'research_artifact')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'research_memo')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'research_memo_evidence')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'company_overview_cache')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "category")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "tags")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "status")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "watchlist_item", "priority")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "watchlist_item", "updated_at"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "watchlist_item", "last_reviewed_at"),
|
||||
).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, "holding", "company_name")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_snapshot")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"parser_engine",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"parser_version",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"taxonomy_regime",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"faithful_rows",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"surface_rows",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"detail_rows",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_snapshot", "kpi_rows"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"computed_definitions",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"normalization_summary",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_context")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_fact")).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_concept", "balance"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_concept", "period_type"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_concept", "data_type"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_concept",
|
||||
"authoritative_concept_key",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_concept",
|
||||
"mapping_method",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_concept", "surface_key"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_concept",
|
||||
"detail_parent_surface_key",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_concept", "kpi_key"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_concept",
|
||||
"residual_flag",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "data_type"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_fact",
|
||||
"authoritative_concept_key",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "mapping_method"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "surface_key"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_fact",
|
||||
"detail_parent_surface_key",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "kpi_key"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "residual_flag"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "filing_taxonomy_fact", "precision"),
|
||||
).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, "filing_taxonomy_fact", "nil")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(__dbInternals.hasColumn(client, "task_run", "stage_context")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
__dbInternals.hasColumn(client, "task_stage_event", "stage_context"),
|
||||
).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "research_journal_entry")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "search_document")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "search_chunk")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "research_artifact")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "research_memo")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "research_memo_evidence")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "company_overview_cache")).toBe(true);
|
||||
|
||||
__dbInternals.loadSqliteExtensions(client);
|
||||
__dbInternals.ensureSearchVirtualTables(client);
|
||||
|
||||
expect(__dbInternals.hasTable(client, 'search_chunk_fts')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'search_chunk_vec')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "search_chunk_fts")).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "search_chunk_vec")).toBe(true);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('backfills legacy taxonomy snapshot sidecar columns and remains idempotent', () => {
|
||||
const client = new Database(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON;');
|
||||
it("backfills legacy taxonomy snapshot sidecar columns and remains idempotent", () => {
|
||||
const client = new Database(":memory:");
|
||||
client.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
applyMigration(client, '0000_cold_silver_centurion.sql');
|
||||
applyMigration(client, '0005_financial_taxonomy_v3.sql');
|
||||
applyMigration(client, "0000_cold_silver_centurion.sql");
|
||||
applyMigration(client, "0005_financial_taxonomy_v3.sql");
|
||||
|
||||
client.exec(`
|
||||
INSERT INTO \`filing\` (
|
||||
@@ -114,7 +257,8 @@ describe('sqlite schema compatibility bootstrap', () => {
|
||||
);
|
||||
`);
|
||||
|
||||
const statementRows = '{"income":[{"label":"Revenue","value":1}],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}';
|
||||
const statementRows =
|
||||
'{"income":[{"label":"Revenue","value":1}],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}';
|
||||
|
||||
client.exec(`
|
||||
INSERT INTO \`filing_taxonomy_snapshot\` (
|
||||
@@ -143,7 +287,9 @@ describe('sqlite schema compatibility bootstrap', () => {
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
|
||||
const row = client.query(`
|
||||
const row = client
|
||||
.query(
|
||||
`
|
||||
SELECT
|
||||
\`parser_engine\`,
|
||||
\`parser_version\`,
|
||||
@@ -152,10 +298,13 @@ describe('sqlite schema compatibility bootstrap', () => {
|
||||
\`surface_rows\`,
|
||||
\`detail_rows\`,
|
||||
\`kpi_rows\`,
|
||||
\`computed_definitions\`,
|
||||
\`normalization_summary\`
|
||||
FROM \`filing_taxonomy_snapshot\`
|
||||
WHERE \`filing_id\` = 1
|
||||
`).get() as {
|
||||
`,
|
||||
)
|
||||
.get() as {
|
||||
parser_engine: string;
|
||||
parser_version: string;
|
||||
taxonomy_regime: string;
|
||||
@@ -163,66 +312,116 @@ describe('sqlite schema compatibility bootstrap', () => {
|
||||
surface_rows: string | null;
|
||||
detail_rows: string | null;
|
||||
kpi_rows: string | null;
|
||||
computed_definitions: string | null;
|
||||
normalization_summary: string | null;
|
||||
};
|
||||
|
||||
expect(row.parser_engine).toBe('fiscal-xbrl');
|
||||
expect(row.parser_version).toBe('unknown');
|
||||
expect(row.taxonomy_regime).toBe('unknown');
|
||||
expect(row.parser_engine).toBe("fiscal-xbrl");
|
||||
expect(row.parser_version).toBe("unknown");
|
||||
expect(row.taxonomy_regime).toBe("unknown");
|
||||
expect(row.faithful_rows).toBe(statementRows);
|
||||
expect(row.surface_rows).toBe('{"income":[],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}');
|
||||
expect(row.detail_rows).toBe('{"income":{},"balance":{},"cash_flow":{},"equity":{},"comprehensive_income":{}}');
|
||||
expect(row.kpi_rows).toBe('[]');
|
||||
expect(row.surface_rows).toBe(
|
||||
'{"income":[],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}',
|
||||
);
|
||||
expect(row.detail_rows).toBe(
|
||||
'{"income":{},"balance":{},"cash_flow":{},"equity":{},"comprehensive_income":{}}',
|
||||
);
|
||||
expect(row.kpi_rows).toBe("[]");
|
||||
expect(row.computed_definitions).toBe("[]");
|
||||
expect(row.normalization_summary).toBeNull();
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('repairs partial taxonomy sidecar drift without requiring a table rebuild', () => {
|
||||
const client = new Database(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON;');
|
||||
it("repairs partial taxonomy sidecar drift without requiring a table rebuild", () => {
|
||||
const client = new Database(":memory:");
|
||||
client.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
applyMigration(client, '0000_cold_silver_centurion.sql');
|
||||
applyMigration(client, '0005_financial_taxonomy_v3.sql');
|
||||
client.exec("ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_engine` text NOT NULL DEFAULT 'legacy-ts';");
|
||||
applyMigration(client, "0000_cold_silver_centurion.sql");
|
||||
applyMigration(client, "0005_financial_taxonomy_v3.sql");
|
||||
client.exec(
|
||||
"ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_engine` text NOT NULL DEFAULT 'legacy-ts';",
|
||||
);
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'parser_engine')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'normalization_summary')).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_context')).toBe(false);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"parser_engine",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"normalization_summary",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_context")).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'parser_version')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'taxonomy_regime')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'normalization_summary')).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_context')).toBe(true);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('throws on missing parser_engine column when verifyCriticalSchema is called', () => {
|
||||
const client = new Database(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON;');
|
||||
|
||||
applyMigration(client, '0000_cold_silver_centurion.sql');
|
||||
applyMigration(client, '0005_financial_taxonomy_v3.sql');
|
||||
|
||||
expect(__dbInternals.hasTable(client, 'filing_taxonomy_snapshot')).toBe(true);
|
||||
expect(__dbInternals.hasColumn(client, 'filing_taxonomy_snapshot', 'parser_engine')).toBe(false);
|
||||
|
||||
expect(() => __dbInternals.verifyCriticalSchema(client)).toThrow(
|
||||
/filing_taxonomy_snapshot is missing columns: parser_engine/
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"parser_version",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"taxonomy_regime",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"normalization_summary",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_context")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('verifyCriticalSchema passes when all required columns exist', () => {
|
||||
const client = new Database(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON;');
|
||||
it("throws on missing parser_engine column when verifyCriticalSchema is called", () => {
|
||||
const client = new Database(":memory:");
|
||||
client.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
applyMigration(client, '0000_cold_silver_centurion.sql');
|
||||
applyMigration(client, '0005_financial_taxonomy_v3.sql');
|
||||
applyMigration(client, "0000_cold_silver_centurion.sql");
|
||||
applyMigration(client, "0005_financial_taxonomy_v3.sql");
|
||||
|
||||
expect(__dbInternals.hasTable(client, "filing_taxonomy_snapshot")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
__dbInternals.hasColumn(
|
||||
client,
|
||||
"filing_taxonomy_snapshot",
|
||||
"parser_engine",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(() => __dbInternals.verifyCriticalSchema(client)).toThrow(
|
||||
/filing_taxonomy_snapshot is missing columns: parser_engine/,
|
||||
);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it("verifyCriticalSchema passes when all required columns exist", () => {
|
||||
const client = new Database(":memory:");
|
||||
client.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
applyMigration(client, "0000_cold_silver_centurion.sql");
|
||||
applyMigration(client, "0005_financial_taxonomy_v3.sql");
|
||||
|
||||
__dbInternals.ensureLocalSqliteSchema(client);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Database } from "bun:sqlite";
|
||||
|
||||
const DEFAULT_SURFACE_ROWS_JSON = '{"income":[],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}';
|
||||
const DEFAULT_DETAIL_ROWS_JSON = '{"income":{},"balance":{},"cash_flow":{},"equity":{},"comprehensive_income":{}}';
|
||||
const DEFAULT_SURFACE_ROWS_JSON =
|
||||
'{"income":[],"balance":[],"cash_flow":[],"equity":[],"comprehensive_income":[]}';
|
||||
const DEFAULT_DETAIL_ROWS_JSON =
|
||||
'{"income":{},"balance":{},"cash_flow":{},"equity":{},"comprehensive_income":{}}';
|
||||
|
||||
type MissingColumnDefinition = {
|
||||
name: string;
|
||||
@@ -12,36 +14,49 @@ type MissingColumnDefinition = {
|
||||
|
||||
export function hasTable(client: Database, tableName: string) {
|
||||
const row = client
|
||||
.query('SELECT name FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1')
|
||||
.get('table', tableName) as { name: string } | null;
|
||||
.query("SELECT name FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1")
|
||||
.get("table", tableName) as { name: string } | null;
|
||||
|
||||
return row !== null;
|
||||
}
|
||||
|
||||
export function hasColumn(client: Database, tableName: string, columnName: string) {
|
||||
export function hasColumn(
|
||||
client: Database,
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
) {
|
||||
if (!hasTable(client, tableName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rows = client.query(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }>;
|
||||
const rows = client.query(`PRAGMA table_info(${tableName})`).all() as Array<{
|
||||
name: string;
|
||||
}>;
|
||||
return rows.some((row) => row.name === columnName);
|
||||
}
|
||||
|
||||
export function applySqlFile(client: Database, fileName: string) {
|
||||
const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8');
|
||||
const sql = readFileSync(join(process.cwd(), "drizzle", fileName), "utf8");
|
||||
client.exec(sql);
|
||||
}
|
||||
|
||||
export function applyBaseSchemaCompat(client: Database) {
|
||||
const sql = readFileSync(join(process.cwd(), 'drizzle', '0000_cold_silver_centurion.sql'), 'utf8')
|
||||
.replaceAll('CREATE TABLE `', 'CREATE TABLE IF NOT EXISTS `')
|
||||
.replaceAll('CREATE UNIQUE INDEX `', 'CREATE UNIQUE INDEX IF NOT EXISTS `')
|
||||
.replaceAll('CREATE INDEX `', 'CREATE INDEX IF NOT EXISTS `');
|
||||
const sql = readFileSync(
|
||||
join(process.cwd(), "drizzle", "0000_cold_silver_centurion.sql"),
|
||||
"utf8",
|
||||
)
|
||||
.replaceAll("CREATE TABLE `", "CREATE TABLE IF NOT EXISTS `")
|
||||
.replaceAll("CREATE UNIQUE INDEX `", "CREATE UNIQUE INDEX IF NOT EXISTS `")
|
||||
.replaceAll("CREATE INDEX `", "CREATE INDEX IF NOT EXISTS `");
|
||||
|
||||
client.exec(sql);
|
||||
}
|
||||
|
||||
function ensureColumns(client: Database, tableName: string, columns: MissingColumnDefinition[]) {
|
||||
function ensureColumns(
|
||||
client: Database,
|
||||
tableName: string,
|
||||
columns: MissingColumnDefinition[],
|
||||
) {
|
||||
if (!hasTable(client, tableName)) {
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +69,7 @@ function ensureColumns(client: Database, tableName: string, columns: MissingColu
|
||||
}
|
||||
|
||||
function ensureResearchWorkspaceSchema(client: Database) {
|
||||
if (!hasTable(client, 'research_artifact')) {
|
||||
if (!hasTable(client, "research_artifact")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`research_artifact\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
@@ -84,7 +99,7 @@ function ensureResearchWorkspaceSchema(client: Database) {
|
||||
`);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'research_memo')) {
|
||||
if (!hasTable(client, "research_memo")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`research_memo\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
@@ -110,7 +125,7 @@ function ensureResearchWorkspaceSchema(client: Database) {
|
||||
`);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'research_memo_evidence')) {
|
||||
if (!hasTable(client, "research_memo_evidence")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`research_memo_evidence\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
@@ -126,15 +141,33 @@ function ensureResearchWorkspaceSchema(client: Database) {
|
||||
`);
|
||||
}
|
||||
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_artifact_ticker_idx` ON `research_artifact` (`user_id`, `ticker`, `updated_at`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_artifact_kind_idx` ON `research_artifact` (`user_id`, `kind`, `updated_at`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_artifact_accession_idx` ON `research_artifact` (`user_id`, `accession_number`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_artifact_source_idx` ON `research_artifact` (`user_id`, `source`, `updated_at`);');
|
||||
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `research_memo_ticker_uidx` ON `research_memo` (`user_id`, `ticker`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_memo_updated_idx` ON `research_memo` (`user_id`, `updated_at`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_memo_evidence_memo_idx` ON `research_memo_evidence` (`memo_id`, `section`, `sort_order`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_memo_evidence_artifact_idx` ON `research_memo_evidence` (`artifact_id`);');
|
||||
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `research_memo_evidence_unique_uidx` ON `research_memo_evidence` (`memo_id`, `artifact_id`, `section`);');
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_artifact_ticker_idx` ON `research_artifact` (`user_id`, `ticker`, `updated_at`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_artifact_kind_idx` ON `research_artifact` (`user_id`, `kind`, `updated_at`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_artifact_accession_idx` ON `research_artifact` (`user_id`, `accession_number`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_artifact_source_idx` ON `research_artifact` (`user_id`, `source`, `updated_at`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `research_memo_ticker_uidx` ON `research_memo` (`user_id`, `ticker`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_memo_updated_idx` ON `research_memo` (`user_id`, `updated_at`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_memo_evidence_memo_idx` ON `research_memo_evidence` (`memo_id`, `section`, `sort_order`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_memo_evidence_artifact_idx` ON `research_memo_evidence` (`artifact_id`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `research_memo_evidence_unique_uidx` ON `research_memo_evidence` (`memo_id`, `artifact_id`, `section`);",
|
||||
);
|
||||
client.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS \`research_artifact_fts\` USING fts5(
|
||||
artifact_id UNINDEXED,
|
||||
@@ -268,7 +301,7 @@ function ensureResearchWorkspaceSchema(client: Database) {
|
||||
);
|
||||
`);
|
||||
|
||||
client.exec('DELETE FROM `research_artifact_fts`;');
|
||||
client.exec("DELETE FROM `research_artifact_fts`;");
|
||||
client.exec(`
|
||||
INSERT INTO \`research_artifact_fts\` (
|
||||
\`artifact_id\`,
|
||||
@@ -297,39 +330,71 @@ function ensureResearchWorkspaceSchema(client: Database) {
|
||||
}
|
||||
|
||||
const TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS = [
|
||||
'parser_engine',
|
||||
'parser_version',
|
||||
'taxonomy_regime',
|
||||
'fiscal_pack',
|
||||
'faithful_rows',
|
||||
'surface_rows',
|
||||
'detail_rows',
|
||||
'kpi_rows',
|
||||
'normalization_summary'
|
||||
"parser_engine",
|
||||
"parser_version",
|
||||
"taxonomy_regime",
|
||||
"fiscal_pack",
|
||||
"faithful_rows",
|
||||
"surface_rows",
|
||||
"detail_rows",
|
||||
"kpi_rows",
|
||||
"computed_definitions",
|
||||
"normalization_summary",
|
||||
] as const;
|
||||
|
||||
function ensureTaxonomySnapshotCompat(client: Database) {
|
||||
if (!hasTable(client, 'filing_taxonomy_snapshot')) {
|
||||
if (!hasTable(client, "filing_taxonomy_snapshot")) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureColumns(client, 'filing_taxonomy_snapshot', [
|
||||
{ name: 'parser_engine', sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_engine` text NOT NULL DEFAULT 'fiscal-xbrl';" },
|
||||
{ name: 'parser_version', sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_version` text NOT NULL DEFAULT 'unknown';" },
|
||||
{ name: 'taxonomy_regime', sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `taxonomy_regime` text NOT NULL DEFAULT 'unknown';" },
|
||||
{ name: 'fiscal_pack', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `fiscal_pack` text;' },
|
||||
{ name: 'faithful_rows', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `faithful_rows` text;' },
|
||||
{ name: 'surface_rows', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `surface_rows` text;' },
|
||||
{ name: 'detail_rows', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `detail_rows` text;' },
|
||||
{ name: 'kpi_rows', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `kpi_rows` text;' },
|
||||
{ name: 'normalization_summary', sql: 'ALTER TABLE `filing_taxonomy_snapshot` ADD `normalization_summary` text;' }
|
||||
ensureColumns(client, "filing_taxonomy_snapshot", [
|
||||
{
|
||||
name: "parser_engine",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_engine` text NOT NULL DEFAULT 'fiscal-xbrl';",
|
||||
},
|
||||
{
|
||||
name: "parser_version",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `parser_version` text NOT NULL DEFAULT 'unknown';",
|
||||
},
|
||||
{
|
||||
name: "taxonomy_regime",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `taxonomy_regime` text NOT NULL DEFAULT 'unknown';",
|
||||
},
|
||||
{
|
||||
name: "fiscal_pack",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `fiscal_pack` text;",
|
||||
},
|
||||
{
|
||||
name: "faithful_rows",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `faithful_rows` text;",
|
||||
},
|
||||
{
|
||||
name: "surface_rows",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `surface_rows` text;",
|
||||
},
|
||||
{
|
||||
name: "detail_rows",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `detail_rows` text;",
|
||||
},
|
||||
{
|
||||
name: "kpi_rows",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `kpi_rows` text;",
|
||||
},
|
||||
{
|
||||
name: "computed_definitions",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `computed_definitions` text;",
|
||||
},
|
||||
{
|
||||
name: "normalization_summary",
|
||||
sql: "ALTER TABLE `filing_taxonomy_snapshot` ADD `normalization_summary` text;",
|
||||
},
|
||||
]);
|
||||
|
||||
for (const columnName of TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS) {
|
||||
if (!hasColumn(client, 'filing_taxonomy_snapshot', columnName)) {
|
||||
if (!hasColumn(client, "filing_taxonomy_snapshot", columnName)) {
|
||||
throw new Error(
|
||||
`Schema compat failed: filing_taxonomy_snapshot missing required column '${columnName}'. ` +
|
||||
`Delete the database file and restart to rebuild schema.`
|
||||
`Delete the database file and restart to rebuild schema.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -340,12 +405,13 @@ function ensureTaxonomySnapshotCompat(client: Database) {
|
||||
\`faithful_rows\` = COALESCE(\`faithful_rows\`, \`statement_rows\`),
|
||||
\`surface_rows\` = COALESCE(\`surface_rows\`, '${DEFAULT_SURFACE_ROWS_JSON}'),
|
||||
\`detail_rows\` = COALESCE(\`detail_rows\`, '${DEFAULT_DETAIL_ROWS_JSON}'),
|
||||
\`kpi_rows\` = COALESCE(\`kpi_rows\`, '[]');
|
||||
\`kpi_rows\` = COALESCE(\`kpi_rows\`, '[]'),
|
||||
\`computed_definitions\` = COALESCE(\`computed_definitions\`, '[]');
|
||||
`);
|
||||
}
|
||||
|
||||
function ensureTaxonomyContextCompat(client: Database) {
|
||||
if (!hasTable(client, 'filing_taxonomy_context')) {
|
||||
if (!hasTable(client, "filing_taxonomy_context")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`filing_taxonomy_context\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
@@ -364,35 +430,93 @@ function ensureTaxonomyContextCompat(client: Database) {
|
||||
`);
|
||||
}
|
||||
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `filing_taxonomy_context_snapshot_idx` ON `filing_taxonomy_context` (`snapshot_id`);');
|
||||
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `filing_taxonomy_context_uidx` ON `filing_taxonomy_context` (`snapshot_id`,`context_id`);');
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `filing_taxonomy_context_snapshot_idx` ON `filing_taxonomy_context` (`snapshot_id`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `filing_taxonomy_context_uidx` ON `filing_taxonomy_context` (`snapshot_id`,`context_id`);",
|
||||
);
|
||||
}
|
||||
|
||||
function ensureTaxonomyConceptCompat(client: Database) {
|
||||
ensureColumns(client, 'filing_taxonomy_concept', [
|
||||
{ name: 'balance', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `balance` text;' },
|
||||
{ name: 'period_type', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `period_type` text;' },
|
||||
{ name: 'data_type', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `data_type` text;' },
|
||||
{ name: 'authoritative_concept_key', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `authoritative_concept_key` text;' },
|
||||
{ name: 'mapping_method', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `mapping_method` text;' },
|
||||
{ name: 'surface_key', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `surface_key` text;' },
|
||||
{ name: 'detail_parent_surface_key', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `detail_parent_surface_key` text;' },
|
||||
{ name: 'kpi_key', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `kpi_key` text;' },
|
||||
{ name: 'residual_flag', sql: 'ALTER TABLE `filing_taxonomy_concept` ADD `residual_flag` integer NOT NULL DEFAULT false;' }
|
||||
ensureColumns(client, "filing_taxonomy_concept", [
|
||||
{
|
||||
name: "balance",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `balance` text;",
|
||||
},
|
||||
{
|
||||
name: "period_type",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `period_type` text;",
|
||||
},
|
||||
{
|
||||
name: "data_type",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `data_type` text;",
|
||||
},
|
||||
{
|
||||
name: "authoritative_concept_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `authoritative_concept_key` text;",
|
||||
},
|
||||
{
|
||||
name: "mapping_method",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `mapping_method` text;",
|
||||
},
|
||||
{
|
||||
name: "surface_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `surface_key` text;",
|
||||
},
|
||||
{
|
||||
name: "detail_parent_surface_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `detail_parent_surface_key` text;",
|
||||
},
|
||||
{
|
||||
name: "kpi_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `kpi_key` text;",
|
||||
},
|
||||
{
|
||||
name: "residual_flag",
|
||||
sql: "ALTER TABLE `filing_taxonomy_concept` ADD `residual_flag` integer NOT NULL DEFAULT false;",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function ensureTaxonomyFactCompat(client: Database) {
|
||||
ensureColumns(client, 'filing_taxonomy_fact', [
|
||||
{ name: 'data_type', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `data_type` text;' },
|
||||
{ name: 'authoritative_concept_key', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `authoritative_concept_key` text;' },
|
||||
{ name: 'mapping_method', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `mapping_method` text;' },
|
||||
{ name: 'surface_key', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `surface_key` text;' },
|
||||
{ name: 'detail_parent_surface_key', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `detail_parent_surface_key` text;' },
|
||||
{ name: 'kpi_key', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `kpi_key` text;' },
|
||||
{ name: 'residual_flag', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `residual_flag` integer NOT NULL DEFAULT false;' },
|
||||
{ name: 'precision', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `precision` text;' },
|
||||
{ name: 'nil', sql: 'ALTER TABLE `filing_taxonomy_fact` ADD `nil` integer NOT NULL DEFAULT false;' }
|
||||
ensureColumns(client, "filing_taxonomy_fact", [
|
||||
{
|
||||
name: "data_type",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `data_type` text;",
|
||||
},
|
||||
{
|
||||
name: "authoritative_concept_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `authoritative_concept_key` text;",
|
||||
},
|
||||
{
|
||||
name: "mapping_method",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `mapping_method` text;",
|
||||
},
|
||||
{
|
||||
name: "surface_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `surface_key` text;",
|
||||
},
|
||||
{
|
||||
name: "detail_parent_surface_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `detail_parent_surface_key` text;",
|
||||
},
|
||||
{
|
||||
name: "kpi_key",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `kpi_key` text;",
|
||||
},
|
||||
{
|
||||
name: "residual_flag",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `residual_flag` integer NOT NULL DEFAULT false;",
|
||||
},
|
||||
{
|
||||
name: "precision",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `precision` text;",
|
||||
},
|
||||
{
|
||||
name: "nil",
|
||||
sql: "ALTER TABLE `filing_taxonomy_fact` ADD `nil` integer NOT NULL DEFAULT false;",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -405,18 +529,18 @@ function ensureTaxonomyCompat(client: Database) {
|
||||
|
||||
export function ensureLocalSqliteSchema(client: Database) {
|
||||
const missingBaseSchema = [
|
||||
'filing',
|
||||
'watchlist_item',
|
||||
'holding',
|
||||
'task_run',
|
||||
'portfolio_insight'
|
||||
"filing",
|
||||
"watchlist_item",
|
||||
"holding",
|
||||
"task_run",
|
||||
"portfolio_insight",
|
||||
].some((tableName) => !hasTable(client, tableName));
|
||||
|
||||
if (missingBaseSchema) {
|
||||
applyBaseSchemaCompat(client);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'user')) {
|
||||
if (!hasTable(client, "user")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`user\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
@@ -432,10 +556,12 @@ export function ensureLocalSqliteSchema(client: Database) {
|
||||
\`banExpires\` integer
|
||||
);
|
||||
`);
|
||||
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `user_email_uidx` ON `user` (`email`);');
|
||||
client.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `user_email_uidx` ON `user` (`email`);",
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'organization')) {
|
||||
if (!hasTable(client, "organization")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`organization\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
@@ -446,46 +572,86 @@ export function ensureLocalSqliteSchema(client: Database) {
|
||||
\`metadata\` text
|
||||
);
|
||||
`);
|
||||
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `organization_slug_uidx` ON `organization` (`slug`);');
|
||||
client.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `organization_slug_uidx` ON `organization` (`slug`);",
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'filing_statement_snapshot')) {
|
||||
applySqlFile(client, '0001_glossy_statement_snapshots.sql');
|
||||
if (!hasTable(client, "filing_statement_snapshot")) {
|
||||
applySqlFile(client, "0001_glossy_statement_snapshots.sql");
|
||||
}
|
||||
|
||||
ensureColumns(client, 'task_run', [
|
||||
{ name: 'stage', sql: "ALTER TABLE `task_run` ADD `stage` text NOT NULL DEFAULT 'queued';" },
|
||||
{ name: 'stage_detail', sql: 'ALTER TABLE `task_run` ADD `stage_detail` text;' },
|
||||
{ name: 'stage_context', sql: 'ALTER TABLE `task_run` ADD `stage_context` text;' },
|
||||
{ name: 'resource_key', sql: 'ALTER TABLE `task_run` ADD `resource_key` text;' },
|
||||
{ name: 'notification_read_at', sql: 'ALTER TABLE `task_run` ADD `notification_read_at` text;' },
|
||||
{ name: 'notification_silenced_at', sql: 'ALTER TABLE `task_run` ADD `notification_silenced_at` text;' }
|
||||
ensureColumns(client, "task_run", [
|
||||
{
|
||||
name: "stage",
|
||||
sql: "ALTER TABLE `task_run` ADD `stage` text NOT NULL DEFAULT 'queued';",
|
||||
},
|
||||
{
|
||||
name: "stage_detail",
|
||||
sql: "ALTER TABLE `task_run` ADD `stage_detail` text;",
|
||||
},
|
||||
{
|
||||
name: "stage_context",
|
||||
sql: "ALTER TABLE `task_run` ADD `stage_context` text;",
|
||||
},
|
||||
{
|
||||
name: "resource_key",
|
||||
sql: "ALTER TABLE `task_run` ADD `resource_key` text;",
|
||||
},
|
||||
{
|
||||
name: "notification_read_at",
|
||||
sql: "ALTER TABLE `task_run` ADD `notification_read_at` text;",
|
||||
},
|
||||
{
|
||||
name: "notification_silenced_at",
|
||||
sql: "ALTER TABLE `task_run` ADD `notification_silenced_at` text;",
|
||||
},
|
||||
]);
|
||||
|
||||
if (!hasTable(client, 'task_stage_event')) {
|
||||
applySqlFile(client, '0003_task_stage_event_timeline.sql');
|
||||
if (!hasTable(client, "task_stage_event")) {
|
||||
applySqlFile(client, "0003_task_stage_event_timeline.sql");
|
||||
}
|
||||
|
||||
if (hasTable(client, 'task_stage_event') && !hasColumn(client, 'task_stage_event', 'stage_context')) {
|
||||
client.exec('ALTER TABLE `task_stage_event` ADD `stage_context` text;');
|
||||
if (
|
||||
hasTable(client, "task_stage_event") &&
|
||||
!hasColumn(client, "task_stage_event", "stage_context")
|
||||
) {
|
||||
client.exec("ALTER TABLE `task_stage_event` ADD `stage_context` text;");
|
||||
}
|
||||
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `task_user_updated_idx` ON `task_run` (`user_id`, `updated_at`);');
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `task_user_updated_idx` ON `task_run` (`user_id`, `updated_at`);",
|
||||
);
|
||||
|
||||
client.exec(`CREATE UNIQUE INDEX IF NOT EXISTS task_active_resource_uidx
|
||||
ON task_run (user_id, task_type, resource_key)
|
||||
WHERE resource_key IS NOT NULL AND status IN ('queued', 'running');`);
|
||||
|
||||
ensureColumns(client, 'watchlist_item', [
|
||||
{ name: 'category', sql: 'ALTER TABLE `watchlist_item` ADD `category` text;' },
|
||||
{ name: 'tags', sql: 'ALTER TABLE `watchlist_item` ADD `tags` text;' },
|
||||
{ name: 'status', sql: "ALTER TABLE `watchlist_item` ADD `status` text NOT NULL DEFAULT 'backlog';" },
|
||||
{ name: 'priority', sql: "ALTER TABLE `watchlist_item` ADD `priority` text NOT NULL DEFAULT 'medium';" },
|
||||
{ name: 'updated_at', sql: "ALTER TABLE `watchlist_item` ADD `updated_at` text NOT NULL DEFAULT '';" },
|
||||
{ name: 'last_reviewed_at', sql: 'ALTER TABLE `watchlist_item` ADD `last_reviewed_at` text;' }
|
||||
ensureColumns(client, "watchlist_item", [
|
||||
{
|
||||
name: "category",
|
||||
sql: "ALTER TABLE `watchlist_item` ADD `category` text;",
|
||||
},
|
||||
{ name: "tags", sql: "ALTER TABLE `watchlist_item` ADD `tags` text;" },
|
||||
{
|
||||
name: "status",
|
||||
sql: "ALTER TABLE `watchlist_item` ADD `status` text NOT NULL DEFAULT 'backlog';",
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
sql: "ALTER TABLE `watchlist_item` ADD `priority` text NOT NULL DEFAULT 'medium';",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
sql: "ALTER TABLE `watchlist_item` ADD `updated_at` text NOT NULL DEFAULT '';",
|
||||
},
|
||||
{
|
||||
name: "last_reviewed_at",
|
||||
sql: "ALTER TABLE `watchlist_item` ADD `last_reviewed_at` text;",
|
||||
},
|
||||
]);
|
||||
|
||||
if (hasTable(client, 'watchlist_item')) {
|
||||
if (hasTable(client, "watchlist_item")) {
|
||||
client.exec(`
|
||||
UPDATE \`watchlist_item\`
|
||||
SET
|
||||
@@ -503,27 +669,32 @@ WHERE resource_key IS NOT NULL AND status IN ('queued', 'running');`);
|
||||
END;
|
||||
`);
|
||||
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `watchlist_user_updated_idx` ON `watchlist_item` (`user_id`, `updated_at`);');
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `watchlist_user_updated_idx` ON `watchlist_item` (`user_id`, `updated_at`);",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTable(client, 'holding') && !hasColumn(client, 'holding', 'company_name')) {
|
||||
client.exec('ALTER TABLE `holding` ADD `company_name` text;');
|
||||
if (
|
||||
hasTable(client, "holding") &&
|
||||
!hasColumn(client, "holding", "company_name")
|
||||
) {
|
||||
client.exec("ALTER TABLE `holding` ADD `company_name` text;");
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'filing_taxonomy_snapshot')) {
|
||||
applySqlFile(client, '0005_financial_taxonomy_v3.sql');
|
||||
if (!hasTable(client, "filing_taxonomy_snapshot")) {
|
||||
applySqlFile(client, "0005_financial_taxonomy_v3.sql");
|
||||
}
|
||||
ensureTaxonomyCompat(client);
|
||||
|
||||
if (!hasTable(client, 'company_financial_bundle')) {
|
||||
applySqlFile(client, '0007_company_financial_bundles.sql');
|
||||
if (!hasTable(client, "company_financial_bundle")) {
|
||||
applySqlFile(client, "0007_company_financial_bundles.sql");
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'company_overview_cache')) {
|
||||
applySqlFile(client, '0012_company_overview_cache.sql');
|
||||
if (!hasTable(client, "company_overview_cache")) {
|
||||
applySqlFile(client, "0012_company_overview_cache.sql");
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'research_journal_entry')) {
|
||||
if (!hasTable(client, "research_journal_entry")) {
|
||||
client.exec(`
|
||||
CREATE TABLE IF NOT EXISTS \`research_journal_entry\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
@@ -539,12 +710,16 @@ WHERE resource_key IS NOT NULL AND status IN ('queued', 'running');`);
|
||||
FOREIGN KEY (\`user_id\`) REFERENCES \`user\`(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`);
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_journal_ticker_idx` ON `research_journal_entry` (`user_id`, `ticker`, `created_at`);');
|
||||
client.exec('CREATE INDEX IF NOT EXISTS `research_journal_accession_idx` ON `research_journal_entry` (`user_id`, `accession_number`);');
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_journal_ticker_idx` ON `research_journal_entry` (`user_id`, `ticker`, `created_at`);",
|
||||
);
|
||||
client.exec(
|
||||
"CREATE INDEX IF NOT EXISTS `research_journal_accession_idx` ON `research_journal_entry` (`user_id`, `accession_number`);",
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasTable(client, 'search_document')) {
|
||||
applySqlFile(client, '0008_search_rag.sql');
|
||||
if (!hasTable(client, "search_document")) {
|
||||
applySqlFile(client, "0008_search_rag.sql");
|
||||
}
|
||||
|
||||
ensureResearchWorkspaceSchema(client);
|
||||
@@ -555,7 +730,7 @@ export const __sqliteSchemaCompatInternals = {
|
||||
applySqlFile,
|
||||
hasColumn,
|
||||
hasTable,
|
||||
TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS
|
||||
TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS,
|
||||
};
|
||||
|
||||
export { TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS };
|
||||
|
||||
Reference in New Issue
Block a user