query: memoize resolved index scans per query (join speedup)

ober

79fc97eaed3bdbd2a4e9d3c8b23e0d56843d4a45

diff --git a/lib/jerboa-db/query/engine.ss b/lib/jerboa-db/query/engine.ss
index 9849bc8..3fbec9d 100644
--- a/lib/jerboa-db/query/engine.ss
+++ b/lib/jerboa-db/query/engine.ss
@@ -183,6 +183,14 @@
            [b4 (and b3 (unify op-spec (datom-added? d) b3))])
       b4))
 
+  ;; Per-query memo for resolved index scans: maps (db index e a v tx) -> the
+  ;; current datoms for that bound-component lookup, so a nested-loop join that
+  ;; re-probes the same key across many outer bindings (e.g. track/duration of a
+  ;; track that recurs in a shared-artist join) pays the index scan + current-
+  ;; resolution once instead of per binding. Bound fresh per query-db call;
+  ;; read-only against an immutable db-value, so results are identical.
+  (def current-query-cache (make-parameter #f))
+
   (def (evaluate-data-pattern db pattern bindings schema)
     ;; pattern: (e-spec a-spec v-spec) or (e-spec a-spec v-spec tx-spec)
     ;; or (e-spec a-spec v-spec tx-spec op-spec)
@@ -230,16 +238,18 @@
                    [(vaet) (make-datom (greatest-fixnum) aid
                              (if v-val v-val +max-val+)
                              (greatest-fixnum) #t)])]
-             [raw-datoms (dbi-range idx lo hi)]
-             ;; Apply db-level filters (as-of, since, history)
-             [datoms (filter (lambda (d) (db-filter-datom? db d)) raw-datoms)])
-        ;; For non-history queries, resolve to current state:
-        ;; Group by (e, a, v), keep only those whose highest-tx datom is added?=#t.
-        ;; In history mode, return all datoms.
+             [cache (current-query-cache)]
+             [ckey  (and cache (list db index-name e-val aid v-val tx-val))])
+        ;; Resolve to current datoms for this lookup (memoized per query).
         (let ([effective-datoms
-                (if (db-value-history? db)
-                    datoms
-                    (resolve-current-datoms datoms))])
+                (or (and cache (hashtable-ref cache ckey #f))
+                    (let* ([raw-datoms (dbi-range idx lo hi)]
+                           [datoms (filter (lambda (d) (db-filter-datom? db d)) raw-datoms)]
+                           [ed (if (db-value-history? db)
+                                   datoms
+                                   (resolve-current-datoms datoms))])
+                      (when cache (hashtable-set! cache ckey ed))
+                      ed))])
           ;; Produce new bindings for each matching datom
           (let loop ([ds effective-datoms] [results '()])
             (if (null? ds)
@@ -1297,6 +1307,7 @@
   ;; ---- Top-level query function ----
 
   (def (query-db parsed db . inputs)
+    (parameterize ([current-query-cache (make-hashtable equal-hash equal?)])
     (let ([find-vars (parsed-query-find-vars parsed)]
           [in-vars (parsed-query-in-vars parsed)]
           [where-clauses (parsed-query-where-clauses parsed)]
@@ -1316,7 +1327,7 @@
         ;; :limit on a shape we don't stream — correct via truncation
         [limit (limit-take (routed-result parsed db inputs) limit)]
         ;; no limit — full routing
-        [else (routed-result parsed db inputs)])))
+        [else (routed-result parsed db inputs)]))))
 
   (def (query-db-general schema find-vars in-vars where-clauses rules-ht db inputs)
     (let* ([rules-ht rules-ht]