Add an SQLite connection abstraction.

This commit is contained in:
Nick Alexander 2016-07-11 22:02:53 -07:00
parent d42e2f02a6
commit 724c37466d
7 changed files with 254 additions and 2 deletions

View file

@ -42,7 +42,9 @@
:profiles {:dev {:dependencies [[com.cemerick/piggieback "0.2.1"]
[org.clojure/tools.nrepl "0.2.10"]
[tempfile "0.2.0"]]
[tempfile "0.2.0"]
[org.clojure/java.jdbc "0.6.2-alpha1"]
[org.xerial/sqlite-jdbc "3.8.11.2"]]
:repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
:plugins [[lein-cljsbuild "1.1.2"]
[lein-doo "0.1.6"]]

View file

@ -0,0 +1,40 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.jdbc-sqlite
(:require
[datomish.pair-chan :refer [go-pair]]
[datomish.sqlite :as s]
[clojure.java.jdbc :as j]
[clojure.core.async :as a]))
(deftype JDBCSQLiteConnection [spec]
s/ISQLiteConnection
(-execute!
[db sql bindings]
(go-pair
(j/execute! (.-spec db) (into [sql] bindings) {:transaction? false})))
(-each
[db sql bindings row-cb]
(go-pair
(let [rows (j/query (.-spec db) (into [sql] bindings))]
(when row-cb
(doseq [row rows] (row-cb row)))
(count rows))))
(close [db]
(go-pair
(.close (:connection (.-spec db))))))
(defn open
[path & {:keys [mode]}]
(let [spec {:classname "org.sqlite.JDBC"
:subprotocol "sqlite"
:subname path}] ;; TODO: use mode.
(go-pair
(->>
(j/get-connection spec)
(assoc spec :connection)
(->JDBCSQLiteConnection)))))

View file

