|
| 1 | +-- The proposed store.db migration 63, exactly as the design note specifies it. |
| 2 | +-- migcheck.rs runs this file unchanged against a clone of the whole store and checks every |
| 3 | +-- row it moves. It must stay plain SQL: cortexkit-store migrations are static SQL batches |
| 4 | +-- run inside one transaction together with their version record. |
| 5 | +-- |
| 6 | +-- SQLite 3.46.0 (libsqlite3-sys 0.30.1, the version ck-mc links) or newer is required for |
| 7 | +-- ORDER BY inside an aggregate. |
| 8 | + |
| 9 | +-- 1. Shape guards. Each INSERT counts the rows the codec could not round-trip, and the CHECK |
| 10 | +-- fails the whole migration, rolling it back, if any count is not zero. The store then |
| 11 | +-- stays at version 62 and an older binary can still open it. |
| 12 | +CREATE TEMP TABLE mc_migration_63_guard ( |
| 13 | + problem TEXT NOT NULL, |
| 14 | + bad_rows INTEGER NOT NULL CHECK (bad_rows = 0) |
| 15 | +); |
| 16 | +INSERT INTO mc_migration_63_guard |
| 17 | +SELECT 'core_state.frozen_units is present but not an array', COUNT(*) |
| 18 | + FROM mc_cache_state |
| 19 | + WHERE json_type(core_state, '$.frozen_units') NOT IN ('array'); |
| 20 | +INSERT INTO mc_migration_63_guard |
| 21 | +SELECT 'a frozen unit is not a JSON object', COUNT(*) |
| 22 | + FROM mc_cache_state AS s, json_each(s.core_state, '$.frozen_units') AS e |
| 23 | + WHERE json_type(s.core_state, '$.frozen_units') = 'array' AND e.type <> 'object'; |
| 24 | +INSERT INTO mc_migration_63_guard |
| 25 | +SELECT 'meta.resolved_compartment_boundaries is present but not an array', COUNT(*) |
| 26 | + FROM mc_cache_state |
| 27 | + WHERE json_type(meta, '$.resolved_compartment_boundaries') NOT IN ('array', 'null'); |
| 28 | +INSERT INTO mc_migration_63_guard |
| 29 | +SELECT 'a resolved compartment boundary is not a JSON object', COUNT(*) |
| 30 | + FROM mc_cache_state AS s, json_each(s.meta, '$.resolved_compartment_boundaries') AS e |
| 31 | + WHERE json_type(s.meta, '$.resolved_compartment_boundaries') = 'array' AND e.type <> 'object'; |
| 32 | +INSERT INTO mc_migration_63_guard |
| 33 | +SELECT 'meta.tail_hygiene_baseline is present but not an object', COUNT(*) |
| 34 | + FROM mc_cache_state |
| 35 | + WHERE json_type(meta, '$.tail_hygiene_baseline') NOT IN ('object', 'null'); |
| 36 | +DROP TABLE mc_migration_63_guard; |
| 37 | + |
| 38 | +-- 2. Frozen units: fixed-size positional chunks of 64 units. |
| 39 | +-- json(e.value) is required. Under GROUP BY, SQLite's sorter drops the JSON subtype of |
| 40 | +-- json_each.value, and json_group_array(e.value) then stores every unit as a JSON string. |
| 41 | +CREATE TABLE mc_cache_frozen_chunks ( |
| 42 | + session_id TEXT NOT NULL, |
| 43 | + chunk INTEGER NOT NULL, |
| 44 | + body TEXT NOT NULL, |
| 45 | + PRIMARY KEY (session_id, chunk) |
| 46 | +); |
| 47 | +INSERT INTO mc_cache_frozen_chunks (session_id, chunk, body) |
| 48 | +SELECT s.session_id, e.key / 64, json_group_array(json(e.value) ORDER BY e.key) |
| 49 | + FROM mc_cache_state AS s, json_each(s.core_state, '$.frozen_units') AS e |
| 50 | + GROUP BY s.session_id, e.key / 64; |
| 51 | + |
| 52 | +-- 3. Values replaced wholesale: one row per non-empty section. No row means the section is |
| 53 | +-- absent (an empty boundary list, or no tail baseline). |
| 54 | +CREATE TABLE mc_cache_sections ( |
| 55 | + session_id TEXT NOT NULL, |
| 56 | + section TEXT NOT NULL, |
| 57 | + body TEXT NOT NULL, |
| 58 | + PRIMARY KEY (session_id, section) |
| 59 | +); |
| 60 | +INSERT INTO mc_cache_sections (session_id, section, body) |
| 61 | +SELECT session_id, 'resolved_compartment_boundaries', |
| 62 | + json_extract(meta, '$.resolved_compartment_boundaries') |
| 63 | + FROM mc_cache_state |
| 64 | + WHERE json_type(meta, '$.resolved_compartment_boundaries') = 'array' |
| 65 | + AND json_array_length(meta, '$.resolved_compartment_boundaries') > 0; |
| 66 | +INSERT INTO mc_cache_sections (session_id, section, body) |
| 67 | +SELECT session_id, 'tail_hygiene_baseline', json_extract(meta, '$.tail_hygiene_baseline') |
| 68 | + FROM mc_cache_state |
| 69 | + WHERE json_type(meta, '$.tail_hygiene_baseline') = 'object'; |
| 70 | + |
| 71 | +-- 4. The small row. section_index records what the chunk and section rows must hold. |
| 72 | +-- "sv" is the sections version: 0 marks rows written by this migration, whose digests |
| 73 | +-- are filled in by the open-time backfill. Only codec writers set it, always to >= 1. |
| 74 | +-- json_patch drops the keys whose value is NULL, so an absent section has no key. |
| 75 | +-- Every SET expression reads the row as it was before this UPDATE. |
| 76 | +ALTER TABLE mc_cache_state ADD COLUMN section_index TEXT NOT NULL DEFAULT '{}'; |
| 77 | +UPDATE mc_cache_state SET |
| 78 | + section_index = json_patch('{}', json_object( |
| 79 | + 'sv', 0, |
| 80 | + 'f', json_object( |
| 81 | + 'n', COALESCE(json_array_length(core_state, '$.frozen_units'), 0), |
| 82 | + 'c', (COALESCE(json_array_length(core_state, '$.frozen_units'), 0) + 63) / 64), |
| 83 | + 'b', CASE WHEN json_array_length(meta, '$.resolved_compartment_boundaries') > 0 |
| 84 | + THEN json_object('n', json_array_length(meta, '$.resolved_compartment_boundaries')) |
| 85 | + END, |
| 86 | + 't', CASE WHEN json_type(meta, '$.tail_hygiene_baseline') = 'object' |
| 87 | + THEN json('{}') |
| 88 | + END)), |
| 89 | + core_state = json_remove(core_state, '$.frozen_units'), |
| 90 | + meta = json_remove(meta, '$.resolved_compartment_boundaries', '$.tail_hygiene_baseline'); |
| 91 | + |
| 92 | +-- 5. Pass-trace histories as ring rows. The sequence orders entries; the slot only bounds |
| 93 | +-- storage, so readers order by seq and never by slot. The request history lives today in a |
| 94 | +-- carrier entry inside scheduler_interesting_history; only the newest carrier counts, |
| 95 | +-- as in pass_trace_meta_parts. |
| 96 | +CREATE TABLE mc_pass_trace_history ( |
| 97 | + session_id TEXT NOT NULL, |
| 98 | + kind TEXT NOT NULL, -- 'scheduler' | 'interesting' | 'request' |
| 99 | + slot INTEGER NOT NULL, -- seq % 256, or seq % 32 for 'request' |
| 100 | + seq INTEGER NOT NULL, |
| 101 | + entry TEXT NOT NULL, |
| 102 | + PRIMARY KEY (session_id, kind, slot) |
| 103 | +); |
| 104 | +INSERT INTO mc_pass_trace_history (session_id, kind, slot, seq, entry) |
| 105 | +SELECT t.session_id, 'scheduler', e.key % 256, e.key, json(e.value) |
| 106 | + FROM mc_pass_trace AS t, json_each(t.scheduler_history) AS e; |
| 107 | +INSERT INTO mc_pass_trace_history (session_id, kind, slot, seq, entry) |
| 108 | +SELECT t.session_id, 'interesting', e.key % 256, e.key, json(e.value) |
| 109 | + FROM mc_pass_trace AS t, json_each(t.scheduler_interesting_history) AS e |
| 110 | + WHERE json_extract(e.value, '$.scheduler_decision') IS NOT '__request_trace_history__'; |
| 111 | +INSERT INTO mc_pass_trace_history (session_id, kind, slot, seq, entry) |
| 112 | +SELECT t.session_id, 'request', r.key % 32, r.key, json(r.value) |
| 113 | + FROM mc_pass_trace AS t, json_each(t.scheduler_interesting_history) AS c, |
| 114 | + json_each(c.value, '$.request_history') AS r |
| 115 | + WHERE c.key = (SELECT MAX(c2.key) FROM json_each(t.scheduler_interesting_history) AS c2 |
| 116 | + WHERE json_extract(c2.value, '$.scheduler_decision') = '__request_trace_history__'); |
| 117 | +ALTER TABLE mc_pass_trace ADD COLUMN scheduler_next_seq INTEGER NOT NULL DEFAULT 0; |
| 118 | +ALTER TABLE mc_pass_trace ADD COLUMN interesting_next_seq INTEGER NOT NULL DEFAULT 0; |
| 119 | +ALTER TABLE mc_pass_trace ADD COLUMN request_next_seq INTEGER NOT NULL DEFAULT 0; |
| 120 | +UPDATE mc_pass_trace SET |
| 121 | + scheduler_next_seq = COALESCE(json_array_length(scheduler_history), 0), |
| 122 | + interesting_next_seq = COALESCE(json_array_length(scheduler_interesting_history), 0), |
| 123 | + request_next_seq = COALESCE((SELECT MAX(seq) + 1 FROM mc_pass_trace_history AS h |
| 124 | + WHERE h.session_id = mc_pass_trace.session_id |
| 125 | + AND h.kind = 'request'), 0); |
| 126 | +-- Dropping the array columns makes a reader that names them fail loudly instead of reading |
| 127 | +-- a frozen copy. Readers that select * must move to the view below. |
| 128 | +ALTER TABLE mc_pass_trace DROP COLUMN scheduler_history; |
| 129 | +ALTER TABLE mc_pass_trace DROP COLUMN scheduler_interesting_history; |
| 130 | +CREATE VIEW mc_pass_trace_history_arrays AS |
| 131 | +SELECT t.session_id, |
| 132 | + (SELECT json_group_array(json(h.entry) ORDER BY h.seq) FROM mc_pass_trace_history AS h |
| 133 | + WHERE h.session_id = t.session_id AND h.kind = 'scheduler') AS scheduler_history, |
| 134 | + (SELECT json_group_array(json(h.entry) ORDER BY h.seq) FROM mc_pass_trace_history AS h |
| 135 | + WHERE h.session_id = t.session_id AND h.kind = 'interesting') AS scheduler_interesting_history, |
| 136 | + (SELECT json_group_array(json(h.entry) ORDER BY h.seq) FROM mc_pass_trace_history AS h |
| 137 | + WHERE h.session_id = t.session_id AND h.kind = 'request') AS request_history |
| 138 | + FROM mc_pass_trace AS t; |
0 commit comments