├── .cljstyle ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── doc ├── css │ └── default.css ├── index.html ├── js │ ├── jquery.min.js │ └── page_effects.js ├── stch.sql.ddl.html ├── stch.sql.format.html ├── stch.sql.html ├── stch.sql.types.html └── stch.sql.util.html ├── examples └── sql.clj ├── project.clj ├── spec └── stch │ ├── sql │ └── ddl_spec.clj │ └── sql_spec.clj └── src └── stch ├── sql.clj └── sql ├── ddl.clj ├── format.clj ├── types.clj └── util.clj /.cljstyle: -------------------------------------------------------------------------------- 1 | {:list-indent-size 1 2 | :indents {defrecord [[:inner 0] [:inner 1]] 3 | defrecord' [[:inner 0] [:inner 1]] 4 | deftype [[:inner 0] [:inner 1]] 5 | create [[:inner 0]] 6 | alt [[:inner 0]] 7 | #"(describe|context|it|around)" [[:inner 0]]} 8 | :line-break-functions? false 9 | :line-break-vars? false 10 | :max-consecutive-blank-lines 1 11 | :padding-lines 1 12 | :reformat-types? false} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | .DS_Store 14 | .nrepl-port 15 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 0.1.1 4 | 5 | 1. Add PostgreSQL serial types. 6 | 7 | ## 0.1.0 8 | 9 | 1. Initial release. 10 | -------------------------------------------------------------------------------- /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 Washington 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 | # stch.sql 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/stch-library/sql.svg)](https://clojars.org/stch-library/sql) 4 | 5 | A DSL in Clojure for SQL query, DML, and DDL. Supports a majority of MySQL's statements. 6 | 7 | Based on code from [Honey SQL](https://github.com/jkk/honeysql) and ideas from [Lobos](http://budu.github.io/lobos/) and [SQLingvo](https://github.com/r0man/sqlingvo). Many thanks to the authors of those libraries. 8 | 9 | ## Installation 10 | 11 | Add the following to your project dependencies: 12 | 13 | ```clojure 14 | [stch-library/sql "0.1.1"] 15 | ``` 16 | 17 | ## API Documentation 18 | 19 | http://stch-library.github.io/sql 20 | 21 | Note: This library uses [stch.schema](https://github.com/stch-library/schema). Please refer to that project page for more information regarding type annotations and their meaning. 22 | 23 | ## How to use 24 | 25 | This library is split into two distinct offerings. 26 | 27 | 1. Query and DML 28 | 2. DDL 29 | 30 | Query and DML functions are in stch.sql, accompanied by stch.sql.format. DDL functions are in stch.sql.ddl. 31 | 32 | Below is a small sampling of what you can do with this library. Please see the unit-tests in the spec directory for a comprehensive look at what's possible. 33 | 34 | ### Query 35 | 36 | Queries are composed of threaded fn calls, producing a map, which when formatted will produce a JDBC compatible vector. 37 | 38 | ```clojure 39 | (require '[stch.sql.format :as sql] 40 | '[stch.sql :refer :all]) 41 | 42 | (-> (select :*) 43 | (from :foo) 44 | (where '(= firstName "Billy")) 45 | sql/format) 46 | ; ["SELECT * FROM foo WHERE firstName = ?" "Billy"] 47 | ``` 48 | 49 | If you are using [clojure.jdbc](https://github.com/niwibe/clojure.jdbc), you can extend the ISQLStatement protocol, so that you don't have to manually call sql/format. 50 | 51 | ```clojure 52 | (require '[jdbc.types :as types]) 53 | 54 | (extend-protocol types/ISQLStatement 55 | clojure.lang.APersistentMap 56 | (normalize [this conn options] 57 | (types/normalize (sql/format this) conn options))) 58 | 59 | (let [id 1234 60 | sql (-> (select :*) 61 | (from :users) 62 | (where `(= userID ~id))) 63 | result (query conn sql {:identifiers identity})] 64 | (-> result first)) 65 | ``` 66 | 67 | SELECT 68 | 69 | ```clojure 70 | (-> (select :*) 71 | sql/format) 72 | ; ["SELECT *"] 73 | 74 | ; Dashes to underscores 75 | (-> (select :first-name) 76 | sql/format) 77 | ; ["SELECT first_name"] 78 | 79 | ; NULL 80 | (-> (select nil) 81 | sql/format) 82 | ; ["SELECT NULL"] 83 | 84 | ; Aliased table name 85 | (-> (select [:firstName :name]) 86 | sql/format) 87 | ; ["SELECT firstName AS name"] 88 | 89 | ; Modifiers 90 | (-> (select :id) 91 | (modifiers :distinct) 92 | (from :users) 93 | sql/format) 94 | ; ["SELECT DISTINCT id FROM users"] 95 | ``` 96 | 97 | Functions 98 | 99 | ```clojure 100 | ; Prefix 101 | (-> (select '(now)) 102 | sql/format) 103 | ; ["SELECT now()"] 104 | 105 | ; Infix 106 | (-> (select '(<> 1 2)) sql/format) 107 | ; ["SELECT 1 <> 2"] 108 | 109 | ; Convenience shorthand 110 | (-> (select '(count-distinct id)) 111 | sql/format) 112 | ; ["SELECT COUNT(DISTINCT id)"] 113 | ``` 114 | 115 | WHERE clause 116 | 117 | ```clojure 118 | ; Implicit AND 119 | (-> (select :*) 120 | (from :users) 121 | (where '(= id 5) 122 | '(= status "active")) 123 | sql/format) 124 | ; ["SELECT * FROM users WHERE (id = 5 AND status = ?)" "active"] 125 | 126 | ; OR 127 | (-> (select :*) 128 | (from :users) 129 | (where '(or (= id 5) 130 | (= status "active"))) 131 | sql/format) 132 | ; ["SELECT * FROM users WHERE (id = 5 OR status = ?)" "active"] 133 | ``` 134 | 135 | Parameters 136 | 137 | ```clojure 138 | ; Named 139 | (-> (select :*) 140 | (from :users) 141 | (where '(= name ?name) 142 | '(= userID ?userID)) 143 | (sql/format :params {:name "Billy" 144 | :userID 3})) 145 | ; ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 146 | 147 | ; Sequential 148 | (-> (select :*) 149 | (from :users) 150 | (where '(= name ?name) 151 | '(= userID ?userID)) 152 | (sql/format :params ["Billy" 3])) 153 | ; ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 154 | 155 | ; Unnamed 156 | (-> (select :*) 157 | (from :users) 158 | (where '(= name ?) 159 | '(= userID ?)) 160 | (sql/format :params ["Billy" 3])) 161 | ; ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 162 | 163 | ; Spliced 164 | (def n "Billy") 165 | 166 | (-> (select :*) 167 | (from :foo) 168 | (where `(= name ~n)) 169 | sql/format) 170 | ; ["SELECT * FROM foo WHERE name = ?" "Billy"] 171 | ``` 172 | 173 | JOIN: join, left-join, right-join 174 | 175 | ```clojure 176 | (-> (select :*) 177 | (from :users) 178 | (join :contacts 179 | '(= users.id contacts.id)) 180 | sql/format) 181 | ; ["SELECT * FROM users INNER JOIN contacts ON users.id = contacts.id"] 182 | 183 | ; Aliased table name 184 | (-> (select :*) 185 | (from :foo) 186 | (join :bar '(= foo.x bar.x) 187 | [:baz :b] '(= bar.x b.x)) 188 | sql/format) 189 | ; ["SELECT * FROM foo INNER JOIN bar ON foo.x = bar.x INNER JOIN baz AS b ON bar.x = b.x"] 190 | ``` 191 | 192 | ORDER BY 193 | 194 | ```clojure 195 | (-> (select :*) 196 | (from :users) 197 | (order-by :name :age) 198 | sql/format) 199 | ; ["SELECT * FROM users ORDER BY name, age"] 200 | 201 | ; DESC 202 | (-> (select :*) 203 | (from :users) 204 | (order-by (desc :name)) 205 | sql/format) 206 | ; ["SELECT * FROM users ORDER BY name DESC"] 207 | ``` 208 | 209 | GROUP BY 210 | 211 | ```clojure 212 | (-> (select :*) 213 | (from :users) 214 | (group :name :age) 215 | sql/format) 216 | ; ["SELECT * FROM users GROUP BY name, age"] 217 | ``` 218 | 219 | HAVING 220 | 221 | ```clojure 222 | (-> (select :*) 223 | (from :users) 224 | (having '(> (count email) 2)) 225 | sql/format) 226 | ; ["SELECT * FROM users HAVING count(email) > 2"] 227 | ``` 228 | 229 | LIMIT 230 | 231 | ```clojure 232 | (-> (select :*) 233 | (from :users) 234 | (limit 50) 235 | sql/format) 236 | ; ["SELECT * FROM users LIMIT 50"] 237 | 238 | ; OFFSET 239 | (-> (select :*) 240 | (from :users) 241 | (limit 50) 242 | (offset 50) 243 | sql/format) 244 | ; ["SELECT * FROM users LIMIT 50 OFFSET 50"] 245 | ``` 246 | 247 | Subqueries 248 | 249 | ```clojure 250 | (-> (select :*) 251 | (from :users) 252 | (where `(in id 253 | ~(-> (select :userid) 254 | (from :contacts)))) 255 | sql/format) 256 | ; ["SELECT * FROM users WHERE (id IN (SELECT userid FROM contacts))"] 257 | ``` 258 | 259 | UNION 260 | 261 | ```clojure 262 | (sql/format 263 | (union (-> (select :name :email) 264 | (from :users)) 265 | (-> (select :name :email) 266 | (from :deleted-users)))) 267 | ; ["(SELECT name, email FROM users) UNION (SELECT name, email FROM deleted_users)"] 268 | ``` 269 | 270 | Order doesn't matter 271 | 272 | ```clojure 273 | (-> (where '(= userid 1234)) 274 | (from :users) 275 | (order-by :first-name :last-name) 276 | (select :first-name :last-name) 277 | sql/format) 278 | ; ["SELECT first_name, last_name FROM users WHERE userid = 1234 ORDER BY first_name, last_name"] 279 | ``` 280 | 281 | Composition 282 | 283 | select, from, join, left-join, right-join, where, having, group, and order-by are compositional by design. The way in which each composes should be fairly intuitive. Here are some examples. 284 | 285 | ```clojure 286 | (def query 287 | (-> (select :first-name) 288 | (from :users) 289 | (join :contacts '(= users.cid contacts.cid)) 290 | (where '(= id 5)) 291 | (group :first-name) 292 | (order-by :first-name))) 293 | 294 | (-> query 295 | (select :last-name) 296 | (join :perms '(= users.uid perms.uid)) 297 | (where '(= status "active")) 298 | (group :last-name) 299 | (order-by :last-name) 300 | sql/format) 301 | ; ["SELECT first_name, last_name FROM users INNER JOIN contacts ON users.cid = contacts.cid INNER JOIN perms ON users.uid = perms.uid WHERE (id = 5 AND status = ?) GROUP BY first_name, last_name ORDER BY first_name, last_name" "active"] 302 | ``` 303 | 304 | ### DML 305 | 306 | INSERT 307 | 308 | ```clojure 309 | ; Vector of maps 310 | (-> (insert-into :users) 311 | (values [{:name "Billy" :age 35} 312 | {:name "Joey" :age 37}]) 313 | sql/format) 314 | ; ["INSERT INTO users (age, name) VALUES (35, ?), (37, ?)" "Billy" "Joey"] 315 | 316 | ; Vector of vectors 317 | (-> (insert-into :users) 318 | (values [["Billy" 35]["Joey" 37]]) 319 | sql/format) 320 | ; ["INSERT INTO users VALUES (?, 35), (?, 37)" "Billy" "Joey"] 321 | 322 | ; ON DUPLICATE KEY 323 | (-> (insert-into :foo) 324 | (columns :a :b :c) 325 | (values [[1 2 3]]) 326 | (on-dup-key {:c 9}) 327 | sql/format) 328 | ; ["INSERT INTO foo (a, b, c) VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = 9"] 329 | ``` 330 | 331 | INSERT/SELECT 332 | 333 | ```clojure 334 | (-> (insert-into :foo) 335 | (columns :a :b :c) 336 | (select :x.a :y.b :z.c) 337 | (from :x) 338 | (join :y '(= x.id y.id) 339 | :z '(= y.id z.id)) 340 | sql/format) 341 | ; ["INSERT INTO foo (a, b, c) SELECT x.a, y.b, z.c FROM x INNER JOIN y ON x.id = y.id INNER JOIN z ON y.id = z.id"] 342 | ``` 343 | 344 | UPDATE 345 | 346 | ```clojure 347 | (-> (update :users) 348 | (setv {:name "Billy", :age 35}) 349 | (where '(= id 234)) 350 | sql/format) 351 | ; ["UPDATE users SET age = 35, name = ? WHERE id = 234" "Billy"] 352 | ``` 353 | 354 | DELETE 355 | 356 | ```clojure 357 | (-> (delete-from :foo) 358 | (where '(= email "billy@bob.com")) 359 | sql/format) 360 | ; ["DELETE FROM foo WHERE email = ?" "billy@bob.com"] 361 | 362 | ; Multiple tables 363 | (-> (delete-from :t1) 364 | (using :t1 :t2) 365 | (where '(= t1.x t2.x) 366 | '(= t2.y 3)) 367 | sql/format) 368 | ; ["DELETE FROM t1 USING t1, t2 WHERE (t1.x = t2.x AND t2.y = 3)"] 369 | ``` 370 | 371 | REPLACE (same behavior as insert) 372 | 373 | ```clojure 374 | (-> (replace-into :users) 375 | (values [{:name "Billy" :age 35} 376 | {:name "Joey" :age 37}]) 377 | sql/format) 378 | ; ["REPLACE INTO users (age, name) VALUES (35, ?), (37, ?)" "Billy" "Joey"] 379 | ``` 380 | 381 | ### Quoting 382 | 383 | Quote style to use for identifiers. Options include: 384 | 385 | 1. :ansi (PostgreSQL) 386 | 2. :mysql 387 | 3. :sqlserver 388 | 4. :oracle 389 | 390 | Defaults to no quoting. 391 | 392 | ```clojure 393 | (-> (select :users.name 394 | :contacts.* 395 | '(date_format dob "%m/%d/%Y")) 396 | (from :users) 397 | (join :contacts 398 | '(= users.id contacts.userid)) 399 | (where '(in users.status ["active" 400 | "pending"])) 401 | (group :users.status) 402 | (order-by (asc :contacts.last-name)) 403 | (limit 25) 404 | (sql/format :quoting :mysql)) 405 | ; ["SELECT `users`.`name`, `contacts`.*, date_format(`dob`, ?) FROM `users` INNER JOIN `contacts` ON `users`.`id` = `contacts`.`userid` WHERE (`users`.`status` IN (?, ?)) GROUP BY `users`.`status` ORDER BY `contacts`.`last_name` ASC LIMIT 25" "%m/%d/%Y" "active" "pending"] 406 | ``` 407 | 408 | ### DDL 409 | 410 | The two primary functions are create and alt, which take a table record and produce a SQL string. 411 | 412 | #### CREATE 413 | 414 | ```clojure 415 | (use 'stch.sql.ddl) 416 | 417 | (create 418 | (-> (table :users) 419 | (integer :userID :unsigned :not-null) 420 | (integer :orgID) 421 | (set' :groups ["user" "admin"] (default "user")) 422 | (enum :status ["active" "inactive"]) 423 | (decimal :ranking '(3 1) (default 0)) 424 | (varchar :username [50]) 425 | (chr :countryCode [2] (default "US")) 426 | (primary-key :userID) 427 | (index [:userID :orgID]) 428 | (unique :username) 429 | (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 430 | (engine :InnoDB) 431 | (collate :utf8-general-ci)) 432 | ; "CREATE TABLE users (userID INT UNSIGNED NOT NULL, orgID INT, groups SET('user', 'admin') DEFAULT 'user', status ENUM('active', 'inactive'), ranking DECIMAL(3, 1) DEFAULT 0, username VARCHAR(50), countryCode CHAR(2) DEFAULT 'US', PRIMARY KEY(userID), INDEX(userID, orgID), UNIQUE(username), FOREIGN KEY(orgID) REFERENCES orgs(orgID) ON DELETE CASCADE) ENGINE=InnoDB, COLLATE=utf8_general_ci" 433 | ``` 434 | 435 | All column types have a corresponding function. See API for details. 436 | 437 | ```clojure 438 | (create 439 | (-> (table :users) 440 | (chr :countryCode [2]))) 441 | ; "CREATE TABLE users (countryCode CHAR(2))" 442 | ``` 443 | 444 | INDEX 445 | 446 | ```clojure 447 | (create 448 | (-> (table :users) 449 | (integer :user_id) 450 | (index :user_id))) 451 | ; "CREATE TABLE users (user_id INT, INDEX(user_id))" 452 | 453 | ; Multiple columns 454 | (create 455 | (-> (table :users) 456 | (varchar :first_name [100]) 457 | (varchar :last_name [100]) 458 | (index [:first_name :last_name]))) 459 | ; "CREATE TABLE users (first_name VARCHAR(100), last_name VARCHAR(100), INDEX(first_name, last_name))" 460 | 461 | ; FOREIGN KEY 462 | (create 463 | (-> (table :contacts) 464 | (integer :user_id) 465 | (foreign-key :user_id '(users user_id) :on-delete-cascade))) 466 | ; "CREATE TABLE contacts (user_id INT, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE)" 467 | 468 | ; Named key 469 | (create 470 | (-> (table :users) 471 | (varchar :username [50]) 472 | (constraint :uname (unique :username)))) 473 | ; "CREATE TABLE users (username VARCHAR(50), CONSTRAINT uname UNIQUE(username))" 474 | ``` 475 | 476 | Table Options 477 | 478 | ```clojure 479 | (create 480 | (-> (table :users) 481 | (varchar :username [50])) 482 | (engine :InnoDB) 483 | (character-set :utf8)) 484 | ; "CREATE TABLE users (username VARCHAR(50)) ENGINE=InnoDB, CHARACTER SET=utf8" 485 | ``` 486 | 487 | Appending 488 | 489 | ```clojure 490 | (defcolumns cols 491 | (-> (integer :user-id :unsigned :not-null) 492 | (index :user-id))) 493 | 494 | (create 495 | (-> (table :users) 496 | (append cols) 497 | (varchar :username [50]))) 498 | ; "CREATE TABLE users (user_id INT UNSIGNED NOT NULL, INDEX(user_id), username VARCHAR(50))" 499 | ``` 500 | 501 | TEMPORARY TABLE 502 | 503 | ```clojure 504 | (create 505 | (-> (temp-table :users) 506 | (integer :userID))) 507 | ; "CREATE TEMPORARY TABLE users (userID INT)" 508 | ``` 509 | 510 | #### ALTER 511 | 512 | ```clojure 513 | (alt 514 | (-> (table :users) 515 | (add (varchar :email [50]) (after :userID)) 516 | (add (varchar :firstName [25]) :first) 517 | (add (index [:firstName :lastName])) 518 | (add (index '(username ranking))) 519 | (add (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 520 | (change :username (varchar :username [100])) 521 | (drop-default :ranking) 522 | (set-default :ranking 1) 523 | (drop-column :countryCode) 524 | (drop-index :uname) 525 | (drop-primary-key) 526 | (drop-foreign-key :fk1))) 527 | ; "ALTER TABLE users ADD COLUMN email VARCHAR(50) AFTER userID, ADD COLUMN firstName VARCHAR(25) FIRST, ADD INDEX(firstName, lastName), ADD INDEX(username, ranking), ADD FOREIGN KEY(orgID) REFERENCES orgs(orgID) ON DELETE CASCADE, CHANGE username username VARCHAR(100), ALTER COLUMN ranking DROP DEFAULT, ALTER COLUMN ranking SET DEFAULT 1, DROP COLUMN countryCode, DROP INDEX uname, DROP PRIMARY KEY, DROP FOREIGN KEY fk1" 528 | ``` 529 | 530 | ADD 531 | 532 | ```clojure 533 | ; AFTER 534 | (alt 535 | (-> (table :users) 536 | (add (varchar :email [50]) (after :userID)))) 537 | ; "ALTER TABLE users ADD COLUMN email VARCHAR(50) AFTER userID" 538 | 539 | ; FIRST 540 | (alt 541 | (-> (table :users) 542 | (add (varchar :email [50]) :first))) 543 | ; "ALTER TABLE users ADD COLUMN email VARCHAR(50) FIRST" 544 | 545 | ; INDEX 546 | (alt 547 | (-> (table :users) 548 | (add (index :firstName)))) 549 | ; "ALTER TABLE users ADD INDEX(firstName)" 550 | ``` 551 | 552 | CHANGE 553 | 554 | ```clojure 555 | (alt 556 | (-> (table :users) 557 | (change :username (varchar :username [100])))) 558 | ; "ALTER TABLE users CHANGE username username VARCHAR(100)" 559 | ``` 560 | 561 | SET DEFAULT 562 | 563 | ```clojure 564 | (alt 565 | (-> (table :users) 566 | (set-default :ranking 1))) 567 | ; "ALTER TABLE users ALTER COLUMN ranking SET DEFAULT 1" 568 | ``` 569 | 570 | DROP COLUMN 571 | 572 | ```clojure 573 | (alt 574 | (-> (table :users) 575 | (drop-column :username))) 576 | ; "ALTER TABLE users DROP COLUMN username" 577 | ``` 578 | 579 | ## Unit-tests 580 | 581 | Run "lein spec" 582 | -------------------------------------------------------------------------------- /doc/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 11pt; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | } 10 | 11 | h2 { 12 | font-weight: normal; 13 | font-size: 18pt; 14 | margin: 10px 0 0.4em 0; 15 | } 16 | 17 | #header, #content, .sidebar { 18 | position: fixed; 19 | } 20 | 21 | #header { 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | height: 20px; 26 | background: #444; 27 | color: #fff; 28 | padding: 5px; 29 | } 30 | 31 | #content { 32 | top: 30px; 33 | right: 0; 34 | bottom: 0; 35 | overflow: auto; 36 | background: #fff; 37 | color: #333; 38 | padding: 0 1em; 39 | } 40 | 41 | .sidebar { 42 | position: fixed; 43 | top: 30px; 44 | bottom: 0; 45 | overflow: auto; 46 | } 47 | 48 | #namespaces { 49 | background: #e6e6e6; 50 | border-right: solid 1px #bbb; 51 | left: 0; 52 | width: 250px; 53 | } 54 | 55 | #vars { 56 | background: #eeeeee; 57 | border-right: solid 1px #ccc; 58 | left: 251px; 59 | width: 200px; 60 | } 61 | 62 | .namespace-index { 63 | left: 251px; 64 | } 65 | 66 | .namespace-docs { 67 | left: 452px; 68 | } 69 | 70 | #header { 71 | background: -moz-linear-gradient(top, #555 0%, #222 100%); 72 | background: -webkit-linear-gradient(top, #555 0%, #333 100%); 73 | background: -o-linear-gradient(top, #555 0%, #222 100%); 74 | background: -ms-linear-gradient(top, #555 0%, #222 100%); 75 | background: linear-gradient(top, #555 0%, #222 100%); 76 | box-shadow: 0 0 10px #555; 77 | z-index: 100; 78 | } 79 | 80 | #header h1 { 81 | margin: 0; 82 | padding: 0; 83 | font-size: 12pt; 84 | font-weight: normal; 85 | text-shadow: -1px -1px 0px #333; 86 | } 87 | 88 | #header a, .sidebar a { 89 | display: block; 90 | text-decoration: none; 91 | } 92 | 93 | #header a { 94 | color: #fff; 95 | } 96 | 97 | .sidebar a { 98 | color: #333; 99 | } 100 | 101 | #header h2 { 102 | float: right; 103 | font-size: 9pt; 104 | font-weight: normal; 105 | margin: 3px 3px; 106 | color: #bbb; 107 | } 108 | 109 | #header h2 a { 110 | display: inline; 111 | } 112 | 113 | .sidebar h3 { 114 | margin: 0; 115 | padding: 10px 0.5em 0 0.5em; 116 | font-size: 14pt; 117 | font-weight: normal 118 | } 119 | 120 | .sidebar ul { 121 | padding: 0.5em 0em; 122 | margin: 0; 123 | } 124 | 125 | .sidebar li { 126 | display: block; 127 | } 128 | 129 | .sidebar li a { 130 | padding: 7px 10px; 131 | } 132 | 133 | #namespaces li.current a { 134 | border-left: 3px solid #a33; 135 | padding-left: 7px; 136 | color: #a33; 137 | } 138 | 139 | #vars li.current a { 140 | border-left: 3px solid #33a; 141 | padding-left: 7px; 142 | color: #33a; 143 | } 144 | 145 | #content h3 { 146 | margin-bottom: 0.5em; 147 | font-size: 13pt; 148 | font-weight: bold; 149 | } 150 | 151 | .public h3, h4.macro { 152 | margin: 0; 153 | float: left; 154 | } 155 | 156 | .usage { 157 | clear: both; 158 | } 159 | 160 | h4.macro { 161 | font-variant: small-caps; 162 | font-size: 13px; 163 | font-weight: bold; 164 | color: #717171; 165 | margin-top: 3px; 166 | margin-left: 10px; 167 | } 168 | 169 | .public { 170 | margin-top: 1.5em; 171 | margin-bottom: 2.0em; 172 | } 173 | 174 | .public:last-child { 175 | margin-bottom: 20%; 176 | } 177 | 178 | .namespace:last-child { 179 | margin-bottom: 10%; 180 | } 181 | 182 | .index { 183 | padding: 0; 184 | } 185 | 186 | .index * { 187 | display: inline; 188 | } 189 | 190 | .index li { 191 | padding: 0 .5em; 192 | } 193 | 194 | .index ul { 195 | padding-left: 0; 196 | } 197 | 198 | .usage code { 199 | display: block; 200 | color: #008; 201 | } 202 | 203 | .doc { 204 | margin-bottom: .5em; 205 | } 206 | 207 | .src-link a { 208 | font-size: 9pt; 209 | } 210 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | Sql 0.1.0 API documentation

Sql 0.1.0 API documentation

A DSL in Clojure for SQL query, DML, and DDL.

stch.sql

SQL DSL for query and data manipulation language
3 | (DML). Supports a majority of MySQL's statements.

stch.sql.ddl

SQL DSL for data definition language (DDL).
4 | Supports a majority of MySQL's statements for
5 | tables and databases.

Public variables and functions:

stch.sql.format

Format query and DML statements. Use with stch.sql.
6 | 

stch.sql.types

Types for query and DML statements.
7 | 

stch.sql.util

Shared utility fns for query, DML, and DDL.
8 | 

Public variables and functions:

-------------------------------------------------------------------------------- /doc/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function setCurrentVarLink() { 15 | $('#vars li').removeClass('current') 16 | $('.public'). 17 | filter(function(index) { return visibleInParent(this) }). 18 | each(function(index, element) { 19 | findLinkByFragment("#vars a", element.id). 20 | parent(). 21 | addClass('current') 22 | }) 23 | } 24 | 25 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 26 | 27 | function scrollPositionId(element) { 28 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 29 | return 'scroll::' + $(element).attr('id') + '::' + directory 30 | } 31 | 32 | function storeScrollPosition(element) { 33 | if (!hasStorage) return; 34 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 35 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 36 | } 37 | 38 | function recallScrollPosition(element) { 39 | if (!hasStorage) return; 40 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 41 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 42 | } 43 | 44 | function persistScrollPosition(element) { 45 | recallScrollPosition(element) 46 | $(element).scroll(function() { storeScrollPosition(element) }) 47 | } 48 | 49 | function sidebarContentWidth(element) { 50 | var widths = $(element).find('span').map(function() { return $(this).width() }) 51 | return Math.max.apply(Math, widths) 52 | } 53 | 54 | function resizeNamespaces() { 55 | var width = sidebarContentWidth('#namespaces') + 40 56 | $('#namespaces').css('width', width) 57 | $('#vars, .namespace-index').css('left', width + 1) 58 | $('.namespace-docs').css('left', $('#vars').width() + width + 2) 59 | } 60 | 61 | $(window).ready(resizeNamespaces) 62 | $(window).ready(setCurrentVarLink) 63 | $(window).ready(function() { persistScrollPosition('#namespaces')}) 64 | $(window).ready(function() { 65 | $('#content').scroll(setCurrentVarLink) 66 | $(window).resize(setCurrentVarLink) 67 | }) 68 | -------------------------------------------------------------------------------- /doc/stch.sql.format.html: -------------------------------------------------------------------------------- 1 | 2 | stch.sql.format documentation

stch.sql.format documentation

Format query and DML statements. Use with stch.sql.
 3 | 

*clause*

During formatting, *clause* is bound to :select, :from, :where, etc.
 4 | 

*fn-context?*

*input-params*

*param-counter*

*param-names*

*params*

Will be bound to an atom-vector that accumulates SQL parameters across
 5 | possibly-recursive function calls

*quote-identifier-fn*

*subquery?*

-to-sql

(-to-sql x)

ToSQL

clause-order

Determines the order that clauses will be placed within generated SQL
 6 | 

expand-binary-ops

(expand-binary-ops op & args)

fn-aliases

fn-handler

format

(format sql-map & params-or-opts)
Takes a SQL map and optional input parameters and returns a vector
 7 | of a SQL string and parameters, as expected by clojure.java.jdbc.
 8 | 
 9 | Input parameters will be filled into designated spots according to
10 | name (if a map is provided) or by position (if a sequence is provided).
11 | 
12 | Instead of passing parameters, you can use keyword arguments:
13 |   :params - input parameters
14 |   :quoting - quote style to use for identifiers; one of :ansi (PostgreSQL),
15 |              :mysql, :sqlserver, or :oracle. Defaults to no quoting.
16 |   :return-param-names - when true, returns a vector of
17 |                         [sql-str param-values param-names]

format-clause

Takes a map entry representing a clause and returns an SQL string
18 | 

format-join

(format-join type table pred)

format-predicate

(format-predicate pred & {:keys [quoting]})
Formats a predicate (e.g., for WHERE, JOIN, or HAVING) as a string.
19 | 

format-predicate*

(format-predicate* pred)

infix-fns

known-clauses

quote-identifier

(quote-identifier x & {:keys [style split], :or {split true}})

sqlable?

(sqlable? x)

to-sql

(to-sql x)
-------------------------------------------------------------------------------- /doc/stch.sql.html: -------------------------------------------------------------------------------- 1 | 2 | stch.sql documentation

stch.sql documentation

SQL DSL for query and data manipulation language
3 | (DML). Supports a majority of MySQL's statements.

asc

(asc field)

build-clause

collify

(collify x)

columns

(columns & args__266__auto__)

defhelper

macro

(defhelper helper arglist & more)

delete-from

(delete-from & args__266__auto__)

desc

(desc field)

from

(from & args__266__auto__)

group

(group & args)

having

(having & args)

insert-into

(insert-into table)(insert-into m table)

join

(join & args__266__auto__)

left-join

(left-join & args__266__auto__)

limit

(limit & args__266__auto__)

modifiers

(modifiers & args__266__auto__)

offset

(offset & args__266__auto__)

on-dup-key

(on-dup-key vs)(on-dup-key m vs)

order-by

(order-by & args__266__auto__)

query-values

(query-values vs)(query-values m vs)

replace-columns

(replace-columns & args__266__auto__)

replace-from

(replace-from & args__266__auto__)

replace-group

(replace-group & args)

replace-having

(replace-having & args)

replace-into

(replace-into table)(replace-into m table)

replace-join

(replace-join & args__266__auto__)

replace-left-join

(replace-left-join & args__266__auto__)

replace-modifiers

(replace-modifiers & args__266__auto__)

replace-order-by

(replace-order-by & args__266__auto__)

replace-right-join

(replace-right-join & args__266__auto__)

replace-select

(replace-select & args__266__auto__)

replace-values

(replace-values vs)(replace-values m vs)

replace-where

(replace-where & args)

right-join

(right-join & args__266__auto__)

select

(select & args__266__auto__)

setv

(setv vs)(setv m vs)

un-select

(un-select & args__266__auto__)

union

(union & select-stmts)

update

(update & args__266__auto__)

using

(using & args__266__auto__)

values

(values vs)(values m vs)

values'

(values' x)

where

(where & args)
-------------------------------------------------------------------------------- /doc/stch.sql.types.html: -------------------------------------------------------------------------------- 1 | 2 | stch.sql.types documentation

stch.sql.types documentation

Types for query and DML statements.
3 | 

->SqlCall

(->SqlCall name args _meta)
Positional factory function for class stch.sql.types.SqlCall.
4 | 

->SqlParam

(->SqlParam name _meta)
Positional factory function for class stch.sql.types.SqlParam.
5 | 

->SqlRaw

(->SqlRaw s _meta)
Positional factory function for class stch.sql.types.SqlRaw.
6 | 

call

(call name & args)
Represents a SQL function call. Name should be a keyword.
7 | 

param

(param name)
Represents a SQL parameter which can be filled in later
8 | 

param-name

(param-name param)

raw

(raw s)
Represents a raw SQL string
9 | 

read-sql-call

(read-sql-call form)

read-sql-param

(read-sql-param form)

read-sql-raw

(read-sql-raw form)
-------------------------------------------------------------------------------- /doc/stch.sql.util.html: -------------------------------------------------------------------------------- 1 | 2 | stch.sql.util documentation

stch.sql.util documentation

Shared utility fns for query, DML, and DDL.
3 | 

comma-join

(comma-join s)

pad

(pad s)

paren-wrap

(paren-wrap x)

space-join

(space-join s)

undasherize

(undasherize s)
-------------------------------------------------------------------------------- /examples/sql.clj: -------------------------------------------------------------------------------- 1 | ;; To be run in a REPL 2 | 3 | 4 | ; DSL 5 | (require '[stch.sql.format :as sql]) 6 | (use 'stch.sql.dsl) 7 | 8 | (-> (union (-> (select :name :email) 9 | (from :users)) 10 | (-> (select :name :email) 11 | (from :deleted-users))) 12 | sql/format) 13 | 14 | (-> (update :foo) 15 | (setv {:name "Billy", :age 35}) 16 | (where '(= userID 234)) 17 | sql/format) 18 | 19 | (-> (update :items :month) 20 | (setv '{items.price month.price}) 21 | (where '(= items.id months.id)) 22 | sql/format) 23 | 24 | (-> (update :items :month) 25 | (modifiers :ignore) 26 | (setv '{items.price month.price}) 27 | (where '(= items.id months.id)) 28 | sql/format) 29 | 30 | (-> (delete-from :foo) 31 | (where '(= email "billy@bob.com")) 32 | sql/format) 33 | 34 | (-> (delete-from :foo) 35 | (modifiers :ignore) 36 | (where '(= email "billy@bob.com")) 37 | sql/format) 38 | 39 | (-> (delete-from :t1) 40 | (using :t1 :t2) 41 | (where '(= t1.x t2.x) 42 | '(= t2.y 3)) 43 | sql/format) 44 | 45 | (-> (insert-into :foo) 46 | (columns :name :age) 47 | (values [["Billy" 35] ["Joey" 37]]) 48 | sql/format) 49 | 50 | (-> (insert-into :foo) 51 | (values [{:name "Billy" :age 35} 52 | {:name "Joey" :age 37}]) 53 | sql/format) 54 | 55 | (-> (insert-into :foo) 56 | (modifiers :ignore) 57 | (values [{:name "Billy" :activated '(now)}]) 58 | sql/format) 59 | 60 | (-> (insert-into :foo) 61 | (columns :a :b :c) 62 | (select :x.a :y.b :z.c) 63 | (from :x) 64 | (join :y '(= x.id y.id) 65 | :z '(= y.id z.id)) 66 | sql/format) 67 | 68 | (-> (insert-into :foo) 69 | (columns :a :b :c) 70 | (values [[1 2 3]]) 71 | (on-dup-key {:c 9}) 72 | sql/format) 73 | 74 | (-> (insert-into :foo) 75 | (columns :a :b :c) 76 | (values [[1 2 3]]) 77 | (on-dup-key {:c (values' :a)}) 78 | sql/format) 79 | 80 | (-> (replace-into :foo) 81 | (columns :a :b :c) 82 | (values [[1 2 3]]) 83 | sql/format) 84 | 85 | (-> (select :*) 86 | (from :foo) 87 | (where '(= firstName "Billy")) 88 | (where '(= lastName "Bob")) 89 | sql/format) 90 | 91 | (-> (select [:firstName :name]) 92 | (from [:systemUsers :users]) 93 | (where '(and (= firstName "Billy") 94 | (= lastName "Bob"))) 95 | (group '(count some_field)) 96 | sql/format) 97 | 98 | (let [match-name 99 | '((= firstName "Billy") 100 | (= lastName "Bob")) 101 | match-alias "billy-bob"] 102 | (-> (select [:firstName :name]) 103 | (from [:systemUsers :users]) 104 | (where `(or (and ~@match-name) 105 | (= alias ~match-alias))) 106 | (group '(count some_field)) 107 | sql/format)) 108 | 109 | (-> (select :*) 110 | (from :users) 111 | (where '(in id [1 2 3])) 112 | sql/format) 113 | 114 | (let [ids [1 2 3]] 115 | (-> (select :*) 116 | (from :users) 117 | (where `(not-in id ~ids)) 118 | sql/format)) 119 | 120 | (-> (select '(now)) 121 | sql/format) 122 | 123 | (-> (select '(count-distinct id)) 124 | sql/format) 125 | 126 | (-> (select '(<> 1 2)) sql/format) 127 | 128 | (-> (select 129 | '(concat 130 | (if (= gender "M") 131 | "Mr." "Ms.") 132 | lastName)) 133 | sql/format) 134 | 135 | (-> (select '(<> 1 (<> 2 3))) 136 | sql/format) 137 | 138 | (let [field :id] 139 | (-> (apply select [field :name :email]) 140 | (from :foo) 141 | sql/format)) 142 | 143 | (let [n "Billy"] 144 | (-> (select :*) 145 | (from :foo) 146 | (join :bar '(= foo.x bar.x)) 147 | (join [:baz :b] '(= bar.x b.x)) 148 | (where `(= name ~n)) 149 | sql/format)) 150 | 151 | (-> (select :*) 152 | (from :users) 153 | (join :contacts 154 | '(= users.id contacts.id)) 155 | sql/format) 156 | 157 | (-> (select :*) 158 | (from :foo) 159 | (join :bar '(= foo.x bar.x) 160 | [:baz :b] '(= bar.x b.x)) 161 | sql/format) 162 | 163 | (-> (select :*) 164 | (from :foo) 165 | (join :bar '(and (= foo.x bar.x) 166 | (= foo.y bar.y))) 167 | sql/format) 168 | 169 | (-> (select :foo.name) 170 | (from :foo) 171 | (where '(= activated (curdate)) 172 | '(= userID 3)) 173 | (sql/format)) 174 | 175 | (-> (select :foo.name) 176 | (from :foo) 177 | (where '(= deactivateDate nil)) 178 | (sql/format)) 179 | 180 | (-> (select :foo.name) 181 | (from :foo) 182 | (where '(is deactivateDate nil)) 183 | (sql/format)) 184 | 185 | (-> (select :foo.name) 186 | (from :foo) 187 | (where '(is-not deactivateDate nil)) 188 | (sql/format)) 189 | 190 | (-> (select :foo.name, :?name) 191 | (from :foo) 192 | (where '(= name ?name) 193 | '(= userID ?userID)) 194 | (sql/format :params {:name "Billy" 195 | :userID 3})) 196 | 197 | (-> (select :foo.name) 198 | (from :foo) 199 | (where '(= name ?name) 200 | '(= userID ?userID)) 201 | (sql/format :params ["Billy" 3])) 202 | 203 | (-> (select :foo.name) 204 | (from :foo) 205 | (where '(= name ?) 206 | '(= userID ?)) 207 | (sql/format :params ["Billy" 3])) 208 | 209 | (-> (select :*) 210 | (from :foo) 211 | (order-by [:name :desc] :activated) 212 | sql/format) 213 | 214 | (-> (select :*) 215 | (from :foo) 216 | (order-by [:name 'desc]) 217 | sql/format) 218 | 219 | (-> (select :*) 220 | (from :foo) 221 | (order-by (desc :name)) 222 | sql/format) 223 | 224 | (-> (select :*) 225 | (from :foo) 226 | (order-by (asc :name)) 227 | sql/format) 228 | 229 | (-> (select :userID) 230 | (from :foo) 231 | (order-by '(sum totalSteps)) 232 | sql/format) 233 | 234 | (-> (select :*) 235 | (from :foo) 236 | (group :org :group) 237 | sql/format) 238 | 239 | (-> (select :*) 240 | (from :foo) 241 | (from :bar) 242 | sql/format) 243 | 244 | (-> (select :*) 245 | (select :name) 246 | (from :foo) 247 | sql/format) 248 | 249 | (-> (select :*) 250 | (from :foo) 251 | (where '(= firstName "Billy")) 252 | (where '(= lastName "Bob")) 253 | sql/format) 254 | 255 | (-> (select :*) 256 | (from :foo) 257 | (join :bar '(= foo.x bar.x)) 258 | (join :baz '(= bar.x baz.x)) 259 | sql/format) 260 | 261 | (-> (select :*) 262 | (from :foo) 263 | (group :firstName) 264 | (group :lastName) 265 | sql/format) 266 | 267 | (-> (select :*) 268 | (from :foo) 269 | (order-by :firstName) 270 | (order-by :lastName) 271 | sql/format) 272 | 273 | (-> (select :*) 274 | (from :foo) 275 | (having '(> (count this) 2)) 276 | (having '(> (count that) 2)) 277 | sql/format) 278 | 279 | (-> (select '(now) 280 | '(sum points) 281 | '(date_format dob "%m/%d/%Y") 282 | :email 283 | [:artists.name :aname]) 284 | (from [:foo :f]) 285 | (join :draq '(= f.b draq.x)) 286 | (join [:artists :a] 287 | '(= foo.artist_id a.artist_id)) 288 | (where '(= a ?baz) 289 | '(!= b "Elvis") 290 | '(or (= c "Beatles") 291 | (= c "Rolling Stones"))) 292 | (group :foo.id '(date activated)) 293 | (having '(< f.e 50)) 294 | (order-by (asc :c.quux)) 295 | (limit 50) 296 | (sql/format :params {:baz "BAZ"} 297 | :quoting :mysql)) 298 | 299 | ; DDL 300 | (use 'stch.sql.ddl) 301 | 302 | (create (db :prod) (character-set :utf8)) 303 | 304 | (alt-db :prod (character-set :utf8)) 305 | 306 | (drop-db :prod) 307 | (drop-db (db :prod)) 308 | 309 | (-> (columns) 310 | (integer :userID :unsigned :not-null)) 311 | 312 | (defcolumns user-id 313 | (integer :userID :unsigned :not-null) 314 | (index :userID)) 315 | 316 | (defcolumns org-id 317 | (integer :orgID :unsigned :not-null) 318 | (index :orgID)) 319 | 320 | (-> (table :contest) 321 | (append user-id) 322 | (append org-id)) 323 | 324 | (deftable contest 325 | (append user-id) 326 | (append org-id)) 327 | 328 | (create 329 | (-> (table :users) 330 | (set' :groups ["user" "admin"] (default "user")) 331 | (enum :status ["active" "inactive"]) 332 | (decimal :ranking '(3 1) (default 0)) 333 | (chr :countryCode [2] (default "US")))) 334 | 335 | (create 336 | (-> (table :users) 337 | (integer :userID :unsigned :not-null :primary-key) 338 | (varchar :username [50]) 339 | (constraint :uname (unique :username)) 340 | (constraint :fk1 (foreign-key :userID 341 | '(users userID) 342 | :on-delete-cascade)))) 343 | 344 | (create 345 | (-> (table :users) 346 | (integer :userID :unsigned :not-null) 347 | (integer :orgID) 348 | (set' :groups ["user" "admin"] (default "user")) 349 | (enum :status ["active" "inactive"]) 350 | (decimal :ranking '(3 1) (default 0)) 351 | (varchar :username [50]) 352 | (chr :countryCode [2] (default "US")) 353 | (primary-key :userID) 354 | (index [:userID :orgID]) 355 | (unique :username) 356 | (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 357 | (engine :InnoDB) 358 | (collate :utf8-general-ci)) 359 | 360 | (alt 361 | (-> (table :users) 362 | (add (varchar :email [50]) (after :userID)) 363 | (add (varchar :firstName [25]) :first) 364 | (add (index [:firstName :lastName])) 365 | (add (index '(username ranking))) 366 | (add (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 367 | (change :username (varchar :username [100])) 368 | (drop-default :ranking) 369 | (set-default :ranking 1) 370 | (drop-column :countryCode) 371 | (drop-index :uname) 372 | (drop-primary-key) 373 | (drop-foreign-key :fk1))) 374 | 375 | (alt (-> (table :system-users) 376 | (rename :users))) 377 | 378 | (def first-name (varchar :firstName [50])) 379 | 380 | (create 381 | (-> (table :users) 382 | (append first-name) 383 | (index first-name))) 384 | 385 | (defcolumns full-name 386 | (varchar :firstName [50]) 387 | (varchar :lastName [50])) 388 | 389 | (create 390 | (-> (table :users) 391 | (append full-name) 392 | (index full-name))) 393 | 394 | (alt 395 | (-> (table :users) 396 | (add first-name))) 397 | 398 | (alt 399 | (-> (table :users) 400 | (add full-name))) 401 | 402 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject stch-library/sql "0.1.4" 2 | :description 3 | "A DSL in Clojure for SQL query, DML, and DDL." 4 | :url "https://github.com/stch-library/sql" 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :dependencies [[stch-library/schema "0.3.3"]] 8 | :profiles 9 | {:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 10 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} 11 | :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} 12 | :dev [:1.10 13 | {:dependencies [[speclj "3.0.2"]]}]} 14 | :plugins [[speclj "3.0.2"] 15 | [codox "0.6.7"]] 16 | :codox {:src-dir-uri "https://github.com/stch-library/sql/blob/master/" 17 | :src-linenum-anchor-prefix "L"} 18 | :test-paths ["spec"] 19 | :aliases {"test-all" ["with-profile" "1.8,dev:1.9,dev:1.10,dev" "spec"]}) 20 | -------------------------------------------------------------------------------- /spec/stch/sql/ddl_spec.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql.ddl-spec 2 | (:require 3 | ;; exclude speclj.core/update 4 | [speclj.core :refer [describe around context it should=]] 5 | [stch.schema :refer [with-fn-validation]] 6 | [stch.sql.ddl :refer :all] 7 | [stch.util :refer [named?]])) 8 | 9 | (describe "create table" 10 | (around [it] 11 | (with-fn-validation (it))) 12 | (context "columns" 13 | (context "numeric" 14 | (it "bool" 15 | (should= "CREATE TABLE users (enabled BOOL)" 16 | (create 17 | (-> (table :users) 18 | (bool :enabled))))) 19 | (it "tiny-int" 20 | (should= "CREATE TABLE users (age TINYINT)" 21 | (create 22 | (-> (table :users) 23 | (tiny-int :age))))) 24 | (it "small-int" 25 | (should= "CREATE TABLE users (points SMALLINT)" 26 | (create 27 | (-> (table :users) 28 | (small-int :points))))) 29 | (it "medium-int" 30 | (should= "CREATE TABLE users (points MEDIUMINT)" 31 | (create 32 | (-> (table :users) 33 | (medium-int :points))))) 34 | (it "integer" 35 | (should= "CREATE TABLE users (points INT)" 36 | (create 37 | (-> (table :users) 38 | (integer :points))))) 39 | (it "big-int" 40 | (should= "CREATE TABLE users (points BIGINT)" 41 | (create 42 | (-> (table :users) 43 | (big-int :points))))) 44 | (it "decimal" 45 | (should= "CREATE TABLE users (score DECIMAL(3, 1))" 46 | (create 47 | (-> (table :users) 48 | (decimal :score [3 1]))))) 49 | (it "float'" 50 | (should= "CREATE TABLE users (points FLOAT)" 51 | (create 52 | (-> (table :users) 53 | (float' :points))))) 54 | (it "double'" 55 | (should= "CREATE TABLE users (points DOUBLE)" 56 | (create 57 | (-> (table :users) 58 | (double' :points)))))) 59 | (context "serial" 60 | (it "small-serial" 61 | (should= "CREATE TABLE users (id SMALLSERIAL)" 62 | (create 63 | (-> (table :users) 64 | (small-serial :id))))) 65 | (it "serial" 66 | (should= "CREATE TABLE users (id SERIAL)" 67 | (create 68 | (-> (table :users) 69 | (serial :id))))) 70 | (it "big-serial" 71 | (should= "CREATE TABLE users (id BIGSERIAL)" 72 | (create 73 | (-> (table :users) 74 | (big-serial :id)))))) 75 | (context "string" 76 | (it "chr" 77 | (should= "CREATE TABLE users (countryCode CHAR(2))" 78 | (create 79 | (-> (table :users) 80 | (chr :countryCode [2]))))) 81 | (it "varchar" 82 | (should= "CREATE TABLE users (name VARCHAR(50))" 83 | (create 84 | (-> (table :users) 85 | (varchar :name [50]))))) 86 | (it "binary" 87 | (should= "CREATE TABLE users (countryCode BINARY(2))" 88 | (create 89 | (-> (table :users) 90 | (binary :countryCode [2]))))) 91 | (it "varbinary" 92 | (should= "CREATE TABLE users (name VARBINARY(50))" 93 | (create 94 | (-> (table :users) 95 | (varbinary :name [50]))))) 96 | (it "blob" 97 | (should= "CREATE TABLE users (aboutMe BLOB)" 98 | (create 99 | (-> (table :users) 100 | (blob :aboutMe))))) 101 | (it "text" 102 | (should= "CREATE TABLE users (aboutMe TEXT)" 103 | (create 104 | (-> (table :users) 105 | (text :aboutMe))))) 106 | (it "enum" 107 | (should= "CREATE TABLE users (status ENUM('active', 'inactive'))" 108 | (create 109 | (-> (table :users) 110 | (enum :status ["active" "inactive"]))))) 111 | (it "set'" 112 | (should= "CREATE TABLE users (groups SET('user', 'admin'))" 113 | (create 114 | (-> (table :users) 115 | (set' :groups ["user" "admin"])))))) 116 | (context "date and time" 117 | (it "date" 118 | (should= "CREATE TABLE users (activated DATE)" 119 | (create 120 | (-> (table :users) 121 | (date :activated))))) 122 | (it "datetime" 123 | (should= "CREATE TABLE users (activated DATETIME)" 124 | (create 125 | (-> (table :users) 126 | (datetime :activated))))) 127 | (it "time'" 128 | (should= "CREATE TABLE logs (instant TIME)" 129 | (create 130 | (-> (table :logs) 131 | (time' :instant))))) 132 | (it "year" 133 | (should= "CREATE TABLE users (birthYear YEAR)" 134 | (create 135 | (-> (table :users) 136 | (year :birthYear)))))) 137 | (context "spatial" 138 | (it "geometry" 139 | (should= "CREATE TABLE geom (g GEOMETRY)" 140 | (create 141 | (-> (table :geom) 142 | (geometry :g))))) 143 | (it "point" 144 | (should= "CREATE TABLE geom (p POINT)" 145 | (create 146 | (-> (table :geom) 147 | (point :p))))) 148 | (it "linestring" 149 | (should= "CREATE TABLE geom (l LINESTRING)" 150 | (create 151 | (-> (table :geom) 152 | (linestring :l))))) 153 | (it "polygon" 154 | (should= "CREATE TABLE geom (p POLYGON)" 155 | (create 156 | (-> (table :geom) 157 | (polygon :p)))))) 158 | (context "options" 159 | (it "default" 160 | (should= "CREATE TABLE users (countryCode CHAR(2) DEFAULT 'US')" 161 | (create 162 | (-> (table :users) 163 | (chr :countryCode [2] (default "US")))))) 164 | (it "keywords" 165 | (should= "CREATE TABLE users (user_id INT UNSIGNED NOT NULL PRIMARY KEY)" 166 | (create 167 | (-> (table :users) 168 | (integer :user_id :unsigned :not-null :primary-key))))) 169 | (it "auto increment" 170 | (should= "CREATE TABLE users (user_id INT AUTO_INCREMENT)" 171 | (create 172 | (-> (table :users) 173 | (integer :user_id :auto-increment))))))) 174 | (context "indexes" 175 | (it "primary-key" 176 | (should= "CREATE TABLE users (user_id INT, PRIMARY KEY(user_id))" 177 | (create 178 | (-> (table :users) 179 | (integer :user_id) 180 | (primary-key :user_id))))) 181 | (context "index" 182 | (it "single column" 183 | (should= "CREATE TABLE users (user_id INT, INDEX(user_id))" 184 | (create 185 | (-> (table :users) 186 | (integer :user_id) 187 | (index :user_id))))) 188 | (it "multi-column" 189 | (should= "CREATE TABLE users (first_name VARCHAR(100), last_name VARCHAR(100), INDEX(first_name, last_name))" 190 | (create 191 | (-> (table :users) 192 | (varchar :first_name [100]) 193 | (varchar :last_name [100]) 194 | (index [:first_name :last_name]))))) 195 | (let [first-name (varchar :first_name [100])] 196 | (it "column record" 197 | (should= "CREATE TABLE users (first_name VARCHAR(100), INDEX(first_name))" 198 | (create 199 | (-> (table :users) 200 | (append first-name) 201 | (index first-name))))))) 202 | (it "unique" 203 | (should= "CREATE TABLE users (user_id INT, UNIQUE(user_id))" 204 | (create 205 | (-> (table :users) 206 | (integer :user_id) 207 | (unique :user_id))))) 208 | (it "foreign key" 209 | (should= "CREATE TABLE contacts (user_id INT, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE)" 210 | (create 211 | (-> (table :contacts) 212 | (integer :user_id) 213 | (foreign-key :user_id '(users user_id) :on-delete-cascade))))) 214 | (it "fulltext" 215 | (should= "CREATE TABLE users (name VARCHAR(50), FULLTEXT(name))" 216 | (create 217 | (-> (table :users) 218 | (varchar :name [50]) 219 | (fulltext :name))))) 220 | (it "spatial" 221 | (should= "CREATE TABLE geom (g GEOMETRY, SPATIAL INDEX(g))" 222 | (create 223 | (-> (table :geom) 224 | (geometry :g) 225 | (spatial :g))))) 226 | (it "constraint (named keys)" 227 | (should= "CREATE TABLE users (username VARCHAR(50), CONSTRAINT uname UNIQUE(username))" 228 | (create 229 | (-> (table :users) 230 | (varchar :username [50]) 231 | (constraint :uname (unique :username))))))) 232 | (context "table options" 233 | (it "engine" 234 | (should= "CREATE TABLE users (username VARCHAR(50)) ENGINE=InnoDB" 235 | (create 236 | (-> (table :users) 237 | (varchar :username [50])) 238 | (engine :InnoDB)))) 239 | (it "collate" 240 | (should= "CREATE TABLE users (username VARCHAR(50)) COLLATE=utf8_general_ci" 241 | (create 242 | (-> (table :users) 243 | (varchar :username [50])) 244 | (collate :utf8-general-ci)))) 245 | (it "character-set" 246 | (should= "CREATE TABLE users (username VARCHAR(50)) CHARACTER SET=utf8" 247 | (create 248 | (-> (table :users) 249 | (varchar :username [50])) 250 | (character-set :utf8)))) 251 | (it "auto-inc" 252 | (should= "CREATE TABLE users (username VARCHAR(50)) AUTO_INCREMENT=1000" 253 | (create 254 | (-> (table :users) 255 | (varchar :username [50])) 256 | (auto-inc 1000))))) 257 | (context "append" 258 | (let [cols (-> (columns) 259 | (integer :user-id :unsigned :not-null) 260 | (index :user-id))] 261 | (it "columns" 262 | (should= "CREATE TABLE users (user_id INT UNSIGNED NOT NULL, INDEX(user_id), username VARCHAR(50))" 263 | (create 264 | (-> (table :users) 265 | (append cols) 266 | (varchar :username [50])))))) 267 | (let [col (integer :user-id :unsigned :not-null)] 268 | (it "column" 269 | (should= "CREATE TABLE users (user_id INT UNSIGNED NOT NULL, username VARCHAR(50))" 270 | (create 271 | (-> (table :users) 272 | (append col) 273 | (varchar :username [50])))))) 274 | (let [idx (index :user-id)] 275 | (it "index" 276 | (should= "CREATE TABLE users (user_id INT UNSIGNED NOT NULL, INDEX(user_id))" 277 | (create 278 | (-> (table :users) 279 | (integer :user-id :unsigned :not-null) 280 | (append idx))))))) 281 | (it "temporary" 282 | (should= "CREATE TEMPORARY TABLE users (userID INT)" 283 | (create 284 | (-> (temp-table :users) 285 | (integer :userID))))) 286 | (it "complex" 287 | (should= "CREATE TABLE users (userID INT UNSIGNED NOT NULL, orgID INT, groups SET('user', 'admin') DEFAULT 'user', status ENUM('active', 'inactive'), ranking DECIMAL(3, 1) DEFAULT 0, username VARCHAR(50), countryCode CHAR(2) DEFAULT 'US', PRIMARY KEY(userID), INDEX(userID, orgID), UNIQUE(username), FOREIGN KEY(orgID) REFERENCES orgs(orgID) ON DELETE CASCADE) ENGINE=InnoDB, COLLATE=utf8_general_ci" 288 | (create 289 | (-> (table :users) 290 | (integer :userID :unsigned :not-null) 291 | (integer :orgID) 292 | (set' :groups ["user" "admin"] (default "user")) 293 | (enum :status ["active" "inactive"]) 294 | (decimal :ranking '(3 1) (default 0)) 295 | (varchar :username [50]) 296 | (chr :countryCode [2] (default "US")) 297 | (primary-key :userID) 298 | (index [:userID :orgID]) 299 | (unique :username) 300 | (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 301 | (engine :InnoDB) 302 | (collate :utf8-general-ci))))) 303 | 304 | (describe "alter table" 305 | (around [it] 306 | (with-fn-validation (it))) 307 | (context "add column" 308 | (it "no position" 309 | (should= "ALTER TABLE users ADD COLUMN email VARCHAR(50)" 310 | (alt 311 | (-> (table :users) 312 | (add (varchar :email [50])))))) 313 | (context "after" 314 | (it "keyword" 315 | (should= "ALTER TABLE users ADD COLUMN email VARCHAR(50) AFTER userID" 316 | (alt 317 | (-> (table :users) 318 | (add (varchar :email [50]) (after :userID)))))) 319 | (it "column record" 320 | (should= "ALTER TABLE users ADD COLUMN email VARCHAR(50) AFTER userID" 321 | (alt 322 | (-> (table :users) 323 | (add (varchar :email [50]) 324 | (after (integer :userID)))))))) 325 | (it "first" 326 | (should= "ALTER TABLE users ADD COLUMN email VARCHAR(50) FIRST" 327 | (alt 328 | (-> (table :users) 329 | (add (varchar :email [50]) :first))))) 330 | (let [cols (-> (columns) 331 | (varchar :firstName [50]) 332 | (varchar :lastName [50]))] 333 | (it "columns" 334 | (should= "ALTER TABLE users ADD COLUMN firstName VARCHAR(50), ADD COLUMN lastName VARCHAR(50)" 335 | (alt 336 | (-> (table :users) 337 | (add cols))))))) 338 | (context "add index" 339 | (it "index" 340 | (should= "ALTER TABLE users ADD INDEX(firstName)" 341 | (alt 342 | (-> (table :users) 343 | (add (index :firstName)))))) 344 | (it "foreign key" 345 | (should= "ALTER TABLE users ADD FOREIGN KEY(orgID) REFERENCES orgs(orgID) ON DELETE CASCADE" 346 | (alt 347 | (-> (table :users) 348 | (add (foreign-key :orgID '(orgs orgID) :on-delete-cascade))))))) 349 | (context "change" 350 | (it "keyword" 351 | (should= "ALTER TABLE users CHANGE username username VARCHAR(100)" 352 | (alt 353 | (-> (table :users) 354 | (change :username (varchar :username [100])))))) 355 | (let [username (varchar :username [50])] 356 | (it "column record" 357 | (should= "ALTER TABLE users CHANGE username username VARCHAR(100)" 358 | (alt 359 | (-> (table :users) 360 | (change username (varchar :username [100])))))))) 361 | (context "set-default" 362 | (it "keyword" 363 | (should= "ALTER TABLE users ALTER COLUMN ranking SET DEFAULT 1" 364 | (alt 365 | (-> (table :users) 366 | (set-default :ranking 1))))) 367 | (let [ranking (integer :ranking)] 368 | (it "column record" 369 | (should= "ALTER TABLE users ALTER COLUMN ranking SET DEFAULT 1" 370 | (alt 371 | (-> (table :users) 372 | (set-default ranking 1))))))) 373 | (context "drop-default" 374 | (it "keyword" 375 | (should= "ALTER TABLE users ALTER COLUMN ranking DROP DEFAULT" 376 | (alt 377 | (-> (table :users) 378 | (drop-default :ranking))))) 379 | (let [ranking (integer :ranking)] 380 | (it "column record" 381 | (should= "ALTER TABLE users ALTER COLUMN ranking DROP DEFAULT" 382 | (alt 383 | (-> (table :users) 384 | (drop-default ranking))))))) 385 | (context "drop-column" 386 | (it "keyword" 387 | (should= "ALTER TABLE users DROP COLUMN username" 388 | (alt 389 | (-> (table :users) 390 | (drop-column :username))))) 391 | (let [username (varchar :username [25])] 392 | (it "column record" 393 | (should= "ALTER TABLE users DROP COLUMN username" 394 | (alt 395 | (-> (table :users) 396 | (drop-column username))))))) 397 | (context "drop-index" 398 | (it "keyword" 399 | (should= "ALTER TABLE users DROP INDEX uname" 400 | (alt 401 | (-> (table :users) 402 | (drop-index :uname))))) 403 | (let [idx (constraint :uname (index :username))] 404 | (it "constraint record" 405 | (should= "ALTER TABLE users DROP INDEX uname" 406 | (alt 407 | (-> (table :users) 408 | (drop-index idx))))))) 409 | (it "drop-primary-key" 410 | (should= "ALTER TABLE users DROP PRIMARY KEY" 411 | (alt 412 | (-> (table :users) 413 | (drop-primary-key))))) 414 | (context "drop-foreign-key" 415 | (it "keyword" 416 | (should= "ALTER TABLE users DROP FOREIGN KEY fk1" 417 | (alt 418 | (-> (table :users) 419 | (drop-foreign-key :fk1))))) 420 | (let [idx (constraint :fk1 (foreign-key :orgID '(orgs orgID) :on-delete-cascade))] 421 | (it "constraint record" 422 | (should= "ALTER TABLE users DROP FOREIGN KEY fk1" 423 | (alt 424 | (-> (table :users) 425 | (drop-foreign-key idx))))))) 426 | (context "rename table" 427 | (it "keyword" 428 | (should= "ALTER TABLE system_users RENAME users" 429 | (alt (-> (table :system-users) 430 | (rename :users))))) 431 | (let [tbl (table :users)] 432 | (it "table record" 433 | (should= "ALTER TABLE system_users RENAME users" 434 | (alt (-> (table :system-users) 435 | (rename tbl))))))) 436 | (it "complex" 437 | (should= "ALTER TABLE users ADD COLUMN email VARCHAR(50) AFTER userID, ADD COLUMN firstName VARCHAR(25) FIRST, ADD INDEX(firstName, lastName), ADD INDEX(username, ranking), ADD FOREIGN KEY(orgID) REFERENCES orgs(orgID) ON DELETE CASCADE, CHANGE username username VARCHAR(100), ALTER COLUMN ranking DROP DEFAULT, ALTER COLUMN ranking SET DEFAULT 1, DROP COLUMN countryCode, DROP INDEX uname, DROP PRIMARY KEY, DROP FOREIGN KEY fk1" 438 | (alt 439 | (-> (table :users) 440 | (add (varchar :email [50]) (after :userID)) 441 | (add (varchar :firstName [25]) :first) 442 | (add (index [:firstName :lastName])) 443 | (add (index '(username ranking))) 444 | (add (foreign-key :orgID '(orgs orgID) :on-delete-cascade)) 445 | (change :username (varchar :username [100])) 446 | (drop-default :ranking) 447 | (set-default :ranking 1) 448 | (drop-column :countryCode) 449 | (drop-index :uname) 450 | (drop-primary-key) 451 | (drop-foreign-key :fk1)))))) 452 | 453 | (describe "drop table" 454 | (around [it] 455 | (with-fn-validation (it))) 456 | (context "drop-table" 457 | (it "keyword" 458 | (should= "DROP TABLE users, contacts" 459 | (drop-table :users :contacts))) 460 | (it "table record" 461 | (should= "DROP TABLE users, contacts" 462 | (drop-table (table :users) 463 | (table :contacts))))) 464 | (context "drop-temp-table" 465 | (it "keyword" 466 | (should= "DROP TEMPORARY TABLE users, contacts" 467 | (drop-temp-table :users :contacts))) 468 | (it "table record" 469 | (should= "DROP TEMPORARY TABLE users, contacts" 470 | (drop-temp-table (table :users) 471 | (table :contacts)))))) 472 | 473 | (describe "truncate table" 474 | (around [it] 475 | (with-fn-validation (it))) 476 | (it "keyword" 477 | (should= "TRUNCATE TABLE users" 478 | (truncate :users))) 479 | (it "table record" 480 | (should= "TRUNCATE TABLE users" 481 | (truncate (table :users))))) 482 | 483 | (describe "db" 484 | (around [it] 485 | (with-fn-validation (it))) 486 | (context "create" 487 | (it "no options" 488 | (should= "CREATE DATABASE prod" 489 | (create (db :prod)))) 490 | (it "with options" 491 | (should= "CREATE DATABASE prod CHARACTER SET=utf8" 492 | (create (db :prod) (character-set :utf8))))) 493 | (context "alt-db" 494 | (it "keyword" 495 | (should= "ALTER DATABASE prod CHARACTER SET=utf8" 496 | (alt-db :prod (character-set :utf8)))) 497 | (it "db record" 498 | (should= "ALTER DATABASE prod CHARACTER SET=utf8" 499 | (alt-db (db :prod) (character-set :utf8))))) 500 | (context "drop-db" 501 | (it "keyword" 502 | (should= "DROP DATABASE prod" 503 | (drop-db :prod))) 504 | (it "db record" 505 | (should= "DROP DATABASE prod" 506 | (drop-db (db :prod))))) 507 | (context "rename-db" 508 | (it "keyword" 509 | (should= "RENAME DATABASE prod TO production" 510 | (rename-db :prod :production))) 511 | (it "db record" 512 | (should= "RENAME DATABASE prod TO production" 513 | (rename-db (db :prod) 514 | (db :production)))))) 515 | -------------------------------------------------------------------------------- /spec/stch/sql_spec.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql-spec 2 | (:refer-clojure :exclude [update]) 3 | (:require 4 | [speclj.core :refer :all] 5 | [stch.sql :refer :all] 6 | [stch.sql.format :as sql])) 7 | 8 | (describe "select" 9 | (context "select" 10 | (it "keyword" 11 | (should= ["SELECT *"] 12 | (-> (select :*) 13 | sql/format))) 14 | (it "multiple keywords" 15 | (should= ["SELECT name, email"] 16 | (-> (select :name :email) 17 | sql/format))) 18 | (it "select x2" 19 | (should= ["SELECT name, email"] 20 | (-> (select :name) 21 | (select :email) 22 | sql/format))) 23 | (it "dashes to underscores" 24 | (should= ["SELECT first_name"] 25 | (-> (select :first-name) 26 | sql/format))) 27 | (it "table.field" 28 | (should= ["SELECT users.name"] 29 | (-> (select :users.name) 30 | sql/format))) 31 | (it "symbol" 32 | (should= ["SELECT name"] 33 | (-> (select 'name) 34 | sql/format))) 35 | (it "string" 36 | (should= ["SELECT ?" "name"] 37 | (-> (select "name") 38 | sql/format))) 39 | (it "nil" 40 | (should= ["SELECT NULL"] 41 | (-> (select nil) 42 | sql/format))) 43 | (it "as" 44 | (should= ["SELECT firstName AS name"] 45 | (-> (select [:firstName :name]) 46 | sql/format))) 47 | (it "prefix function" 48 | (should= ["SELECT now()"] 49 | (-> (select '(now)) 50 | sql/format))) 51 | (it "infix function" 52 | (should= ["SELECT 1 <> 2"] 53 | (-> (select '(<> 1 2)) 54 | sql/format))) 55 | (it "nested functions" 56 | (should= ["SELECT concat(if(gender = ?, ?, ?), lastName)" "M" "Mr." "Ms."] 57 | (-> (select 58 | '(concat 59 | (if (= gender "M") 60 | "Mr." "Ms.") 61 | lastName)) 62 | sql/format)))) 63 | (context "from" 64 | (it "keyword" 65 | (should= ["SELECT * FROM users"] 66 | (-> (select :*) 67 | (from :users) 68 | sql/format))) 69 | (it "keywords" 70 | (should= ["SELECT * FROM users, contacts"] 71 | (-> (select :*) 72 | (from :users :contacts) 73 | sql/format))) 74 | (it "from x2" 75 | (should= ["SELECT * FROM users, contacts"] 76 | (-> (select :*) 77 | (from :users) 78 | (from :contacts) 79 | sql/format))) 80 | (it "symbol" 81 | (should= ["SELECT * FROM users"] 82 | (-> (select :*) 83 | (from 'users) 84 | sql/format))) 85 | (it "as" 86 | (should= ["SELECT * FROM organizations AS orgs"] 87 | (-> (select :*) 88 | (from [:organizations :orgs]) 89 | sql/format)))) 90 | (context "where" 91 | (it "list" 92 | (should= ["SELECT * FROM users WHERE id = 5"] 93 | (-> (select :*) 94 | (from :users) 95 | (where '(= id 5)) 96 | sql/format))) 97 | (it "quoted list" 98 | (should= ["SELECT * FROM users WHERE name = ?" "Billy"] 99 | (let [name "Billy"] 100 | (-> (select :*) 101 | (from :users) 102 | (where `(= name ~name)) 103 | sql/format)))) 104 | (it "two lists (implicit AND)" 105 | (should= ["SELECT * FROM users WHERE (id = 5 AND status = ?)" "active"] 106 | (-> (select :*) 107 | (from :users) 108 | (where '(= id 5) 109 | '(= status "active")) 110 | sql/format))) 111 | (it "AND" 112 | (should= ["SELECT * FROM users WHERE (id = 5 AND status = ?)" "active"] 113 | (-> (select :*) 114 | (from :users) 115 | (where '(and (= id 5) 116 | (= status "active"))) 117 | sql/format))) 118 | (it "OR" 119 | (should= ["SELECT * FROM users WHERE (id = 5 OR status = ?)" "active"] 120 | (-> (select :*) 121 | (from :users) 122 | (where '(or (= id 5) 123 | (= status "active"))) 124 | sql/format))) 125 | (it "where x2" 126 | (should= ["SELECT * FROM users WHERE (id = 5 AND status = ?)" "active"] 127 | (-> (select :*) 128 | (from :users) 129 | (where '(= id 5)) 130 | (where '(= status "active")) 131 | sql/format))) 132 | (it "function call in list" 133 | (should= ["SELECT * FROM users WHERE activated = curdate()"] 134 | (-> (select :*) 135 | (from :users) 136 | (where '(= activated (curdate))) 137 | sql/format))) 138 | (it "IS" 139 | (should= ["SELECT * FROM users WHERE deactivateDate IS NULL"] 140 | (-> (select :*) 141 | (from :users) 142 | (where '(is deactivateDate nil)) 143 | sql/format))) 144 | (it "IS NOT" 145 | (should= ["SELECT * FROM users WHERE deactivateDate IS NOT NULL"] 146 | (-> (select :*) 147 | (from :users) 148 | (where '(is-not deactivateDate nil)) 149 | sql/format))) 150 | (context "named parameters" 151 | (it "map params" 152 | (should= ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 153 | (-> (select :*) 154 | (from :users) 155 | (where '(= name ?name) 156 | '(= userID ?userID)) 157 | (sql/format :params {:name "Billy" 158 | :userID 3})))) 159 | (it "sequential params" 160 | (should= ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 161 | (-> (select :*) 162 | (from :users) 163 | (where '(= name ?name) 164 | '(= userID ?userID)) 165 | (sql/format :params ["Billy" 3]))))) 166 | (context "unnamed parameters" 167 | (it "sequential params" 168 | (should= ["SELECT * FROM users WHERE (name = ? AND userID = ?)" "Billy" 3] 169 | (-> (select :*) 170 | (from :users) 171 | (where '(= name ?) 172 | '(= userID ?)) 173 | (sql/format :params ["Billy" 3])))))) 174 | (context "join" 175 | (it "single join" 176 | (should= ["SELECT * FROM users INNER JOIN contacts ON users.id = contacts.id"] 177 | (-> (select :*) 178 | (from :users) 179 | (join :contacts 180 | '(= users.id contacts.id)) 181 | sql/format))) 182 | (it "multiple join" 183 | (should= ["SELECT * FROM users INNER JOIN contacts ON users.id = contacts.id INNER JOIN orgs ON users.orgid = orgs.orgid"] 184 | (-> (select :*) 185 | (from :users) 186 | (join :contacts '(= users.id contacts.id) 187 | :orgs '(= users.orgid orgs.orgid)) 188 | sql/format))) 189 | (it "join x2" 190 | (should= ["SELECT * FROM users INNER JOIN contacts ON users.id = contacts.id INNER JOIN orgs ON users.orgid = orgs.orgid"] 191 | (-> (select :*) 192 | (from :users) 193 | (join :contacts '(= users.id contacts.id)) 194 | (join :orgs '(= users.orgid orgs.orgid)) 195 | sql/format))) 196 | (it "AS" 197 | (should= ["SELECT * FROM users INNER JOIN contacts AS c ON users.id = c.id"] 198 | (-> (select :*) 199 | (from :users) 200 | (join [:contacts :c] 201 | '(= users.id c.id)) 202 | sql/format)))) 203 | (context "left join" 204 | (it "single join" 205 | (should= ["SELECT * FROM users LEFT JOIN contacts ON users.id = contacts.id"] 206 | (-> (select :*) 207 | (from :users) 208 | (left-join :contacts 209 | '(= users.id contacts.id)) 210 | sql/format))) 211 | (it "multiple join" 212 | (should= ["SELECT * FROM users LEFT JOIN contacts ON users.id = contacts.id LEFT JOIN orgs ON users.orgid = orgs.orgid"] 213 | (-> (select :*) 214 | (from :users) 215 | (left-join :contacts '(= users.id contacts.id) 216 | :orgs '(= users.orgid orgs.orgid)) 217 | sql/format))) 218 | (it "left join x2" 219 | (should= ["SELECT * FROM users LEFT JOIN contacts ON users.id = contacts.id LEFT JOIN orgs ON users.orgid = orgs.orgid"] 220 | (-> (select :*) 221 | (from :users) 222 | (left-join :contacts '(= users.id contacts.id)) 223 | (left-join :orgs '(= users.orgid orgs.orgid)) 224 | sql/format)))) 225 | (context "right join" 226 | (it "single join" 227 | (should= ["SELECT * FROM users RIGHT JOIN contacts ON users.id = contacts.id"] 228 | (-> (select :*) 229 | (from :users) 230 | (right-join :contacts 231 | '(= users.id contacts.id)) 232 | sql/format))) 233 | (it "multiple join" 234 | (should= ["SELECT * FROM users RIGHT JOIN contacts ON users.id = contacts.id RIGHT JOIN orgs ON users.orgid = orgs.orgid"] 235 | (-> (select :*) 236 | (from :users) 237 | (right-join :contacts '(= users.id contacts.id) 238 | :orgs '(= users.orgid orgs.orgid)) 239 | sql/format))) 240 | (it "right join x2" 241 | (should= ["SELECT * FROM users RIGHT JOIN contacts ON users.id = contacts.id RIGHT JOIN orgs ON users.orgid = orgs.orgid"] 242 | (-> (select :*) 243 | (from :users) 244 | (right-join :contacts '(= users.id contacts.id)) 245 | (right-join :orgs '(= users.orgid orgs.orgid)) 246 | sql/format)))) 247 | (context "order by" 248 | (it "keyword" 249 | (should= ["SELECT * FROM users ORDER BY name"] 250 | (-> (select :*) 251 | (from :users) 252 | (order-by :name) 253 | sql/format))) 254 | (it "keywords" 255 | (should= ["SELECT * FROM users ORDER BY name, age"] 256 | (-> (select :*) 257 | (from :users) 258 | (order-by :name :age) 259 | sql/format))) 260 | (it "order by x2" 261 | (should= ["SELECT * FROM users ORDER BY name, age"] 262 | (-> (select :*) 263 | (from :users) 264 | (order-by :name) 265 | (order-by :age) 266 | sql/format))) 267 | (it "asc" 268 | (should= ["SELECT * FROM users ORDER BY name ASC"] 269 | (-> (select :*) 270 | (from :users) 271 | (order-by (asc :name)) 272 | sql/format))) 273 | (it "desc" 274 | (should= ["SELECT * FROM users ORDER BY name DESC"] 275 | (-> (select :*) 276 | (from :users) 277 | (order-by (desc :name)) 278 | sql/format))) 279 | (it "function" 280 | (should= ["SELECT * FROM users INNER JOIN steps ON users.id = steps.userid ORDER BY sum(totalSteps)"] 281 | (-> (select :*) 282 | (from :users) 283 | (join :steps '(= users.id steps.userid)) 284 | (order-by '(sum totalSteps)) 285 | sql/format)))) 286 | (context "group by" 287 | (it "keyword" 288 | (should= ["SELECT * FROM users GROUP BY name"] 289 | (-> (select :*) 290 | (from :users) 291 | (group :name) 292 | sql/format))) 293 | (it "keywords" 294 | (should= ["SELECT * FROM users GROUP BY name, age"] 295 | (-> (select :*) 296 | (from :users) 297 | (group :name :age) 298 | sql/format))) 299 | (it "group by x2" 300 | (should= ["SELECT * FROM users GROUP BY name, age"] 301 | (-> (select :*) 302 | (from :users) 303 | (group :name) 304 | (group :age) 305 | sql/format)))) 306 | (context "having" 307 | (it "list" 308 | (should= ["SELECT * FROM users HAVING count(email) > 2"] 309 | (-> (select :*) 310 | (from :users) 311 | (having '(> (count email) 2)) 312 | sql/format))) 313 | (it "two lists (implicit AND)" 314 | (should= ["SELECT * FROM users HAVING (count(this) > 2 AND count(that) > 2)"] 315 | (-> (select :*) 316 | (from :users) 317 | (having '(> (count this) 2) 318 | '(> (count that) 2)) 319 | sql/format))) 320 | (it "having x2" 321 | (should= ["SELECT * FROM users HAVING (count(this) > 2 AND count(that) > 2)"] 322 | (-> (select :*) 323 | (from :users) 324 | (having '(> (count this) 2)) 325 | (having '(> (count that) 2)) 326 | sql/format)))) 327 | (context "limit" 328 | (it "number" 329 | (should= ["SELECT * FROM users LIMIT 50"] 330 | (-> (select :*) 331 | (from :users) 332 | (limit 50) 333 | sql/format)))) 334 | (context "offset" 335 | (it "number" 336 | (should= ["SELECT * FROM users LIMIT 50 OFFSET 50"] 337 | (-> (select :*) 338 | (from :users) 339 | (limit 50) 340 | (offset 50) 341 | sql/format)))) 342 | (context "modifiers" 343 | (it "keyword" 344 | (should= ["SELECT DISTINCT id FROM users"] 345 | (-> (select :id) 346 | (modifiers :distinct) 347 | (from :users) 348 | sql/format)))) 349 | (context "subqueries" 350 | (it "where clause" 351 | (should= ["SELECT * FROM users WHERE (id IN (SELECT userid FROM contacts))"] 352 | (-> (select :*) 353 | (from :users) 354 | (where `(in id 355 | ~(-> (select :userid) 356 | (from :contacts)))) 357 | sql/format))))) 358 | 359 | (describe "insert" 360 | (it "vector of maps" 361 | (should= ["INSERT INTO users (name, age) VALUES (?, 35), (?, 37)" "Billy" "Joey"] 362 | (-> (insert-into :users) 363 | (values [{:name "Billy" :age 35} 364 | {:name "Joey" :age 37}]) 365 | sql/format))) 366 | (it "vector of vectors" 367 | (should= ["INSERT INTO users VALUES (?, 35), (?, 37)" "Billy" "Joey"] 368 | (-> (insert-into :users) 369 | (values [["Billy" 35] ["Joey" 37]]) 370 | sql/format))) 371 | (it "modifiers" 372 | (should= ["INSERT IGNORE INTO users VALUES (?, 35), (?, 37)" "Billy" "Joey"] 373 | (-> (insert-into :users) 374 | (modifiers :ignore) 375 | (values [["Billy" 35] ["Joey" 37]]) 376 | sql/format))) 377 | (it "insert select" 378 | (should= ["INSERT INTO foo (a, b, c) SELECT x.a, y.b, z.c FROM x INNER JOIN y ON x.id = y.id INNER JOIN z ON y.id = z.id"] 379 | (-> (insert-into :foo) 380 | (columns :a :b :c) 381 | (select :x.a :y.b :z.c) 382 | (from :x) 383 | (join :y '(= x.id y.id) 384 | :z '(= y.id z.id)) 385 | sql/format))) 386 | (it "on duplicate key" 387 | (should= ["INSERT INTO foo (a, b, c) VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = 9"] 388 | (-> (insert-into :foo) 389 | (columns :a :b :c) 390 | (values [[1 2 3]]) 391 | (on-dup-key {:c 9}) 392 | sql/format))) 393 | (it "on duplicate key values" 394 | (should= ["INSERT INTO foo (a, b, c) VALUES (1, 2, 3) ON DUPLICATE KEY UPDATE c = VALUES(a)"] 395 | (-> (insert-into :foo) 396 | (columns :a :b :c) 397 | (values [[1 2 3]]) 398 | (on-dup-key {:c (values' :a)}) 399 | sql/format)))) 400 | 401 | (describe "update" 402 | (it "setv" 403 | (should= ["UPDATE users SET name = ?, age = 35 WHERE id = 234" "Billy"] 404 | (-> (update :users) 405 | (setv {:name "Billy", :age 35}) 406 | (where '(= id 234)) 407 | sql/format))) 408 | (it "modifiers" 409 | (should= ["UPDATE IGNORE items, month SET items.price = month.price WHERE items.id = months.id"] 410 | (-> (update :items :month) 411 | (modifiers :ignore) 412 | (setv '{items.price month.price}) 413 | (where '(= items.id months.id)) 414 | sql/format)))) 415 | 416 | (describe "delete" 417 | (it "single table" 418 | (should= ["DELETE FROM foo WHERE email = ?" "billy@bob.com"] 419 | (-> (delete-from :foo) 420 | (where '(= email "billy@bob.com")) 421 | sql/format))) 422 | (it "modifiers" 423 | (should= ["DELETE IGNORE FROM foo WHERE email = ?" "billy@bob.com"] 424 | (-> (delete-from :foo) 425 | (modifiers :ignore) 426 | (where '(= email "billy@bob.com")) 427 | sql/format))) 428 | (it "multiple tables" 429 | (should= ["DELETE FROM t1 USING t1, t2 WHERE (t1.x = t2.x AND t2.y = 3)"] 430 | (-> (delete-from :t1) 431 | (using :t1 :t2) 432 | (where '(= t1.x t2.x) 433 | '(= t2.y 3)) 434 | sql/format)))) 435 | 436 | (describe "replace" 437 | (it "vector of maps" 438 | (should= ["REPLACE INTO users (name, age) VALUES (?, 35), (?, 37)" "Billy" "Joey"] 439 | (-> (replace-into :users) 440 | (values [{:name "Billy" :age 35} 441 | {:name "Joey" :age 37}]) 442 | sql/format))) 443 | (it "vector of vectors" 444 | (should= ["REPLACE INTO users VALUES (?, 35), (?, 37)" "Billy" "Joey"] 445 | (-> (replace-into :users) 446 | (values [["Billy" 35] ["Joey" 37]]) 447 | sql/format))) 448 | (it "modifiers" 449 | (should= ["REPLACE IGNORE INTO users VALUES (?, 35), (?, 37)" "Billy" "Joey"] 450 | (-> (replace-into :users) 451 | (modifiers :ignore) 452 | (values [["Billy" 35] ["Joey" 37]]) 453 | sql/format)))) 454 | 455 | (describe "quoting" 456 | (it "mysql" 457 | (should= ["SELECT `users`.`name`, `contacts`.*, date_format(`dob`, ?) FROM `users` INNER JOIN `contacts` ON `users`.`id` = `contacts`.`userid` WHERE (`users`.`status` IN (?, ?)) GROUP BY `users`.`status` ORDER BY `contacts`.`last_name` ASC LIMIT 25" "%m/%d/%Y" "active" "pending"] 458 | (-> (select :users.name 459 | :contacts.* 460 | '(date_format dob "%m/%d/%Y")) 461 | (from :users) 462 | (join :contacts 463 | '(= users.id contacts.userid)) 464 | (where '(in users.status ["active" 465 | "pending"])) 466 | (group :users.status) 467 | (order-by (asc :contacts.last-name)) 468 | (limit 25) 469 | (sql/format :quoting :mysql))))) 470 | 471 | (describe "functions" 472 | (context "=" 473 | (it "non-null values" 474 | (should= ["SELECT 1 = 1"] 475 | (-> (select '(= 1 1)) 476 | sql/format))) 477 | (it "first value is null" 478 | (should= ["SELECT x IS NULL"] 479 | (-> (select '(= nil x)) 480 | sql/format))) 481 | (it "second value is null" 482 | (should= ["SELECT x IS NULL"] 483 | (-> (select '(= x nil)) 484 | sql/format)))) 485 | (context "<>" 486 | (context "aliases" 487 | (it "!=" 488 | (should= ["SELECT 1 <> 2"] 489 | (-> (select '(!= 1 2)) 490 | sql/format))) 491 | (it "not=" 492 | (should= ["SELECT 1 <> 2"] 493 | (-> (select '(not= 1 2)) 494 | sql/format)))) 495 | (it "non-null values" 496 | (should= ["SELECT 1 <> 2"] 497 | (-> (select '(<> 1 2)) 498 | sql/format))) 499 | (it "first value is null" 500 | (should= ["SELECT x IS NOT NULL"] 501 | (-> (select '(<> nil x)) 502 | sql/format))) 503 | (it "second value is null" 504 | (should= ["SELECT x IS NOT NULL"] 505 | (-> (select '(<> x nil)) 506 | sql/format)))) 507 | (it "not" 508 | (should= ["SELECT not(TRUE)"] 509 | (-> (select '(not true)) 510 | sql/format))) 511 | (it "is" 512 | (should= ["SELECT NULL IS NULL"] 513 | (-> (select '(is nil nil)) 514 | sql/format))) 515 | (it "is not" 516 | (should= ["SELECT FALSE IS NOT NULL"] 517 | (-> (select '(is-not false nil)) 518 | sql/format))) 519 | (it "<" 520 | (should= ["SELECT 1 < 2"] 521 | (-> (select '(< 1 2)) 522 | sql/format))) 523 | (it ">" 524 | (should= ["SELECT 2 > 1"] 525 | (-> (select '(> 2 1)) 526 | sql/format))) 527 | (it "<=" 528 | (should= ["SELECT 1 <= 2"] 529 | (-> (select '(<= 1 2)) 530 | sql/format))) 531 | (it ">=" 532 | (should= ["SELECT 2 >= 1"] 533 | (-> (select '(>= 2 1)) 534 | sql/format))) 535 | (it "between" 536 | (should= ["SELECT 2 BETWEEN 1 AND 3"] 537 | (-> (select '(between 2 1 3)) 538 | sql/format))) 539 | (it "not between" 540 | (should= ["SELECT 1 NOT BETWEEN 2 AND 3"] 541 | (-> (select '(not-between 1 2 3)) 542 | sql/format))) 543 | (it "like" 544 | (should= ["SELECT (firstName LIKE ?) FROM users" "bill"] 545 | (-> (select '(like firstName "bill")) 546 | (from :users) 547 | sql/format))) 548 | (it "not like" 549 | (should= ["SELECT (firstName NOT LIKE ?) FROM users" "bill"] 550 | (-> (select '(not-like firstName "bill")) 551 | (from :users) 552 | sql/format))) 553 | (context "in" 554 | (it "select clause" 555 | (should= ["SELECT (1 IN (1, 2))"] 556 | (-> (select '(in 1 [1 2])) 557 | sql/format))) 558 | (it "where clause" 559 | (should= ["SELECT * FROM foo WHERE (1 IN (1, 2))"] 560 | (-> (select :*) 561 | (from :foo) 562 | (where '(in 1 [1 2])) 563 | sql/format)))) 564 | (context "not in" 565 | (it "select clause" 566 | (should= ["SELECT (1 NOT IN (1, 2))"] 567 | (-> (select '(not-in 1 [1 2])) 568 | sql/format))) 569 | (it "where clause" 570 | (should= ["SELECT * FROM foo WHERE (1 NOT IN (1, 2))"] 571 | (-> (select :*) 572 | (from :foo) 573 | (where '(not-in 1 [1 2])) 574 | sql/format)))) 575 | (it "regexp" 576 | (should= ["SELECT (? REGEXP ?)" "Billy" "[A-Z]+"] 577 | (-> (select '(regexp "Billy" "[A-Z]+")) 578 | sql/format))) 579 | (it "not regexp" 580 | (should= ["SELECT (? NOT REGEXP ?)" "Billy" "[A-Z]+"] 581 | (-> (select '(not-regexp "Billy" "[A-Z]+")) 582 | sql/format))) 583 | (it "count-distinct" 584 | (should= ["SELECT COUNT(DISTINCT email) FROM users"] 585 | (-> (select '(count-distinct email)) 586 | (from :users) 587 | sql/format)))) 588 | 589 | (describe "union" 590 | (it "multiple selects" 591 | (should= ["(SELECT name, email FROM users) UNION (SELECT name, email FROM deleted_users)"] 592 | (sql/format 593 | (union (-> (select :name :email) 594 | (from :users)) 595 | (-> (select :name :email) 596 | (from :deleted-users))))))) 597 | 598 | (describe "union-all" 599 | (it "multiple selects" 600 | (should= ["(SELECT name, email FROM users) UNION ALL (SELECT name, email FROM deleted_users)"] 601 | (sql/format 602 | (union-all (-> (select :name :email) 603 | (from :users)) 604 | (-> (select :name :email) 605 | (from :deleted-users))))))) 606 | 607 | (describe "complex" 608 | (it "nested in" 609 | (should= ["SELECT * FROM users WHERE (id IN (SELECT (id IN (1, 2)) FROM contacts))"] 610 | (-> (select :*) 611 | (from :users) 612 | (where `(in id 613 | ~(-> (select '(in id [1 2])) 614 | (from :contacts)))) 615 | sql/format)))) 616 | 617 | -------------------------------------------------------------------------------- /src/stch/sql.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql 2 | "SQL DSL for query and data manipulation language 3 | (DML). Supports a majority of MySQL's statements." 4 | (:refer-clojure :exclude [update]) 5 | (:require 6 | [stch.sql.types :as types])) 7 | 8 | (defmulti build-clause (fn [name & args] 9 | name)) 10 | 11 | (defmethod build-clause :default [_ m & args] 12 | m) 13 | 14 | (defmacro defhelper [helper arglist & more] 15 | (let [kw (keyword (name helper))] 16 | `(do 17 | (defmethod build-clause ~kw ~(into ['_] arglist) ~@more) 18 | (defn ~helper [& args#] 19 | (let [[m# args#] (if (map? (first args#)) 20 | [(first args#) (rest args#)] 21 | [{} args#])] 22 | (build-clause ~kw m# args#)))))) 23 | 24 | (defn collify [x] 25 | (if (coll? x) x [x])) 26 | 27 | (defhelper select [m fields] 28 | (update-in m [:select] concat (collify fields))) 29 | 30 | (defhelper replace-select [m fields] 31 | (assoc m :select (collify fields))) 32 | 33 | (defhelper un-select [m fields] 34 | (update-in m [:select] #(remove (set (collify fields)) %))) 35 | 36 | (defhelper from [m tables] 37 | (update-in m [:from] concat (collify tables))) 38 | 39 | (defhelper replace-from [m tables] 40 | (assoc m :from (collify tables))) 41 | 42 | (defn- prep-where [args] 43 | (let [[m preds] (if (map? (first args)) 44 | [(first args) (rest args)] 45 | [{} args]) 46 | [logic-op preds] (if (keyword? (first preds)) 47 | [(first preds) (rest preds)] 48 | [:and preds]) 49 | pred (if (= 1 (count preds)) 50 | (first preds) 51 | (into [logic-op] preds))] 52 | [m pred logic-op])) 53 | 54 | (defmethod build-clause :where [_ m pred] 55 | (if (nil? pred) 56 | m 57 | (assoc m :where (if (not (nil? (:where m))) 58 | [:and (:where m) pred] 59 | pred)))) 60 | 61 | (defn where [& args] 62 | (let [[m pred logic-op] (prep-where args)] 63 | (if (nil? pred) 64 | m 65 | (assoc m :where (if (not (nil? (:where m))) 66 | [logic-op (:where m) pred] 67 | pred))))) 68 | 69 | (defmethod build-clause :replace-where [_ m pred] 70 | (if (nil? pred) 71 | m 72 | (assoc m :where pred))) 73 | 74 | (defn replace-where [& args] 75 | (let [[m pred] (prep-where args)] 76 | (if (nil? pred) 77 | m 78 | (assoc m :where pred)))) 79 | 80 | (defhelper join [m clauses] 81 | (update-in m [:join] concat clauses)) 82 | 83 | (defhelper replace-join [m clauses] 84 | (assoc m :join clauses)) 85 | 86 | (defhelper left-join [m clauses] 87 | (update-in m [:left-join] concat clauses)) 88 | 89 | (defhelper replace-left-join [m clauses] 90 | (assoc m :left-join clauses)) 91 | 92 | (defhelper right-join [m clauses] 93 | (update-in m [:right-join] concat clauses)) 94 | 95 | (defhelper replace-right-join [m clauses] 96 | (assoc m :right-join clauses)) 97 | 98 | (defmethod build-clause :group-by [_ m fields] 99 | (update-in m [:group-by] concat (collify fields))) 100 | 101 | (defn group [& args] 102 | (let [[m fields] (if (map? (first args)) 103 | [(first args) (rest args)] 104 | [{} args])] 105 | (build-clause :group-by m fields))) 106 | 107 | (defmethod build-clause :replace-group-by [_ m fields] 108 | (assoc m :group-by (collify fields))) 109 | 110 | (defn replace-group [& args] 111 | (let [[m fields] (if (map? (first args)) 112 | [(first args) (rest args)] 113 | [{} args])] 114 | (build-clause :replace-group-by m fields))) 115 | 116 | (defmethod build-clause :having [_ m pred] 117 | (if (nil? pred) 118 | m 119 | (assoc m :having (if (not (nil? (:having m))) 120 | [:and (:having m) pred] 121 | pred)))) 122 | 123 | (defn having [& args] 124 | (let [[m pred logic-op] (prep-where args)] 125 | (if (nil? pred) 126 | m 127 | (assoc m :having (if (not (nil? (:having m))) 128 | [logic-op (:having m) pred] 129 | pred))))) 130 | 131 | (defmethod build-clause :replace-having [_ m pred] 132 | (if (nil? pred) 133 | m 134 | (assoc m :having pred))) 135 | 136 | (defn replace-having [& args] 137 | (let [[m pred] (prep-where args)] 138 | (if (nil? pred) 139 | m 140 | (assoc m :having pred)))) 141 | 142 | (defhelper order-by [m fields] 143 | (update-in m [:order-by] concat (collify fields))) 144 | 145 | (defhelper replace-order-by [m fields] 146 | (assoc m :order-by (collify fields))) 147 | 148 | (defn asc [field] 149 | [field :asc]) 150 | 151 | (defn desc [field] 152 | [field :desc]) 153 | 154 | (defhelper limit [m l] 155 | (if (nil? l) 156 | m 157 | (assoc m :limit (if (coll? l) (first l) l)))) 158 | 159 | (defhelper offset [m o] 160 | (if (nil? o) 161 | m 162 | (assoc m :offset (if (coll? o) (first o) o)))) 163 | 164 | (defhelper modifiers [m ms] 165 | (if (nil? ms) 166 | m 167 | (update-in m [:modifiers] concat (collify ms)))) 168 | 169 | (defhelper replace-modifiers [m ms] 170 | (if (nil? ms) 171 | m 172 | (assoc m :modifiers (collify ms)))) 173 | 174 | (defn union 175 | [& select-stmts] 176 | {:union select-stmts}) 177 | 178 | (defn union-all 179 | [& select-stmts] 180 | {:union-all select-stmts}) 181 | 182 | (defmethod build-clause :insert-into [_ m table] 183 | (assoc m :insert-into table)) 184 | 185 | (defn insert-into 186 | ([table] (insert-into nil table)) 187 | ([m table] (build-clause :insert-into m table))) 188 | 189 | (defmethod build-clause :replace-into [_ m table] 190 | (assoc m :replace-into table)) 191 | 192 | (defn replace-into 193 | ([table] (replace-into nil table)) 194 | ([m table] (build-clause :replace-into m table))) 195 | 196 | (defhelper columns [m fields] 197 | (update-in m [:columns] concat (collify fields))) 198 | 199 | (defhelper replace-columns [m fields] 200 | (assoc m :columns (collify fields))) 201 | 202 | (defmethod build-clause :replace-values [_ m vs] 203 | (assoc m :values vs)) 204 | 205 | (defn replace-values 206 | ([vs] (replace-values nil vs)) 207 | ([m vs] (build-clause :values m vs))) 208 | 209 | (defmethod build-clause :values [_ m vs] 210 | (update-in m [:values] concat vs)) 211 | 212 | (defn values 213 | ([vs] (values nil vs)) 214 | ([m vs] (build-clause :values m vs))) 215 | 216 | (defmethod build-clause :query-values [_ m vs] 217 | (assoc m :query-values vs)) 218 | 219 | (defn query-values 220 | ([vs] (query-values nil vs)) 221 | ([m vs] (build-clause :query-values m vs))) 222 | 223 | (defhelper update [m tables] 224 | (assoc m :update tables)) 225 | 226 | (defmethod build-clause :set [_ m values] 227 | (assoc m :set values)) 228 | 229 | (defn setv 230 | ([vs] (setv nil vs)) 231 | ([m vs] (build-clause :set m vs))) 232 | 233 | (defmethod build-clause :on-dup-key [_ m values] 234 | (assoc m :on-dup-key values)) 235 | 236 | (defn on-dup-key 237 | ([vs] (on-dup-key nil vs)) 238 | ([m vs] (build-clause :on-dup-key m vs))) 239 | 240 | (defn values' [x] 241 | (types/raw (str "VALUES(" (name x) ")"))) 242 | 243 | (defhelper delete-from [m tables] 244 | (assoc m :delete-from tables)) 245 | 246 | (defhelper using [m tables] 247 | (update-in m [:using] concat (collify tables))) 248 | 249 | -------------------------------------------------------------------------------- /src/stch/sql/ddl.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql.ddl 2 | "SQL DSL for data definition language (DDL). 3 | Supports a majority of MySQL's statements for 4 | tables and databases." 5 | (:require 6 | [clojure.string :as string] 7 | [stch.schema :refer :all] 8 | [stch.sql.util :refer :all] 9 | [stch.util :refer [named?]])) 10 | 11 | (def ^:private special-keywords 12 | {"auto-increment" "AUTO_INCREMENT"}) 13 | 14 | (def ^:private parse-identifier 15 | (comp undasherize name)) 16 | 17 | (def ^{:private true :dynamic true} *op*) 18 | 19 | (defn- parse-keyword 20 | "Parse a SQL keyword." 21 | [s] 22 | (-> (string/split s #"-") 23 | space-join 24 | string/upper-case)) 25 | 26 | (defprotocol ToSQL 27 | "Convert an object to SQL." 28 | (to-sql [x])) 29 | 30 | (extend-protocol ToSQL 31 | clojure.lang.Named 32 | (to-sql [kw] 33 | (let [kw (name kw)] 34 | (special-keywords kw (parse-keyword kw)))) 35 | 36 | Number 37 | (to-sql [x] x) 38 | 39 | String 40 | (to-sql [x] (str \' x \')) 41 | 42 | clojure.lang.Sequential 43 | (to-sql [x] 44 | (-> (map to-sql x) 45 | comma-join 46 | paren-wrap))) 47 | 48 | (defprotocol WhatsMyName 49 | (whats-my-name [x])) 50 | 51 | (extend-protocol WhatsMyName 52 | clojure.lang.Named 53 | (whats-my-name [x] (name x)) 54 | 55 | String 56 | (whats-my-name [x] x)) 57 | 58 | (def ^:private parse-name 59 | (comp parse-identifier whats-my-name)) 60 | 61 | (defprotocol AlterAdd 62 | (-add [spec m] [spec m options])) 63 | 64 | (defrecord' DBSpec 65 | [db-name :- Named] 66 | 67 | WhatsMyName 68 | (whats-my-name [_] db-name) 69 | 70 | ToSQL 71 | (to-sql [_] 72 | (str "DATABASE " 73 | (parse-identifier db-name)))) 74 | 75 | (defn' db :- DBSpec 76 | "Create a database." 77 | [db-name :- Named] 78 | (->DBSpec db-name)) 79 | 80 | (defn' db? :- Boolean 81 | [x :- Any] 82 | (instance? DBSpec x)) 83 | 84 | (declare ->AddColumnSpec) 85 | 86 | (defrecord' DefaultValueSpec 87 | [x :- (U Number String)] 88 | 89 | ToSQL 90 | (to-sql [_] 91 | (str "DEFAULT " (to-sql x)))) 92 | 93 | (defn' default :- DefaultValueSpec 94 | "Set the default value for a column. 95 | Use with a column fn." 96 | [x :- (U Number String)] 97 | (->DefaultValueSpec x)) 98 | 99 | (def KeywordOrSym 100 | "Keyword or symbol type definition." 101 | (U Keyword Symbol)) 102 | 103 | (def ColumnOptions 104 | "Column options type definition." 105 | (U [(One (U [Int] [String])) 106 | (U KeywordOrSym DefaultValueSpec)] 107 | [(U KeywordOrSym DefaultValueSpec)])) 108 | 109 | (defrecord' ColumnSpec 110 | [col-name :- Named 111 | col-type :- String 112 | options :- ColumnOptions] 113 | 114 | WhatsMyName 115 | (whats-my-name [_] col-name) 116 | 117 | AlterAdd 118 | (-add [spec m] (-add spec m nil)) 119 | (-add [spec m options] 120 | (->> (->AddColumnSpec spec options) 121 | (update-in m [:columns+keys] conj))) 122 | 123 | ToSQL 124 | (to-sql [_] 125 | (let [first-opt-seq? (sequential? (first options))] 126 | (space-join 127 | (list* (parse-identifier col-name) 128 | (if first-opt-seq? 129 | (str col-type (to-sql (first options))) 130 | col-type) 131 | (if first-opt-seq? 132 | (map to-sql (rest options)) 133 | (map to-sql options))))))) 134 | 135 | (def NamedOrColumn 136 | "Named or column spec type definition." 137 | (U Named ColumnSpec)) 138 | 139 | (defn' column? :- Boolean 140 | [x :- Any] 141 | (instance? ColumnSpec x)) 142 | 143 | (defrecord' ColumnPosition 144 | [preposition :- String 145 | col :- NamedOrColumn] 146 | 147 | ToSQL 148 | (to-sql [_] 149 | (str preposition " " (parse-name col)))) 150 | 151 | (defn' after :- ColumnPosition 152 | "Set column position. Use with add." 153 | [col :- NamedOrColumn] 154 | (->ColumnPosition "AFTER" col)) 155 | 156 | (defrecord' AddColumnSpec 157 | [col-spec :- ColumnSpec 158 | col-position :- (U KeywordOrSym ColumnPosition)] 159 | 160 | ToSQL 161 | (to-sql [_] 162 | (str "ADD COLUMN " (to-sql col-spec) 163 | (when col-position 164 | (str " " (to-sql col-position)))))) 165 | 166 | (defn' column :- ColumnSpec 167 | [col-name col-type options] 168 | (->ColumnSpec col-name col-type options)) 169 | 170 | (defmacro deftypefn 171 | "Define a type fn (e.g., int, varchar)." 172 | [fn-name col-type] 173 | `(defn ~fn-name 174 | [& [arg0# & args#]] 175 | (cond (map? arg0#) 176 | (let [col# (column (first args#) 177 | ~col-type 178 | (rest args#))] 179 | (update-in arg0# [:columns+keys] conj col#)) 180 | (named? arg0#) 181 | (column arg0# ~col-type args#)))) 182 | 183 | ; Numeric 184 | (deftypefn bool "BOOL") 185 | (deftypefn tiny-int "TINYINT") 186 | (deftypefn small-int "SMALLINT") 187 | (deftypefn medium-int "MEDIUMINT") 188 | (deftypefn integer "INT") 189 | (deftypefn big-int "BIGINT") 190 | (deftypefn decimal "DECIMAL") 191 | (deftypefn float' "FLOAT") 192 | (deftypefn double' "DOUBLE") 193 | ; PostgreSQL Serial 194 | (deftypefn small-serial "SMALLSERIAL") 195 | (deftypefn serial "SERIAL") 196 | (deftypefn big-serial "BIGSERIAL") 197 | ; String 198 | (deftypefn chr "CHAR") 199 | (def char' chr) 200 | (deftypefn varchar "VARCHAR") 201 | (deftypefn binary "BINARY") 202 | (deftypefn varbinary "VARBINARY") 203 | (deftypefn blob "BLOB") 204 | (deftypefn text "TEXT") 205 | (deftypefn enum "ENUM") 206 | (deftypefn set' "SET") 207 | ; Date and time 208 | (deftypefn date "DATE") 209 | (deftypefn datetime "DATETIME") 210 | (deftypefn timestamp "TIMESTAMP") 211 | (deftypefn time' "TIME") 212 | (deftypefn year "YEAR") 213 | ; Spatial 214 | (deftypefn geometry "GEOMETRY") 215 | (deftypefn point "POINT") 216 | (deftypefn linestring "LINESTRING") 217 | (deftypefn polygon "POLYGON") 218 | 219 | (declare ->AddIndexSpec) 220 | 221 | (defrecord' IndexSpec 222 | [index-type :- String 223 | index-cols :- (U [NamedOrColumn] NamedOrColumn)] 224 | 225 | AlterAdd 226 | (-add [spec m] 227 | (->> (->AddIndexSpec spec) 228 | (update-in m [:columns+keys] conj))) 229 | 230 | ToSQL 231 | (to-sql [_] 232 | (str index-type 233 | (paren-wrap 234 | (if (sequential? index-cols) 235 | (-> (map parse-name index-cols) 236 | comma-join) 237 | (parse-name index-cols)))))) 238 | 239 | (defn' idx :- IndexSpec 240 | [index-type index-cols] 241 | (->IndexSpec index-type index-cols)) 242 | 243 | (defmacro defindexfn 244 | "Define an index fn." 245 | [fn-name index-type] 246 | `(defn ~fn-name 247 | [& [arg0# & args#]] 248 | (cond (map? arg0#) 249 | (let [index# (idx ~index-type (first args#))] 250 | (update-in arg0# [:columns+keys] conj index#)) 251 | (or (named? arg0#) (sequential? arg0#)) 252 | (idx ~index-type arg0#)))) 253 | 254 | (defindexfn primary-key "PRIMARY KEY") 255 | (defindexfn index "INDEX") 256 | (defindexfn unique "UNIQUE") 257 | (defindexfn fulltext "FULLTEXT") 258 | (defindexfn spatial "SPATIAL INDEX") 259 | 260 | (defrecord' ForeignKeySpec 261 | [field :- NamedOrColumn 262 | references :- (Pair KeywordOrSym KeywordOrSym) 263 | options :- [KeywordOrSym]] 264 | 265 | AlterAdd 266 | (-add [spec m] 267 | (->> (->AddIndexSpec spec) 268 | (update-in m [:columns+keys] conj))) 269 | 270 | ToSQL 271 | (to-sql [_] 272 | (space-join 273 | (list* (str "FOREIGN KEY" 274 | (paren-wrap (parse-name field))) 275 | "REFERENCES" 276 | (str (parse-identifier (first references)) 277 | (-> (second references) 278 | parse-identifier 279 | paren-wrap)) 280 | (map to-sql options))))) 281 | 282 | (defn foreign-key 283 | "Define a foreign key. Use with create or alt." 284 | [& [arg0 & args]] 285 | (cond (map? arg0) 286 | (let [m arg0 287 | [field references & options] args 288 | index (->ForeignKeySpec field references options)] 289 | (update-in m [:columns+keys] conj index)) 290 | (named? arg0) 291 | (let [field arg0 292 | [references & options] args] 293 | (->ForeignKeySpec field references options)))) 294 | 295 | (defrecord' ConstraintSpec 296 | [key-name :- Named 297 | key-spec :- (U IndexSpec ForeignKeySpec)] 298 | 299 | WhatsMyName 300 | (whats-my-name [_] key-name) 301 | 302 | AlterAdd 303 | (-add [spec m] 304 | (->> (->AddIndexSpec spec) 305 | (update-in m [:columns+keys] conj))) 306 | 307 | ToSQL 308 | (to-sql [_] 309 | (space-join 310 | (list "CONSTRAINT" 311 | (parse-identifier key-name) 312 | (to-sql key-spec))))) 313 | 314 | (defn constraint 315 | "Define a named constraint. Use with an index fn." 316 | [& [arg0 & args]] 317 | (cond (map? arg0) 318 | (let [m arg0 319 | [key-name key-spec] args 320 | index (->ConstraintSpec key-name key-spec)] 321 | (update-in m [:columns+keys] conj index)) 322 | (named? arg0) 323 | (let [key-name arg0 324 | [key-spec] args] 325 | (->ConstraintSpec key-name key-spec)))) 326 | 327 | (defn' index? :- Boolean 328 | [x :- Any] 329 | (or (instance? IndexSpec x) 330 | (instance? ForeignKeySpec x) 331 | (instance? ConstraintSpec x))) 332 | 333 | (def Index 334 | "Index type definition." 335 | (U IndexSpec ForeignKeySpec ConstraintSpec)) 336 | 337 | (def ColumnOrIndex 338 | "Column or index type definition." 339 | (U ColumnSpec Index)) 340 | 341 | (defrecord' TableSpec 342 | [table-name :- Named 343 | temporary :- Boolean 344 | columns+keys :- [Any]] 345 | 346 | WhatsMyName 347 | (whats-my-name [_] table-name) 348 | 349 | ToSQL 350 | (to-sql [_] 351 | (let [spec (comma-join (map to-sql columns+keys))] 352 | (str (when temporary "TEMPORARY ") 353 | "TABLE" 354 | (pad (parse-identifier table-name)) 355 | (if (= *op* ::create) 356 | (paren-wrap spec) 357 | spec))))) 358 | 359 | (defn' table :- TableSpec 360 | [table-name :- Named] 361 | (->TableSpec table-name false [])) 362 | 363 | (defn' temp-table :- TableSpec 364 | [table-name :- Named] 365 | (->TableSpec table-name true [])) 366 | 367 | (defn' table? :- Boolean 368 | [x :- Any] 369 | (instance? TableSpec x)) 370 | 371 | (defmacro deftable 372 | [table-name & columns+keys] 373 | `(def ~table-name 374 | (-> (table ~table-name) 375 | ~@columns+keys))) 376 | 377 | (defn' add :- TableSpec 378 | "Add column/index. Use with alt." 379 | ([m :- TableSpec, spec] 380 | (-add spec m)) 381 | ([m :- TableSpec, spec, options] 382 | (-add spec m options))) 383 | 384 | (defrecord' Columns 385 | [columns+keys :- [ColumnOrIndex]] 386 | 387 | AlterAdd 388 | (-add [spec m] 389 | (reduce add m columns+keys))) 390 | 391 | (defn' columns :- Columns 392 | [] 393 | (->Columns [])) 394 | 395 | (defmacro defcolumns 396 | "Define one or more columns." 397 | [name & columns+keys] 398 | `(def ~name (-> (columns) 399 | ~@columns+keys))) 400 | 401 | (defn' columns? :- Boolean 402 | [x :- Any] 403 | (instance? Columns x)) 404 | 405 | (defrecord' TableOption 406 | [option-name :- String 407 | option-value :- (U Int String)] 408 | 409 | ToSQL 410 | (to-sql [_] 411 | (str option-name "=" option-value))) 412 | 413 | (defn' engine :- TableOption 414 | "Set the engine table option." 415 | [eng :- Named] 416 | (->TableOption "ENGINE" (name eng))) 417 | 418 | (defn' collate :- TableOption 419 | "Set the collate table option." 420 | [collation :- Named] 421 | (->TableOption "COLLATE" (-> (name collation) 422 | undasherize))) 423 | 424 | (defn' character-set :- TableOption 425 | "Set the character set table option." 426 | [charset :- Named] 427 | (->TableOption "CHARACTER SET" (name charset))) 428 | 429 | (defn' auto-inc :- TableOption 430 | "Set the auto-increment table option." 431 | [start :- Int] 432 | (->TableOption "AUTO_INCREMENT" start)) 433 | 434 | (defrecord' AddIndexSpec 435 | [index-spec :- Index] 436 | 437 | ToSQL 438 | (to-sql [_] 439 | (str "ADD " (to-sql index-spec)))) 440 | 441 | (defrecord' ChangeSpec 442 | [col :- NamedOrColumn 443 | col-spec :- ColumnSpec] 444 | 445 | ToSQL 446 | (to-sql [_] 447 | (str "CHANGE " 448 | (parse-name col) " " 449 | (to-sql col-spec)))) 450 | 451 | (defn' change :- TableSpec 452 | "Change column. Use with alt." 453 | [m :- TableSpec 454 | col :- NamedOrColumn 455 | col-spec :- ColumnSpec] 456 | (let [stmt (->ChangeSpec col col-spec)] 457 | (update-in m [:columns+keys] conj stmt))) 458 | 459 | (defrecord' SetDefaultSpec 460 | [col :- NamedOrColumn 461 | default :- (U String Number)] 462 | 463 | ToSQL 464 | (to-sql [_] 465 | (space-join 466 | (list "ALTER COLUMN" 467 | (parse-name col) 468 | "SET DEFAULT" 469 | default)))) 470 | 471 | (defn' set-default :- TableSpec 472 | "Set column default. Use with alt." 473 | [m :- TableSpec 474 | col :- NamedOrColumn 475 | default :- (U String Number)] 476 | (let [stmt (->SetDefaultSpec col default)] 477 | (update-in m [:columns+keys] conj stmt))) 478 | 479 | (defrecord' DropDefaultSpec 480 | [col :- NamedOrColumn] 481 | 482 | ToSQL 483 | (to-sql [_] 484 | (space-join 485 | (list "ALTER COLUMN" 486 | (parse-name col) 487 | "DROP DEFAULT")))) 488 | 489 | (defn' drop-default :- TableSpec 490 | "Drop column default. Use with alt." 491 | [m :- TableSpec, col :- NamedOrColumn] 492 | (let [stmt (->DropDefaultSpec col)] 493 | (update-in m [:columns+keys] conj stmt))) 494 | 495 | (defrecord' DropColumnSpec 496 | [col :- NamedOrColumn] 497 | 498 | ToSQL 499 | (to-sql [_] 500 | (str "DROP COLUMN " (parse-name col)))) 501 | 502 | (defn' drop-column :- TableSpec 503 | "Drop a column. Use with alt." 504 | [m :- TableSpec, col :- NamedOrColumn] 505 | (let [stmt (->DropColumnSpec col)] 506 | (update-in m [:columns+keys] conj stmt))) 507 | 508 | (def NamedOrConstraint (U Named ConstraintSpec)) 509 | 510 | (defrecord' DropIndexSpec 511 | [index :- NamedOrConstraint] 512 | 513 | ToSQL 514 | (to-sql [_] 515 | (str "DROP INDEX " 516 | (parse-name index)))) 517 | 518 | (defn' drop-index :- TableSpec 519 | "Drop an index. Use with alt." 520 | [m :- TableSpec, index :- NamedOrConstraint] 521 | (let [stmt (->DropIndexSpec index)] 522 | (update-in m [:columns+keys] conj stmt))) 523 | 524 | (defrecord' DropPrimaryKeySpec [] 525 | ToSQL 526 | (to-sql [_] "DROP PRIMARY KEY")) 527 | 528 | (defn' drop-primary-key :- TableSpec 529 | "Drop a primary key. Use with alt." 530 | [m :- TableSpec] 531 | (let [stmt (->DropPrimaryKeySpec)] 532 | (update-in m [:columns+keys] conj stmt))) 533 | 534 | (defrecord' DropForeignKeySpec 535 | [fk :- NamedOrConstraint] 536 | 537 | ToSQL 538 | (to-sql [_] 539 | (str "DROP FOREIGN KEY " 540 | (parse-name fk)))) 541 | 542 | (defn' drop-foreign-key :- TableSpec 543 | "Drop a foreign key. Use with alt." 544 | [m :- TableSpec, fk :- NamedOrConstraint] 545 | (let [stmt (->DropForeignKeySpec fk)] 546 | (update-in m [:columns+keys] conj stmt))) 547 | 548 | (def NamedOrTable 549 | "Named or table spec type definition." 550 | (U Named TableSpec)) 551 | 552 | (defrecord' RenameSpec 553 | [table :- NamedOrTable] 554 | 555 | ToSQL 556 | (to-sql [_] 557 | (str "RENAME " (parse-name table)))) 558 | 559 | (defn' rename :- TableSpec 560 | "Rename a table. Use with alt." 561 | [m :- TableSpec, table :- NamedOrTable] 562 | (let [stmt (->RenameSpec table)] 563 | (update-in m [:columns+keys] conj stmt))) 564 | 565 | (def TableOrDB 566 | "Table spec or DB spec type definition." 567 | (U TableSpec DBSpec)) 568 | 569 | (defn' create :- String 570 | "Create a table or database with options." 571 | [spec :- TableOrDB & options :- [TableOption]] 572 | (binding [*op* ::create] 573 | (str "CREATE " 574 | (to-sql spec) 575 | (when options 576 | (str " " (comma-join (map to-sql options))))))) 577 | 578 | (defn' alt :- String 579 | "Alter a table." 580 | [spec :- TableSpec] 581 | (binding [*op* ::alter] 582 | (str "ALTER " (to-sql spec)))) 583 | 584 | (def NamedOrDB 585 | "Named or DB spec type definition." 586 | (U Named DBSpec)) 587 | 588 | (defn' alt-db :- String 589 | "Alter a database." 590 | [db :- NamedOrDB, option :- TableOption] 591 | (str "ALTER DATABASE " 592 | (parse-name db) " " 593 | (to-sql option))) 594 | 595 | (defn' drop-table :- String 596 | "Drop one or more tables." 597 | [& tables :- [NamedOrTable]] 598 | (str "DROP TABLE " 599 | (-> (map parse-name tables) 600 | comma-join))) 601 | 602 | (defn' drop-temp-table :- String 603 | "Drop one or more tables." 604 | [& tables :- [NamedOrTable]] 605 | (str "DROP TEMPORARY TABLE " 606 | (-> (map parse-name tables) 607 | comma-join))) 608 | 609 | (defn' drop-db :- String 610 | "Drop a database." 611 | [db :- NamedOrDB] 612 | (str "DROP DATABASE " 613 | (parse-name db))) 614 | 615 | (defn' rename-db :- String 616 | "Rename a database." 617 | [old-db :- NamedOrDB, new-db :- NamedOrDB] 618 | (str "RENAME DATABASE " 619 | (parse-name old-db) 620 | " TO " 621 | (parse-name new-db))) 622 | 623 | (defn' truncate :- String 624 | "Truncate a table." 625 | [table :- NamedOrTable] 626 | (str "TRUNCATE TABLE " 627 | (parse-name table))) 628 | 629 | (defn' append :- TableSpec 630 | "Append a set of columns/indexes or single 631 | column/index to a table." 632 | [m :- TableSpec 633 | x :- (U Columns ColumnSpec Index)] 634 | (let [y (cond (columns? x) 635 | (:columns+keys x) 636 | (or (column? x) (index? x)) 637 | (list x))] 638 | (update-in m [:columns+keys] into y))) 639 | -------------------------------------------------------------------------------- /src/stch/sql/format.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql.format 2 | "Format query and DML statements. Use with stch.sql." 3 | (:refer-clojure :exclude [format]) 4 | (:require 5 | [clojure.string :as string] 6 | [stch.sql.types :refer [call raw param param-name]] 7 | [stch.sql.util :refer :all]) 8 | (:import 9 | (stch.sql.types 10 | SqlCall 11 | SqlParam 12 | SqlRaw))) 13 | 14 | (def ^:dynamic *clause* 15 | "During formatting, *clause* is bound to :select, :from, :where, etc." 16 | nil) 17 | 18 | (def ^:dynamic *params* 19 | "Will be bound to an atom-vector that accumulates SQL parameters across 20 | possibly-recursive function calls" 21 | nil) 22 | 23 | (def ^:dynamic *param-names* nil) 24 | 25 | (def ^:dynamic *param-counter* nil) 26 | 27 | (def ^:dynamic *input-params* nil) 28 | 29 | (def ^:dynamic *fn-context?* false) 30 | 31 | (def ^:dynamic *subquery?* false) 32 | 33 | (def ^:private quote-fns 34 | {:ansi #(str \" % \") 35 | :mysql #(str \` % \`) 36 | :sqlserver #(str \[ % \]) 37 | :oracle #(str \" % \")}) 38 | 39 | (def ^:dynamic *quote-identifier-fn* nil) 40 | 41 | (defn quote-identifier [x & {:keys [style split] :or {split true}}] 42 | (let [qf (if style 43 | (quote-fns style) 44 | *quote-identifier-fn*) 45 | s (cond 46 | (or (keyword? x) (symbol? x)) (undasherize (name x)) 47 | (string? x) (if qf x (undasherize x)) 48 | :else (str x))] 49 | (if-not qf 50 | s 51 | (let [qf* #(if (= "*" %) % (qf %))] 52 | (if-not split 53 | (qf* s) 54 | (let [parts (string/split s #"\.")] 55 | (string/join "." (map qf* parts)))))))) 56 | 57 | (def infix-fns 58 | #{"+" "-" "*" "/" "%" "mod" "|" "&" "^" 59 | "and" "or" "xor" "in" "not in" "like" 60 | "not like" "regexp" "not regexp"}) 61 | 62 | (def fn-aliases 63 | {"is" "=" 64 | "is-not" "<>" 65 | "not=" "<>" 66 | "!=" "<>" 67 | "not-in" "not in" 68 | "not-like" "not like" 69 | "regex" "regexp" 70 | "not-regex" "not regexp" 71 | "not-regexp" "not regexp"}) 72 | 73 | (declare to-sql format-predicate*) 74 | 75 | (defmulti fn-handler (fn [op & args] op)) 76 | 77 | (defn expand-binary-ops [op & args] 78 | (str "(" 79 | (string/join " AND " 80 | (for [[a b] (partition 2 1 args)] 81 | (fn-handler op a b))) 82 | ")")) 83 | 84 | (defmethod fn-handler :default [op & args] 85 | (let [args (map to-sql args)] 86 | (if (infix-fns op) 87 | (let [op (string/upper-case op)] 88 | (paren-wrap (string/join (str " " op " ") args))) 89 | (str op (paren-wrap (comma-join args)))))) 90 | 91 | (defmethod fn-handler "count-distinct" [_ & args] 92 | (str "COUNT(DISTINCT " (comma-join (map to-sql args)) ")")) 93 | 94 | (defmethod fn-handler "distinct-on" [_ & args] 95 | (str "DISTINCT ON (" (comma-join (map to-sql args)) ")")) 96 | 97 | (defmethod fn-handler "=" [_ a b & more] 98 | (if (seq more) 99 | (apply expand-binary-ops "=" a b more) 100 | (cond 101 | (nil? a) (str (to-sql b) " IS NULL") 102 | (nil? b) (str (to-sql a) " IS NULL") 103 | :else (str (to-sql a) " = " (to-sql b))))) 104 | 105 | (defmethod fn-handler "<>" [_ a b & more] 106 | (if (seq more) 107 | (apply expand-binary-ops "<>" a b more) 108 | (cond 109 | (nil? a) (str (to-sql b) " IS NOT NULL") 110 | (nil? b) (str (to-sql a) " IS NOT NULL") 111 | :else (str (to-sql a) " <> " (to-sql b))))) 112 | 113 | (defmethod fn-handler "<" [_ a b & more] 114 | (if (seq more) 115 | (apply expand-binary-ops "<" a b more) 116 | (str (to-sql a) " < " (to-sql b)))) 117 | 118 | (defmethod fn-handler "<=" [_ a b & more] 119 | (if (seq more) 120 | (apply expand-binary-ops "<=" a b more) 121 | (str (to-sql a) " <= " (to-sql b)))) 122 | 123 | (defmethod fn-handler ">" [_ a b & more] 124 | (if (seq more) 125 | (apply expand-binary-ops ">" a b more) 126 | (str (to-sql a) " > " (to-sql b)))) 127 | 128 | (defmethod fn-handler ">=" [_ a b & more] 129 | (if (seq more) 130 | (apply expand-binary-ops ">=" a b more) 131 | (str (to-sql a) " >= " (to-sql b)))) 132 | 133 | (defmethod fn-handler "between" [_ field lower upper] 134 | (str (to-sql field) " BETWEEN " 135 | (to-sql lower) " AND " (to-sql upper))) 136 | 137 | (defmethod fn-handler "not-between" [_ field lower upper] 138 | (str (to-sql field) " NOT BETWEEN " 139 | (to-sql lower) " AND " (to-sql upper))) 140 | 141 | ;; Handles MySql's MATCH (field) AGAINST (pattern). The third argument 142 | ;; can be a set containing one or more of :boolean, :natural, or :expand. 143 | (defmethod fn-handler "match" [_ fields pattern & [opts]] 144 | (str "MATCH (" 145 | (comma-join 146 | (map to-sql (if (coll? fields) fields [fields]))) 147 | ") AGAINST (" 148 | (to-sql pattern) 149 | (when (seq opts) 150 | (str " " (space-join (for [opt opts] 151 | (condp = opt 152 | :boolean "IN BOOLEAN MODE" 153 | :natural "IN NATURAL LANGUAGE MODE" 154 | :expand "WITH QUERY EXPANSION"))))) 155 | ")")) 156 | 157 | (def clause-order 158 | "Determines the order that clauses will be placed within generated SQL" 159 | [:insert-into :replace-into :update :delete-from :using 160 | :columns :set :union :union-all :select :from :join :left-join 161 | :right-join :where :group-by :having :order-by 162 | :limit :offset :values :query-values :on-dup-key]) 163 | 164 | (def known-clauses (set clause-order)) 165 | 166 | (defn format 167 | "Takes a SQL map and optional input parameters and returns a vector 168 | of a SQL string and parameters, as expected by clojure.java.jdbc. 169 | 170 | Input parameters will be filled into designated spots according to 171 | name (if a map is provided) or by position (if a sequence is provided). 172 | 173 | Instead of passing parameters, you can use keyword arguments: 174 | :params - input parameters 175 | :quoting - quote style to use for identifiers; one of :ansi (PostgreSQL), 176 | :mysql, :sqlserver, or :oracle. Defaults to no quoting. 177 | :return-param-names - when true, returns a vector of 178 | [sql-str param-values param-names]" 179 | [sql-map & params-or-opts] 180 | (let [opts (when (keyword? (first params-or-opts)) 181 | (apply hash-map params-or-opts)) 182 | params (if (coll? (first params-or-opts)) 183 | (first params-or-opts) 184 | (:params opts))] 185 | (binding [*params* (atom []) 186 | *param-counter* (atom 0) 187 | *param-names* (atom []) 188 | *input-params* (atom params) 189 | *quote-identifier-fn* (quote-fns (:quoting opts))] 190 | (let [sql-str (to-sql sql-map)] 191 | (if (seq @*params*) 192 | (if (:return-param-names opts) 193 | [sql-str @*params* @*param-names*] 194 | (into [sql-str] @*params*)) 195 | [sql-str]))))) 196 | 197 | (defn format-predicate 198 | "Formats a predicate (e.g., for WHERE, JOIN, or HAVING) as a string." 199 | [pred & {:keys [quoting]}] 200 | (binding [*params* (atom []) 201 | *param-counter* (atom 0) 202 | *param-names* (atom []) 203 | *quote-identifier-fn* (or (quote-fns quoting) 204 | *quote-identifier-fn*)] 205 | (let [sql-str (format-predicate* pred)] 206 | (if (seq @*params*) 207 | (into [sql-str] @*params*) 208 | [sql-str])))) 209 | 210 | (defprotocol ToSQL 211 | (-to-sql [x])) 212 | 213 | (declare -format-clause) 214 | 215 | (extend-protocol ToSQL 216 | clojure.lang.Keyword 217 | (-to-sql [x] 218 | (let [s ^String (name x)] 219 | (if (= (.charAt s 0) \?) 220 | (to-sql (param (keyword (subs s 1)))) 221 | (quote-identifier x)))) 222 | 223 | clojure.lang.Symbol 224 | (-to-sql [x] 225 | (let [s ^String (name x)] 226 | (if (= (.charAt s 0) \?) 227 | (to-sql (param (keyword (subs s 1)))) 228 | (quote-identifier x)))) 229 | 230 | java.lang.Number 231 | (-to-sql [x] (str x)) 232 | 233 | java.lang.Boolean 234 | (-to-sql [x] (if x "TRUE" "FALSE")) 235 | 236 | clojure.lang.IPersistentVector 237 | (-to-sql [x] 238 | (if *fn-context?* 239 | ;; list argument in fn call 240 | (paren-wrap (comma-join (map to-sql x))) 241 | ;; alias 242 | (str (to-sql (first x)) 243 | " AS " 244 | (if (string? (second x)) 245 | (quote-identifier (second x)) 246 | (to-sql (second x)))))) 247 | 248 | clojure.lang.IPersistentList 249 | (-to-sql [x] 250 | (binding [*fn-context?* true] 251 | (let [fn-name (name (first x)) 252 | fn-name (fn-aliases fn-name fn-name)] 253 | (apply fn-handler fn-name (rest x))))) 254 | 255 | SqlCall 256 | (-to-sql [x] 257 | (binding [*fn-context?* true] 258 | (let [fn-name (name (.name x)) 259 | fn-name (fn-aliases fn-name fn-name)] 260 | (apply fn-handler fn-name (.args x))))) 261 | 262 | SqlRaw 263 | (-to-sql [x] (.s x)) 264 | 265 | clojure.lang.IPersistentMap 266 | (-to-sql [x] 267 | (let [clause-ops (filter #(contains? x %) clause-order) 268 | sql-str 269 | (binding [*subquery?* true 270 | *fn-context?* false] 271 | (space-join 272 | (map (comp #(-format-clause % x) #(find x %)) 273 | clause-ops)))] 274 | (if *subquery?* 275 | (paren-wrap sql-str) 276 | sql-str))) 277 | 278 | nil 279 | (-to-sql [x] "NULL")) 280 | 281 | (defn sqlable? [x] 282 | (satisfies? ToSQL x)) 283 | 284 | (defn to-sql [x] 285 | (if (satisfies? ToSQL x) 286 | (-to-sql x) 287 | (let [[x pname] 288 | (if (instance? SqlParam x) 289 | (let [pname (param-name x)] 290 | (if (map? @*input-params*) 291 | [(get @*input-params* pname) pname] 292 | (let [x (first @*input-params*)] 293 | (swap! *input-params* rest) 294 | [x pname]))) 295 | ;; Anonymous param name -- :_1, :_2, etc. 296 | [x (keyword (str "_" (swap! *param-counter* inc)))])] 297 | (swap! *param-names* conj pname) 298 | (swap! *params* conj x) 299 | "?"))) 300 | 301 | ;;;; 302 | 303 | (defn format-predicate* [pred] 304 | (if-not (sequential? pred) 305 | (to-sql pred) 306 | (let [[op & args] pred 307 | op-name (name op)] 308 | (if (= "not" op-name) 309 | (str "NOT " (format-predicate* (first args))) 310 | (if (#{"and" "or" "xor"} op-name) 311 | (paren-wrap 312 | (string/join (str " " (string/upper-case op-name) " ") 313 | (map format-predicate* args))) 314 | (to-sql (apply call pred))))))) 315 | 316 | (defn- format-modifiers [sql-map] 317 | (when (:modifiers sql-map) 318 | (str (space-join (map (comp string/upper-case name) 319 | (:modifiers sql-map))) 320 | " "))) 321 | 322 | (defmulti format-clause 323 | "Takes a map entry representing a clause and returns an SQL string" 324 | (fn [clause _] (key clause))) 325 | 326 | (defn- -format-clause 327 | [clause _] 328 | (binding [*clause* (key clause)] 329 | (format-clause clause _))) 330 | 331 | (defmethod format-clause :default [& _] 332 | "") 333 | 334 | (defmethod format-clause :union [[_ select-stmts] _] 335 | (string/join " UNION " (map to-sql select-stmts))) 336 | 337 | (defmethod format-clause :union-all [[_ select-stmts] _] 338 | (string/join " UNION ALL " (map to-sql select-stmts))) 339 | 340 | (defmethod format-clause :select [[_ fields] sql-map] 341 | (str "SELECT " (format-modifiers sql-map) 342 | (comma-join (map to-sql fields)))) 343 | 344 | (defmethod format-clause :from [[_ tables] _] 345 | (str "FROM " (comma-join (map to-sql tables)))) 346 | 347 | (defmethod format-clause :where [[_ pred] _] 348 | (str "WHERE " (format-predicate* pred))) 349 | 350 | (defn format-join [type table pred] 351 | (str (when type 352 | (str (string/upper-case (name type)) " ")) 353 | "JOIN " (to-sql table) 354 | " ON " (format-predicate* pred))) 355 | 356 | (defmethod format-clause :join [[_ join-groups] _] 357 | (space-join (map #(apply format-join :inner %) 358 | (partition 2 join-groups)))) 359 | 360 | (defmethod format-clause :left-join [[_ join-groups] _] 361 | (space-join (map #(apply format-join :left %) 362 | (partition 2 join-groups)))) 363 | 364 | (defmethod format-clause :right-join [[_ join-groups] _] 365 | (space-join (map #(apply format-join :right %) 366 | (partition 2 join-groups)))) 367 | 368 | (defmethod format-clause :group-by [[_ fields] _] 369 | (str "GROUP BY " (comma-join (map to-sql fields)))) 370 | 371 | (defmethod format-clause :having [[_ pred] _] 372 | (str "HAVING " (format-predicate* pred))) 373 | 374 | (defmethod format-clause :order-by [[_ fields] _] 375 | (str "ORDER BY " 376 | (comma-join 377 | (for [field fields] 378 | (if (vector? field) 379 | (let [[field order] field] 380 | (str (to-sql field) " " 381 | (if (= "desc" (name order)) 382 | "DESC" "ASC"))) 383 | (to-sql field)))))) 384 | 385 | (defmethod format-clause :limit [[_ limit] _] 386 | (str "LIMIT " (to-sql limit))) 387 | 388 | (defmethod format-clause :offset [[_ offset] _] 389 | (str "OFFSET " (to-sql offset))) 390 | 391 | (defmethod format-clause :insert-into [[_ table] sql-map] 392 | (str "INSERT " (format-modifiers sql-map) 393 | "INTO " (to-sql table))) 394 | 395 | (defmethod format-clause :replace-into [[_ table] sql-map] 396 | (str "REPLACE " (format-modifiers sql-map) 397 | "INTO " (to-sql table))) 398 | 399 | (defmethod format-clause :columns [[_ fields] _] 400 | (str "(" (comma-join (map to-sql fields)) ")")) 401 | 402 | (defmethod format-clause :values [[_ values] _] 403 | (if (sequential? (first values)) 404 | (str "VALUES " 405 | (comma-join 406 | (for [x values] 407 | (str "(" (comma-join (map to-sql x)) ")")))) 408 | (str 409 | "(" (comma-join (map to-sql (keys (first values)))) ") VALUES " 410 | (comma-join (for [x values] 411 | (str "(" (comma-join (map to-sql (vals x))) ")")))))) 412 | 413 | (defmethod format-clause :query-values [[_ query-values] _] 414 | (to-sql query-values)) 415 | 416 | (defmethod format-clause :update [[_ tables] sql-map] 417 | (str "UPDATE " (format-modifiers sql-map) 418 | (comma-join (map to-sql tables)))) 419 | 420 | (defmethod format-clause :set [[_ values] _] 421 | (str "SET " (comma-join (for [[k v] values] 422 | (str (to-sql k) " = " (to-sql v)))))) 423 | 424 | (defmethod format-clause :on-dup-key [[_ values] _] 425 | (str "ON DUPLICATE KEY UPDATE " 426 | (comma-join (for [[k v] values] 427 | (str (to-sql k) " = " (to-sql v)))))) 428 | 429 | (defmethod format-clause :delete-from [[_ tables] sql-map] 430 | (str "DELETE " (format-modifiers sql-map) 431 | "FROM " (comma-join (map to-sql tables)))) 432 | 433 | (defmethod format-clause :using [[_ tables] _] 434 | (str "USING " (comma-join (map to-sql tables)))) 435 | -------------------------------------------------------------------------------- /src/stch/sql/types.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql.types 2 | "Types for query and DML statements.") 3 | 4 | (deftype SqlCall [name args _meta] 5 | Object 6 | (hashCode [this] (hash-combine (hash name) (hash args))) 7 | (equals [this x] 8 | (cond (identical? this x) true 9 | (instance? SqlCall x) (and (= (.name this) (.name x)) 10 | (= (.args this) (.args x))) 11 | :else false)) 12 | clojure.lang.IObj 13 | (meta [this] _meta) 14 | (withMeta [this m] (SqlCall. (.name this) (.args this) m))) 15 | 16 | (defn call 17 | "Represents a SQL function call. Name should be a keyword." 18 | [name & args] 19 | (SqlCall. name args nil)) 20 | 21 | (defn read-sql-call [form] 22 | (apply call form)) 23 | 24 | (defmethod print-method SqlCall [^SqlCall o ^java.io.Writer w] 25 | (.write w (str "#sql/call " (pr-str (into [(.name o)] (.args o)))))) 26 | 27 | (defmethod print-dup SqlCall [o w] 28 | (print-method o w)) 29 | 30 | ;;;; 31 | 32 | (deftype SqlRaw [s _meta] 33 | Object 34 | (hashCode [this] (hash-combine (hash (class this)) (hash s))) 35 | (equals [this x] (and (instance? SqlRaw x) (= (.s this) (.s x)))) 36 | clojure.lang.IObj 37 | (meta [this] _meta) 38 | (withMeta [this m] (SqlRaw. (.s this) m))) 39 | 40 | (defn raw 41 | "Represents a raw SQL string" 42 | [s] 43 | (SqlRaw. (str s) nil)) 44 | 45 | (defn read-sql-raw [form] 46 | (raw form)) 47 | 48 | (defmethod print-method SqlRaw [^SqlRaw o ^java.io.Writer w] 49 | (.write w (str "#sql/raw " (pr-str (.s o))))) 50 | 51 | (defmethod print-dup SqlRaw [o w] 52 | (print-method o w)) 53 | 54 | ;;;; 55 | 56 | (deftype SqlParam [name _meta] 57 | Object 58 | (hashCode [this] (hash-combine (hash (class this)) (hash (name name)))) 59 | (equals [this x] (and (instance? SqlParam x) (= (.name this) (.name x)))) 60 | clojure.lang.IObj 61 | (meta [this] _meta) 62 | (withMeta [this m] (SqlParam. (.name this) m))) 63 | 64 | (defn param 65 | "Represents a SQL parameter which can be filled in later" 66 | [name] 67 | (SqlParam. name nil)) 68 | 69 | (defn param-name [^SqlParam param] 70 | (.name param)) 71 | 72 | (defn read-sql-param [form] 73 | (param form)) 74 | 75 | (defmethod print-method SqlParam [^SqlParam o ^java.io.Writer w] 76 | (.write w (str "#sql/param " (pr-str (.name o))))) 77 | 78 | (defmethod print-dup SqlParam [o w] 79 | (print-method o w)) 80 | -------------------------------------------------------------------------------- /src/stch/sql/util.clj: -------------------------------------------------------------------------------- 1 | (ns stch.sql.util 2 | "Shared utility fns for query, DML, and DDL." 3 | (:require 4 | [clojure.string :as string])) 5 | 6 | (defn pad [s] 7 | (str " " s " ")) 8 | 9 | (defn comma-join [s] 10 | (string/join ", " s)) 11 | 12 | (defn space-join [s] 13 | (string/join " " s)) 14 | 15 | (defn paren-wrap [x] 16 | (str "(" x ")")) 17 | 18 | (defn undasherize [s] 19 | (string/replace s "-" "_")) 20 | --------------------------------------------------------------------------------