(ns pocketbook.db-test (:require [clojure.test :refer [deftest is testing use-fixtures]] [pocketbook.db :as db]) (:import [java.io File])) (def ^:dynamic *ds* nil) (defn- temp-db-path [] (str (File/createTempFile "pocketbook-test" ".db"))) (use-fixtures :each (fn [f] (let [path (temp-db-path) ds (db/open path)] (try (binding [*ds* ds] (f)) (finally (.delete (File. path))))))) ;; --------------------------------------------------------------------------- ;; Basic CRUD ;; --------------------------------------------------------------------------- (deftest upsert-new-doc (let [result (db/upsert! *ds* {:id "todo:1" :value {:text "Buy milk" :tags #{:groceries}} :base-version 0})] (is (= :ok (:status result))) (is (= 1 (:version result))) (let [doc (db/get-doc *ds* "todo:1")] (is (= "todo:1" (:id doc))) (is (= {:text "Buy milk" :tags #{:groceries}} (:value doc))) (is (= 1 (:version doc))) (is (false? (:deleted doc)))))) (deftest upsert-update-doc (db/upsert! *ds* {:id "todo:1" :value {:text "v1"} :base-version 0}) (let [result (db/upsert! *ds* {:id "todo:1" :value {:text "v2"} :base-version 1})] (is (= :ok (:status result))) (is (= 2 (:version result))) (is (= {:text "v2"} (:value (db/get-doc *ds* "todo:1")))))) (deftest upsert-conflict (db/upsert! *ds* {:id "todo:1" :value {:text "v1"} :base-version 0}) (let [result (db/upsert! *ds* {:id "todo:1" :value {:text "bad"} :base-version 0})] (is (= :conflict (:status result))) (is (= 1 (:current-version result))) ;; Original doc unchanged (is (= {:text "v1"} (:value (db/get-doc *ds* "todo:1")))))) (deftest upsert-conflict-stale-version (db/upsert! *ds* {:id "todo:1" :value {:text "v1"} :base-version 0}) (db/upsert! *ds* {:id "todo:1" :value {:text "v2"} :base-version 1}) (let [result (db/upsert! *ds* {:id "todo:1" :value {:text "bad"} :base-version 1})] (is (= :conflict (:status result))) (is (= 2 (:current-version result))) (is (= {:text "v2"} (:value result))))) ;; --------------------------------------------------------------------------- ;; Delete ;; --------------------------------------------------------------------------- (deftest soft-delete (db/upsert! *ds* {:id "todo:1" :value {:text "v1"} :base-version 0}) (let [result (db/delete! *ds* {:id "todo:1" :base-version 1})] (is (= :ok (:status result))) (is (= 2 (:version result))) (let [doc (db/get-doc *ds* "todo:1")] (is (true? (:deleted doc))) (is (= 2 (:version doc)))))) (deftest delete-conflict (db/upsert! *ds* {:id "todo:1" :value {:text "v1"} :base-version 0}) (let [result (db/delete! *ds* {:id "todo:1" :base-version 0})] (is (= :conflict (:status result))) (is (= 1 (:current-version result))))) ;; --------------------------------------------------------------------------- ;; Queries ;; --------------------------------------------------------------------------- (deftest docs-since-filters-by-group-and-time (db/upsert! *ds* {:id "todo:1" :value {:text "a"} :base-version 0}) (Thread/sleep 10) (let [t (System/currentTimeMillis)] (Thread/sleep 10) (db/upsert! *ds* {:id "todo:2" :value {:text "b"} :base-version 0}) (db/upsert! *ds* {:id "note:1" :value {:text "c"} :base-version 0}) (let [docs (db/docs-since *ds* "todo" t)] (is (= 1 (count docs))) (is (= "todo:2" (:id (first docs))))))) (deftest all-docs-excludes-deleted (db/upsert! *ds* {:id "todo:1" :value {:text "a"} :base-version 0}) (db/upsert! *ds* {:id "todo:2" :value {:text "b"} :base-version 0}) (db/delete! *ds* {:id "todo:1" :base-version 1}) (let [docs (db/all-docs *ds* "todo")] (is (= 1 (count docs))) (is (= "todo:2" (:id (first docs)))))) ;; --------------------------------------------------------------------------- ;; Clojure type preservation ;; --------------------------------------------------------------------------- (deftest preserves-clojure-types (let [value {:keyword :hello :set #{1 2 3} :vec [1 "two" :three] :uuid (java.util.UUID/randomUUID) :inst #inst "2026-04-04T10:00:00Z" :nested {:a {:b {:c 42}}} :nil-val nil :ratio 22/7 :symbol 'my-sym}] (db/upsert! *ds* {:id "types:1" :value value :base-version 0}) (let [doc (db/get-doc *ds* "types:1")] (is (= (:keyword value) (:keyword (:value doc)))) (is (= (:set value) (:set (:value doc)))) (is (= (:vec value) (:vec (:value doc)))) (is (= (:uuid value) (:uuid (:value doc)))) (is (= (:inst value) (:inst (:value doc)))) (is (= (:nested value) (:nested (:value doc)))) (is (nil? (:nil-val (:value doc)))) (is (= (:symbol value) (:symbol (:value doc))))))) ;; --------------------------------------------------------------------------- ;; Purge ;; --------------------------------------------------------------------------- (deftest purge-deleted-removes-old (db/upsert! *ds* {:id "todo:1" :value {:text "a"} :base-version 0}) (db/delete! *ds* {:id "todo:1" :base-version 1}) ;; Wait a tick so the updated timestamp is in the past (Thread/sleep 20) ;; Purge with a large max-age window — recent deletes are kept (db/purge-deleted! *ds* 999999) (is (some? (db/get-doc *ds* "todo:1"))) ;; Purge with a tiny max-age — everything older than 10ms ago is removed (db/purge-deleted! *ds* 10) (is (nil? (db/get-doc *ds* "todo:1"))))