├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── project.clj ├── src └── psql │ └── core.clj └── test └── psql └── core_test.clj /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | services: 13 | postgres: 14 | image: postgres 15 | ports: 16 | - 5432:5432 17 | env: 18 | POSTGRES_USER: 'douglass' 19 | POSTGRES_PASSWORD: 'password' 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | steps: 26 | - uses: actions/checkout@v2 27 | - {name: install, run: lein deps} 28 | - name: test 29 | run: lein test 30 | env: 31 | POSTGRES_USER: douglass 32 | POSTGRES_PASSWORD: password 33 | # Deploy manually for a time 34 | # - name: deploy 35 | # env: 36 | # CLOJARS_USER: ${{ secrets.CLOJARS_USER }} 37 | # CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }} 38 | # run: lein deploy clojars 39 | # if: github.ref == 'master' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nrepl-port 2 | .lein-failures 3 | target/ 4 | pom.xml -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | a) in the case of the initial Contributor, the initial code and 11 | documentation distributed under this Agreement, and 12 | b) in the case of each subsequent Contributor: 13 | i) changes to the Program, and 14 | ii) additions to the Program; 15 | 16 | where such changes and/or additions to the Program originate from and are 17 | distributed by that particular Contributor. A Contribution 'originates' from a 18 | Contributor if it was added to the Program by such Contributor itself or 19 | anyone acting on such Contributor's behalf. Contributions do not include 20 | additions to the Program which: (i) are separate modules of software 21 | distributed in conjunction with the Program under their own license agreement, 22 | and (ii) are not derivative works of the Program. 23 | "Contributor" means any person or entity that distributes the Program. 24 | 25 | "Licensed Patents" mean patent claims licensable by a Contributor which are 26 | necessarily infringed by the use or sale of its Contribution alone or when 27 | combined with the Program. 28 | 29 | "Program" means the Contributions distributed in accordance with this 30 | Agreement. 31 | 32 | "Recipient" means anyone who receives the Program under this Agreement, 33 | including all Contributors. 34 | 35 | 2. GRANT OF RIGHTS 36 | 37 | a) Subject to the terms of this Agreement, each Contributor hereby grants 38 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 39 | reproduce, prepare derivative works of, publicly display, publicly 40 | perform, distribute and sublicense the Contribution of such Contributor, 41 | if any, and such derivative works, in source code and object code form. 42 | 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | 54 | c) Recipient understands that although each Contributor grants the 55 | licenses to its Contributions set forth herein, no assurances are 56 | provided by any Contributor that the Program does not infringe the patent 57 | or other intellectual property rights of any other entity. Each 58 | Contributor disclaims any liability to Recipient for claims brought by 59 | any other entity based on infringement of intellectual property rights or 60 | otherwise. As a condition to exercising the rights and licenses granted 61 | hereunder, each Recipient hereby assumes sole responsibility to secure 62 | any other intellectual property rights needed, if any. For example, if a 63 | third party patent license is required to allow Recipient to distribute 64 | the Program, it is Recipient's responsibility to acquire that license 65 | before distributing the Program. 66 | 67 | d) Each Contributor represents that to its knowledge it has sufficient 68 | copyright rights in its Contribution, if any, to grant the copyright 69 | license set forth in this Agreement. 70 | 71 | 3. REQUIREMENTS 72 | A Contributor may choose to distribute the Program in object code form under 73 | its own license agreement, provided that: 74 | 75 | a) it complies with the terms and conditions of this Agreement; and 76 | 77 | b) its license agreement: 78 | i) effectively disclaims on behalf of all Contributors all 79 | warranties and conditions, express and implied, including warranties 80 | or conditions of title and non-infringement, and implied warranties 81 | or conditions of merchantability and fitness for a particular 82 | purpose; 83 | ii) effectively excludes on behalf of all Contributors all liability 84 | for damages, including direct, indirect, special, incidental and 85 | consequential damages, such as lost profits; 86 | iii) states that any provisions which differ from this Agreement are 87 | offered by that Contributor alone and not by any other party; and 88 | iv) states that source code for the Program is available from such 89 | Contributor, and informs licensees how to obtain it in a reasonable 90 | manner on or through a medium customarily used for software 91 | exchange. 92 | 93 | When the Program is made available in source code form: 94 | 95 | a) it must be made available under this Agreement; and 96 | 97 | b) a copy of this Agreement must be included with each copy of the 98 | Program. 99 | Contributors may not remove or alter any copyright notices contained within 100 | the Program. 101 | 102 | Each Contributor must identify itself as the originator of its Contribution, 103 | if any, in a manner that reasonably allows subsequent Recipients to identify 104 | the originator of the Contribution. 105 | 106 | 4. COMMERCIAL DISTRIBUTION 107 | Commercial distributors of software may accept certain responsibilities with 108 | respect to end users, business partners and the like. While this license is 109 | intended to facilitate the commercial use of the Program, the Contributor who 110 | includes the Program in a commercial product offering should do so in a manner 111 | which does not create potential liability for other Contributors. Therefore, 112 | if a Contributor includes the Program in a commercial product offering, such 113 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 114 | every other Contributor ("Indemnified Contributor") against any losses, 115 | damages and costs (collectively "Losses") arising from claims, lawsuits and 116 | other legal actions brought by a third party against the Indemnified 117 | Contributor to the extent caused by the acts or omissions of such Commercial 118 | Contributor in connection with its distribution of the Program in a commercial 119 | product offering. The obligations in this section do not apply to any claims 120 | or Losses relating to any actual or alleged intellectual property 121 | infringement. In order to qualify, an Indemnified Contributor must: a) 122 | promptly notify the Commercial Contributor in writing of such claim, and b) 123 | allow the Commercial Contributor to control, and cooperate with the Commercial 124 | Contributor in, the defense and any related settlement negotiations. The 125 | Indemnified Contributor may participate in any such claim at its own expense. 126 | 127 | For example, a Contributor might include the Program in a commercial product 128 | offering, Product X. That Contributor is then a Commercial Contributor. If 129 | that Commercial Contributor then makes performance claims, or offers 130 | warranties related to Product X, those performance claims and warranties are 131 | such Commercial Contributor's responsibility alone. Under this section, the 132 | Commercial Contributor would have to defend claims against the other 133 | Contributors related to those performance claims and warranties, and if a 134 | court requires any other Contributor to pay any damages as a result, the 135 | Commercial Contributor must pay those damages. 136 | 137 | 5. NO WARRANTY 138 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 139 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 140 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 141 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 142 | Recipient is solely responsible for determining the appropriateness of using 143 | and distributing the Program and assumes all risks associated with its 144 | exercise of rights under this Agreement , including but not limited to the 145 | risks and costs of program errors, compliance with applicable laws, damage to 146 | or loss of data, programs or equipment, and unavailability or interruption of 147 | operations. 148 | 149 | 6. DISCLAIMER OF LIABILITY 150 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 151 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 152 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 153 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 154 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 155 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 156 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 157 | OF SUCH DAMAGES. 158 | 159 | 7. GENERAL 160 | 161 | If any provision of this Agreement is invalid or unenforceable under 162 | applicable law, it shall not affect the validity or enforceability of the 163 | remainder of the terms of this Agreement, and without further action by the 164 | parties hereto, such provision shall be reformed to the minimum extent 165 | necessary to make such provision valid and enforceable. 166 | 167 | If Recipient institutes patent litigation against any entity (including a 168 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 169 | (excluding combinations of the Program with other software or hardware) 170 | infringes such Recipient's patent(s), then such Recipient's rights granted 171 | under Section 2(b) shall terminate as of the date such litigation is filed. 172 | 173 | All Recipient's rights under this Agreement shall terminate if it fails to 174 | comply with any of the material terms or conditions of this Agreement and does 175 | not cure such failure in a reasonable period of time after becoming aware of 176 | such noncompliance. If all Recipient's rights under this Agreement terminate, 177 | Recipient agrees to cease use and distribution of the Program as soon as 178 | reasonably practicable. However, Recipient's obligations under this Agreement 179 | and any licenses granted by Recipient relating to the Program shall continue 180 | and survive. 181 | 182 | Everyone is permitted to copy and distribute copies of this Agreement, but in 183 | order to avoid inconsistency the Agreement is copyrighted and may only be 184 | modified in the following manner. The Agreement Steward reserves the right to 185 | publish new versions (including revisions) of this Agreement from time to 186 | time. No one other than the Agreement Steward has the right to modify this 187 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 188 | Eclipse Foundation may assign the responsibility to serve as the Agreement 189 | Steward to a suitable separate entity. Each new version of the Agreement will 190 | be given a distinguishing version number. The Program (including 191 | Contributions) may always be distributed subject to the version of the 192 | Agreement under which it was received. In addition, after a new version of the 193 | Agreement is published, Contributor may elect to distribute the Program 194 | (including its Contributions) under the new version. Except as expressly 195 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 196 | licenses to the intellectual property of any Contributor under this Agreement, 197 | whether expressly, by implication, estoppel or otherwise. All rights in the 198 | Program not expressly granted under this Agreement are reserved. 199 | 200 | This Agreement is governed by the laws of the State of New York and the 201 | intellectual property laws of the United States of America. No party to this 202 | Agreement will bring a legal action under this Agreement more than one year 203 | after the cause of action arose. Each party waives its rights to a jury trial 204 | in any resulting litigation. 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-psql 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/douglass/clj-psql.svg)](https://clojars.org/douglass/clj-psql) 4 | 5 | A small wrapper around `psql`. Intended for use with [babashka](https://github.com/borkdude/babashka) by those that don't want to maintain their own JDBC-enabled binary. 6 | 7 | ## Usage 8 | 9 | The library exposes 4 main fns: 10 | 11 | - `insert!` 12 | - `query` 13 | - `update!` 14 | - `delete!` 15 | 16 | Each fn has two arities: one that takes in connection config and a string query and one that provides a more [`next.jdbc`-esque](https://github.com/seancorfield/next-jdbc/) interface. 17 | 18 | Each example below assumes the following schema: 19 | 20 | ```postgres 21 | create table grades( 22 | name text, 23 | subject text, 24 | grade integer, 25 | comment text default 'N/A', 26 | constraint name_subject primary key(name, subject)); 27 | ``` 28 | 29 | and the following clojure setup: 30 | 31 | ```clojure 32 | user> (require '[psql.core :as psql]) 33 | user> (def conn {:host "localhost" 34 | :name "mydb" 35 | :username "user" 36 | :password "secret"}) 37 | ``` 38 | 39 | ### `insert!` 40 | #### Simple form 41 | Runs the provided command and returns the number of rows inserted: 42 | 43 | ```clojure 44 | user> (psql/insert! conn "insert into grades (name, subject, grade) values ('Bobby Tables', 'Math', 100)") 45 | 1 46 | ``` 47 | 48 | #### JDBC form 49 | Takes in table name as a keyword and a vector (or map if only inserting one row) of data. Returns the inserted rows from the database: 50 | 51 | ```clojure 52 | user> (psql/insert! conn :grades [{:name "Bobby Tables" :subject "English" :grade 72} 53 | {:name "Suzy Butterbean" :subject "Math" :grade 100} 54 | {:name "Suzy Butterbean" :subject "English" :grade 87}]) 55 | ({:name "Bobby Tables", 56 | :subject "English", 57 | :grade "72", 58 | :comment "N/A"} 59 | {:name "Suzy Butterbean", 60 | :subject "Math", 61 | :grade "100", 62 | :comment "N/A"} 63 | {:name "Suzy Butterbean", 64 | :subject "English", 65 | :grade "87", 66 | :comment "N/A"}) 67 | ``` 68 | 69 | ### `query` 70 | #### Simple form 71 | Runs the provided `SELECT` statement and parses the data into a sequence of maps: 72 | 73 | ```clojure 74 | user> (psql/query conn "select name, subject from grades where grade = 100") 75 | ({:name "Bobby Tables", :subject "Math"} 76 | {:name "Suzy Butterbean", :subject "Math"}) 77 | ``` 78 | 79 | #### JDBC form 80 | Takes in the table name as a keyword and a some conditions: 81 | 82 | ```clojure 83 | ;; Map of conditions. All non-sequentials are assumed to be equality checks. 84 | user> (psql/query conn :grades {:name "Bobby Tables"}) 85 | ({:name "Bobby Tables", 86 | :subject "Math", 87 | :grade "100", 88 | :comment "N/A"} 89 | {:name "Bobby Tables", 90 | :subject "English", 91 | :grade "72", 92 | :comment "N/A"}) 93 | ;; Sequentials within the conditions are assumed to be IN checks 94 | user> (psql/query conn :grades {:grade [72 100]}) 95 | ({:name "Bobby Tables", 96 | :subject "Math", 97 | :grade "100", 98 | :comment "N/A"} 99 | {:name "Bobby Tables", 100 | :subject "English", 101 | :grade "72", 102 | :comment "N/A"} 103 | {:name "Suzy Butterbean", 104 | :subject "Math", 105 | :grade "100", 106 | :comment "N/A"}) 107 | ;; Sequentials OF maps are assumed to be discrete sets of conditions and are joined by `OR` 108 | user> (psql/query conn :grades [{:grade [72]} 109 | {:name "Suzy Butterbean" :grade 100}]) 110 | ({:name "Bobby Tables", 111 | :subject "English", 112 | :grade "72", 113 | :comment "N/A"} 114 | {:name "Suzy Butterbean", 115 | :subject "Math", 116 | :grade "100", 117 | :comment "N/A"}) 118 | ``` 119 | 120 | ## `update!` 121 | #### Simple form 122 | Runs the provided `UPDATE` statement and returns the number of rows updated: 123 | 124 | ```clojure 125 | user> (psql/update! conn "update grades set comment = 'null' where name = 'Bobby Tables'") 126 | 2 127 | ``` 128 | #### JDBC form 129 | Takes the table name as a keyword, a map of new values to be applied to the matched rows, and some conditions. Returns the updated rows from the database: 130 | 131 | ```clojure 132 | user> (psql/update! conn :grades {:comment "moving soon"} {:name "Bobby Tables"}) 133 | ({:name "Bobby Tables", 134 | :subject "Math", 135 | :grade "100", 136 | :comment "moving soon"} 137 | {:name "Bobby Tables", 138 | :subject "English", 139 | :grade "72", 140 | :comment "moving soon"}) 141 | ``` 142 | 143 | ## `delete!` 144 | #### Simple form 145 | Runs the provided `DELETE` statement and returns the number of rows removed: 146 | 147 | ```clojure 148 | user> (psql/delete! conn "delete from grades where name = 'Bobby Tables'") 149 | 2 150 | ``` 151 | 152 | #### JDBC form 153 | Takes the table name as a keyword and conditions. Returns the number of rows removed from the database: 154 | 155 | ```clojure 156 | user> (psql/delete! conn :grades {:subject "English"}) 157 | 1 158 | ``` 159 | 160 | ## License 161 | 162 | Copyright © 2020 Darin Douglass 163 | 164 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 165 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject douglass/clj-psql "0.1.2" 2 | :description "A small Clojure wrapper for interacting with Postgres via psql" 3 | :license "EPL 1.0" 4 | :url "https://github.com/DarinDouglass/clj-psql" 5 | :dependencies [[org.clojure/clojure "1.10.1"]] 6 | :test-paths ["test"] 7 | :resource-paths ["resources"] 8 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo" 9 | :username :env/clojars_user 10 | :password :env/clojars_password 11 | :sign-releases false}]]) 12 | -------------------------------------------------------------------------------- /src/psql/core.clj: -------------------------------------------------------------------------------- 1 | (ns psql.core 2 | (:require [clojure.java.shell :as shell] 3 | [clojure.string :as str])) 4 | 5 | (defn- snake_case 6 | "Converts a string/keword value into snake_case" 7 | [value] 8 | (if (string? value) 9 | value 10 | (str/replace (name value) #"-" "_"))) 11 | 12 | (defn- wrap-parens 13 | "Generates a comma-separated string of values, wrapped in parens." 14 | [value] 15 | (format "(%s)" (str/join ", " value))) 16 | 17 | (defn- boxed 18 | "Coerces the provided value into a sequential." 19 | [value] 20 | (if (sequential? value) 21 | value 22 | [value])) 23 | 24 | (defn- format-value 25 | "Attemps to do some minimal coercion to psql formats." 26 | [value] 27 | (cond 28 | (string? value) 29 | (format "'%s'" value) 30 | 31 | (sequential? value) 32 | (wrap-parens (map format-value value)) 33 | 34 | :else 35 | value)) 36 | 37 | (defn- join-kv 38 | "Converts a map into a form suitable for WHERE/SET clauses." 39 | [conditions] 40 | (map (fn [[key value]] 41 | (format "%s %s %s" 42 | (snake_case key) 43 | (if (sequential? value) "in" "=") 44 | (format-value value))) 45 | conditions)) 46 | 47 | (defn- where-clause 48 | "Generates a WHERE clause for the given conditions." 49 | [conditions] 50 | (let [conditions (remove empty? (boxed conditions))] 51 | (if (empty? conditions) 52 | "" 53 | (format "where %s" (->> conditions 54 | (map #(str/join " and " (join-kv %1))) 55 | (map #(format "(%s)" %1)) 56 | (str/join " or ")))))) 57 | 58 | (defn- vector->command 59 | "Takes a jdbc-style vector query and converts it into a plain string query." 60 | [[command & params]] 61 | (apply format (str/replace command #"\?" "%s") (map format-value params))) 62 | 63 | (defn- rows 64 | "Splits a chunk of psql output into individual rows." 65 | [line] 66 | (map str/trim (str/split line #" \| "))) 67 | 68 | (defn- columns 69 | "Parses ordered column names from the first line of psql output." 70 | [lines] 71 | (map (comp keyword #(str/replace %1 #"_" "-")) 72 | (rows (first lines)))) 73 | 74 | (defn output->data 75 | "Converts psql output into clojure data." 76 | [output] 77 | (let [lines (-> output 78 | (str/trim) 79 | (str/split-lines)) 80 | columns (columns lines) 81 | rows (map rows (butlast (drop 2 lines)))] 82 | (map (partial zipmap columns) rows))) 83 | 84 | (defn execute! 85 | "Runs the provided command using psql." 86 | [conn command] 87 | (let [{:keys [host port name username password] :or {port 5432}} conn 88 | {:keys [out err]} (shell/sh "psql" 89 | "-h" host 90 | "-U" username 91 | "-p" (str port) 92 | name 93 | :in command 94 | :env (assoc (into {} (System/getenv)) "PGPASSWORD" password))] 95 | (when-not (str/blank? err) 96 | (throw (ex-info err {:conn conn :command command}))) 97 | out)) 98 | 99 | (defn query 100 | "Runs a SELECT against psql and processes its output." 101 | ([conn command] 102 | (if (vector? command) 103 | (query conn (vector->command command)) 104 | (output->data (execute! conn command)))) 105 | ([conn table conditions] 106 | (query conn (format "select * from %s %s" 107 | (snake_case table) 108 | (where-clause conditions))))) 109 | 110 | (defn insert! 111 | "Runs an INSERT against psql and processes its output. 112 | 113 | Returns the inserted rows on successful insert." 114 | ([conn query] 115 | (if (vector? query) 116 | (insert! conn (vector->command query)) 117 | (let [result (->> query 118 | (execute! conn) 119 | (str/trim) 120 | (re-matches #"INSERT \d+ (\d+)") 121 | (last) 122 | (Integer/parseInt))] 123 | (if (pos? result) 124 | result 125 | nil)))) 126 | ([conn table data] 127 | (let [data (boxed data) 128 | columns (sort (keys (first data))) 129 | values (map (fn [row] 130 | (reduce #(conj %1 (format-value (get row %2))) [] columns)) 131 | data)] 132 | (when (insert! conn (format "insert into %s %s values %s" 133 | (snake_case table) 134 | (wrap-parens (map snake_case columns)) 135 | (str/join ", " (map wrap-parens values)))) 136 | (query conn table data))))) 137 | 138 | (defn delete! 139 | "Runs a DELETE against psql and processes its output." 140 | ([conn query] 141 | (if (vector? query) 142 | (delete! conn (vector->command query)) 143 | (let [result (->> query 144 | (execute! conn) 145 | (str/trim) 146 | (re-matches #"DELETE (\d+)") 147 | (last) 148 | (Integer/parseInt))] 149 | (if (pos? result) 150 | result 151 | nil)))) 152 | ([conn table conditions] 153 | (delete! conn (format "delete from %s %s" 154 | (snake_case table) 155 | (where-clause conditions))))) 156 | 157 | (defn update! 158 | "Runs an UPDATE against psql and processes its output. 159 | 160 | Returns the updated rows on succesful update." 161 | ([conn query] 162 | (if (vector? query) 163 | (update! conn (vector->command query)) 164 | (let [result (->> query 165 | (execute! conn) 166 | (str/trim) 167 | (re-matches #"UPDATE (\d+)") 168 | (last) 169 | (Integer/parseInt))] 170 | (if (pos? result) 171 | result 172 | nil)))) 173 | ([conn table updates conditions] 174 | (when (update! conn (format "update %s set %s %s" 175 | (snake_case table) 176 | (str/join ", " (join-kv updates)) 177 | (where-clause conditions))) 178 | (query conn table conditions)))) 179 | -------------------------------------------------------------------------------- /test/psql/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns psql.core-test 2 | (:require [clojure.test :refer [deftest is testing use-fixtures]] 3 | [psql.core :as sut])) 4 | 5 | (def conn {:host (or (System/getenv "POSTGRES_HOST") "localhost") 6 | :port (or (System/getenv "POSTGRES_PORT") 5432) 7 | :username (or (System/getenv "POSTGRES_USER") "postgres") 8 | :password (or (System/getenv "POSTGRES_PASSWORD") "") 9 | :name (or (System/getenv "POSTGRES_DATABASE") "postgres")}) 10 | (def grades [{:name "Bobby" :subject "Math" :grade 80} 11 | {:name "Bobby" :subject "English" :grade 73} 12 | {:name "Suzy" :subject "Math" :grade 91} 13 | {:name "Suzy" :subject "English" :grade 94}]) 14 | 15 | (use-fixtures :once 16 | (fn [test] 17 | (let [command (str "create table grades(" 18 | " name text," 19 | " subject text," 20 | " grade integer," 21 | " comment text default null," 22 | " constraint name_subject primary key(name, subject))")] 23 | (sut/execute! conn command) 24 | (test) 25 | (sut/execute! conn "drop table grades")))) 26 | 27 | (deftest test-database-interactions 28 | (testing "bad operations throw errors" 29 | (is (thrown? clojure.lang.ExceptionInfo #"." 30 | (sut/execute! conn "select * from not_exists")))) 31 | (testing "we can query the database" 32 | (is (empty? (sut/query conn :grades {})))) 33 | (testing "inserts work propery" 34 | (is (= 3 (count (sut/insert! conn :grades (butlast grades))))) 35 | (testing "vector commands" 36 | (let [{:keys [name subject grade]} (last grades)] 37 | (is (= 1 (sut/insert! conn ["insert into grades (name, subject, grade) values (?, ?, ?)" 38 | name subject grade]))))) 39 | (is (= 2 (count (sut/query conn :grades {:name "Bobby"})))) 40 | (is (= 2 (count (sut/query conn :grades {:name "Suzy"}))))) 41 | (testing "updates work" 42 | (let [condition {:name "Suzy" :subject "Math"}] 43 | (sut/update! conn :grades {:grade 100} condition) 44 | (is (= 100 (->> condition 45 | (sut/query conn :grades) 46 | (first) 47 | (:grade) 48 | (Integer/parseInt)))) 49 | (testing "vector commands" 50 | (sut/update! conn ["update grades set grade = ? where name = ?" 99 "Suzy"]) 51 | (is (= 99 (->> condition 52 | (sut/query conn :grades) 53 | (first) 54 | (:grade) 55 | (Integer/parseInt))))))) 56 | (testing "multi-updates work" 57 | (let [conditions [{:name "Suzy" :subject "Math"} 58 | {:name "Bobby" :subject "English"}]] 59 | (sut/update! conn :grades {:comment "has tutor"} conditions) 60 | (is (every? (comp #{"has tutor"} :comment) (sut/query conn :grades conditions))))) 61 | (testing "deletes work" 62 | (let [condition {:comment "has tutor"}] 63 | (sut/delete! conn :grades condition) 64 | (is (empty? (sut/query conn :grades condition)))) 65 | (testing "vector commands" 66 | (sut/delete! conn ["delete from grades where name = ? and subject = ?" "Bobby" "Math"]) 67 | (is (empty? (sut/query conn :grades {:name "Bobby" :subject "Math"}))))) 68 | (testing "multi-deletes work" 69 | (let [conditions [{:name "Suzy" :subject "English"} 70 | {:name "Suzy" :subject "Math"}]] 71 | (sut/delete! conn :grades conditions) 72 | (is (empty? (sut/query conn :grades conditions)))))) 73 | --------------------------------------------------------------------------------