├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── intro.md ├── project.clj ├── src └── conman │ └── core.clj └── test ├── conman ├── core2_test.clj └── core_test.clj └── queries.sql /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | docker: 5 | - image: clojure:lein 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | key: << checksum "project.clj" >> 10 | - run: lein test 11 | - save_cache: 12 | paths: 13 | - $HOME/.m2 14 | key: << checksum "project.clj" >> 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | 14 | .idea/ 15 | *.iml 16 | 17 | *.db 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.6 2 | - breaking change, Java 8 is no longer supported 3 | 4 | ## 0.9.1 5 | 6 | - bumped up dependencies 7 | - next.jdbc 1.1.613 8 | - hikari-cp 2.13.0 9 | 10 | ## 0.9.0 11 | - bumped up next.jdbc to 1.1.547 12 | 13 | ## 0.8.9 14 | 15 | - [fix for arglists metadata for generated functions](https://github.com/luminus-framework/conman/pull/71) 16 | 17 | ## 0.8.8 18 | 19 | - hikari-cp 2.12.0 20 | 21 | ## 0.8.7 22 | 23 | - fix for disconnect! 24 | 25 | ## 0.8.6 26 | 27 | * improved exception messages 28 | 29 | ## 0.8.5 30 | 31 | * updated dependency versions, switched to use next.jdbc 32 | 33 | ## 0.8.4 34 | 35 | * updated dependency versions 36 | 37 | ## 0.8.1 38 | 39 | * added `bind-connection-map` that returns a map of functions without interning them. 40 | 41 | ## 0.4.0 42 | 43 | * Switch to use [HugSQL](http://www.hugsql.org/) for managing the queries 44 | * no longer relies on an atom to manage the connection 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conman 2 | 3 | ![status](https://circleci.com/gh/luminus-framework/conman.svg?style=shield&circle-token=da462d56802b9a0ff94ecc7992d1d0caf8475d00) 4 | 5 | Luminus database connection management and SQL query generation library 6 | 7 | The library provides pooled connections using the [HikariCP](https://github.com/brettwooldridge/HikariCP) library. 8 | 9 | The queries are generated using [HugSQL](https://github.com/layerware/hugsql) and wrapped with 10 | connection aware functions. 11 | 12 | ## Usage 13 | 14 | [![Clojars Project](http://clojars.org/conman/latest-version.svg)](http://clojars.org/conman) 15 | 16 | Conman relies on a dynamic variable to manage the connection. The dynamic variable allows the connection to be 17 | rebound contextually for transactions. When working with multiple databases, a separate var is required 18 | to track each database connection. 19 | 20 | ### Defining Queries 21 | 22 | SQL statements should be populated in files that are accessible on the resource path. 23 | For example, we could create a file `resources/sql/queries.sql` with 24 | the following content: 25 | 26 | ``` sql 27 | -- :name create-user! :! :n 28 | -- :doc creates a new user record 29 | INSERT INTO users 30 | (id, first_name, last_name, email, pass) 31 | VALUES (:id, :first_name, :last_name, :email, :pass) 32 | 33 | -- :name get-user :? :1 34 | -- :doc retrieve a user given the id. 35 | SELECT * FROM users 36 | WHERE id = :id 37 | 38 | -- :name get-all-users :? :* 39 | -- :doc retrieve all users. 40 | SELECT * FROM users 41 | ``` 42 | See the official [HugSQL docs](http://www.hugsql.org/) for further examples. 43 | 44 | ### Using `bind-connection` 45 | 46 | The queries are bound to the connection using the `bind-connection` macro. This macro 47 | accepts the connection var followed by one or more strings representing SQL query files. 48 | 49 | The lifecycle of the connection is expected to be managed using a library such as [mount](https://github.com/tolitius/mount). The full list of options that can be passed to `pool-spec` can be found [here](https://github.com/tomekw/hikari-cp#configuration-options). 50 | 51 | 52 | ```clojure 53 | (ns myapp.db 54 | (:require [mount.core :refer [defstate]] 55 | [conman.core :as conman])) 56 | 57 | (def pool-spec 58 | {:jdbc-url "jdbc:postgresql://localhost/myapp?user=user&password=pass"}) 59 | 60 | (defstate ^:dynamic *db* 61 | :start (conman/connect! pool-spec) 62 | :stop (conman/disconnect! *db*)) 63 | 64 | (conman/bind-connection *db* "sql/queries.sql") 65 | ``` 66 | 67 | The `bind-connection` generates `create-user!` and `get-user` functions 68 | in the current namespace. These functions can be called in four different ways: 69 | 70 | ```clojure 71 | ;; when called with no argument then the HugSQL generated function 72 | ;; will be called with an empty parameter map and the connection specified in the *db* var 73 | (get-all-users) 74 | 75 | ;; when a parameter map is passed as the argument, then the map and the connection specified 76 | ;; in the *db* var will be passed to the HugSQL generated function 77 | (create-user! {:id "foo" :first_name "Bob" :last_name "Bobberton" :email nil :pass nil}) 78 | 79 | ;; an explicit connection and a parameter map can be 80 | ;; passed to the function 81 | (get-user some-other-conn {:id "foo"}) 82 | 83 | ;; finally, an explicit connection and a parameter map, options, and optional command options 84 | ;; can be passed to the function 85 | (get-user some-other-conn {:id "foo"} opts) 86 | (get-user some-other-conn {:id "foo"} opts cmd-opt1 cmd-opt2) 87 | ``` 88 | 89 | ### Using `bind-connection-map` 90 | 91 | Alternatively, you may wish to use `bind-connection-map` to define the queries. This function will return 92 | a map containing `:snips` and `:fns` keys that point to maps of snippets and queries respectively. 93 | The `snip` and `query` helper functions are provided for accessing the connection map returned by the `bind-connection-map` 94 | function. 95 | 96 | ```clojure 97 | ;; create a connection map given the connection instance and one or more query files: 98 | (def queries (bind-connection-map conn "queries.sql")) 99 | 100 | ;; a HugSQL options map can be passed as the second argument: 101 | (def queries (bind-connection-map conn {:quoting :ansi} "queries.sql")) 102 | 103 | ;; run a query 104 | (query 105 | queries 106 | :add-fruit! 107 | {:name "apple" 108 | :appearance "red" 109 | :cost 1 110 | :grade 1}) 111 | 112 | ;; run a query specifying the connection explicitly 113 | (query 114 | conn 115 | queries 116 | :add-fruit! 117 | {:name "apple" 118 | :appearance "red" 119 | :cost 1 120 | :grade 1}) 121 | 122 | ;; run a query with a snippet 123 | (query 124 | conn 125 | queries 126 | :get-fruit-by 127 | {:by-appearance 128 | (snip queries :by-appearance {:appearance "red"})}) 129 | ``` 130 | 131 | If you use `:cljc` [mode of mount](https://github.com/tolitius/mount/blob/master/doc/clojurescript.md#clojure-and-clojurescript-mode), 132 | where you would need to explicitly `deref` each state, you'll need to use `conman/bind-connection-deref` instead of 133 | `conman/bind-connection`. Both `conman/bind-connection-deref` and `conman/with-transaction` macros still accept undereferenced state, 134 | while in every other case it has to be dereferenced: 135 | 136 | ```clojure 137 | (mount/in-cljc-mode) 138 | 139 | (conman/bind-connection-deref *db* "sql/queries.sql") 140 | 141 | (conman/with-transaction [*db*] 142 | (sql/db-set-rollback-only! @*db*) 143 | (add-memo! {:id 123 :text "Hello"}) 144 | (sql/query @*db* ["SELECT * FROM memos;"])) 145 | 146 | ``` 147 | 148 | Next, the `connect!` function should be called to initialize the database connection. 149 | The function accepts a map with the database specification. 150 | 151 | ```clojure 152 | (def pool-spec 153 | {:jdbc-url "jdbc:postgresql://localhost/myapp?user=user&password=pass"}) 154 | 155 | (connect! pool-spec) 156 | ``` 157 | 158 | For the complete list of configuration options refer to the official [hikari-cp](https://github.com/tomekw/hikari-cp) library documentation. Conman supports the following additional options: 159 | 160 | * `:datasource` 161 | * `:datasource-classname` 162 | 163 | The connection can be terminated by running the `disconnect!` function: 164 | 165 | ```clojure 166 | (disconnect! conn) 167 | ``` 168 | 169 | A connection can be reset using the `reconnect!` function: 170 | 171 | ```clojure 172 | (reconnect! conn pool-spec) 173 | ``` 174 | 175 | When using a dynamic connection, it's possible to use the `with-transaction` 176 | macro to rebind it to the transaction connection. The SQL query functions 177 | generated by the `bind-connection` macro will automatically use the transaction 178 | connection in that case: 179 | 180 | ```clojure 181 | (with-transaction [conn {:rollback-only true}] 182 | (create-user! 183 | {:id "foo" 184 | :first_name "Sam" 185 | :last_name "Smith" 186 | :email "sam.smith@example.com"}) 187 | (get-user {:id "foo"})) 188 | ``` 189 | 190 | The isolation level and readonly status of the transaction may be specified using the `:isolation` 191 | and `:read-only?` keys respectively: 192 | 193 | ```clojure 194 | (with-transaction 195 | [conn {:isolation :serializable}] 196 | (= java.sql.Connection/TRANSACTION_SERIALIZABLE 197 | (.getTransactionIsolation (sql/db-connection conn)))) 198 | (with-transaction 199 | [conn {:isolation :read-uncommitted}] 200 | (= java.sql.Connection/TRANSACTION_READ_UNCOMMITTED 201 | (.getTransactionIsolation (sql/db-connection conn)))) 202 | ``` 203 | 204 | ## License 205 | 206 | Copyright © 2015 Dmitri Sotnikov and Carousel Apps Ltd. 207 | 208 | Distributed under the Eclipse Public License either version 1.0 or (at 209 | your option) any later version. 210 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to luminus-db 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject conman "0.9.6" 2 | :description "a database connection management library" 3 | :url "https://github.com/luminus-framework/conman" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.11.1"] 7 | [com.layerware/hugsql-core "0.5.3"] 8 | [com.layerware/hugsql-adapter-next-jdbc "0.5.3"] 9 | [com.carouselapps/to-jdbc-uri "0.5.0"] 10 | [com.github.seancorfield/next.jdbc "1.2.796"] 11 | [hikari-cp "3.0.1"]] 12 | :profiles 13 | {:dev 14 | {:dependencies [[com.h2database/h2 "2.1.214"] 15 | [mount "0.1.16"]]}}) 16 | -------------------------------------------------------------------------------- /src/conman/core.clj: -------------------------------------------------------------------------------- 1 | (ns conman.core 2 | (:require [clojure.java.io :as io] 3 | [clojure.set :refer [rename-keys]] 4 | [hikari-cp.core :refer [make-datasource]] 5 | [hugsql.core :as hugsql] 6 | [hugsql.adapter.next-jdbc :as next-adapter] 7 | [to-jdbc-uri.core :refer [to-jdbc-uri]]) 8 | (:import [clojure.lang IDeref])) 9 | 10 | (hugsql/set-adapter! (next-adapter/hugsql-adapter-next-jdbc)) 11 | 12 | (defn validate-files [filenames] 13 | (doseq [file filenames] 14 | (when-not (or (instance? java.io.File file) (io/resource file)) 15 | (throw (Exception. (str "conman could not find the query file:" file)))))) 16 | 17 | (defn try-snip [[id snip]] 18 | [id 19 | (update snip :fn 20 | (fn [snip] 21 | (fn [& args] 22 | (try (apply snip args) 23 | (catch Exception e 24 | (throw (ex-info (ex-message e) {:snip-id id} e)))))))]) 25 | 26 | (defn try-query [[id query]] 27 | [id 28 | (update query :fn 29 | (fn [query] 30 | (fn 31 | ([conn params] 32 | (try (query conn params) 33 | (catch Exception e 34 | (throw (ex-info (ex-message e) {:query-id id} e))))) 35 | ([conn params opts & command-opts] 36 | (try (apply query conn params opts command-opts) 37 | (catch Exception e 38 | (throw (ex-info (ex-message e) {:query-id id} e))))))))]) 39 | 40 | (defn load-queries [& args] 41 | (let [options? (map? (first args)) 42 | options (if options? (first args) {}) 43 | filenames (if options? (rest args) args)] 44 | (validate-files filenames) 45 | (reduce 46 | (fn [queries file] 47 | (let [{snips true 48 | fns false} 49 | (group-by 50 | #(-> % second :meta :snip? boolean) 51 | (hugsql/map-of-db-fns file options))] 52 | (-> queries 53 | (update :snips (fnil into {}) (mapv try-snip snips)) 54 | (update :fns (fnil into {}) (mapv try-query fns))))) 55 | {} 56 | filenames))) 57 | 58 | (defn intern-fn [ns id meta f] 59 | (intern ns (with-meta (symbol (name id)) meta) f)) 60 | 61 | (defmacro bind-connection [conn & filenames] 62 | `(let [{snips# :snips fns# :fns :as queries#} (conman.core/load-queries ~@filenames)] 63 | (doseq [[id# {fn# :fn meta# :meta}] snips#] 64 | (conman.core/intern-fn *ns* id# meta# fn#)) 65 | (doseq [[id# {query# :fn meta# :meta}] fns#] 66 | (conman.core/intern-fn *ns* id# 67 | ;; Need to explicitly set :arglists since we don't use defn. 68 | ;; Another option would be to generate defns. 69 | (assoc meta# 70 | :arglists (quote ~'([] [params] [db params options & command-options]))) 71 | (fn f# 72 | ([] (query# ~conn {})) 73 | ([params#] (query# ~conn params#)) 74 | ([conn# params# & args#] (apply query# conn# params# args#))))) 75 | queries#)) 76 | 77 | (defmacro bind-connection-deref [conn & filenames] 78 | `(let [{snips# :snips fns# :fns :as queries#} (conman.core/load-queries ~@filenames)] 79 | (doseq [[id# {fn# :fn meta# :meta}] snips#] 80 | (conman.core/intern-fn *ns* id# meta# fn#)) 81 | (doseq [[id# {query# :fn meta# :meta}] fns#] 82 | (conman.core/intern-fn *ns* id# 83 | (assoc meta# 84 | :arglists (quote ~'([] [params] [db params options & command-options]))) 85 | (fn f# 86 | ([] (query# (deref ~conn) {})) 87 | ([params#] (query# (deref ~conn) params#)) 88 | ([conn# params# & args#] (apply query# conn# params# args#))))) 89 | queries#)) 90 | 91 | (defn bind-connection-map [conn & args] 92 | (-> (apply load-queries args) 93 | (update :snips 94 | (fn [snips] 95 | (reduce (fn [acc [id snip]] (assoc acc id snip)) {} snips))) 96 | (update :fns 97 | (fn [queries] 98 | (reduce 99 | (fn [acc [id query]] 100 | (assoc acc id 101 | (update query 102 | :fn 103 | (fn [query] 104 | (fn fn# 105 | ([] (query conn {})) 106 | ([params] 107 | (query conn params)) 108 | ([conn params & args] (apply query conn params args))))))) 109 | {} 110 | queries))))) 111 | 112 | (defn find-fn [connection-map query-type k] 113 | (or (get-in connection-map [query-type k :fn]) 114 | (throw (IllegalArgumentException. 115 | (str (if (= query-type :snips) "no snippet" "no query") 116 | " found for the key: " k 117 | "', available queries: " (keys (get connection-map query-type))))))) 118 | 119 | (defn snip [connection-map snip-key & args] 120 | "runs a SQL query snippet 121 | queries - a map of queries 122 | id - keyword indicating the name of the query 123 | args - arguments that will be passed to the query" 124 | (apply (find-fn connection-map :snips snip-key) args)) 125 | 126 | (defn query 127 | "runs a database query and returns the result 128 | conn - database connection 129 | queries - a map of queries 130 | id - keyword indicating the name of the query 131 | args - arguments that will be passed to the query" 132 | ([connection-map query-key] 133 | ((find-fn connection-map :fns query-key))) 134 | ([connection-map query-key params] 135 | ((find-fn connection-map :fns query-key) params)) 136 | ([conn connection-map query-key params & opts] 137 | (apply (find-fn connection-map :fns query-key) conn params opts))) 138 | 139 | (defn- format-url [pool-spec] 140 | (if (:jdbc-url pool-spec) 141 | (update pool-spec :jdbc-url to-jdbc-uri) 142 | pool-spec)) 143 | 144 | (defn make-config [{:keys [jdbc-url adapter datasource datasource-classname] :as pool-spec}] 145 | (when (not (or jdbc-url adapter datasource datasource-classname)) 146 | (throw (Exception. "one of :jdbc-url, :adapter, :datasource, or :datasource-classname is required to initialize the connection!"))) 147 | (-> pool-spec 148 | (format-url) 149 | (rename-keys 150 | {:auto-commit? :auto-commit 151 | :conn-timeout :connection-timeout 152 | :min-idle :minimum-idle 153 | :max-pool-size :maximum-pool-size}))) 154 | 155 | (defn connect! 156 | "attempts to create a new connection and set it as the value of the conn atom, 157 | does nothing if conn atom is already populated" 158 | [pool-spec] 159 | (make-datasource (make-config pool-spec))) 160 | 161 | (defn disconnect! 162 | "checks if there's a connection and closes it 163 | resets the conn to nil" 164 | [conn] 165 | (when (and (instance? com.zaxxer.hikari.HikariDataSource conn) 166 | (not (.isClosed conn))) 167 | (.close conn))) 168 | 169 | (defn reconnect! 170 | "calls disconnect! to ensure the connection is closed 171 | then calls connect! to establish a new connection" 172 | [conn pool-spec] 173 | (disconnect! conn) 174 | (connect! pool-spec)) 175 | 176 | (extend-protocol next.jdbc.protocols/Sourceable 177 | IDeref 178 | (get-datasource [this] 179 | (next.jdbc.protocols/get-datasource (deref this)))) 180 | 181 | (defmacro with-transaction 182 | "Runs the body in a transaction where t-conn is the name of the transaction connection. 183 | The body will be evaluated within a binding where conn is set to the transactional 184 | connection. The isolation level and readonly status of the transaction may also be specified. 185 | (with-transaction [conn {:isolation level :read-only? true}] 186 | ... t-conn ...) 187 | See next.jdbc/transact for more details on the semantics of the :isolation and 188 | :read-only options." 189 | [[dbsym & opts] & body] 190 | `(if (instance? IDeref ~dbsym) 191 | (next.jdbc/with-transaction [t-conn# (deref ~dbsym) ~@opts] 192 | (binding [~dbsym (delay t-conn#)] 193 | ~@body)) 194 | (next.jdbc/with-transaction [t-conn# ~dbsym ~@opts] 195 | (binding [~dbsym t-conn#] 196 | ~@body)))) 197 | -------------------------------------------------------------------------------- /test/conman/core2_test.clj: -------------------------------------------------------------------------------- 1 | ;; A subset of conman.core-test to test the case when `conn` is an IDeref 2 | (ns conman.core2-test 3 | (:require [clojure.test :refer :all] 4 | [conman.core :refer :all] 5 | [next.jdbc :as jdbc] 6 | [clojure.java.io :as io] 7 | [mount.core :as m])) 8 | 9 | (m/defstate ^:dynamic conn2 10 | :start {:jdbcUrl "jdbc:h2:./test.db" 11 | :make-pool? true 12 | :naming {:keys clojure.string/lower-case 13 | :fields clojure.string/upper-case}}) 14 | 15 | (bind-connection-deref conn2 "queries.sql") 16 | 17 | (defn delete-test-db [] 18 | (io/delete-file "test.db.mv.db" true) 19 | (io/delete-file "test.db.trace.db" true)) 20 | 21 | (defn create-test-table [] 22 | (jdbc/execute! 23 | conn2 24 | ["DROP TABLE fruits IF EXISTS; 25 | CREATE TABLE fruits ( 26 | id int default 0, 27 | name varchar(32) primary key, 28 | appearance varchar(32), 29 | cost int, 30 | grade int 31 | );"])) 32 | 33 | (use-fixtures 34 | :once 35 | (fn [f] 36 | (m/in-cljc-mode) 37 | (m/start #'conn2) 38 | (delete-test-db) 39 | (create-test-table) 40 | (f) 41 | (m/stop))) 42 | 43 | (deftest transaction 44 | (with-transaction 45 | [conn2 {:rollback-only true}] 46 | (is 47 | (= 1 48 | (add-fruit! 49 | {:name "apple" 50 | :appearance "red" 51 | :cost 1 52 | :grade 1})))) 53 | (is 54 | (= [] 55 | (get-fruit {:name "apple"})))) 56 | -------------------------------------------------------------------------------- /test/conman/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns conman.core-test 2 | (:require [clojure.test :refer :all] 3 | [conman.core :refer :all] 4 | [next.jdbc :as jdbc] 5 | [clojure.repl :refer [doc]] 6 | [clojure.java.io :as io] 7 | [mount.core :as m])) 8 | 9 | (m/defstate ^:dynamic conn 10 | :start (connect! 11 | {:jdbc-url "jdbc:h2:./test.db" 12 | :make-pool? true 13 | :naming {:keys clojure.string/lower-case 14 | :fields clojure.string/upper-case}}) 15 | :stop (disconnect! conn)) 16 | 17 | (bind-connection conn "queries.sql") 18 | 19 | (defn delete-test-db [] 20 | (io/delete-file "test.db.mv.db" true) 21 | (io/delete-file "test.db.trace.db" true)) 22 | 23 | (defn create-test-table [] 24 | (jdbc/execute! 25 | conn 26 | ["DROP TABLE fruits IF EXISTS; 27 | CREATE TABLE fruits ( 28 | id int default 0, 29 | name varchar(32) primary key, 30 | appearance varchar(32), 31 | cost int, 32 | grade int 33 | );"])) 34 | 35 | (use-fixtures 36 | :once 37 | (fn [f] 38 | (m/in-clj-mode) 39 | (m/start #'conn) 40 | (delete-test-db) 41 | (create-test-table) 42 | (f) 43 | (m/stop))) 44 | 45 | (deftest doc-test 46 | (is (= "-------------------------\nconman.core-test/get-fruit\n([] [params] [db params options & command-options])\n gets fruit by name\n" 47 | (with-out-str (doc get-fruit))))) 48 | 49 | (deftest datasource 50 | (is 51 | (instance? 52 | clojure.lang.PersistentArrayMap 53 | (make-config 54 | {:jdbc-url "jdbc:h2:./test.db" 55 | :datasource-classname "org.h2.Driver"})))) 56 | 57 | (deftest datasource-classname 58 | (is 59 | (instance? 60 | clojure.lang.PersistentArrayMap 61 | (make-config 62 | {:datasource-classname "org.h2.Driver" 63 | :jdbc-url "jdbc:h2:./test.db"})))) 64 | 65 | (deftest jdbc-url 66 | (is 67 | (instance? 68 | clojure.lang.PersistentArrayMap 69 | (make-config 70 | {:jdbc-url "jdbc:h2:./test.db"})))) 71 | 72 | (deftest transaction 73 | (with-transaction 74 | [conn {:rollback-only true}] 75 | (is 76 | (= 1 77 | (add-fruit! 78 | {:name "apple" 79 | :appearance "red" 80 | :cost 1 81 | :grade 1}))) 82 | (is 83 | (= [{:appearance "red" :cost 1 :grade 1 :id 0 :name "apple"}] 84 | (get-fruit {:name "apple"})))) 85 | (is 86 | (= [] 87 | (get-fruit {:name "apple"})))) 88 | 89 | (deftest transaction-options 90 | (with-transaction 91 | [conn {:isolation :serializable}] 92 | (is (= java.sql.Connection/TRANSACTION_SERIALIZABLE 93 | (.getTransactionIsolation conn)))) 94 | (with-transaction 95 | [conn {:isolation :read-uncommitted}] 96 | (is (= java.sql.Connection/TRANSACTION_READ_UNCOMMITTED 97 | (.getTransactionIsolation conn))))) 98 | 99 | (deftest hugsql-snippets 100 | (is (= 1 101 | (add-fruit! 102 | {:name "orange" 103 | :appearance "orange" 104 | :cost 1 105 | :grade 1}))) 106 | (is (= "orange" 107 | (:name 108 | (get-fruit-by {:by-appearance 109 | (by-appearance {:appearance "orange"})}))))) 110 | 111 | (deftest explicit-queries 112 | (let [queries (load-queries "queries.sql")] 113 | (is (= 1 114 | (query conn 115 | queries 116 | :add-fruit! 117 | {:name "banana" 118 | :appearance "banana" 119 | :cost 1 120 | :grade 1}))) 121 | (is (= "banana" 122 | (-> (query conn 123 | queries 124 | :get-fruit 125 | {:name "banana"}) 126 | first 127 | :name))) 128 | (is (= "banana" 129 | (:name (query conn 130 | queries 131 | :get-fruit-by 132 | {:by-appearance 133 | (snip queries :by-appearance {:appearance "banana"})})))) 134 | (query 135 | conn 136 | queries 137 | :add-fruit! 138 | {:name "foo" 139 | :appearance "foo" 140 | :cost 1 141 | :grade 1}) 142 | (try 143 | (with-transaction [conn] 144 | (query 145 | conn 146 | queries 147 | :add-fruit! 148 | {:name "baz" 149 | :appearance "baz" 150 | :cost 1 151 | :grade 1}) 152 | (query 153 | conn 154 | queries 155 | :add-fruit! 156 | {:name "foo" 157 | :appearance "foo" 158 | :cost 1 159 | :grade 1})) 160 | (catch Exception _)) 161 | (is (= [] (query conn queries :get-fruit {:name "baz"}))))) 162 | 163 | (deftest query-map 164 | (let [queries (bind-connection-map conn "queries.sql")] 165 | (is (= 1 166 | (query 167 | queries 168 | :add-fruit! 169 | {:name "apple" 170 | :appearance "red" 171 | :cost 1 172 | :grade 1}))) 173 | 174 | (is (= [{:id 0, :name "apple", :appearance "red", :cost 1, :grade 1}] 175 | (query queries :get-fruit {:name "apple"}))) 176 | 177 | (is (= ["appearance = ?" "red"] 178 | (snip queries :by-appearance {:appearance "red"}))) 179 | (is 180 | (= 181 | {:id 0, :name "apple", :appearance "red", :cost 1, :grade 1} 182 | (query 183 | conn 184 | queries 185 | :get-fruit-by 186 | {:by-appearance 187 | (snip queries :by-appearance {:appearance "red"})}))))) 188 | 189 | (deftest queries-from-file-object 190 | (let [{:keys [snips fns]} (bind-connection-map conn (java.io.File. "test/queries.sql"))] 191 | (is (= #{:by-appearance} (set (keys snips)))) 192 | (is (= #{:add-fruit! :get-fruit :get-fruit-by} (set (keys fns)))))) 193 | -------------------------------------------------------------------------------- /test/queries.sql: -------------------------------------------------------------------------------- 1 | -- :name add-fruit! :! :n 2 | INSERT INTO fruits 3 | (name, appearance, cost, grade) 4 | VALUES 5 | (:name, :appearance, :cost, :grade) 6 | 7 | -- :name get-fruit :? :* 8 | -- :doc gets fruit by name 9 | SELECT * FROM fruits 10 | WHERE name = :name 11 | 12 | -- :snip by-appearance 13 | appearance = :appearance 14 | 15 | -- :name get-fruit-by :? :1 16 | SELECT * FROM fruits 17 | WHERE :snip:by-appearance 18 | --------------------------------------------------------------------------------