├── .gitignore ├── src └── toadie │ ├── utils.clj │ ├── named_params.clj │ └── core.clj ├── dev └── user.clj ├── test └── toadie │ ├── test_helpers.clj │ ├── batch_insert.clj │ ├── delete_test.clj │ ├── update_test.clj │ ├── insert_test.clj │ ├── named_params_tests.clj │ ├── advanced_query_test.clj │ └── query_test.clj ├── project.clj ├── CHANGELOG.md ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea/ 13 | Clojure REPL 14 | -------------------------------------------------------------------------------- /src/toadie/utils.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.utils) 2 | 3 | (defn uuid 4 | ([] (java.util.UUID/randomUUID)) 5 | ([s] (condp instance? s 6 | java.util.UUID s 7 | (java.util.UUID/fromString s)))) 8 | 9 | (defn random-string [] 10 | (-> (java.util.UUID/randomUUID) (str) (clojure.string/replace #"-" ""))) 11 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.tools.namespace.repl :as tnr] 3 | [proto] 4 | [clojure.repl :refer :all] 5 | [clojure.test :refer :all])) 6 | 7 | (defn start 8 | []) 9 | 10 | (defn reset [] 11 | (tnr/refresh :after 'user/start)) 12 | 13 | (println "dev/user.clj loaded correctly.") 14 | -------------------------------------------------------------------------------- /test/toadie/test_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.test-helpers 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [environ.core :refer [env]])) 6 | 7 | (def test-store 8 | (toadie/docstore (env :test-db-url))) 9 | 10 | (defn drop-table [table-name] 11 | (sql/db-do-commands (:db-spec test-store) (str "drop table if exists " table-name))) 12 | 13 | (defn drop-tables [f] 14 | (f) (drop-table "people") (drop-table "posts")) 15 | -------------------------------------------------------------------------------- /src/toadie/named_params.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.named-params 2 | (:require [clojure.string :as str])) 3 | 4 | (defn to-statement 5 | ([s] [s]) 6 | ([s params] 7 | (let [found (re-seq #"@:\w+" s)] 8 | (loop [query s 9 | vals [] 10 | els found] 11 | (if 12 | (= 0 (count els)) 13 | (into [query] vals) 14 | (let [upd (str/replace-first query (first els) "?") 15 | key (keyword (subs (first els) 2)) 16 | pval (key params)] 17 | (recur upd (conj vals pval) (rest els)))))))) 18 | -------------------------------------------------------------------------------- /test/toadie/batch_insert.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.batch-insert 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (def people 13 | [{:name "a" :surname "b" :age 42}{:name "c" :surname "d" :age 43}{:name "e" :surname "f" :age 44}]) 14 | 15 | (deftest insert 16 | (testing "after batch-insert to database" 17 | (let [inserted (toadie/batch-insert test-store :people people)] 18 | (testing "should return count of inserted elements" 19 | (is (= inserted 3)))))) 20 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject toadie "0.3.0" 2 | :description "Turn postgresql database into document storage!!" 3 | :url "https://github.com/itmeze/toadie" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :profiles { :dev { 7 | :source-paths ["dev" "src" "test"] 8 | :dependencies [[org.clojure/tools.namespace "0.2.11"]]}} 9 | :dependencies [[org.clojure/clojure "1.8.0"] 10 | [org.clojure/java.jdbc "0.4.2"] 11 | [org.postgresql/postgresql "9.4-1201-jdbc41"] 12 | [cheshire "5.5.0"] 13 | [clj-time "0.11.0"] 14 | [proto-repl "0.1.2"] 15 | [environ "1.0.2"] 16 | [org.clojure/data.csv "0.1.3"]]) 17 | -------------------------------------------------------------------------------- /test/toadie/delete_test.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.delete-test 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (deftest delete-by-id 13 | (let [maria (toadie/save test-store :people {:name "maria" :age 56}) 14 | michal (toadie/save test-store :people {:name "michal" :age 34})] 15 | (testing "should be able to delete by id" 16 | (let [_ (toadie/delete-by-id test-store :people (:id maria)) 17 | result (toadie/query test-store :people {})] 18 | (is (= (count result) 1)) 19 | (is (= (:id (first result)) (:id michal))))))) 20 | -------------------------------------------------------------------------------- /test/toadie/update_test.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.update-test 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (deftest update-with-id 13 | (testing "saving object with an id should update it" 14 | (let [inserted (toadie/save test-store :people {:name "michal"}) 15 | updated (toadie/save test-store :people (assoc inserted :name "maria")) 16 | result (toadie/query test-store :people {})] 17 | (is (= (count result) 1)) 18 | (is (= (:name (first result)) "maria")) 19 | (is (= (:id inserted) (:id updated) (:id (first result))))))) 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased][unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2016-02-02 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2016-02-02 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [unreleased]: https://github.com/your-name/toadie/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/toadie/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 ITmeze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/toadie/insert_test.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.insert-test 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (deftest insert 13 | (testing "after inserting to database" 14 | (let [inserted (toadie/save test-store :people {:name "maria" :surname "johnson" :age 42})] 15 | (testing "should assoc id to map" 16 | (is (inserted :id))) 17 | (testing "should store in database" 18 | (let [res (toadie/raw-query test-store "select count(*) from people") 19 | c (count res)] 20 | (is (= c 1))))))) 21 | 22 | (def v-people 23 | [{:name "maria" :surname "johnson" :age 42} 24 | {:name "michal" :surname "itmeze" :age 32}]) 25 | 26 | (deftest multi-insert 27 | (testing "insert x elements should return x maps" 28 | (let [all-inserted (toadie/save test-store :people v-people)] 29 | (is (count all-inserted) 2)))) 30 | -------------------------------------------------------------------------------- /test/toadie/named_params_tests.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.named-params-tests 2 | (:require [clojure.test :refer :all] 3 | [toadie.named-params :as nparams])) 4 | 5 | (deftest to-statement 6 | (testing "should do nothing for simple statement" 7 | (is (= ["select * from people"] (nparams/to-statement "select * from people")))) 8 | (testing "should be fine with an empty map" 9 | (is (= ["select * from people"] (nparams/to-statement "select * from people" {:name "michal"})))) 10 | (testing "should convert named parameter" 11 | (let [s (nparams/to-statement "select * from people where name = @:name" {:name "mike"})] 12 | (is (= ["select * from people where name = ?" "mike"] s)))) 13 | (testing "should convert multiple params in order" 14 | (let [s (nparams/to-statement "select * from people where name = @:name and surname = @:surname" {:name "mike" :surname "none"})] 15 | (is (= ["select * from people where name = ? and surname = ?" "mike" "none"] s)))) 16 | (testing "should convert multiple params in order even if duplicated" 17 | (let [s (nparams/to-statement "select * from people where name = @:name and surname = @:surname and child_name = @:name" {:name "mike" :surname "none"})] 18 | (is (= ["select * from people where name = ? and surname = ? and child_name = ?" "mike" "none" "mike"] s)))) 19 | (testing "should avoid casted elements" 20 | (let [s (nparams/to-statement "select * from people where age::int = @:age" {:age 23})] 21 | (is (= ["select * from people where age::int = ?" 23] s))))) 22 | -------------------------------------------------------------------------------- /test/toadie/advanced_query_test.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.advanced-query-test 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (def people-multi-search 13 | [{:name "m1" :age 12 :height 1.86 :member-since (c/to-date (time/date-time 1982 11 30))}, 14 | {:name "m2" :age 13 :height 1.74 :member-since (c/to-date (time/date-time 1982 11 30))} 15 | {:name "m3" :age 14 :height 2.06}]) 16 | 17 | (deftest multi-where 18 | (let [_ (toadie/save test-store :people people-multi-search)] 19 | (testing "can have :and queries" 20 | (let [result (toadie/query test-store :people {:where [[:> :age 12] :and [:> :height 2.00]]})] 21 | (is (= (count result) 1)) 22 | (is (= (:name (first result) "m3"))))) 23 | (testing "can have multiple :and queries" 24 | (let [res (toadie/query test-store :people {:where [[:= :age 12] :or [:= :age 13] :or [:= :age 14]]})] 25 | (is (= (count res) 3)))) 26 | (testing "can have nested queries" 27 | (let [result (toadie/query test-store :people {:where [[[:like :name "m%"] :or [:> :age 12]] :and [:> :height 1.80]]})] 28 | (is (= (count result) 2)))) 29 | (testing "super nested one" 30 | (let [w [[[:= :name "m1"] :or [:= :name "m2"] :or [:> :height 2.0]] :and [:>= :age 13]] 31 | res (toadie/query test-store :people {:where w})] 32 | (is (= (count res) 2)))))) 33 | -------------------------------------------------------------------------------- /test/toadie/query_test.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.query-test 2 | (:require [clojure.test :refer :all] 3 | [toadie.core :as toadie] 4 | [clojure.java.jdbc :as sql] 5 | [clj-time.core :as time] 6 | [clj-time.coerce :as c] 7 | [environ.core :refer [env]] 8 | [toadie.test-helpers :refer :all])) 9 | 10 | (use-fixtures :each drop-tables) 11 | 12 | (deftest load-by-id 13 | (let [maria (toadie/save test-store :people {:name "maria" :age 56}) 14 | michal (toadie/save test-store :people {:name "michal" :age 34})] 15 | (testing "should be able to retrive by id" 16 | (let [result (toadie/load-by-id test-store :people (:id maria))] 17 | (is (= (:name result) "maria")))))) 18 | 19 | 20 | (deftest limit-and-offset-search 21 | (let [m1 (toadie/save test-store :people {:name "m1"}) 22 | m2 (toadie/save test-store :people {:name "m2"}) 23 | m3 (toadie/save test-store :people {:name "m3"})] 24 | (testing "with limit" 25 | (let [result (toadie/query test-store :people {:limit 1})] 26 | (is (= (count result) 1))) 27 | (let [result (toadie/query test-store :people {:limit 2})] 28 | (is (= (count result) 2)))) 29 | (testing "offset" 30 | (let [result (toadie/query test-store :people {:offset 3})] 31 | (is (= (count result) 0))) 32 | (let [result (toadie/query test-store :people {:offset 1})] 33 | (is (= (count result) 2)))))) 34 | 35 | (deftest simple-search 36 | (let [m1 (toadie/save test-store :people {:name "m1" :age 12 :height 1.86 :member-since (c/to-date (time/date-time 1982 11 30))}) 37 | m2 (toadie/save test-store :people {:name "m2" :age 13 :height 1.74 :member-since (c/to-date (time/date-time 1982 11 30))}) 38 | m3 (toadie/save test-store :people {:name "m3" :age 14 :height 2.06})] 39 | (testing "where property equals string" 40 | (let [result (toadie/query test-store :people {:where [:= :name "m1"]})] 41 | (is (= (count result) 1)) 42 | (is (= (:name (first result)) "m1")))) 43 | (testing "where property equals a value" 44 | (let [result (toadie/query test-store :people {:where [:= :age 13]})] 45 | (is (= (count result)) 1) 46 | (is (= (:name (first result)) "m2")))) 47 | (testing "integer comparison" 48 | (let [result (toadie/query test-store :people {:where [:> :age 13]})] 49 | (is (= (count result)) 1) 50 | (is (= (:name (first result)) "m3"))) 51 | (let [result (toadie/query test-store :people {:where [:>= :age 12]})] 52 | (is (= (count result)) 3)) 53 | (let [result (toadie/query test-store :people {:where [:< :age 14]})] 54 | (is (= (count result)) 2))) 55 | (testing "double comparison" 56 | (let [result (toadie/query test-store :people {:where [:> :height 1.90]})] 57 | (is (= (count result) 1)) 58 | (is (= (:name (first result) "m3"))))))) 59 | 60 | (deftest like-search 61 | (let [m1 (toadie/save test-store :people {:name "maria"}) 62 | m2 (toadie/save test-store :people {:name "john"})] 63 | (testing "where property like string" 64 | (let [result (toadie/query test-store :people {:where [:like :name "ma%"]})] 65 | (is (= (count result) 1)) 66 | (is (= (:name (first result)) "maria")))))) 67 | 68 | (deftest contains-condition 69 | (let [t1 (toadie/save test-store :posts {:title "t1" :tags ["web" "testing"]}) 70 | t2 (toadie/save test-store :posts {:title "t2" :tags []}) 71 | t3 (toadie/save test-store :posts {:title "t3" :tags ["clojure" "parinfer"]})] 72 | (testing "contains single array" 73 | (let [result (toadie/query test-store :posts {:where [:contains {:tags ["clojure"]}]})] 74 | (is (= (count result) 1)) 75 | (is (= (:title (first result)) "t3")))) 76 | (testing "contains all array elements" 77 | (let [result (toadie/query test-store :posts {:where [:contains {:tags ["clojure" "web"]}]})] 78 | (is (= (count result) 0))) 79 | (let [result (toadie/query test-store :posts {:where [:contains {:tags ["testing" "web"]}]})] 80 | (is (= (count result) 1)) 81 | (is (= (:title (first result) "t1"))))))) 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toadie 2 | 3 | A Clojure library designed to let you work with PostgreSQL as a document store! 4 | 5 | ## Why 6 | 7 | Since version 9.4 Postres allows for storing, querying and indexing on json column types. This allows to easily persist clojure's map in database. 8 | 9 | This concept is not new, take a look at Elixir's Moebius (https://github.com/robconery/moebius). Especially part with JSONB support. 10 | 11 | ##Geting stared 12 | 13 | Toadie artifacts are [deployed to clojars] (https://clojars.org/toadie) 14 | 15 | With Leiningen: 16 | 17 | [toadie "0.3.0"] 18 | 19 | With Gradle: 20 | 21 | compile "toadie:toadie:0.3.0" 22 | 23 | With Maven: 24 | 25 | 26 | toadie 27 | toadie 28 | 0.3.0 29 | 30 | 31 | We get started by calling _docstore_ function: 32 | 33 | ``` clojure 34 | (use '[toadie.core :as toadie]) 35 | 36 | ;pass connection string to docstore method 37 | (def db (toadie/docstore conn-str)) 38 | 39 | ;initilize with map 40 | (def db (toadie/docstore { 41 | :serialize (fn [x] (to-json x)) 42 | :deserialize (fn [x] (from-json x)) 43 | :classname "org.postgresql.Driver" 44 | :subprotocol "postgresql" 45 | :subname "localhost:1234" 46 | :user "a_user" 47 | :password "secret"})) 48 | ``` 49 | 50 | Docstore' map can be created with connection string or map that is further on passed when initializing connection to clojure.java.jdbc. Optionally, docstore's map can contain _serialize_ and _deserialize_ functions that are used to convert between clojure's map and it's json representation. Toadie uses Cheshire by default. 51 | 52 | ## Inserting/Update 53 | 54 | ``` clojure 55 | 56 | ;insert map into :people collection 57 | (toadie/save db :people {:name "maria" :surname "johnson" :age 42}) 58 | user=> 59 | {:age 42, 60 | :name "maria", 61 | :surname "johnson", 62 | :id "26ecbf13-4628-430e-b998-6022db44b334"} 63 | 64 | ;insert vector of maps 65 | (toadie/save db :people [{:name "michal"} {:name "marcelina"}]) 66 | user=> 67 | ({:name "michal", :id "f5ec1d0b-6f13-433f-b8c5-f9643231f1ff"} 68 | {:name "marcelina", :id "57107e40-e0d6-4146-9d3d-a16912add53f"}) 69 | 70 | ;update map by passing :id key 71 | (toadie/save db :people {:name "maria" :surname "johnson" :age 43, :id "26ecbf13-4628-430e-b998-6022db44b334"}) 72 | ``` 73 | 74 | At first call to _save_ will create destination table. _save_ returns map with assoc id key from database. As from the example above, function works with both maps and vectors of maps. 75 | 76 | When id key is present in map, record is going to be updated, instead of being inserted. 77 | 78 | Toadie supports batch inserts. Those use Postgres Copy functionality: 79 | ``` clojure 80 | (toadie/batch-insert db :people [{:name "maria" :surname "johnson" :age 43, :id "26ecbf13-4628-430e-b998-6022db44b334"}{:name "other"}]) 81 | ``` 82 | 83 | When testing on local machine, batch insert via 'copy' turned out to be 100x faster than 'insert' one by one :) 84 | 85 | ## Querying 86 | 87 | Querying is best explained by examples: 88 | 89 | ``` clojure 90 | 91 | ;quey :people collection taking just 3 entities 92 | (toadie/query db :people {:limit 3}) 93 | 94 | ;quey :people collection skiping first 10 results and taking just 4 entities 95 | (toadie/query db :people {:limit 4 offset 10}) 96 | 97 | ;query :people collection where :name equals "m1" 98 | (toadie/query db :people {:where [:= :name "m1"]}) 99 | 100 | ;query :people collection where age is > 13 101 | (toadie/query db :people {:where [:> :age 13]}) 102 | 103 | ;query :people with name starting with "ma" 104 | (toadie/query db :people {:where [:like :name "ma%"]}) 105 | 106 | ;query :posts collection where any of tags is "clojure" 107 | (toadie/query db :posts {:where [:contains {:tags ["clojure"]}]}) 108 | 109 | ;query :posts collection where posts' tags are "clojure" and "web" 110 | (toadie/query db :posts {:where [:contains {:tags ["clojure" "web"]}]}) 111 | 112 | ;multiple where conditions with :and and :or 113 | (toadie/query db :people {:where [[[:like :name "m%"] :or [:> :age 12]] :and [:> :height 1.80]]}) 114 | 115 | ;where with nested conditions 116 | (toadie/query db :people {:where [[[:= :name "m1"] :or [:= :name "m2"] :or [:> :height 2.0]] :and [:>= :age 13]]}) 117 | 118 | ;and so on, and so on 119 | ``` 120 | 121 | ## Delete 122 | 123 | Currently only delete-by-id is supported 124 | 125 | ``` clojure 126 | (toadie/delete-by-id db :people (:id "26ecbf13-4628-430e-b998-6022db44b334")) 127 | ``` 128 | 129 | # Change Log 130 | 131 | ## [0.3.0] - 2016-07-27 132 | ### Added 133 | - toadie now supports batch-insert 134 | 135 | ## License 136 | 137 | The MIT License (MIT) 138 | -------------------------------------------------------------------------------- /src/toadie/core.clj: -------------------------------------------------------------------------------- 1 | (ns toadie.core 2 | (require [clojure.java.jdbc :as sql] 3 | [toadie.utils :refer :all] 4 | [toadie.named-params :as nparams] 5 | [clojure.core :refer :all] 6 | [clojure.string :as string] 7 | [cheshire.core :as json] 8 | [clojure.data.csv :as csv]) 9 | (:import (org.postgresql.util PGobject)) 10 | (:import org.postgresql.copy.CopyManager) 11 | (:import (java.io StringWriter StringReader))) 12 | 13 | (defn sql-create-table [name] 14 | (str "create table " name "(id uuid primary key not null,body jsonb not null);")) 15 | 16 | (defn sql-create-json-index [table-name] 17 | (str "create index idx_" table-name " on " table-name " using GIN(body jsonb_path_ops);")) 18 | 19 | (defn sql-load-doc [table-name id] 20 | [(str "select * from " table-name " where id = ?") (uuid id)]) 21 | 22 | (defn serialize [s] 23 | (json/generate-string s)) 24 | 25 | (defn deserialize [s] 26 | (json/parse-string s true)) 27 | 28 | (defn to-pg-jsonb-value [str-val] 29 | (doto 30 | (PGobject.) 31 | (.setType "jsonb") 32 | (.setValue str-val))) 33 | 34 | (defn setup [serialize-json deserialize-json] 35 | (extend-protocol sql/ISQLValue 36 | clojure.lang.IPersistentMap 37 | (sql-value [value] (to-pg-jsonb-value (serialize-json value))) 38 | clojure.lang.IPersistentVector 39 | (sql-value [value] (to-pg-jsonb-value (serialize-json value)))) 40 | 41 | (extend-protocol sql/IResultSetReadColumn 42 | PGobject 43 | (result-set-read-column [pgobj _metadata _index] 44 | (let [type (.getType pgobj) 45 | value (.getValue pgobj)] 46 | (if (= type "jsonb") 47 | (deserialize-json value) 48 | value))))) 49 | 50 | (setup serialize deserialize) 51 | 52 | (defn docstore [ps] 53 | "Sets up a doc store" 54 | (let [defaults {:serialize serialize :deserialize deserialize} 55 | sett (if (string? ps) {:db-spec ps} ps) 56 | merged (merge defaults sett)] 57 | (setup (:serialize merged) (:deserialize merged)) 58 | merged)) 59 | 60 | (defn- create-table [db name] 61 | (sql/db-do-commands (:db-spec db) (sql-create-table name)) 62 | (sql/db-do-commands (:db-spec db) (sql-create-json-index name))) 63 | 64 | (defn row-data-to-map [d] 65 | (map #(:body %) d)) 66 | 67 | (defn- save-single [db n data] 68 | (try 69 | (-> 70 | (cond 71 | (:id data) (clojure.java.jdbc/query (:db-spec db) [(str "Update " (name n) " set body = ? where id = ? returning *") data (uuid (:id data))]) 72 | :else (let [uuid (uuid)] 73 | (sql/insert! (:db-spec db) n {:id uuid :body (assoc data :id uuid)}))) 74 | (row-data-to-map) 75 | (first)) 76 | (catch java.sql.SQLException e 77 | ;(sql/print-sql-exception e) 78 | (create-table db (name n)) 79 | (save-single db n data)) 80 | (catch Exception e 81 | ;(println (.toString e)) 82 | (throw e)))) 83 | 84 | (defn save [db n data] 85 | (if (vector? data) 86 | (doall (map #(save-single db n %) data)) 87 | (save-single db n data))) 88 | 89 | (defn to-reader [db data] 90 | (let [data-with-ids (map #(assoc % :id (uuid)) data) 91 | els (map #(vector (str (:id %)) ((:serialize db) %)) data-with-ids) 92 | sw (StringWriter.) 93 | writer (csv/write-csv sw els)] 94 | (StringReader. (.toString sw)))) 95 | 96 | (defn batch-insert [db n data] 97 | (try 98 | (let [rec (to-reader db data) 99 | conn (sql/get-connection (:db-spec db)) 100 | man (CopyManager. conn)] 101 | (.copyIn man (str "COPY " (name n) " from STDIN with (format csv)") rec)) 102 | (catch java.sql.SQLException e 103 | ;(sql/print-sql-exception e) 104 | (create-table db (name n)) 105 | (batch-insert db n data)))) 106 | 107 | (defn raw-query [db query] 108 | (try 109 | (row-data-to-map (sql/query (:db-spec db) query)) 110 | (catch Exception e 111 | (throw e)))) 112 | 113 | (defn load-by-id [db n id] 114 | (try 115 | (-> 116 | (sql/query (:db-spec db) (sql-load-doc (name n) id)) 117 | (row-data-to-map) 118 | (first)) 119 | (catch Exception e 120 | (create-table db (name n)) 121 | (load db n id)))) 122 | 123 | (defn delete-by-id [db n id] 124 | (try 125 | (-> 126 | (sql/delete! (:db-spec db) n ["id = ?" (uuid id)])) 127 | (catch Exception e 128 | (throw e)))) 129 | 130 | (defn- limit-sql [query] 131 | (if-let [limit (:limit query)] 132 | (str "limit " limit))) 133 | 134 | (defn- offset-sql [query] 135 | (if-let [offset (:offset query)] 136 | (str "offset " offset))) 137 | 138 | (defn- select-sql [col] 139 | (str "select * from " (name col))) 140 | 141 | (defn to-sql-value [val] 142 | (condp instance? val 143 | String (str "'" val "'") 144 | val)) 145 | 146 | (defn to-db-type-cast [val] 147 | (condp instance? val 148 | Long "::bigint" 149 | Integer "::bigint" 150 | Double "::numeric" 151 | "")) 152 | 153 | (defn- where-compop-sql [compop path value] 154 | (let [pname (random-string)] 155 | [(str "(body->>'" (name path) "')" (to-db-type-cast value) " " (name compop) " @:" pname) {(keyword pname) value}])) 156 | 157 | (defn- where-contains-sql [db val] 158 | (let [pname (random-string) 159 | js ((:serialize db) val)] 160 | [(str "body @> '" js "'")])) 161 | 162 | (defn- where-sql-simple [db part-where] 163 | (let [op (first part-where)] 164 | (cond 165 | (some #{op} [:> :>= := :<= :< :like]) (where-compop-sql op (second part-where) (last part-where)) 166 | (= op :contains) (where-contains-sql db (second part-where)) 167 | :else (throw (Exception. (str "Provided condition:" (name op) " is not supported.")))))) 168 | 169 | (defn where-sql [db where] 170 | (if 171 | (vector? (first where)) 172 | (let [m (map-indexed (fn [idx item] (if (even? idx) (where-sql db item) [(name item) {}])) where) 173 | r (reduce (fn [[sj ps] [s p]] [(str sj " " s) (merge ps p)]) (vec m))] 174 | (let [[q ps] r] 175 | [(str "(" q ")") ps])) 176 | (where-sql-simple db where))) 177 | 178 | (defn to-sql [db col q] 179 | (if-let [where (:where q)] 180 | (let [[where-s where-ps] (where-sql db where)] 181 | [(str (select-sql col) " where " where-s " " (offset-sql q) " " (limit-sql q)) where-ps]) 182 | [(str (select-sql col) " " (offset-sql q) " " (limit-sql q))])) 183 | 184 | (defn query [db col q] 185 | (let [[q-sql q-params] (to-sql db col q) 186 | parsed (nparams/to-statement q-sql q-params)] 187 | (raw-query db parsed))) 188 | --------------------------------------------------------------------------------