dependencies
| (this space intentionally left almost blank) | |||
The core namespace for the refdb library. | (ns refdb.core (:refer-clojure :exclude [get find]) (:require [clojure.edn :as edn] [clojure.java.io :as io] [clojure.string :as string] [refdb.internal.core :refer [load-form]] [refdb.migrate :as migrate])) | |||
(def refdb-version "0.6") | ||||
(defrecord Collection [coll-file meta-file coll-ref name]) | ||||
(defrecord RefDB [path collections]) | ||||
(defmethod print-dup RefDB [{:keys [path collections version]} w]
(.write w (format "#<RefDB:
{:path %s
:version %s
:collections %s}>"
(pr-str path)
(pr-str version)
(pr-str (map key collections))))) | ||||
(defmethod print-method RefDB [{:keys [path collections version]} w]
(.write w (format "#<RefDB:
{:path %s
:version %s
:collections %s}>"
(pr-str path)
(pr-str version)
(pr-str (map key collections))))) | ||||
(defn db-version [{:keys [version] :as db-spec}]
(assert version "No version found, assume < 6.0")
version) | ||||
(defn join-path [& args]
{:pre [(every? (comp not nil?) args)]}
(let [ensure-no-delims #(string/replace % #"(?:^/)|(?:/$)" )]
(str (when (.startsWith (first args) "/") \/)
(string/join "/" (map ensure-no-delims args))))) | ||||
(defn- transaction-dir [{:keys [path] :as db-spec}]
(join-path path "_transaction")) | ||||
(defn- transaction-file [transaction] "transaction.clj") | ||||
Is | (defn literal? [x] (or (true? x) (false? x) (keyword? x) (string? x) (number? x))) | |||
Is | (defn regex? [x] (instance? java.util.regex.Pattern x)) | |||
(defn dbref [db-spec coll] (-> db-spec :collections coll :coll-ref)) | ||||
(defn with-schema [coll-kw schema valid-fn]
(map->Collection
{:name (name coll-kw)
:key coll-kw
::schema schema
::validate valid-fn})) | ||||
(defn coerce-coll [x]
(if (keyword? x) (map->Collection {:name (name x) :key x}) x)) | ||||
(defmulti collection (fn [{:keys [version]} & _] version)) | ||||
(defmethod collection "0.6"
[{:keys [path no-write? version]} {:keys [name key] :as collection}]
[key (merge collection
{:coll-ref (ref nil)
:meta (ref {:items #{} :count 0})}
(when path
{:coll-dir (io/file path name)
:meta-file (-> path (io/file name) (io/file "meta"))}))]) | ||||
(defmethod collection "0.5"
[{:keys [path no-write? version]} {:keys [name key] :as collection}]
[key (merge collection
{:coll-ref (ref nil)}
(when path
{:coll-file (io/file path (format "%s.clj" name))
:coll-dir (io/file path name)
:meta-file (io/file path (format "%s.meta.clj" name))}))]) | ||||
Creates a db-spec. Takes a map of
| (defn db-spec
[{:keys [path no-write? version] :as opts} & collections]
(let [path (cond (string? path)
(if-let [resource (io/resource path)]
(io/file resource)
(io/file path))
(instance? java.net.URI path)
(io/file path)
:default path)
meta (io/file path "meta")
{:keys [version] :as meta}
(if (.exists meta)
(load-form meta)
{:version (or version "0.5") :collections (set (map :key collections))})
opts (assoc opts :path path :version version)]
(assert (or path no-write?)
"Option `path`, or :no-write? must be specified.")
(assert (or (and (instance? java.io.File path) (.exists path)) no-write?)
"If `no-write?` not specified, option `path` must either
be, or convert to a java.io.File, and it must exist.")
(map->RefDB
(assoc opts
:collections
(->> collections
(map (comp (partial collection opts) coerce-coll))
(into {})))))) | |||
Initialize the collections in a db-spec.
... to initialize all collections in the
... to only initialize some of them. | (defn init!
[{:keys [collections no-write? version] :as db-spec} & {:keys [only]}]
(doseq [[_ {:keys [coll-ref meta-file meta coll-dir name]}]
(if only (select-keys collections only) collections)]
(when (and (not no-write?) (not (.exists coll-dir)))
(.mkdirs coll-dir))
(when (and (not no-write?) (not (.exists meta-file)))
(spit meta-file (pr-str @meta)))
(when (and meta-file (.exists meta-file))
(dosync
(ref-set meta (load-form meta-file))
(doseq [id (:items @meta)]
(let [coll-file (-> coll-dir (io/file (str id)) (io/file "current"))
{:keys [id] :as form} (load-form coll-file)]
(alter coll-ref assoc-in [:items id] form))))))) | |||
(defn spit-record [file record]
(spit file
(str (when (meta record) (str \^ (pr-str (meta record)) \space))
(prn-str record)))) | ||||
Persists | (defn write!
[{{n :count} :history :as x} db-spec coll-key {:keys [id] :as record}]
(let [{dir :coll-dir :keys [meta meta-file]} (-> db-spec :collections coll-key)
count (or n 0)
record (some-> record (assoc-in [:history :count] (inc count)))
record-dir (io/file dir (str id))]
(when-not (.exists record-dir) (.mkdirs record-dir))
(when x
(let [hist-dir (-> record-dir (io/file "history"))]
(when-not (.exists hist-dir) (.mkdirs hist-dir))
(spit-record (io/file hist-dir (pr-str count)) x)))
(spit meta-file @meta)
(spit-record (-> record-dir (io/file "current")) record)
record)) | |||
Writes a | (defn write-transaction!
[{:keys [no-write? path] :as db-spec} {:keys [id inst name] :as transaction}]
{:pre [(or no-write? path)
(= ::transaction (type transaction))
(map? transaction)
(and id inst name)]}
(when-not no-write?
(let [transaction-dir (transaction-dir db-spec)]
(try
(.mkdir (io/file transaction-dir))
(spit (io/file (join-path transaction-dir
(transaction-file transaction)))
(prn-str transaction)
:append true)
true
(catch Exception _))))) | |||
(defn transaction [{:keys [path] :as db-spec} record]
"Returns the `record`'s transaction."
{:pre [(:transaction (meta record))]}
(let [t (:transaction (meta record))
reader-opts {:eof nil :readers *data-readers*}]
(with-open [r (java.io.PushbackReader.
(io/reader
(io/file (join-path (transaction-dir db-spec)
(transaction-file t)))))]
(loop [form (edn/read reader-opts r)]
(when form
(if (= (:id form) t) form (recur (edn/read reader-opts r)))))))) | ||||
Gets a the next available ID for the database. | (defn get-id
[db-spec coll]
(let [coll (dbref db-spec coll)]
(dosync (-> coll
(alter update-in [:last-id] (fnil inc -1))
(:last-id))))) | |||
Gets an item from the collection by id. | (defn get
[db-spec coll id]
(let [match (-> @(dbref db-spec coll) (get-in [:items id]))]
(when-not (::deleted match) match))) | |||
(defn created [db-spec coll id] ; TODO: get first history item
(let [match (-> @(dbref db-spec coll) (get-in [:items id]) last)]
(when-not (::deleted match)
(:inst match)))) | ||||
Returns truthy if the predicate, pred matches the item. If the predicate is
| (defn pred-match?
[pred item]
(let [?fn (condp = (::? (meta pred)) ::and every? ::or some every?)]
(cond (or (nil? pred) (empty? pred)) true
(vector? pred)
(?fn #(pred-match? % item) pred)
(map? pred)
(?fn (fn [[k v]]
(let [value (if (vector? k) (get-in item k) (k item))]
(cond (literal? v) (= value v)
(regex? v) (when value (re-seq v value))
(and (set? v) (vector? value))
(or (v value) (some v value))
:else (v value))))
pred)
:default nil))) | |||
Creates | (defn ?and
([pred]
(with-meta pred {::? ::and}))
([head & tail]
(with-meta `[~head ~@tail] {::? ::and}))) | |||
Creates | (defn ?or
([pred]
(with-meta pred {::? ::or}))
([head & tail]
(with-meta `[~head ~@tail] {::? ::or}))) | |||
Finds an item, or items in the collection by predicate. The
predicate should be a map of
Predicates can match by
Predicates can have sub-maps, and sets can be used to partially match collections.
You can also search deeper into a match using a vector as a key.
By default a predicate's matching behavior for key-vals is AND. E.G.,
finds cats with
| (defn find
[db-spec coll pred]
(->> (vals (:items @(dbref db-spec coll)))
(filter (partial pred-match? pred))
(remove ::deleted))) | |||
(defmacro with-transaction [db-spec transaction & body]
`(do
(if (clojure.lang.LockingTransaction/isRunning)
(do ~@body)
(sync nil ~@body))
(write-transaction!
~db-spec
(with-meta
(assoc ~transaction
:inst (java.util.Date.)
:id (java.util.UUID/randomUUID)
:body '~body)
{:type ::transaction})))) | ||||
(defn validate [db-spec coll m]
(let [coll (some-> db-spec :collections coll)]
(when-let [schema (::schema coll)]
((::validate coll) schema m)))) | ||||
Saves item(s) | (defn save!
[db-spec coll m]
(assert (map? m) "Argument `m` must satisfy map?.")
(assert (keyword? coll) "Argument `coll` must be a keyword, naming the coll.")
(validate db-spec coll m)
(let [coll-ref (dbref db-spec coll)
exists? (or (:exists? (meta m))
(and (:id m) (get db-spec coll (:id m))))
id (or (:id m) (get-id db-spec coll))
m (assoc m :id id :inst (java.util.Date.))]
(dosync
(when-not exists?
(if (integer? id)
(alter coll-ref update-in [:meta :last-id] (fnil max 0) id))
(alter coll-ref update-in [:meta :count] (fnil inc 0)))
(alter (-> db-spec :collections coll :meta) update-in [:items] conj id)
(-> coll-ref
(alter update-in [:items id] write! db-spec coll m)
(get-in [:items id]))))) | |||
'Updates' item with id | (defn update!
[db-spec coll id f & args]
{:pre [(fn? f) (or (integer? id) (string? id) (keyword? id))]}
(let [exists? (get db-spec coll id)]
(if exists?
(save! db-spec coll (with-meta (apply f exists? args) {:exists? true}))
(throw
(ex-info "Item with `id` must exist in coll." {:coll coll :id id}))))) | |||
(defn delete! [db-spec coll m]
{:pre [(:id m)]}
(save! db-spec coll (assoc m ::deleted (java.util.Date.)))) | ||||
Resets the | (defn destroy!
[db-spec coll]
(do (dosync (ref-set (dbref db-spec coll) {}))
(write! nil db-spec coll nil))) | |||
Returns | (defn history
[db-spec coll {id :id :as record} & [n]]
(assert (not (:no-write? db-spec))
"History does not work without durable storage")
(let [coll-ref (dbref db-spec coll)
dir (-> (get-in db-spec [:collections coll :coll-dir])
(io/file (str id))
(io/file "history"))
past (some->> (get-in @coll-ref [:items id :history :count])
(range 1)
(reverse)
(map (fn [i] (load-form (io/file dir (str i))))))]
(if n (take n past) past))) | |||
Returns the | (defn previous ([db-spec coll record n] (nth (history db-spec coll record) n)) ([db-spec coll record] (previous db-spec coll record 0))) | |||
(ns refdb.internal.core (:require [clojure.edn :as edn] [clojure.java.io :as io])) | ||||
(defn load-form [file]
(when (.exists file)
(with-open [reader (java.io.PushbackReader. (io/reader file))]
(loop [out nil]
(if-let [form (edn/read {:eof nil :readers *data-readers*} reader)]
(recur form)
out))))) | ||||
(ns refdb.migrate (:require [clojure.edn :as edn] [clojure.java.io :as io])) | ||||
(defn spit-history [{{n :count} :history :as x} dir {:keys [id] :as record}]
(let [count (or n 0)
hist-dir (-> dir (io/file (str id)) (io/file "history"))
record (assoc-in record [:history :count] (inc count))]
(when-not (.exists hist-dir) (.mkdirs hist-dir))
(when x (spit (io/file hist-dir (pr-str count)) x))
(spit (-> dir (io/file (str id)) (io/file "current")) record)
record)) | ||||
(defn cp-r [source dest]
(let [ch-path (fn [source dest x]
(.toFile
(.resolve (.toPath dest)
(.relativize (.toPath source) (.toPath x)))))
coll (->> (file-seq source)
(remove #{source})
(map (juxt identity (partial ch-path source dest))))
dirs (filter #(.isDirectory (first %)) coll)
files (remove #(.isDirectory (first %)) coll)]
(when-not (.exists dest) (.mkdirs dest))
(doseq [[_ dir] dirs] (.mkdirs dir))
(doseq [[old new] files] (io/copy old new)))) | ||||
(defmulti migrate! (fn [[from to] & _] [from to])) | ||||
(defmethod migrate! ["0.5" "0.6"]
[_ {:keys [collections no-write? path] :as db-spec}]
(println "Performing migration from refdb v0.5 to v0.6")
(let [tmp (io/file path "_0.5")]
(println (format "Backing up data to %s\n" tmp))
(cp-r path tmp))
(doseq [[_ {:keys [coll-ref meta-file coll-file coll-dir name] :as coll}]
collections]
(when-not (.exists coll-dir) (.mkdirs coll-dir))
(when (and meta-file (.exists meta-file))
(dosync
(ref-set coll-ref {})
(when (.exists coll-file)
(with-open [reader (java.io.PushbackReader. (io/reader coll-file))]
(loop []
(when-let [{:keys [id] :as x} (edn/read {:eof nil} reader)]
(do
(when (integer? id)
(alter coll-ref update-in [:last-id] (fnil max 0) id))
(when-not (get-in @coll-ref [:items id])
(alter coll-ref update-in [:count] (fnil inc 0)))
(alter coll-ref update-in [:items id] spit-history coll-dir x)
(recur))))))
(let [ids (keys (:items @coll-ref))]
(spit (io/file coll-dir "meta") {:items (set ids) :count (count ids)}))
(.delete coll-file)
(.delete meta-file))))
(spit (io/file path "meta")
{:version "0.6" :collections (set (keys collections))})) | ||||