├── .travis.yml ├── test └── taoclj │ ├── foundation │ ├── sql │ │ ├── templated_casts.sql │ │ ├── templated_transform.sql │ │ ├── select_arrays_2dimensions.sql │ │ ├── templated_sections.sql │ │ ├── templated_insert_query.sql │ │ ├── templated_select1_record.sql │ │ └── templated_select_query.sql │ ├── tests_config.clj │ ├── delete_tests.clj │ ├── type_conversion_tests.clj │ ├── templating │ │ └── parsing_test.clj │ ├── json_tests.clj │ ├── dsl_test.clj │ ├── select_tests.clj │ ├── update_tests.clj │ ├── templated_tests.clj │ ├── naming_test.clj │ └── insert_tests.clj │ └── foundation_test.clj ├── docs ├── listen-notify.md ├── deleting-data.md ├── datatype-mappings.md ├── connections-setup.md ├── updating-data.md ├── query-threading.md ├── json-support.md ├── dynamic-queries.md ├── selecting-data.md ├── raw-queries.md ├── inserting-data.md └── templated-queries.md ├── .gitignore ├── resources ├── foundation_tests.sql └── examples.sql ├── project.clj ├── src └── taoclj │ ├── foundation │ ├── naming.clj │ ├── templating │ │ ├── loading.clj │ │ ├── parsing.clj │ │ └── generation.clj │ ├── datasources.clj │ ├── dsl.clj │ ├── writing.clj │ ├── execution.clj │ ├── templating.clj │ └── reading.clj │ └── foundation.clj ├── README.md └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | jdk: 4 | - oraclejdk8 5 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_casts.sql: -------------------------------------------------------------------------------- 1 | select '123'::int as num; 2 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_transform.sql: -------------------------------------------------------------------------------- 1 | select id, name 2 | from templated_transform 3 | -------------------------------------------------------------------------------- /docs/listen-notify.md: -------------------------------------------------------------------------------- 1 | 2 | # Listen Notify 3 | - TODO. we have functional code, not yet fleshed out. 4 | 5 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/select_arrays_2dimensions.sql: -------------------------------------------------------------------------------- 1 | select id, letters[1:2][2:3] from select_arrays_2dimensions; 2 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_sections.sql: -------------------------------------------------------------------------------- 1 | select * 2 | from templated_sections 3 | where :section/filters 4 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_insert_query.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO templated_query_inserts (name) 2 | VALUES ('bob'),('bill'); 3 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_select1_record.sql: -------------------------------------------------------------------------------- 1 | select id, name 2 | from templated_select1_record 3 | where id = :id 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /test/taoclj/foundation/sql/templated_select_query.sql: -------------------------------------------------------------------------------- 1 | select id, name 2 | from templated_query_selects 3 | where id in (:ids) 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/foundation_tests.sql: -------------------------------------------------------------------------------- 1 | -- script to setup tests database 2 | 3 | CREATE DATABASE foundation_tests; 4 | CREATE USER foundation_tests_user WITH PASSWORD 'password'; 5 | 6 | -------------------------------------------------------------------------------- /docs/deleting-data.md: -------------------------------------------------------------------------------- 1 | # Deleting Data with DSL 2 | 3 | 4 | ```clojure 5 | 6 | ; a simple delete 7 | (pg/trx-> examples-db 8 | (delete :categories {:id 1})) 9 | 10 | ``` 11 | 12 | -------------------------------------------------------------------------------- /docs/datatype-mappings.md: -------------------------------------------------------------------------------- 1 | # Datatype Mappings 2 | 3 | Clojure Data Type | Postgresql Type 4 | ------------------ | --------------- 5 | string | text 6 | java.time.Instant | timestampz 7 | string sequence | text array 8 | integer sequence | int array 9 | map | json 10 | java.lang.UUID | uuid 11 | 12 | -------------------------------------------------------------------------------- /docs/connections-setup.md: -------------------------------------------------------------------------------- 1 | ## Connection Setup 2 | 3 | 4 | ```clojure 5 | (require '[taoclj.foundation :as pg]) 6 | 7 | (pg/def-datasource examples-db 8 | {:host "localhost" 9 | :port 5432 10 | :database "examples_db" 11 | :username "examples_app" 12 | :password "password" 13 | :pooled false }) 14 | 15 | 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/updating-data.md: -------------------------------------------------------------------------------- 1 | # Updating Data with DSL 2 | 3 | 4 | ```clojure 5 | 6 | ; a simple update 7 | (pg/trx-> examples-db 8 | (pg/update :categories 9 | {:name "Category A2"} ; new column values 10 | {:id 1})) ; where id = 1 11 | 12 | ``` 13 | 14 | * update dsl syntax is likely to be changed. 15 | -------------------------------------------------------------------------------- /test/taoclj/foundation/tests_config.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.tests-config 2 | (:import [com.impossibl.postgres.jdbc PGDataSource])) 3 | 4 | 5 | (def tests-db 6 | (doto (PGDataSource.) 7 | (.setServerName "localhost") ; todo move into 8 | (.setPort 5432) 9 | (.setDatabaseName "foundation_tests") 10 | (.setUser "foundation_tests_user") 11 | (.setPassword "password"))) 12 | 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.taoclj/foundation "0.1.3" 2 | 3 | :description "A clojure data access library for postgresql." 4 | 5 | :url "https://github.com/mikeball/foundation" 6 | 7 | :license {:name "Eclipse Public License" 8 | :url "http://www.eclipse.org/legal/epl-v10.html"} 9 | 10 | :dependencies [[org.clojure/clojure "1.10.1"] 11 | [com.impossibl.pgjdbc-ng/pgjdbc-ng "0.8.2"] 12 | [com.zaxxer/HikariCP "3.3.1"] 13 | [cheshire "5.8.1"]]) 14 | 15 | -------------------------------------------------------------------------------- /docs/query-threading.md: -------------------------------------------------------------------------------- 1 | # Query Threading Operators 2 | 3 | The primary interaction with the database uses a threading operator like model, 4 | with the results of each query appended to the main result set. 5 | 6 | ```clojure 7 | qry-> ; intended for non transactional statement sets 8 | trx-> ; intended for transactional statement sets 9 | 10 | 11 | ; on success 12 | ; select returns rows if any are present 13 | ; select returns nil if no rows are present 14 | ; insert returns generated id's as sequence 15 | ; update returns rows effected count 16 | 17 | ; on any exception 18 | ; all statements print exception to standard out and return false 19 | 20 | 21 | ``` 22 | -------------------------------------------------------------------------------- /src/taoclj/foundation/naming.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.naming) 2 | 3 | 4 | (defn- convert-char [c] 5 | (let [n (int c)] 6 | (if (or (and (>= n 48) (<= n 57)) 7 | (and (>= n 65) (<= n 122))) 8 | c))) 9 | 10 | (defn- from-db-char [c] 11 | (if (= c \_) \- 12 | (convert-char c))) 13 | 14 | 15 | (defn- to-db-char [c] 16 | (if (= c \-) \_ 17 | (convert-char c))) 18 | 19 | 20 | (defn- to-db-name [column-name] 21 | (apply str (map to-db-char (name column-name)))) 22 | 23 | 24 | (defn from-db-name [^String column-name] 25 | (keyword (apply str (map from-db-char column-name)))) 26 | 27 | 28 | (defn to-quoted-db-name [column-name] 29 | (str "\"" (to-db-name column-name) "\"")) 30 | -------------------------------------------------------------------------------- /resources/examples.sql: -------------------------------------------------------------------------------- 1 | /* CREATE DATABASE examples_db; 2 | CREATE USER examples_app WITH PASSWORD 'password'; */ 3 | 4 | 5 | DROP TABLE IF EXISTS products; 6 | DROP TABLE IF EXISTS categories; 7 | 8 | 9 | CREATE TABLE categories ( 10 | id serial primary key not null, 11 | name text not null 12 | ); 13 | 14 | CREATE TABLE products ( 15 | id serial primary key not null, 16 | category_id int not null references categories(id), 17 | name text not null 18 | ); 19 | 20 | 21 | 22 | 23 | GRANT SELECT,INSERT,UPDATE,DELETE ON categories TO examples_app; 24 | GRANT SELECT,USAGE ON categories_id_seq TO examples_app; 25 | GRANT SELECT,INSERT,UPDATE,DELETE ON products TO examples_app; 26 | GRANT SELECT,USAGE ON products_id_seq TO examples_app; 27 | -------------------------------------------------------------------------------- /docs/json-support.md: -------------------------------------------------------------------------------- 1 | # JSON Support 2 | 3 | JSON datatyes are presently partially supported. Postgres JSON datatype can be both inserted and selected. JSONB may only be selected at this point, as the driver does not yet support Postgres JSONB parameters in prepared statements. See https://github.com/impossibl/pgjdbc-ng/issues/163 4 | 5 | If using tempated queries, JSON paramters in queries only function as the entire json parameter. It is not possible to use a query parameter to set a value embedded in a json structure. 6 | 7 | ```clojure 8 | 9 | ; :options is a json column 10 | (pg/trx-> examples-db 11 | (pg/insert :products {:options {:color "blue"}})) 12 | 13 | => 1 14 | 15 | ; json and jsonb columns are converted to clojure maps 16 | (pg/qry-> examples-db 17 | (pg/select :products {})) 18 | 19 | => ({:options {:color "blue"}}) 20 | 21 | ``` 22 | -------------------------------------------------------------------------------- /test/taoclj/foundation/delete_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.delete-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | 9 | 10 | (deftest delete-records 11 | 12 | (with-open [cnx (.getConnection tests-db)] 13 | (execute cnx "DROP TABLE IF EXISTS delete_records;") 14 | (execute cnx "CREATE TABLE delete_records (id serial primary key not null, name text);") 15 | (execute cnx "INSERT INTO delete_records (name) values ('bob'),('bill');")) 16 | 17 | (trx-> tests-db 18 | (delete :delete-records {:id 1})) 19 | 20 | (is (= [{:id 2 :name "bill"}] 21 | (qry-> tests-db 22 | (execute "SELECT id, name FROM delete_records;")))) 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /docs/dynamic-queries.md: -------------------------------------------------------------------------------- 1 | # Dynamic Queries 2 | 3 | 4 | Create the file /path/to/my_query_with_sections.sql 5 | 6 | ```sql 7 | select id, name from products :section/my-where 8 | ``` 9 | Now define a query to use the file, and specify a section handler 10 | function that returns a string to be inserted into the template. 11 | ```clojure 12 | (def-query my-query3 13 | {:file "path/to/my_query_with_sections.sql" 14 | :section/my-where (fn [params] 15 | (if (:name params) "where name=:name")) }) 16 | 17 | ; Do not use raw parameter values in your section handler! 18 | ; Only use the parameter name placeholder just as you do in the other parts of the 19 | ; query, and foundation will parameterize the values for you. 20 | 21 | 22 | ; now use the query with sections 23 | (pg/qry-> examples-db 24 | (my-query3 {:name "Product A"})) 25 | 26 | => ({:id 1 :name "Product A"}) 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /src/taoclj/foundation/templating/loading.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templating.loading 2 | (:require [clojure.java.io :refer [resource]] 3 | [taoclj.foundation.templating.parsing :refer [scan-sql]]) 4 | (:import [java.io FileNotFoundException])) 5 | 6 | 7 | 8 | 9 | (defn load-template-file [path] 10 | (or (some-> path resource slurp) 11 | (throw (FileNotFoundException. path)))) 12 | 13 | ; (load-template-file "taoclj/sql/test-def-select1.sql") 14 | 15 | 16 | 17 | 18 | (defn load-template [options] 19 | (let [raw (load-template-file (:file options)) 20 | raw-queries [raw] ; eventually support multiple queries per file. 21 | ] 22 | (map scan-sql raw-queries))) 23 | 24 | 25 | 26 | ;; (scan-sql "select * from users where id = :id order by {{something}}" 27 | ;; ) 28 | 29 | 30 | ;(load-template {:file "taoclj/sql/test-def-select1.sql"} 31 | ; ) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/taoclj/foundation/type_conversion_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.type-conversion-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | 9 | (deftest handle-cast-to-integer 10 | (is (= '({:num 123}) 11 | (qry-> tests-db 12 | (execute "select '123'::int as num;"))))) 13 | 14 | 15 | (deftest handle-cast-to-text 16 | (is (= '({:txt "abc"}) 17 | (qry-> tests-db 18 | (execute "select 'abc'::text as txt;"))))) 19 | 20 | 21 | 22 | 23 | (deftest templated-casts-are-handled 24 | 25 | (def-query templated-casts-query 26 | {:file "taoclj/foundation/sql/templated_casts.sql"}) 27 | 28 | (is (= '({:num 123}) 29 | (qry-> tests-db 30 | (templated-casts-query {})))) 31 | 32 | ) 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/selecting-data.md: -------------------------------------------------------------------------------- 1 | # Selecting Data with DSL 2 | 3 | ```clojure 4 | 5 | ; select all categories 6 | (pg/qry-> examples-db 7 | (pg/select :categories {})) 8 | 9 | => ({:id 1 :name "category 1"} 10 | {:id 2 :name "category 2"}) ; returns list of all matching results 11 | 12 | 13 | ; select all records with a where clause 14 | (pg/qry-> examples-db 15 | (pg/select :products {:category-id 6})) 16 | 17 | => ({:id 1, :category-id 6, :name "Product A"} 18 | {:id 2, :category-id 6, :name "Product B"}) 19 | 20 | 21 | 22 | ; select a single row 23 | (pg/qry-> examples-db 24 | (pg/select1 :products {:id 1})) 25 | 26 | => {:id 1, :category-id 6, :name "Product A"} 27 | 28 | 29 | 30 | ; issue multiple select statements at once 31 | (pg/qry-> examples-db 32 | (pg/select1 :categories {:id 6}) 33 | (pg/select :products {:category-id 6})) 34 | 35 | => [{:id 6, :name "Category 6"} 36 | ({:id 1, :category-id 6, :name "Product A"} 37 | {:id 2, :category-id 6, :name "Product B"})] 38 | 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/raw-queries.md: -------------------------------------------------------------------------------- 1 | # Raw Queries 2 | 3 | WARNING execute is not safe from sql injection. Do not use with user supplied input. 4 | 5 | ```clojure 6 | 7 | (ns foundation-examples.raw-queries 8 | (:require [taoclj.foundation :as pg] 9 | [taoclj.foundation.execution :as execution])) 10 | 11 | 12 | (pg/def-datasource examples-db 13 | {:host "localhost" 14 | :port 5432 15 | :database "examples_db" 16 | :username "examples_app" 17 | :password "password" 18 | :pooled false }) 19 | 20 | 21 | (with-open [cnx (.getConnection examples-db)] 22 | (execution/execute cnx "CREATE TABLE people (id serial primary key not null, name text);") 23 | (execution/execute cnx "CREATE TABLE places (id serial primary key not null, name text);") 24 | (execution/execute cnx "INSERT INTO people (name) VALUES('bob');") 25 | (execution/execute cnx "INSERT INTO places (name) VALUES('vegas');")) 26 | 27 | => true 28 | 29 | 30 | (with-open [cnx (.getConnection examples-db)] 31 | (execution/execute cnx "select * from people;select * from places;")) 32 | 33 | => [(({:id 1, :name "bob"}) ({:id 1, :name "vegas"}))] 34 | 35 | 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /test/taoclj/foundation/templating/parsing_test.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templating.parsing-test 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation.templating.parsing :refer :all])) 4 | 5 | 6 | 7 | (deftest params-have-allowed-characters 8 | (are [given expected] (= expected 9 | (param-character? given)) 10 | \a true 11 | \A true 12 | \z true 13 | \Z true 14 | \1 true 15 | \9 true 16 | \0 true 17 | \- true 18 | 19 | \space false 20 | \; false 21 | \) false 22 | \= false 23 | 24 | )) 25 | 26 | 27 | 28 | 29 | (deftest sql-is-scanned 30 | (are [given expected] (= expected 31 | (scan-sql given)) 32 | 33 | "abc" ["abc"] 34 | "a=:b" ["a=" :b] 35 | "a=:b;" ["a=" :b ";"] 36 | " a = :b " [" a = " :b " "] 37 | 38 | "a=:b and c=:d" 39 | ["a=" :b " and c=" :d] 40 | 41 | "a=:b and c in(:d);" 42 | ["a=" :b " and c in(" :d ");"] 43 | 44 | )) 45 | 46 | 47 | 48 | (deftest sql-single-line-comments-are-ignored 49 | (are [given expected] 50 | (= expected (scan-sql given)) 51 | 52 | "--abc" [""] 53 | "abc --xyx" ["abc "] 54 | 55 | "abc --xyz \n efg" 56 | ["abc " " efg"] 57 | 58 | )) 59 | 60 | 61 | (deftest sql-multi-line-comments-are-ignored 62 | (are [given expected] 63 | (= expected (scan-sql given)) 64 | 65 | "abc /* xyz */ 123" 66 | ["abc " " 123"] 67 | 68 | )) 69 | -------------------------------------------------------------------------------- /test/taoclj/foundation/json_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.json-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | (deftest read-json-columns 9 | (is (= '({:person {:name "bob"}}) 10 | (qry-> tests-db 11 | (execute "select '{\"name\" : \"bob\"}'::json as person;"))))) 12 | 13 | 14 | (deftest insert-json-columns 15 | (with-open [cnx (.getConnection tests-db)] 16 | (execute cnx "DROP TABLE IF EXISTS insert_json;") 17 | (execute cnx (str "CREATE TABLE insert_json (id serial primary key not null," 18 | " person json not null);"))) 19 | (trx-> tests-db 20 | (insert :insert-json {:person {:name "bob"}})) 21 | 22 | (is (= [{:id 1 :person {:name "bob"}}] 23 | (qry-> tests-db 24 | (execute "SELECT id, person FROM insert_json;")) )) 25 | ) 26 | 27 | 28 | ; waiting on driver support... 29 | ;; (deftest insert-jsonb-columns 30 | ;; (with-open [cnx (.getConnection tests-db)] 31 | ;; (execute cnx "DROP TABLE IF EXISTS insert_jsonb;") 32 | ;; (execute cnx (str "CREATE TABLE insert_jsonb (id serial primary key not null," 33 | ;; " person jsonb not null);"))) 34 | ;; (trx-> tests-db 35 | ;; (insert :insert-jsonb {:person {:name "bob"}})) 36 | 37 | ;; (is (= [{:id 1 :person {:name "bob"}}] 38 | ;; (qry-> tests-db 39 | ;; (execute "SELECT id, person FROM insert_jsonb;")) )) 40 | ;; ) 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/taoclj/foundation/datasources.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.datasources 2 | (:import [com.zaxxer.hikari HikariDataSource] 3 | [com.impossibl.postgres.jdbc PGDataSource])) 4 | 5 | 6 | (defn create-datasource [config] 7 | (doto (PGDataSource.) 8 | (.setHost (:host config)) 9 | (.setPort (:port config)) 10 | (.setDatabase (:database config)) 11 | (.setUser (:username config)) 12 | (.setPassword (:password config)) 13 | ; (.setSSL (or (:ssl config) false)) 14 | )) 15 | 16 | ;; (create-datasource 17 | ;; {:host "localhost" 18 | ;; :port 5432 19 | ;; :database "foundation_tests" 20 | ;; :username "foundation_tests_user" 21 | ;; :password "password" 22 | ;; }) 23 | 24 | 25 | 26 | (defn create-pooled-datasource [config] 27 | (doto (HikariDataSource.) 28 | (.setDataSourceClassName "com.impossibl.postgres.jdbc.PGDataSource") 29 | 30 | (.addDataSourceProperty "Host" (:host config)) 31 | (.addDataSourceProperty "Port" (:port config)) 32 | (.addDataSourceProperty "Database" (:database config)) 33 | (.addDataSourceProperty "User" (:username config)) 34 | (.addDataSourceProperty "Password" (:password config)) 35 | 36 | ;; (.addDataSourceProperty "??" (or (:secure-connection config) false)) 37 | 38 | 39 | (.setConnectionTimeout 5000) 40 | (.setMaximumPoolSize 3) 41 | ;(.addDataSourceProperty "cachePrepStmts", "true"); 42 | ;(.addDataSourceProperty "prepStmtCacheSize", "250"); 43 | ;(.addDataSourceProperty "prepStmtCacheSqlLimit", "2048"); 44 | ;(.addDataSourceProperty "useServerPrepStmts", "true"); 45 | 46 | )) 47 | 48 | -------------------------------------------------------------------------------- /docs/inserting-data.md: -------------------------------------------------------------------------------- 1 | # Inserting Data with DSL 2 | 3 | 4 | 5 | ```clojure 6 | 7 | ; insert single record 8 | (pg/trx-> examples-db 9 | (pg/insert :products {:category-id 1 :name "Product A"})) 10 | 11 | => 1 12 | 13 | 14 | 15 | ; Insert multiple records in a single transaction, as independant statements 16 | (pg/trx-> examples-db 17 | (pg/insert :categories {:name "Category 2"}) 18 | (pg/insert :categories {:name "Category 3"})) 19 | 20 | => (2 3) ; returns generated id's as sequence 21 | 22 | 23 | 24 | ; Insert multiple rows at once, as a single statement in map format. 25 | (pg/trx-> examples-db 26 | (pg/insert :categories [{:name "Category 4"} 27 | {:name "Category 5"}])) 28 | => (4 5) 29 | 30 | 31 | 32 | 33 | ; insert parent and product children using with-rs macro 34 | (pg/trx-> examples-db 35 | (pg/insert :categories {:name "Category 6"}) 36 | (pg/insert :products (pg/with-rs 37 | 38 | ; a sequence of values for insertion 39 | ["Product B" "Product C"] 40 | 41 | ; this is the template to use for each item upon insert 42 | ; rs - implicitly available and is the resultset 43 | ; item - implicitly available name for each value 44 | {:category-id (first rs) 45 | :name item} 46 | 47 | ))) 48 | 49 | => [6 (2 3)] ; returns the generated category id in first element, 50 | ; and sequence of product id's in second. 51 | 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/templated-queries.md: -------------------------------------------------------------------------------- 1 | # Templated Queries 2 | 3 | First write your query in a sql file and place it on your classpath. It may be any type of sql query select/insert/update/delete. 4 | 5 | 6 | ## Create a template such as /path/to/my_query.sql 7 | ```sql 8 | select p.id, p.name, c.name as category_name 9 | from products p 10 | inner join categories c on p.category_id = c.id 11 | where p.category_id = :category-id 12 | ``` 13 | 14 | ## Define the query and reference the template file. 15 | ```clojure 16 | (pg/def-query my-query 17 | {:file "path/to/my_query.sql"}) 18 | 19 | ; now use the templated query 20 | (pg/qry-> examples-db 21 | (my-query {:category-id 6})) 22 | 23 | => ({:id 1 :name "Product A" :category-name "Category 6"} 24 | {:id 2 :name "Product B" :category-name "Category 6"}) 25 | 26 | ``` 27 | 28 | ## You can also specify a result-set transformation function 29 | ```clojure 30 | (pg/def-query my-query2 31 | {:file "path/to/my_query.sql" 32 | :transform (fn [rows] 33 | (map (fn [row] (str (:name row) " - " (:category-name row))) 34 | rows)) }) 35 | 36 | ; now use the templated query with row transformation 37 | (pg/qry-> examples-db 38 | (my-query2 {:category-id 6})) 39 | 40 | => ("Product A - Category 6" 41 | "Product B - Category 6") 42 | ``` 43 | 44 | 45 | 46 | 47 | ## Other 48 | 49 | ```clojure 50 | ; There is also a select a single result with sql template file (may be removed) 51 | def-select1 52 | 53 | ``` 54 | 55 | FYI: each file/query (at present) may contain only 1 statement because it's passed to the database as a JDBC prepared statement, which allows only a single sql statement per JDBC statement. A potential future feature would be to support multiple statements per file by splitting the statements and executing each seperately. 56 | 57 | -------------------------------------------------------------------------------- /src/taoclj/foundation/dsl.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.dsl 2 | (:require [clojure.string :refer [join]] 3 | [taoclj.foundation.naming :refer [to-quoted-db-name]])) 4 | 5 | 6 | (defn to-column-list [columns] 7 | (join "," (map (fn [col] (to-quoted-db-name col)) 8 | columns))) 9 | 10 | 11 | (defn to-values-list [column-count] 12 | (str "(" (join ", " (repeat column-count "?")) ")")) 13 | 14 | 15 | (defn to-insert-values [row-count column-count] 16 | (join "," 17 | (repeat row-count (to-values-list column-count)))) 18 | 19 | 20 | (defn to-where [columns] 21 | (if columns 22 | (str " WHERE " 23 | (join " AND " 24 | (map #(str (to-quoted-db-name %) "=?") 25 | columns))))) 26 | ; (to-where [:id :id2]) 27 | 28 | 29 | (defn to-limit-offset [limit] 30 | (if limit (str " LIMIT " limit))) 31 | 32 | 33 | (defn to-update-set-list [columns] 34 | (str " SET " 35 | (join "," 36 | (map #(str (to-quoted-db-name %) "=?") 37 | columns)))) 38 | ; (to-set-list [:first :last]) 39 | 40 | 41 | (defn to-sql-select [table-name columns where-columns limit] 42 | (str "SELECT " 43 | (if columns (to-column-list columns) "*") 44 | " FROM " 45 | (to-quoted-db-name table-name) 46 | (to-where where-columns) 47 | (to-limit-offset limit))) 48 | ; (to-sql-select :users nil [:id] 1) 49 | ; (to-sql-select :insert-single-record nil nil nil) 50 | 51 | 52 | (defn to-sql-insert [table-name columns row-count] 53 | (str "INSERT INTO " 54 | (to-quoted-db-name table-name) 55 | (str "(" (to-column-list columns) ")") 56 | "VALUES" 57 | (to-insert-values row-count (count columns)))) 58 | 59 | 60 | (defn to-sql-delete [table-name where-columns] 61 | (str "DELETE FROM " 62 | (to-quoted-db-name table-name) 63 | (to-where where-columns))) 64 | ; (to-sql-delete :users [:id :id2]) 65 | 66 | 67 | (defn to-sql-update [table-name columns where-columns] 68 | (str "UPDATE " 69 | (to-quoted-db-name table-name) 70 | (to-update-set-list columns) 71 | (to-where where-columns))) 72 | ; (to-sql-update :users [:first-name :last-name] [:id]) 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/taoclj/foundation/writing.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.writing 2 | (:require [cheshire.core :as cheshire]) 3 | (:import [java.sql Statement PreparedStatement Timestamp])) 4 | 5 | 6 | (defn to-sql-array [statement value] 7 | (let [java-array (into-array value) 8 | value-type (class (first value)) 9 | 10 | array-type (cond (= value-type java.lang.String) 11 | "text" 12 | 13 | (or (= value-type java.lang.Integer) 14 | (= value-type java.lang.Long)) 15 | "integer" 16 | 17 | :default 18 | (throw (Exception. "Array type not supported!"))) 19 | 20 | connection (.getConnection statement)] 21 | (.createArrayOf connection array-type java-array))) 22 | 23 | 24 | 25 | 26 | (defprotocol SqlParam 27 | (set-param [v s i])) 28 | 29 | (extend-protocol SqlParam 30 | 31 | java.lang.String 32 | (set-param [v s i] 33 | (.setString s i v)) 34 | 35 | java.lang.Integer 36 | (set-param [v s i] 37 | (.setInt s i v)) 38 | 39 | java.lang.Long 40 | (set-param [v s i] 41 | (.setLong s i v)) 42 | 43 | java.lang.Boolean 44 | (set-param [v s i] 45 | (.setBoolean s i v)) 46 | 47 | java.time.Instant 48 | (set-param [v s i] 49 | (.setTimestamp s i (Timestamp/from v))) 50 | 51 | clojure.lang.PersistentArrayMap 52 | (set-param [v s i] 53 | (.setObject s i (cheshire/generate-string v))) 54 | 55 | clojure.lang.PersistentVector 56 | (set-param [v s i] 57 | (.setArray s i (to-sql-array s v))) 58 | 59 | clojure.lang.PersistentList 60 | (set-param [v s i] 61 | (.setArray s i (to-sql-array s v))) 62 | 63 | java.lang.Object 64 | (set-param [v s i] 65 | (.setObject s i v)) 66 | 67 | nil 68 | (set-param [v s i] 69 | (.setObject s i nil))) 70 | 71 | 72 | 73 | ; this version handles exploded list parameters 74 | ; perhaps we just take a parsed query map? 75 | (defn set-parameter-values [^Statement statement param-values] 76 | (doall 77 | (map-indexed 78 | (fn [index value] 79 | (set-param value statement (+ 1 index)) ) 80 | 81 | param-values))) 82 | 83 | ; compiled-query 84 | ; {:sql "select * from users where id=? and name in(?,?,?)" 85 | ; :param-values (1 "bob" "joe" "bill")} 86 | 87 | -------------------------------------------------------------------------------- /test/taoclj/foundation/dsl_test.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.dsl-test 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation.dsl :refer :all])) 4 | 5 | 6 | (deftest selects-are-generated 7 | (are [table columns where-columns limit expected] 8 | (= expected (to-sql-select table columns where-columns limit)) 9 | 10 | "users" nil ["id"] nil 11 | "SELECT * FROM \"users\" WHERE \"id\"=?" 12 | 13 | :users [:first-name :last-name] [:id :id2] nil 14 | "SELECT \"first_name\",\"last_name\" FROM \"users\" WHERE \"id\"=? AND \"id2\"=?" 15 | 16 | 17 | "users" nil [:id] 1 18 | "SELECT * FROM \"users\" WHERE \"id\"=? LIMIT 1" 19 | 20 | )) 21 | 22 | 23 | 24 | (deftest inserts-are-generated 25 | (are [table columns row-count expected] 26 | (= expected 27 | (to-sql-insert table columns row-count)) 28 | 29 | "app-users" ["first-name" "last-name"] 1 30 | "INSERT INTO \"app_users\"(\"first_name\",\"last_name\")VALUES(?, ?)" 31 | 32 | "app-users" ["first-name" "last-name"] 2 33 | "INSERT INTO \"app_users\"(\"first_name\",\"last_name\")VALUES(?, ?),(?, ?)" 34 | 35 | :app-users [:first-name :last-name] 2 36 | "INSERT INTO \"app_users\"(\"first_name\",\"last_name\")VALUES(?, ?),(?, ?)" 37 | )) 38 | 39 | 40 | (deftest deletes-are-generated 41 | (are [table where-columns expected] 42 | (= expected 43 | (to-sql-delete table where-columns)) 44 | 45 | "users" ["id"] 46 | "DELETE FROM \"users\" WHERE \"id\"=?" 47 | 48 | :users [:id :id2] 49 | "DELETE FROM \"users\" WHERE \"id\"=? AND \"id2\"=?" 50 | )) 51 | 52 | 53 | 54 | 55 | (deftest updates-are-generated 56 | (are [table columns where-columns expected] 57 | (= expected 58 | (to-sql-update table columns where-columns)) 59 | 60 | :users [:first-name :last-name] [:id] 61 | "UPDATE \"users\" SET \"first_name\"=?,\"last_name\"=? WHERE \"id\"=?" 62 | 63 | "users" ["name"] ["id" "id2"] 64 | "UPDATE \"users\" SET \"name\"=? WHERE \"id\"=? AND \"id2\"=?" 65 | 66 | )) 67 | 68 | 69 | ;; (to-sql-update :users [:name] [:id :id2] 70 | ;; ) 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ; (run-tests *ns*) 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/taoclj/foundation/execution.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.execution 2 | (:require [taoclj.foundation.reading :as reading] 3 | [taoclj.foundation.writing :as writing] 4 | [taoclj.foundation.dsl :as dsl]) 5 | (:import [java.sql Statement])) 6 | 7 | 8 | 9 | (defn execute 10 | "Executes raw, unsafe sql. Uses jdbc statement under the hood so we can 11 | support multiple resultsets, but which can not be parameterized." 12 | ([cnx sql] (execute [] cnx sql)) 13 | ([rs cnx sql] 14 | (let [statement (.createStatement cnx) 15 | has-result (.execute statement sql)] 16 | (conj rs 17 | (if-not has-result ; was a result set returned? 18 | (do (.close statement) true) ; return some metadata?? 19 | (reading/read-resultsets statement nil)))))) 20 | 21 | ;(with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 22 | ; (execute [] cnx "select * from insert_single_record;" ) ) 23 | 24 | 25 | 26 | (defn execute-prepared-query 27 | "Sets parameter values and executes a jdbc prepared statement." 28 | [cnx compiled-query] 29 | 30 | (let [statement (.prepareStatement cnx (:sql compiled-query))] 31 | (writing/set-parameter-values statement 32 | (:param-values compiled-query)) 33 | 34 | (if (.execute statement) 35 | ; .execute returns true there is a result set present, so read it 36 | (reading/read-resultset (.getResultSet statement) nil) 37 | 38 | ; .execute returns false if the first result is an update count 39 | (let [rowcount (.getUpdateCount statement)] 40 | (.close statement) 41 | {:row-count rowcount}) ))) 42 | 43 | 44 | (defn execute-select [rs cnx table-name columns where-equals single?] 45 | (let [where-columns (keys where-equals) 46 | limit (if single? 1 nil) 47 | compiled {:sql (dsl/to-sql-select table-name columns where-columns limit) 48 | :param-values (map where-equals where-columns)}] 49 | (conj rs 50 | (let [result (execute-prepared-query cnx compiled)] 51 | (if single? (first result) result))))) 52 | 53 | 54 | 55 | (defn execute-prepared-insert [cnx table-name data] 56 | (let [column-names (keys data) 57 | sql (dsl/to-sql-insert table-name column-names 1) 58 | statement (.prepareStatement cnx sql (Statement/RETURN_GENERATED_KEYS))] 59 | 60 | (writing/set-parameter-values statement (map data column-names)) 61 | 62 | (let [rowcount (.executeUpdate statement) 63 | generated-keys (.getGeneratedKeys statement) 64 | has-keys (.next generated-keys) 65 | generated-id (.getObject generated-keys 1)] 66 | 67 | (.close statement) 68 | generated-id ) )) 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/taoclj/foundation_test.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation-test 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | (deftest can-connect 9 | (is (= (with-open [cnx (.getConnection tests-db)] 10 | (execute cnx "select 'ehlo' as msg;")) 11 | '(({:msg "ehlo"}))))) 12 | 13 | 14 | (deftest qry->single-statements 15 | (is (= '({:msg "ehlo"}) 16 | (qry-> tests-db 17 | (execute "select 'ehlo' as msg;"))))) 18 | 19 | (deftest trx->single-statements 20 | (is (= '({:msg "ehlo"}) 21 | (trx-> tests-db 22 | (execute "select 'ehlo' as msg;"))))) 23 | 24 | 25 | (deftest qry->multiple-statements 26 | (is (= '[({:msg1 "ehlo1"}) ({:msg2 "ehlo2"})] 27 | (qry-> tests-db 28 | (execute "select 'ehlo1' as msg1;") 29 | (execute "select 'ehlo2' as msg2;"))))) 30 | 31 | (deftest trx->multiple-statements 32 | (is (= '[({:msg3 "ehlo3"}) ({:msg4 "ehlo4"})] 33 | (trx-> tests-db 34 | (execute "select 'ehlo3' as msg3;") 35 | (execute "select 'ehlo4' as msg4;"))))) 36 | 37 | 38 | (deftest qry->no-result 39 | (is (= nil ; nil may be bad choice for result on nothing found. 40 | (qry-> tests-db 41 | (execute "select 'ehlo' where true=false;"))))) 42 | 43 | (deftest trx->no-result 44 | (is (= nil ; nil may be bad choice for result on nothing found. 45 | (trx-> tests-db 46 | (execute "select 'ehlo' where true=false;"))))) 47 | 48 | 49 | (deftest multiple-results-in-single-statement-returned 50 | (is (= '(({:msg1 "ehlo1"}) ({:msg2 "ehlo2"})) 51 | (qry-> tests-db 52 | (execute "select 'ehlo1' as msg1; select 'ehlo2' as msg2;"))))) 53 | 54 | 55 | 56 | ; qry-> returns false on errors 57 | 58 | 59 | 60 | 61 | 62 | ; ********** Select Tests *********************** 63 | 64 | 65 | ; (run-tests *ns*) 66 | 67 | ; (run-tests 'taoclj.foundation-test) 68 | 69 | 70 | ;; (trx-> datasource 71 | ;; (insert :users {:name "Bob" :username "bob" :password "abc123"}) 72 | ;; (insert :user-roles (with-rs 1 {:user-id (first rs) 73 | ;; :role-id item}))) 74 | 75 | 76 | ;; (qry-> tests-db 77 | ;; (execute "SELECT id, name FROM insert_single_record;") 78 | ;; (first-result) 79 | 80 | 81 | ;; {:iso :read-commited} 82 | ;; ) 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ; insert multiple records with same fields 99 | 100 | ; insert multiple records with different fields 101 | 102 | ; error-is-thrown-when-mixing-maps-and-vectors 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/taoclj/foundation/templating.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templating 2 | (:require [taoclj.foundation.execution :as execution] 3 | [taoclj.foundation.templating.loading :refer [load-template]] 4 | [taoclj.foundation.templating.generation :as generation])) 5 | 6 | 7 | (defn extract-section-handlers [options] 8 | (->> options (map (fn [kv] 9 | (if (generation/is-section-name? (first kv)) 10 | kv))) 11 | (remove nil?) 12 | (into {}))) 13 | 14 | 15 | (defn generate-def-query [name options] 16 | (let [queries (gensym "queries") 17 | scanned (gensym "scanned-query") 18 | rs (gensym "rs") 19 | cnx (gensym "cnx") 20 | params (gensym "params") 21 | compiled (gensym "compiled") 22 | sections (gensym "sections") 23 | transform (gensym "transform") 24 | result (gensym "result")] 25 | 26 | `(def ~name 27 | (let [~queries (~load-template ~options) 28 | ~sections (~extract-section-handlers ~options) 29 | ~transform ~(if (:transform options) (:transform options) (fn [r] r))] 30 | 31 | (fn [~rs ~cnx ~params] 32 | (let [~scanned (first ~queries) 33 | ~compiled (generation/compile-query ~scanned ~params ~sections)] 34 | 35 | (conj ~rs 36 | 37 | (let [~result (execution/execute-prepared-query ~cnx ~compiled) ] 38 | 39 | (if (nil? ~result) ~result 40 | (~transform ~result)) 41 | 42 | )))))))) 43 | 44 | 45 | 46 | ; convert name to generate-def-query? 47 | ; compile-query should be statement delimiter(;) 48 | ; aware and parse multiple queries if present 49 | ; convert to use multiple prepared queries 50 | 51 | (defn generate-def-select [name options single?] 52 | (let [queries (gensym "queries") 53 | scanned (gensym "scanned-query") 54 | rs (gensym "rs") 55 | cnx (gensym "cnx") 56 | params (gensym "params") 57 | compiled (gensym "compiled") 58 | sections (gensym "sections") 59 | transform (gensym "transform") 60 | results1 (gensym "results1") 61 | results2 (gensym "results2")] 62 | `(def ~name 63 | (let [~queries (~load-template ~options) 64 | ~sections (~extract-section-handlers ~options) 65 | ~transform ~(if (:transform options) (:transform options) (fn [r] r))] 66 | 67 | (fn [~rs ~cnx ~params] 68 | (let [~scanned (first ~queries) 69 | ~compiled (generation/compile-query ~scanned ~params ~sections)] 70 | (conj ~rs 71 | 72 | (let [~results1 (execution/execute-prepared-query ~cnx ~compiled) 73 | ~results2 ~(if single? `(first ~results1) results1) ] 74 | 75 | (if (nil? ~results2) ~results2 76 | (~transform ~results2)) 77 | 78 | )))))))) 79 | ; (generate-def-select 'select-session {} true) 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/taoclj/foundation/reading.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.reading 2 | (:require [taoclj.foundation.naming :refer [from-db-name]] 3 | [cheshire.core :as cheshire]) 4 | (:import [java.sql Types])) 5 | 6 | 7 | ; *** result set readers ********************* 8 | 9 | 10 | 11 | ; array conversions 12 | (def type-int-array (Class/forName "[I")) 13 | (def type-string-array (Class/forName "[Ljava.lang.String;")) 14 | 15 | 16 | ; left off need to add support for all array types 17 | ; also get type only once, use let block 18 | (defn is-array? [item] 19 | (or (= type-string-array (type item)))) 20 | 21 | 22 | (defn convert-array [array] 23 | (let [top-level-list (seq (.getArray array))] 24 | ; top-level-list 25 | (map (fn [item] 26 | (if (is-array? item) 27 | (seq item) ; convert sub-arrays 28 | item)) 29 | top-level-list) )) 30 | 31 | 32 | (defn convert-from-db [rsmeta result-set index] 33 | (let [ct (.getColumnType rsmeta index) 34 | ctn (.getColumnTypeName rsmeta index)] 35 | 36 | (cond (= ct Types/TIMESTAMP) 37 | (if-let [ts (.getTimestamp result-set index)] 38 | (.toInstant ts)) 39 | 40 | (= ct Types/ARRAY) 41 | (if-let [a (.getArray result-set index)] 42 | (convert-array a)) 43 | 44 | (= ctn "json") 45 | (if-let [json (.getObject result-set index)] 46 | (cheshire/parse-string json (fn [k] (keyword k)))) 47 | 48 | :default 49 | (.getObject result-set index)))) 50 | 51 | 52 | (defn read-resultset 53 | ([^java.sql.ResultSet rs] (read-resultset rs nil)) 54 | ([^java.sql.ResultSet rs result-format] 55 | 56 | (let [rsmeta (.getMetaData rs) 57 | idxs (range 1 (inc (.getColumnCount rsmeta))) 58 | 59 | columns (map from-db-name 60 | (map #(.getColumnLabel rsmeta %) idxs) ) 61 | 62 | dups (or (apply distinct? columns) 63 | (throw (Exception. "ResultSet must have unique column names"))) 64 | 65 | ; break out function for perf 66 | get-row-vals (fn [] (map (fn [^Integer i] 67 | (convert-from-db rsmeta rs i)) 68 | idxs)) 69 | 70 | ; break out function for perf 71 | read-rows (fn readrow [] 72 | (when (.next rs) 73 | (if (= result-format :rows) 74 | (cons (vec (get-row-vals)) (readrow)) 75 | (cons (zipmap columns (get-row-vals)) (readrow)))))] 76 | 77 | 78 | (if (= result-format :rows) 79 | (cons (vec columns) (read-rows)) 80 | (read-rows)) ))) 81 | 82 | 83 | (defn read-resultsets [^java.sql.Statement statement result-format] 84 | (let [read-sets (fn readrs [] 85 | (let [rs (.getResultSet statement)] 86 | (cons (read-resultset rs result-format) 87 | (if (.getMoreResults statement) 88 | (readrs))))) 89 | results (read-sets)] 90 | (if (= 1 (count results)) 91 | (first results) 92 | results ))) 93 | -------------------------------------------------------------------------------- /test/taoclj/foundation/select_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.select-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | (deftest select1-record 9 | 10 | (with-open [cnx (.getConnection tests-db)] 11 | (execute cnx "DROP TABLE IF EXISTS select1_record;") 12 | (execute cnx "CREATE TABLE select1_record (id serial primary key not null, name text);") 13 | (execute cnx "INSERT INTO select1_record (name) VALUES('bob');")) 14 | 15 | 16 | (is (= {:id 1 :name "bob"} 17 | (qry-> tests-db 18 | (select1 :select1-record {:id 1})))) 19 | 20 | (is (= nil 21 | (qry-> tests-db 22 | (select1 :select1-record {:id 2})))) 23 | 24 | ) 25 | 26 | 27 | 28 | (deftest select-records 29 | 30 | (with-open [cnx (.getConnection tests-db)] 31 | (execute cnx "DROP TABLE IF EXISTS select_records;") 32 | (execute cnx "CREATE TABLE select_records (id serial primary key not null, name text);") 33 | (execute cnx "INSERT INTO select_records (name) VALUES ('bob'),('bill');")) 34 | 35 | (is (= [{:id 1 :name "bob"} {:id 2 :name "bill"}] 36 | (qry-> tests-db 37 | (select :select-records {})))) 38 | 39 | (is (= nil 40 | (qry-> tests-db 41 | (select :select-records {:id 3})))) 42 | 43 | ) 44 | 45 | 46 | (deftest select-records-with-nulls 47 | (with-open [cnx (.getConnection tests-db)] 48 | (execute cnx "DROP TABLE IF EXISTS select_records_with_nulls;") 49 | (execute cnx "CREATE TABLE select_records_with_nulls (id serial primary key not null, 50 | i integer, t text, tsz timestamptz, b boolean);") 51 | (execute cnx "INSERT INTO select_records_with_nulls (i,t,tsz,b) VALUES (null,null,null,null);")) 52 | 53 | (is (= [{:id 1 :i nil :t nil :tsz nil :b nil}] 54 | (qry-> tests-db 55 | (select :select-records-with-nulls {}))))) 56 | 57 | 58 | 59 | 60 | (deftest select-arrays 61 | 62 | (with-open [cnx (.getConnection tests-db)] 63 | (execute cnx "DROP TABLE IF EXISTS select_arrays;") 64 | (execute cnx "CREATE TABLE select_arrays (id serial primary key not null, names text[], numbers integer[]);") 65 | (execute cnx (str "INSERT INTO select_arrays (names,numbers) VALUES " 66 | "('{\"bob\", \"bill\"}','{101, 202}');"))) 67 | 68 | (is (= {:id 1 :names ["bob" "bill"] :numbers [101 202]} 69 | (qry-> tests-db 70 | (select1 :select-arrays {:id 1})))) 71 | 72 | ) 73 | 74 | 75 | 76 | (deftest select-arrays-2dimensions 77 | 78 | (with-open [cnx (.getConnection tests-db)] 79 | (execute cnx "DROP TABLE IF EXISTS select_arrays_2dimensions;") 80 | (execute cnx "CREATE TABLE select_arrays_2dimensions (id serial primary key not null, letters text[][]);") 81 | (execute cnx (str "INSERT INTO select_arrays_2dimensions (letters) VALUES " 82 | "('{{\"a\", \"b\", \"c\"}, {\"x\", \"y\", \"z\"}}');"))) 83 | 84 | (def-query select-arrays-2dimensions-query 85 | {:file "taoclj/foundation/sql/select_arrays_2dimensions.sql"}) 86 | 87 | (is (= '({:id 1 :letters (("b" "c") ("y" "z"))}) 88 | (qry-> tests-db 89 | (select-arrays-2dimensions-query {})))) 90 | 91 | ) 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/taoclj/foundation/templating/parsing.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templating.parsing) 2 | 3 | 4 | (defn is-newline? [c] 5 | (= c \newline)) 6 | 7 | 8 | (defn param-character? 9 | "Checks if a character is allowed in a param." 10 | [c] 11 | (or (= c \-) 12 | (= c \_) 13 | (let [n (int c)] 14 | (or (and (>= n 48) (<= n 57)) 15 | (and (>= n 65) (<= n 122)) 16 | (= n 47) ; forward slash 17 | )))) 18 | ; (param-character? \/) 19 | 20 | 21 | 22 | (declare scan-sql) 23 | 24 | 25 | (defn scan-param [input] 26 | (loop [raw input buf ""] 27 | (if-let [current-char (first raw)] 28 | (cond (not (param-character? current-char)) ; we are changing context 29 | (concat (list (keyword buf)) 30 | (scan-sql raw)) 31 | :else 32 | (recur (rest raw) 33 | (str buf current-char))) 34 | [(keyword buf)]))) 35 | 36 | 37 | 38 | (defn scan-single-line-comment [input] 39 | (loop [raw input] 40 | (if-let [current-char (first raw)] 41 | 42 | (if (is-newline? current-char) ; we are changing context 43 | (scan-sql (rest raw)) 44 | (recur (rest raw)) 45 | 46 | )))) 47 | 48 | ;; (scan-single-line-comment (seq "--abc \n asdf")) 49 | 50 | 51 | (defn scan-multi-line-comment [input] 52 | (loop [raw input] 53 | (let [current-char (first raw) 54 | next-char (second raw)] 55 | (if current-char 56 | (cond 57 | ; is this the begining of a nested comment block? 58 | ;; TODO: support multiple levels of nesting 59 | ;; (= current-char \/) (= next-char \*) 60 | 61 | 62 | ; are we encountering the end of the current multi-line-comment block? 63 | (and (= current-char \*) (= next-char \/)) 64 | (scan-sql (drop 2 raw)) 65 | 66 | :else (recur (rest raw)) ))))) 67 | ;; (scan-multi-line-comment (seq "/*abc */ xyz")) 68 | 69 | 70 | 71 | (defn scan-sql [input] 72 | ; could we memoize/cache this here? right level? 73 | (loop [raw (seq input) buf "" prior-char nil] 74 | 75 | (let [current-char (first raw) 76 | next-char (second raw)] 77 | 78 | (if current-char 79 | (cond 80 | ; we are changing context to read keyword 81 | (and (= current-char \:) 82 | (not= prior-char \:) 83 | (not= next-char \:) 84 | (re-matches #"[a-zA-Z]" (str next-char))) 85 | 86 | (concat (list buf) 87 | (scan-param (rest raw))) 88 | 89 | 90 | ; we are changing context to read single line comment 91 | (and (= current-char \-) (= next-char \-)) 92 | (concat (list buf) 93 | (scan-single-line-comment (rest raw))) 94 | 95 | 96 | ; are we encountering a muli-line comment? 97 | (and (= current-char \/) (= next-char \*)) 98 | (concat (list buf) 99 | (scan-multi-line-comment (drop 2 raw))) 100 | 101 | :else 102 | (recur (rest raw) 103 | (str buf current-char) 104 | current-char)) 105 | 106 | [buf] )))) 107 | 108 | 109 | ;; (scan-sql "abc /* xyz */ 123") 110 | 111 | ;; (scan-sql "select * from users -- where id=:id and name in(:names); 112 | ;; select * from something;" 113 | ;; ) 114 | 115 | ;; (scan-sql "select * from customers where id=:id :section/myorder" 116 | ;; ) 117 | 118 | ; (scan-sql "select '{\"name\":\"bob\"}'::json as person;") 119 | ; (scan-sql "::int") 120 | ; (scan-sql "select '123'::int as num;") 121 | ; (scan-sql "id=:a and name=:b;") 122 | ; (scan-sql "select * from users where id=:id and name in(:names);") 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /test/taoclj/foundation/update_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.update-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | 9 | (deftest update-records 10 | 11 | (with-open [cnx (.getConnection tests-db)] 12 | (execute cnx "DROP TABLE IF EXISTS update_records;") 13 | (execute cnx "CREATE TABLE update_records (id serial primary key not null, name text);") 14 | (execute cnx "INSERT INTO update_records (name) values ('bob'),('bill');")) 15 | 16 | (trx-> tests-db 17 | (update :update-records {:name "joe"} {:id 2})) 18 | 19 | (is (= [{:id 2 :name "joe"}] 20 | (qry-> tests-db 21 | (execute "SELECT id, name FROM update_records where id=2;")))) 22 | ) 23 | 24 | 25 | (deftest update-nulls 26 | (with-open [cnx (.getConnection tests-db)] 27 | (execute cnx "DROP TABLE IF EXISTS update_nulls;") 28 | (execute cnx (str "CREATE TABLE update_nulls (id serial primary key not null, " 29 | " t text, i integer, b boolean, tz timestamptz);")) 30 | (execute cnx "INSERT INTO update_nulls (t,i,b,tz) values ('a',1,true,'2016-02-05');")) 31 | 32 | (trx-> tests-db 33 | (update :update-nulls {:t nil :i nil :b nil :tz nil} {:id 1})) 34 | 35 | (is (= [{:id 1 :t nil :i nil :b nil :tz nil}] 36 | (qry-> tests-db 37 | (execute "SELECT * FROM update_nulls where id=1;")))) 38 | ) 39 | 40 | 41 | (deftest insert-nulls 42 | 43 | (with-open [cnx (.getConnection tests-db)] 44 | (execute cnx "DROP TABLE IF EXISTS insert_nulls;") 45 | (execute cnx (str "CREATE TABLE insert_nulls (id serial primary key not null, " 46 | " t text, i integer, b boolean, tz timestamptz);"))) 47 | 48 | (trx-> tests-db 49 | (insert :insert-nulls {:t nil :i nil :b nil :tz nil})) 50 | 51 | (is (= [{:id 1 :t nil :i nil :b nil :tz nil}] 52 | (qry-> tests-db 53 | (execute "SELECT * FROM insert_nulls;")) )) 54 | 55 | ) 56 | 57 | 58 | (deftest update-records-of-all-types 59 | (with-open [cnx (.getConnection tests-db)] 60 | (execute cnx "DROP TABLE IF EXISTS update_records_of_all_types;") 61 | (execute cnx "CREATE TABLE update_records_of_all_types (id serial primary key not null, 62 | i integer, b boolean, t text, tsz timestamptz);") 63 | 64 | (execute cnx "INSERT INTO update_records_of_all_types (i,b,t,tsz) 65 | values (null,null,null,null);")) 66 | 67 | (let [now (java.time.Instant/now)] 68 | (trx-> tests-db 69 | (update :update-records-of-all-types 70 | {:i 101 :b true :t "abc" :tsz now} 71 | {:id 1})) 72 | 73 | (is (= [{:id 1 :i 101 :b true :t "abc" :tsz now}] 74 | (qry-> tests-db 75 | (execute "SELECT id,i,b,t,tsz FROM update_records_of_all_types where id=1;"))))) 76 | ) 77 | 78 | 79 | 80 | 81 | 82 | (deftest update-records-arrays 83 | 84 | (with-open [cnx (.getConnection tests-db)] 85 | (execute cnx "DROP TABLE IF EXISTS update_records_arrays;") 86 | (execute cnx (str "CREATE TABLE update_records_arrays (id serial primary key not null," 87 | " names text[] not null, numbers integer[] not null);")) 88 | (execute cnx (str "INSERT INTO update_records_arrays (names,numbers)" 89 | " values ('{\"bob\",\"bill\"}','{101,102}');")) 90 | ) 91 | 92 | (trx-> tests-db 93 | (update :update-records-arrays 94 | {:names ["john" "jane"] :numbers [505 606]} 95 | {:id 1} )) 96 | 97 | (is (= [{:id 1 :names ["john" "jane"] :numbers [505 606]}] 98 | (qry-> tests-db 99 | (execute "SELECT id, names, numbers FROM update_records_arrays;")) )) 100 | 101 | ) 102 | -------------------------------------------------------------------------------- /test/taoclj/foundation/templated_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templated-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | 9 | (deftest templated-select1-record 10 | 11 | (with-open [cnx (.getConnection tests-db)] 12 | (execute cnx "DROP TABLE IF EXISTS templated_select1_record;") 13 | (execute cnx "CREATE TABLE templated_select1_record (id serial primary key not null, name text);") 14 | (execute cnx "INSERT INTO templated_select1_record (name) VALUES ('bob');")) 15 | 16 | 17 | (def-select1 templated-select1-record-query 18 | {:file "taoclj/foundation/sql/templated_select1_record.sql"}) 19 | 20 | 21 | (is (= {:id 1 :name "bob"} 22 | (qry-> tests-db 23 | (templated-select1-record-query {:id 1})))) 24 | 25 | (is (= nil 26 | (qry-> tests-db 27 | (templated-select1-record-query {:id 2})))) 28 | 29 | ) 30 | 31 | 32 | 33 | (deftest templated-query-selects-return-expected 34 | 35 | (with-open [cnx (.getConnection tests-db)] 36 | (execute cnx "DROP TABLE IF EXISTS templated_query_selects;") 37 | (execute cnx "CREATE TABLE templated_query_selects (id serial primary key not null, name text);") 38 | (execute cnx "INSERT INTO templated_query_selects (name) VALUES ('bob'),('bill');")) 39 | 40 | 41 | (def-query templated-select-query 42 | {:file "taoclj/foundation/sql/templated_select_query.sql"}) 43 | 44 | 45 | (is (= [{:id 1 :name "bob"}] 46 | (qry-> tests-db 47 | (templated-select-query {:ids [1]})))) 48 | 49 | (is (= [{:id 1 :name "bob"} {:id 2 :name "bill"}] 50 | (qry-> tests-db 51 | (templated-select-query {:ids [1 2]})))) 52 | 53 | (is (= nil 54 | (qry-> tests-db 55 | (templated-select-query {:ids [3]})))) 56 | 57 | ) 58 | 59 | 60 | (deftest templated-query-inserts-return-expected 61 | 62 | (with-open [cnx (.getConnection tests-db)] 63 | (execute cnx "DROP TABLE IF EXISTS templated_query_inserts;") 64 | (execute cnx "CREATE TABLE templated_query_inserts (id serial primary key not null, name text);")) 65 | 66 | (def-query templated-insert-query 67 | {:file "taoclj/foundation/sql/templated_insert_query.sql"}) 68 | 69 | (is (= {:row-count 2} 70 | (qry-> tests-db 71 | (templated-insert-query {})))) 72 | 73 | ) 74 | 75 | 76 | (deftest templated-transform-returns 77 | 78 | (with-open [cnx (.getConnection tests-db)] 79 | (execute cnx "DROP TABLE IF EXISTS templated_transform;") 80 | (execute cnx "CREATE TABLE templated_transform (id serial primary key not null, name text);") 81 | (execute cnx "INSERT INTO templated_transform (name) VALUES ('bob'),('bill');")) 82 | 83 | 84 | (def-query templated-transform 85 | {:file "taoclj/foundation/sql/templated_transform.sql" 86 | :transform #(map :name %)}) 87 | 88 | (is (= ["bob" "bill"] 89 | (qry-> tests-db 90 | (templated-transform {})))) 91 | 92 | ) 93 | 94 | 95 | 96 | (deftest templated-sections-are-handled 97 | 98 | (with-open [cnx (.getConnection tests-db)] 99 | (execute cnx "DROP TABLE IF EXISTS templated_sections;") 100 | (execute cnx "CREATE TABLE templated_sections (id serial primary key not null, name text);") 101 | (execute cnx "INSERT INTO templated_sections (name) VALUES ('bob'),('bill');")) 102 | 103 | (def-query templated-sections-query 104 | {:file "taoclj/foundation/sql/templated_sections.sql" 105 | :section/filters (fn [params] 106 | (if (:name params) "name=:name"))}) 107 | 108 | (is (= '({:id 1 :name "bob"}) 109 | (qry-> tests-db 110 | (templated-sections-query {:name "bob"})))) 111 | 112 | ) 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/taoclj/foundation/templating/generation.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.templating.generation 2 | (:require [clojure.string :refer [join]] 3 | [taoclj.foundation.templating.parsing :as parsing])) 4 | 5 | 6 | (defn to-placeholder-query 7 | [scanned-query params-info] 8 | (apply str 9 | (map (fn [tok] 10 | (if-not (keyword? tok) tok 11 | (let [n (tok params-info)] 12 | (if-not n "?" 13 | (join "," (repeat n "?")))))) 14 | scanned-query))) 15 | 16 | ; (to-placeholder-query ["select * from users where id=" :id] 17 | ; {:id 3} ) 18 | 19 | 20 | (def list-param? (some-fn list? vector? seq?)) 21 | 22 | (defn is-section-name? [candidate] 23 | (and (keyword? candidate) 24 | (= "section" (namespace candidate)))) 25 | 26 | (defn call-section-handler [section-name section-handlers params] 27 | (let [handler (section-name section-handlers)] 28 | (if-not handler 29 | (throw (Exception. (str "Unable to locate section handler for " section-name "!"))) 30 | (handler params)))) 31 | 32 | (defn prepare-query [scanned-query params section-handlers] 33 | (->> scanned-query 34 | (map (fn [element] 35 | (if-not (is-section-name? element) element 36 | (-> (call-section-handler element section-handlers params) 37 | (parsing/scan-sql))))) 38 | (flatten))) 39 | 40 | ;; (prepare-query 41 | ;; ["select * from customers where id=" :id " " :section/filters] 42 | ;; {:id 1} 43 | ;; {:section/filters (fn [params] "and category_id = :category-id")} 44 | ;; ) 45 | 46 | 47 | (defn compile-query [scanned-query params section-handlers] 48 | (let [prepared-query (prepare-query scanned-query params section-handlers) 49 | param-names (filter keyword? prepared-query) 50 | param-info (reduce (fn [info key] 51 | (let [val (params key)] 52 | (if (list-param? val) 53 | (assoc info key (count val))))) 54 | {} 55 | (keys params))] 56 | 57 | {:sql (to-placeholder-query prepared-query param-info) ; memoize eventually 58 | :param-values (flatten (map params param-names))})) 59 | 60 | 61 | ;; (compile-query ["select * from customers where id=" :id " " :section/filters] 62 | ;; {:id 1 :category-id 222} 63 | ;; {:section/filters (fn [params] "and category_id=:category-id")} 64 | ;; ) 65 | 66 | 67 | 68 | ;; ; a dynamic section function is defined. 69 | ;; (def-query myquery 70 | ;; {:file "taoclj/foundation/sql/myquery.sql" 71 | ;; :section/myorder (fn [params] "order by :name desc")}) 72 | 73 | 74 | ;; ; raw query in the file looks like this 75 | ;; "select * from customers where id=:id :section/myorder" 76 | 77 | 78 | ;; ; pre-call time scan generates this 79 | ;; ["select * from customers where id=" :id " " :section/myorder] 80 | 81 | 82 | ;; ; call time calls section function with parameters and returns string 83 | ;; ; * throws exeption if section not found 84 | ;; :section/myorder => "order by :name desc" 85 | 86 | ;; ; the string is then scanned into standard structure 87 | ;; ["order by " :name " desc"] 88 | 89 | 90 | ;; ; the dynamic scanned structure is spliced into main query structure 91 | ;; ["select * from customers where id=" :id "order by " :name " desc"] 92 | 93 | 94 | ;; ; from there we can compile as usual 95 | 96 | 97 | 98 | ;; (compile-query ["select * from customers order by " :section/myorder] 99 | ;; {:id 1} 100 | ;; ) 101 | 102 | 103 | ;; (compile-query ["select * from users where id=" :id " and name in(" :names ")"] 104 | ;; {:id 1 :names ["bob" "joe" "bill"]} 105 | ;; ) 106 | 107 | 108 | ; {:sql "select * from users where id=? and name in(?,?,?)" 109 | ; :param-values (1 "bob" "joe" "bill")} 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /test/taoclj/foundation/naming_test.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.naming-test 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer [trx-> qry-> insert select1]] 4 | [taoclj.foundation.naming :refer :all] 5 | [taoclj.foundation.tests-config :refer [tests-db]] 6 | [taoclj.foundation.execution :refer [execute]]) 7 | (:import [java.time Instant] 8 | [java.sql Types])) 9 | 10 | 11 | (deftest names-are-translated-from-db-representation 12 | (are [given expected] (= expected 13 | (from-db-name given)) 14 | "aaa" :aaa 15 | "000" :000 16 | "999" :999 17 | "zzz_zzz" :zzz-zzz 18 | "aa;a" :aaa 19 | "AbC" :AbC 20 | )) 21 | 22 | (deftest names-are-translated-to-db-representation 23 | (are [given expected] (= expected 24 | (to-quoted-db-name given)) 25 | "aaa" "\"aaa\"" 26 | :aaa "\"aaa\"" 27 | "000" "\"000\"" 28 | :999 "\"999\"" 29 | "zzz-zzz" "\"zzz_zzz\"" 30 | :zzz-zzz "\"zzz_zzz\"" 31 | "aa;a" "\"aaa\"" )) 32 | 33 | 34 | 35 | 36 | (deftest string-types-are-round-tripped 37 | 38 | (with-open [cnx (.getConnection tests-db)] 39 | (execute cnx "DROP TABLE IF EXISTS string_types_are_round_tripped;") 40 | (execute cnx "CREATE TABLE string_types_are_round_tripped (id serial primary key not null, first_name text);")) 41 | 42 | (trx-> tests-db 43 | (insert :string-types-are-round-tripped {:first-name "bob"})) 44 | 45 | (let [result (qry-> tests-db 46 | (select1 :string-types-are-round-tripped {:id 1}))] 47 | 48 | (is (= java.lang.String 49 | (-> result :first-name class))) 50 | 51 | )) 52 | 53 | 54 | 55 | 56 | (deftest integer-types-are-round-tripped 57 | 58 | (with-open [cnx (.getConnection tests-db)] 59 | (execute cnx "DROP TABLE IF EXISTS integer_types_are_round_tripped;") 60 | (execute cnx (str "CREATE TABLE integer_types_are_round_tripped (id serial primary key not null," 61 | "ex1 smallint, ex2 integer, ex3 bigint);"))) 62 | 63 | (trx-> tests-db 64 | (insert :integer-types-are-round-tripped {:ex1 111 65 | :ex2 222 66 | :ex3 333 })) 67 | 68 | (let [result (qry-> tests-db 69 | (select1 :integer-types-are-round-tripped {:id 1}))] 70 | 71 | (is (= java.lang.Integer (-> result :id class))) 72 | (is (= java.lang.Short (-> result :ex1 class))) 73 | (is (= java.lang.Integer (-> result :ex2 class))) 74 | (is (= java.lang.Long (-> result :ex3 class))))) 75 | 76 | 77 | 78 | (deftest datetime-types-are-round-tripped 79 | 80 | (with-open [cnx (.getConnection tests-db)] 81 | (execute cnx "DROP TABLE IF EXISTS datetimes_are_round_tripped;") 82 | (execute cnx (str "CREATE TABLE datetimes_are_round_tripped (id serial primary key not null," 83 | "ex1 timestamptz);"))) 84 | 85 | (trx-> tests-db 86 | (insert :datetimes-are-round-tripped {:ex1 (Instant/now)})) 87 | 88 | (let [result (qry-> tests-db 89 | (select1 :datetimes-are-round-tripped {:id 1}))] 90 | 91 | (is (= java.time.Instant (-> result :ex1 class))) 92 | 93 | )) 94 | 95 | 96 | 97 | ; (qry-> tests-db 98 | ; (select1 :datetimes-are-round-tripped {:id 1}) 99 | ;) 100 | 101 | 102 | (deftest uuid-types-are-round-tripped 103 | 104 | (with-open [cnx (.getConnection tests-db)] 105 | (execute cnx "DROP TABLE IF EXISTS uuids_are_round_tripped;") 106 | (execute cnx (str "CREATE TABLE uuids_are_round_tripped (id serial primary key not null," 107 | "ex1 uuid);"))) 108 | 109 | (trx-> tests-db 110 | (insert :uuids-are-round-tripped {:ex1 (java.util.UUID/randomUUID)})) 111 | 112 | (let [result (qry-> tests-db 113 | (select1 :uuids-are-round-tripped {:id 1}))] 114 | 115 | (is (= java.util.UUID (-> result :ex1 class))) 116 | 117 | )) 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | ; (run-tests 'taoclj.foundation.naming-test) 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /test/taoclj/foundation/insert_tests.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation.insert-tests 2 | (:require [clojure.test :refer :all] 3 | [taoclj.foundation :refer :all] 4 | [taoclj.foundation.tests-config :refer [tests-db]] 5 | [taoclj.foundation.execution :refer [execute]])) 6 | 7 | 8 | (deftest insert-records 9 | 10 | (with-open [cnx (.getConnection tests-db)] 11 | (execute cnx "DROP TABLE IF EXISTS insert_records;") 12 | (execute cnx "CREATE TABLE insert_records (id serial primary key not null, name text);")) 13 | 14 | (trx-> tests-db 15 | (insert :insert-records {:name "bob"})) 16 | 17 | (is (= [{:id 1 :name "bob"}] 18 | (qry-> tests-db 19 | (execute "SELECT id, name FROM insert_records;")) )) 20 | 21 | ) 22 | 23 | 24 | (deftest insert-nulls 25 | 26 | (with-open [cnx (.getConnection tests-db)] 27 | (execute cnx "DROP TABLE IF EXISTS insert_nulls;") 28 | (execute cnx (str "CREATE TABLE insert_nulls (id serial primary key not null, " 29 | " t text, i integer, b boolean, tz timestamptz);"))) 30 | 31 | (trx-> tests-db 32 | (insert :insert-nulls {:t nil :i nil :b nil :tz nil})) 33 | 34 | (is (= [{:id 1 :t nil :i nil :b nil :tz nil}] 35 | (qry-> tests-db 36 | (execute "SELECT * FROM insert_nulls;")) )) 37 | 38 | ) 39 | 40 | 41 | (deftest insert-records-arrays 42 | 43 | (with-open [cnx (.getConnection tests-db)] 44 | (execute cnx "DROP TABLE IF EXISTS insert_records_arrays;") 45 | (execute cnx (str "CREATE TABLE insert_records_arrays (id serial primary key not null," 46 | " names text[] not null, numbers integer[] not null);"))) 47 | 48 | (trx-> tests-db 49 | (insert :insert-records-arrays 50 | {:names ["bob" "bill"] :numbers [101 202]})) 51 | 52 | (is (= [{:id 1 :names ["bob" "bill"] :numbers [101 202]}] 53 | (qry-> tests-db 54 | (execute "SELECT id, names, numbers FROM insert_records_arrays;")) )) 55 | 56 | ) 57 | 58 | 59 | 60 | 61 | (deftest insert-multiple-records 62 | 63 | (with-open [cnx (.getConnection tests-db)] 64 | (execute cnx "DROP TABLE IF EXISTS insert_multiple_records;") 65 | (execute cnx "CREATE TABLE insert_multiple_records (id serial primary key not null, name text);")) 66 | 67 | (trx-> tests-db 68 | (insert :insert-multiple-records [{:name "bob"} {:name "bill"}])) 69 | 70 | (is (= [{:id 1 :name "bob"} {:id 2 :name "bill"}] 71 | (qry-> tests-db 72 | (execute "SELECT id, name FROM insert_multiple_records;")) )) 73 | ) 74 | 75 | 76 | 77 | (deftest insert-parent-child-with-rs 78 | 79 | (with-open [cnx (.getConnection tests-db)] 80 | (execute cnx "DROP TABLE IF EXISTS parent_records; DROP TABLE IF EXISTS child_records;") 81 | (execute cnx "CREATE TABLE parent_records (id serial primary key not null, name text);") 82 | (execute cnx "CREATE TABLE child_records (parent_id int not null, related_id int not null);")) 83 | 84 | (trx-> tests-db 85 | (insert :parent-records {:name "bob"}) 86 | (insert :child-records (with-rs 22 {:parent-id (first rs) 87 | :related-id item}))) 88 | 89 | (is (= [ [{:id 1 :name "bob"}] [{:parent-id 1 :related-id 22}] ] 90 | (qry-> tests-db 91 | (execute "SELECT id, name FROM parent_records;") 92 | (execute "SELECT parent_id, related_id FROM child_records;")))) 93 | 94 | ) 95 | 96 | 97 | 98 | (deftest insert-parent-child-with-rs-multiple-records 99 | 100 | (with-open [cnx (.getConnection tests-db)] 101 | (execute cnx "DROP TABLE IF EXISTS parent_records; DROP TABLE IF EXISTS child_records;") 102 | (execute cnx "CREATE TABLE parent_records (id serial primary key not null, name text);") 103 | (execute cnx "CREATE TABLE child_records (parent_id int not null, related_id int not null);")) 104 | 105 | (trx-> tests-db 106 | (insert :parent-records {:name "bob"}) 107 | (insert :child-records (with-rs [22 33] {:parent-id (first rs) 108 | :related-id item}))) 109 | 110 | (is (= [ [{:id 1 :name "bob"}] [{:parent-id 1 :related-id 22} 111 | {:parent-id 1 :related-id 33}] ] 112 | (qry-> tests-db 113 | (execute "SELECT id, name FROM parent_records;") 114 | (execute "SELECT parent_id, related_id FROM child_records;")))) 115 | 116 | ) 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundation/PG 2 | 3 | A complete toolkit for talking to Postgres. A work in progress, but usable. 4 | 5 | ## Key Features & Goals 6 | - Simple to run query sets and transaction sets. 7 | - SQL templating similar to yesql 8 | - Parameters passed as PreparedStatement parameters 9 | - A way to add dynamic query sections 10 | - Automatic conversion of datetime datatypes 11 | - Integer and text array support 12 | - JSON support (partial/pending) 13 | - Multiple result set support 14 | - Automatic conversion from dash to underscore and back 15 | - Keyword case unchanged, rather than automatically converted to lower case. 16 | - Connection pooling with the excellent [HikariCP](http://brettwooldridge.github.io/HikariCP/) 17 | 18 | 19 | 20 | ## Installation 21 | 22 | Add this to your [Leiningen](https://github.com/technomancy/leiningen) `:dependencies` 23 | 24 | [![Clojars Project](http://clojars.org/org.taoclj/foundation/latest-version.svg)](http://clojars.org/org.taoclj/foundation) 25 | 26 | 27 | 28 | ## Quick Start 29 | ```clojure 30 | 31 | (require '[taoclj.foundation :as pg]) 32 | 33 | 34 | ;; for the sql structure see /resources/examples.sql 35 | 36 | 37 | ; define the datasource with your database details 38 | (pg/def-datasource examples-db {:host "localhost" 39 | :port 5432 40 | :database "examples_db" 41 | :username "examples_app" 42 | :password "password" 43 | :pooled false }) 44 | 45 | 46 | 47 | ; insert a single record, returns the database generated key 48 | (pg/trx-> examples-db 49 | (pg/insert :categories {:name "Category A"})) 50 | 51 | => 1 52 | 53 | 54 | ; select a single record, returns a map representing the row 55 | (pg/qry-> examples-db 56 | (pg/select1 :categories {:id 1})) 57 | 58 | => {:id 1, :name "Category A"} 59 | 60 | 61 | ; insert a multiple records, returns the generated keys as sequence 62 | (pg/trx-> examples-db 63 | (pg/insert :categories [{:name "Category B"} 64 | {:name "Category C"}])) 65 | => (2 3) 66 | 67 | 68 | ; select multiple records, returns a sequence 69 | (pg/qry-> examples-db 70 | (pg/select :categories {})) 71 | 72 | => ({:id 1, :name "Category A"} 73 | {:id 2, :name "Category B"} 74 | {:id 3, :name "Category C"}) 75 | 76 | 77 | 78 | ; insert a new category and 2 child products, returns generated keys 79 | (pg/trx-> examples-db 80 | (pg/insert :categories {:name "Category D"}) 81 | (pg/insert :products (pg/with-rs 82 | 83 | ; a sequence of values for insertion 84 | ["Product D1" "Product D2"] 85 | 86 | ; this is the template to use for each item upon insert 87 | ; rs - implicitly available and is the resultset 88 | ; item - implicitly available name for each value 89 | {:category-id (first rs) 90 | :name item} 91 | 92 | ))) 93 | 94 | => [4 (1 2)] 95 | 96 | ``` 97 | 98 | ```sql 99 | -- write more complex queries in sql template files 100 | 101 | select p.id, p.name, c.name as category_name 102 | from products p 103 | inner join categories c on p.category_id = c.id 104 | where p.category_id = :category-id 105 | ``` 106 | 107 | ```clojure 108 | 109 | ; define the query and reference the template file. 110 | (pg/def-query my-query 111 | {:file "path_to/my_query.sql"}) 112 | 113 | ; now use our template query 114 | (pg/qry-> examples-db 115 | (my-query {:category-id 4})) 116 | 117 | 118 | => ({:id 1, :name "Product D1", :category-name "Category D"} 119 | {:id 2, :name "Product D2", :category-name "Category D"}) 120 | 121 | 122 | ``` 123 | 124 | 125 | 126 | 127 | ## Docs and Howto 128 | 129 | - [Query Threading - running sets of queries](docs/query-threading.md) 130 | - [Templated Queries - for more complex queries](docs/templated-queries.md) 131 | - [Dynamic Queries - create dynamic sections](docs/dynamic-queries.md) 132 | - [Using JSON - how to use JSON datatypes](docs/json-support.md) 133 | - [Selects - DSL for simple select queries](docs/selecting-data.md) 134 | - [Inserts - DSL for inserting data](docs/inserting-data.md) 135 | - [Updates - DSL for updating data](docs/updating-data.md) 136 | - [Deletes - DSL for deleting data](docs/deleting-data.md) 137 | - [Raw Queries - using raw unsecure sql statements](docs/raw-queries.md) 138 | - [Listen/Notify - push notifications from postgres](docs/listen-notify.md) 139 | 140 | 141 | 142 | 143 | ## Rationale 144 | 145 | I simply wanted something as easy to get data into and out of postgres. I wanted DateTime's converted automatically, and underscores converted to dashes. Also I really wanted a clean sytax structure for exectuting multiple queries at once, such as adding a parent and multiple child records. I wanted sensible result back based on outcome of query. None of the other clojure sql access libaries was quite what I desired. 146 | 147 | After much experimentation, I concluded that insert, updates and very simple select statements are better handled using a lightweight DSL. This allow transparent handling of both single items as well as sequences of items, batch inserts, cuts down on number of templated queries we need to write and means we don't have to rely on function naming conventions for return values. For any query beyond the most trivial, it's a better solution to then use a templated query. 148 | 149 | The primary goal is ease of use while also encouraging a correct path. I want to embrace postgres to fullest extent possible, and support postgresql extended datatypes (eg arrays, json, hstore, gis) 150 | 151 | Why support only postgresql? Simply I've chosen to build first class support for 1 database rather than lowest common denominator support for all given limited time. Also a desire to out of the box map more complex types such as arrays & json, and also to support listen/notify. 152 | 153 | 154 | 155 | 156 | 157 | ## Intentionally Not Included 158 | 159 | Support for other databases such as MySql, MS Sql, Oracle, etc. 160 | 161 | Data definition sql and schema migrations. We believe that these concerns should 162 | be handled outside of the data layer of your application and no plans to include 163 | these into this library. 164 | 165 | 166 | 167 | 168 | 169 | ## License 170 | 171 | Copyright © 2015 Michael Ball 172 | 173 | Distributed under the Eclipse Public License either version 1.0 or (at 174 | your option) any later version. 175 | -------------------------------------------------------------------------------- /src/taoclj/foundation.clj: -------------------------------------------------------------------------------- 1 | (ns taoclj.foundation 2 | (:require [taoclj.foundation.dsl :refer [to-sql-insert 3 | to-sql-delete to-sql-update]] 4 | [taoclj.foundation.datasources :as datasources] 5 | [taoclj.foundation.execution :as execution] 6 | [taoclj.foundation.templating :as templating] 7 | [taoclj.foundation.reading :refer [read-resultsets read-resultset]] 8 | [taoclj.foundation.writing :refer [set-parameter-values]]) 9 | (:import [java.time Instant] 10 | [java.sql Connection Statement ])) 11 | 12 | 13 | 14 | (defn select 15 | ([rs cnx table-name where-equals] 16 | (execution/execute-select rs cnx table-name nil where-equals false)) 17 | ([rs cnx table-name columns where-equals] 18 | (execution/execute-select rs cnx table-name columns where-equals false))) 19 | 20 | 21 | (defn select1 22 | ([rs cnx table-name where-equals] 23 | (execution/execute-select rs cnx table-name nil where-equals true)) 24 | ([rs cnx table-name columns where-equals] 25 | (execution/execute-select rs cnx table-name columns where-equals true))) 26 | 27 | ;(with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 28 | ; (select [] cnx :insert-single-record {:id 2} ) 29 | ;) 30 | 31 | 32 | 33 | ; just make this def-query, get rid of select and select1 notion 34 | ; for templated queries? 35 | (defmacro def-query [name options] 36 | (templating/generate-def-query name options)) 37 | 38 | 39 | ; remove this? do we really need this? 40 | (defmacro def-select1 [name options] 41 | (templating/generate-def-select name options true)) 42 | 43 | 44 | ; (def-select1 select1-example 45 | ; {:file "taoclj/sql/test-def-select1.sql"}) 46 | 47 | ;; (with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 48 | ;; (select1-example [] cnx {:ids [1]}) 49 | ;; ) 50 | 51 | 52 | 53 | 54 | 55 | ; data can be 56 | ; 1 - a single map with column names/values 57 | ; 2 - a vector of maps 58 | ; 3 - a function returning any of the above 59 | 60 | (defn insert [rs cnx table-name data] 61 | (let [resolved-data (cond (map? data) data 62 | (sequential? data) data 63 | (ifn? data) (data rs) ; perhaps validate the data returned? 64 | :default (throw 65 | (Exception. "Parameter data is not valid!")))] 66 | (conj rs 67 | (cond (map? resolved-data) 68 | (execution/execute-prepared-insert cnx table-name resolved-data) 69 | 70 | (sequential? resolved-data) 71 | (doall (map #(execution/execute-prepared-insert cnx table-name %) 72 | resolved-data)) 73 | 74 | :default 75 | (throw (Exception. "Invalid data parameter")))))) 76 | 77 | ;; (with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 78 | ;; (insert [] cnx :insert-multiple-records {:name "bob"})) 79 | 80 | ;; (with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 81 | ;; (insert [] cnx :insert-multiple-records [{:name "bob"} {:name "bill"}])) 82 | 83 | 84 | 85 | 86 | (defn validate-with-rs-template [columns item-structure] 87 | ; columns must be nil or vector of keywords/strings 88 | ; item template structure must be a vector or map 89 | ) 90 | ; (validate columns item-structure) 91 | 92 | 93 | 94 | ; todo: should this be moved somwhere else? 95 | (defn- with-rs* 96 | ([data item-template] 97 | (with-rs* data nil item-template)) 98 | 99 | ([data columns item-template] 100 | (validate-with-rs-template columns item-template) 101 | 102 | `(fn [~'rs] 103 | (let [~'item ~data] 104 | ~(if-not (sequential? data) 105 | 106 | `(if ~columns 107 | (concat [~columns] [~item-template]) 108 | ~item-template ) 109 | 110 | `(if ~columns 111 | (concat [~columns] (map (fn [~'item] ~item-template) ~data)) 112 | (map (fn [~'item] ~item-template) ~data)) 113 | 114 | ))))) 115 | 116 | 117 | (defmacro with-rs [& args] (apply with-rs* args)) 118 | 119 | ;; ((with-rs [22 33] {:user-id (first rs) 120 | ;; :role-id item}) 121 | ;; [11]) 122 | 123 | ;; (macroexpand '(with-rs [22 33] nil {:user-id (first rs) 124 | ;; :role-id item}) 125 | ;; ) 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ; DELETE syntax 137 | 138 | (defn execute-prepared-delete [cnx table-name where-clause] 139 | (let [where-columns (keys where-clause) 140 | sql (to-sql-delete table-name where-columns) 141 | statement (.prepareStatement cnx sql) ] 142 | 143 | (set-parameter-values statement (map where-clause where-columns)) 144 | 145 | (let [rowcount (.executeUpdate statement)] 146 | (.close statement) 147 | rowcount ))) 148 | 149 | 150 | 151 | (defn delete [rs cnx table-name where-clause] 152 | (conj rs (execute-prepared-delete cnx table-name where-clause))) 153 | 154 | ;; (with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 155 | ;; (delete [] cnx :insert-records {}) 156 | ;; ) 157 | 158 | 159 | 160 | 161 | (defn update 162 | ; I don't know if I'm happy with this syntx for multiple rows needing updates... 163 | "Executes simple update statements. 164 | 165 | (trx-> datasource 166 | (update :users {:name \"joe\"} {:id 1})) 167 | " 168 | [rs ^Connection cnx table-name columns where] 169 | 170 | (let [column-names (keys columns) 171 | where-columns (keys where) 172 | sql (to-sql-update table-name column-names where-columns) 173 | param-values (concat (map columns column-names) 174 | (map where where-columns)) 175 | statement (.prepareStatement cnx sql) ] 176 | (set-parameter-values statement param-values) 177 | (let [rowcount (.executeUpdate statement)] 178 | (.close statement) 179 | (conj rs rowcount)) )) 180 | 181 | 182 | ; (with-open [cnx (.getConnection taoclj.foundation.tests-config/tests-db)] 183 | ; (update [] cnx :update-records {:id 1 :name "joe"} {:id 2}) 184 | ; ) 185 | 186 | 187 | 188 | 189 | 190 | (defn nth-result 191 | "Extracts the nth result from a list of foundation results" 192 | [rs _ n] 193 | (nth rs n)) 194 | 195 | 196 | (defmacro trx-> [db & statements] 197 | (let [cnx (gensym "cnx") 198 | ex (gensym "ex") 199 | result-set (gensym "result-set") 200 | transform (fn [statement] 201 | (concat [(first statement) cnx] (rest statement))) 202 | full-statements (map transform statements)] 203 | 204 | `(let [~cnx (try (.getConnection ~db) 205 | (catch Exception ~ex nil))] 206 | (if-not ~cnx false 207 | (try 208 | (.setAutoCommit ~cnx false) 209 | 210 | (let [~result-set (-> [] 211 | ~@full-statements)] 212 | (.commit ~cnx) 213 | (if (= 1 (count ~result-set)) 214 | (first ~result-set) 215 | ~result-set)) 216 | 217 | (catch Exception ~ex 218 | (.rollback ~cnx) 219 | (println ~ex) 220 | false) 221 | 222 | (finally 223 | (.close ~cnx))))))) 224 | 225 | 226 | 227 | ; custom exception handling? 228 | ; (def-trx-> 229 | ; {:on-exception (fn [e] "do stuff with error...")}) 230 | 231 | 232 | 233 | 234 | ; TODO: add try/catch? 235 | (defmacro qry-> [db & statements] 236 | (let [cnx (gensym "cnx") 237 | result-set (gensym "result-set") 238 | transform (fn [statement] 239 | (concat [(first statement) cnx] (rest statement))) 240 | full-statements (map transform statements)] 241 | 242 | `(with-open [~cnx (.getConnection ~db)] 243 | 244 | (let [~result-set (-> [] ~@full-statements)] 245 | (if (= 1 (count ~result-set)) 246 | (first ~result-set) 247 | ~result-set)) 248 | 249 | ))) 250 | 251 | ;; (qry-> taoclj.foundation.tests-config/tests-db 252 | ;; (execute "select 'ehlo1' as msg1;")) 253 | 254 | 255 | 256 | 257 | 258 | (defmacro def-datasource 259 | "Creates a JDBC datasource" 260 | [dsname config] 261 | ; todo validate config... 262 | `(def ~dsname 263 | (if (:pooled ~config) 264 | (~datasources/create-pooled-datasource ~config) 265 | (~datasources/create-datasource ~config)))) 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------