@ -0,0 +1,37 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.promise-sqlite
(:require
[datomish.sqlite :as s]
[cljs-promises.async]
[cljs.nodejs :as nodejs]))
(def sqlite (nodejs/require "promise-sqlite"))
(defrecord SQLite3Connection [db]
s/ISQLiteConnection
(-execute!
[db sql bindings]
(cljs-promises.async/pair-port
(.run (.-db db) sql (or (clj->js bindings) #js []))))
(-each
[db sql bindings row-cb]
(let [cb (fn [row]
(row-cb (js->clj row :keywordize-keys true)))]
(cljs-promises.async/pair-port
(.each (.-db db) sql (or (clj->js bindings) #js []) (when row-cb cb)))))
(close
[db]
(cljs-promises.async/pair-port
(.close (.-db db)))))
(defn open
[path & {:keys [mode] :or {:mode 6}}]
(cljs-promises.async/pair-port
(->
(.open sqlite.DB path (clj->js {:mode mode}))
(.then ->SQLite3Connection))))

70
src/datomish/sqlite.cljc Normal file
View file

@ -0,0 +1,70 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.sqlite
#?(:cljs
(:require-macros
[datomish.pair-chan :refer [go-pair <?]]
[cljs.core.async.macros :refer [go]]))
#?(:clj
(:require
[datomish.pair-chan :refer [go-pair <?]]
[clojure.core.async :refer [go <! >!]])
:cljs
(:require
[datomish.pair-chan]
[cljs.core.async :as a :refer [<! >!]])))
(defprotocol ISQLiteConnection
(-execute!
[db sql bindings]
"Execute the given SQL string with the specified bindings. Returns a pair channel resolving
to a query dependent `[result error]` pair.")
(-each
[db sql bindings row-cb]
"Execute the given SQL string with the specified bindings, invoking the given `row-cb` callback
function (if provided) with each returned row. Each row will be presented to `row-cb` as a
map-like object, such that `(:column-name row)` succeeds. Returns a pair channel of `[result
error]`, where `result` to the number of rows returned.")
(close
[db]
"Close this SQLite connection. Returns a pair channel of [nil error]."))
(defn execute!
[db [sql & bindings]]
(-execute! db sql bindings))
(defn each-row
[db [sql & bindings] row-cb]
(-each db sql bindings row-cb))
(defn reduce-rows
[db [sql & bindings] initial f]
(let [acc (atom initial)]
(go
(let [[_ err] (<! (-each db sql bindings #(swap! acc f %)))]
(if err
[nil err]
[@acc nil])))))
(defn all-rows
[db [sql & bindings :as rest]]
(reduce-rows db rest [] conj))
(defn in-transaction! [db chan-fn]
(go
(try
(<? (execute! db ["BEGIN TRANSACTION"]))
(let [[v e] (<! (chan-fn))]
(if v
(do
(<? (execute! db ["COMMIT"]))
[v nil])
(do
(<? (execute! db ["ROLLBACK TRANSACTION"]))
[nil e])))
(catch #?(:clj Exception :cljs js/Error) e
[nil e]))))

View file

@ -0,0 +1,48 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.jdbc-sqlite-test
(:require
[datomish.sqlite :as s]
[datomish.pair-chan :refer [go-pair <?]]
[datomish.jdbc-sqlite :as j]
[datomish.test-macros :refer [deftest-async]]
[tempfile.core :refer [tempfile with-tempfile]]
[clojure.core.async :as a :refer [<! >!]]
[clojure.test :as t :refer [is are deftest testing]]))
(deftest-async test-all-rows
(with-tempfile [t (tempfile)]
(with-open [db (<? (j/open t))]
(<? (s/execute! db ["CREATE TABLE test (a INTEGER)"]))
(<? (s/execute! db ["INSERT INTO test VALUES (?)" 1]))
(<? (s/execute! db ["INSERT INTO test VALUES (?)" 2]))
(let [rows (<? (s/all-rows db ["SELECT * FROM test ORDER BY a ASC"]))]
(is (= rows [{:a 1} {:a 2}]))))))
(deftest-async test-in-transaction!
(with-tempfile [t (tempfile)]
(with-open [db (<? (j/open t))]
(<? (s/execute! db ["CREATE TABLE ta (a INTEGER)"]))
(<? (s/execute! db ["CREATE TABLE tb (b INTEGER)"]))
(<? (s/execute! db ["INSERT INTO ta VALUES (?)" 1]))
(let [[v e] (<! (s/in-transaction! db #(s/execute! db ["INSERT INTO tb VALUES (?)" 2])))]
(is (not e)))
(let [rows (<? (s/all-rows db ["SELECT * FROM ta ORDER BY a ASC"]))]
(is (= rows [{:a 1}])))
(let [rows (<? (s/all-rows db ["SELECT * FROM tb ORDER BY b ASC"]))]
(is (= rows [{:b 2}])))
(println "a")
(let [f #(go-pair
;; The first succeeds ...
(<? (s/execute! db ["INSERT INTO ta VALUES (?)" 3]))
;; ... but will get rolled back by the second failing.
(<? (s/execute! db ["INSERT INTO tb VALUES (?)" 4 "bad parameter"])))
[v e] (<! (s/in-transaction! db f))]
(is (some? e)))
;; No changes, since the transaction as a whole failed.
(let [rows (<? (s/all-rows db ["SELECT * FROM ta ORDER BY a ASC"]))]
(is (= rows [{:a 1}])))
(let [rows (<? (s/all-rows db ["SELECT * FROM tb ORDER BY b ASC"]))]
(is (= rows [{:b 2}]))))))

View file

@ -0,0 +1,52 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
(ns datomish.promise-sqlite-test
(:require-macros
[datomish.pair-chan :refer [go-pair <?]]
[datomish.test-macros :refer [deftest-async with-open]]
[datomish.node-tempfile-macros :refer [with-tempfile]]
[cljs.core.async.macros])
(:require
[datomish.node-tempfile :refer [tempfile]]
[cljs.core.async :refer [<! >!]]
[cljs.test :refer-macros [is are deftest testing async]]
[datomish.pair-chan]
[datomish.sqlite :as s]
[datomish.promise-sqlite :as ps]))
(deftest-async test-all-rows
(with-tempfile [t (tempfile)]
(with-open [db (<? (ps/open (.name t) :mode 6))]
(<? (s/execute! db ["CREATE TABLE test (a INTEGER)"]))
(<? (s/execute! db ["INSERT INTO test VALUES (?)" 1]))
(<? (s/execute! db ["INSERT INTO test VALUES (?)" 2]))
(let [rows (<? (s/all-rows db ["SELECT * FROM test ORDER BY a ASC"]))]
(is (= rows [{:a 1} {:a 2}]))))))
(deftest-async test-in-transaction!
(with-tempfile [t (tempfile)]
(with-open [db (<? (ps/open (.name t) :mode 6))]
(<? (s/execute! db ["CREATE TABLE ta (a INTEGER)"]))
(<? (s/execute! db ["CREATE TABLE tb (b INTEGER)"]))
(<? (s/execute! db ["INSERT INTO ta VALUES (?)" 1]))
(let [[v e] (<! (s/in-transaction! db #(s/execute! db ["INSERT INTO tb VALUES (?)" 2])))]
(is (not e)))
(let [rows (<? (s/all-rows db ["SELECT * FROM ta ORDER BY a ASC"]))]
(is (= rows [{:a 1}])))
(let [rows (<? (s/all-rows db ["SELECT * FROM tb ORDER BY b ASC"]))]
(is (= rows [{:b 2}])))
(println "a")
(let [f #(go-pair
;; The first succeeds ...
(<? (s/execute! db ["INSERT INTO ta VALUES (?)" 3]))
;; ... but will get rolled back by the second failing.
(<? (s/execute! db ["INSERT INTO tb VALUES (?)" 4 "bad parameter"])))
[v e] (<! (s/in-transaction! db f))]
(is (some? e)))
;; No changes, since the transaction as a whole failed.
(let [rows (<? (s/all-rows db ["SELECT * FROM ta ORDER BY a ASC"]))]
(is (= rows [{:a 1}])))
(let [rows (<? (s/all-rows db ["SELECT * FROM tb ORDER BY b ASC"]))]
(is (= rows [{:b 2}]))))))

View file

@ -2,6 +2,9 @@
(:require
[doo.runner :refer-macros [doo-tests doo-all-tests]]
[cljs.test :as t :refer-macros [is are deftest testing]]
datomish.promise-sqlite-test
datomish.test-macros-test))
(doo-tests 'datomish.test-macros-test)
(doo-tests
'datomish.promise-sqlite-test
'datomish.test-macros-test)