├── .travis.yml ├── .gitignore ├── test └── korma │ └── test │ ├── sql │ └── utils.clj │ ├── integration │ ├── delete.clj │ ├── driver_properties.clj │ ├── mysql.clj │ ├── per_query_database.clj │ ├── update.clj │ ├── with_db.clj │ ├── one_to_one.clj │ ├── one_to_many.clj │ └── helpers.clj │ └── db.clj ├── src └── korma │ ├── sql │ ├── utils.clj │ ├── fns.clj │ └── engine.clj │ ├── db.clj │ └── core.clj ├── project.clj ├── doc ├── korma.mysql.html ├── js │ └── page_effects.js ├── korma.config.html ├── index.html ├── css │ └── default.css ├── korma.db.html └── korma.core.html ├── README.md └── HISTORY.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein run-tests 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-failures 6 | .lein-deps-sum 7 | .lein-repl-history 8 | .nrepl-port 9 | *.swp 10 | /target/ 11 | *.iml 12 | /.idea 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /test/korma/test/sql/utils.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.sql.utils 2 | (:use [clojure.test :only [deftest is testing]] 3 | [korma.sql.utils :only [left-assoc]])) 4 | 5 | (deftest test-left-assoc 6 | (testing "left-assoc with empty list" 7 | (is (= "" (left-assoc [])))) 8 | (testing "left-assoc with one item" 9 | (is (= "1" (left-assoc (list 1))))) 10 | (testing "left-assoc with multiple items" 11 | (is (= "(((1)2)3)4" (left-assoc (list 1 2 3 4)))))) 12 | -------------------------------------------------------------------------------- /test/korma/test/integration/delete.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.delete 2 | (:refer-clojure :exclude [update]) 3 | (:use clojure.test 4 | korma.core 5 | korma.db 6 | [korma.test.integration.helpers :only [populate address]])) 7 | 8 | (defdb mem-db (h2 {:db "mem:delete"})) 9 | 10 | (use-fixtures :once (fn [f] 11 | (default-connection mem-db) 12 | (populate 10) 13 | (f))) 14 | 15 | (deftest delete-returns-count-of-deleted-rows 16 | (testing "Deleting one row" 17 | (is (= 1 (delete address (where {:id 1}))))) 18 | (testing "Deleting multiple rows" 19 | (is (= 3 (delete address (where {:id [< 5]}))))) 20 | (testing "No rows are deleted" 21 | (is (= 0 (delete address (where {:id -1})))))) 22 | -------------------------------------------------------------------------------- /test/korma/test/integration/driver_properties.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.driver-properties 2 | (:refer-clojure :exclude [update]) 3 | (:use clojure.test 4 | korma.db 5 | korma.core 6 | [korma.test.integration.helpers :only [reset-schema]])) 7 | 8 | (defdb mem-db (h2 {:db "mem:driver_properties"})) 9 | (defdb mem-db-no-literals-pooled (h2 {:db "mem:driver_properties" :ALLOW_LITERALS "NONE"})) 10 | (defdb mem-db-no-literals-without-pool (h2 {:db "mem:driver_properties" :ALLOW_LITERALS "NONE" :make-pool? false})) 11 | 12 | (deftest driver-properties-can-be-set-in-db-spec 13 | (with-db mem-db 14 | (reset-schema)) 15 | (testing "Using connection pool" 16 | (is (thrown-with-msg? org.h2.jdbc.JdbcSQLException 17 | #"Literals of this kind are not allowed" 18 | (with-db mem-db-no-literals-pooled 19 | (exec-raw "select * from \"users\" where id = 1"))))) 20 | (testing "Without connection pool" 21 | (is (thrown-with-msg? org.h2.jdbc.JdbcSQLException 22 | #"Literals of this kind are not allowed" 23 | (with-db mem-db-no-literals-without-pool 24 | (exec-raw "select * from \"users\" where id = 1")))))) 25 | -------------------------------------------------------------------------------- /test/korma/test/integration/mysql.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.mysql 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojure.java.jdbc :as jdbc]) 4 | (:use clojure.test 5 | korma.core 6 | korma.db)) 7 | 8 | (defdb live-db-mysql (mysql {:db "korma" :user "root"})) 9 | (defentity users-live-mysql (database live-db-mysql)) 10 | 11 | (def mysql-uri 12 | {:connection-uri "jdbc:mysql://localhost/?user=root"}) 13 | 14 | (defn- setup-korma-db [] 15 | (jdbc/db-do-commands mysql-uri 16 | ["CREATE DATABASE IF NOT EXISTS korma;" 17 | "USE korma;" 18 | "CREATE TABLE IF NOT EXISTS `users-live-mysql` (name varchar(200));"])) 19 | 20 | (defn- clean-korma-db [] 21 | (jdbc/db-do-commands mysql-uri "DROP DATABASE korma;")) 22 | 23 | (use-fixtures :each (fn [f] 24 | (default-connection live-db-mysql) 25 | (setup-korma-db) 26 | (f) 27 | (clean-korma-db))) 28 | 29 | (deftest test-nested-transactions-work 30 | (transaction 31 | (insert users-live-mysql (values {:name "thiago"})) 32 | (transaction 33 | (update users-live-mysql (set-fields {:name "THIAGO"}) (where {:name "thiago"}))))) 34 | 35 | (deftest mysql-count 36 | (insert users-live-mysql (values {:name "thiago"})) 37 | (is (= [{:cnt 1}] 38 | (select users-live-mysql (aggregate (count :*) :cnt))))) 39 | -------------------------------------------------------------------------------- /src/korma/sql/utils.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:no-doc true} 2 | korma.sql.utils 3 | (:require [clojure.string :as string])) 4 | 5 | ;;***************************************************** 6 | ;; map-types 7 | ;;***************************************************** 8 | 9 | (defn generated [s] 10 | {::generated s}) 11 | 12 | (defn sub-query [s] 13 | {::sub s}) 14 | 15 | (defn pred [p args] 16 | {::pred p ::args args}) 17 | 18 | (defn func [f args] 19 | {::func f ::args args}) 20 | 21 | (defn func? [m] 22 | (::func m)) 23 | 24 | (defn pred? [m] 25 | (::pred m)) 26 | 27 | (defn args? [m] 28 | (::args m)) 29 | 30 | (defn sub-query? [m] 31 | (::sub m)) 32 | 33 | (defn generated? [m] 34 | (::generated m)) 35 | 36 | (defn special-map? [m] 37 | (boolean (some #(% m) [func? pred? sub-query? generated?]))) 38 | 39 | ;;***************************************************** 40 | ;; str-utils 41 | ;;***************************************************** 42 | 43 | (defn comma-separated [vs] 44 | (string/join ", " vs)) 45 | 46 | (defn wrap [v] 47 | (str "(" v ")")) 48 | 49 | (defn left-assoc [vs] 50 | (loop [ret "" [v & vs] vs] 51 | (cond 52 | (nil? v) ret 53 | (nil? vs) (str ret v) 54 | :else (recur (wrap (str ret v)) vs)))) 55 | 56 | ;;***************************************************** 57 | ;; collection-utils 58 | ;;***************************************************** 59 | 60 | (defn vconcat [v1 v2] 61 | (vec (concat v1 v2))) 62 | -------------------------------------------------------------------------------- /test/korma/test/integration/per_query_database.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.per-query-database 2 | (:use clojure.test 3 | [korma.db :only [h2 with-db create-db default-connection]] 4 | [korma.core :only [defentity has-many exec-raw insert values select with database]])) 5 | 6 | (def mem-db (create-db (h2 {:db "mem:query_database"}))) 7 | 8 | (defentity email) 9 | 10 | (defentity user 11 | (has-many email)) 12 | 13 | (def schema 14 | ["drop table if exists \"user\";" 15 | "drop table if exists \"email\";" 16 | "create table \"user\" (\"id\" integer primary key, 17 | \"name\" varchar(100));" 18 | "create table \"email\" (\"id\" integer primary key, 19 | \"user_id\" integer, 20 | \"email_address\" varchar(100), 21 | foreign key (\"user_id\") references \"user\"(\"id\"));"]) 22 | 23 | (defn- setup-db [] 24 | (with-db mem-db 25 | (dorun (map exec-raw schema)) 26 | (insert user (values {:id 1 :name "Chris"})) 27 | (insert email (values {:id 1 :user_id 1 :email_address "chris@email.com"})))) 28 | 29 | (use-fixtures :once (fn [f] 30 | (default-connection nil) 31 | (setup-db) 32 | (f))) 33 | 34 | (deftest use-database-from-parent-when-fetching-children 35 | (is (= [{:id 1 :name "Chris" :email [{:id 1 :user_id 1 :email_address "chris@email.com"}]}] 36 | (select user 37 | (database mem-db) 38 | (with email))))) 39 | -------------------------------------------------------------------------------- /test/korma/test/integration/update.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.update 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojure.string]) 4 | (:use clojure.test 5 | [korma.db :only [defdb h2 default-connection]] 6 | [korma.core :only [defentity table transform exec-raw insert values update update* set-fields where]])) 7 | 8 | (defdb mem-db (h2 {:db "mem:update"})) 9 | 10 | (defentity user) 11 | 12 | (defentity user-with-transform 13 | (table :user) 14 | (transform #(update-in % [:name] clojure.string/capitalize))) 15 | 16 | (def schema 17 | ["drop table if exists \"user\";" 18 | "create table \"user\" (\"id\" integer primary key, 19 | \"name\" varchar(100), 20 | \"active\" boolean default false);"]) 21 | 22 | (defn- setup-db [] 23 | (dorun (map exec-raw schema)) 24 | (insert user (values [{:id 1 :name "James"} 25 | {:id 2 :name "John"} 26 | {:id 3 :name "Robert"}]))) 27 | 28 | (use-fixtures :once (fn [f] 29 | (default-connection mem-db) 30 | (setup-db) 31 | (f))) 32 | 33 | (deftest update-returns-count-of-updated-rows 34 | (testing "Updating one row" 35 | (is (= 1 (update user (set-fields {:active true}) (where {:id 1}))))) 36 | (testing "Updating multiple rows" 37 | (is (= 3 (update user (set-fields {:active true}))))) 38 | (testing "No rows are updated" 39 | (is (= 0 (update user (set-fields {:active true}) (where {:id [> 10]}))))) 40 | (testing "Entity with transform fn" 41 | (is (= 1 (update user-with-transform (set-fields {:active true}) (where {:id 1})))))) 42 | -------------------------------------------------------------------------------- /src/korma/sql/fns.clj: -------------------------------------------------------------------------------- 1 | (ns korma.sql.fns 2 | (:require [korma.db :as db] 3 | [korma.sql.engine :as eng] 4 | [korma.sql.utils :as utils]) 5 | (:use [korma.sql.engine :only [infix group-with sql-func trinary wrapper]])) 6 | 7 | ;;***************************************************** 8 | ;; Predicates 9 | ;;***************************************************** 10 | 11 | 12 | (defn pred-and [& args] 13 | (apply eng/pred-and args)) 14 | 15 | (defn pred-or [& args] (group-with " OR " args)) 16 | (defn pred-not [v] (wrapper "NOT" v)) 17 | 18 | (defn pred-in [k v] (infix k "IN" v)) 19 | (defn pred-not-in [k v] (infix k "NOT IN" v)) 20 | (defn pred-> [k v] (infix k ">" v)) 21 | (defn pred-< [k v] (infix k "<" v)) 22 | (defn pred->= [k v] (infix k ">=" v)) 23 | (defn pred-<= [k v] (infix k "<=" v)) 24 | (defn pred-like [k v] (infix k "LIKE" v)) 25 | (defn pred-ilike [k v] (infix k "ILIKE" v)) 26 | 27 | (defn pred-exists [v] (wrapper "EXISTS" v)) 28 | 29 | (defn pred-between [k [v1 v2]] 30 | (trinary k "BETWEEN" v1 "AND" v2)) 31 | 32 | (def pred-= eng/pred-=) 33 | (defn pred-not= [k v] (cond 34 | (and k v) (infix k "<>" v) 35 | k (infix k "IS NOT" v) 36 | v (infix v "IS NOT" k))) 37 | 38 | ;;***************************************************** 39 | ;; Aggregates 40 | ;;***************************************************** 41 | 42 | (defn agg-count [_query_ v] 43 | (if (= "*" (name v)) 44 | (sql-func "COUNT" (utils/generated "*")) 45 | (sql-func "COUNT" v))) 46 | 47 | (defn agg-sum [_query_ v] (sql-func "SUM" v)) 48 | (defn agg-avg [_query_ v] (sql-func "AVG" v)) 49 | (defn agg-stdev [_query_ v] (sql-func "STDEV" v)) 50 | (defn agg-min [_query_ v] (sql-func "MIN" v)) 51 | (defn agg-max [_query_ v] (sql-func "MAX" v)) 52 | (defn agg-first [_query_ v] (sql-func "FIRST" v)) 53 | (defn agg-last [_query_ v] (sql-func "LAST" v)) 54 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject korma "0.5.0-RC1" 2 | :description "Tasty SQL for Clojure" 3 | :url "http://github.com/korma/Korma" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :mailing-list {:name "Korma Google Group" 7 | :subscribe "https://groups.google.com/group/sqlkorma"} 8 | :codox {:exclude [korma.sql.engine 9 | korma.sql.fns 10 | korma.sql.utils] 11 | :src-dir-uri "https://github.com/korma/Korma/blob/master/" 12 | :src-linenum-anchor-prefix "L"} 13 | 14 | :dependencies [[org.clojure/clojure "1.8.0"] 15 | [com.mchange/c3p0 "0.9.5.2"] 16 | [org.clojure/java.jdbc "0.6.1"]] 17 | 18 | :min-lein-version "2.0.0" 19 | 20 | :profiles {:dev {:dependencies [[gui-diff "0.6.6"] 21 | [postgresql "9.3-1102.jdbc41"] 22 | [slamhound "1.5.5"] 23 | [criterium "0.4.3"]] 24 | :plugins [[codox "0.8.12"] 25 | [jonase/eastwood "0.2.1"] 26 | [lein-localrepo "0.5.3"]]} 27 | :test {:dependencies [[mysql/mysql-connector-java "5.1.35"] 28 | [com.h2database/h2 "1.4.187"] 29 | [criterium "0.4.3"]]} 30 | :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} 31 | :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} 32 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 33 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 34 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 35 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0-alpha13"]]}} 36 | :aliases {"run-tests" ["with-profile" "1.4:1.5:1.6:1.7:1.8:1.9" "test"] 37 | "slamhound" ["run" "-m" "slam.hound"]} 38 | :jvm-opts ["-Dline.separator=\n"]) 39 | -------------------------------------------------------------------------------- /test/korma/test/integration/with_db.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.with-db 2 | (:refer-clojure :exclude [update]) 3 | (:use clojure.test 4 | korma.db 5 | korma.core 6 | korma.test.integration.helpers) 7 | (:import [java.util.concurrent CountDownLatch])) 8 | 9 | (defn mem-db [] 10 | (create-db (h2 {:db "mem:with_db_test"}))) 11 | 12 | (defn mem-db-2 [] 13 | (create-db (h2 {:db "mem:with_db_test_2"}))) 14 | 15 | (deftest with-db-success 16 | (testing "with-db with setting *current-db* in with-db binding" 17 | (with-db (mem-db) (populate 2)) 18 | (is (> (count (:address (first (with-db (mem-db) 19 | (select user (with address))))))) 1))) 20 | 21 | (defn delete-some-rows-from-db [] 22 | (delete address)) 23 | 24 | (deftest thread-safe-with-db 25 | (testing "with-db using binding" 26 | (let [db1 (mem-db) 27 | db2 (mem-db-2)] 28 | ;; let's create some records in the first db 29 | (with-db db1 (populate 2)) 30 | 31 | ;; let's create some records in the second db 32 | (with-db db2 (populate 2)) 33 | 34 | (default-connection db1) 35 | 36 | ;; we will create 2 threads and execute them at the same time 37 | ;; using a CountDownLatch to synchronize their execution 38 | (let [latch (CountDownLatch. 2) 39 | t1 (future (with-db db2 40 | (.countDown latch) 41 | (.await latch) 42 | (delete-some-rows-from-db))) 43 | t2 (future (with-db db1 44 | (.countDown latch) 45 | (.await latch) 46 | (delete-some-rows-from-db)))] 47 | @t1 48 | @t2 49 | (.await latch)) 50 | 51 | (default-connection nil) 52 | 53 | (let [addresses-mem-db-1 (with-db db1 54 | (select address)) 55 | addresses-mem-db-2 (with-db db2 56 | (select address))] 57 | (is (= (count addresses-mem-db-1) 0)) 58 | (is (= (count addresses-mem-db-2) 0)))))) 59 | -------------------------------------------------------------------------------- /doc/korma.mysql.html: -------------------------------------------------------------------------------- 1 | 2 | korma.mysql documentation

korma.mysql

count

(count _query_ v)
On MySQL, when an argument for COUNT() is a '*',
3 | it must be a simple '*', instead of 'fieldname.*'.
-------------------------------------------------------------------------------- /test/korma/test/integration/one_to_one.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.one-to-one 2 | (:require clojure.string) 3 | (:use clojure.test 4 | [korma.db :only [defdb h2 default-connection]] 5 | [korma.core :only [defentity pk fk belongs-to has-one transform 6 | exec-raw insert values select with]])) 7 | 8 | (defdb mem-db (h2 {:db "mem:one_to_one_test"})) 9 | 10 | (defentity state 11 | (pk :state_id) 12 | (transform #(update-in % [:name] clojure.string/capitalize))) 13 | 14 | (defentity address 15 | (pk :address_id) 16 | (belongs-to state (fk :id_of_state)) 17 | (transform #(update-in % [:street] clojure.string/capitalize))) 18 | 19 | (defentity user 20 | (has-one address (fk :id_of_user))) 21 | 22 | (def schema 23 | ["drop table if exists \"state\";" 24 | "drop table if exists \"user\";" 25 | "drop table if exists \"address\";" 26 | "create table \"state\" (\"state_id\" varchar(20), 27 | \"name\" varchar(100));" 28 | "create table \"user\" (\"id\" integer primary key, 29 | \"name\" varchar(100));" 30 | "create table \"address\" (\"address_id\" integer primary key, 31 | \"id_of_user\" integer, 32 | \"id_of_state\" varchar(20), 33 | \"street\" varchar(200), 34 | foreign key (\"id_of_user\") references \"user\"(\"id\"), 35 | foreign key (\"id_of_state\") references \"state\"(\"state_id\"));"]) 36 | 37 | (defn- setup-db [] 38 | (default-connection mem-db) 39 | (dorun (map exec-raw schema)) 40 | (insert :state (values [{:state_id "CA" :name "california"} 41 | {:state_id "PA" :name "pennsylvania"}])) 42 | (insert :user (values [{:id 1 :name "Chris"} 43 | {:id 2 :name "Alex"}])) 44 | (insert :address (values [{:address_id 101 :street "main street" :id_of_state "CA" :id_of_user 1} 45 | {:address_id 102 :street "park street" :id_of_state "PA" :id_of_user 2}]))) 46 | 47 | (use-fixtures :once (fn [f] 48 | (setup-db) 49 | (f))) 50 | 51 | (deftest belongs-to-with-transformer-users-correct-join-keys 52 | (is (= [{:id 1 :name "Chris" :address_id 101 :street "Main street" :id_of_state "CA" :id_of_user 1} 53 | {:id 2 :name "Alex" :address_id 102 :street "Park street" :id_of_state "PA" :id_of_user 2}] 54 | (select user (with address))))) 55 | 56 | (deftest has-one-with-transformer-users-correct-join-keys 57 | (is (= [{:address_id 101 :street "Main street" :id_of_state "CA" :id_of_user 1 :state_id "CA" :name "California"} 58 | {:address_id 102 :street "Park street" :id_of_state "PA" :id_of_user 2 :state_id "PA" :name "Pennsylvania"}] 59 | (select address (with state))))) 60 | -------------------------------------------------------------------------------- /test/korma/test/integration/one_to_many.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.one-to-many 2 | (:refer-clojure :exclude [update]) 3 | (:require [clojure.string :as string] 4 | [criterium.core :as cr]) 5 | (:use clojure.test 6 | korma.core 7 | korma.db 8 | korma.test.integration.helpers)) 9 | 10 | (defn mem-db [] 11 | (create-db (h2 {:db "mem:one_to_many_test"}))) 12 | 13 | (def ^:dynamic *data*) 14 | 15 | (use-fixtures :each 16 | (fn [t] 17 | (with-db (mem-db) 18 | (transaction 19 | (binding [*data* (populate 100)] 20 | (t)) 21 | (rollback))))) 22 | 23 | (deftest test-one-to-many 24 | (is (= (sort-by :id (:user *data*)) 25 | (select user 26 | (with address) 27 | (order :id)))) 28 | (doseq [u (:user *data*)] 29 | (is 30 | (= [u] 31 | (select user 32 | (where {:id (:id u)}) 33 | (with address))))) 34 | (doseq [u (:user *data*)] 35 | (is 36 | (= [(update-in u [:address] 37 | (fn [addrs] 38 | (map #(select-keys % [:street :city]) addrs)))] 39 | (select user 40 | (where {:id (:id u)}) 41 | (with address 42 | (fields :street :city))))))) 43 | 44 | (deftest test-one-to-many-batch 45 | (let [user-ids (map :id (:user *data*))] 46 | (is 47 | (= (select user 48 | (where {:id [in user-ids]}) 49 | (with address 50 | (with state))) 51 | (select user 52 | (where {:id [in user-ids]}) 53 | (with-batch address 54 | (with-batch state)))) 55 | "`with-batch` should return the same data as `with`") 56 | (is 57 | (= (select user 58 | (where {:id [in user-ids]}) 59 | (with address 60 | ;; with-batch will add the foreign key 61 | (fields :user_id :street :city) 62 | (with state))) 63 | (select user 64 | (where {:id [in user-ids]}) 65 | (with-batch address 66 | (fields :street :city) 67 | (with-batch state)))) 68 | "`with-batch` should return the same data as `with` when using explicit projection"))) 69 | 70 | (defn- getenv [s] 71 | (or (System/getenv s) 72 | (System/getProperty s))) 73 | 74 | (when (= (getenv "BENCH") "true") 75 | (deftest bench-one-to-many-batch 76 | (let [user-ids (map :id (:user *data*))] 77 | (println "benchmarking with plain `with`") 78 | (cr/quick-bench 79 | (->> 80 | (select user 81 | (where {:id [in user-ids]}) 82 | (with address 83 | (with state))) 84 | (map 85 | #(update-in % [:address] doall)) 86 | doall)) 87 | (println "benchmarking with `with-batch`") 88 | (cr/quick-bench 89 | (select user 90 | (where {:id [in user-ids]}) 91 | (with-batch address 92 | (with-batch state))))))) 93 | 94 | (deftest test-one-to-many-batch-limitations 95 | (doseq [banned [#(order % :street) 96 | #(group % :street) 97 | #(limit % 1) 98 | #(offset % 1)]] 99 | (is (thrown? Exception 100 | (select user 101 | (with-batch address 102 | banned)))))) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Korma 2 | 3 | Tasty SQL for Clojure. 4 | 5 | ## TravisCI Status 6 | 7 | [![Build Status](https://travis-ci.org/korma/Korma.png)](https://travis-ci.org/korma/Korma) 8 | 9 | ## Getting started 10 | 11 | Simply add Korma as a dependency to your lein project: 12 | 13 | ```clojure 14 | [korma "0.4.3"] 15 | ``` 16 | 17 | ## Stable and Edge releases 18 | 19 | The most recent stable release is Korma 0.4.3, as noted above. 20 | 21 | The current edge release is Korma 0.5.0-RC1 (released Feb 28, 2018). It is not guaranteed to be bug free, and we'd welcome help from the community in identifying any bugs or regressions. The plan is to either release a second release candidate in May or, if no bugs have surfaced, for Korma 0.5.0 to be released at that time. 22 | 23 | ## Docs and Real Usage 24 | 25 | * [http://sqlkorma.com](http://sqlkorma.com) 26 | * [API Docs](http://korma.github.com/Korma/) 27 | 28 | To get rid of the ridiculously verbose logging, add the following into src/log4j.xml: 29 | 30 | ```xml 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ``` 39 | 40 | And include log4j in your project.clj: 41 | 42 | ```clojure 43 | [log4j "1.2.15" :exclusions [javax.mail/mail 44 | javax.jms/jms 45 | com.sun.jdmk/jmxtools 46 | com.sun.jmx/jmxri]] 47 | ``` 48 | 49 | ## Examples of generated queries: 50 | 51 | ```clojure 52 | 53 | (use 'korma.db) 54 | (defdb db (postgres {:db "mydb" 55 | :user "user" 56 | :password "dbpass"})) 57 | 58 | (use 'korma.core) 59 | (defentity users) 60 | 61 | (select users) 62 | ;; executes: SELECT * FROM users 63 | 64 | (select users 65 | (fields :username :id)) 66 | ;; executes: SELECT users.username, users.id FROM users 67 | 68 | (select users 69 | (where {:username "chris"})) 70 | ;; executes: SELECT * FROM users WHERE (users.username = 'chris') 71 | 72 | (select users 73 | (where {:active true}) 74 | (order :created) 75 | (limit 5) 76 | (offset 3)) 77 | ;; executes: SELECT * FROM users WHERE (users.active = TRUE) ORDER BY users.created DESC LIMIT 5 OFFSET 3 78 | 79 | (select users 80 | (where (or (= :username "chris") 81 | (= :email "chris@chris.com")))) 82 | ;; executes: SELECT * FROM users WHERE (users.username = 'chris' OR users.email = 'chris@chris.com') 83 | 84 | (select users 85 | (where {:username [like "chris"] 86 | :status "active" 87 | :location [not= nil]})) 88 | ;; executes SELECT * FROM users WHERE (users.username LIKE 'chris' AND users.status = 'active' AND users.location IS NOT NULL) 89 | 90 | (select users 91 | (where (or {:username "chris" 92 | :first "chris"} 93 | {:email [like "%@chris.com"]}))) 94 | ;; executes: SELECT * FROM users WHERE ((users.username = 'chris' AND users.first = 'chris') OR users.email LIKE '%@chris.com)' 95 | 96 | 97 | (defentity address 98 | (entity-fields :street :city :zip)) 99 | 100 | (defentity users 101 | (has-one address)) 102 | 103 | (select users 104 | (with address)) 105 | ;; SELECT address.street, address.city, address.zip FROM users LEFT JOIN address ON users.id = address.users_id 106 | 107 | ``` 108 | 109 | ## License 110 | 111 | Copyright (C) 2011 Chris Granger 112 | 113 | Distributed under the Eclipse Public License, the same as Clojure. 114 | -------------------------------------------------------------------------------- /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 scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('#vars a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment("#vars a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('#vars .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function resizeSidebars() { 75 | var nsWidth = sidebarContentWidth('#namespaces') + 30 76 | var varWidth = 0 77 | 78 | if ($('#vars').length != 0) { 79 | varWidth = sidebarContentWidth('#vars') + 30 80 | } 81 | 82 | // snap to grid 83 | var snap = 30; 84 | nsWidth = Math.ceil(nsWidth / snap) * snap; 85 | varWidth = Math.ceil(varWidth / snap) * snap; 86 | 87 | $('#namespaces').css('width', nsWidth) 88 | $('#vars').css('width', varWidth) 89 | $('#vars, .namespace-index').css('left', nsWidth + 1) 90 | $('.namespace-docs').css('left', nsWidth + varWidth + 2) 91 | } 92 | 93 | $(window).ready(resizeSidebars) 94 | $(window).ready(setCurrentVarLink) 95 | $(window).ready(function() { persistScrollPosition('#namespaces')}) 96 | $(window).ready(function() { 97 | $('#content').scroll(setCurrentVarLink) 98 | $(window).resize(setCurrentVarLink) 99 | }) 100 | -------------------------------------------------------------------------------- /test/korma/test/integration/helpers.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.integration.helpers 2 | (:refer-clojure :exclude [update]) 3 | (:use clojure.test 4 | korma.core)) 5 | 6 | (declare user address state) 7 | 8 | (defentity user 9 | (table :users) 10 | (has-many address (fk :user_id)) 11 | (transform 12 | #(update-in % [:address] (partial sort-by :id)))) 13 | 14 | (defentity address 15 | (belongs-to user (fk :user_id)) 16 | (belongs-to state)) 17 | 18 | (defentity state) 19 | 20 | (def initial-data 21 | {:state 22 | [{:id "CA" :name "California"} 23 | {:id "PA" :name "Pennsylvania"}] 24 | :user [] 25 | :address []}) 26 | 27 | (def schema 28 | ["drop table if exists \"state\";" 29 | "create table \"state\" (\"id\" varchar(20), \"name\" varchar(100));" 30 | "drop table if exists \"users\";" 31 | "create table \"users\" (\"id\" integer auto_increment primary key, \"name\" varchar(100), \"age\" integer);" 32 | "drop table if exists \"address\";" 33 | "create table \"address\" (\"id\" integer auto_increment primary key, \"user_id\" integer , \"state_id\" varchar(20), \"number\" varchar(20), \"street\" varchar(200), \"city\" varchar(200), \"zip\" varchar(10), foreign key (\"user_id\") references \"users\"(\"id\"), foreign key (\"state_id\") references \"state\"(\"id\"));"]) 34 | 35 | (defn- random-string [] 36 | (str (java.util.UUID/randomUUID))) 37 | 38 | (defn reset-schema [] 39 | (dorun 40 | (map exec-raw schema))) 41 | 42 | (defn- populate-states [data] 43 | (insert state 44 | (values (:state data)))) 45 | 46 | (defn- populate-users 47 | "add num-users to the database and to the `data` map" 48 | [data num-users] 49 | (reduce 50 | (fn [data user-id] 51 | (let [u {:id user-id 52 | :name (random-string) 53 | :age (rand-int 100)}] 54 | (insert user 55 | (values u)) 56 | (update-in data 57 | [:user] 58 | conj (assoc u :address [])))) 59 | data 60 | (range num-users))) 61 | 62 | (defn- populate-addresses 63 | "add up to max-addresses-per-user addresses to each user. ensure that at least one user has no addresses at all" 64 | [data max-addresses-per-user] 65 | (assoc data 66 | :user (vec 67 | (cons 68 | (first (:user data)) 69 | (map 70 | (fn [user] 71 | (let [addrs (doall 72 | (for [n (range (rand-int max-addresses-per-user))] 73 | (let [a {:user_id (:id user) 74 | :street (random-string) 75 | :number (subs (random-string) 0 10) 76 | :city (random-string) 77 | :zip (str (rand-int 10000)) 78 | :state_id (-> data :state rand-nth :id)} 79 | inserted (insert address (values a)) 80 | ;; insert returns a map with a single key 81 | ;; the key depends on the underlying database, but the 82 | ;; value is the generated value of the key column 83 | inserted-id (first (vals inserted)) 84 | a (assoc a :id inserted-id)] 85 | a)))] 86 | (assoc user :address (vec addrs)))) 87 | (rest (:user data))))))) 88 | 89 | (defn populate 90 | "populate the test database with random data and return a data structure that mirrors the data inserted into the database." 91 | [num-users] 92 | (reset-schema) 93 | (-> initial-data 94 | populate-states 95 | (populate-users num-users) 96 | (populate-addresses 10))) 97 | -------------------------------------------------------------------------------- /doc/korma.config.html: -------------------------------------------------------------------------------- 1 | 2 | korma.config documentation

korma.config

extract-options

(extract-options {:keys [naming delimiters subprotocol alias-delimiter]})

merge-defaults

(merge-defaults opts)

options

set-delimiters

(set-delimiters & cs)
Set the global default for field delimiters in connections. Delimiters can
 3 | either be a string or a vector of the start and end:
 4 | 
 5 | (set-delimiters "`")
 6 | (set-delimiters ["[" "]"])

set-naming

(set-naming strategy)
Set the naming strategy to use. The strategy should be a map with transforms
 7 | to be applied to field names (:fields) and the keys for generated maps (:keys)
 8 | e.g:
 9 | 
10 | (set-naming {:keys string/lower-case :fields string/upper-case})
-------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 0.5.0-RC1 2 | --------- 3 | * BREAKING CHANGE: remove global options (immoh) 4 | * Support :connection-uri for db configuration (immoh) 5 | * Fix: do not prefix asterisk when used as count column (immoh) 6 | * Add support for for composite primary and foreign keys (k13gomez) 7 | * Add license to project definition (paipa4) 8 | * Update `insert` docstring to more accurately reflect hash created by MSSQL (rpazyaquian) 9 | * Add function for adding SQL comments to queries (AlexBaranosky) 10 | 11 | 12 | 0.4.3 13 | ----- 14 | * Upgrade clojure.java.jdbc to 0.6.1 (venantius) 15 | * Upgrade c3p0 to 0.9.5.2 (gfZeng) 16 | * Add support for excluding c3p0 (k13gomez) 17 | * Add support for foreign key name option for parent table in relation (k13gomez) 18 | * Make order of rows for insert queries not matter (tie-rack) 19 | * Add support for ILIKE predicate (arttuka) 20 | * Add support for predicate keywords (zjhmale) 21 | * Fix reflection warnings (divs1210) 22 | * Fix arglist metadata for `join` (venantius) 23 | * Fix syntax error in README (zjhsdtc) 24 | * Update entity-fields documentation (jdpopkin) 25 | 26 | 27 | 0.4.2 28 | ----- 29 | * Fix regression in 0.4.1 causing global config changes not to take affect if defdb is called first 30 | * This fix reverts "Honor db specific options when using `with-db`" which will be reimplemented in future release 31 | * Bump clojure/java.jdbc version to 0.3.7 (danielsoro) 32 | 33 | 0.4.1 34 | ----- 35 | * Fix warnings when using Clojure 1.7 (glittershark) 36 | * Bump clojure/java.jdbc version to 0.3.6 37 | * Add possibility to set initial pool size for connection pool (AKurilin) 38 | * Add support for driver properties when using connection pool (federkasten) 39 | * Return number of deleted rows 40 | * Honor db specific options when using `with-db` (ls4f) 41 | * Fix lazy namespace resolution of relations (juhovh) 42 | * Fix arity of `order` (lokori) 43 | * Stop printing exceptions to \*out\* 44 | 45 | 0.4.0 46 | ----- 47 | * Stop using deprecated implementation of [clojure.java.jdbc](https://github.com/clojure/java.jdbc) (scttnlsn) 48 | * Return number of updated rows 49 | * Add support for read-only transactions and transactions with a specific isolation level (ls4f) 50 | * Add support for joining entities using a specific join type (starks67) 51 | 52 | 0.3.3 53 | ----- 54 | * Fix regression in 0.3.2 causing NPE when fetching relations and default connection hasn't been set 55 | * Fix exception with multiple raws in where clause 56 | * Use db from main query when fetching children with no db specified 57 | 58 | 0.3.2 59 | ----- 60 | * Support EXISTS keyword (pocket7878) 61 | * Add helper for creating FirebirdSQL connection (ls4f) 62 | * Fix concurrency issue on with-db (David Raphael) 63 | * Use bind parameters for boolean values 64 | * Fix error message for missing db connection when lazily fetching subentities 65 | 66 | 0.3.1 67 | ----- 68 | * SQL generation is now deterministic across Clojure versions 69 | * Fix for using wrong delimiters in JOIN query for has-one and belongs-to relations 70 | * Fix join columns used for fetching belongs-to relation when subentity is not using default primary key and has transform fn defined 71 | * Fix for losing db connection or using wrong connection when lazily fetching subentities (David Raphael) 72 | 73 | 0.3.0 74 | ----- 75 | No changes since the previous release candidate. 76 | 77 | 0.3.0-RC8 78 | --------- 79 | * Ensure fields are not overriden when using transform fn on subentity of one-to-one relation 80 | 81 | 0.3.0-RC7 82 | --------- 83 | * Upgrade java.jdbc to 0.3, use deprecated namespace (MerelyAPseudonym) 84 | * Add support for HP Vertica (Chris Benninger) 85 | * Add transform fn support to one-to-one relations (Immo Heikkinen) 86 | 87 | 0.3.0-RC6 88 | --------- 89 | * Options for IdleConnectionTestPeriod and TestConnectionOnCheckin (federkasten) 90 | * defonce config options to prevent issues with MySQL delimiters (cosmi) 91 | * README cleanup (writa) 92 | * Can now run a test query on checkout for connection verification (joekarl) 93 | * Introduced alias-delimiter option to choose word for alias (ktsujister and Ringman) 94 | * Fixed issue with set-fields not applying db delimiters (mjtodd) 95 | * Made korma.sql.engine/bind-params public for incubator (sgrove) 96 | 97 | 0.3.0-RC5 98 | --------- 99 | 100 | * support nested `korma.db/transaction` calls (josephwilk) 101 | * integrated with TravisCI 102 | 103 | 0.3.0-RC4 104 | --------- 105 | 106 | * MS Access db config helper function (Vadim Geshel) 107 | * bugfix in `do-query` w/ options (David Kettering) 108 | 109 | 0.3.0-RC3 110 | --------- 111 | 112 | * Add db-spec creator for ODBC connections (David Kettering) 113 | * Add stdev aggregate (David Kettering) 114 | * Parenthesize multiple JOIN expressions (David Kettering) 115 | * Use optional AS keyword in alias clauses (David Kettering) 116 | * Use <> instead of != in relational comparisons (David Kettering) 117 | 118 | 0.3.0-RC2 119 | --------- 120 | 121 | * Connection pool always returns a connection pool, use :make-pool? option to disable pool creation 122 | 123 | 0.3.0-RC1 124 | --------- 125 | 126 | * Allow opting out of the connection pool creation 127 | * Allow sending other kinds of sb specs straight through to java.jdbc 128 | 129 | 0.3.0-beta15 130 | --------------------- 131 | 132 | * transactional wrapping for multiple databases (Timo Westkämper) 133 | * fixing macro expansion bug introduced in recent beta (Moritz Heidkamp, Joshua Eckroth) 134 | * Can use mysql/count so that (count :*) works correctly on MySQL (Tsutomu YANO) 135 | * Added `union`, `union-all`, and `intersect` queries 136 | 137 | 0.3.0-beta14 138 | ------------ 139 | 140 | * Support for `many-to-many` relationships (Dennis Roberts) 141 | * Fixed table-name delimiting, and at the same time, support Postgres' schema and 142 | queries covering multiple databases. 143 | See: https://github.com/korma/Korma/pull/105 (Tsutomu YANO) 144 | 145 | 0.3.0-beta13 146 | ------------ 147 | 148 | * Set Min/Max Connection Pool Size from db spec (Nick Zalabak) 149 | * Merge defaults instead of overwriting them (Jim Crossley) 150 | * DB specs can reference existing datasources or JNDI references (Jim Crossley) 151 | * Added `between` predicate (Charles Duffy) 152 | * Corrected default port for MS SQL Server (Alexander Zolotko) 153 | * Added basic `having` support (Mike Aldred) 154 | * Added `korma.db/h2` to create a database specification for a h2 database (Juha Syrjälä) 155 | * Insert statements with empty values turn into SQL of "DO 0", a NOOP 156 | * Empty where clauses are ignored, instead of creating illegal SQL 157 | 158 | *started on Dec 27, 2012* 159 | 160 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | Korma 0.5.0-RC1 API documentation

Korma 0.5.0-RC1

Tasty SQL for Clojure

korma.core

Core querying and entity functions

korma.db

Functions for creating and managing database specifications.
-------------------------------------------------------------------------------- /doc/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 15px; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | margin: 15px 0; 10 | } 11 | 12 | h2 { 13 | font-weight: normal; 14 | font-size: 28px; 15 | padding: 10px 0 2px 0; 16 | margin: 0; 17 | } 18 | 19 | #header, #content, .sidebar { 20 | position: fixed; 21 | } 22 | 23 | #header { 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | height: 20px; 28 | background: #444; 29 | color: #fff; 30 | padding: 5px 7px; 31 | } 32 | 33 | #content { 34 | top: 30px; 35 | right: 0; 36 | bottom: 0; 37 | overflow: auto; 38 | background: #fff; 39 | color: #333; 40 | padding: 0 18px; 41 | } 42 | 43 | .sidebar { 44 | position: fixed; 45 | top: 30px; 46 | bottom: 0; 47 | overflow: auto; 48 | } 49 | 50 | #namespaces { 51 | background: #e2e2e2; 52 | border-right: solid 1px #cccccc; 53 | left: 0; 54 | width: 250px; 55 | } 56 | 57 | #vars { 58 | background: #f2f2f2; 59 | border-right: solid 1px #cccccc; 60 | left: 251px; 61 | width: 200px; 62 | } 63 | 64 | .namespace-index { 65 | left: 251px; 66 | } 67 | 68 | .namespace-docs { 69 | left: 452px; 70 | } 71 | 72 | #header { 73 | background: -moz-linear-gradient(top, #555 0%, #222 100%); 74 | background: -webkit-linear-gradient(top, #555 0%, #333 100%); 75 | background: -o-linear-gradient(top, #555 0%, #222 100%); 76 | background: -ms-linear-gradient(top, #555 0%, #222 100%); 77 | background: linear-gradient(top, #555 0%, #222 100%); 78 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 79 | z-index: 100; 80 | } 81 | 82 | #header h1 { 83 | margin: 0; 84 | padding: 0; 85 | font-size: 12pt; 86 | font-weight: lighter; 87 | text-shadow: -1px -1px 0px #333; 88 | } 89 | 90 | #header a, .sidebar a { 91 | display: block; 92 | text-decoration: none; 93 | } 94 | 95 | #header a { 96 | color: #fff; 97 | } 98 | 99 | .sidebar a { 100 | color: #333; 101 | } 102 | 103 | #header h2 { 104 | float: right; 105 | font-size: 9pt; 106 | font-weight: normal; 107 | margin: 3px 3px; 108 | padding: 0; 109 | color: #bbb; 110 | } 111 | 112 | #header h2 a { 113 | display: inline; 114 | } 115 | 116 | .sidebar h3 { 117 | margin: 0; 118 | padding: 10px 10px 0 10px; 119 | font-size: 19px; 120 | font-weight: normal; 121 | } 122 | 123 | .sidebar ul { 124 | padding: 0.5em 0em; 125 | margin: 0; 126 | } 127 | 128 | .sidebar li { 129 | display: block; 130 | vertical-align: middle; 131 | } 132 | 133 | .sidebar li a, .sidebar li .no-link { 134 | border-left: 3px solid transparent; 135 | padding: 0 7px; 136 | white-space: nowrap; 137 | } 138 | 139 | .sidebar li .no-link { 140 | display: block; 141 | color: #777; 142 | font-style: italic; 143 | } 144 | 145 | .sidebar li .inner { 146 | display: inline-block; 147 | padding-top: 7px; 148 | height: 24px; 149 | } 150 | 151 | .sidebar li a, .sidebar li .tree { 152 | height: 31px; 153 | } 154 | 155 | .depth-1 .inner { padding-left: 2px; } 156 | .depth-2 .inner { padding-left: 6px; } 157 | .depth-3 .inner { padding-left: 20px; } 158 | .depth-4 .inner { padding-left: 34px; } 159 | .depth-5 .inner { padding-left: 48px; } 160 | .depth-6 .inner { padding-left: 62px; } 161 | 162 | .sidebar li .tree { 163 | display: block; 164 | float: left; 165 | position: relative; 166 | top: -10px; 167 | margin: 0 4px 0 0; 168 | padding: 0; 169 | } 170 | 171 | .sidebar li.depth-1 .tree { 172 | display: none; 173 | } 174 | 175 | .sidebar li .tree .top, .sidebar li .tree .bottom { 176 | display: block; 177 | margin: 0; 178 | padding: 0; 179 | width: 7px; 180 | } 181 | 182 | .sidebar li .tree .top { 183 | border-left: 1px solid #aaa; 184 | border-bottom: 1px solid #aaa; 185 | height: 19px; 186 | } 187 | 188 | .sidebar li .tree .bottom { 189 | height: 22px; 190 | } 191 | 192 | .sidebar li.branch .tree .bottom { 193 | border-left: 1px solid #aaa; 194 | } 195 | 196 | #namespaces li.current a { 197 | border-left: 3px solid #a33; 198 | color: #a33; 199 | } 200 | 201 | #vars li.current a { 202 | border-left: 3px solid #33a; 203 | color: #33a; 204 | } 205 | 206 | #content h3 { 207 | font-size: 13pt; 208 | font-weight: bold; 209 | } 210 | 211 | .public h3 { 212 | margin: 0; 213 | float: left; 214 | } 215 | 216 | .usage { 217 | clear: both; 218 | } 219 | 220 | .public { 221 | margin: 0; 222 | border-top: 1px solid #e0e0e0; 223 | padding-top: 14px; 224 | padding-bottom: 6px; 225 | } 226 | 227 | .public:last-child { 228 | margin-bottom: 20%; 229 | } 230 | 231 | .members .public:last-child { 232 | margin-bottom: 0; 233 | } 234 | 235 | .members { 236 | margin: 15px 0; 237 | } 238 | 239 | .members h4 { 240 | color: #555; 241 | font-weight: normal; 242 | font-variant: small-caps; 243 | margin: 0 0 5px 0; 244 | } 245 | 246 | .members .inner { 247 | padding-top: 5px; 248 | padding-left: 12px; 249 | margin-top: 2px; 250 | margin-left: 7px; 251 | border-left: 1px solid #bbb; 252 | } 253 | 254 | #content .members .inner h3 { 255 | font-size: 12pt; 256 | } 257 | 258 | .members .public { 259 | border-top: none; 260 | margin-top: 0; 261 | padding-top: 6px; 262 | padding-bottom: 0; 263 | } 264 | 265 | .members .public:first-child { 266 | padding-top: 0; 267 | } 268 | 269 | h4.type, 270 | h4.dynamic, 271 | h4.added, 272 | h4.deprecated { 273 | float: left; 274 | margin: 3px 10px 15px 0; 275 | font-size: 15px; 276 | font-weight: bold; 277 | font-variant: small-caps; 278 | } 279 | 280 | .public h4.type, 281 | .public h4.dynamic, 282 | .public h4.added, 283 | .public h4.deprecated { 284 | font-size: 13px; 285 | font-weight: bold; 286 | margin: 3px 0 0 10px; 287 | } 288 | 289 | .members h4.type, 290 | .members h4.added, 291 | .members h4.deprecated { 292 | margin-top: 1px; 293 | } 294 | 295 | h4.type { 296 | color: #717171; 297 | } 298 | 299 | h4.dynamic { 300 | color: #9933aa; 301 | } 302 | 303 | h4.added { 304 | color: #508820; 305 | } 306 | 307 | h4.deprecated { 308 | color: #880000; 309 | } 310 | 311 | .namespace { 312 | margin-bottom: 30px; 313 | } 314 | 315 | .namespace:last-child { 316 | margin-bottom: 10%; 317 | } 318 | 319 | .index { 320 | padding: 0; 321 | font-size: 80%; 322 | margin: 15px 0; 323 | line-height: 16px; 324 | } 325 | 326 | .index * { 327 | display: inline; 328 | } 329 | 330 | .index p { 331 | padding-right: 3px; 332 | } 333 | 334 | .index li { 335 | padding-right: 5px; 336 | } 337 | 338 | .index ul { 339 | padding-left: 0; 340 | } 341 | 342 | .usage code { 343 | display: block; 344 | color: #008; 345 | margin: 2px 0; 346 | } 347 | 348 | .usage code:first-child { 349 | padding-top: 10px; 350 | } 351 | 352 | p { 353 | margin: 15px 0; 354 | } 355 | 356 | .public p:first-child, .public pre.plaintext { 357 | margin-top: 12px; 358 | } 359 | 360 | .doc { 361 | margin: 0 0 26px 0; 362 | clear: both; 363 | } 364 | 365 | .public .doc { 366 | margin: 0; 367 | } 368 | 369 | .namespace-index .doc { 370 | margin-bottom: 20px; 371 | } 372 | 373 | .namespace-index .namespace .doc { 374 | margin-bottom: 10px; 375 | } 376 | 377 | .markdown { 378 | line-height: 18px; 379 | font-size: 14px; 380 | } 381 | 382 | .doc, .public, .namespace .index { 383 | max-width: 680px; 384 | overflow-x: visible; 385 | } 386 | 387 | .markdown code, .src-link a { 388 | background: #f6f6f6; 389 | border: 1px solid #e4e4e4; 390 | border-radius: 2px; 391 | } 392 | 393 | .markdown pre { 394 | background: #f4f4f4; 395 | border: 1px solid #e0e0e0; 396 | border-radius: 2px; 397 | padding: 5px 10px; 398 | margin: 0 10px; 399 | } 400 | 401 | .markdown pre code { 402 | background: transparent; 403 | border: none; 404 | } 405 | 406 | .doc ul, .doc ol { 407 | padding-left: 30px; 408 | } 409 | 410 | .doc table { 411 | border-collapse: collapse; 412 | margin: 0 10px; 413 | } 414 | 415 | .doc table td, .doc table th { 416 | border: 1px solid #dddddd; 417 | padding: 4px 6px; 418 | } 419 | 420 | .doc table th { 421 | background: #f2f2f2; 422 | } 423 | 424 | .doc dl { 425 | margin: 0 10px 20px 10px; 426 | } 427 | 428 | .doc dl dt { 429 | font-weight: bold; 430 | margin: 0; 431 | padding: 3px 0; 432 | border-bottom: 1px solid #ddd; 433 | } 434 | 435 | .doc dl dd { 436 | padding: 5px 0; 437 | margin: 0 0 5px 10px; 438 | } 439 | 440 | .doc abbr { 441 | border-bottom: 1px dotted #333; 442 | font-variant: none 443 | cursor: help; 444 | } 445 | 446 | .src-link { 447 | margin-bottom: 15px; 448 | } 449 | 450 | .src-link a { 451 | font-size: 70%; 452 | padding: 1px 4px; 453 | text-decoration: none; 454 | color: #5555bb; 455 | } -------------------------------------------------------------------------------- /test/korma/test/db.clj: -------------------------------------------------------------------------------- 1 | (ns korma.test.db 2 | (:use [clojure.test :only [deftest is testing use-fixtures]] 3 | [korma.core :only [exec-raw]] 4 | [korma.db :only [connection-pool defdb get-connection h2 extract-options create-db 5 | msaccess mssql mysql odbc oracle postgres sqlite3 vertica 6 | firebird default-connection transaction]])) 7 | 8 | (defdb mem-db (h2 {:db "mem:test"})) 9 | 10 | (use-fixtures :once 11 | (fn [f] 12 | (default-connection mem-db) 13 | (f))) 14 | 15 | (def db-config-with-defaults 16 | {:classname "org.h2.Driver" 17 | :subprotocol "h2" 18 | :subname "mem:db_connectivity_test_db" 19 | :user "bob" 20 | :password "password"}) 21 | 22 | (def db-config-with-options-set 23 | {:classname "org.h2.Driver" 24 | :subprotocol "h2" 25 | :subname "mem:db_connectivity_test_db" 26 | :excess-timeout 99 27 | :idle-timeout 88 28 | :initial-pool-size 10 29 | :minimum-pool-size 5 30 | :maximum-pool-size 20 31 | :test-connection-on-checkout true 32 | :test-connection-query "SELECT 1" 33 | :useUnicode true 34 | :connectTimeout 1000}) 35 | 36 | (deftest can-extract-options-merging-delimiters 37 | (let [original (extract-options nil)] 38 | (is (= (assoc original :delimiters ["`" "`"]) 39 | (extract-options {:delimiters ["`" "`"]}))))) 40 | 41 | (deftest can-extract-options-merging-alias-delimiter 42 | (let [original (extract-options nil)] 43 | (is (= (assoc original :alias-delimiter " az ") 44 | (extract-options {:alias-delimiter " az "}))))) 45 | 46 | (deftest can-extract-options-merging-naming 47 | (let [custom-fn (fn [& args] args) 48 | original (extract-options nil)] 49 | (is (= (assoc original :naming {:fields custom-fn 50 | :keys custom-fn}) 51 | (extract-options {:naming {:fields custom-fn 52 | :keys custom-fn}}))))) 53 | 54 | (deftest connection-pooling-default-test 55 | (let [pool (connection-pool db-config-with-defaults) 56 | datasource (:datasource pool)] 57 | (is (= "org.h2.Driver" (.getDriverClass datasource))) 58 | (is (= "jdbc:h2:mem:db_connectivity_test_db" (.getJdbcUrl datasource))) 59 | (is (= "bob" (.getUser datasource))) 60 | (is (= "password" (.getPassword datasource))) 61 | 62 | (is (= 1800 (.getMaxIdleTimeExcessConnections datasource))) 63 | (is (= 10800 (.getMaxIdleTime datasource))) 64 | (is (= 3 (.getMinPoolSize datasource))) 65 | (is (= 15 (.getMaxPoolSize datasource))) 66 | (is (= false (.isTestConnectionOnCheckout datasource))))) 67 | 68 | (deftest connection-pooling-test 69 | (let [pool (connection-pool db-config-with-options-set) 70 | datasource (:datasource pool)] 71 | (is (= 99 (.getMaxIdleTimeExcessConnections datasource))) 72 | (is (= 88 (.getMaxIdleTime datasource))) 73 | (is (= 10 (.getInitialPoolSize datasource))) 74 | (is (= 5 (.getMinPoolSize datasource))) 75 | (is (= 20 (.getMaxPoolSize datasource))) 76 | (is (= (:test-connection-query db-config-with-options-set) (.getPreferredTestQuery datasource))) 77 | (is (= true (.isTestConnectionOnCheckout datasource))) 78 | (is (= {"useUnicode" "true" 79 | "connectTimeout" "1000"} 80 | (.getProperties datasource))))) 81 | 82 | (deftest spec-with-missing-keys-returns-itself 83 | (defdb valid {:datasource :from-app-server}) 84 | (is (= {:datasource :from-app-server} (get-connection valid)))) 85 | 86 | (deftest configuration-using-connection-uri 87 | (let [db (create-db {:connection-uri "jdbc:mysql://localhost:3306/db" 88 | :minimum-pool-size 66})] 89 | (testing "jdbc url is set correctly" 90 | (is (= "jdbc:mysql://localhost:3306/db" 91 | (-> db :pool deref :datasource .getJdbcUrl)))) 92 | (testing "driver class is set correctly" 93 | (is (= "com.mysql.jdbc.Driver" 94 | (-> db :pool deref :datasource .getDriverClass)))) 95 | (testing "connection pool options are set" 96 | (is (= 66 (-> db :pool deref :datasource .getMinPoolSize)))) 97 | (testing "options are set based on subprotocol from uri" 98 | (is (= [\` \`] (-> db :options :delimiters)))))) 99 | 100 | ;;; DB spec creation fns 101 | 102 | (deftest test-firebird 103 | (testing "firebirdsql - driver class" 104 | (is (= "org.firebirdsql.jdbc.FBDriver" 105 | (-> (create-db (firebird {})) :pool deref :datasource .getDriverClass)))) 106 | (testing "firebirdsql - defaults" 107 | (is (= {:subprotocol "firebirdsql" 108 | :subname "localhost/3050:" 109 | :encoding "UTF8"} 110 | (firebird {})))) 111 | (testing "firebirdsql - options selected" 112 | (is (= {:subprotocol "firebirdsql" 113 | :subname "host/port:db" 114 | :make-pool? false 115 | :encoding "NONE"} 116 | (firebird {:host "host" 117 | :port "port" 118 | :db "db" 119 | :encoding "NONE" 120 | :make-pool? false}))))) 121 | 122 | (deftest test-postgres 123 | (testing "postgres - driver class" 124 | (is (= "org.postgresql.Driver" 125 | (-> (create-db (postgres {})) :pool deref :datasource .getDriverClass)))) 126 | (testing "postgres - defaults" 127 | (is (= {:subprotocol "postgresql" 128 | :subname "//localhost:5432/"} 129 | (postgres {})))) 130 | (testing "postgres - options selected" 131 | (is (= {:subprotocol "postgresql" 132 | :subname "//host:port/db" 133 | :make-pool? false} 134 | (postgres {:host "host" 135 | :port "port" 136 | :db "db" 137 | :make-pool? false}))))) 138 | 139 | (deftest test-oracle 140 | (testing "oracle - driver class" 141 | (is (= "oracle.jdbc.driver.OracleDriver" 142 | (-> (create-db (oracle {})) :pool deref :datasource .getDriverClass)))) 143 | (testing "oracle - defaults" 144 | (is (= {:subprotocol "oracle:thin" 145 | :subname "@localhost:1521"} 146 | (oracle {})))) 147 | (testing "oracle - options selected" 148 | (is (= {:subprotocol "oracle:thin" 149 | :subname "@host:port" 150 | :make-pool? false} 151 | (oracle {:host "host" 152 | :port "port" 153 | :make-pool? false}))))) 154 | 155 | (deftest test-mysql 156 | (testing "mysql - driver class" 157 | (is (= "com.mysql.jdbc.Driver" 158 | (-> (create-db (mysql {})) :pool deref :datasource .getDriverClass)))) 159 | (testing "mysql - delimeters" 160 | (is (= [\` \`] 161 | (-> (create-db (mysql {})) :options :delimiters)))) 162 | (testing "mysql - defaults" 163 | (is (= {:subprotocol "mysql" 164 | :subname "//localhost:3306/"} 165 | (mysql {})))) 166 | (testing "mysql - options selected" 167 | (is (= {:subprotocol "mysql" 168 | :subname "//host:port/db" 169 | :make-pool? false} 170 | (mysql {:host "host" 171 | :port "port" 172 | :db "db" 173 | :make-pool? false}))))) 174 | 175 | (deftest test-vertica 176 | (testing "vertica - driver class" 177 | (is (= "com.vertica.jdbc.Driver" 178 | (-> (create-db (vertica {})) :pool deref :datasource .getDriverClass)))) 179 | (testing "vertica - defaults" 180 | (is (= {:subprotocol "vertica" 181 | :subname "//localhost:5433/"} 182 | (vertica {})))) 183 | (testing "vertica - options selected" 184 | (is (= {:subprotocol "vertica" 185 | :subname "//host:port/db" 186 | :make-pool? false} 187 | (vertica {:host "host" 188 | :port "port" 189 | :db "db" 190 | :make-pool? false}))))) 191 | 192 | (deftest test-mssql 193 | (testing "mssql - driver class" 194 | (is (= "com.microsoft.sqlserver.jdbc.SQLServerDriver" 195 | (-> (create-db (mssql {})) :pool deref :datasource .getDriverClass)))) 196 | (testing "mssql - defaults" 197 | (is (= {:subprotocol "sqlserver" 198 | :subname "//localhost:1433;database=;user=dbuser;password=dbpassword"} 199 | (mssql {})))) 200 | (testing "mssql - options selected" 201 | (is (= {:password "password" 202 | :user "user" 203 | :subprotocol "sqlserver" 204 | :subname "//host:port;database=db;user=user;password=password" 205 | :make-pool? false} 206 | (mssql {:host "host" 207 | :port "port" 208 | :db "db" 209 | :user "user" 210 | :password "password" 211 | :make-pool? false}))))) 212 | 213 | (deftest test-msaccess 214 | (testing "msaccess - driver class" 215 | (is (= "sun.jdbc.odbc.JdbcOdbcDriver" 216 | (-> (create-db (msaccess {:make-pool? true})) :pool deref :datasource .getDriverClass)))) 217 | (testing "msaccess - defaults" 218 | (is (= {:subprotocol "odbc" 219 | :subname "Driver={Microsoft Access Driver (*.mdb)};Dbq=" 220 | :make-pool? false} 221 | (msaccess {})))) 222 | (testing "msaccess - .mdb selected" 223 | (is (= {:subprotocol "odbc" 224 | :subname "Driver={Microsoft Access Driver (*.mdb)};Dbq=db.mdb" 225 | :make-pool? true} 226 | (msaccess {:db "db.mdb" :make-pool? true})))) 227 | (testing "msaccess - .accdb selected" 228 | (is (= {:subprotocol "odbc" 229 | :subname (str "Driver={Microsoft Access Driver (*.mdb, *.accdb)};" 230 | "Dbq=db.accdb") 231 | :make-pool? true} 232 | (msaccess {:db "db.accdb" :make-pool? true}))))) 233 | 234 | (deftest test-odbc 235 | (testing "odbc - driver class" 236 | (is (= "sun.jdbc.odbc.JdbcOdbcDriver" 237 | (-> (create-db (odbc {})) :pool deref :datasource .getDriverClass)))) 238 | (testing "odbc - defaults" 239 | (is (= {:subprotocol "odbc" 240 | :subname ""} 241 | (odbc {})))) 242 | (testing "odbc - options selected" 243 | (is (= {:subprotocol "odbc" 244 | :subname "MyDsn" 245 | :make-pool? false} 246 | (odbc {:dsn "MyDsn" :make-pool? false}))))) 247 | 248 | (deftest test-sqlite3 249 | (testing "sqlite3 - driver class" 250 | (is (= "org.sqlite.JDBC" 251 | (-> (create-db (sqlite3 {})) :pool deref :datasource .getDriverClass)))) 252 | (testing "sqlite3 - defaults" 253 | (is (= {:subprotocol "sqlite" 254 | :subname "sqlite.db"} 255 | (sqlite3 {})))) 256 | (testing "sqlite3 - options selected" 257 | (is (= {:subprotocol "sqlite" 258 | :subname "db" 259 | :make-pool? false} 260 | (sqlite3 {:db "db" :make-pool? false}))))) 261 | 262 | (deftest test-h2 263 | (testing "h2 - driver class" 264 | (is (= "org.h2.Driver" 265 | (-> (create-db (h2 {})) :pool deref :datasource .getDriverClass)))) 266 | (testing "h2 - defaults" 267 | (is (= {:subprotocol "h2" 268 | :subname "h2.db"} 269 | (h2 {})))) 270 | (testing "h2 - options selected" 271 | (is (= {:subprotocol "h2" 272 | :subname "db" 273 | :make-pool? false} 274 | (h2 {:db "db" :make-pool? false}))))) 275 | 276 | (deftest transaction-options 277 | (testing "if transaction macro respects isolation levels" 278 | (is (not= (transaction {:isolation :repeatable-read} (exec-raw "CALL LOCK_MODE()" :results)) 279 | (transaction {:isolation :read-committed} (exec-raw "CALL LOCK_MODE()" :results)))) 280 | (is (thrown? Exception (transaction {:isolation :no-such-isolation} (exec-raw "CALL LOCK_MODE()" :results)))))) 281 | -------------------------------------------------------------------------------- /src/korma/db.clj: -------------------------------------------------------------------------------- 1 | (ns korma.db 2 | "Functions for creating and managing database specifications." 3 | (:require [clojure.java.jdbc :as jdbc] 4 | [clojure.string])) 5 | 6 | (defonce _default (atom nil)) 7 | 8 | (def ^:dynamic *current-db* nil) 9 | (def ^:dynamic *current-conn* nil) 10 | 11 | (defn- ->delimiters [delimiters] 12 | (if delimiters 13 | (let [[begin end] delimiters 14 | end (or end begin)] 15 | [begin end]) 16 | ["\"" "\""])) 17 | 18 | (defn- ->naming [strategy] 19 | (merge {:fields identity 20 | :keys identity} strategy)) 21 | 22 | (defn- ->alias-delimiter [alias-delimiter] 23 | (or alias-delimiter " AS ")) 24 | 25 | (defn extract-options [{:keys [naming 26 | delimiters 27 | alias-delimiter]}] 28 | {:naming (->naming naming) 29 | :delimiters (->delimiters delimiters) 30 | :alias-delimiter (->alias-delimiter alias-delimiter)}) 31 | 32 | (defn default-connection 33 | "Set the database connection that Korma should use by default when no 34 | alternative is specified." 35 | [conn] 36 | (reset! _default conn)) 37 | 38 | (defn- as-properties [m] 39 | (let [p (java.util.Properties.)] 40 | (doseq [[k v] m] 41 | (.setProperty p (name k) (str v))) 42 | p)) 43 | 44 | (def c3p0-enabled? 45 | (try 46 | (import 'com.mchange.v2.c3p0.ComboPooledDataSource) 47 | true 48 | (catch Throwable _ false))) 49 | 50 | (defmacro resolve-new [class] 51 | (when-let [resolved (resolve class)] 52 | `(new ~resolved))) 53 | 54 | (defn connection-pool 55 | "Create a connection pool for the given database spec." 56 | [{:keys [connection-uri subprotocol subname classname 57 | excess-timeout idle-timeout 58 | initial-pool-size minimum-pool-size maximum-pool-size 59 | test-connection-query 60 | idle-connection-test-period 61 | test-connection-on-checkin 62 | test-connection-on-checkout] 63 | :or {excess-timeout (* 30 60) 64 | idle-timeout (* 3 60 60) 65 | initial-pool-size 3 66 | minimum-pool-size 3 67 | maximum-pool-size 15 68 | test-connection-query nil 69 | idle-connection-test-period 0 70 | test-connection-on-checkin false 71 | test-connection-on-checkout false} 72 | :as spec}] 73 | (when-not c3p0-enabled? 74 | (throw (Exception. "com.mchange.v2.c3p0.ComboPooledDataSource not found in class path."))) 75 | {:datasource (doto (resolve-new ComboPooledDataSource) 76 | (.setDriverClass classname) 77 | (.setJdbcUrl (or connection-uri (str "jdbc:" subprotocol ":" subname))) 78 | (.setProperties (as-properties (dissoc spec 79 | :make-pool? :classname :subprotocol :subname :connection-uri 80 | :naming :delimiters :alias-delimiter 81 | :excess-timeout :idle-timeout 82 | :initial-pool-size :minimum-pool-size :maximum-pool-size 83 | :test-connection-query 84 | :idle-connection-test-period 85 | :test-connection-on-checkin 86 | :test-connection-on-checkout))) 87 | (.setMaxIdleTimeExcessConnections excess-timeout) 88 | (.setMaxIdleTime idle-timeout) 89 | (.setInitialPoolSize initial-pool-size) 90 | (.setMinPoolSize minimum-pool-size) 91 | (.setMaxPoolSize maximum-pool-size) 92 | (.setIdleConnectionTestPeriod idle-connection-test-period) 93 | (.setTestConnectionOnCheckin test-connection-on-checkin) 94 | (.setTestConnectionOnCheckout test-connection-on-checkout) 95 | (.setPreferredTestQuery test-connection-query))}) 96 | 97 | (defn delay-pool 98 | "Return a delay for creating a connection pool for the given spec." 99 | [spec] 100 | (delay (connection-pool spec))) 101 | 102 | (defn get-connection 103 | "Get a connection from the potentially delayed connection object." 104 | [db] 105 | (let [db (or (:pool db) db)] 106 | (if-not db 107 | (throw (Exception. "No valid DB connection selected.")) 108 | (if (delay? db) 109 | @db 110 | db)))) 111 | 112 | (def ^:private subprotocol->classname {"firebirdsql" "org.firebirdsql.jdbc.FBDriver" 113 | "postgresql" "org.postgresql.Driver" 114 | "postgres" "org.postgresql.Driver" 115 | "oracle" "oracle.jdbc.driver.OracleDriver" 116 | "mysql" "com.mysql.jdbc.Driver" 117 | "vertica" "com.vertica.jdbc.Driver" 118 | "sqlserver" "com.microsoft.sqlserver.jdbc.SQLServerDriver" 119 | "odbc" "sun.jdbc.odbc.JdbcOdbcDriver" 120 | "sqlite" "org.sqlite.JDBC" 121 | "h2" "org.h2.Driver"}) 122 | 123 | (def ^:private subprotocol->options {"mysql" {:delimiters "`"}}) 124 | 125 | (defn- complete-spec [{:keys [connection-uri subprotocol] :as spec}] 126 | (if-let [uri-or-subprotocol (or connection-uri subprotocol)] 127 | (let [lookup-key (first (drop-while #{"jdbc"} (clojure.string/split uri-or-subprotocol #":")))] 128 | (merge 129 | {:classname (subprotocol->classname lookup-key) 130 | :make-pool? true} 131 | (subprotocol->options lookup-key) 132 | spec)) 133 | spec)) 134 | 135 | (defn create-db 136 | "Create a db connection object manually instead of using defdb. This is often 137 | useful for creating connections dynamically, and probably should be followed 138 | up with: 139 | 140 | (default-connection my-new-conn) 141 | 142 | If the spec includes `:make-pool? true` makes a connection pool from the spec." 143 | [spec] 144 | (let [spec (complete-spec spec)] 145 | {:pool (if (:make-pool? spec) 146 | (delay-pool spec) 147 | spec) 148 | :options (extract-options spec)})) 149 | 150 | (defmacro defdb 151 | "Define a database specification. The last evaluated defdb will be used by 152 | default for all queries where no database is specified by the entity." 153 | [db-name spec] 154 | `(let [spec# ~spec] 155 | (defonce ~db-name (create-db spec#)) 156 | (default-connection ~db-name))) 157 | 158 | (defn firebird 159 | "Create a database specification for a FirebirdSQL database. Opts should include 160 | keys for :db, :user, :password. You can also optionally set host, port and make-pool?" 161 | [{:keys [host port db] 162 | :or {host "localhost", port 3050, db ""} 163 | :as opts}] 164 | (merge {:subprotocol "firebirdsql" 165 | :subname (str host "/" port ":" db) 166 | :encoding "UTF8"} 167 | (dissoc opts :host :port :db))) 168 | 169 | (defn postgres 170 | "Create a database specification for a postgres database. Opts should include 171 | keys for :db, :user, and :password. You can also optionally set host and 172 | port." 173 | [{:keys [host port db] 174 | :or {host "localhost", port 5432, db ""} 175 | :as opts}] 176 | (merge {:subprotocol "postgresql" 177 | :subname (str "//" host ":" port "/" db)} 178 | (dissoc opts :host :port :db))) 179 | 180 | (defn oracle 181 | "Create a database specification for an Oracle database. Opts should include keys 182 | for :user and :password. You can also optionally set host and port." 183 | [{:keys [host port] 184 | :or {host "localhost", port 1521} 185 | :as opts}] 186 | (merge {:subprotocol "oracle:thin" 187 | :subname (str "@" host ":" port)} 188 | (dissoc opts :host :port))) 189 | 190 | (defn mysql 191 | "Create a database specification for a mysql database. Opts should include keys 192 | for :db, :user, and :password. You can also optionally set host and port. 193 | Delimiters are automatically set to \"`\"." 194 | [{:keys [host port db] 195 | :or {host "localhost", port 3306, db ""} 196 | :as opts}] 197 | (merge {:subprotocol "mysql" 198 | :subname (str "//" host ":" port "/" db)} 199 | (dissoc opts :host :port :db))) 200 | 201 | (defn vertica 202 | "Create a database specification for a vertica database. Opts should include keys 203 | for :db, :user, and :password. You can also optionally set host and port. 204 | Delimiters are automatically set to \"`\"." 205 | [{:keys [host port db] 206 | :or {host "localhost", port 5433, db ""} 207 | :as opts}] 208 | (merge {:subprotocol "vertica" 209 | :subname (str "//" host ":" port "/" db)} 210 | (dissoc opts :host :port :db))) 211 | 212 | (defn mssql 213 | "Create a database specification for a mssql database. Opts should include keys 214 | for :db, :user, and :password. You can also optionally set host and port." 215 | [{:keys [user password db host port] 216 | :or {user "dbuser", password "dbpassword", db "", host "localhost", port 1433} 217 | :as opts}] 218 | (merge {:subprotocol "sqlserver" 219 | :subname (str "//" host ":" port ";database=" db ";user=" user ";password=" password)} 220 | (dissoc opts :host :port :db))) 221 | 222 | (defn msaccess 223 | "Create a database specification for a Microsoft Access database. Opts 224 | should include keys for :db and optionally :make-pool?." 225 | [{:keys [^String db] 226 | :or {db ""} 227 | :as opts}] 228 | (merge {:subprotocol "odbc" 229 | :subname (str "Driver={Microsoft Access Driver (*.mdb" 230 | (when (.endsWith db ".accdb") ", *.accdb") 231 | ")};Dbq=" db) 232 | :make-pool? false} 233 | (dissoc opts :db))) 234 | 235 | (defn odbc 236 | "Create a database specification for an ODBC DSN. Opts 237 | should include keys for :dsn and optionally :make-pool?." 238 | [{:keys [dsn] 239 | :or {dsn ""} 240 | :as opts}] 241 | (merge {:subprotocol "odbc" 242 | :subname dsn} 243 | (dissoc opts :dsn))) 244 | 245 | (defn sqlite3 246 | "Create a database specification for a SQLite3 database. Opts should include a 247 | key for :db which is the path to the database file." 248 | [{:keys [db] 249 | :or {db "sqlite.db"} 250 | :as opts}] 251 | (merge {:subprotocol "sqlite" 252 | :subname db} 253 | (dissoc opts :db))) 254 | 255 | (defn h2 256 | "Create a database specification for a h2 database. Opts should include a key 257 | for :db which is the path to the database file." 258 | [{:keys [db] 259 | :or {db "h2.db"} 260 | :as opts}] 261 | (merge {:subprotocol "h2" 262 | :subname db} 263 | (dissoc opts :db))) 264 | 265 | (defmacro transaction 266 | "Execute all queries within the body in a single transaction. 267 | Optionally takes as a first argument a map to specify the :isolation and :read-only? properties of the transaction." 268 | {:arglists '([body] [options & body])} 269 | [& body] 270 | (let [options (first body) 271 | check-options (and (-> body rest seq) 272 | (map? options)) 273 | {:keys [isolation read-only?]} (when check-options options) 274 | body (if check-options (rest body) body)] 275 | `(binding [*current-db* (or *current-db* @_default)] 276 | (jdbc/with-db-transaction [conn# (or *current-conn* (get-connection *current-db*)) {:isolation ~isolation :read-only? ~read-only?}] 277 | (binding [*current-conn* conn#] 278 | ~@body))))) 279 | 280 | (defn rollback 281 | "Tell this current transaction to rollback." 282 | [] 283 | (jdbc/db-set-rollback-only! *current-conn*)) 284 | 285 | (defn is-rollback? 286 | "Returns true if the current transaction will be rolled back" 287 | [] 288 | (jdbc/db-is-rollback-only *current-conn*)) 289 | 290 | (defn- exec-sql [{:keys [results sql-str params]}] 291 | (let [keys (get-in *current-db* [:options :naming :keys]) 292 | sql-params (apply vector sql-str params)] 293 | (case results 294 | :results (jdbc/query *current-conn* sql-params {:identifiers keys}) 295 | :keys (jdbc/db-do-prepared-return-keys *current-conn* sql-params) 296 | (jdbc/db-do-prepared *current-conn* sql-params)))) 297 | 298 | (defmacro with-db 299 | "Execute all queries within the body using the given db spec" 300 | [db & body] 301 | `(jdbc/with-db-connection [conn# (korma.db/get-connection ~db)] 302 | (binding [*current-db* ~db 303 | *current-conn* conn#] 304 | ~@body))) 305 | 306 | (defn do-query [{:keys [db] :as query}] 307 | (if *current-conn* 308 | (exec-sql query) 309 | (with-db (or db @_default) 310 | (exec-sql query)))) 311 | -------------------------------------------------------------------------------- /doc/korma.db.html: -------------------------------------------------------------------------------- 1 | 2 | korma.db documentation

korma.db

Functions for creating and managing database specifications.
 3 | 

*current-conn*

dynamic

*current-db*

dynamic

_default

c3p0-enabled?

connection-pool

(connection-pool {:keys [connection-uri subprotocol subname classname excess-timeout idle-timeout initial-pool-size minimum-pool-size maximum-pool-size test-connection-query idle-connection-test-period test-connection-on-checkin test-connection-on-checkout], :or {maximum-pool-size 15, idle-timeout (* 3 60 60), excess-timeout (* 30 60), idle-connection-test-period 0, test-connection-query nil, test-connection-on-checkin false, test-connection-on-checkout false, initial-pool-size 3, minimum-pool-size 3}, :as spec})
Create a connection pool for the given database spec.
 4 | 

create-db

(create-db spec)
Create a db connection object manually instead of using defdb. This is often
 5 | useful for creating connections dynamically, and probably should be followed
 6 | up with:
 7 | 
 8 | (default-connection my-new-conn)
 9 | 
10 | If the spec includes `:make-pool? true` makes a connection pool from the spec.

default-connection

(default-connection conn)
Set the database connection that Korma should use by default when no
11 | alternative is specified.

defdb

macro

(defdb db-name spec)
Define a database specification. The last evaluated defdb will be used by
12 | default for all queries where no database is specified by the entity.

delay-pool

(delay-pool spec)
Return a delay for creating a connection pool for the given spec.
13 | 

do-query

(do-query {:keys [db], :as query})

extract-options

(extract-options {:keys [naming delimiters alias-delimiter]})

firebird

(firebird {:keys [host port db], :or {host "localhost", port 3050, db ""}, :as opts})
Create a database specification for a FirebirdSQL database. Opts should include
14 | keys for :db, :user, :password. You can also optionally set host, port and make-pool?

get-connection

(get-connection db)
Get a connection from the potentially delayed connection object.
15 | 

h2

(h2 {:keys [db], :or {db "h2.db"}, :as opts})
Create a database specification for a h2 database. Opts should include a key
16 | for :db which is the path to the database file.

is-rollback?

(is-rollback?)
Returns true if the current transaction will be rolled back
17 | 

msaccess

(msaccess {:keys [db], :or {db ""}, :as opts})
Create a database specification for a Microsoft Access database. Opts
18 | should include keys for :db and optionally :make-pool?.

mssql

(mssql {:keys [user password db host port], :or {user "dbuser", password "dbpassword", db "", host "localhost", port 1433}, :as opts})
Create a database specification for a mssql database. Opts should include keys
19 | for :db, :user, and :password. You can also optionally set host and port.

mysql

(mysql {:keys [host port db], :or {host "localhost", port 3306, db ""}, :as opts})
Create a database specification for a mysql database. Opts should include keys
20 | for :db, :user, and :password. You can also optionally set host and port.
21 | Delimiters are automatically set to "`".

odbc

(odbc {:keys [dsn], :or {dsn ""}, :as opts})
Create a database specification for an ODBC DSN. Opts
22 | should include keys for :dsn and optionally :make-pool?.

oracle

(oracle {:keys [host port], :or {host "localhost", port 1521}, :as opts})
Create a database specification for an Oracle database. Opts should include keys
23 | for :user and :password. You can also optionally set host and port.

postgres

(postgres {:keys [host port db], :or {host "localhost", port 5432, db ""}, :as opts})
Create a database specification for a postgres database. Opts should include
24 | keys for :db, :user, and :password. You can also optionally set host and
25 | port.

resolve-new

macro

(resolve-new class)

rollback

(rollback)
Tell this current transaction to rollback.
26 | 

sqlite3

(sqlite3 {:keys [db], :or {db "sqlite.db"}, :as opts})
Create a database specification for a SQLite3 database. Opts should include a
27 | key for :db which is the path to the database file.

transaction

macro

(transaction body)(transaction options & body)
Execute all queries within the body in a single transaction.
28 | Optionally takes as a first argument a map to specify the :isolation and :read-only? properties of the transaction.

vertica

(vertica {:keys [host port db], :or {host "localhost", port 5433, db ""}, :as opts})
Create a database specification for a vertica database. Opts should include keys
29 | for :db, :user, and :password. You can also optionally set host and port.
30 | Delimiters are automatically set to "`".

with-db

macro

(with-db db & body)
Execute all queries within the body using the given db spec
31 | 
-------------------------------------------------------------------------------- /src/korma/sql/engine.clj: -------------------------------------------------------------------------------- 1 | (ns korma.sql.engine 2 | (:require [clojure.string :as string] 3 | [clojure.walk :as walk] 4 | [korma.db :as db] 5 | [korma.sql.utils :as utils])) 6 | 7 | ;;***************************************************** 8 | ;; dynamic vars 9 | ;;***************************************************** 10 | 11 | (def ^{:dynamic true} *bound-table* nil) 12 | (def ^{:dynamic true} *bound-aliases* #{}) 13 | (def ^{:dynamic true} *bound-params* nil) 14 | (def ^{:dynamic true} *bound-options* nil) 15 | 16 | ;;***************************************************** 17 | ;; delimiters 18 | ;;***************************************************** 19 | 20 | (defn delimit-str [s] 21 | (let [{:keys [naming delimiters]} *bound-options* 22 | [begin end] delimiters 23 | ->field (:fields naming)] 24 | (str begin (->field s) end))) 25 | 26 | ;;***************************************************** 27 | ;; Str utils 28 | ;;***************************************************** 29 | 30 | (declare pred-map str-value) 31 | 32 | (defn str-values [vs] 33 | (map str-value vs)) 34 | 35 | (defn comma-values [vs] 36 | (utils/comma-separated (str-values vs))) 37 | 38 | (defn wrap-values [vs] 39 | (if (seq vs) 40 | (utils/wrap (comma-values vs)) 41 | "(NULL)")) 42 | 43 | (defn map-val [v] 44 | (let [func (utils/func? v) 45 | generated (utils/generated? v) 46 | args (utils/args? v) 47 | sub (utils/sub-query? v) 48 | pred (utils/pred? v)] 49 | (cond 50 | generated generated 51 | pred (apply pred args) 52 | func (let [vs (comma-values args)] 53 | (format func vs)) 54 | sub (do 55 | (swap! *bound-params* utils/vconcat (:params sub)) 56 | (utils/wrap (:sql-str sub))) 57 | :else (pred-map v)))) 58 | 59 | (defn table-alias [{:keys [table alias]}] 60 | (or alias table)) 61 | 62 | (defn table-identifier [table-name] 63 | (let [parts (string/split table-name #"\.")] 64 | (if (next parts) 65 | (string/join "." (map delimit-str parts)) 66 | (delimit-str table-name)))) 67 | 68 | (defn ^String field-identifier [field] 69 | (cond 70 | (map? field) (map-val field) 71 | (string? field) field 72 | (= "*" (name field)) "*" 73 | :else (let [field-name (name field) 74 | parts (string/split field-name #"\.")] 75 | (if-not (next parts) 76 | (delimit-str field-name) 77 | (string/join "." (map delimit-str parts)))))) 78 | 79 | (defn prefix [ent field] 80 | (let [field-name (field-identifier field) 81 | not-already-prefixed? (and (keyword? field) 82 | (not (*bound-aliases* field)) 83 | (= -1 (.indexOf field-name ".")))] 84 | (if not-already-prefixed? 85 | (let [table (if (string? ent) 86 | ent 87 | (table-alias ent))] 88 | (str (table-identifier table) "." field-name)) 89 | field-name))) 90 | 91 | (defn try-prefix [v] 92 | (if (and (keyword? v) 93 | *bound-table*) 94 | (utils/generated (prefix *bound-table* v)) 95 | v)) 96 | 97 | (defn alias-clause [alias] 98 | (when alias 99 | (str (:alias-delimiter *bound-options*) 100 | (delimit-str (name alias))))) 101 | 102 | (defn field-str [v] 103 | (let [[fname alias] (if (vector? v) 104 | v 105 | [v nil]) 106 | fname (cond 107 | (map? fname) (map-val fname) 108 | *bound-table* (prefix *bound-table* fname) 109 | :else (field-identifier fname)) 110 | alias-str (alias-clause alias)] 111 | (str fname alias-str))) 112 | 113 | (defn coll-str [v] 114 | (wrap-values v)) 115 | 116 | (defn table-str [v] 117 | (if (utils/special-map? v) 118 | (map-val v) 119 | (let [tstr (cond 120 | (string? v) v 121 | (map? v) (:table v) 122 | :else (name v))] 123 | (table-identifier tstr)))) 124 | 125 | (defn parameterize [v] 126 | (when *bound-params* 127 | (swap! *bound-params* conj v)) 128 | "?") 129 | 130 | (defn str-value [v] 131 | (cond 132 | (map? v) (map-val v) 133 | (keyword? v) (field-str v) 134 | (nil? v) "NULL" 135 | (coll? v) (coll-str v) 136 | :else (parameterize v))) 137 | 138 | (defn not-nil? [& vs] 139 | (every? #(not (nil? %)) vs)) 140 | 141 | ;;***************************************************** 142 | ;; Bindings 143 | ;;***************************************************** 144 | 145 | (defmacro bind-query [query & body] 146 | `(binding [*bound-table* (if (= :select (:type ~query)) 147 | (table-alias ~query) 148 | (:table ~query)) 149 | *bound-aliases* (or (:aliases ~query) #{}) 150 | *bound-options* (or (get-in ~query [:db :options]) 151 | (:options db/*current-db*) 152 | (:options @db/_default))] 153 | ~@body)) 154 | 155 | ;;***************************************************** 156 | ;; Predicates 157 | ;;***************************************************** 158 | 159 | (def predicates 160 | (let [predicates-s {'like 'korma.sql.fns/pred-like 161 | 'ilike 'korma.sql.fns/pred-ilike 162 | 'and 'korma.sql.fns/pred-and 163 | 'or 'korma.sql.fns/pred-or 164 | 'not 'korma.sql.fns/pred-not 165 | 'in 'korma.sql.fns/pred-in 166 | 'exists 'korma.sql.fns/pred-exists 167 | 'not-in 'korma.sql.fns/pred-not-in 168 | 'between 'korma.sql.fns/pred-between 169 | '> 'korma.sql.fns/pred-> 170 | '< 'korma.sql.fns/pred-< 171 | '>= 'korma.sql.fns/pred->= 172 | '<= 'korma.sql.fns/pred-<= 173 | 'not= 'korma.sql.fns/pred-not= 174 | '= 'korma.sql.fns/pred-=} 175 | predicates-k (into {} (map (fn [[k v]] {(keyword k) v}) predicates-s))] 176 | (merge predicates-s predicates-k))) 177 | 178 | (defn do-infix [k op v] 179 | (string/join " " [(str-value k) op (str-value v)])) 180 | 181 | (defn do-group [op vs] 182 | (utils/wrap (string/join op (str-values vs)))) 183 | 184 | (defn do-wrapper [op v] 185 | (str op (utils/wrap (str-value v)))) 186 | 187 | (defn do-trinary [k op v1 sep v2] 188 | (utils/wrap (string/join " " [(str-value k) op (str-value v1) sep (str-value v2)]))) 189 | 190 | (defn trinary [k op v1 sep v2] 191 | (utils/pred do-trinary [(try-prefix k) op (try-prefix v1) sep (try-prefix v2)])) 192 | 193 | (defn infix [k op v] 194 | (utils/pred do-infix [(try-prefix k) op (try-prefix v)])) 195 | 196 | (defn group-with [op vs] 197 | (utils/pred do-group [op (doall (map pred-map vs))])) 198 | 199 | (defn wrapper [op v] 200 | (utils/pred do-wrapper [op v])) 201 | 202 | (defn pred-and [& args] 203 | (group-with " AND " args)) 204 | 205 | (defn pred-= [k v] 206 | (cond 207 | (not-nil? k v) (infix k "=" v) 208 | (not-nil? k) (infix k "IS" v) 209 | (not-nil? v) (infix v "IS" k))) 210 | 211 | (defn set= [[k v]] 212 | (map-val (infix k "=" v))) 213 | 214 | (defn pred-vec [[k v]] 215 | (let [[func value] (if (vector? v) 216 | v 217 | [pred-= v]) 218 | pred? (predicates func) 219 | func (if pred? 220 | (resolve pred?) 221 | func)] 222 | (func k value))) 223 | 224 | (defn pred-map [m] 225 | (if (and (map? m) 226 | (not (utils/special-map? m))) 227 | (apply pred-and (doall (map pred-vec (sort-by (comp str key) m)))) 228 | m)) 229 | 230 | (defn parse-where [form] 231 | (if (string? form) 232 | form 233 | (walk/postwalk-replace predicates form))) 234 | 235 | ;;***************************************************** 236 | ;; Aggregates 237 | ;;***************************************************** 238 | 239 | (def aggregates {'count 'korma.sql.fns/agg-count 240 | 'min 'korma.sql.fns/agg-min 241 | 'max 'korma.sql.fns/agg-max 242 | 'first 'korma.sql.fns/agg-first 243 | 'last 'korma.sql.fns/agg-last 244 | 'avg 'korma.sql.fns/agg-avg 245 | 'stdev 'korma.sql.fns/agg-stdev 246 | 'sum 'korma.sql.fns/agg-sum}) 247 | 248 | (defn sql-func [op & vs] 249 | (utils/func (str (string/upper-case op) "(%s)") (map try-prefix vs))) 250 | 251 | (defn parse-aggregate [form] 252 | (if (string? form) 253 | form 254 | (walk/postwalk-replace aggregates form))) 255 | 256 | ;;***************************************************** 257 | ;; Clauses 258 | ;;***************************************************** 259 | 260 | (defn from-table [v & [already-aliased?]] 261 | (cond 262 | (string? v) (table-str v) 263 | (vector? v) (let [[table alias] v] 264 | (str (from-table table :aliased) (alias-clause alias))) 265 | (map? v) (if (:table v) 266 | (let [{:keys [table alias]} v] 267 | (str (table-str table) (when-not already-aliased? (alias-clause alias)))) 268 | (map-val v)) 269 | :else (table-str v))) 270 | 271 | (defn join-clause [join-type table on-clause] 272 | (let [join-type (string/upper-case (name join-type)) 273 | table (from-table table) 274 | join (str " " join-type " JOIN " table " ON ")] 275 | (str join (str-value on-clause)))) 276 | 277 | (defn insert-values-clause [ks vs] 278 | (for [v vs] 279 | (wrap-values (map #(get v %) ks)))) 280 | 281 | ;;***************************************************** 282 | ;; Query types 283 | ;;***************************************************** 284 | 285 | (defn- make-comment [query] 286 | (if (seq (:comments query)) 287 | (->> query 288 | :comments 289 | (map (fn [cmt] 290 | (str "-- " 291 | (string/join "\n-- " (string/split cmt #"\n")) 292 | "\n"))) 293 | (apply str)) 294 | "")) 295 | 296 | (defn sql-select [query] 297 | (let [clauses (map field-str (:fields query)) 298 | modifiers-clause (when (seq (:modifiers query)) 299 | (str (reduce str (:modifiers query)) " ")) 300 | clauses-str (utils/comma-separated clauses) 301 | neue-sql (str 302 | (make-comment query) 303 | "SELECT " modifiers-clause clauses-str)] 304 | (assoc query :sql-str neue-sql))) 305 | 306 | (defn sql-update [query] 307 | (let [neue-sql (str 308 | (make-comment query) 309 | "UPDATE " (table-str query))] 310 | (assoc query :sql-str neue-sql))) 311 | 312 | (defn sql-delete [query] 313 | (let [neue-sql (str 314 | (make-comment query) 315 | "DELETE FROM " (table-str query))] 316 | (assoc query :sql-str neue-sql))) 317 | 318 | (def noop-query "DO 0") 319 | 320 | (defn sql-insert [query] 321 | (let [ins-keys (sort (distinct (mapcat keys (:values query)))) 322 | keys-clause (utils/comma-separated (map field-identifier ins-keys)) 323 | ins-values (insert-values-clause ins-keys (:values query)) 324 | values-clause (utils/comma-separated ins-values) 325 | neue-sql (if-not (empty? ins-keys) 326 | (str "INSERT INTO " (table-str query) " " (utils/wrap keys-clause) " VALUES " values-clause) 327 | noop-query)] 328 | (assoc query :sql-str (str 329 | (make-comment query) 330 | neue-sql)))) 331 | 332 | ;;***************************************************** 333 | ;; Sql parts 334 | ;;***************************************************** 335 | 336 | (defn sql-set [query] 337 | (let [fields (for [[k v] (sort (:set-fields query))] 338 | [(utils/generated (field-identifier k)) (utils/generated (str-value v))]) 339 | clauses (map set= fields) 340 | clauses-str (utils/comma-separated clauses) 341 | neue-sql (str " SET " clauses-str)] 342 | (update-in query [:sql-str] str neue-sql))) 343 | 344 | (defn sql-joins [query] 345 | (let [clauses (for [[type table clause] (:joins query)] 346 | (join-clause type table clause)) 347 | tables (utils/comma-separated (map from-table (:from query))) 348 | clauses-str (utils/left-assoc (cons (str tables (first clauses)) 349 | (rest clauses)))] 350 | (update-in query [:sql-str] str " FROM " clauses-str))) 351 | 352 | (defn- sql-where-or-having [where-or-having-kw where-or-having-str query] 353 | (if (empty? (get query where-or-having-kw)) 354 | query 355 | (let [clauses (map #(if (map? %) (map-val %) %) 356 | (get query where-or-having-kw)) 357 | clauses-str (string/join " AND " clauses) 358 | neue-sql (str where-or-having-str clauses-str)] 359 | (if (= "()" clauses-str) 360 | query 361 | (update-in query [:sql-str] str neue-sql))))) 362 | 363 | (def sql-where (partial sql-where-or-having :where " WHERE ")) 364 | (def sql-having (partial sql-where-or-having :having " HAVING ")) 365 | 366 | (defn sql-order [query] 367 | (if (seq (:order query)) 368 | (let [clauses (for [[k dir] (:order query)] 369 | (str (str-value k) " " (string/upper-case (name dir)))) 370 | clauses-str (utils/comma-separated clauses) 371 | neue-sql (str " ORDER BY " clauses-str)] 372 | (update-in query [:sql-str] str neue-sql)) 373 | query)) 374 | 375 | (defn sql-group [query] 376 | (if (seq (:group query)) 377 | (let [clauses (map field-str (:group query)) 378 | clauses-str (utils/comma-separated clauses) 379 | neue-sql (str " GROUP BY " clauses-str)] 380 | (update-in query [:sql-str] str neue-sql)) 381 | query)) 382 | 383 | (defn sql-limit-offset [{:keys [limit offset] :as query}] 384 | (let [limit-sql (when limit 385 | (str " LIMIT " limit)) 386 | offset-sql (when offset 387 | (str " OFFSET " offset))] 388 | (update-in query [:sql-str] str limit-sql offset-sql))) 389 | 390 | ;;***************************************************** 391 | ;; Combination Queries 392 | ;;***************************************************** 393 | 394 | (defn- sql-combination-query [type query] 395 | (let [sub-query-sqls (map map-val (:queries query)) 396 | neue-sql (str 397 | (make-comment query) 398 | (string/join (str " " type " ") sub-query-sqls))] 399 | (assoc query :sql-str neue-sql))) 400 | 401 | (def sql-union (partial sql-combination-query "UNION")) 402 | (def sql-union-all (partial sql-combination-query "UNION ALL")) 403 | (def sql-intersect (partial sql-combination-query "INTERSECT")) 404 | 405 | ;;***************************************************** 406 | ;; To sql 407 | ;;***************************************************** 408 | 409 | (defmacro bind-params [& body] 410 | `(binding [*bound-params* (atom [])] 411 | (let [query# (do ~@body)] 412 | (update-in query# [:params] utils/vconcat @*bound-params*)))) 413 | 414 | (defn ->sql [query] 415 | (bind-params 416 | (case (:type query) 417 | :union (-> query sql-union sql-order) 418 | :union-all (-> query sql-union-all sql-order) 419 | :intersect (-> query sql-intersect sql-order) 420 | :select (-> query 421 | sql-select 422 | sql-joins 423 | sql-where 424 | sql-group 425 | sql-having 426 | sql-order 427 | sql-limit-offset) 428 | :update (-> query 429 | sql-update 430 | sql-set 431 | sql-where) 432 | :delete (-> query 433 | sql-delete 434 | sql-where) 435 | :insert (-> query 436 | sql-insert)))) 437 | -------------------------------------------------------------------------------- /src/korma/core.clj: -------------------------------------------------------------------------------- 1 | (ns korma.core 2 | "Core querying and entity functions" 3 | (:refer-clojure :exclude [update]) 4 | (:require [clojure.set :as set] 5 | [clojure.string :as string] 6 | [korma.db :as db] 7 | [korma.sql.engine :as eng] 8 | [korma.sql.fns :as sfns] 9 | [korma.sql.utils :as utils]) 10 | (:use [korma.sql.engine :only [bind-query]])) 11 | 12 | (def ^{:dynamic true} *exec-mode* false) 13 | (declare get-rel) 14 | 15 | ;;***************************************************** 16 | ;; Query types 17 | ;;***************************************************** 18 | 19 | (defn empty-query [ent] 20 | (let [ent (if (keyword? ent) 21 | (name ent) 22 | ent) 23 | [ent table alias db] (if (string? ent) 24 | [{:table ent} ent nil nil] 25 | [ent (:table ent) (:alias ent) (:db ent)])] 26 | {:ent ent 27 | :table table 28 | :db db 29 | :alias alias})) 30 | 31 | (defmacro ^{:private true} make-query [ent m] 32 | `(let [ent# ~ent] 33 | (if (:type ent#) 34 | ent# 35 | (let [~'this-query (empty-query ent#)] 36 | (merge ~'this-query ~m))))) 37 | 38 | (defn select* 39 | "Create a select query with fields provided in Ent. If fields are not provided, 40 | create an empty select query. Ent can either be an entity defined by defentity, 41 | or a string of the table name" 42 | [ent] 43 | (let [default-fields (not-empty (:fields ent))] 44 | (make-query ent {:type :select 45 | :fields (or default-fields [::*]) 46 | :from [(:ent this-query)] 47 | :modifiers [] 48 | :joins [] 49 | :where [] 50 | :order [] 51 | :aliases #{} 52 | :group [] 53 | :results :results}))) 54 | 55 | (defn update* 56 | "Create an empty update query. Ent can either be an entity defined by defentity, 57 | or a string of the table name." 58 | [ent] 59 | (make-query ent {:type :update 60 | :fields {} 61 | :where [] 62 | :post-queries [first]})) 63 | 64 | (defn delete* 65 | "Create an empty delete query. Ent can either be an entity defined by defentity, 66 | or a string of the table name" 67 | [ent] 68 | (make-query ent {:type :delete 69 | :where [] 70 | :post-queries [first]})) 71 | 72 | (defn insert* 73 | "Create an empty insert query. Ent can either be an entity defined by defentity, 74 | or a string of the table name" 75 | [ent] 76 | (make-query ent {:type :insert 77 | :values [] 78 | :results :keys})) 79 | 80 | (defn union* 81 | "Create an empty union query." 82 | [] 83 | {:type :union 84 | :queries [] 85 | :order [] 86 | :results :results}) 87 | 88 | (defn union-all* 89 | "Create an empty union-all query." 90 | [] 91 | {:type :union-all 92 | :queries [] 93 | :order [] 94 | :results :results}) 95 | 96 | (defn intersect* 97 | "Create an empty intersect query." 98 | [] 99 | {:type :intersect 100 | :queries [] 101 | :order [] 102 | :results :results}) 103 | 104 | ;;***************************************************** 105 | ;; Query macros 106 | ;;***************************************************** 107 | 108 | (defn- make-query-then-exec [query-fn-var body & args] 109 | `(let [query# (-> (~query-fn-var ~@args) 110 | ~@body)] 111 | (exec query#))) 112 | 113 | (defmacro select 114 | "Creates a select query, applies any modifying functions in the body and then 115 | executes it. `ent` is either a string or an entity created by defentity. 116 | 117 | ex: (select user 118 | (fields :name :email) 119 | (where {:id 2}))" 120 | [ent & body] 121 | (make-query-then-exec #'select* body ent)) 122 | 123 | (defmacro update 124 | "Creates an update query, applies any modifying functions in the body and then 125 | executes it. `ent` is either a string or an entity created by defentity. 126 | Returns number of updated rows as provided by the JDBC driver. 127 | 128 | ex: (update user 129 | (set-fields {:name \"chris\"}) 130 | (where {:id 4}))" 131 | [ent & body] 132 | (make-query-then-exec #'update* body ent)) 133 | 134 | (defmacro delete 135 | "Creates a delete query, applies any modifying functions in the body and then 136 | executes it. `ent` is either a string or an entity created by defentity. 137 | Returns number of deleted rows as provided by the JDBC driver. 138 | 139 | ex: (delete user 140 | (where {:id 7}))" 141 | [ent & body] 142 | (make-query-then-exec #'delete* body ent)) 143 | 144 | (defmacro insert 145 | "Creates an insert query, applies any modifying functions in the body 146 | and then executes it. `ent` is either a string or an entity created by 147 | defentity. The return value is the last inserted item, but its 148 | representation is dependent on the database driver used 149 | (e.g. postgresql returns the full row as a hash, 150 | MySQL returns a {:generated_key } hash, 151 | and MSSQL returns a {:generated_keys } hash). 152 | 153 | ex: (insert user 154 | (values [{:name \"chris\"} {:name \"john\"}]))" 155 | [ent & body] 156 | (make-query-then-exec #'insert* body ent)) 157 | 158 | (defmacro union 159 | "Creates a union query, applies any modifying functions in the body and then 160 | executes it. 161 | 162 | ex: (union 163 | (queries (subselect user 164 | (where {:id 7})) 165 | (subselect user-backup 166 | (where {:id 7}))) 167 | (order :name))" 168 | [& body] 169 | (make-query-then-exec #'union* body)) 170 | 171 | (defmacro union-all 172 | "Creates a union-all query, applies any modifying functions in the body and then 173 | executes it. 174 | 175 | ex: (union-all 176 | (queries (subselect user 177 | (where {:id 7})) 178 | (subselect user-backup 179 | (where {:id 7}))) 180 | (order :name))" 181 | [& body] 182 | (make-query-then-exec #'union-all* body)) 183 | 184 | (defmacro intersect 185 | "Creates an intersect query, applies any modifying functions in the body and then 186 | executes it. 187 | 188 | ex: (intersect 189 | (queries (subselect user 190 | (where {:id 7})) 191 | (subselect user-backup 192 | (where {:id 8}))) 193 | (order :name))" 194 | [& body] 195 | (make-query-then-exec #'intersect* body)) 196 | 197 | ;;***************************************************** 198 | ;; Query parts 199 | ;;***************************************************** 200 | 201 | (defn- update-fields [query fs] 202 | (let [[first-cur] (:fields query)] 203 | (if (= first-cur ::*) 204 | (assoc query :fields fs) 205 | (update-in query [:fields] utils/vconcat fs)))) 206 | 207 | (defn fields 208 | "Set the fields to be selected in a query. Fields can either be a keyword 209 | or a vector of two keywords [field alias]: 210 | 211 | (fields query :name [:firstname :first])" 212 | [query & vs] 213 | (let [aliases (set (map second (filter vector? vs)))] 214 | (-> query 215 | (update-in [:aliases] set/union aliases) 216 | (update-fields (vec vs))))) 217 | 218 | (defn set-fields 219 | "Set the fields and values for an update query." 220 | [query fields-map] 221 | (update-in query [:set-fields] merge fields-map)) 222 | 223 | (defn from 224 | "Add tables to the from clause." 225 | [query table] 226 | (update-in query [:from] conj table)) 227 | 228 | (defn- where-or-having-form [where*-or-having* query form] 229 | `(let [q# ~query] 230 | (~where*-or-having* q# 231 | (bind-query q# 232 | (eng/pred-map ~(eng/parse-where `~form)))))) 233 | 234 | (defn where* 235 | "Add a where clause to the query. Clause can be either a map or a string, and 236 | will be AND'ed to the other clauses." 237 | [query clause] 238 | (update-in query [:where] conj clause)) 239 | 240 | (defmacro where 241 | "Add a where clause to the query, expressing the clause in clojure expressions 242 | with keywords used to reference fields. 243 | e.g. (where query (or (= :hits 1) (> :hits 5))) 244 | 245 | Available predicates: and, or, =, not=, <, >, <=, >=, in, like, not, between 246 | 247 | Where can also take a map at any point and will create a clause that compares keys 248 | to values. The value can be a vector with one of the above predicate functions 249 | describing how the key is related to the value: 250 | (where query {:name [like \"chris\"]})" 251 | [query form] 252 | (where-or-having-form #'where* query form)) 253 | 254 | (defn having* 255 | "Add a having clause to the query. Clause can be either a map or a string, and 256 | will be AND'ed to the other clauses." 257 | [query clause] 258 | (update-in query [:having] conj clause)) 259 | 260 | (defmacro having 261 | "Add a having clause to the query, expressing the clause in clojure expressions 262 | with keywords used to reference fields. 263 | e.g. (having query (or (= :hits 1) (> :hits 5))) 264 | 265 | Available predicates: and, or, =, not=, <, >, <=, >=, in, like, not, between 266 | 267 | Having can also take a map at any point and will create a clause that compares 268 | keys to values. The value can be a vector with one of the above predicate 269 | functions describing how the key is related to the value: 270 | (having query {:name [like \"chris\"}) 271 | 272 | Having only works if you have an aggregation, using it without one will cause 273 | an error." 274 | [query form] 275 | (where-or-having-form #'having* query form)) 276 | 277 | (defn order 278 | "Add an ORDER BY clause to a select, union, union-all, or intersect query. 279 | field should be a keyword of the field name, dir is ASC by default. 280 | 281 | (order query :created :asc)" 282 | ([query field dir] 283 | (update-in query [:order] conj [field (or dir :ASC)])) 284 | ([query field] 285 | (order query field :ASC))) 286 | 287 | (defn values 288 | "Add records to an insert clause. values can either be a vector of maps or a 289 | single map. 290 | 291 | (values query [{:name \"john\"} {:name \"ed\"}])" 292 | [query values] 293 | (update-in query [:values] utils/vconcat (if (map? values) 294 | [values] 295 | values))) 296 | 297 | (defn join* [query type table clause] 298 | (update-in query [:joins] conj [type table clause])) 299 | 300 | (defn add-joins 301 | ([query ent rel] 302 | (add-joins query ent rel :left)) 303 | ([query ent rel type] 304 | (bind-query 305 | query 306 | (if-let [join-table (:join-table rel)] 307 | (let [{:keys [lpk lfk rpk rfk]} rel] 308 | (-> query 309 | (join* type join-table 310 | (apply sfns/pred-and 311 | (map sfns/pred-= lpk @lfk))) 312 | (join* type ent 313 | (apply sfns/pred-and 314 | (map sfns/pred-= @rfk rpk))))) 315 | (join* query type ent 316 | (let [{:keys [pk fk]} rel] 317 | (apply sfns/pred-and 318 | (map sfns/pred-= pk fk)))))))) 319 | 320 | (defmacro join 321 | "Add a join clause to a select query, specifying an entity defined by defentity, or the table name to 322 | join and the predicate to join on. If the entity relationship uses a join 323 | table then two clauses will be added. Otherwise, only one clause 324 | will be added. 325 | 326 | (join query addresses) 327 | (join query :right addresses) 328 | (join query addresses (= :addres.users_id :users.id)) 329 | (join query :right addresses (= :address.users_id :users.id))" 330 | {:arglists '([query ent] [query type-or-table ent-or-clause] [query type table clause])} 331 | ([query ent] 332 | `(join ~query :left ~ent)) 333 | ([query type-or-table ent-or-clause] 334 | `(if (entity? ~ent-or-clause) 335 | (let [q# ~query 336 | e# ~ent-or-clause 337 | rel# (get-rel (:ent q#) e#) 338 | type# ~type-or-table] 339 | (add-joins q# e# rel# type#)) 340 | (join ~query :left ~type-or-table ~ent-or-clause))) 341 | ([query type table clause] 342 | `(join* ~query ~type ~table (eng/pred-map ~(eng/parse-where clause))))) 343 | 344 | (defn post-query 345 | "Add a function representing a query that should be executed for each result 346 | in a select. This is done lazily over the result set." 347 | [query post] 348 | (update-in query [:post-queries] conj post)) 349 | 350 | (defn limit 351 | "Add a limit clause to a select query." 352 | [query v] 353 | (assoc query :limit v)) 354 | 355 | (defn offset 356 | "Add an offset clause to a select query." 357 | [query v] 358 | (assoc query :offset v)) 359 | 360 | (defn group 361 | "Add a group-by clause to a select query" 362 | [query & fields] 363 | (update-in query [:group] utils/vconcat fields)) 364 | 365 | (defn add-comment 366 | "Add a comment clause to a select query" 367 | [query comment-str] 368 | (update-in query [:comments] utils/vconcat [comment-str])) 369 | 370 | (defmacro aggregate 371 | "Use a SQL aggregator function, aliasing the results, and optionally grouping by 372 | a field: 373 | 374 | (select users 375 | (aggregate (count :*) :cnt :status)) 376 | 377 | Aggregates available: count, sum, avg, min, max, first, last" 378 | [query agg alias & [group-by]] 379 | `(let [q# ~query] 380 | (bind-query q# 381 | (let [res# (fields q# [(-> q# ~(eng/parse-aggregate agg)) ~alias])] 382 | (if ~group-by 383 | (group res# ~group-by) 384 | res#))))) 385 | 386 | (defn queries 387 | "Adds a group of queries to a union, union-all or intersect" 388 | [query & queries] 389 | (update-in query [:queries] utils/vconcat queries)) 390 | 391 | ;;***************************************************** 392 | ;; Other sql 393 | ;;***************************************************** 394 | 395 | (defn sqlfn* 396 | "Call an arbitrary SQL function by providing the name of the function 397 | and its params" 398 | [fn-name & params] 399 | (apply eng/sql-func (name fn-name) params)) 400 | 401 | (defmacro sqlfn 402 | "Call an arbitrary SQL function by providing func as a symbol or keyword 403 | and its params" 404 | [func & params] 405 | `(sqlfn* (quote ~func) ~@params)) 406 | 407 | (defmacro subselect 408 | "Create a subselect clause to be used in queries. This works exactly like 409 | (select ...) execept it will wrap the query in ( .. ) and make sure it can be 410 | used in any current query: 411 | 412 | (select users 413 | (where {:id [in (subselect users2 (fields :id))]}))" 414 | [& parts] 415 | `(utils/sub-query (query-only (select ~@parts)))) 416 | 417 | (defn modifier 418 | "Add a modifer to the beginning of a query: 419 | 420 | (select orders 421 | (modifier \"DISTINCT\"))" 422 | [query & modifiers] 423 | (update-in query [:modifiers] conj (reduce str modifiers))) 424 | 425 | (defn raw 426 | "Embed a raw string of SQL in a query. This is used when Korma doesn't 427 | provide some specific functionality you're looking for: 428 | 429 | (select users 430 | (fields (raw \"PERIOD(NOW(), NOW())\")))" 431 | [s] 432 | (utils/generated s)) 433 | 434 | ;;***************************************************** 435 | ;; Query exec 436 | ;;***************************************************** 437 | 438 | (defmacro sql-only 439 | "Wrap around a set of queries so that instead of executing, each will return a 440 | string of the SQL that would be used." 441 | [& body] 442 | `(binding [*exec-mode* :sql] 443 | ~@body)) 444 | 445 | (defmacro dry-run 446 | "Wrap around a set of queries to print to the console all SQL that would 447 | be run and return dummy values instead of executing them." 448 | [& body] 449 | `(binding [*exec-mode* :dry-run] 450 | ~@body)) 451 | 452 | (defmacro query-only 453 | "Wrap around a set of queries to force them to return their query objects." 454 | [& body] 455 | `(binding [*exec-mode* :query] 456 | ~@body)) 457 | 458 | (defn as-sql 459 | "Force a query to return a string of SQL when (exec) is called." 460 | [query] 461 | (bind-query query (:sql-str (eng/->sql query)))) 462 | 463 | (defn- apply-posts 464 | [query results] 465 | (if-let [posts (seq (:post-queries query))] 466 | (let [post-fn (apply comp posts)] 467 | (post-fn results)) 468 | results)) 469 | 470 | (defn- apply-transforms 471 | [query results] 472 | (if (#{:delete :update} (:type query)) 473 | results 474 | (if-let [trans (-> query :ent :transforms seq)] 475 | (let [trans-fn (apply comp trans)] 476 | (if (sequential? results) 477 | (map trans-fn results) 478 | (trans-fn results))) 479 | results))) 480 | 481 | (defn- apply-prepares 482 | [query] 483 | (if-let [preps (-> query :ent :prepares seq)] 484 | (let [prep-fn (apply comp preps)] 485 | (case (:type query) 486 | :insert (update-in query [:values] #(map prep-fn %)) 487 | :update (update-in query [:set-fields] prep-fn) 488 | query)) 489 | query)) 490 | 491 | (defn exec 492 | "Execute a query map and return the results." 493 | [query] 494 | (let [query (apply-prepares query) 495 | query (bind-query query (eng/->sql query)) 496 | sql (:sql-str query) 497 | params (:params query)] 498 | (cond 499 | (:sql query) sql 500 | (= *exec-mode* :sql) sql 501 | (= *exec-mode* :query) query 502 | (= *exec-mode* :dry-run) (do 503 | (println "dry run ::" sql "::" (vec params)) 504 | (let [fk-key (->> query :ent :rel vals 505 | (map deref) 506 | (filter (comp #{:belongs-to} :rel-type)) 507 | (map :fk-key)) 508 | pk-key (-> query :ent :pk) 509 | result-keys (concat 510 | pk-key 511 | (apply concat fk-key)) 512 | results (apply-posts query [(zipmap result-keys (repeat 1))])] 513 | (first results) 514 | results)) 515 | :else (let [results (db/do-query query)] 516 | (apply-transforms query (apply-posts query results)))))) 517 | 518 | (defn exec-raw 519 | "Execute a raw SQL string, supplying whether results should be returned. `sql` 520 | can either be a string or a vector of the sql string and its params. You can 521 | also optionally provide the connection to execute against as the first 522 | parameter. 523 | 524 | (exec-raw [\"SELECT * FROM users WHERE age > ?\" [5]] :results)" 525 | [conn? & [sql with-results?]] 526 | (let [sql-vec (fn [v] (if (vector? v) v [v nil])) 527 | [conn? [sql-str params] with-results?] (if (or (string? conn?) 528 | (vector? conn?)) 529 | [nil (sql-vec conn?) sql] 530 | [conn? (sql-vec sql) with-results?])] 531 | (db/do-query {:db conn? :results with-results? :sql-str sql-str :params params}))) 532 | 533 | ;;***************************************************** 534 | ;; Entities 535 | ;;***************************************************** 536 | 537 | (defn entity? [x] (= (type x) ::Entity)) 538 | 539 | (defn create-entity 540 | "Create an entity representing a table in a database." 541 | [table] 542 | ^{:type ::Entity} 543 | {:table table 544 | :name table 545 | :pk '(:id) 546 | :db nil 547 | :transforms '() 548 | :prepares '() 549 | :fields [] 550 | :rel {}}) 551 | 552 | (defn- simple-table-name [ent] 553 | (last (string/split (:table ent) #"\."))) 554 | 555 | (defn- default-fk-name [ent] 556 | (cond 557 | (map? ent) (list (keyword (str (simple-table-name ent) "_id"))) 558 | (var? ent) (recur @ent) 559 | :else (throw (Exception. (str "Can't determine default fk for " ent))))) 560 | 561 | (defn- to-raw-key [ent k] 562 | (map #(raw (eng/prefix ent %)) k)) 563 | 564 | (defn- many-to-many-keys [parent child {:keys [join-table lfk rfk]}] 565 | {:lpk (to-raw-key parent (:pk parent)) 566 | :lfk (delay (to-raw-key {:table (name join-table)} @lfk)) 567 | :rfk (delay (to-raw-key {:table (name join-table)} @rfk)) 568 | :rpk (to-raw-key child (:pk child)) 569 | :join-table join-table}) 570 | 571 | (defn- get-db-keys [parent child {:keys [pk fk]}] 572 | (let [pk-key (or pk (:pk parent)) 573 | fk-key (or fk (default-fk-name parent))] 574 | {:pk (to-raw-key parent pk-key) 575 | :fk (to-raw-key child fk-key) 576 | :fk-key fk-key})) 577 | 578 | (defn- db-keys-and-foreign-ent [type ent sub-ent opts] 579 | (case type 580 | :many-to-many [(many-to-many-keys ent sub-ent opts) sub-ent] 581 | (:has-one :has-many) [(get-db-keys ent sub-ent opts) sub-ent] 582 | :belongs-to [(get-db-keys sub-ent ent opts) ent])) 583 | 584 | (defn create-relation [ent sub-ent type opts] 585 | (let [[db-keys foreign-ent] (db-keys-and-foreign-ent type ent sub-ent opts) 586 | fk-override (when-let [fk-key (:fk opts)] 587 | {:fk (to-raw-key foreign-ent fk-key) 588 | :fk-key fk-key})] 589 | (merge {:table (:table sub-ent) 590 | :alias (:alias sub-ent) 591 | :rel-type type} 592 | db-keys 593 | fk-override))) 594 | 595 | (defn rel [ent sub-ent type opts] 596 | (let [var-name (-> sub-ent meta :name) 597 | var-ns (-> sub-ent meta :ns)] 598 | (assoc-in ent [:rel (name var-name)] 599 | (delay 600 | (let [resolved (ns-resolve var-ns var-name) 601 | sub-ent (when resolved (deref sub-ent))] 602 | (when-not (map? sub-ent) 603 | (throw (Exception. (format "Entity used in relationship does not exist: %s" (name var-name))))) 604 | (create-relation ent sub-ent type opts)))))) 605 | 606 | (defn get-rel [ent sub-ent] 607 | (let [sub-name (if (map? sub-ent) 608 | (:name sub-ent) 609 | sub-ent)] 610 | (force (get-in ent [:rel sub-name])))) 611 | 612 | (defmacro has-one 613 | "Add a has-one relationship for the given entity. It is assumed that the foreign key 614 | is on the sub-entity with the format table_id: user.id = address.user_id 615 | Can optionally pass a map with a :fk key collection or use the helper `fk` function 616 | to explicitly set the foreign key. 617 | 618 | (has-one users address) 619 | (has-one users address (fk :userId))" 620 | ([ent sub-ent] 621 | `(has-one ~ent ~sub-ent nil)) 622 | ([ent sub-ent opts] 623 | `(rel ~ent (var ~sub-ent) :has-one ~opts))) 624 | 625 | (defmacro belongs-to 626 | "Add a belongs-to relationship for the given entity. It is assumed that the foreign key 627 | is on the current entity with the format sub-ent-table_id: email.user_id = user.id. 628 | Can optionally pass a map with a :fk key collection or use the helper `fk` function 629 | to explicitly set the foreign key. 630 | 631 | (belongs-to users email) 632 | (belongs-to users email (fk :userId))" 633 | ([ent sub-ent] 634 | `(belongs-to ~ent ~sub-ent nil)) 635 | ([ent sub-ent opts] 636 | `(rel ~ent (var ~sub-ent) :belongs-to ~opts))) 637 | 638 | (defmacro has-many 639 | "Add a has-many relation for the given entity. It is assumed that the foreign key 640 | is on the sub-entity with the format table_id: user.id = email.user_id 641 | Can optionally pass a map with a :fk key collection or use the helper `fk` function 642 | to explicitly set the foreign key. 643 | 644 | (has-many users email) 645 | (has-many users email (fk :emailID))" 646 | ([ent sub-ent] 647 | `(has-many ~ent ~sub-ent nil)) 648 | ([ent sub-ent opts] 649 | `(rel ~ent (var ~sub-ent) :has-many ~opts))) 650 | 651 | (defn many-to-many-fn [ent sub-ent-var join-table opts] 652 | (let [opts (assoc opts 653 | :join-table join-table 654 | :lfk (delay (get opts :lfk (default-fk-name ent))) 655 | :rfk (delay (get opts :rfk (default-fk-name sub-ent-var))))] 656 | (rel ent sub-ent-var :many-to-many opts))) 657 | 658 | (defmacro many-to-many 659 | "Add a many-to-many relation for the given entity. It is assumed that a join 660 | table is used to implement the relationship and that the foreign keys are in 661 | the join table with the format table_id: 662 | user.id = user_email.user_id 663 | user_email.email_id = email.id 664 | Can optionally pass a map with :lfk and :rfk key collections or use the 665 | helper `lfk`, `rfk` functions to explicitly set the join keys. 666 | 667 | (many-to-many email :user_email) 668 | (many-to-many email :user_email (lfk :user_id) 669 | (rfk :email_id))" 670 | ([ent sub-ent join-table] 671 | `(many-to-many ~ent ~sub-ent ~join-table nil)) 672 | ([ent sub-ent join-table fk1 fk2] 673 | `(many-to-many ~ent ~sub-ent ~join-table (merge ~fk1 ~fk2))) 674 | ([ent sub-ent join-table opts] 675 | `(many-to-many-fn ~ent (var ~sub-ent) ~join-table ~opts))) 676 | 677 | (defn entity-fields 678 | "Set the fields to be retrieved in all select queries for the 679 | entity." 680 | [ent & fields] 681 | (update-in ent [:fields] utils/vconcat fields)) 682 | 683 | (defn table 684 | "Set the name of the table and an optional alias to be used for the entity. 685 | By default the table is the name of entity's symbol." 686 | [ent t & [alias]] 687 | (let [tname (if (or (keyword? t) 688 | (string? t)) 689 | (name t) 690 | (if alias 691 | t 692 | (throw (Exception. "Generated tables must have aliases.")))) 693 | ent (assoc ent :table tname)] 694 | (if alias 695 | (assoc ent :alias (name alias)) 696 | ent))) 697 | 698 | (defn pk 699 | "Set the primary key used for an entity. :id by default." 700 | ([ent & pk] 701 | (assoc ent :pk (map keyword pk)))) 702 | 703 | (defn fk 704 | "Set the foreign key used for an entity relationship." 705 | ([& fk] 706 | {:fk (map keyword fk)})) 707 | 708 | (defn lfk 709 | "Set the left foreign key used for an entity relationship." 710 | ([& lfk] 711 | {:lfk (map keyword lfk)})) 712 | 713 | (defn rfk 714 | "Set the right foreign key used for an entity relationship." 715 | ([& rfk] 716 | {:rfk (map keyword rfk)})) 717 | 718 | (defn database 719 | "Set the database connection to be used for this entity." 720 | [ent db] 721 | (assoc ent :db db )) 722 | 723 | (defn transform 724 | "Add a function to be applied to results coming from the database" 725 | [ent func] 726 | (update-in ent [:transforms] conj func)) 727 | 728 | (defn prepare 729 | "Add a function to be applied to records/values going into the database" 730 | [ent func] 731 | (update-in ent [:prepares] conj func)) 732 | 733 | (defmacro defentity 734 | "Define an entity representing a table in the database, applying any modifications in 735 | the body." 736 | [ent & body] 737 | `(let [e# (-> (create-entity ~(name ent)) 738 | ~@body)] 739 | (def ~ent e#))) 740 | 741 | ;;***************************************************** 742 | ;; With 743 | ;;***************************************************** 744 | 745 | (defn- force-prefix [ent fields] 746 | (->> fields 747 | (map (fn [field] 748 | (if (vector? field) 749 | [(utils/generated (eng/prefix ent (first field))) (second field)] 750 | (eng/prefix ent field)))) 751 | vec)) 752 | 753 | (defn- merge-query [sub-query query] 754 | (reduce (fn [query' k] 755 | (update-in query' [k] into (get sub-query k))) 756 | query 757 | [:aliases :fields :group :joins :order :params :post-queries :where])) 758 | 759 | (defn- make-sub-query [sub-ent body-fn] 760 | (let [sub-query (select* sub-ent)] 761 | (bind-query sub-query 762 | (-> sub-query 763 | (body-fn) 764 | (update-in [:fields] #(force-prefix sub-ent %)) 765 | (update-in [:order] #(force-prefix sub-ent %)) 766 | (update-in [:group] #(force-prefix sub-ent %)))))) 767 | 768 | (defn assoc-db-to-entity [query ent] 769 | (if-let [db (or (:db ent) (:db query) db/*current-db*)] 770 | (database ent db) 771 | ent)) 772 | 773 | (defn- with-one-to-many [rel query ent body-fn] 774 | (let [pk (get-in query [:ent :pk]) 775 | fk-key (:fk-key rel) 776 | table (keyword (eng/table-alias ent)) 777 | ent (assoc-db-to-entity query ent)] 778 | (post-query query 779 | (partial map 780 | #(assoc % table 781 | (bind-query 782 | query 783 | (select ent 784 | (body-fn) 785 | (where (apply sfns/pred-and 786 | (map sfns/pred-= fk-key 787 | (map (fn [k] (get % k)) pk))))))))))) 788 | 789 | (defn- make-key-unique [->key m k n] 790 | (let [unique-key (if (= n 1) k (keyword (->key (str (name k) "_" n))))] 791 | (if (contains? m unique-key) 792 | (recur ->key m k (inc n)) 793 | unique-key))) 794 | 795 | (defn- merge-with-unique-keys [->key m1 m2] 796 | (reduce (fn [m [k v]] (assoc m (make-key-unique ->key m k 1) v)) m1 m2)) 797 | 798 | (defn- get-join-keys [rel ent sub-ent] 799 | (case (:rel-type rel) 800 | :has-one [(:pk ent) (:fk-key rel)] 801 | :belongs-to [(:fk-key rel) (:pk sub-ent)])) 802 | 803 | (defn- with-one-to-one-later [rel query sub-ent body-fn] 804 | (let [sub-ent (assoc-db-to-entity query sub-ent) 805 | [ent-key sub-ent-key] (get-join-keys rel (:ent query) sub-ent)] 806 | (post-query query 807 | (partial map 808 | (fn [ent] 809 | (bind-query 810 | query 811 | (merge-with-unique-keys (get-in eng/*bound-options* [:naming :keys]) 812 | ent 813 | (first 814 | (select sub-ent 815 | (body-fn) 816 | (where 817 | (let [ent-kval (map #(get ent %) ent-key)] 818 | (apply sfns/pred-and 819 | (map sfns/pred-= sub-ent-key ent-kval))))))))))))) 820 | 821 | (defn- with-one-to-one-now [rel query sub-ent body-fn] 822 | (let [ent (:ent query) 823 | table (if (:alias rel) [(:table sub-ent) (:alias sub-ent)] (:table sub-ent)) 824 | [ent-key sub-ent-key] (get-join-keys rel ent sub-ent)] 825 | (bind-query query 826 | (merge-query 827 | (make-sub-query sub-ent body-fn) 828 | (join query 829 | table 830 | (apply sfns/pred-and 831 | (map sfns/pred-= 832 | (to-raw-key sub-ent sub-ent-key) 833 | (to-raw-key ent ent-key)))))))) 834 | 835 | (defn- with-many-to-many [{:keys [lfk rfk rpk join-table]} query ent body-fn] 836 | (let [pk (get-in query [:ent :pk]) 837 | table (keyword (eng/table-alias ent)) 838 | ent (assoc-db-to-entity query ent)] 839 | (post-query query 840 | (partial map 841 | #(assoc % table 842 | (bind-query 843 | query 844 | (select ent 845 | (join :inner join-table 846 | (apply sfns/pred-and 847 | (map sfns/pred-= @rfk rpk))) 848 | (body-fn) 849 | (where (apply sfns/pred-and 850 | (map sfns/pred-= @lfk (map (fn [k] (get % k)) pk))))))))))) 851 | 852 | (defn with* [query sub-ent body-fn] 853 | (let [{:keys [rel-type] :as rel} (get-rel (:ent query) sub-ent) 854 | transforms (seq (:transforms sub-ent))] 855 | (cond 856 | (and (#{:belongs-to :has-one} rel-type) 857 | (not transforms)) (with-one-to-one-now rel query sub-ent body-fn) 858 | (#{:belongs-to :has-one} rel-type) (with-one-to-one-later rel query sub-ent body-fn) 859 | (= :has-many rel-type) (with-one-to-many rel query sub-ent body-fn) 860 | (= :many-to-many rel-type) (with-many-to-many rel query sub-ent body-fn) 861 | :else (throw (Exception. (str "No relationship defined for table: " 862 | (:table sub-ent))))))) 863 | 864 | (defmacro with 865 | "Add a related entity to the given select query. If the entity has a relationship 866 | type of :belongs-to or :has-one, the requested fields will be returned directly in 867 | the result map. If the entity is a :has-many, a second query will be executed lazily 868 | and a key of the entity name will be assoc'd with a vector of the results. 869 | 870 | (defentity email (entity-fields :email)) 871 | (defentity user (has-many email)) 872 | (select user 873 | (with email) => [{:name \"chris\" :email [{email: \"c@c.com\"}]} ... 874 | 875 | With can also take a body that will further refine the relation: 876 | (select user 877 | (with address 878 | (with state) 879 | (fields :address.city :state.state) 880 | (where {:address.zip x})))" 881 | [query ent & body] 882 | `(with* ~query ~ent (fn [q#] 883 | (-> q# 884 | ~@body)))) 885 | 886 | (defn- ensure-fields 887 | "ensure that fields in fs are included in the query's result set" 888 | [query fs] 889 | (let [[first-cur] (:fields query)] 890 | (if (= first-cur ::*) 891 | query 892 | (update-in query [:fields] utils/vconcat fs)))) 893 | 894 | (defn- ensure-valid-subquery [q] 895 | (if (->> (select-keys q [:order 896 | :group 897 | :limit 898 | :offset 899 | :having 900 | :modifiers]) 901 | vals 902 | (some not-empty)) 903 | (throw (Exception. (str "`with-batch` supports only `where` and `fields` " 904 | "options, no sorting, grouping, limits, offsets, " 905 | "modifiers"))) 906 | q)) 907 | 908 | (defn- with-later-batch [rel query ent body-fn] 909 | (let [pk (get-in query [:ent :pk]) 910 | fk-key (:fk-key rel) 911 | table (keyword (eng/table-alias ent)) 912 | in-batch (= 1 (count pk))] 913 | (post-query query 914 | (fn [rows] 915 | (let [fks (for [row rows] 916 | (map #(get row %) pk)) 917 | child-rows (select ent 918 | (body-fn) 919 | (where 920 | (if in-batch 921 | {(first fk-key) [in (vec (flatten fks))]} 922 | (apply sfns/pred-or 923 | (map #(apply sfns/pred-and 924 | (map sfns/pred-= fk-key %)) fks)))) 925 | (ensure-fields (vec fk-key)) 926 | (ensure-valid-subquery)) 927 | child-rows-by-pk (group-by (fn [row] 928 | (vec (map #(get row %) fk-key))) child-rows)] 929 | (map (fn [row] 930 | (assoc row 931 | table (get child-rows-by-pk (vec (map #(get row %) pk))))) 932 | rows)))))) 933 | 934 | (defn with-batch* [query sub-ent body-fn] 935 | (let [rel (get-rel (:ent query) sub-ent)] 936 | (case (:rel-type rel) 937 | (:has-one :belongs-to :many-to-many) (with* query sub-ent body-fn) 938 | :has-many (with-later-batch rel query sub-ent body-fn) 939 | (throw (Exception. (str "No relationship defined for table: " 940 | (:table sub-ent))))))) 941 | 942 | (defmacro with-batch 943 | "Add a related entity. This behaves like `with`, except that, for has-many 944 | relationships, it runs a single query to get relations of all fetched rows. 945 | This is faster than regular `with` but it doesn't support many of the 946 | additional options (order, limit, offset, group, having)" 947 | [query ent & body] 948 | `(with-batch* ~query ~ent (fn [q#] 949 | (-> q# 950 | ~@body)))) 951 | -------------------------------------------------------------------------------- /doc/korma.core.html: -------------------------------------------------------------------------------- 1 | 2 | korma.core documentation

korma.core

Core querying and entity functions
  3 | 

*exec-mode*

dynamic

add-comment

(add-comment query comment-str)
Add a comment clause to a select query
  4 | 

add-joins

(add-joins query ent rel)(add-joins query ent rel type)

aggregate

macro

(aggregate query agg alias & [group-by])
Use a SQL aggregator function, aliasing the results, and optionally grouping by
  5 | a field:
  6 | 
  7 | (select users
  8 |   (aggregate (count :*) :cnt :status))
  9 | 
 10 | Aggregates available: count, sum, avg, min, max, first, last

as-sql

(as-sql query)
Force a query to return a string of SQL when (exec) is called.
 11 | 

assoc-db-to-entity

(assoc-db-to-entity query ent)

belongs-to

macro

(belongs-to ent sub-ent)(belongs-to ent sub-ent opts)
Add a belongs-to relationship for the given entity. It is assumed that the foreign key
 12 | is on the current entity with the format sub-ent-table_id: email.user_id = user.id.
 13 | Can optionally pass a map with a :fk key collection or use the helper `fk` function
 14 | to explicitly set the foreign key.
 15 | 
 16 | (belongs-to users email)
 17 | (belongs-to users email (fk :userId))

create-entity

(create-entity table)
Create an entity representing a table in a database.
 18 | 

create-relation

(create-relation ent sub-ent type opts)

database

(database ent db)
Set the database connection to be used for this entity.
 19 | 

defentity

macro

(defentity ent & body)
Define an entity representing a table in the database, applying any modifications in
 20 | the body.

delete

macro

(delete ent & body)
Creates a delete query, applies any modifying functions in the body and then
 21 | executes it. `ent` is either a string or an entity created by defentity.
 22 | Returns number of deleted rows as provided by the JDBC driver.
 23 | 
 24 | ex: (delete user
 25 |       (where {:id 7}))

delete*

(delete* ent)
Create an empty delete query. Ent can either be an entity defined by defentity,
 26 | or a string of the table name

dry-run

macro

(dry-run & body)
Wrap around a set of queries to print to the console all SQL that would
 27 | be run and return dummy values instead of executing them.

empty-query

(empty-query ent)

entity-fields

(entity-fields ent & fields)
Set the fields to be retrieved in all select queries for the
 28 | entity.

entity?

(entity? x)

exec

(exec query)
Execute a query map and return the results.
 29 | 

exec-raw

(exec-raw conn? & [sql with-results?])
Execute a raw SQL string, supplying whether results should be returned. `sql`
 30 | can either be a string or a vector of the sql string and its params. You can
 31 | also optionally provide the connection to execute against as the first
 32 | parameter.
 33 | 
 34 | (exec-raw ["SELECT * FROM users WHERE age > ?" [5]] :results)

fields

(fields query & vs)
Set the fields to be selected in a query. Fields can either be a keyword
 35 | or a vector of two keywords [field alias]:
 36 | 
 37 | (fields query :name [:firstname :first])

fk

(fk & fk)
Set the foreign key used for an entity relationship.
 38 | 

from

(from query table)
Add tables to the from clause.
 39 | 

get-rel

(get-rel ent sub-ent)

group

(group query & fields)
Add a group-by clause to a select query
 40 | 

has-many

macro

(has-many ent sub-ent)(has-many ent sub-ent opts)
Add a has-many relation for the given entity. It is assumed that the foreign key
 41 | is on the sub-entity with the format table_id: user.id = email.user_id
 42 | Can optionally pass a map with a :fk key collection or use the helper `fk` function
 43 | to explicitly set the foreign key.
 44 | 
 45 | (has-many users email)
 46 | (has-many users email (fk :emailID))

has-one

macro

(has-one ent sub-ent)(has-one ent sub-ent opts)
Add a has-one relationship for the given entity. It is assumed that the foreign key
 47 | is on the sub-entity with the format table_id: user.id = address.user_id
 48 | Can optionally pass a map with a :fk key collection or use the helper `fk` function
 49 | to explicitly set the foreign key.
 50 | 
 51 | (has-one users address)
 52 | (has-one users address (fk :userId))

having

macro

(having query form)
Add a having clause to the query, expressing the clause in clojure expressions
 53 | with keywords used to reference fields.
 54 | e.g. (having query (or (= :hits 1) (> :hits 5)))
 55 | 
 56 | Available predicates: and, or, =, not=, <, >, <=, >=, in, like, not, between
 57 | 
 58 | Having can also take a map at any point and will create a clause that compares
 59 | keys to values. The value can be a vector with one of the above predicate
 60 | functions describing how the key is related to the value:
 61 |   (having query {:name [like "chris"})
 62 | 
 63 | Having only works if you have an aggregation, using it without one will cause
 64 | an error.

having*

(having* query clause)
Add a having clause to the query. Clause can be either a map or a string, and
 65 | will be AND'ed to the other clauses.

insert

macro

(insert ent & body)
Creates an insert query, applies any modifying functions in the body
 66 | and then executes it. `ent` is either a string or an entity created by
 67 | defentity. The return value is the last inserted item, but its
 68 | representation is dependent on the database driver used
 69 | (e.g. postgresql returns the full row as a hash,
 70 | MySQL returns a {:generated_key <ID>} hash,
 71 | and MSSQL returns a {:generated_keys <ID>} hash).
 72 | 
 73 | ex: (insert user
 74 |       (values [{:name "chris"} {:name "john"}]))

insert*

(insert* ent)
Create an empty insert query. Ent can either be an entity defined by defentity,
 75 | or a string of the table name

intersect

macro

(intersect & body)
Creates an intersect query, applies any modifying functions in the body and then
 76 | executes it.
 77 | 
 78 | ex: (intersect
 79 |       (queries (subselect user
 80 |                  (where {:id 7}))
 81 |                (subselect user-backup
 82 |                  (where {:id 8})))
 83 |       (order :name))

intersect*

(intersect*)
Create an empty intersect query.
 84 | 

join

macro

(join query ent)(join query type-or-table ent-or-clause)(join query type table clause)
Add a join clause to a select query, specifying an entity defined by defentity, or the table name to
 85 | join and the predicate to join on. If the entity relationship uses a join
 86 | table then two clauses will be added. Otherwise, only one clause
 87 | will be added.
 88 | 
 89 | (join query addresses)
 90 | (join query :right addresses)
 91 | (join query addresses (= :addres.users_id :users.id))
 92 | (join query :right addresses (= :address.users_id :users.id))

join*

(join* query type table clause)

lfk

(lfk & lfk)
Set the left foreign key used for an entity relationship.
 93 | 

limit

(limit query v)
Add a limit clause to a select query.
 94 | 

many-to-many

macro

(many-to-many ent sub-ent join-table)(many-to-many ent sub-ent join-table fk1 fk2)(many-to-many ent sub-ent join-table opts)
Add a many-to-many relation for the given entity.  It is assumed that a join
 95 | table is used to implement the relationship and that the foreign keys are in
 96 | the join table with the format table_id:
 97 |   user.id = user_email.user_id
 98 |   user_email.email_id = email.id
 99 | Can optionally pass a map with :lfk and :rfk key collections or use the
100 | helper `lfk`, `rfk` functions to explicitly set the join keys.
101 | 
102 | (many-to-many email :user_email)
103 | (many-to-many email :user_email (lfk :user_id)
104 |                                 (rfk :email_id))

many-to-many-fn

(many-to-many-fn ent sub-ent-var join-table opts)

modifier

(modifier query & modifiers)
Add a modifer to the beginning of a query:
105 | 
106 | (select orders
107 |   (modifier "DISTINCT"))

offset

(offset query v)
Add an offset clause to a select query.
108 | 

order

(order query field dir)(order query field)
Add an ORDER BY clause to a select, union, union-all, or intersect query.
109 | field should be a keyword of the field name, dir is ASC by default.
110 | 
111 | (order query :created :asc)

pk

(pk ent & pk)
Set the primary key used for an entity. :id by default.
112 | 

post-query

(post-query query post)
Add a function representing a query that should be executed for each result
113 | in a select. This is done lazily over the result set.

prepare

(prepare ent func)
Add a function to be applied to records/values going into the database
114 | 

queries

(queries query & queries)
Adds a group of queries to a union, union-all or intersect
115 | 

query-only

macro

(query-only & body)
Wrap around a set of queries to force them to return their query objects.
116 | 

raw

(raw s)
Embed a raw string of SQL in a query. This is used when Korma doesn't
117 | provide some specific functionality you're looking for:
118 | 
119 | (select users
120 |   (fields (raw "PERIOD(NOW(), NOW())")))

rel

(rel ent sub-ent type opts)

rfk

(rfk & rfk)
Set the right foreign key used for an entity relationship.
121 | 

select

macro

(select ent & body)
Creates a select query, applies any modifying functions in the body and then
122 | executes it. `ent` is either a string or an entity created by defentity.
123 | 
124 | ex: (select user
125 |       (fields :name :email)
126 |       (where {:id 2}))

select*

(select* ent)
Create a select query with fields provided in Ent.  If fields are not provided,
127 | create an empty select query. Ent can either be an entity defined by defentity,
128 | or a string of the table name

set-fields

(set-fields query fields-map)
Set the fields and values for an update query.
129 | 

sql-only

macro

(sql-only & body)
Wrap around a set of queries so that instead of executing, each will return a
130 | string of the SQL that would be used.

sqlfn

macro

(sqlfn func & params)
Call an arbitrary SQL function by providing func as a symbol or keyword
131 | and its params

sqlfn*

(sqlfn* fn-name & params)
Call an arbitrary SQL function by providing the name of the function
132 | and its params

subselect

macro

(subselect & parts)
Create a subselect clause to be used in queries. This works exactly like
133 | (select ...) execept it will wrap the query in ( .. ) and make sure it can be
134 | used in any current query:
135 | 
136 | (select users
137 |   (where {:id [in (subselect users2 (fields :id))]}))

table

(table ent t & [alias])
Set the name of the table and an optional alias to be used for the entity.
138 | By default the table is the name of entity's symbol.

transform

(transform ent func)
Add a function to be applied to results coming from the database
139 | 

union

macro

(union & body)
Creates a union query, applies any modifying functions in the body and then
140 | executes it.
141 | 
142 | ex: (union
143 |       (queries (subselect user
144 |                  (where {:id 7}))
145 |                (subselect user-backup
146 |                  (where {:id 7})))
147 |       (order :name))

union*

(union*)
Create an empty union query.
148 | 

union-all

macro

(union-all & body)
Creates a union-all query, applies any modifying functions in the body and then
149 | executes it.
150 | 
151 | ex: (union-all
152 |       (queries (subselect user
153 |                  (where {:id 7}))
154 |                (subselect user-backup
155 |                  (where {:id 7})))
156 |       (order :name))

union-all*

(union-all*)
Create an empty union-all query.
157 | 

update

macro

(update ent & body)
Creates an update query, applies any modifying functions in the body and then
158 | executes it. `ent` is either a string or an entity created by defentity.
159 | Returns number of updated rows as provided by the JDBC driver.
160 | 
161 | ex: (update user
162 |       (set-fields {:name "chris"})
163 |       (where {:id 4}))

update*

(update* ent)
Create an empty update query. Ent can either be an entity defined by defentity,
164 | or a string of the table name.

values

(values query values)
Add records to an insert clause. values can either be a vector of maps or a
165 | single map.
166 | 
167 | (values query [{:name "john"} {:name "ed"}])

where

macro

(where query form)
Add a where clause to the query, expressing the clause in clojure expressions
168 | with keywords used to reference fields.
169 | e.g. (where query (or (= :hits 1) (> :hits 5)))
170 | 
171 | Available predicates: and, or, =, not=, <, >, <=, >=, in, like, not, between
172 | 
173 | Where can also take a map at any point and will create a clause that compares keys
174 | to values. The value can be a vector with one of the above predicate functions
175 | describing how the key is related to the value:
176 |   (where query {:name [like "chris"]})

where*

(where* query clause)
Add a where clause to the query. Clause can be either a map or a string, and
177 | will be AND'ed to the other clauses.

with

macro

(with query ent & body)
Add a related entity to the given select query. If the entity has a relationship
178 | type of :belongs-to or :has-one, the requested fields will be returned directly in
179 | the result map. If the entity is a :has-many, a second query will be executed lazily
180 | and a key of the entity name will be assoc'd with a vector of the results.
181 | 
182 | (defentity email (entity-fields :email))
183 | (defentity user (has-many email))
184 | (select user
185 |   (with email) => [{:name "chris" :email [{email: "c@c.com"}]} ...
186 | 
187 | With can also take a body that will further refine the relation:
188 | (select user
189 |    (with address
190 |       (with state)
191 |       (fields :address.city :state.state)
192 |       (where {:address.zip x})))

with*

(with* query sub-ent body-fn)

with-batch

macro

(with-batch query ent & body)
Add a related entity. This behaves like `with`, except that, for has-many
193 | relationships, it runs a single query to get relations of all fetched rows.
194 | This is faster than regular `with` but it doesn't support many of the
195 | additional options (order, limit, offset, group, having)

with-batch*

(with-batch* query sub-ent body-fn)
--------------------------------------------------------------------------------