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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
//! DDL statements for the Macrame bitemporal schema as specified in §4.
/// GLOB pattern matching the canonical timestamp form `YYYY-MM-DDTHH:MM:SS.ffffffZ`.
///
/// A macro rather than a `const` so it can be spliced into the DDL literals by
/// `concat!`, which only accepts literals. Kept byte-identical to
/// [`crate::util::timestamp::CANONICAL_TS_GLOB`] by the unit test at the bottom
/// of this file — the storage-layer guard and the Rust-layer guard must agree
/// or one of them is decorative.
/// Table-level CHECK asserting every temporal column is canonical (§4.1, 0.5.4).
///
/// Timestamps are compared lexicographically everywhere — in SQL predicates, in
/// `MAX(recorded_at)` when the clock recovers its floor, and in Rust `str`
/// ordering. That is sound only if every value has the same width, so mixing
/// `...T00:00:00Z` with `...T00:00:00.000000Z` makes `<=` disagree with
/// chronology and traversals return empty sets with no error. The `Z` suffix
/// alone does not achieve this; a fixed width does, and a CHECK is what makes
/// it a property of the data rather than a convention.
/// A macro rather than a `const` for the same reason as [`ts_glob`]: `concat!`
/// splices it into the table DDL and only accepts literals. [`WEIGHT_CHECK`] is
/// the same text as a value, and carries the reasoning.
/// Table-level CHECK on `links.weight` (§4.7, T2.1, D-083).
///
/// Three clauses. Only the first is the one the item asked for; the other two
/// were found by probing what the first still admits.
///
/// `weight >= 0.0` is the item as written: shortest-path analytics are unsound
/// over negative weights, so Dijkstra and A\* refuse the graph at load time
/// (D-039). Until now that refusal was the *only* place the property was
/// enforced, which made it §4.7's one genuinely open gap — a database this crate
/// wrote by itself could hold a row this crate would not read back.
///
/// `typeof(weight) = 'real'` closes a hole the item does not mention and which
/// probing found. `REAL` in SQLite is an **affinity**, not a type: values that
/// can be converted are, and values that cannot are stored as they came. `'abc'`
/// cannot become a number, so it is stored as TEXT — and in SQLite's type
/// ordering every text value sorts above every numeric one, so `'abc' >= 0.0`
/// is *true* and the first clause passes it through.
///
/// That is not a wrong answer on the read side. It is a **panic**: reading a
/// text `weight` as `f64` reaches `unreachable!("invalid value type")` inside
/// libsql 0.9.30, in whatever unrelated query first touches the row. Measured,
/// not reasoned about — see `examples/weight_check_probe.rs`.
///
/// The clause costs one `typeof` per insert and refuses nothing legitimate:
/// `3`, `'5'` and `1.0` all arrive as REAL through affinity conversion and pass.
/// It is taken **now** rather than in a later rung because SQLite has no
/// `ADD CONSTRAINT` — every clause added later costs another full rebuild of the
/// largest table in the schema.
///
/// `weight < 9e999` refuses `+∞`, and the reason is not the one anybody
/// predicted. The plan expected the CHECK to admit infinity and argued the
/// loader guard would catch it; the guard tests `< 0.0` and `is_nan()`, so it
/// does not. The next guess — mine — was that this is harmless, since IEEE
/// infinity propagates through addition and stays totally ordered, leaving
/// Dijkstra terminating with "that edge is unusable": an odd answer, not a wrong
/// one.
///
/// Both were wrong, and a test found it. **An infinite weight makes the
/// transaction log unreplayable.** The log trigger serialises the row to JSON,
/// and JSON has no representation for infinity, so the payload round-trips into
/// `ReplayCorrupt { reason: "number out of range" }` — every later
/// `reconstruct()` fails, including the one `close()` performs. The ledger is
/// the source of truth under Doctrine III, so a value that cannot survive the
/// log is not an eccentric weight, it is a corrupt one.
///
/// `9e999` is the idiom because SQLite has no `isinf`: the literal overflows to
/// `+∞` on parse, and `inf < inf` is false. Finite values, including `1e308`,
/// pass.
///
/// The loader guard still **stays**, for the reason the constraint cannot cover:
/// `links_current` carries no CHECK, and neither do cold files created before
/// this rung.
pub const WEIGHT_CHECK: &str = weight_check!;
/// The `RAISE(ABORT, …)` messages the schema's guards emit (§4.3).
///
/// Spliced into the trigger DDL *and* matched by [`crate::error::abort_kind`],
/// so the guard and its classifier cannot drift. When they drift the failure is
/// silent in the worst direction: the guard still fires, but the typed error
/// (`SingleOpenViolation`, `RecordedAtRegression`, `ArchiveViolation`) degrades
/// into an opaque `Engine` error that no caller can match on.
pub const ABORT_SINGLE_OPEN: &str = abort_single_open!;
pub const ABORT_MONOTONIC_RA: &str = abort_monotonic_ra!;
pub const ABORT_DELETE_GUARD: &str = abort_delete_guard!;
pub const ABORT_CROSS_LINEAGE: &str = abort_cross_lineage!;
pub const ABORT_BRANCH_IMMUTABLE: &str = abort_branch_immutable!;
pub const ABORT_BRANCHES_FROZEN: &str = abort_branches_frozen!;
/// The root lineage every pre-v12 row is stamped with (§15.2, v12, D-214).
///
/// A macro as well as a `const` for [`ts_glob`]'s reason: it is spliced into
/// the column defaults by `concat!`, which takes only literals. One spelling,
/// so the default in the DDL, the seed row, and the Rust layer cannot drift
/// into three databases that disagree about what the trunk is called.
pub const MAIN_BRANCH: &str = main_branch!;
/// The `branch_id` column, identical on all four ledger tables (§15.2, D-214).
///
/// The default is what makes the rung `ALTER TABLE` rather than a rewrite —
/// SQLite records a constant default in the schema header and rewrites no row,
/// measured at 83–139 µs over 20,000 rows in `examples/branch_identity_probe.rs`
/// §1.
///
/// # The `REFERENCES` clause, and the condition it is actually gated on
///
/// SQLite specifies that a column added by `ALTER TABLE … ADD COLUMN` carrying
/// a `REFERENCES` clause **must default to NULL** when foreign keys are
/// enabled, because pre-existing rows cannot be validated against the new
/// parent. That collides head-on with `NOT NULL DEFAULT 'main'`.
///
/// libSQL 0.9.30 applies that rule **dynamically rather than statically**, and
/// probe §15 pins the four cases: the statement is refused only when the table
/// **holds rows** *and* foreign keys are **on**. An empty table takes it with
/// keys on; a populated table takes it with keys off. Being inside a
/// transaction changes nothing either way.
///
/// This is the whole reason the v11 → v12 rung sets
/// [`suspends_foreign_keys`]. It is worth being exact about what that buys,
/// because "suspend the constraint to install the constraint" invites the
/// suspicion that the result is decorative — §15 measures it and it is not.
/// After an ALTER taken with keys suspended the clause is in `sqlite_master`,
/// `PRAGMA foreign_key_list(concepts)` reports the key, an insert naming an
/// unknown branch is refused with extended code 787, and deleting a referenced
/// branch is refused **by the engine** rather than by a trigger. Enforcement is
/// a per-connection pragma; the constraint is schema. Suspending the first
/// never weakened the second.
///
/// Nor does the suspension launder a violation past the commit: `apply_step`
/// runs `PRAGMA foreign_key_check` inside the transaction, and §15 confirms it
/// reports the orphan when one is deliberately planted during the window.
///
/// Taken deliberately, with the dependency named in §19 rather than absorbed.
/// The exposure is narrow and it is on the **upgrade** path only: fresh
/// databases put the clause in a `CREATE TABLE`, where no engine has ever
/// disputed it. If upstream ever tightens to SQLite's static reading, the rung
/// fails **loudly** — at a named step, inside `BEGIN IMMEDIATE`, leaving the
/// database honestly at v11 — and the fallback is one line: drop the clause
/// from the ALTER and let the `branches` write guard carry lineage integrity
/// alone, which is the weaker guarantee and the one D-030 has a name for.
///
/// [`suspends_foreign_keys`]: super::migrations
pub const BRANCH_COLUMN: &str = branch_column!;
/// Marker table probed by the delete guards (D-008 revised).
///
/// The archive session creates this table and drops it again inside the single
/// `BEGIN IMMEDIATE … COMMIT` archive transaction, so it never exists as
/// committed state. Connection-locality — the property the original
/// `temp.sqlite_master` probe was reaching for — is preserved by two
/// independent mechanisms: uncommitted DDL is visible only to the writing
/// connection, and the archive transaction holds the write lock for its
/// duration, so no other connection can reach the guard at all.
pub const ARCHIVE_SESSION_MARKER: &str = "macrame_archive_session";
/// The concepts insert log trigger, **marker-gated since v10** (0.9.0, C3).
///
/// # Why an archive session must not log a concept insert
///
/// Rehydration is a physical move back and mints no transaction-time facts
/// (§2.3): the concept returns to the hot table, the log entries describing it
/// were never removed, and nothing about what was believed — or when — has
/// changed. An unconditional `AFTER INSERT` makes that impossible to honour,
/// because the move *is* an insert.
///
/// **And the damage is worse than a spurious row, which is what forced the
/// rung.** The rehydrated row carries its **original** `recorded_at`, but the
/// log row it would write gets a **new** `seq_id` at the end of the log. The
/// fold partitions by `(table_name, entity_id)` and takes
/// `ROW_NUMBER() OVER (… ORDER BY seq_id DESC) = 1` — last writer wins by
/// *sequence*, not by timestamp. So the rehydration `'I'` would outrank the
/// later `'U'` that retired the concept, and every `reconstruct` after the
/// original creation time would return it **un-retired**. Rehydration would
/// resurrect a belief the ledger had superseded, silently and retroactively,
/// which is precisely what [Doctrine III] forbids.
///
/// Only the *insert* trigger is gated. `trg_concepts_log_update` stays
/// unconditional because nothing inside a session updates a concept — archival
/// deletes and rehydration inserts — so gating it would suppress nothing and
/// widen the hole for no reason.
pub const CREATE_CONCEPTS_LOG_INSERT: &str = concat!;
/// The concepts delete guard, **marker-gated since v9** (0.9.0, C2, D-126).
///
/// A `pub const` rather than an anonymous entry in [`CREATE_TRIGGERS`] because
/// two readers need exactly this text: the baseline, which installs it on a new
/// database, and the `v8 → v9` rung, which replaces the v8 body on an existing
/// one. A second copy is a copy that drifts, and this trigger is the one whose
/// body carries a doctrine decision.
///
/// # What changed, and why re-issuing the baseline could not do it
///
/// Through v8 this guard was **unconditional**: `BEFORE DELETE ON concepts`
/// aborting every time, on the reasoning that concepts are never physically
/// archived ([D-022](../../docs/architecture/s13-decision-register.md)). C2
/// makes that false — a declared archive session may now move a retired,
/// unreferenced concept to the cold file — so the guard takes the same shape its
/// two siblings have had since 0.5.3: it fires **unless** the archive-session
/// marker is present.
///
/// It needs a rung of its own, and that was measured rather than assumed
/// (D-126). `CREATE TRIGGER IF NOT EXISTS` on an existing name keeps the **old
/// body** — re-issuing the baseline against a v8 database leaves the
/// unconditional guard exactly where it was — and `verify` compared `type` and
/// `name` and never bodies, so the stale guard passed verification in silence.
/// Both halves are now closed: the rung drops and recreates, and `verify`
/// checks that every delete guard's body probes the marker.
pub const CREATE_CONCEPTS_GUARD_DELETE: &str = concat!;
/// The `concepts` ledger table (§4.1).
///
/// # `rowid_pk` is explicit, and that is the whole point (v8, D-119)
///
/// Through v7 this table declared `id TEXT PRIMARY KEY`, which left its rowid
/// **implicit** — and `concepts_fts` is external-content keyed on that rowid.
/// `VACUUM` renumbers implicit rowids, which would silently decouple the search
/// index from the rows it indexes: no error, no integrity-check failure, just
/// results that stop matching.
///
/// [D-071](../../docs/architecture/s13-decision-register.md) proved the hazard
/// unreachable *by consequence rather than by design* — `trg_concepts_guard_delete`
/// is unconditional, so rowids are dense `1..n` and `VACUUM`'s renumbering is
/// the identity map. 0.9.0's archival makes them sparse and makes the hazard
/// real, so v8 replaces the accident with a column: an `INTEGER PRIMARY KEY` is
/// a stored value, and `VACUUM` preserves it whether the numbering is dense or
/// not (measured in `examples/concepts_rebuild_probe.rs` §5).
///
/// SQLite permits one primary key per table, so `id` becomes `NOT NULL UNIQUE`.
/// That keeps it a valid foreign-key parent for `links.source_id` /
/// `links.target_id` and keeps `ON CONFLICT(id)` working, but it **is** a
/// primary-key change — which [D-036](../../docs/architecture/s13-decision-register.md)
/// forbids outright after 1.0. Taken pre-1.0 on purpose, or never.
/// The lineage register (§15.2, v12, D-214).
///
/// Four columns and no more, because a branch is **not** a third temporal axis
/// (Doctrine II): it carries no interval of its own, only the point in the
/// second clock where it diverged.
///
/// `parent_id` is a self-referencing foreign key, declarable here because it
/// sits in a `CREATE TABLE` where SQLite permits forward and self references
/// freely. `NULL` marks the root, and the paired `CHECK` makes "root" a single
/// state rather than two columns that can disagree: a row with a parent and no
/// fork point is a lineage whose ancestry cannot be resolved, and a row with a
/// fork point and no parent is a divergence from nothing.
///
/// `forked_at` is in the **`recorded_at` domain** — the transaction-time
/// instant the lineage diverged, which is what §15.3's visibility cutoffs are
/// computed over. Not a valid-time bound: a branch does not believe things
/// about a period, it believes them from a moment onward.
///
/// The ordering `CHECK` is row-local on purpose. `forked_at <= created_at` is
/// checkable from the row itself; an ordering against the *parent's* row is
/// not, and a `CHECK` cannot see another row. The cross-row half is `fork()`'s
/// to enforce at D-034's boundary, and saying so here is cheaper than a
/// constraint that looks complete and is not.
///
/// **Which cross-row ordering, corrected in 0.14.7.** This said "the fork point
/// is at or after the parent's *creation*" from v12 until `fork()` existed to
/// enforce it, and that turned out to be uncheckable rather than merely
/// unenforced: [`seed_root_branch`](crate::schema) stamps the trunk's
/// `created_at` from `SystemTime::now()` during migration — before the
/// database's injected clock is resolved, and it cannot simply run after,
/// because the clock's floor is read from tables the migration creates. So
/// `created_at` is not on the ledger's timeline and comparing a `forked_at` to
/// it is comparing two clocks. What [`Database::fork`](crate::Database::fork) enforces instead is
/// `forked_at >= parent.forked_at`, both issued by the same clock, which makes
/// fork points non-decreasing down any root path.
pub const CREATE_BRANCHES_TABLE: &str = concat!;
/// Seed the root lineage, idempotently.
///
/// One statement shared by the baseline and the v11 → v12 rung, taking
/// `created_at` as a parameter. `OR IGNORE` rather than `IF NOT EXISTS`
/// gymnastics because both callers may run against a database that already has
/// the row — the rung on a retry, the baseline never, but a single statement
/// that is safe for both is one fewer thing to reason about.
///
/// This must run **before** any row is stamped, on both paths: every
/// `branch_id` default names `'main'`, and the foreign key means a database
/// without this row cannot accept a single write.
pub const SEED_MAIN_BRANCH: &str = concat!;
/// `branches` is append-only outside an archive session (§15.2, §15.4).
///
/// The two guards no longer say the same thing, and 0.14.13 is where they
/// parted. This one stays **unconditional**: no session of any kind may edit a
/// lineage record in place. [`CREATE_BRANCHES_GUARD_DELETE`] is now gated on
/// the archive-session marker like its three siblings, because
/// [`crate::Database::archive_branch`] made removing a lineage record a legal
/// operation — see that guard for what changed and why the change needed a
/// rung of its own.
///
/// # Why `UPDATE` is refused whole-row
///
/// The foreign key already refuses renaming or deleting a lineage any row
/// still points at, so this guard is not what keeps the ledger from being
/// orphaned. What it keeps is narrower and harder to see: `parent_id` and
/// `forked_at` are the inputs to ancestry, so editing either **re-derives the
/// visibility of rows already written**, with no new assertion anywhere. That
/// is the move [Doctrine III] forbids, reachable by one raw-SQL statement, and
/// no foreign key has anything to say about it.
///
/// Whole-row rather than a named subset because nothing on the row legitimately
/// changes, and a whole-row guard needs no maintenance the day a column is
/// added.
///
/// [Doctrine III]: ../../docs/architecture/README.md
pub const CREATE_BRANCHES_GUARD_UPDATE: &str = concat!;
/// The delete half of the rule, **marker-gated since v13** (0.14.13, §15.4,
/// [D-230](../../docs/architecture/s13-decision-register.md#d-230)).
///
/// # What changed
///
/// Through v12 this guard was unconditional, and its own docstring said why:
/// *"there is no session in which removing a lineage record is legal — branches
/// are never archived"*. [`crate::Database::archive_branch`] makes that false.
/// The sentence was a true description of the operations that existed, written
/// as though it were a property of the table, which is the shape D-035 asks to
/// be stated rather than assumed.
///
/// **The lineage row must move, and that is forced rather than chosen.** An
/// abandonment arm that took the branch's `links` and left its `branches` row
/// would leave `hot_log_reach` unsound: that probe's argument rests on *the
/// newest row per entity is never archivable*, which holds for a predicate
/// needing a later row to exist and fails for one that takes a whole lineage.
/// Moving the `branches` row is what makes a hot fold that omits the lineage
/// **correct rather than silently short** — every read and write naming the
/// name now raises [`crate::DbError::UnknownBranch`], which is a refusal, not a
/// wrong answer.
///
/// # Why it needed a rung
///
/// [`CREATE_CONCEPTS_GUARD_DELETE`]'s reason, measured once already (D-126):
/// `CREATE TRIGGER IF NOT EXISTS` on an existing name keeps the **old body**,
/// so re-issuing the baseline against a v12 database leaves the unconditional
/// guard exactly where it is and `archive_branch` fails on every ledger that
/// was not created by this build. The v12 → v13 rung drops and recreates, and
/// `verify` now carries this name in `DELETE_GUARDS`, so a database whose
/// guard predates the change is refused at open with a sentence rather than at
/// archive time with a trigger abort.
///
/// The update guard is deliberately **not** gated — see
/// [`CREATE_BRANCHES_GUARD_UPDATE`]. Archival is a move; there is still no
/// session in which editing a lineage's parent or fork point is legal, and
/// gating both would have suspended a rule the operation does not need
/// suspended.
pub const CREATE_BRANCHES_GUARD_DELETE: &str = concat!;
/// A branch inherits concepts; it does not restate them (§15.2, D-214).
///
/// `concepts` is a current-state projection keyed by identity — `id` is
/// `NOT NULL UNIQUE` and the write path uses `ON CONFLICT(id) DO UPDATE` — so
/// two lineages holding different beliefs about one concept is two rows with
/// one `id`, which the unique index refuses on its own (probe §2). What it
/// refuses it refuses as a *constraint failure*, naming nothing; this guard
/// turns the same refusal into a sentence that says which rule was broken.
///
/// It fires **before** `ON CONFLICT` is considered, which is not obvious and
/// was measured rather than assumed (probe §7): a cross-lineage upsert is
/// refused, a same-lineage one is accepted, and a new id is accepted.
///
/// Exact-branch equality, not ancestry. A branch that may restate its parent's
/// concepts is the overlay design, and the overlay is deferred with its reopen
/// trigger named (D-214) — a guard that quietly permitted the ancestry case
/// would ship half of it with none of the machinery that makes it correct.
pub const CREATE_CONCEPTS_GUARD_LINEAGE: &str = concat!;
/// `branch_id` records where a row was minted, and minting happened once.
///
/// The column is **provenance, not identity** (D-214), and the distinction is
/// exactly what this guard keeps true. An `UPDATE` that moved a concept between
/// lineages would rewrite where a belief came from without asserting anything
/// new — the same shape as editing `branches.parent_id`, and forbidden for the
/// same reason.
pub const CREATE_CONCEPTS_GUARD_BRANCH: &str = concat!;
pub const CREATE_CONCEPTS_TABLE: &str = concat!;
/// The ledger. Append-only, one row per assertion, and **keyed by lineage
/// since v15** (0.14.15, §15.4, [D-232]).
///
/// # `branch_id` is in the key, and the release it took to get there
///
/// v12 put `branch_id` in the key of `links_current` and left it out of this
/// one, on an argument the v12 rung records: "`links` accepts both rows because
/// `recorded_at` is already in its key". That is true of two assertions made at
/// two instants, which is what a probe testing it by hand produces — and it is
/// the reason the gap read as latent for seven releases.
///
/// It was not latent. **The batch write paths take one stamp for the whole
/// batch** ([D-014]), and `reject_overlaps_within` groups candidates by
/// `(source, target, edge_type, branch_id)` — so a pair differing *only* in
/// lineage is not an overlap, is passed straight through, and collides on the
/// key with a bare `UNIQUE constraint failed: links.…`. Both batch surfaces
/// reach it, and `examples/links_key_reach_probe.rs` is the reproduction.
///
/// # Why `branch_id` goes last
///
/// The same reason the v12 rung gives for `links_current`, and it is stronger
/// here because this table's autoindex is the *only* index over four of its
/// columns: the leading `(source_id, target_id, edge_type, valid_from,
/// recorded_at)` prefix is what `temporal::archive`'s predicates and
/// `integrity::shadow` seek on, and appending leaves every one of those plans
/// untouched. A branch-leading key would have re-planned the archive sweep to
/// buy nothing — the probe's §5 has the plans.
///
/// # What this does not change
///
/// Not Doctrine III, and not what a row means. The key admits a row the old key
/// refused; it removes none, alters none, and merges none. Every database that
/// climbed the v14 → v15 rung holds exactly the rows it held before, which is
/// what makes the rung a copy rather than a decision.
///
/// [D-014]: ../../docs/architecture/s13-decision-register.md#d-014
/// [D-232]: ../../docs/architecture/s13-decision-register.md#d-232
pub const CREATE_LINKS_TABLE: &str = concat!;
pub const CREATE_LINKS_CURRENT_TABLE: &str = concat!;
pub const CREATE_TRANSACTION_LOG_TABLE: &str = concat!;
/// The per-model embedding table (§4.1, D-005), for a validated model name.
///
/// A function rather than a `const` because the table's identity *and its
/// column type* both depend on the model: `F32_BLOB(dim)` carries the declared
/// dimension in the schema, which is what [`crate::vector::declared_dimension`]
/// reads back so the crate never keeps a second copy of it.
///
/// Deliberately not part of the baseline migration. Which models exist is an
/// application's choice made over time, not a property of the schema version,
/// and D-036 classifies these tables as disposable periphery: a migration may
/// drop one and re-embed. `IF NOT EXISTS` makes registration idempotent.
///
/// No temporal columns, on purpose. Doctrine VII makes an embedding a derived
/// artifact of a model applied to content — it has no valid time of its own, and
/// giving it a `recorded_at` would put a third clock next to the two §2 permits
/// and invite queries that mix them.
/// The DiskANN index over a model's vectors.
///
/// **Load-bearing for correctness, not only for speed.** Measured against
/// libSQL 0.9.30: a blob of the wrong length inserted into an `F32_BLOB(4)`
/// column is *accepted* while no vector index exists, and rejected — with the
/// row not landing — once one does. §4.1 previously claimed the column type
/// enforced its own dimension at insert time; it does not. So this index is
/// created together with the table it indexes and is never optional, and
/// dropping it to speed up a bulk load would silently disarm the only
/// storage-layer check on dimension.
/// Derived analytics output, keyed by concept and label (§5.4, D-041).
///
/// Deliberately outside the ledger. Three properties are load-bearing and each
/// is the opposite of what the four normative tables above do.
///
/// **No log trigger.** Nothing in [`CREATE_TRIGGERS`] fires on this table, so an
/// annotation never reaches `transaction_log`. That is Doctrine VII's reasoning
/// about embeddings applied to the other derived artifact: a community label is
/// a function of an algorithm, a version of that algorithm, and a graph — not a
/// statement about the world, and a ledger that records it is recording the
/// analytics schedule as though it were history. A reconstruction that wants
/// labels recomputes them, which is the only honest way to ask what a past
/// graph's communities *were*.
///
/// **No delete guard.** Doctrine V protects the hot ledger tables; this table is
/// derivative state in Doctrine VI's second category, so wiping it must stay a
/// legal, ordinary operation — a rerun replaces the previous pass, and dropping
/// the whole table costs nothing but the recomputation.
///
/// **Upsert on `(concept_id, label)`.** One current value per label per concept.
/// Storing a history of successive runs here would be the ledger again, by
/// another name.
///
/// The foreign key is safe in a way `links_current`'s omitted ones are not:
/// concepts are never physically deleted (D-022), and this table is rebuilt by
/// re-running an algorithm that read `concepts` in the first place, so there is
/// no insertion-order problem to solve.
/// One row, one bit: has anything ever been deleted from `transaction_log`?
/// (v16, 0.15.7, W14.5, [D-249], review C-5.)
///
/// # Why this is a table and not a query
///
/// The bit is `temporal::replay`'s reach guard, and until v16 the
/// guard computed it: `MIN(seq_id) = 1 AND COUNT(*) = MAX(seq_id)`, exact
/// because `seq_id` is `INTEGER PRIMARY KEY AUTOINCREMENT` and never reused.
/// The `MIN` and `MAX` are index seeks; the `COUNT(*)` is a scan of the whole
/// log, and it ran on every recorded-time read below the newest surviving
/// stamp. Measured (`examples/log_integrity_probe.rs`): 0.134 ms at 2,000 rows
/// and **32.6 ms at 500,000**, against an id-bounded hydration that is flat at
/// 0.14 ms whatever the log holds. Reading this row instead is 0.033 ms at
/// every size.
///
/// There is no cheaper exact query. `LOG_ARCHIVABLE` removes superseded rows
/// wherever they sit, so a gap can be anywhere in the sequence and only
/// counting finds it. What there is instead is a fact the storage already
/// knows at the moment it becomes true, and did not write down.
///
/// # Why a trigger and not the archive code
///
/// [`CREATE_TXLOG_MARK_GAP`] maintains it, so the bit is a property of the
/// **table** rather than of the crate's archive path. §4.2 admits that raw SQL
/// against the same file can do what this API refuses; a bit maintained in Rust
/// would be wrong after exactly that, and wrong in the direction that folds a
/// gap silently. A trigger is wrong in neither direction, because there is no
/// route to deleting a log row that does not pass through it.
///
/// # The seed is computed, not assumed
///
/// A database arriving at v16 may already have gaps, so
/// [`SEED_LOG_INTEGRITY`] derives the initial value from the log's own
/// `sqlite_sequence` high-water mark. That test is exact where the guard's old
/// `COUNT(*) = MAX(seq_id)` was exact **and in one state where it was not** —
/// a hot log archived down to nothing, which the old form called intact. See
/// `the_bit_agrees_with_the_count_it_replaced`.
///
/// [D-249]: ../../docs/architecture/s13-decision-register.md#d-249
pub const CREATE_LOG_INTEGRITY_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS log_integrity (
id INTEGER PRIMARY KEY CHECK (id = 1),
rows_removed INTEGER NOT NULL DEFAULT 0 CHECK (rows_removed IN (0, 1))
)
"#;
/// Compute [`CREATE_LOG_INTEGRITY_TABLE`]'s bit from the log itself (v16, [D-249]).
///
/// One statement, run by `baseline` and by the v15 -> v16 rung alike, because a
/// rule stated twice is a rule that can disagree with itself ([D-035]) — and
/// these two would have: a baseline log is empty, an upgraded one may have been
/// archived for years, and "empty" is exactly where the obvious test is wrong.
///
/// # The witness is `sqlite_sequence`, not `MAX(seq_id)`
///
/// `transaction_log.seq_id` is `INTEGER PRIMARY KEY AUTOINCREMENT`, so SQLite
/// keeps the high-water mark of every id it has ever allocated in
/// `sqlite_sequence`, and **deleting rows does not lower it**. A rolled-back
/// transaction rolls the counter back with it ([D-049]), so the mark is exactly
/// the number of rows the log has ever held. Therefore `COUNT(*) = seq` holds
/// if and only if nothing has left, whatever the shape of what left: interior
/// gaps, a raised floor, or every row at once.
///
/// That last one is why this is not the test `temporal::replay` used
/// before v16. `MIN(seq_id) = 1 AND COUNT(*) = MAX(seq_id)` is exact on a
/// non-empty log and says *intact* on an empty one, which is right for a
/// database that has never been written and wrong for one that has been fully
/// archived — the two states it cannot see apart. `sqlite_sequence` sees them
/// apart: no row for a young log, a positive mark for an emptied one.
///
/// `OR REPLACE` because the ladder re-runs rungs over a stamped-back database
/// and this one has to be idempotent. It is: the value is a function of the
/// log, not of what is already in the row.
///
/// [D-035]: ../../docs/architecture/s13-decision-register.md#d-035
/// [D-049]: ../../docs/architecture/s13-decision-register.md#d-049
/// [D-249]: ../../docs/architecture/s13-decision-register.md#d-249
pub const SEED_LOG_INTEGRITY: &str = r#"
INSERT OR REPLACE INTO log_integrity (id, rows_removed)
SELECT 1, CASE
WHEN (SELECT COUNT(*) FROM transaction_log)
= COALESCE(
(SELECT seq FROM sqlite_sequence WHERE name = 'transaction_log'),
0)
THEN 0
ELSE 1
END
"#;
/// Set the bit when a log row is physically deleted (v16, [D-249]).
///
/// `AFTER DELETE`, so it fires only on a delete that happened —
/// `trg_txlog_guard_delete`'s `BEFORE DELETE` ([`CREATE_TRIGGERS`]) aborts
/// first when there is
/// no archive session, and an aborted delete must not mark the log.
///
/// It is `FOR EACH ROW` and it writes the same value every time, which looks
/// wasteful and is the cheapest correct shape available: SQLite has no
/// statement-level triggers, and a `WHEN` clause reading `log_integrity` to
/// skip the write would cost a lookup per row to save a one-page update per
/// row. Measured on a 333,000-row archive session: 2,520 ms without the
/// trigger, 2,663 ms with it — **0.43 us per row deleted, 5.6% of a delete
/// that was already the expensive half of archiving**. The read it pays for
/// runs on every recorded-time read; this runs once per archive.
pub const CREATE_TXLOG_MARK_GAP: &str = r#"
CREATE TRIGGER IF NOT EXISTS trg_txlog_mark_gap
AFTER DELETE ON transaction_log
BEGIN
UPDATE log_integrity SET rows_removed = 1 WHERE id = 1;
END;
"#;
pub const CREATE_ANALYTICS_ANNOTATIONS_TABLE: &str = concat!;
/// The keyword half of hybrid search: an FTS5 index over concept text (§5.9).
///
/// **External content.** The table declares `content='concepts'`, so the tokens
/// are indexed but the text itself is not duplicated — FTS5 reads it back from
/// `concepts` by rowid when it needs a column value. Two reasons beyond the
/// storage saving, and the second is the one that decided it:
///
/// * There is exactly one copy of the text, so the index cannot disagree with
/// the concept about what the concept says. A standalone FTS table would be a
/// second description of data the ledger already holds, which is the failure
/// class D-030 and D-035 exist to prevent.
/// * `INSERT INTO concepts_fts(concepts_fts) VALUES('rebuild')` reconstructs the
/// whole index from the content table in one statement. D-036 requires every
/// derivative table to be rebuildable from the ledger, and here that is the
/// engine's own operation rather than code of ours that has to be kept honest.
///
/// The cost is that external-content tables do not maintain themselves: an
/// `UPDATE` must retract the *old* terms before adding the new ones, using the
/// old column values. That is what `trg_concepts_fts_update` does, and getting
/// it wrong leaves an index that still matches text no concept contains.
///
/// **`content_rowid` names `rowid_pk`, not `rowid` (v8, D-119).** They are the
/// same value — an `INTEGER PRIMARY KEY` *is* the rowid — but naming the column
/// is what makes the key a declared one rather than an implicit one `VACUUM` is
/// free to renumber. See [`CREATE_CONCEPTS_TABLE`].
pub const CREATE_CONCEPTS_FTS: &str = r#"
CREATE VIRTUAL TABLE IF NOT EXISTS concepts_fts USING fts5(
title,
content,
content='concepts',
content_rowid='rowid_pk'
);
"#;
/// FTS5's own consistency check — **and it cannot see the failure that matters**
/// (§5.9, D-071).
///
/// Kept as a named constant so the finding has somewhere to live, and used by
/// `an_emptied_fts_index_still_passes_integrity_check`, which is a tripwire
/// rather than a guarantee.
///
/// On this libSQL build (0.9.30), `'integrity-check'` verifies the index's
/// *internal* consistency and not its agreement with the content table. Measured:
/// after `'delete-all'` the index answers zero matches where it answered ten, and
/// both `'integrity-check'` and `'integrity-check', 0` still report success. So a
/// `verify_fts()` built on this would report a healthy index for an empty one —
/// which is why there is no `verify_fts()`. See D-071.
pub const VERIFY_CONCEPTS_FTS: &str =
"INSERT INTO concepts_fts (concepts_fts) VALUES ('integrity-check');";
/// Reconstruct the FTS index from `concepts` (§5.9, D-036).
///
/// The engine's own operation, so the rebuild path is not a second
/// implementation of the triggers that could drift from them.
pub const REBUILD_CONCEPTS_FTS: &str =
"INSERT INTO concepts_fts (concepts_fts) VALUES ('rebuild');";
/// Refresh the query planner's statistics (0.12.4, D-149).
///
/// # Why this exists at all
///
/// Until 0.12.4 nothing in this crate ever ran `ANALYZE`, so `sqlite_stat1` did
/// not exist in any database Macrame had created and **every plan was costed
/// against SQLite's built-in defaults**: assume ~1M rows, assume each bound
/// equality column divides the search by ten. That estimate is *structural* — a
/// function of how many columns a query binds, not of what the table holds.
///
/// Which is a restatement of this schema's own worst recurring defect. From
/// `tests/index_plan_tests.rs`: *"a covering index captures a query because it
/// contains the columns, not because it discriminates."* D-042, D-059 and D-064
/// are three instances of a planner doing the only thing available to it.
/// [`CREATE_INDICES`] declares two indices that both lead on `source_id`, and
/// with no statistics the planner separates them by column count alone.
///
/// # Bounded by construction
///
/// `ANALYZE` is a **write** — it writes `sqlite_stat1` and takes the write lock —
/// so unbounded on a populated `links_current` it is exactly the kind of
/// unbudgeted hold `CHUNK_BUDGET` exists to prevent. [`ANALYSIS_LIMIT`], set once
/// per connection in `configure`, caps the rows examined per index and makes the
/// cost a function of the index count rather than the table size. That is what
/// lets this be scheduled as ordinary low-priority work.
pub const ANALYZE: &str = "ANALYZE;";
/// Re-analyse only what has gone stale (0.12.4, D-149).
///
/// SQLite tracks how much each table has changed since its last analysis and
/// runs `ANALYZE` only where it believes the statistics no longer hold. A no-op
/// when nothing has moved, which is what makes it safe to call on a schedule
/// rather than only on demand.
///
/// Bounded by [`ANALYSIS_LIMIT`] like everything else on the connection.
pub const OPTIMIZE: &str = "PRAGMA optimize;";
/// The row cap that makes [`ANALYZE`] budgetable.
///
/// 400 is SQLite's own documented recommendation. It buys approximate statistics
/// in roughly constant time instead of exact statistics in time proportional to
/// the table — and approximate is emphatically enough here, because the decision
/// being informed is *which of two indices discriminates*, not a cardinality
/// estimate anyone reads.
///
/// Set on the connection rather than around each call, so it also bounds the
/// analysis [`OPTIMIZE`] triggers internally. A limit that applied only to the
/// explicit path would leave the scheduled one unbounded, which is the half that
/// runs without anybody watching.
///
/// # Measured in 0.12.23: it is a constant factor, not a bound (D-166)
///
/// D-149 claimed this makes `ANALYZE`'s cost "a function of the index count
/// rather than the table size". Measured on this schema — `examples/analyze_hold.rs`,
/// which times the crate's own hold beside the same file analysed with the
/// pragma off and on:
///
/// | edges | crate's hold | limit off | limit 400 |
/// |---|---|---|---|
/// | 10,000 | 5.26 ms | 18.4 ms | 6.01 ms |
/// | 40,000 | 19.1 ms | 78.6 ms | 19.4 ms |
///
/// The pragma **is** in force — the crate's hold tracks the capped arm and not
/// the uncapped one, which is how it is established at all, since the
/// connection that runs `ANALYZE` is the actor's and no test can reach it. It
/// is worth 3.1× at 10,000 edges and 4.1× at 40,000.
///
/// What it does not do is remove the table from the equation: over that 4×
/// range the capped time grew 3.2×. So `analyze()` on a 40,000-edge ledger
/// holds the write lock for ~19 ms, about 6× [`crate::CHUNK_BUDGET`], and
/// [`crate::metrics::CommandKind::Analyze`] is **not** budget-exempt — it
/// appears in `metrics().budget_violations()` and always had.
///
/// Since 0.13.24 that kind is `analyze()` alone; `optimize()` reports as
/// [`crate::metrics::CommandKind::Optimize`] and is separately, deliberately
/// not exempt (W10.5,
/// [D-197](../../docs/architecture/s13-decision-register.md#d-197)).
pub const ANALYSIS_LIMIT: &str = "PRAGMA analysis_limit = 400";
/// Every index the schema declares.
///
/// # Two entries left in v8, and why the list is now allowed to be short
///
/// `idx_annotations_label` and `idx_lc_tgt_active` were dropped by the v7 → v8
/// rung ([D-089](../../docs/architecture/s13-decision-register.md), completed by
/// D-118). Neither had a reader anywhere in the crate — `analytics_annotations`
/// is never selected from here at all, and no query seeks on
/// `links_current.target_id` as a leading column — so each was an index write
/// per insert, forever, buying nothing. One of them was on the crate's hottest
/// write path.
///
/// `tests/index_plan_tests.rs` now requires the unread set to be **empty**,
/// which turns "these two are known bad" into "an index with no reader is a red
/// test". That is the guarantee this list is kept short by.
///
/// # `idx_links_target` is not `idx_lc_tgt_active` coming back
///
/// The two look like the same index and are not, which is worth stating because
/// the resemblance is the trap. `idx_lc_tgt_active` was `(target_id, valid_to)`
/// on **`links_current`**, the materialized projection, and it was dropped
/// because *nothing in the crate seeks on it* — no reader, pure write cost.
/// `idx_links_target` is `(target_id)` on **`links`**, the ledger, and it exists
/// because `CONCEPTS_ARCHIVABLE` seeks on exactly that column and the plan is
/// measured before and after.
///
/// D-089's rule was never "no index on a target column". It was "an index needs
/// a named query that seeks on it", and the registry is what enforces the
/// difference rather than this paragraph.
/// The two indices on `links_current`, named because a rung has to restore
/// them (§15.2, D-214).
///
/// `links_current` is derivative, so the v11 → v12 rung re-creates it rather
/// than altering it — and `DROP TABLE` takes the table's indices with it.
/// Neither [`CREATE_LINKS_CURRENT_TABLE`] nor `rebuild_within` puts them back:
/// the first declares a table and the second fills one. The open-time schema
/// verifier is what noticed, which is the argument for having it.
///
/// `pub(crate)` rather than `pub`: every other const this module publishes
/// describes the schema a caller might want to read, and these two exist
/// only so a rung can put back what its own `DROP TABLE` removed. The
/// published form of an index is still [`CREATE_INDICES`], which contains
/// both of these.
///
/// Named consts rather than a `CREATE_INDICES` scan for `ON links_current`,
/// because a rung should state which indices it owes rather than derive the
/// list from a definition that will keep changing after it. If a later release
/// adds a third index here, that release's rung adds it — this one is a
/// statement about v12 and stays one.
pub const LC_TRAVERSAL_COVER: &str = "CREATE INDEX IF NOT EXISTS \
idx_lc_traversal_cover ON links_current \
(source_id, valid_from, valid_to, weight, edge_type, target_id);";
/// See [`LC_TRAVERSAL_COVER`].
pub const LC_OPEN_INTERVAL: &str = "CREATE INDEX IF NOT EXISTS \
idx_lc_open_interval ON links_current \
(source_id, target_id, edge_type, valid_to, valid_from);";
/// The lineage read's own index (0.14.14, §15.4, D-231, shipped v13 -> v14).
///
/// See [`CREATE_INDICES`] for what seeks on it and why it is a **second** index
/// rather than a column added to [`LC_TRAVERSAL_COVER`], which is what §15.4
/// asked for.
pub const LC_LINEAGE_CUT: &str = "CREATE INDEX IF NOT EXISTS \
idx_lc_lineage_cut ON links_current \
(branch_id, recorded_at, source_id, target_id, edge_type, valid_from, \
valid_to, weight);";
/// Every index declared `ON links_current`, for the shadow swap to put back
/// (0.15.19, review C-12).
///
/// # Why this exists next to [`CREATE_INDICES`] rather than being a scan of it
///
/// `integrity::shadow`'s swap does `DROP TABLE links_current`, which takes the
/// table's indexes with it, and has to recreate exactly the set the projection
/// has **today** — unlike a migration rung, which owes the set its own version
/// declared and is right to name them one at a time. So the swap did the one
/// thing available to it and filtered `CREATE_INDICES` on
/// `stmt.contains("links_current")`.
///
/// That test has a false positive waiting: any future index on **`links`**
/// whose text happens to mention `links_current` — in a partial-index `WHERE`,
/// or in the comment above it, since these are one string each — would be
/// recreated against the renamed table inside the swap's transaction. It would
/// either fail the swap or leave an index nobody declared.
///
/// A name is a name. The list is spelled out, and
/// `every_links_current_index_is_in_the_swap_list` fails the build if
/// `CREATE_INDICES` gains an entry on this table that is not here — which is
/// the property the substring test was reaching for and could not state.
pub const LINKS_CURRENT_INDICES: & =
&;
/// Every trigger that names `links_current`, with the name to drop it by
/// (0.15.19, review C-12).
///
/// Paired rather than two lists, because the swap needs both halves and needs
/// them to agree: `ALTER TABLE … RENAME` re-resolves every trigger body, so a
/// trigger naming `links_current` must be **dropped** before the rename and
/// **recreated** after it (see the `integrity::shadow` module header, which
/// measured that). A trigger dropped but not recreated leaves the projection
/// unmaintained; one recreated but not dropped fails the rename. Deriving the
/// name from the DDL by string surgery would be the same substring match one
/// layer down, so the pair is written out and
/// `every_links_current_trigger_is_in_the_swap_list` checks both directions.
pub const LINKS_CURRENT_TRIGGERS: & = &;
pub const CREATE_INDICES: & = &;
/// Every trigger the schema declares.
///
/// **`IF NOT EXISTS` means a changed body does not reach an existing file.**
/// `migrations::verify` checks trigger *presence by name*, which is deliberate
/// (a count refuses healthy databases) but does not and cannot notice that a
/// trigger present under the right name carries an older body. A database
/// stamped v5 by an earlier build therefore keeps whatever trigger text it was
/// created with until a rung drops and recreates it.
///
/// This is why the payload carries a version. Changing a log trigger's payload
/// splits the database population in two — files created after the change write
/// the new shape, files created before keep writing the old one — and the only
/// thing that makes that survivable is that every reader accepts both. A
/// payload change that did *not* bump `v` would be indistinguishable at read
/// time from corruption, which is the case `DbError::PayloadVersion` exists for.
///
/// The v1 → v2 concept payload (defect V) is deliberately left to ride along on
/// the next rung that has to move `user_version` anyway rather than claiming one
/// of its own: an old file loses `embedding_model` from its temporal reads, which
/// is exactly the behaviour it had before, and gains it the moment it is
/// migrated. Nothing regresses in the meantime.
/// `links_current` maintenance, one row per open belief **per lineage**.
///
/// A named `const` since v12 for [`CREATE_CONCEPTS_LOG_INSERT`]'s reason: the
/// rung has to re-issue this exact body, and a rung with its own copy is a copy
/// that drifts. The conflict target matches the table's primary key, which now
/// ends in `branch_id` — without that, a branch asserting an edge its parent
/// already holds would *overwrite* the parent's row instead of adding its own.
pub const CREATE_LINKS_CURRENT_SYNC: &str = r#"
CREATE TRIGGER IF NOT EXISTS trg_links_current_sync
AFTER INSERT ON links
BEGIN
INSERT INTO links_current
(source_id, target_id, edge_type, valid_from, valid_to,
weight, properties, recorded_at, branch_id)
VALUES
(NEW.source_id, NEW.target_id, NEW.edge_type, NEW.valid_from,
NEW.valid_to, NEW.weight, NEW.properties, NEW.recorded_at,
NEW.branch_id)
ON CONFLICT(source_id, target_id, edge_type, valid_from, branch_id) DO UPDATE SET
valid_to = excluded.valid_to,
weight = excluded.weight,
properties = excluded.properties,
recorded_at = excluded.recorded_at
WHERE excluded.recorded_at > links_current.recorded_at;
END;
"#;
/// One open interval per edge **per lineage** (§4.3, branch-scoped at v12).
///
/// The `branch_id` clause is row-level and deliberately not ancestry-aware. A
/// branch that inherits an open interval from its parent and asserts its own is
/// not violating this rule — it is superseding a belief, which is the thing a
/// branch is for.
///
/// # The question this comment parked, answered at 0.14.8 (D-225)
///
/// *Whether the inherited interval should also close.* It should not, and
/// cannot: closing the ancestor's row is the parent corruption Doctrine III
/// forbids, and `links` is append-only so no statement in the crate could do
/// it. What a branch writes instead is its **own** row at the ancestor's key,
/// which the read prefers by `dist` — shadow retirement.
///
/// The half a trigger genuinely cannot answer went to the Rust layer, where
/// the ancestry is reachable: `lineage::overlap_candidates_resolved` refuses an
/// assertion whose interval overlaps **what the writing lineage can see**,
/// which is the read's definition applied to the write. That is a guard against
/// callers going through the actor and not against raw SQL, which is the same
/// honest cost `reject_overlapping_interval` has carried since D-060 — a
/// trigger able to make it would need a recursive ancestry walk on every
/// insert, on the path D-059 exists to keep fast.
pub const CREATE_LINKS_SINGLE_OPEN: &str = concat!;
/// The update half of the concepts log. See [`CREATE_CONCEPTS_LOG_INSERT`].
///
/// Unconditional where its insert sibling is marker-gated, and the asymmetry is
/// deliberate: nothing inside an archive session updates a concept, so gating
/// this would suppress nothing.
///
/// `branch_id` is in the column list since v12 and the omission would have been
/// expensive. `concepts` permits a **same-lineage** update — the guards refuse
/// cross-lineage inserts and `branch_id` changes, not this — so a branch
/// correcting a concept it minted would have logged the change against `'main'`,
/// putting a branch's own history in the trunk's fold and leaving the row
/// invisible to the abandonment sweep that §15.5's `archive` arm performs.
pub const CREATE_CONCEPTS_LOG_UPDATE: &str = r#"
CREATE TRIGGER IF NOT EXISTS trg_concepts_log_update
AFTER UPDATE ON concepts
BEGIN
INSERT INTO transaction_log (table_name, entity_id, operation, payload, recorded_at, branch_id)
VALUES ('concepts', NEW.id, 'U',
json_object('v', 2, 'title', NEW.title, 'content', NEW.content,
'valid_from', NEW.valid_from, 'valid_to', NEW.valid_to,
'retired', NEW.retired,
'embedding_model', NEW.embedding_model),
NEW.recorded_at, NEW.branch_id);
END;
"#;
/// The links log, and the entry whose `entity_id` is composed rather than copied.
///
/// `source|target|type|valid_from` identifies an edge assertion and carries **no
/// lineage**, which is why `branch_id` had to become a column of its own rather
/// than a fifth field in that string. Re-keying `entity_id` was the other
/// option and was rejected: it changes what a log entry identifies, so rows
/// written before the rung would no longer match rows written after it, and the
/// fold would silently split one edge's history in two.
///
/// With the column present, the four folds in `temporal::replay` — a private
/// module, so the name is plain text rather than a link that would not resolve —
/// partition by `(table_name, entity_id, branch_id)` and two lineages'
/// assertions about one edge stay two beliefs. Without it they collapse to
/// whichever has the higher `seq_id` — no error, no drift report, just one
/// lineage's belief gone.
pub const CREATE_LINKS_LOG_INSERT: &str = r#"
CREATE TRIGGER IF NOT EXISTS trg_links_log_insert
AFTER INSERT ON links
BEGIN
INSERT INTO transaction_log (table_name, entity_id, operation, payload, recorded_at, branch_id)
VALUES ('links',
NEW.source_id || '|' || NEW.target_id || '|' || NEW.edge_type || '|' || NEW.valid_from,
'I',
json_object('v', 1, 'source_id', NEW.source_id, 'target_id', NEW.target_id,
'edge_type', NEW.edge_type, 'valid_from', NEW.valid_from,
'valid_to', NEW.valid_to, 'weight', NEW.weight,
'properties', json(NEW.properties)),
NEW.recorded_at, NEW.branch_id);
END;
"#;
/// The ledger's delete guard, named since v15 (0.14.15, [D-232]).
///
/// An anonymous entry in [`CREATE_TRIGGERS`] until a rung needed to put it
/// back: the v14 → v15 rung rebuilds `links`, `DROP TABLE` takes its four
/// triggers with it, and a rung cannot re-issue a body it has no name for.
/// Promoted rather than copied, which is the rule
/// [`CREATE_LINKS_CURRENT_SYNC`] states — a rung with its own copy of a trigger
/// is a copy that drifts. The v12 rung promoted four bodies for exactly this
/// reason; this is the fifth.
///
/// D-008 (revised): probe `main.sqlite_master` for the archive-session marker.
/// SQLite forbids a trigger in `main` from referencing objects in another
/// database, temp included, so the original `temp.sqlite_master` probe fails at
/// `CREATE TRIGGER` time and is unimplementable.
pub const CREATE_LINKS_GUARD_DELETE: &str = concat!;
pub const CREATE_TRIGGERS: & = &;