@suss/datalog 0.37.1 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,43 +1,91 @@
1
1
  // src/factIndex.ts
2
- var emptyNode = () => ({
3
- present: false,
4
- tag: void 0,
5
- next: void 0
6
- });
2
+ var FactKey = class {
3
+ constructor(tuple) {
4
+ this.tuple = tuple;
5
+ }
6
+ tuple;
7
+ present = false;
8
+ tag = void 0;
9
+ };
10
+ var Branch = class {
11
+ /** The key of a tuple that ends at this column, as a shorter one in a relation of mixed arity does. */
12
+ here = void 0;
13
+ next = /* @__PURE__ */ new Map();
14
+ };
15
+ function sameFrom(a, b, from) {
16
+ if (a.length !== b.length) {
17
+ return false;
18
+ }
19
+ for (let at = from; at < a.length; at++) {
20
+ if (a[at] !== b[at]) {
21
+ return false;
22
+ }
23
+ }
24
+ return true;
25
+ }
26
+ function place(branch, key, depth) {
27
+ if (depth === key.tuple.length) {
28
+ branch.here = key;
29
+ } else {
30
+ branch.next.set(key.tuple[depth], key);
31
+ }
32
+ }
33
+ function soleKey(branch) {
34
+ if (branch.here !== void 0) {
35
+ return branch.next.size === 0 ? branch.here : void 0;
36
+ }
37
+ if (branch.next.size !== 1) {
38
+ return void 0;
39
+ }
40
+ const only = branch.next.values().next().value;
41
+ return only instanceof FactKey ? only : void 0;
42
+ }
7
43
  var FactIndex = class {
8
44
  /** How many facts are present. */
9
45
  size = 0;
10
- root = emptyNode();
46
+ root = new Branch();
11
47
  /** This tuple's key here, whether or not the fact is present. */
12
48
  key(tuple) {
13
- let node = this.root;
14
- for (let i = 0; i < tuple.length; i++) {
15
- let next = node.next;
16
- if (next === void 0) {
17
- next = /* @__PURE__ */ new Map();
18
- node.next = next;
49
+ let branch = this.root;
50
+ for (let depth = 0; ; depth++) {
51
+ if (depth === tuple.length) {
52
+ branch.here ??= new FactKey(tuple);
53
+ return branch.here;
19
54
  }
20
- const atom = tuple[i];
21
- let child = next.get(atom);
55
+ const atom = tuple[depth];
56
+ const child = branch.next.get(atom);
22
57
  if (child === void 0) {
23
- child = emptyNode();
24
- next.set(atom, child);
58
+ const key = new FactKey(tuple);
59
+ branch.next.set(atom, key);
60
+ return key;
25
61
  }
26
- node = child;
62
+ if (child instanceof Branch) {
63
+ branch = child;
64
+ continue;
65
+ }
66
+ if (sameFrom(child.tuple, tuple, depth + 1)) {
67
+ return child;
68
+ }
69
+ const split = new Branch();
70
+ place(split, child, depth + 1);
71
+ branch.next.set(atom, split);
72
+ branch = split;
27
73
  }
28
- return node;
29
74
  }
30
75
  /** The key of a fact this relation has, or undefined when it has none. */
31
76
  find(tuple) {
32
- let node = this.root;
33
- for (let i = 0; i < tuple.length; i++) {
34
- const child = node.next?.get(tuple[i]);
77
+ let branch = this.root;
78
+ for (let depth = 0; ; depth++) {
79
+ const child = depth === tuple.length ? branch.here : branch.next.get(tuple[depth]);
35
80
  if (child === void 0) {
36
81
  return void 0;
37
82
  }
38
- node = child;
83
+ if (child instanceof Branch) {
84
+ branch = child;
85
+ continue;
86
+ }
87
+ return child.present && sameFrom(child.tuple, tuple, depth + 1) ? child : void 0;
39
88
  }
40
- return node.present ? node : void 0;
41
89
  }
42
90
  has(key) {
43
91
  return key.present;
@@ -61,32 +109,45 @@ var FactIndex = class {
61
109
  this.size -= 1;
62
110
  }
63
111
  /**
64
- * Drop what is left of a removed tuple's path, up to the first node
65
- * another fact still needs. Without this a relation that is retracted
66
- * from over and over keeps a node per tuple it ever had.
112
+ * Drop a removed tuple's key, and any branch left with one key to
113
+ * tell apart. Without this a relation that is retracted from over and
114
+ * over keeps a key per tuple it ever had.
67
115
  */
68
116
  prune(tuple) {
69
- const path = [this.root];
70
- let node = this.root;
71
- for (let i = 0; i < tuple.length; i++) {
72
- const child = node.next?.get(tuple[i]);
117
+ const path = [];
118
+ let branch = this.root;
119
+ let depth = 0;
120
+ for (; ; depth++) {
121
+ path.push(branch);
122
+ const child = depth === tuple.length ? branch.here : branch.next.get(tuple[depth]);
73
123
  if (child === void 0) {
74
124
  return;
75
125
  }
76
- node = child;
77
- path.push(child);
126
+ if (child instanceof Branch) {
127
+ branch = child;
128
+ continue;
129
+ }
130
+ if (child.present || !sameFrom(child.tuple, tuple, depth + 1)) {
131
+ return;
132
+ }
133
+ break;
134
+ }
135
+ if (depth === tuple.length) {
136
+ branch.here = void 0;
137
+ } else {
138
+ branch.next.delete(tuple[depth]);
78
139
  }
79
- for (let i = path.length - 1; i > 0; i--) {
80
- const child = path[i];
81
- if (child.present || (child.next?.size ?? 0) > 0) {
140
+ for (let at = path.length - 1; at > 0; at--) {
141
+ const sole = soleKey(path[at]);
142
+ if (sole === void 0) {
82
143
  return;
83
144
  }
84
- path[i - 1].next?.delete(tuple[i - 1]);
145
+ path[at - 1].next.set(tuple[at - 1], sole);
85
146
  }
86
147
  }
87
148
  /** Drop every fact, and whatever was kept in order to find them. */
88
149
  clear() {
89
- this.root = emptyNode();
150
+ this.root = new Branch();
90
151
  this.size = 0;
91
152
  }
92
153
  };
@@ -523,6 +584,24 @@ function formatProfile(profile) {
523
584
  }
524
585
 
525
586
  // src/relation.ts
587
+ var isLone = (bucket) => typeof bucket[0] !== "object";
588
+ var NO_TUPLES = [];
589
+ function tuplesIn(bucket) {
590
+ if (bucket === void 0) {
591
+ return NO_TUPLES;
592
+ }
593
+ return isLone(bucket) ? [bucket] : bucket;
594
+ }
595
+ function fileUnder(buckets, key, tuple) {
596
+ const bucket = buckets.get(key);
597
+ if (bucket === void 0) {
598
+ buckets.set(key, tuple);
599
+ } else if (isLone(bucket)) {
600
+ buckets.set(key, [bucket, tuple]);
601
+ } else {
602
+ bucket.push(tuple);
603
+ }
604
+ }
526
605
  var sameTuple = (a, b) => a.length === b.length && a.every((atom, at) => atom === b[at]);
527
606
  function storedTuplesOf(relation, facts) {
528
607
  const column = relation.columns.findIndex((buckets2) => buckets2 !== void 0);
@@ -531,19 +610,24 @@ function storedTuplesOf(relation, facts) {
531
610
  return null;
532
611
  }
533
612
  const stored = facts.map(
534
- (fact) => buckets.get(fact[column])?.find((tuple) => sameTuple(tuple, fact))
613
+ (fact) => tuplesIn(buckets.get(fact[column])).find(
614
+ (tuple) => sameTuple(tuple, fact)
615
+ )
535
616
  );
536
617
  return stored.every((tuple) => tuple !== void 0) ? stored : null;
537
618
  }
538
619
  function removeFrom(buckets, key, tuple) {
539
620
  const bucket = buckets.get(key);
540
- const at = bucket?.indexOf(tuple) ?? -1;
541
- if (bucket === void 0 || at === -1) {
621
+ if (bucket === void 0) {
542
622
  return;
543
623
  }
544
- bucket.splice(at, 1);
545
- if (bucket.length === 0) {
624
+ if (isLone(bucket)) {
546
625
  buckets.delete(key);
626
+ return;
627
+ }
628
+ bucket.splice(bucket.indexOf(tuple), 1);
629
+ if (bucket.length === 1) {
630
+ buckets.set(key, bucket[0]);
547
631
  }
548
632
  }
549
633
  function removeTuples(relation, stored) {
@@ -595,7 +679,7 @@ function addToCombined(index, tuple) {
595
679
  }
596
680
  level = next;
597
681
  }
598
- addToBucket(
682
+ fileUnder(
599
683
  level,
600
684
  tuple[index.columns[last]],
601
685
  tuple
@@ -614,10 +698,10 @@ function combinedBucketIn(relation, mask, values) {
614
698
  for (const value of values) {
615
699
  level = level.get(value);
616
700
  if (level === void 0) {
617
- return [];
701
+ return NO_TUPLES;
618
702
  }
619
703
  }
620
- return level;
704
+ return tuplesIn(level);
621
705
  }
622
706
  function addToBucket(buckets, key, tuple) {
623
707
  const bucket = buckets.get(key);
@@ -627,19 +711,291 @@ function addToBucket(buckets, key, tuple) {
627
711
  bucket.push(tuple);
628
712
  }
629
713
  }
630
- function bucketIn(relation, column, value) {
714
+ function columnIndex(relation, column) {
631
715
  let buckets = relation.columns[column];
632
716
  if (buckets === void 0) {
633
717
  buckets = /* @__PURE__ */ new Map();
634
718
  for (const tuple of relation.tuples) {
635
719
  const at = tuple[column];
636
720
  if (at !== void 0) {
637
- addToBucket(buckets, at, tuple);
721
+ fileUnder(buckets, at, tuple);
638
722
  }
639
723
  }
640
724
  relation.columns[column] = buckets;
641
725
  }
642
- return buckets.get(value) ?? [];
726
+ return buckets;
727
+ }
728
+ function bucketIn(relation, column, value) {
729
+ return tuplesIn(columnIndex(relation, column).get(value));
730
+ }
731
+ function bucketSize(relation, column, value) {
732
+ const bucket = columnIndex(relation, column).get(value);
733
+ if (bucket === void 0) {
734
+ return 0;
735
+ }
736
+ return isLone(bucket) ? 1 : bucket.length;
737
+ }
738
+
739
+ // src/reads.ts
740
+ function noteIn(reads, column, value) {
741
+ if (reads.whole) {
742
+ return;
743
+ }
744
+ let values = reads.columns[column];
745
+ if (values === void 0) {
746
+ values = /* @__PURE__ */ new Set();
747
+ reads.columns[column] = values;
748
+ }
749
+ values.add(value);
750
+ }
751
+ var ReadLog = class {
752
+ constructor(skipped) {
753
+ this.skipped = skipped;
754
+ }
755
+ skipped;
756
+ /** Null for a relation the caller said to leave out. */
757
+ byRelation = /* @__PURE__ */ new Map();
758
+ lookedUp(relation, column, value) {
759
+ const reads = this.readsOf(relation);
760
+ if (reads !== null) {
761
+ noteIn(reads, column, value);
762
+ }
763
+ }
764
+ readWhole(relation) {
765
+ const reads = this.readsOf(relation);
766
+ if (reads !== null) {
767
+ reads.whole = true;
768
+ }
769
+ }
770
+ /** Note everything an inner log noted, for an answer cached inside another. */
771
+ absorb(inner) {
772
+ for (const [relation, reads] of inner.byRelation) {
773
+ if (reads === null) {
774
+ continue;
775
+ }
776
+ if (reads.whole) {
777
+ this.readWhole(relation);
778
+ continue;
779
+ }
780
+ reads.columns.forEach((values, column) => {
781
+ for (const value of values ?? []) {
782
+ this.lookedUp(relation, column, value);
783
+ }
784
+ });
785
+ }
786
+ }
787
+ /**
788
+ * The reads with what each one finds in `db` now. One question looks
789
+ * up the same values in many columns, so each value is kept once, and
790
+ * each column keeps a bit per value saying whether it was looked up
791
+ * there.
792
+ */
793
+ counted(db) {
794
+ const names = [];
795
+ const groups = [];
796
+ const columns = [];
797
+ const lookedUp = [];
798
+ const positions = /* @__PURE__ */ new Map();
799
+ const values = [];
800
+ const addGroup = (column, seen) => {
801
+ groups.push(names.length - 1);
802
+ columns.push(column);
803
+ lookedUp.push(seen);
804
+ for (const value of seen) {
805
+ if (!positions.has(value)) {
806
+ positions.set(value, values.length);
807
+ values.push(value);
808
+ }
809
+ }
810
+ };
811
+ for (const [relation, reads] of this.byRelation) {
812
+ if (reads === null) {
813
+ continue;
814
+ }
815
+ names.push(relation);
816
+ if (reads.whole) {
817
+ addGroup(WHOLE, NONE);
818
+ continue;
819
+ }
820
+ reads.columns.forEach((seen, column) => {
821
+ if (seen !== void 0) {
822
+ addGroup(column, seen);
823
+ }
824
+ });
825
+ }
826
+ const words = Math.ceil(values.length / 32);
827
+ const bits = new Uint32Array(groups.length * words);
828
+ let found = 0;
829
+ lookedUp.forEach((seen, group) => {
830
+ const relation = db.relationOf(names[groups[group]]);
831
+ const column = columns[group];
832
+ if (column === WHOLE) {
833
+ found += relation?.tuples.length ?? 0;
834
+ }
835
+ for (const value of seen) {
836
+ const at = positions.get(value);
837
+ bits[group * words + (at >>> 5)] |= 1 << (at & 31);
838
+ found += relation === void 0 ? 0 : bucketSize(relation, column, value);
839
+ }
840
+ });
841
+ return new ReadsNoted(
842
+ db,
843
+ this.skipped,
844
+ { names, groups, columns, values, bits },
845
+ found
846
+ );
847
+ }
848
+ /** @internal Null for a relation the caller said to leave out. */
849
+ readsOf(relation) {
850
+ let reads = this.byRelation.get(relation);
851
+ if (reads === void 0) {
852
+ reads = this.skipped.has(relation) ? null : { whole: false, columns: [] };
853
+ this.byRelation.set(relation, reads);
854
+ }
855
+ return reads;
856
+ }
857
+ };
858
+ var WHOLE = -1;
859
+ var NONE = /* @__PURE__ */ new Set();
860
+ var ReadsNoted = class {
861
+ constructor(db, skipped, lookups, found) {
862
+ this.skipped = skipped;
863
+ this.found = found;
864
+ this.names = lookups.names;
865
+ this.groups = lookups.groups;
866
+ this.columns = lookups.columns;
867
+ this.values = lookups.values;
868
+ this.bits = lookups.bits;
869
+ this.words = Math.ceil(lookups.values.length / 32);
870
+ this.relations = lookups.names.map((name) => db.relationOf(name));
871
+ this.removed = this.sumOver(db, removedFrom);
872
+ this.facts = this.sumOver(db, factsIn);
873
+ }
874
+ skipped;
875
+ found;
876
+ names;
877
+ groups;
878
+ columns;
879
+ values;
880
+ bits;
881
+ words;
882
+ removed;
883
+ /** The facts in every relation read, when the counts last matched. */
884
+ facts;
885
+ /** Each relation read, found by name the first time a check needs it. */
886
+ relations;
887
+ /**
888
+ * Whether every read would find what it found then. A fact left in a
889
+ * skipped relation would join a question asked now without anything
890
+ * here having looked it up, so one there makes the answer no. The
891
+ * counts are taken again only when a relation read has grown.
892
+ */
893
+ unchangedIn(db) {
894
+ if (!allEmpty(db, this.skipped)) {
895
+ return false;
896
+ }
897
+ if (this.sumOver(db, removedFrom) !== this.removed) {
898
+ return false;
899
+ }
900
+ const facts = this.sumOver(db, factsIn);
901
+ if (facts === this.facts) {
902
+ return true;
903
+ }
904
+ if (this.countIn(db, this.found) !== this.found) {
905
+ return false;
906
+ }
907
+ this.facts = facts;
908
+ return true;
909
+ }
910
+ relationAt(db, at) {
911
+ let relation = this.relations[at];
912
+ if (relation === void 0) {
913
+ relation = db.relationOf(this.names[at]);
914
+ this.relations[at] = relation;
915
+ }
916
+ return relation;
917
+ }
918
+ sumOver(db, measure) {
919
+ let sum = 0;
920
+ for (let at = 0; at < this.names.length; at++) {
921
+ const relation = this.relationAt(db, at);
922
+ sum += relation === void 0 ? 0 : measure(relation);
923
+ }
924
+ return sum;
925
+ }
926
+ /**
927
+ * The facts the reads find, stopping once past `limit`. With nothing
928
+ * retracted every count only grows, so passing the old total already
929
+ * says that one of them grew.
930
+ */
931
+ countIn(db, limit) {
932
+ let found = 0;
933
+ for (let group = 0; group < this.groups.length && found <= limit; group++) {
934
+ const relation = this.relationAt(db, this.groups[group]);
935
+ const column = this.columns[group];
936
+ if (column === WHOLE) {
937
+ found += relation?.tuples.length ?? 0;
938
+ } else if (relation !== void 0) {
939
+ found += this.countGroup(relation, group, column);
940
+ }
941
+ }
942
+ return found;
943
+ }
944
+ countGroup(relation, group, column) {
945
+ let found = 0;
946
+ const first = group * this.words;
947
+ for (let word = 0; word < this.words; word++) {
948
+ let left = this.bits[first + word];
949
+ while (left !== 0) {
950
+ const bit = 31 - Math.clz32(left & -left);
951
+ left &= left - 1;
952
+ const value = this.values[word * 32 + bit];
953
+ found += bucketSize(relation, column, value);
954
+ }
955
+ }
956
+ return found;
957
+ }
958
+ };
959
+ var removedFrom = (relation) => relation.removed;
960
+ var factsIn = (relation) => relation.tuples.length;
961
+ var skippedIn = /* @__PURE__ */ new WeakMap();
962
+ function allEmpty(db, skipped) {
963
+ let bySet = skippedIn.get(db);
964
+ if (bySet === void 0) {
965
+ bySet = /* @__PURE__ */ new WeakMap();
966
+ skippedIn.set(db, bySet);
967
+ }
968
+ let known = bySet.get(skipped);
969
+ if (known === void 0) {
970
+ known = { found: [], missing: [...skipped] };
971
+ bySet.set(skipped, known);
972
+ }
973
+ if (known.missing.length > 0) {
974
+ const stillMissing = [];
975
+ for (const name of known.missing) {
976
+ const relation = db.relationOf(name);
977
+ if (relation === void 0) {
978
+ stillMissing.push(name);
979
+ } else {
980
+ known.found.push(relation);
981
+ }
982
+ }
983
+ known.missing = stillMissing;
984
+ }
985
+ return known.found.every((relation) => relation.tuples.length === 0);
986
+ }
987
+ function noteReads(db, skipped, work) {
988
+ const outer = db.readLog;
989
+ const log = new ReadLog(skipped);
990
+ const emptyAtStart = allEmpty(db, skipped);
991
+ db.readLog = log;
992
+ try {
993
+ const value = work();
994
+ return { value, reads: emptyAtStart ? log.counted(db) : null };
995
+ } finally {
996
+ db.readLog = outer;
997
+ outer?.absorb(log);
998
+ }
643
999
  }
644
1000
 
645
1001
  // src/stratum.ts
@@ -665,7 +1021,7 @@ function planStratum(rules) {
665
1021
  }
666
1022
  return { rules, derived, readers, marked: new Uint8Array(rules.length) };
667
1023
  }
668
- var NONE = [];
1024
+ var NONE2 = [];
669
1025
  function rulesReading(stratum, seed, derivedOnly) {
670
1026
  const lists = [];
671
1027
  for (const [relation, tuples] of seed) {
@@ -681,7 +1037,7 @@ function rulesReading(stratum, seed, derivedOnly) {
681
1037
  }
682
1038
  }
683
1039
  if (lists.length === 0) {
684
- return NONE;
1040
+ return NONE2;
685
1041
  }
686
1042
  if (lists.length === 1) {
687
1043
  return lists[0];
@@ -851,6 +1207,8 @@ var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName
851
1207
  var FEW_REMOVED = 16;
852
1208
  var Database = class {
853
1209
  store = /* @__PURE__ */ new Map();
1210
+ /** @internal Where reads are noted while `noteReads` runs. */
1211
+ readLog = null;
854
1212
  relation(name) {
855
1213
  const existing = this.store.get(name);
856
1214
  if (existing !== void 0) {
@@ -860,7 +1218,8 @@ var Database = class {
860
1218
  index: new FactIndex(),
861
1219
  tuples: [],
862
1220
  columns: [],
863
- combined: /* @__PURE__ */ new Map()
1221
+ combined: /* @__PURE__ */ new Map(),
1222
+ removed: 0
864
1223
  };
865
1224
  this.store.set(name, created);
866
1225
  return created;
@@ -905,7 +1264,7 @@ var Database = class {
905
1264
  const buckets = relation.columns[column];
906
1265
  const value = tuple[column];
907
1266
  if (buckets !== void 0 && value !== void 0) {
908
- addToBucket(buckets, value, tuple);
1267
+ fileUnder(buckets, value, tuple);
909
1268
  }
910
1269
  }
911
1270
  for (const combined of relation.combined.values()) {
@@ -915,6 +1274,7 @@ var Database = class {
915
1274
  }
916
1275
  /** The tag stored for this fact, or undefined when there is none. */
917
1276
  tagOf(relationName, tuple) {
1277
+ this.noteTupleRead(relationName, tuple);
918
1278
  const relation = this.store.get(relationName);
919
1279
  if (relation === void 0) {
920
1280
  return void 0;
@@ -923,9 +1283,20 @@ var Database = class {
923
1283
  return key === void 0 ? void 0 : relation.index.tagOf(key);
924
1284
  }
925
1285
  has(relationName, tuple) {
1286
+ this.noteTupleRead(relationName, tuple);
926
1287
  return this.store.get(relationName)?.index.find(tuple) !== void 0;
927
1288
  }
1289
+ /** A fact matching the whole tuple agrees on its first column, so that lookup is enough to note. */
1290
+ noteTupleRead(relationName, tuple) {
1291
+ const first = tuple[0];
1292
+ if (first === void 0) {
1293
+ this.readLog?.readWhole(relationName);
1294
+ return;
1295
+ }
1296
+ this.readLog?.lookedUp(relationName, 0, first);
1297
+ }
928
1298
  facts(relationName) {
1299
+ this.readLog?.readWhole(relationName);
929
1300
  return this.store.get(relationName)?.tuples ?? [];
930
1301
  }
931
1302
  /**
@@ -938,6 +1309,7 @@ var Database = class {
938
1309
  * gets an index.
939
1310
  */
940
1311
  lookup(relationName, column, value) {
1312
+ this.readLog?.lookedUp(relationName, column, value);
941
1313
  const relation = this.store.get(relationName);
942
1314
  return relation === void 0 ? [] : bucketIn(relation, column, value);
943
1315
  }
@@ -973,6 +1345,7 @@ var Database = class {
973
1345
  for (const key of going) {
974
1346
  index.remove(key);
975
1347
  }
1348
+ relation.removed += going.size;
976
1349
  if (index.size === 0) {
977
1350
  relation.tuples = [];
978
1351
  index.clear();
@@ -1008,6 +1381,7 @@ var Database = class {
1008
1381
  return 0;
1009
1382
  }
1010
1383
  const removed = relation.tuples.length;
1384
+ relation.removed += removed;
1011
1385
  relation.tuples = [];
1012
1386
  relation.index.clear();
1013
1387
  relation.columns.length = 0;
@@ -1016,12 +1390,15 @@ var Database = class {
1016
1390
  return removed;
1017
1391
  }
1018
1392
  size(relationName) {
1019
- return this.store.get(relationName)?.tuples.length ?? 0;
1393
+ this.readLog?.readWhole(relationName);
1394
+ return sizeOf(this, relationName);
1020
1395
  }
1021
1396
  relationNames() {
1022
1397
  return [...this.store.keys()];
1023
1398
  }
1024
1399
  };
1400
+ var tuplesOf = (db, relation) => db.relationOf(relation)?.tuples ?? [];
1401
+ var sizeOf = (db, relation) => db.relationOf(relation)?.tuples.length ?? 0;
1025
1402
  function rulesDeriving(rules, relations) {
1026
1403
  const wanted = new Set(relations);
1027
1404
  const pending = [...relations];
@@ -1217,6 +1594,7 @@ function evaluateRule(db, deltas, r, deltaIndex, budget) {
1217
1594
  }
1218
1595
  const byValue = newFactsByColumn(rows, start.column, budget);
1219
1596
  const first = body[start.index];
1597
+ noteSharedLookups(db, first, start, rows);
1220
1598
  for (const tuple of start.source) {
1221
1599
  budget.examined++;
1222
1600
  if (budget.examined > budget.limit) {
@@ -1248,7 +1626,7 @@ function smallerStart(db, body, deltaAt, rows) {
1248
1626
  if (column === -1) {
1249
1627
  continue;
1250
1628
  }
1251
- const source = narrowedSource(db, literal, null) ?? db.facts(literal.relation);
1629
+ const source = narrowedSource(db, literal, null) ?? tuplesOf(db, literal.relation);
1252
1630
  const worthIt = indexed?.has(column) ? source.length < rows.length : source.length * 2 < rows.length;
1253
1631
  if (worthIt && (best === null || source.length < best.source.length)) {
1254
1632
  best = {
@@ -1306,6 +1684,7 @@ function nextLiteral(db, body, taken, bindings) {
1306
1684
  continue;
1307
1685
  }
1308
1686
  if (source.length === 0) {
1687
+ noteLookup(db, literal, bindings);
1309
1688
  return -1;
1310
1689
  }
1311
1690
  if (narrowest === null || source.length < narrowest.length) {
@@ -1314,6 +1693,7 @@ function nextLiteral(db, body, taken, bindings) {
1314
1693
  }
1315
1694
  }
1316
1695
  if (narrowest !== null) {
1696
+ noteLookup(db, body[pick], bindings);
1317
1697
  nextSource = narrowedFurther(db, body[pick], bindings, narrowest);
1318
1698
  return nextSource.length === 0 ? -1 : pick;
1319
1699
  }
@@ -1325,6 +1705,59 @@ function nextLiteral(db, body, taken, bindings) {
1325
1705
  }
1326
1706
  return body.findIndex((_, index) => !(taken & 1 << index));
1327
1707
  }
1708
+ function noteLookup(db, literal, bindings) {
1709
+ const reads = db.readLog?.readsOf(literal.relation) ?? null;
1710
+ if (reads === null) {
1711
+ return;
1712
+ }
1713
+ for (let column2 = 0; column2 < literal.terms.length; column2++) {
1714
+ const term2 = literal.terms[column2];
1715
+ if (term2.type === "variable") {
1716
+ const value = boundValue(bindings, term2.name);
1717
+ if (value !== void 0) {
1718
+ noteIn(reads, column2, value);
1719
+ return;
1720
+ }
1721
+ }
1722
+ }
1723
+ const column = fewestFactsUnderConstant(db, literal);
1724
+ const term = literal.terms[column];
1725
+ if (term?.type === "constant") {
1726
+ noteIn(reads, column, term.value);
1727
+ }
1728
+ }
1729
+ function fewestFactsUnderConstant(db, literal) {
1730
+ const relation = db.relationOf(literal.relation);
1731
+ let best = -1;
1732
+ let bestFacts = Number.POSITIVE_INFINITY;
1733
+ for (let column = 0; column < literal.terms.length; column++) {
1734
+ const term = literal.terms[column];
1735
+ if (term.type !== "constant") {
1736
+ continue;
1737
+ }
1738
+ const facts = relation === void 0 ? 0 : bucketSize(relation, column, term.value);
1739
+ if (facts < bestFacts) {
1740
+ best = column;
1741
+ bestFacts = facts;
1742
+ }
1743
+ }
1744
+ return best;
1745
+ }
1746
+ function noteSharedLookups(db, first, start, rows) {
1747
+ const reads = db.readLog?.readsOf(first.relation) ?? null;
1748
+ if (reads === null) {
1749
+ return;
1750
+ }
1751
+ const column = first.terms.findIndex(
1752
+ (term) => term.type === "variable" && term.name === start.variable
1753
+ );
1754
+ for (const row of rows) {
1755
+ const value = row[start.column];
1756
+ if (value !== void 0) {
1757
+ noteIn(reads, column, value);
1758
+ }
1759
+ }
1760
+ }
1328
1761
  function narrowedSource(db, literal, bindings) {
1329
1762
  const relation = db.relationOf(literal.relation);
1330
1763
  let narrowest = null;
@@ -1376,7 +1809,11 @@ function constantColumnsOf(literal) {
1376
1809
  values.push(term.value);
1377
1810
  }
1378
1811
  });
1379
- known = columns.length === 0 ? null : { columns, values };
1812
+ known = columns.length === 0 ? null : {
1813
+ columns,
1814
+ values,
1815
+ bucketKey: deltaBucketKey(literal.relation, columns[0])
1816
+ };
1380
1817
  constantColumns.set(literal, known);
1381
1818
  }
1382
1819
  return known;
@@ -1389,7 +1826,7 @@ function hasNoNewRows(deltas, literal) {
1389
1826
  if (rows.length === 0 || constants === null) {
1390
1827
  return rows.length === 0;
1391
1828
  }
1392
- const buckets = deltaBuckets.get(deltas)?.get(deltaBucketKey(literal.relation, constants.columns[0]));
1829
+ const buckets = deltaBuckets.get(deltas)?.get(constants.bucketKey);
1393
1830
  return buckets !== void 0 && !buckets.has(constants.values[0]);
1394
1831
  }
1395
1832
  function deltaRows(deltas, literal, budget) {
@@ -1404,7 +1841,7 @@ function deltaRows(deltas, literal, budget) {
1404
1841
  deltaBuckets.set(deltas, byColumn);
1405
1842
  }
1406
1843
  const column = constants.columns[0];
1407
- const key = deltaBucketKey(literal.relation, column);
1844
+ const key = constants.bucketKey;
1408
1845
  let buckets = byColumn.get(key);
1409
1846
  if (buckets === void 0) {
1410
1847
  buckets = /* @__PURE__ */ new Map();
@@ -1549,7 +1986,7 @@ function canResume(rules, state) {
1549
1986
  function factsSince(db, marks) {
1550
1987
  const added = /* @__PURE__ */ new Map();
1551
1988
  for (const name of db.relationNames()) {
1552
- const all = db.facts(name);
1989
+ const all = tuplesOf(db, name);
1553
1990
  const mark = marks.get(name) ?? 0;
1554
1991
  if (all.length > mark) {
1555
1992
  added.set(name, all.slice(mark));
@@ -1560,7 +1997,7 @@ function factsSince(db, marks) {
1560
1997
  function currentMarks(db) {
1561
1998
  const marks = /* @__PURE__ */ new Map();
1562
1999
  for (const name of db.relationNames()) {
1563
- marks.set(name, db.size(name));
2000
+ marks.set(name, sizeOf(db, name));
1564
2001
  }
1565
2002
  return marks;
1566
2003
  }
@@ -1622,9 +2059,14 @@ function shapeOf(rules) {
1622
2059
  return shape;
1623
2060
  }
1624
2061
  function couldProduce(db, rule2) {
1625
- return rule2.body.every(
1626
- (literal) => literal.negated || db.size(literal.relation) > 0
2062
+ const empty = rule2.body.find(
2063
+ (literal) => !literal.negated && sizeOf(db, literal.relation) === 0
1627
2064
  );
2065
+ if (empty === void 0) {
2066
+ return true;
2067
+ }
2068
+ db.readLog?.readWhole(empty.relation);
2069
+ return false;
1628
2070
  }
1629
2071
  function storedMergeFor(algebra) {
1630
2072
  return (stored, incoming) => {
@@ -1673,6 +2115,17 @@ function runRules(db, rules, algebra, given) {
1673
2115
  };
1674
2116
  const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
1675
2117
  const marks = canResume(rules, state) ? state.marks : void 0;
2118
+ const producible = /* @__PURE__ */ new Set();
2119
+ const canProduce = (r) => {
2120
+ if (producible.has(r)) {
2121
+ return true;
2122
+ }
2123
+ if (!couldProduce(db, r)) {
2124
+ return false;
2125
+ }
2126
+ producible.add(r);
2127
+ return true;
2128
+ };
1676
2129
  const runStratum = (stratum) => {
1677
2130
  let delta = /* @__PURE__ */ new Map();
1678
2131
  const record = (relation, tuple) => {
@@ -1715,7 +2168,7 @@ function runRules(db, rules, algebra, given) {
1715
2168
  return;
1716
2169
  }
1717
2170
  const startedAt2 = performance.now();
1718
- const before = db.size(r.head.relation);
2171
+ const before = sizeOf(db, r.head.relation);
1719
2172
  const readBefore = budget.examined;
1720
2173
  deriveInto(r, seed, deltaIndex);
1721
2174
  chargeRule(
@@ -1723,14 +2176,14 @@ function runRules(db, rules, algebra, given) {
1723
2176
  r.head.relation,
1724
2177
  r.body.map(literalName),
1725
2178
  performance.now() - startedAt2,
1726
- db.size(r.head.relation) - before,
2179
+ sizeOf(db, r.head.relation) - before,
1727
2180
  budget.examined - readBefore
1728
2181
  );
1729
2182
  };
1730
2183
  const applyDelta = (seed, derivedOnly) => {
1731
2184
  for (const at of rulesReading(stratum, seed, derivedOnly)) {
1732
2185
  const r = stratum.rules[at];
1733
- if (!couldProduce(db, r)) {
2186
+ if (!canProduce(r)) {
1734
2187
  continue;
1735
2188
  }
1736
2189
  const positives = positivesOf(r);
@@ -1748,13 +2201,13 @@ function runRules(db, rules, algebra, given) {
1748
2201
  };
1749
2202
  if (marks === void 0 || marks === null) {
1750
2203
  for (const r of stratum.rules) {
1751
- if (!couldProduce(db, r)) {
2204
+ if (!canProduce(r)) {
1752
2205
  continue;
1753
2206
  }
1754
2207
  const all = /* @__PURE__ */ new Map();
1755
2208
  for (const l of r.body) {
1756
2209
  if (!l.negated) {
1757
- all.set(l.relation, db.facts(l.relation));
2210
+ all.set(l.relation, tuplesOf(db, l.relation));
1758
2211
  }
1759
2212
  }
1760
2213
  runOneRule(r, all, -1);
@@ -1791,7 +2244,7 @@ function runRules(db, rules, algebra, given) {
1791
2244
  state.marks = currentMarks(db);
1792
2245
  if (isProfiling()) {
1793
2246
  chargeRelationSizes(
1794
- db.relationNames().map((name) => [name, db.size(name)]),
2247
+ db.relationNames().map((name) => [name, sizeOf(db, name)]),
1795
2248
  derivedRelations
1796
2249
  );
1797
2250
  }
@@ -1827,6 +2280,7 @@ export {
1827
2280
  formatProfile,
1828
2281
  lit,
1829
2282
  notLit,
2283
+ noteReads,
1830
2284
  profileEvaluation,
1831
2285
  profileEvaluationAsync,
1832
2286
  proofOf,