Skip to main content

dbg_cli/session_db/
schema.rs

1//! SessionDb schema DDL.
2//!
3//! Every minor version can break schema. Old DBs that don't match
4//! `SCHEMA_VERSION` fail to load with a clear "re-collect" message.
5//! There is no migration path — the raw native files under
6//! `.dbg/sessions/<label>/raw/` are the durable artifact and `dbg profile-*`
7//! can always regenerate an index from them.
8
9use anyhow::Result;
10use rusqlite::Connection;
11
12use super::TargetClass;
13
14/// Bump this on every schema-breaking change. No migrations.
15pub const SCHEMA_VERSION: i64 = 3;
16
17/// Shared meta tables — always created regardless of track or target class.
18pub const CORE_DDL: &str = "
19CREATE TABLE IF NOT EXISTS sessions (
20    id              TEXT PRIMARY KEY,
21    kind            TEXT NOT NULL CHECK (kind IN ('debug','profile')),
22    target          TEXT NOT NULL,
23    target_class    TEXT NOT NULL,
24    target_hash     TEXT,
25    started_at      TEXT NOT NULL,
26    ended_at        TEXT,
27    label           TEXT NOT NULL,
28    created_by      TEXT NOT NULL DEFAULT 'auto'
29);
30
31CREATE TABLE IF NOT EXISTS layers (
32    id              INTEGER PRIMARY KEY,
33    session_id      TEXT NOT NULL REFERENCES sessions(id),
34    source          TEXT NOT NULL,
35    file            TEXT,
36    collected_at    TEXT,
37    command_used    TEXT,
38    collection_secs REAL,
39    target_hash     TEXT
40);
41CREATE INDEX IF NOT EXISTS idx_layers_session ON layers(session_id);
42
43CREATE TABLE IF NOT EXISTS symbols (
44    id              INTEGER PRIMARY KEY,
45    session_id      TEXT NOT NULL REFERENCES sessions(id),
46    lang            TEXT NOT NULL,
47    fqn             TEXT NOT NULL,
48    file            TEXT,
49    line            INTEGER,
50    demangled       TEXT,
51    raw             TEXT NOT NULL,
52    is_synthetic    INTEGER NOT NULL DEFAULT 0,
53    UNIQUE(session_id, lang, fqn)
54);
55CREATE INDEX IF NOT EXISTS idx_symbols_fqn ON symbols(lang, fqn);
56
57CREATE TABLE IF NOT EXISTS meta (
58    session_id      TEXT NOT NULL REFERENCES sessions(id),
59    key             TEXT NOT NULL,
60    value           TEXT,
61    PRIMARY KEY (session_id, key)
62);
63
64CREATE TABLE IF NOT EXISTS failures (
65    session_id      TEXT NOT NULL REFERENCES sessions(id),
66    phase           TEXT,
67    error           TEXT
68);
69
70CREATE TABLE IF NOT EXISTS regions (
71    id              INTEGER PRIMARY KEY,
72    session_id      TEXT NOT NULL REFERENCES sessions(id),
73    name            TEXT NOT NULL,
74    start_us        REAL,
75    duration_us     REAL,
76    thread          TEXT,
77    layer_id        INTEGER REFERENCES layers(id)
78);
79CREATE INDEX IF NOT EXISTS idx_regions_session ON regions(session_id);
80
81CREATE TABLE IF NOT EXISTS allocations (
82    id              INTEGER PRIMARY KEY,
83    session_id      TEXT NOT NULL REFERENCES sessions(id),
84    op              TEXT NOT NULL,
85    address         INTEGER NOT NULL,
86    bytes           INTEGER NOT NULL,
87    start_us        REAL,
88    heap            TEXT NOT NULL DEFAULT 'default',
89    thread          TEXT,
90    stack_json      TEXT,
91    layer_id        INTEGER REFERENCES layers(id)
92);
93CREATE INDEX IF NOT EXISTS idx_alloc_addr ON allocations(session_id, address);
94CREATE INDEX IF NOT EXISTS idx_alloc_time ON allocations(session_id, start_us);
95";
96
97/// Debug track tables — created for every session regardless of kind.
98/// A profile session simply never writes to them.
99pub const DEBUG_DDL: &str = "
100CREATE TABLE IF NOT EXISTS commands (
101    seq             INTEGER PRIMARY KEY,
102    session_id      TEXT NOT NULL REFERENCES sessions(id),
103    input           TEXT NOT NULL,
104    output_head     TEXT,
105    output_file     TEXT,
106    output_bytes    INTEGER,
107    ts              TEXT NOT NULL,
108    canonical_op    TEXT
109);
110CREATE INDEX IF NOT EXISTS idx_commands_session ON commands(session_id);
111
112CREATE TABLE IF NOT EXISTS breakpoint_hits (
113    id              INTEGER PRIMARY KEY,
114    session_id      TEXT NOT NULL REFERENCES sessions(id),
115    location_key    TEXT NOT NULL,
116    hit_seq         INTEGER NOT NULL,
117    thread          TEXT,
118    ts              TEXT NOT NULL,
119    locals_json     TEXT,
120    stack_json      TEXT
121);
122CREATE INDEX IF NOT EXISTS idx_bp_location ON breakpoint_hits(session_id, location_key);
123
124CREATE TABLE IF NOT EXISTS watch_evals (
125    id              INTEGER PRIMARY KEY,
126    session_id      TEXT NOT NULL REFERENCES sessions(id),
127    hit_id          INTEGER REFERENCES breakpoint_hits(id),
128    expr            TEXT NOT NULL,
129    value           TEXT,
130    type_name       TEXT,
131    ts              TEXT NOT NULL
132);
133";
134
135/// Cross-track shared tables — joinable from either debug or profile
136/// tracks via `symbol_id`. Populated by on-demand collectors.
137pub const CROSSTRACK_DDL: &str = "
138CREATE TABLE IF NOT EXISTS disassembly (
139    id              INTEGER PRIMARY KEY,
140    session_id      TEXT NOT NULL REFERENCES sessions(id),
141    symbol_id       INTEGER REFERENCES symbols(id),
142    source          TEXT NOT NULL,
143    tier            TEXT,
144    code_bytes      INTEGER,
145    asm_text        TEXT NOT NULL,
146    asm_lines_json  TEXT,
147    collected_at    TEXT NOT NULL,
148    trigger         TEXT,
149    layer_id        INTEGER REFERENCES layers(id)
150);
151CREATE INDEX IF NOT EXISTS idx_disasm_sym ON disassembly(symbol_id);
152CREATE UNIQUE INDEX IF NOT EXISTS idx_disasm_identity
153    ON disassembly(session_id, symbol_id, source, COALESCE(tier, ''));
154
155CREATE TABLE IF NOT EXISTS source_snapshots (
156    id              INTEGER PRIMARY KEY,
157    session_id      TEXT NOT NULL REFERENCES sessions(id),
158    symbol_id       INTEGER REFERENCES symbols(id),
159    file            TEXT,
160    line_start      INTEGER,
161    line_end        INTEGER,
162    text            TEXT,
163    content_hash    TEXT,
164    collected_at    TEXT NOT NULL
165);
166CREATE INDEX IF NOT EXISTS idx_src_sym ON source_snapshots(symbol_id);
167
168CREATE TABLE IF NOT EXISTS alloc_sites (
169    id              INTEGER PRIMARY KEY,
170    session_id      TEXT NOT NULL REFERENCES sessions(id),
171    symbol_id       INTEGER REFERENCES symbols(id),
172    bytes_total     INTEGER,
173    count           INTEGER,
174    largest_bytes   INTEGER,
175    collected_at    TEXT NOT NULL,
176    layer_id        INTEGER REFERENCES layers(id)
177);
178CREATE INDEX IF NOT EXISTS idx_alloc_site_sym ON alloc_sites(symbol_id);
179
180CREATE TABLE IF NOT EXISTS insn_hits (
181    id              INTEGER PRIMARY KEY,
182    session_id      TEXT NOT NULL REFERENCES sessions(id),
183    target          TEXT NOT NULL,
184    hit_count       INTEGER NOT NULL,
185    sample_basis    TEXT NOT NULL,
186    sample_period   INTEGER,
187    window_us       REAL,
188    backend         TEXT NOT NULL,
189    collected_at    TEXT NOT NULL,
190    detail_json     TEXT
191);
192CREATE INDEX IF NOT EXISTS idx_insn_hits_target ON insn_hits(session_id, target);
193
194CREATE TABLE IF NOT EXISTS insn_hit_details (
195    id              INTEGER PRIMARY KEY,
196    insn_hit_id     INTEGER NOT NULL REFERENCES insn_hits(id),
197    ts_us           REAL,
198    stack_json      TEXT,
199    regs_json       TEXT
200);
201CREATE INDEX IF NOT EXISTS idx_insn_hit_details ON insn_hit_details(insn_hit_id);
202";
203
204/// GPU domain tables (CUDA launches, ncu metrics, memcpy transfers,
205/// framework ops). From gdbg's current schema, with `session_id` added.
206pub const GPU_DDL: &str = "
207CREATE TABLE IF NOT EXISTS launches (
208    id              INTEGER PRIMARY KEY,
209    session_id      TEXT NOT NULL REFERENCES sessions(id),
210    kernel_name     TEXT NOT NULL,
211    duration_us     REAL NOT NULL,
212    grid_x          INTEGER, grid_y INTEGER, grid_z INTEGER,
213    block_x         INTEGER, block_y INTEGER, block_z INTEGER,
214    stream_id       INTEGER,
215    start_us        REAL,
216    correlation_id  INTEGER,
217    layer_id        INTEGER REFERENCES layers(id)
218);
219CREATE INDEX IF NOT EXISTS idx_launches_kernel ON launches(session_id, kernel_name);
220CREATE INDEX IF NOT EXISTS idx_launches_start  ON launches(session_id, start_us);
221CREATE INDEX IF NOT EXISTS idx_launches_stream ON launches(session_id, stream_id);
222
223CREATE TABLE IF NOT EXISTS metrics (
224    session_id                TEXT NOT NULL REFERENCES sessions(id),
225    kernel_name               TEXT NOT NULL,
226    occupancy_pct             REAL,
227    compute_throughput_pct    REAL,
228    memory_throughput_pct     REAL,
229    registers_per_thread      INTEGER,
230    shared_mem_static_bytes   INTEGER,
231    shared_mem_dynamic_bytes  INTEGER,
232    l2_hit_rate_pct           REAL,
233    achieved_bandwidth_gb_s   REAL,
234    peak_bandwidth_gb_s       REAL,
235    boundedness               TEXT,
236    layer_id                  INTEGER REFERENCES layers(id),
237    PRIMARY KEY (session_id, kernel_name)
238);
239
240CREATE TABLE IF NOT EXISTS transfers (
241    id              INTEGER PRIMARY KEY,
242    session_id      TEXT NOT NULL REFERENCES sessions(id),
243    kind            TEXT NOT NULL,
244    bytes           INTEGER,
245    duration_us     REAL,
246    start_us        REAL,
247    stream_id       INTEGER,
248    layer_id        INTEGER REFERENCES layers(id)
249);
250
251CREATE TABLE IF NOT EXISTS ops (
252    id              INTEGER PRIMARY KEY,
253    session_id      TEXT NOT NULL REFERENCES sessions(id),
254    name            TEXT NOT NULL,
255    module_path     TEXT,
256    cpu_time_us     REAL,
257    gpu_time_us     REAL,
258    input_shapes    TEXT,
259    layer_id        INTEGER REFERENCES layers(id)
260);
261
262CREATE TABLE IF NOT EXISTS op_kernel_map (
263    session_id      TEXT NOT NULL REFERENCES sessions(id),
264    op_id           INTEGER NOT NULL REFERENCES ops(id),
265    kernel_name     TEXT NOT NULL,
266    PRIMARY KEY (op_id, kernel_name)
267);
268";
269
270/// CPU-unified profile tables — sampling + EAV counters. Used by
271/// native-cpu, managed-dotnet, jvm, python, js/node target classes.
272pub const CPU_DDL: &str = "
273CREATE TABLE IF NOT EXISTS samples (
274    id              INTEGER PRIMARY KEY,
275    session_id      TEXT NOT NULL REFERENCES sessions(id),
276    symbol_id       INTEGER REFERENCES symbols(id),
277    thread          TEXT,
278    start_us        REAL,
279    duration_us     REAL,
280    cpu_ns          INTEGER,
281    weight          REAL NOT NULL DEFAULT 1.0,
282    stack_json      TEXT,
283    layer_id        INTEGER REFERENCES layers(id)
284);
285CREATE INDEX IF NOT EXISTS idx_samples_sym   ON samples(session_id, symbol_id);
286CREATE INDEX IF NOT EXISTS idx_samples_start ON samples(session_id, start_us);
287
288CREATE TABLE IF NOT EXISTS counters (
289    id              INTEGER PRIMARY KEY,
290    session_id      TEXT NOT NULL REFERENCES sessions(id),
291    name            TEXT NOT NULL,
292    symbol_id       INTEGER REFERENCES symbols(id),
293    value           REAL NOT NULL,
294    unit            TEXT,
295    layer_id        INTEGER REFERENCES layers(id)
296);
297CREATE INDEX IF NOT EXISTS idx_counters_name ON counters(session_id, name);
298";
299
300/// Managed-runtime tables (.NET, JVM).
301pub const MANAGED_DDL: &str = "
302CREATE TABLE IF NOT EXISTS gc_events (
303    id                INTEGER PRIMARY KEY,
304    session_id        TEXT NOT NULL REFERENCES sessions(id),
305    kind              TEXT NOT NULL,
306    pause_us          REAL,
307    start_us          REAL,
308    heap_before_bytes INTEGER,
309    heap_after_bytes  INTEGER,
310    reason            TEXT,
311    layer_id          INTEGER REFERENCES layers(id)
312);
313CREATE INDEX IF NOT EXISTS idx_gc_start ON gc_events(session_id, start_us);
314
315CREATE TABLE IF NOT EXISTS jit_events (
316    id              INTEGER PRIMARY KEY,
317    session_id      TEXT NOT NULL REFERENCES sessions(id),
318    symbol_id       INTEGER REFERENCES symbols(id),
319    compile_us      REAL,
320    code_bytes      INTEGER,
321    tier            TEXT,
322    start_us        REAL,
323    layer_id        INTEGER REFERENCES layers(id)
324);
325";
326
327/// Python-specific tables (GIL contention).
328pub const PYTHON_DDL: &str = "
329CREATE TABLE IF NOT EXISTS gil_events (
330    id              INTEGER PRIMARY KEY,
331    session_id      TEXT NOT NULL REFERENCES sessions(id),
332    kind            TEXT NOT NULL,
333    thread          TEXT,
334    start_us        REAL,
335    duration_us     REAL,
336    layer_id        INTEGER REFERENCES layers(id)
337);
338";
339
340/// Node/JS-specific tables (event-loop lag).
341pub const NODE_DDL: &str = "
342CREATE TABLE IF NOT EXISTS event_loop_lags (
343    id              INTEGER PRIMARY KEY,
344    session_id      TEXT NOT NULL REFERENCES sessions(id),
345    lag_us          REAL NOT NULL,
346    start_us        REAL,
347    phase           TEXT,
348    layer_id        INTEGER REFERENCES layers(id)
349);
350";
351
352/// Apply the shared meta + debug + cross-track DDL plus the per-class
353/// domain tables. Safe to call on a fresh or existing DB; uses
354/// `CREATE TABLE IF NOT EXISTS` everywhere. Schema-version enforcement
355/// happens in `SessionDb::open`, not here.
356pub fn apply(conn: &Connection, class: TargetClass) -> Result<()> {
357    conn.execute_batch(CORE_DDL)?;
358    conn.execute_batch(DEBUG_DDL)?;
359    conn.execute_batch(CROSSTRACK_DDL)?;
360    match class {
361        TargetClass::Gpu => conn.execute_batch(GPU_DDL)?,
362        TargetClass::NativeCpu => conn.execute_batch(CPU_DDL)?,
363        TargetClass::ManagedDotnet | TargetClass::Jvm => {
364            conn.execute_batch(CPU_DDL)?;
365            conn.execute_batch(MANAGED_DDL)?;
366        }
367        TargetClass::Python => {
368            conn.execute_batch(CPU_DDL)?;
369            conn.execute_batch(PYTHON_DDL)?;
370        }
371        TargetClass::JsNode => {
372            conn.execute_batch(CPU_DDL)?;
373            conn.execute_batch(NODE_DDL)?;
374        }
375        // Ruby and PHP share the CPU-unified profile shape for now.
376        // They get their own TargetClass so the `sessions` listing shows
377        // a meaningful runtime tag instead of `native-cpu`.
378        TargetClass::Ruby | TargetClass::Php => {
379            conn.execute_batch(CPU_DDL)?;
380        }
381    }
382    conn.execute_batch(OWNERSHIP_TRIGGERS)?;
383    match class {
384        TargetClass::Gpu => conn.execute_batch(GPU_OWNERSHIP_TRIGGERS)?,
385        TargetClass::NativeCpu
386        | TargetClass::ManagedDotnet
387        | TargetClass::Jvm
388        | TargetClass::Python
389        | TargetClass::JsNode
390        | TargetClass::Ruby
391        | TargetClass::Php => conn.execute_batch(CPU_OWNERSHIP_TRIGGERS)?,
392    }
393    match class {
394        TargetClass::ManagedDotnet | TargetClass::Jvm => {
395            conn.execute_batch(MANAGED_OWNERSHIP_TRIGGERS)?
396        }
397        TargetClass::Python => conn.execute_batch(PYTHON_OWNERSHIP_TRIGGERS)?,
398        TargetClass::JsNode => conn.execute_batch(NODE_OWNERSHIP_TRIGGERS)?,
399        _ => {}
400    }
401    Ok(())
402}
403
404/// SQLite's single-column foreign keys do not prove that a child link and
405/// its parent belong to the same session. These triggers add that missing
406/// invariant for every cross-track link while keeping the public insert
407/// shape stable for collectors.
408pub const OWNERSHIP_TRIGGERS: &str = r#"
409CREATE TRIGGER IF NOT EXISTS disassembly_symbol_same_session
410BEFORE INSERT ON disassembly WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
411 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
412BEGIN SELECT RAISE(ABORT, 'disassembly symbol belongs to another session'); END;
413CREATE TRIGGER IF NOT EXISTS disassembly_layer_same_session
414BEFORE INSERT ON disassembly WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
415 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
416BEGIN SELECT RAISE(ABORT, 'disassembly layer belongs to another session'); END;
417CREATE TRIGGER IF NOT EXISTS disassembly_symbol_update_same_session
418BEFORE UPDATE OF session_id, symbol_id ON disassembly
419WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
420 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
421BEGIN SELECT RAISE(ABORT, 'disassembly symbol belongs to another session'); END;
422CREATE TRIGGER IF NOT EXISTS disassembly_layer_update_same_session
423BEFORE UPDATE OF session_id, layer_id ON disassembly
424WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
425 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
426BEGIN SELECT RAISE(ABORT, 'disassembly layer belongs to another session'); END;
427CREATE TRIGGER IF NOT EXISTS source_symbol_same_session
428BEFORE INSERT ON source_snapshots WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
429 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
430BEGIN SELECT RAISE(ABORT, 'source symbol belongs to another session'); END;
431CREATE TRIGGER IF NOT EXISTS source_symbol_update_same_session
432BEFORE UPDATE OF session_id, symbol_id ON source_snapshots
433WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
434 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
435BEGIN SELECT RAISE(ABORT, 'source symbol belongs to another session'); END;
436CREATE TRIGGER IF NOT EXISTS alloc_site_symbol_same_session
437BEFORE INSERT ON alloc_sites WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
438 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
439BEGIN SELECT RAISE(ABORT, 'allocation symbol belongs to another session'); END;
440CREATE TRIGGER IF NOT EXISTS alloc_site_symbol_update_same_session
441BEFORE UPDATE OF session_id, symbol_id ON alloc_sites
442WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
443 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
444BEGIN SELECT RAISE(ABORT, 'allocation symbol belongs to another session'); END;
445CREATE TRIGGER IF NOT EXISTS alloc_site_layer_same_session
446BEFORE INSERT ON alloc_sites WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
447 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
448BEGIN SELECT RAISE(ABORT, 'allocation layer belongs to another session'); END;
449CREATE TRIGGER IF NOT EXISTS alloc_site_layer_update_same_session
450BEFORE UPDATE OF session_id, layer_id ON alloc_sites
451WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
452 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
453BEGIN SELECT RAISE(ABORT, 'allocation layer belongs to another session'); END;
454CREATE TRIGGER IF NOT EXISTS watch_hit_same_session
455BEFORE INSERT ON watch_evals WHEN NEW.hit_id IS NOT NULL AND NOT EXISTS
456 (SELECT 1 FROM breakpoint_hits WHERE id=NEW.hit_id AND session_id=NEW.session_id)
457BEGIN SELECT RAISE(ABORT, 'watch hit belongs to another session'); END;
458CREATE TRIGGER IF NOT EXISTS watch_hit_update_same_session
459BEFORE UPDATE OF session_id, hit_id ON watch_evals
460WHEN NEW.hit_id IS NOT NULL AND NOT EXISTS
461 (SELECT 1 FROM breakpoint_hits WHERE id=NEW.hit_id AND session_id=NEW.session_id)
462BEGIN SELECT RAISE(ABORT, 'watch hit belongs to another session'); END;
463CREATE TRIGGER IF NOT EXISTS regions_layer_same_session
464BEFORE INSERT ON regions WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
465 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
466BEGIN SELECT RAISE(ABORT, 'region layer belongs to another session'); END;
467CREATE TRIGGER IF NOT EXISTS regions_layer_update_same_session
468BEFORE UPDATE OF session_id, layer_id ON regions
469WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
470 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
471BEGIN SELECT RAISE(ABORT, 'region layer belongs to another session'); END;
472CREATE TRIGGER IF NOT EXISTS allocations_layer_same_session
473BEFORE INSERT ON allocations WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
474 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
475BEGIN SELECT RAISE(ABORT, 'allocation layer belongs to another session'); END;
476CREATE TRIGGER IF NOT EXISTS allocations_layer_update_same_session
477BEFORE UPDATE OF session_id, layer_id ON allocations
478WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
479 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
480BEGIN SELECT RAISE(ABORT, 'allocation layer belongs to another session'); END;
481"#;
482
483pub const GPU_OWNERSHIP_TRIGGERS: &str = r#"
484CREATE TRIGGER IF NOT EXISTS op_map_same_session
485BEFORE INSERT ON op_kernel_map WHEN NOT EXISTS
486 (SELECT 1 FROM ops WHERE id=NEW.op_id AND session_id=NEW.session_id)
487BEGIN SELECT RAISE(ABORT, 'operator link belongs to another session'); END;
488CREATE TRIGGER IF NOT EXISTS op_map_update_same_session
489BEFORE UPDATE OF session_id, op_id ON op_kernel_map WHEN NOT EXISTS
490 (SELECT 1 FROM ops WHERE id=NEW.op_id AND session_id=NEW.session_id)
491BEGIN SELECT RAISE(ABORT, 'operator link belongs to another session'); END;
492CREATE TRIGGER IF NOT EXISTS launches_layer_same_session
493BEFORE INSERT ON launches WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
494 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
495BEGIN SELECT RAISE(ABORT, 'launch layer belongs to another session'); END;
496CREATE TRIGGER IF NOT EXISTS launches_layer_update_same_session
497BEFORE UPDATE OF session_id, layer_id ON launches
498WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
499 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
500BEGIN SELECT RAISE(ABORT, 'launch layer belongs to another session'); END;
501CREATE TRIGGER IF NOT EXISTS metrics_layer_same_session
502BEFORE INSERT ON metrics WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
503 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
504BEGIN SELECT RAISE(ABORT, 'metric layer belongs to another session'); END;
505CREATE TRIGGER IF NOT EXISTS metrics_layer_update_same_session
506BEFORE UPDATE OF session_id, layer_id ON metrics
507WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
508 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
509BEGIN SELECT RAISE(ABORT, 'metric layer belongs to another session'); END;
510CREATE TRIGGER IF NOT EXISTS transfers_layer_same_session
511BEFORE INSERT ON transfers WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
512 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
513BEGIN SELECT RAISE(ABORT, 'transfer layer belongs to another session'); END;
514CREATE TRIGGER IF NOT EXISTS transfers_layer_update_same_session
515BEFORE UPDATE OF session_id, layer_id ON transfers
516WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
517 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
518BEGIN SELECT RAISE(ABORT, 'transfer layer belongs to another session'); END;
519CREATE TRIGGER IF NOT EXISTS ops_layer_same_session
520BEFORE INSERT ON ops WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
521 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
522BEGIN SELECT RAISE(ABORT, 'operator layer belongs to another session'); END;
523CREATE TRIGGER IF NOT EXISTS ops_layer_update_same_session
524BEFORE UPDATE OF session_id, layer_id ON ops
525WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
526 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
527BEGIN SELECT RAISE(ABORT, 'operator layer belongs to another session'); END;
528"#;
529
530pub const CPU_OWNERSHIP_TRIGGERS: &str = r#"
531CREATE TRIGGER IF NOT EXISTS samples_layer_same_session
532BEFORE INSERT ON samples WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
533 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
534BEGIN SELECT RAISE(ABORT, 'sample layer belongs to another session'); END;
535CREATE TRIGGER IF NOT EXISTS samples_symbol_same_session
536BEFORE INSERT ON samples WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
537 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
538BEGIN SELECT RAISE(ABORT, 'sample symbol belongs to another session'); END;
539CREATE TRIGGER IF NOT EXISTS samples_layer_update_same_session
540BEFORE UPDATE OF session_id, layer_id ON samples
541WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
542 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
543BEGIN SELECT RAISE(ABORT, 'sample layer belongs to another session'); END;
544CREATE TRIGGER IF NOT EXISTS samples_symbol_update_same_session
545BEFORE UPDATE OF session_id, symbol_id ON samples
546WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
547 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
548BEGIN SELECT RAISE(ABORT, 'sample symbol belongs to another session'); END;
549CREATE TRIGGER IF NOT EXISTS counters_layer_same_session
550BEFORE INSERT ON counters WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
551 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
552BEGIN SELECT RAISE(ABORT, 'counter layer belongs to another session'); END;
553CREATE TRIGGER IF NOT EXISTS counters_symbol_same_session
554BEFORE INSERT ON counters WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
555 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
556BEGIN SELECT RAISE(ABORT, 'counter symbol belongs to another session'); END;
557CREATE TRIGGER IF NOT EXISTS counters_layer_update_same_session
558BEFORE UPDATE OF session_id, layer_id ON counters
559WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
560 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
561BEGIN SELECT RAISE(ABORT, 'counter layer belongs to another session'); END;
562CREATE TRIGGER IF NOT EXISTS counters_symbol_update_same_session
563BEFORE UPDATE OF session_id, symbol_id ON counters
564WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
565 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
566BEGIN SELECT RAISE(ABORT, 'counter symbol belongs to another session'); END;
567"#;
568
569pub const MANAGED_OWNERSHIP_TRIGGERS: &str = r#"
570CREATE TRIGGER IF NOT EXISTS gc_events_layer_same_session
571BEFORE INSERT ON gc_events WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
572 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
573BEGIN SELECT RAISE(ABORT, 'GC layer belongs to another session'); END;
574CREATE TRIGGER IF NOT EXISTS gc_events_layer_update_same_session
575BEFORE UPDATE OF session_id, layer_id ON gc_events
576WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
577 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
578BEGIN SELECT RAISE(ABORT, 'GC layer belongs to another session'); END;
579CREATE TRIGGER IF NOT EXISTS jit_events_symbol_same_session
580BEFORE INSERT ON jit_events WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
581 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
582BEGIN SELECT RAISE(ABORT, 'JIT symbol belongs to another session'); END;
583CREATE TRIGGER IF NOT EXISTS jit_events_layer_same_session
584BEFORE INSERT ON jit_events WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
585 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
586BEGIN SELECT RAISE(ABORT, 'JIT layer belongs to another session'); END;
587CREATE TRIGGER IF NOT EXISTS jit_events_symbol_update_same_session
588BEFORE UPDATE OF session_id, symbol_id ON jit_events
589WHEN NEW.symbol_id IS NOT NULL AND NOT EXISTS
590 (SELECT 1 FROM symbols WHERE id=NEW.symbol_id AND session_id=NEW.session_id)
591BEGIN SELECT RAISE(ABORT, 'JIT symbol belongs to another session'); END;
592CREATE TRIGGER IF NOT EXISTS jit_events_layer_update_same_session
593BEFORE UPDATE OF session_id, layer_id ON jit_events
594WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
595 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
596BEGIN SELECT RAISE(ABORT, 'JIT layer belongs to another session'); END;
597"#;
598
599pub const PYTHON_OWNERSHIP_TRIGGERS: &str = r#"
600CREATE TRIGGER IF NOT EXISTS gil_events_layer_same_session
601BEFORE INSERT ON gil_events WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
602 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
603BEGIN SELECT RAISE(ABORT, 'GIL layer belongs to another session'); END;
604CREATE TRIGGER IF NOT EXISTS gil_events_layer_update_same_session
605BEFORE UPDATE OF session_id, layer_id ON gil_events
606WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
607 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
608BEGIN SELECT RAISE(ABORT, 'GIL layer belongs to another session'); END;
609"#;
610
611pub const NODE_OWNERSHIP_TRIGGERS: &str = r#"
612CREATE TRIGGER IF NOT EXISTS event_loop_lags_layer_same_session
613BEFORE INSERT ON event_loop_lags WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
614 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
615BEGIN SELECT RAISE(ABORT, 'event-loop layer belongs to another session'); END;
616CREATE TRIGGER IF NOT EXISTS event_loop_lags_layer_update_same_session
617BEFORE UPDATE OF session_id, layer_id ON event_loop_lags
618WHEN NEW.layer_id IS NOT NULL AND NOT EXISTS
619 (SELECT 1 FROM layers WHERE id=NEW.layer_id AND session_id=NEW.session_id)
620BEGIN SELECT RAISE(ABORT, 'event-loop layer belongs to another session'); END;
621"#;
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    fn in_mem() -> Connection {
628        Connection::open_in_memory().unwrap()
629    }
630
631    #[test]
632    fn applies_cleanly_per_class() {
633        for class in [
634            TargetClass::Gpu,
635            TargetClass::NativeCpu,
636            TargetClass::ManagedDotnet,
637            TargetClass::Jvm,
638            TargetClass::Python,
639            TargetClass::JsNode,
640            TargetClass::Ruby,
641            TargetClass::Php,
642        ] {
643            let c = in_mem();
644            apply(&c, class).unwrap_or_else(|e| panic!("apply({class:?}): {e}"));
645        }
646    }
647
648    #[test]
649    fn apply_is_idempotent() {
650        let c = in_mem();
651        apply(&c, TargetClass::Gpu).unwrap();
652        apply(&c, TargetClass::Gpu).unwrap();
653        apply(&c, TargetClass::Gpu).unwrap();
654    }
655
656    #[test]
657    fn core_tables_exist_after_apply() {
658        let c = in_mem();
659        apply(&c, TargetClass::NativeCpu).unwrap();
660        for t in [
661            "sessions",
662            "layers",
663            "symbols",
664            "meta",
665            "failures",
666            "regions",
667            "allocations",
668            "commands",
669            "breakpoint_hits",
670            "watch_evals",
671            "disassembly",
672            "source_snapshots",
673            "alloc_sites",
674            "insn_hits",
675            "insn_hit_details",
676            "samples",
677            "counters",
678        ] {
679            let exists: i64 = c
680                .query_row(
681                    "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
682                    [t],
683                    |r| r.get(0),
684                )
685                .unwrap();
686            assert_eq!(exists, 1, "missing table: {t}");
687        }
688    }
689
690    #[test]
691    fn gpu_class_has_launches_but_not_samples() {
692        let c = in_mem();
693        apply(&c, TargetClass::Gpu).unwrap();
694        let launches: i64 = c
695            .query_row(
696                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='launches'",
697                [],
698                |r| r.get(0),
699            )
700            .unwrap();
701        assert_eq!(launches, 1);
702        let samples: i64 = c
703            .query_row(
704                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='samples'",
705                [],
706                |r| r.get(0),
707            )
708            .unwrap();
709        assert_eq!(samples, 0, "samples table should not exist for GPU class");
710    }
711
712    #[test]
713    fn allocation_layer_links_must_stay_in_the_same_session() {
714        let c = in_mem();
715        apply(&c, TargetClass::NativeCpu).unwrap();
716        c.execute("INSERT INTO sessions (id, kind, target, target_class, started_at, label) VALUES ('a', 'debug', 't', 'native-cpu', 'now', 'a')", []).unwrap();
717        c.execute("INSERT INTO sessions (id, kind, target, target_class, started_at, label) VALUES ('b', 'debug', 't', 'native-cpu', 'now', 'b')", []).unwrap();
718        c.execute(
719            "INSERT INTO layers (session_id, source) VALUES ('b', 'other')",
720            [],
721        )
722        .unwrap();
723        let layer_id = c.last_insert_rowid();
724        assert!(c.execute(
725            "INSERT INTO alloc_sites (session_id, bytes_total, layer_id, collected_at) VALUES ('a', 1, ?1, 'now')",
726            [layer_id],
727        ).is_err());
728    }
729}