Add an SQLite connection abstraction.
This commit is contained in:
parent
d42e2f02a6
commit
724c37466d
7 changed files with 254 additions and 2 deletions
|
@ -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"]]
|
||||
|
|
40
src/datomish/jdbc_sqlite.clj
Normal file
40
src/datomish/jdbc_sqlite.clj
Normal 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)))))
|
37
src/datomish/promise_sqlite.cljs
Normal file
37
src/datomish/promise_sqlite.cljs
Normal 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
70
src/datomish/sqlite.cljc
Normal 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]))))
|
48
test/datomish/jdbc_sqlite_test.clj
Normal file
48
test/datomish/jdbc_sqlite_test.clj
Normal 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}]))))))
|
52
test/datomish/promise_sqlite_test.cljs
Normal file
52
test/datomish/promise_sqlite_test.cljs
Normal 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}]))))))
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue