datalog: dl-retract! preserves EDB in mixed relations
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 27s

A "mixed" relation has both user-asserted facts AND rules with the
same head. Previously dl-retract! wiped every rule-head relation
wholesale before re-saturating — the saturator only re-derives the
IDB portion, so explicit EDB facts vanished even for a no-op retract
of a non-existent tuple. Repro:

  (let ((db (dl-program "p(a). p(b). p(X) :- q(X). q(c).")))
    (dl-retract! db (quote (p z)))
    (dl-query db (quote (p X))))

went from {a, b, c} to just {c}.

Fix: track :edb-keys provenance in the db.

  - dl-make-db now allocates an :edb-keys dict.
  - dl-add-fact! (public) marks (rel-key, tuple-key) in :edb-keys.
  - New internal dl-add-derived! does the append without marking.
  - Saturator (semi-naive + naive driver) now calls dl-add-derived!.
  - dl-retract! strips only the IDB-derived portion of rule-head
    relations (anything not in :edb-keys) and preserves the EDB
    portion through the re-saturate pass.

2 new regression tests; conformance 262/262.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 07:51:08 +00:00
parent 285cd530eb
commit c6f646607e
7 changed files with 184 additions and 40 deletions

View File

@@ -91,14 +91,18 @@
(dl-saturate! db)
db)))
;; Remove a fact: drop matching tuples from EDB AND wipe all derived
;; tuples (any IDB tuple may have transitively depended on the removed
;; fact). Then re-saturate to repopulate IDB. EDB facts that were
;; asserted via dl-add-fact! are preserved unless they match `lit`.
;; Remove a fact and re-saturate. Mixed relations (which have BOTH
;; user-asserted facts AND rules) are supported via :edb-keys provenance
;; — explicit facts are marked at dl-add-fact! time, the saturator uses
;; dl-add-derived! which doesn't mark them, so the retract pass can
;; safely wipe IDB-derived tuples while preserving the user's EDB.
;;
;; To distinguish EDB from IDB, we treat any fact for a relation that
;; has rules as IDB; otherwise EDB. (Phase 9 simplification — Phase 10
;; may track provenance.)
;; Effect:
;; - remove tuples matching `lit` from :facts and :edb-keys
;; - for every relation that has a rule (i.e. potentially IDB or
;; mixed), drop the IDB-derived portion (anything not in :edb-keys)
;; so the saturator can re-derive cleanly
;; - re-saturate
(define
dl-retract!
(fn
@@ -106,14 +110,20 @@
(let
((rel-key (dl-rel-name lit)))
(do
;; Drop the matching tuple from its relation list (if EDB-only).
;; Drop the matching tuple from its relation list, its facts-keys,
;; its first-arg index, AND from :edb-keys (if present).
(when
(has-key? (get db :facts) rel-key)
(let
((existing (get (get db :facts) rel-key))
(kept (list))
(kept-keys {})
(kept-index {}))
(kept-index {})
(edb-rel (cond
((has-key? (get db :edb-keys) rel-key)
(get (get db :edb-keys) rel-key))
(else nil)))
(kept-edb {}))
(do
(for-each
(fn
@@ -122,7 +132,13 @@
(not (dl-tuple-equal? t lit))
(do
(append! kept t)
(dict-set! kept-keys (dl-tuple-key t) true)
(let ((tk (dl-tuple-key t)))
(do
(dict-set! kept-keys tk true)
(when
(and (not (nil? edb-rel))
(has-key? edb-rel tk))
(dict-set! kept-edb tk true))))
(when
(>= (len t) 2)
(let ((k (dl-arg-key (nth t 1))))
@@ -134,17 +150,51 @@
existing)
(dict-set! (get db :facts) rel-key kept)
(dict-set! (get db :facts-keys) rel-key kept-keys)
(dict-set! (get db :facts-index) rel-key kept-index))))
;; Wipe all relations that have a rule (these are IDB) so the
;; saturator regenerates them from the surviving EDB.
(dict-set! (get db :facts-index) rel-key kept-index)
(when
(not (nil? edb-rel))
(dict-set! (get db :edb-keys) rel-key kept-edb)))))
;; For each rule-head relation, strip the IDB-derived tuples
;; (anything not marked in :edb-keys) so the saturator can
;; cleanly re-derive without leaving stale tuples that depended
;; on the now-removed fact.
(let ((rule-heads (dl-rule-head-rels db)))
(for-each
(fn
(k)
(do
(dict-set! (get db :facts) k (list))
(dict-set! (get db :facts-keys) k {})
(dict-set! (get db :facts-index) k {})))
(when
(has-key? (get db :facts) k)
(let
((existing (get (get db :facts) k))
(kept (list))
(kept-keys {})
(kept-index {})
(edb-rel (cond
((has-key? (get db :edb-keys) k)
(get (get db :edb-keys) k))
(else {}))))
(do
(for-each
(fn
(t)
(let ((tk (dl-tuple-key t)))
(when
(has-key? edb-rel tk)
(do
(append! kept t)
(dict-set! kept-keys tk true)
(when
(>= (len t) 2)
(let ((kk (dl-arg-key (nth t 1))))
(do
(when
(not (has-key? kept-index kk))
(dict-set! kept-index kk (list)))
(append! (get kept-index kk) t))))))))
existing)
(dict-set! (get db :facts) k kept)
(dict-set! (get db :facts-keys) k kept-keys)
(dict-set! (get db :facts-index) k kept-index)))))
rule-heads))
(dl-saturate! db)
db))))