-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdb.ts
More file actions
2275 lines (2105 loc) · 75 KB
/
Copy pathdb.ts
File metadata and controls
2275 lines (2105 loc) · 75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { openCodemapDatabase } from "./sqlite-db";
import type { CodemapDatabase, BindValues } from "./sqlite-db";
/** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns).
* See `docs/architecture.md` § Schema Versioning. */
export const SCHEMA_VERSION = 40;
/** Moat-A: default call-graph surfaces exclude callback-synthesis edges. */
export const CALLS_AST_ONLY_SQL = "(provenance IS NULL OR provenance = 'ast')";
/**
* `meta` key tracking the FTS5 state at the last reindex; mismatch with the
* current resolved config triggers a forced `--full` rebuild
* (`docs/plans/fts5-mermaid.md` Q3).
*/
export const META_FTS5_ENABLED_KEY = "fts5_enabled";
export type { CodemapDatabase };
export function openDb(): CodemapDatabase {
return openCodemapDatabase();
}
export function closeDb(db: CodemapDatabase, opts?: { readonly?: boolean }) {
try {
if (!opts?.readonly) {
db.run("PRAGMA analysis_limit = 400");
db.run("PRAGMA optimize");
}
} finally {
db.close();
}
}
export function createTables(db: CodemapDatabase) {
db.run(`
CREATE TABLE IF NOT EXISTS files (
path TEXT PRIMARY KEY,
content_hash TEXT NOT NULL,
size INTEGER NOT NULL,
line_count INTEGER NOT NULL,
language TEXT NOT NULL,
last_modified INTEGER NOT NULL,
indexed_at INTEGER NOT NULL,
is_barrel INTEGER NOT NULL DEFAULT 0,
has_side_effects INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS symbols (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
signature TEXT NOT NULL,
is_exported INTEGER NOT NULL DEFAULT 0,
is_default_export INTEGER NOT NULL DEFAULT 0,
members TEXT,
doc_comment TEXT,
value TEXT,
parent_name TEXT,
visibility TEXT,
complexity REAL,
cognitive_complexity INTEGER,
name_column_start INTEGER NOT NULL DEFAULT 0,
name_column_end INTEGER NOT NULL DEFAULT 0,
scope_local_id INTEGER NOT NULL DEFAULT 0,
body_line_count INTEGER,
param_count INTEGER,
nesting_depth INTEGER,
return_type TEXT,
is_async INTEGER NOT NULL DEFAULT 0,
is_generator INTEGER NOT NULL DEFAULT 0,
body_hash TEXT
) STRICT;
-- One row per indexed file. Pure counters from the AST walk.
-- Joins to files(path).
CREATE TABLE IF NOT EXISTS file_metrics (
file_path TEXT PRIMARY KEY REFERENCES files(path) ON DELETE CASCADE,
total_lines INTEGER NOT NULL,
code_lines INTEGER NOT NULL,
blank_lines INTEGER NOT NULL,
comment_lines INTEGER NOT NULL,
let_count INTEGER NOT NULL DEFAULT 0,
const_count INTEGER NOT NULL DEFAULT 0,
var_count INTEGER NOT NULL DEFAULT 0,
function_count INTEGER NOT NULL DEFAULT 0,
arrow_count INTEGER NOT NULL DEFAULT 0,
class_count INTEGER NOT NULL DEFAULT 0,
interface_count INTEGER NOT NULL DEFAULT 0,
export_count INTEGER NOT NULL DEFAULT 0
) STRICT;
-- Git churn metrics per indexed file (populated each index pass via churn-ingest, golden uses seed-file-churn).
CREATE TABLE IF NOT EXISTS file_churn (
file_path TEXT PRIMARY KEY REFERENCES files(path) ON DELETE CASCADE,
commit_count INTEGER NOT NULL,
weighted_commits REAL NOT NULL,
lines_added INTEGER NOT NULL,
lines_removed INTEGER NOT NULL,
last_commit_at TEXT,
churn_trend TEXT,
computed_at TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS imports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
source TEXT NOT NULL,
resolved_path TEXT,
specifiers TEXT NOT NULL,
is_type_only INTEGER NOT NULL DEFAULT 0,
line_number INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS exports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
re_export_source TEXT,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
is_re_export INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS components (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
props_type TEXT,
hooks_used TEXT NOT NULL,
is_default_export INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS dependencies (
from_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
to_path TEXT NOT NULL,
PRIMARY KEY (from_path, to_path)
) STRICT, WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS markers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
line_number INTEGER NOT NULL,
kind TEXT NOT NULL,
content TEXT NOT NULL,
column_start INTEGER NOT NULL DEFAULT 0,
column_end INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS css_variables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
value TEXT,
scope TEXT NOT NULL,
line_number INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS css_classes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
is_module INTEGER NOT NULL DEFAULT 0,
line_number INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS css_keyframes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
line_number INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
caller_name TEXT NOT NULL,
caller_scope TEXT NOT NULL,
callee_name TEXT NOT NULL,
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
args_count INTEGER,
is_method_call INTEGER NOT NULL DEFAULT 0,
is_constructor_call INTEGER NOT NULL DEFAULT 0,
is_optional_chain INTEGER NOT NULL DEFAULT 0,
callee_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL,
callee_resolution_kind TEXT CHECK (
callee_resolution_kind IS NULL OR callee_resolution_kind IN (
'same-file', 'imported', 're-exported', 'global', 'unresolved'
)
),
provenance TEXT CHECK (
provenance IS NULL OR provenance IN ('ast', 'heuristic')
)
) STRICT;
-- Phase-1 staging queue for call sites pending cross-file callee resolution.
CREATE TABLE IF NOT EXISTS unresolved_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
caller_scope TEXT,
callee_name TEXT NOT NULL,
line_start INTEGER NOT NULL,
column_start INTEGER,
reference_kind TEXT NOT NULL DEFAULT 'call',
created_at TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS type_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
symbol_name TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT,
is_optional INTEGER NOT NULL DEFAULT 0,
is_readonly INTEGER NOT NULL DEFAULT 0
) STRICT;
-- Class/interface extends + implements edges (one row per base).
CREATE TABLE IF NOT EXISTS type_heritage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
child_file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
child_name TEXT NOT NULL,
child_kind TEXT NOT NULL,
child_line_start INTEGER NOT NULL,
relation TEXT NOT NULL CHECK (relation IN ('extends', 'implements')),
base_simple_name TEXT NOT NULL,
base_qualified_name TEXT,
base_file_path TEXT,
base_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL,
resolution_kind TEXT NOT NULL CHECK (
resolution_kind IN ('same-file', 'imported', 'qualified-unresolved', 'unresolved')
),
type_args TEXT
) STRICT;
-- Lexical scope graph per R.11. Block/for/catch deferred — body refs
-- resolve to the enclosing function/method scope (conservative escape
-- valve). local_id is parser-assigned so refs avoid SQLite rowid
-- round-trips.
CREATE TABLE IF NOT EXISTS scopes (
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
local_id INTEGER NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('module','function','arrow','class','method','interface','type-alias','for','catch')),
parent_local_id INTEGER,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
owner_symbol_name TEXT,
PRIMARY KEY (file_path, local_id)
) STRICT, WITHOUT ROWID;
-- Identifier USEs per R.11 (kinds: value/type/jsx). is_write per R.13.
-- Compound assign emits two rows, declaration-with-init emits write
-- only. scope_local_id joins scopes(local_id), 0 = module.
CREATE TABLE IF NOT EXISTS "references" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('value','type','jsx','member')),
scope_local_id INTEGER NOT NULL DEFAULT 0,
is_write INTEGER NOT NULL DEFAULT 0
) STRICT;
-- Per R.12. resolved_symbol_id NULL when is_external/global/unresolved.
-- Re-export chain walk defers to Tier 6.
CREATE TABLE IF NOT EXISTS bindings (
reference_id INTEGER PRIMARY KEY REFERENCES "references"(id) ON DELETE CASCADE,
resolved_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL,
resolution_kind TEXT NOT NULL CHECK (resolution_kind IN (
'same-file','imported','re-exported','global','unresolved'
)),
is_external INTEGER NOT NULL DEFAULT 0
) STRICT, WITHOUT ROWID;
-- Test suite metadata: describe / it / test / suite blocks with
-- their hierarchy + skip/only/todo flags. framework is detected
-- from imports (vitest / jest / bun-test / node-test / mocha) and
-- defaults to 'unknown' when no test framework import is found
-- in the file. parent_suite_id is NULL for top-level blocks.
CREATE TABLE IF NOT EXISTS test_suites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('describe','it','test','suite','context')),
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
parent_suite_id INTEGER REFERENCES test_suites(id) ON DELETE CASCADE,
is_skipped INTEGER NOT NULL DEFAULT 0,
is_only INTEGER NOT NULL DEFAULT 0,
is_todo INTEGER NOT NULL DEFAULT 0,
framework TEXT NOT NULL CHECK (framework IN ('vitest','jest','bun-test','node-test','mocha','unknown'))
) STRICT;
-- Runtime markers — operational signals worth auditing: console
-- calls, debugger statements, raw throws, process.env reads. detail
-- is the qualifier (console.log → 'log', throw → expression text,
-- process.env.X → 'X').
CREATE TABLE IF NOT EXISTS runtime_markers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('console','debugger','throw','process-env')),
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
detail TEXT,
scope_local_id INTEGER NOT NULL DEFAULT 0
) STRICT;
-- First-class parameter rows: one row per leaf parameter binding,
-- ordered by position. Keyed by (file_path, owner_name, owner_kind)
-- to disambiguate same-name functions vs methods in the same file.
-- type_text is the stringified annotation. default_text is the raw
-- default expression source (NULL when there is no default).
CREATE TABLE IF NOT EXISTS function_params (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
owner_name TEXT NOT NULL,
owner_kind TEXT NOT NULL,
position INTEGER NOT NULL,
name TEXT NOT NULL,
type_text TEXT,
default_text TEXT,
is_rest INTEGER NOT NULL DEFAULT 0,
is_optional INTEGER NOT NULL DEFAULT 0,
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL
) STRICT;
-- Materialised re-export chains. One row per (from_file, from_name)
-- pointing at the terminal definition site after walking through
-- barrel files (bounded at 10 hops). Same engine as bindings-engine
-- exposes the walk to ad-hoc SQL.
CREATE TABLE IF NOT EXISTS re_export_chains (
from_file TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
from_name TEXT NOT NULL,
to_file TEXT NOT NULL,
to_name TEXT NOT NULL,
hops INTEGER NOT NULL,
truncated INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (from_file, from_name)
) STRICT, WITHOUT ROWID;
-- Strongly-connected component (SCC) of the import dependency graph.
-- Only cyclic files appear here. Files sharing cycle_id import each
-- other directly or transitively. Computed via Tarjan's SCC on
-- dependencies after the full index pass.
CREATE TABLE IF NOT EXISTS module_cycles (
file_path TEXT PRIMARY KEY REFERENCES files(path) ON DELETE CASCADE,
cycle_id INTEGER NOT NULL,
cycle_size INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS dynamic_imports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
source_kind TEXT NOT NULL CHECK (source_kind IN ('literal','template','expression')),
source_text TEXT,
resolved_path TEXT,
in_async_fn INTEGER NOT NULL DEFAULT 0,
scope_local_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS jsx_elements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
component_name TEXT NOT NULL,
line_start INTEGER NOT NULL,
line_end INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
is_self_closing INTEGER NOT NULL DEFAULT 0,
is_fragment INTEGER NOT NULL DEFAULT 0,
namespace_prefix TEXT,
parent_element_id INTEGER REFERENCES jsx_elements(id),
children_count INTEGER NOT NULL DEFAULT 0,
is_lowercase INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS jsx_attributes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
element_id INTEGER NOT NULL REFERENCES jsx_elements(id) ON DELETE CASCADE,
name TEXT NOT NULL,
line INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
value_kind TEXT NOT NULL CHECK (value_kind IN ('string','expression','boolean','spread','element')),
value_text TEXT
) STRICT;
CREATE TABLE IF NOT EXISTS async_calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
caller_scope TEXT NOT NULL,
awaited_expression TEXT,
awaited_callee_name TEXT,
line_start INTEGER NOT NULL,
column_start INTEGER NOT NULL,
in_loop INTEGER NOT NULL DEFAULT 0,
in_try INTEGER NOT NULL DEFAULT 0,
scope_local_id INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS try_catch (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
containing_scope_local_id INTEGER NOT NULL DEFAULT 0,
try_line_start INTEGER NOT NULL,
try_line_end INTEGER NOT NULL,
has_catch INTEGER NOT NULL DEFAULT 0,
catch_param TEXT,
catch_rethrows INTEGER NOT NULL DEFAULT 0,
catch_logs_only INTEGER NOT NULL DEFAULT 0,
has_finally INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE IF NOT EXISTS decorators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
target_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL,
target_kind TEXT NOT NULL CHECK (target_kind IN ('class','method','property','parameter','accessor')),
name TEXT NOT NULL,
line INTEGER NOT NULL,
column_start INTEGER NOT NULL,
args_text TEXT
) STRICT;
CREATE TABLE IF NOT EXISTS jsdoc_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol_id INTEGER NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
name TEXT,
type_text TEXT,
description TEXT
) STRICT;
-- Per-specifier breakdown of imports.specifiers JSON blob. Recipes that
-- want specifier-precise rewrites (rename specifier, dedupe, type-only
-- migrate) JOIN this table. The original imports.specifiers JSON stays
-- in place as a v1 convenience surface.
CREATE TABLE IF NOT EXISTS import_specifiers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
import_id INTEGER REFERENCES imports(id) ON DELETE CASCADE,
source TEXT NOT NULL,
line INTEGER NOT NULL,
column_start INTEGER NOT NULL,
column_end INTEGER NOT NULL,
imported_name TEXT NOT NULL,
local_name TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('named','default','namespace','side-effect')),
is_type_only INTEGER NOT NULL DEFAULT 0
) STRICT;
-- Opt-in suppressions — recipes LEFT JOIN to honor, ad-hoc SQL unaffected.
-- line_number > 0 = next-line scope (suppressed line). 0 = file scope.
-- Sourced from // codemap-ignore-{next-line,file} <recipe-id> directives (see markers.ts).
CREATE TABLE IF NOT EXISTS suppressions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
line_number INTEGER NOT NULL,
recipe_id TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT
) STRICT, WITHOUT ROWID;
-- User-data table: query result snapshots for --save-baseline / --baseline.
-- Lives next to the index tables so the entire codemap state is one SQLite file
-- (no parallel JSON files / new gitignore entries). Intentionally absent from
-- dropAll() so --full and SCHEMA_VERSION rebuilds preserve baselines (only
-- index tables get dropped). Future schema bumps that change THIS tables shape
-- need an in-place migration rather than relying on the schema-mismatch rebuild.
CREATE TABLE IF NOT EXISTS query_baselines (
name TEXT PRIMARY KEY,
recipe_id TEXT,
sql TEXT NOT NULL,
rows_json TEXT NOT NULL,
row_count INTEGER NOT NULL,
git_ref TEXT,
created_at INTEGER NOT NULL
) STRICT;
-- User-data table: static coverage snapshots ingested via codemap ingest-coverage
-- (Istanbul coverage-final.json + LCOV lcov.info, written by every modern test
-- runner). Joins to symbols on the natural key (file_path, name, line_start) —
-- intentionally NOT a FK to symbols.id, because dropAll() drops symbols on every
-- --full reindex and the recreated rows get fresh AUTOINCREMENT ids. Natural-key
-- rows survive that churn. Like query_baselines, intentionally excluded from
-- dropAll() so a --full rebuild doesn't nuke the user's last ingest. Orphan
-- cleanup (file deleted from project) lives at the end of every ingest in
-- application/coverage-engine.ts, not here. See docs/plans/coverage-ingestion.md
-- (D6) for the unwind on why CASCADE was rejected.
CREATE TABLE IF NOT EXISTS coverage (
file_path TEXT NOT NULL,
name TEXT NOT NULL,
line_start INTEGER NOT NULL,
coverage_pct REAL,
hit_statements INTEGER NOT NULL,
total_statements INTEGER NOT NULL,
PRIMARY KEY (file_path, name, line_start)
) STRICT, WITHOUT ROWID;
-- User-data table: per-recipe last_run_at + run_count for agent-host
-- ranking. Joined inline into --recipes-json / codemap://recipes via
-- loadRecipeRecency. Like query_baselines / coverage, intentionally absent
-- from dropAll() so --full and SCHEMA_VERSION rebuilds preserve activity
-- history. 90-day window is eager-on-write (recordRecipeRun DELETEs stale
-- rows before its upsert) — reads stay pure. recipe_id is loose (no FK,
-- can match bundled or project recipe ids). See docs/architecture.md.
CREATE TABLE IF NOT EXISTS recipe_recency (
recipe_id TEXT PRIMARY KEY,
last_run_at INTEGER NOT NULL,
run_count INTEGER NOT NULL DEFAULT 1
) STRICT, WITHOUT ROWID;
-- Config-derived: reconcileBoundaryRules clears and re-fills from
-- .codemap/config boundaries on every index pass. Dropped on --full
-- like the other index tables (unlike query_baselines / coverage which
-- are user data and persist). Joined against dependencies by the
-- bundled boundary-violations recipe.
CREATE TABLE IF NOT EXISTS boundary_rules (
name TEXT PRIMARY KEY,
from_glob TEXT NOT NULL,
to_glob TEXT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('deny', 'allow'))
) STRICT, WITHOUT ROWID;
`);
// Separate statement: FTS5 virtual-table CREATE doesn't accept STRICT and
// can't live inside the createTables block above. Always-create (empty when
// disabled) so toggling fts5: false → true needs only a --full, not a
// schema bump. Tokeniser per `docs/plans/fts5-mermaid.md` Q1.
db.run(
`CREATE VIRTUAL TABLE IF NOT EXISTS source_fts USING fts5(
file_path UNINDEXED,
content,
tokenize = 'porter unicode61'
)`,
);
}
/**
* Upsert one file's source into `source_fts`. DELETE + INSERT because FTS5
* virtual tables don't support `INSERT OR REPLACE`. Caller gates on the
* FTS5 toggle.
*/
export function upsertSourceFts(
db: CodemapDatabase,
filePath: string,
content: string,
) {
db.run("DELETE FROM source_fts WHERE file_path = ?", [filePath]);
db.run("INSERT INTO source_fts (file_path, content) VALUES (?, ?)", [
filePath,
content,
]);
}
/**
* `source_fts` isn't FK-linked to `files` (FTS5 virtual tables can't be FK
* targets), so CASCADE doesn't reach it — incremental-delete callers must
* mirror the DELETE explicitly.
*/
export function deleteSourceFts(db: CodemapDatabase, filePath: string) {
db.run("DELETE FROM source_fts WHERE file_path = ?", [filePath]);
}
/**
* Batch-delete FTS5 rows via `WHERE file_path IN (?, …)` — FTS5 accepts
* arbitrary `DELETE … WHERE` predicates (only INSERT/UPDATE have shape constraints).
*/
export function deleteSourceFtsBatch(db: CodemapDatabase, filePaths: string[]) {
if (filePaths.length === 0) return;
const placeholders = filePaths.map(() => "?").join(",");
db.run(
`DELETE FROM source_fts WHERE file_path IN (${placeholders})`,
filePaths,
);
}
export function clearSourceFts(db: CodemapDatabase) {
db.run("DELETE FROM source_fts");
}
export function createIndexes(db: CodemapDatabase) {
db.run(`
-- Covering indexes: include columns returned by common queries to avoid table lookups
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name, kind, file_path, line_start, line_end, signature, is_exported);
CREATE INDEX IF NOT EXISTS idx_symbols_name_covering ON symbols(name, kind, file_path, line_start, line_end, signature, is_exported, parent_name, visibility);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind, is_exported, name, file_path);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
-- Partial indexes: subset indexes for common filtered AI agent queries
CREATE INDEX IF NOT EXISTS idx_symbols_exported ON symbols(name, kind, file_path, signature)
WHERE is_exported = 1;
CREATE INDEX IF NOT EXISTS idx_symbols_functions ON symbols(name, file_path, line_start, line_end, signature)
WHERE kind = 'function';
CREATE INDEX IF NOT EXISTS idx_symbols_visibility ON symbols(visibility, file_path, name, line_start)
WHERE visibility IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_symbols_async ON symbols(file_path, name, return_type)
WHERE is_async = 1;
CREATE INDEX IF NOT EXISTS idx_symbols_body_hash ON symbols(body_hash)
WHERE body_hash IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_imports_source ON imports(source, file_path);
CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_path, file_path);
CREATE INDEX IF NOT EXISTS idx_imports_file ON imports(file_path);
CREATE INDEX IF NOT EXISTS idx_exports_name ON exports(name, file_path, kind, is_default);
CREATE INDEX IF NOT EXISTS idx_exports_file ON exports(file_path);
CREATE INDEX IF NOT EXISTS idx_exports_position ON exports(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_exports_re_export ON exports(is_re_export, file_path);
CREATE INDEX IF NOT EXISTS idx_components_name ON components(name, file_path, props_type, hooks_used);
CREATE INDEX IF NOT EXISTS idx_components_file ON components(file_path, name);
-- WITHOUT ROWID tables already have a clustered PK — this covers reverse lookups
CREATE INDEX IF NOT EXISTS idx_dependencies_to ON dependencies(to_path, from_path);
CREATE INDEX IF NOT EXISTS idx_markers_kind ON markers(kind, file_path, line_number, content);
CREATE INDEX IF NOT EXISTS idx_markers_file ON markers(file_path);
-- Suppressions: most recipe LEFT JOINs key on (recipe_id, file_path[, line_number]).
CREATE INDEX IF NOT EXISTS idx_suppressions_lookup ON suppressions(recipe_id, file_path, line_number);
CREATE INDEX IF NOT EXISTS idx_suppressions_file ON suppressions(file_path);
CREATE INDEX IF NOT EXISTS idx_css_variables_name ON css_variables(name, value, scope, file_path);
CREATE INDEX IF NOT EXISTS idx_css_variables_file ON css_variables(file_path);
CREATE INDEX IF NOT EXISTS idx_css_classes_name ON css_classes(name, file_path, is_module);
CREATE INDEX IF NOT EXISTS idx_css_classes_file ON css_classes(file_path);
CREATE INDEX IF NOT EXISTS idx_css_keyframes_name ON css_keyframes(name, file_path);
CREATE INDEX IF NOT EXISTS idx_type_members_symbol ON type_members(symbol_name, file_path, name, type, is_optional, is_readonly);
CREATE INDEX IF NOT EXISTS idx_type_members_file ON type_members(file_path);
CREATE INDEX IF NOT EXISTS idx_type_heritage_child ON type_heritage(child_file_path, child_name, relation);
CREATE INDEX IF NOT EXISTS idx_type_heritage_base ON type_heritage(base_simple_name, base_file_path);
CREATE INDEX IF NOT EXISTS idx_type_heritage_base_symbol ON type_heritage(base_symbol_id);
CREATE INDEX IF NOT EXISTS idx_scopes_parent ON scopes(file_path, parent_local_id);
CREATE INDEX IF NOT EXISTS idx_scopes_kind ON scopes(kind, file_path);
CREATE INDEX IF NOT EXISTS idx_scopes_owner ON scopes(owner_symbol_name, file_path);
CREATE INDEX IF NOT EXISTS idx_references_name ON "references"(name, file_path);
CREATE INDEX IF NOT EXISTS idx_references_file ON "references"(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_references_kind ON "references"(kind, file_path);
CREATE INDEX IF NOT EXISTS idx_references_writes ON "references"(name, is_write) WHERE is_write = 1;
CREATE INDEX IF NOT EXISTS idx_bindings_resolved ON bindings(resolved_symbol_id);
CREATE INDEX IF NOT EXISTS idx_bindings_kind ON bindings(resolution_kind);
CREATE INDEX IF NOT EXISTS idx_module_cycles_cid ON module_cycles(cycle_id);
CREATE INDEX IF NOT EXISTS idx_module_cycles_size ON module_cycles(cycle_size);
CREATE INDEX IF NOT EXISTS idx_dynamic_imports_file ON dynamic_imports(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_dynamic_imports_resolved ON dynamic_imports(resolved_path, file_path)
WHERE resolved_path IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_jsx_elements_name ON jsx_elements(component_name, file_path);
CREATE INDEX IF NOT EXISTS idx_jsx_elements_file ON jsx_elements(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_jsx_attrs_name ON jsx_attributes(name);
CREATE INDEX IF NOT EXISTS idx_jsx_attrs_element ON jsx_attributes(element_id);
CREATE INDEX IF NOT EXISTS idx_async_calls_callee ON async_calls(awaited_callee_name, file_path);
CREATE INDEX IF NOT EXISTS idx_async_calls_file ON async_calls(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_async_calls_loop ON async_calls(in_loop) WHERE in_loop = 1;
CREATE INDEX IF NOT EXISTS idx_try_catch_file ON try_catch(file_path, try_line_start);
CREATE INDEX IF NOT EXISTS idx_try_catch_logs ON try_catch(catch_logs_only) WHERE catch_logs_only = 1;
CREATE INDEX IF NOT EXISTS idx_decorators_name ON decorators(name, file_path);
CREATE INDEX IF NOT EXISTS idx_decorators_target ON decorators(target_symbol_id);
CREATE INDEX IF NOT EXISTS idx_jsdoc_tags_symbol ON jsdoc_tags(symbol_id);
CREATE INDEX IF NOT EXISTS idx_jsdoc_tags_tag ON jsdoc_tags(tag);
CREATE INDEX IF NOT EXISTS idx_re_export_chains_to ON re_export_chains(to_file, to_name);
CREATE INDEX IF NOT EXISTS idx_re_export_chains_truncated ON re_export_chains(truncated) WHERE truncated = 1;
CREATE INDEX IF NOT EXISTS idx_function_params_owner ON function_params(file_path, owner_name);
CREATE INDEX IF NOT EXISTS idx_function_params_name ON function_params(name);
CREATE INDEX IF NOT EXISTS idx_function_params_type ON function_params(type_text) WHERE type_text IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_runtime_markers_kind ON runtime_markers(kind);
CREATE INDEX IF NOT EXISTS idx_runtime_markers_file ON runtime_markers(file_path);
CREATE INDEX IF NOT EXISTS idx_runtime_markers_detail ON runtime_markers(detail) WHERE detail IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_test_suites_file ON test_suites(file_path);
CREATE INDEX IF NOT EXISTS idx_test_suites_kind ON test_suites(kind);
CREATE INDEX IF NOT EXISTS idx_test_suites_parent ON test_suites(parent_suite_id);
CREATE INDEX IF NOT EXISTS idx_test_suites_skipped ON test_suites(is_skipped) WHERE is_skipped = 1;
CREATE INDEX IF NOT EXISTS idx_import_specifiers_imported ON import_specifiers(imported_name, file_path);
CREATE INDEX IF NOT EXISTS idx_import_specifiers_local ON import_specifiers(local_name, file_path);
CREATE INDEX IF NOT EXISTS idx_import_specifiers_file ON import_specifiers(file_path, line);
CREATE INDEX IF NOT EXISTS idx_import_specifiers_source ON import_specifiers(source, file_path);
CREATE INDEX IF NOT EXISTS idx_import_specifiers_import ON import_specifiers(import_id) WHERE import_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_import_specifiers_kind ON import_specifiers(kind, file_path);
CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_name, file_path);
CREATE INDEX IF NOT EXISTS idx_calls_scope ON calls(caller_scope, file_path, callee_name);
CREATE INDEX IF NOT EXISTS idx_calls_callee ON calls(callee_name, file_path);
CREATE INDEX IF NOT EXISTS idx_calls_file ON calls(file_path);
CREATE INDEX IF NOT EXISTS idx_calls_position ON calls(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_calls_callee_symbol ON calls(callee_symbol_id)
WHERE callee_symbol_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_calls_resolution ON calls(callee_resolution_kind, file_path)
WHERE callee_resolution_kind IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_unresolved_calls_file ON unresolved_calls(file_path, line_start);
CREATE INDEX IF NOT EXISTS idx_unresolved_calls_callee ON unresolved_calls(callee_name, file_path);
-- Mirrors the typical join shape symbols.(file_path, name, line_start).
-- The (file_path, name) prefix also covers GROUP BY file_path scans
-- used by the bundled files-by-coverage recipe (D2 + D13).
CREATE INDEX IF NOT EXISTS idx_coverage_file_name ON coverage(file_path, name);
-- Powers the lazy 90-day prune (DELETE WHERE last_run_at < cutoff) inside
-- loadRecipeRecency. Tiny table (one row per known recipe id) — index keeps
-- the prune predictable as project-recipe counts grow.
CREATE INDEX IF NOT EXISTS idx_recipe_recency_last_run ON recipe_recency(last_run_at);
`);
}
export function createSchema(db: CodemapDatabase) {
const hasMeta = db
.query<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name='meta'",
)
.get();
if (hasMeta) {
const row = db
.query<{ value: string }>("SELECT value FROM meta WHERE key = ?")
.get("schema_version");
if (row && row.value !== String(SCHEMA_VERSION)) {
console.log(
` Schema version mismatch (${row.value} -> ${SCHEMA_VERSION}), rebuilding...`,
);
dropAll(db);
}
}
createTables(db);
createIndexes(db);
setMeta(db, "schema_version", String(SCHEMA_VERSION));
}
export function dropAll(db: CodemapDatabase) {
db.run(`
DROP TABLE IF EXISTS jsdoc_tags;
DROP TABLE IF EXISTS decorators;
DROP TABLE IF EXISTS try_catch;
DROP TABLE IF EXISTS async_calls;
DROP TABLE IF EXISTS jsx_attributes;
DROP TABLE IF EXISTS jsx_elements;
DROP TABLE IF EXISTS module_cycles;
DROP TABLE IF EXISTS dynamic_imports;
DROP TABLE IF EXISTS re_export_chains;
DROP TABLE IF EXISTS function_params;
DROP TABLE IF EXISTS runtime_markers;
DROP TABLE IF EXISTS test_suites;
DROP TABLE IF EXISTS file_metrics;
DROP TABLE IF EXISTS file_churn;
DROP TABLE IF EXISTS unresolved_calls;
DROP TABLE IF EXISTS bindings;
DROP TABLE IF EXISTS "references";
DROP TABLE IF EXISTS calls;
DROP TABLE IF EXISTS suppressions;
DROP TABLE IF EXISTS scopes;
DROP TABLE IF EXISTS import_specifiers;
DROP TABLE IF EXISTS type_members;
DROP TABLE IF EXISTS type_heritage;
DROP TABLE IF EXISTS dependencies;
DROP TABLE IF EXISTS markers;
DROP TABLE IF EXISTS components;
DROP TABLE IF EXISTS imports;
DROP TABLE IF EXISTS exports;
DROP TABLE IF EXISTS symbols;
DROP TABLE IF EXISTS css_variables;
DROP TABLE IF EXISTS css_classes;
DROP TABLE IF EXISTS css_keyframes;
DROP TABLE IF EXISTS source_fts;
DROP TABLE IF EXISTS boundary_rules;
DROP TABLE IF EXISTS files;
DROP TABLE IF EXISTS meta;
`);
}
export function getMeta(db: CodemapDatabase, key: string): string | undefined {
const row = db
.query<{ value: string }>("SELECT value FROM meta WHERE key = ?")
.get(key);
return row?.value;
}
export function setMeta(db: CodemapDatabase, key: string, value: string) {
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)", [
key,
value,
]);
}
/** One row in `boundary_rules`. Shape mirrors the Zod `boundaries` field. */
export interface BoundaryRuleRow {
name: string;
from_glob: string;
to_glob: string;
action: "deny" | "allow";
}
/**
* Replace `boundary_rules` with `rules` — config is the single source of
* truth, this table is a denormalised lookup. Idempotent; cheap (one row
* per declared boundary).
*
* Atomic via SAVEPOINT: a duplicate `name` (PRIMARY KEY collision — Zod
* doesn't dedupe) would otherwise wipe the previous good state and leave
* the table half-populated. SAVEPOINT works inside or outside an open
* transaction, so callers don't need to coordinate.
*/
export function reconcileBoundaryRules(
db: CodemapDatabase,
rules: ReadonlyArray<BoundaryRuleRow>,
) {
db.run("SAVEPOINT reconcile_boundary_rules");
try {
db.run("DELETE FROM boundary_rules");
for (const rule of rules) {
db.run(
"INSERT INTO boundary_rules (name, from_glob, to_glob, action) VALUES (?, ?, ?, ?)",
[rule.name, rule.from_glob, rule.to_glob, rule.action],
);
}
db.run("RELEASE SAVEPOINT reconcile_boundary_rules");
} catch (error) {
db.run("ROLLBACK TO SAVEPOINT reconcile_boundary_rules");
db.run("RELEASE SAVEPOINT reconcile_boundary_rules");
throw error;
}
}
export function deleteFileData(db: CodemapDatabase, filePath: string) {
deleteSourceFts(db, filePath);
db.run("DELETE FROM files WHERE path = ?", [filePath]);
}
/**
* Header row for every indexed file; all other rows FK `file_path` here with
* `ON DELETE CASCADE`. `content_hash` is SHA-256 hex (see `src/hash.ts`) and
* drives incremental staleness detection + the `files-hashes` recipe.
*/
export interface FileRow {
path: string;
content_hash: string;
size: number;
line_count: number;
language: string;
last_modified: number;
indexed_at: number;
is_barrel?: number;
has_side_effects?: number;
}
export function insertFile(db: CodemapDatabase, file: FileRow) {
db.run(
`INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at, is_barrel, has_side_effects)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
file.path,
file.content_hash,
file.size,
file.line_count,
file.language,
file.last_modified,
file.indexed_at,
file.is_barrel ?? 0,
file.has_side_effects ?? 0,
],
);
}
/**
* Function / const / class / interface / type / enum, plus class members
* (`method` / `property` / `getter` / `setter`) — class members carry
* `parent_name`. JSDoc tags in `doc_comment` power the `deprecated-symbols`
* and `visibility-tags` recipes; `members` is JSON for enums.
*/
export interface SymbolRow {
file_path: string;
name: string;
kind: string;
line_start: number;
line_end: number;
signature: string;
is_exported: number;
is_default_export: number;
members: string | null;
doc_comment: string | null;
value: string | null;
parent_name: string | null;
/**
* JSDoc visibility tag: `public` / `private` / `internal` / `alpha` / `beta`.
* Null when the doc has no visibility tag (or no doc at all). First match
* in document order wins when multiple tags are present.
*/
visibility: string | null;
/**
* Cyclomatic complexity (1 + branching nodes). Function-shaped symbols
* only; `null` for non-function kinds (interfaces, types, enums, plain
* consts) and for symbols without a walked body. Optional for back-
* compat with callers that built `SymbolRow` literals before the
* column existed; absence binds as `null`.
*/
complexity?: number | null;
/**
* SonarSource cognitive complexity for function-shaped symbols (same NULL
* rules as `complexity`). Optional for back-compat; absence binds as `null`.
*/
cognitive_complexity?: number | null;
/** 0-based byte column of the symbol-name token start on `line_start` (per [R.6]). Optional for back-compat; defaults to 0. */
name_column_start?: number;
/** 0-based byte column one past the symbol-name token end. Optional for back-compat; defaults to 0. */
name_column_end?: number;
/** Scope where the NAME is declared (parent of the body's own scope). Joins scopes.local_id. Defaults to 0 (module). */
scope_local_id?: number;
/** Body line count (line_end - line_start + 1) for function-shaped symbols. NULL otherwise. */
body_line_count?: number | null;
/** Param count for function-shaped symbols. NULL otherwise. */
param_count?: number | null;
/** Max nesting depth (conditionals/loops/ternaries) for function-shaped symbols. NULL otherwise. */
nesting_depth?: number | null;
/** Stringified return type for function-shaped symbols; NULL when unannotated or N/A. */
return_type?: string | null;
is_async?: number;
is_generator?: number;
/**
* SHA-256 of canonicalized function body AST for function-shaped symbols
* (`function`, `method`, `getter`, `setter`). NULL for non-functions and
* trivial bodies (`body_line_count < 2`).
*/
body_hash?: string | null;
}
// SQLite 3.32+ (2020+) default; bun:sqlite + better-sqlite3 12.x both ship
// with newer SQLite. Older builds default to 999.
const SQLITE_MAX_VARS = 32766;
// Cap rows per batch even when col_count would allow more — keeps per-batch
// JS allocations bounded and avoids pathologically long SQL strings.
const MAX_ROWS_PER_BATCH = 5000;
// Memo per (one, count) tuple — collapses tail-batch placeholder rebuilds (and the
// resulting unique SQL strings hitting stmtCache) to O(1) cache hits.
const placeholderCache = new Map<string, Map<number, string>>();
function getPlaceholders(one: string, count: number): string {
let perOne = placeholderCache.get(one);
if (perOne === undefined) {
perOne = new Map();
placeholderCache.set(one, perOne);
}
let s = perOne.get(count);
if (s === undefined) {
s = Array(count).fill(one).join(",");
perOne.set(count, s);
}
return s;
}
// Memo per `one` (placeholder shape per table) so col-count + batch-size
// math runs once per table, not per call.
const batchSizeCache = new Map<string, number>();
function batchSizeForTuple(one: string): number {
let n = batchSizeCache.get(one);
if (n !== undefined) return n;
let cols = 0;
// Count `?` chars — placeholder shape is "(?,?,?,...)"; one row's params.
for (let i = 0; i < one.length; i++) if (one.charCodeAt(i) === 63) cols++;
n = Math.min(
MAX_ROWS_PER_BATCH,
Math.floor(SQLITE_MAX_VARS / Math.max(cols, 1)),
);
batchSizeCache.set(one, n);
return n;