From 22f068f20d8203e8cb5db2f74d418a4c4785175b Mon Sep 17 00:00:00 2001 From: andersmurphy Date: Fri, 26 Dec 2025 10:58:27 +0000 Subject: [PATCH] Implement session changeset undo --- src/sqlite4clj/impl/api.clj | 4 ++ src/sqlite4clj/session.clj | 134 ++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/sqlite4clj/session.clj diff --git a/src/sqlite4clj/impl/api.clj b/src/sqlite4clj/impl/api.clj index 3893e9a..a4f2050 100644 --- a/src/sqlite4clj/impl/api.clj +++ b/src/sqlite4clj/impl/api.clj @@ -64,6 +64,10 @@ (defonce init-lib (initialize nil)) +(defcfn free + sqlite3_free + [::mem/pointer] ::mem/void) + (defcfn errmsg sqlite3_errmsg [::mem/pointer] ::mem/c-string) diff --git a/src/sqlite4clj/session.clj b/src/sqlite4clj/session.clj new file mode 100644 index 0000000..98dd7e9 --- /dev/null +++ b/src/sqlite4clj/session.clj @@ -0,0 +1,134 @@ +(ns sqlite4clj.session + (:require + [coffi.ffi :as ffi :refer [defcfn]] + [coffi.mem :as mem] + [sqlite4clj.impl.api :as api])) + +;; ========= SESSION extension ========= +;; https://sqlite.org/session/changegroup.html + +(defcfn session-create + "sqlite3session_create" + [::mem/pointer ::mem/c-string ::mem/pointer] ::mem/int + sqlite3session-create-native + [pdb] + (with-open [arena (mem/confined-arena)] + (let [ppSession (mem/alloc-instance ::mem/pointer arena) + code (sqlite3session-create-native pdb "main" ppSession)] + (if (api/sqlite-ok? code) + (mem/deserialize-from ppSession ::mem/pointer) + (throw (api/sqlite-ex-info pdb code {})))))) + +(defcfn session-attach + sqlite3session_attach + [::mem/pointer ::mem/c-string] ::mem/int) + +(defcfn session-delete + sqlite3session_delete + [::mem/pointer] ::mem/int) + +(defcfn session-changeset + "sqlite3session_changeset" + [::mem/pointer ::mem/pointer ::mem/pointer] ::mem/int + sqlite3session-patchset-native + [pdb pSession] + (with-open [arena (mem/confined-arena)] + (let [pnPatchset (mem/alloc-instance ::mem/int arena) + ppPatchset (mem/alloc-instance ::mem/pointer arena) + code (sqlite3session-patchset-native pSession + pnPatchset + ppPatchset)] + (if (api/sqlite-ok? code) + [(mem/deserialize-from pnPatchset ::mem/int) + (mem/deserialize-from ppPatchset ::mem/pointer)] + (throw (api/sqlite-ex-info pdb code {})))))) + +(defcfn changeset-invert + "sqlite3changeset_invert" + [::mem/int ::mem/pointer + ::mem/pointer ::mem/pointer] ::mem/int + sqlite3changeset-invert-native + [pdb nInSet pInSet] + (with-open [arena (mem/confined-arena)] + (let [pnOutSet (mem/alloc-instance ::mem/int arena) + ppOutSet (mem/alloc-instance ::mem/pointer arena) + code (sqlite3changeset-invert-native + nInSet pInSet + pnOutSet ppOutSet)] + (if (api/sqlite-ok? code) + [(mem/deserialize-from pnOutSet ::mem/int) + (mem/deserialize-from ppOutSet ::mem/pointer)] + (throw (api/sqlite-ex-info pdb code {})))))) + +(defcfn changeset-apply + sqlite3changeset_apply + [::mem/pointer ;; db + ::mem/int ;; size of changeset + ::mem/pointer ;; changeset + ::mem/pointer ;; xFilter + ::mem/pointer ;; xConflict + ::mem/pointer ;; First arg to conflict + ] ::mem/int) + +(defn new-session + "Creates a session and attaches it to the database." + [conn] + (let [pdb (:pdb conn) + pSession (session-create pdb)] + (session-attach pSession nil) + (atom pSession))) + +(defn cancel-session + "Cancels session without undoing changes." + [session] + (when-let [session @session] + (session-delete session))) + +(defn undo-session + "Undoes the current session and deletes it." + [conn session] + (when-let [pSession @session] + (let [pdb (:pdb conn) + [nSet pSet] (session-changeset pdb pSession) + _ (session-delete pSession) + [nInvertSet pInvertSet] (changeset-invert pdb nSet pSet)] + (with-open [arena (mem/confined-arena)] + (let [x-conflict + ;; Fails if there's a conflict (there should never be a conflict) + ;; when using undo-session correctly. + (mem/serialize (fn [_ _ _] (int 0)) + [::ffi/fn + [::mem/pointer ::mem/int ::mem/pointer] + ::mem/int + :raw-fn? true] + arena)] + (changeset-apply pdb nInvertSet pInvertSet nil x-conflict nil))) + (api/free pSet) + (api/free pInvertSet) + (reset! session nil)))) + +(comment + (require '[sqlite4clj.core :as d]) + + (defonce db + (d/init-db! "database.db" + {:read-only true + :pool-size 4 + :pragma {:foreign_keys false}})) + + (def reader (db :reader)) + (def writer (db :writer)) + + (d/q writer + ["CREATE TABLE IF NOT EXISTS bar(id INT PRIMARY KEY, data BLOB)"]) + + (let [session (d/with-conn [conn (:writer db)] + (new-session conn))] + (println (d/q (:reader db) ["select count(*) from bar"])) + (d/q (:writer db) + ["insert into bar (id, data) VALUES (?, ?)" + (str (random-uuid)) 34]) + (println (d/q (:reader db) ["select count(*) from bar"])) + (d/with-conn [conn (:writer db)] + (undo-session conn session)) + (println (d/q (:reader db) ["select count(*) from bar"]))))