(ns datascript-diff (:require [datascript.core :as d] [datascript.btset :as btset])) (defn- keys-eq? [a b] (or (identical? (.-keys a) (.-keys b)) (let [achunk (btset/iter-chunk a) bchunk (btset/iter-chunk b)] (and (== (count achunk) (count bchunk)) (every? #(= (nth achunk %) (nth bchunk %)) (range (count achunk))))))) (defn datom-exists? [db datom] (let [e (.-e datom) a (.-a datom) v (.-v datom)] (not-empty (d/datoms db :eavt e a v)))) (defn add-retract [action datom] [action (.-e datom) (.-a datom) (.-v datom)]) (defn datoms-diff "Given two datascript dbs, return the :db/add :db/retract statements to bring them in sync" ([a-db b-db] (datoms-diff a-db b-db (d/datoms a-db :eavt) (d/datoms b-db :eavt) [])) ([a-db b-db a b accum] (if (and (nil? a) (nil? b)) accum (if (and (some? a) (some? b) (keys-eq? a b)) (recur a-db b-db (btset/iter-chunked-next a) (btset/iter-chunked-next b) accum) (let [achunk (btset/iter-chunk a) bchunk (btset/iter-chunk b) diff (map (fn [x] (let [a-datom (nth achunk x) b-datom (nth bchunk x) same? (= a-datom b-datom)] (when-not same? (cond-> [] ;; If a exists, but b doesn't, then we need to add a (and a-datom (nil? b-datom) (not (datom-exists? b-db a-datom))) (conj (add-retract :db/add a-datom)) ;; if b exists, but a doesn't, then we need to add b (and b-datom (nil? a-datom) (not (datom-exists? a-db b-datom))) (conj (add-retract :db/add b-datom)) ;; if a and b exist, but a isn't in b-db, then issue a retraction (and a-datom b-datom (not (datom-exists? b-db a-datom))) (conj (add-retract :db/retract a-datom)) ;; if a and b exist, but b isn't in a-db, then issue an addition (and a-datom b-datom (not (datom-exists? a-db b-datom))) (conj (add-retract :db/add b-datom)))))) (range 25)) filtered (filter not-empty diff) merged (mapcat into filtered)] (recur a-db b-db (btset/iter-chunked-next a) (btset/iter-chunked-next b) (into accum merged))))))) (defn simple-test [] (let [sample-db (-> (d/empty-db {:first-name {:db/index true} :last-name {:db/index true} :phone {:db/index true} :email {:db/index true}}) (d/db-with [[:db/add 1 :first-name "Sean"] [:db/add 1 :last-name "Tempesta"] [:db/add 1 :phone "555-5555"]])) changes [[:db/add 1 :email "sean.tempesta@gmail.com"] [:db/retract 1 :last-name "Tempesta"] [:db/retract 1 :phone "555-5555"] [:db/add 1 :phone "555-1234"]] changed-db (d/db-with sample-db changes)] (= changes (datoms-diff sample-db changed-db))))