├── .travis.yml ├── test └── jeesql │ ├── sample_files │ ├── syntax_error.sql │ ├── tagged_no_comments.sql │ ├── tagged_no_name.sql │ ├── quoting.sql │ ├── acceptance_test_single.sql │ ├── tagged_two_names.sql │ ├── current_time.sql │ ├── queries_with_attributes.sql │ ├── complicated_docstring.sql │ ├── parser_edge_cases.sql │ ├── inline_comments.sql │ ├── mixed_parameters.sql │ ├── combined_file.sql │ └── acceptance_test_combined.sql │ ├── util_test.clj │ ├── positional_test.clj │ ├── statement_parser_test.clj │ ├── queryfile_parser_test.clj │ ├── core_test.clj │ ├── generate_test.clj │ └── acceptance_test.clj ├── src └── jeesql │ ├── types.clj │ ├── postgres.clj │ ├── array.clj │ ├── util.clj │ ├── statement_parser.clj │ ├── core.clj │ ├── queryfile_parser.clj │ ├── autoreload.clj │ └── generate.clj ├── .gitignore ├── project.clj ├── README.md └── LICENSE.txt /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/syntax_error.sql: -------------------------------------------------------------------------------- 1 | -- name: syntax-error 2 | SELOCT * 3 | FROM user; 4 | -------------------------------------------------------------------------------- /src/jeesql/types.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.types) 2 | 3 | (defrecord Query 4 | [name docstring statement]) 5 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/tagged_no_comments.sql: -------------------------------------------------------------------------------- 1 | -- name: the-time 2 | SELECT CURRENT_TIMESTAMP 3 | FROM SYSIBM.SYSDUMMY1 4 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/tagged_no_name.sql: -------------------------------------------------------------------------------- 1 | -- Error - This query has no name. 2 | SELECT CURRENT_TIMESTAMP FROM SYSIBM.SYSDUMMY1 3 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/quoting.sql: -------------------------------------------------------------------------------- 1 | -- name: quoting 2 | -- SQL's quoting rules. 3 | SELECT ''''||'can''t'||'''' AS word 4 | FROM SYSIBM.SYSDUMMY1 5 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/acceptance_test_single.sql: -------------------------------------------------------------------------------- 1 | -- Just selects the current time. 2 | -- Nothing fancy. 3 | SELECT CURRENT_TIMESTAMP AS time 4 | FROM SYSIBM.SYSDUMMY1 5 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/tagged_two_names.sql: -------------------------------------------------------------------------------- 1 | -- name: the-time 2 | -- Error - This query has two names. 3 | -- name: the-time2 4 | SELECT CURRENT_TIMESTAMP FROM SYSIBM.SYSDUMMY1 5 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/current_time.sql: -------------------------------------------------------------------------------- 1 | -- name: current-time-query 2 | -- Just selects the current time. 3 | -- Nothing fancy. 4 | SELECT CURRENT_TIMESTAMP AS time 5 | FROM SYSIBM.SYSDUMMY1 6 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/queries_with_attributes.sql: -------------------------------------------------------------------------------- 1 | -- name: my-attributed-query 2 | -- meaning-of-life: 42 3 | -- result-type: :single 4 | -- This is the comment 5 | SELECT * FROM sometable 6 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/complicated_docstring.sql: -------------------------------------------------------------------------------- 1 | -- This is a simple query. 2 | -- 3 | -- but... 4 | -- 5 | -- The docstring 6 | -- is tricksy. 7 | SELECT CURRENT_TIMESTAMP AS time 8 | FROM SYSIBM.SYSDUMMY1 9 | -- Isn't it? 10 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/parser_edge_cases.sql: -------------------------------------------------------------------------------- 1 | -- name: this-has-trailing-whitespace 2 | -- The name has trailing whitespace here ^^^^ 3 | -- But the parser should not blow up. 4 | SELECT CURRENT_TIMESTAMP 5 | FROM SYSIBM.SYSDUMMY1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | 14 | /derby.log 15 | /README.html 16 | /README.pdf 17 | /README.tex 18 | /.nrepl-port 19 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/inline_comments.sql: -------------------------------------------------------------------------------- 1 | -- name: inline-comments-query 2 | -- It's the time query again, but there's an inline comment to make the parser fret. 3 | SELECT 4 | CURRENT_TIMESTAMP AS time, -- Here is an inline comment. 5 | 'Not -- a comment' AS string 6 | FROM SYSIBM.SYSDUMMY1 7 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/mixed_parameters.sql: -------------------------------------------------------------------------------- 1 | -- name: mixed-parameters-query 2 | -- default-parameters: {:value3 1} 3 | -- Here's a query with some named and some anonymous parameters. 4 | -- (...and some repeats.) 5 | SELECT CURRENT_TIMESTAMP AS time 6 | FROM SYSIBM.SYSDUMMY1 7 | WHERE :value1 = 1 8 | AND :value2 = 2 9 | AND :value2 = 2 10 | AND :value3 = 1 11 | -------------------------------------------------------------------------------- /test/jeesql/sample_files/combined_file.sql: -------------------------------------------------------------------------------- 1 | -- name: the-time 2 | -- This is another time query. 3 | -- Exciting, huh? 4 | SELECT CURRENT_TIMESTAMP 5 | FROM SYSIBM.SYSDUMMY1 6 | 7 | -- name: sums 8 | -- Just in case you've forgotten 9 | -- I made you a sum. 10 | SELECT 11 | :a + 1 adder, 12 | :b - 1 subtractor 13 | FROM SYSIBM.SYSDUMMY1 14 | 15 | -- name: edge 16 | -- And here's an edge case. 17 | -- Comments in the middle of the query. 18 | SELECT 19 | 1 + 1 AS two 20 | -- I find this query dull. 21 | FROM SYSIBM.SYSDUMMY1 22 | 23 | 24 | -- name: query-with-default 25 | -- default-parameters: {:foo 42} 26 | -- This query has default parameter foo 27 | SELECT * FROM foobar WHERE foo = :foo AND bar = :bar 28 | -------------------------------------------------------------------------------- /src/jeesql/postgres.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.postgres 2 | "Utilities for PostgreSQL" 3 | (:require [clojure.java.jdbc :as jdbc] 4 | [clojure.string :as str])) 5 | 6 | (def +query-plan-keyword+ (keyword "query plan")) 7 | 8 | (defn report-slow-queries [operation-type duration-ms db query] 9 | (println "------- Slow " (name operation-type) " (" duration-ms "ms) ------ \n" 10 | (first query)) 11 | (when (= :query operation-type) 12 | (println "PARAMS: " (pr-str (rest query))) 13 | (let [explain-query (concat [(str "EXPLAIN ANALYZE " (first query))] 14 | (rest query))] 15 | (println "EXPLAIN ANALYZE OUTPUT FOR QUERY:\n" 16 | (str/join "\n" 17 | (map +query-plan-keyword+ 18 | (jdbc/query db explain-query))))))) 19 | -------------------------------------------------------------------------------- /test/jeesql/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.util-test 2 | (:require [expectations :refer :all] 3 | [clojure.template :refer [do-template]] 4 | [jeesql.util :refer :all])) 5 | 6 | ;;; Test underscores-to-dashes 7 | (do-template [input output] 8 | (expect output 9 | (underscores-to-dashes input)) 10 | 11 | "nochange" "nochange" 12 | "current_time" "current-time" 13 | "this_is_it" "this-is-it") 14 | 15 | ;;; Test slurp-from-classpath 16 | (expect #"\bSELECT\b" 17 | (slurp-from-classpath "jeesql/sample_files/current_time.sql")) 18 | 19 | (expect java.io.FileNotFoundException 20 | (slurp-from-classpath "nothing/here")) 21 | 22 | ;;; Test str-non-nil 23 | (expect "" 24 | (str-non-nil)) 25 | 26 | (expect "1" 27 | (str-non-nil 1)) 28 | 29 | (expect ":a2cd" 30 | (str-non-nil :a 2 'c "d")) 31 | -------------------------------------------------------------------------------- /src/jeesql/array.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.array 2 | "Support for SQL array parameters. Require this namespace to have seqable values turned 3 | into JDBC array parameters." 4 | (:require [clojure.java.jdbc :as jdbc]) 5 | (:import [java.sql PreparedStatement])) 6 | 7 | (def +sql-type+ 8 | {java.lang.Long "INTEGER" 9 | java.lang.String "VARCHAR"}) 10 | 11 | (defn- to-sql-array [^PreparedStatement ps coll] 12 | (let [java-array (into-array coll) 13 | array-type (.getComponentType (.getClass java-array)) 14 | sql-type (get +sql-type+ array-type)] 15 | (if-not sql-type 16 | (throw (RuntimeException. (str "Unable to determine SQL type for Java type: " sql-type))) 17 | (.createArrayOf (.getConnection ps) 18 | sql-type 19 | java-array)))) 20 | 21 | 22 | (extend-protocol jdbc/ISQLParameter 23 | clojure.lang.Seqable 24 | (set-parameter [v ^PreparedStatement s ^long i] 25 | (.setArray s i 26 | (to-sql-array s v)))) 27 | -------------------------------------------------------------------------------- /src/jeesql/util.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.util 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as string] 4 | [clojure.pprint :refer [pprint]]) 5 | (:import [java.io FileNotFoundException])) 6 | 7 | (defn underscores-to-dashes 8 | [string] 9 | (when string 10 | (string/replace string "_" "-"))) 11 | 12 | (defn str-non-nil 13 | "Exactly like `clojure.core/str`, except it returns an empty string 14 | with no args (whereas `str` would return `nil`)." 15 | [& args] 16 | (apply str "" args)) 17 | 18 | (defn resource-file-url [path] 19 | (or (io/resource path) 20 | (throw (FileNotFoundException. path)))) 21 | 22 | (defn slurp-from-classpath 23 | "Slurps a file from the classpath." 24 | [path] 25 | (slurp (resource-file-url path))) 26 | 27 | ;;; TODO There may well be a built-in for this. If there is, I have not found it. 28 | (defn create-root-var 29 | "Given a name and a value, intern a var in the current namespace, taking metadata from the value." 30 | ([name value] 31 | (create-root-var *ns* name value)) 32 | 33 | ([ns name value] 34 | (intern ns 35 | (with-meta (symbol name) 36 | (meta value)) 37 | value))) 38 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject webjure/jeesql "0.4.7" 2 | :description "A Clojure library for using SQL" 3 | :url "https://github.com/tatut/jeesql" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/java.jdbc "0.6.2-alpha3"] 8 | [org.clojure/core.async "0.3.443"]] 9 | ;:pedantic? :abort 10 | :scm {:name "git" 11 | :url "https://github.com/tatut/jeesql"} 12 | :profiles {:dev {:dependencies [[expectations "2.1.3" :exclusions [org.clojure/clojure]] 13 | [org.apache.derby/derby "10.11.1.1"]] 14 | :plugins [[lein-autoexpect "1.4.0"] 15 | [lein-expectations "0.0.8"]]} 16 | :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} 17 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 18 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 19 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}} 20 | :aliases {"test-all" ["with-profile" "+1.5:+1.6:+1.7:+1.8" "do" 21 | ["clean"] 22 | ["expectations"]] 23 | "test-ancient" ["expectations"]}) 24 | -------------------------------------------------------------------------------- /src/jeesql/statement_parser.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.statement-parser 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :refer [join]] 4 | [jeesql.util :refer [str-non-nil]] 5 | [clojure.string :as str]) 6 | (:import [jeesql.types Query])) 7 | 8 | (def ^{:doc "Regular expression to split statement into three parts: before the first parameter, 9 | the parameter name and the rest of the statement. A parameter always starts with a single colon and 10 | may contain alphanumerics as well as '-', '_' and '?' characters."} 11 | parameter #"(?s)(.*?[^:\\]):(\p{Alpha}[\p{Alnum}\_\-\?\./]*)(.*)") 12 | 13 | (defn- replace-escaped-colon [string] 14 | (str/replace string #"\\:" ":")) 15 | 16 | (defn- parse-statement 17 | [statement context] 18 | (loop [acc [] 19 | rest-of-statement statement] 20 | (let [[_ before parameter after :as match] (re-find parameter rest-of-statement)] 21 | (if-not match 22 | (if rest-of-statement 23 | (conj acc (replace-escaped-colon rest-of-statement)) 24 | acc) 25 | (recur (into acc 26 | [(replace-escaped-colon before) (symbol parameter)]) 27 | after))))) 28 | 29 | (defmulti tokenize 30 | "Turn a raw SQL statement into a vector of SQL-substrings 31 | interspersed with clojure symbols for the query's parameters. 32 | 33 | For example, `(parse-statement \"SELECT * FROM person WHERE :age > age\")` 34 | becomes: `[\"SELECT * FROM person WHERE \" age \" > age\"]`" 35 | (fn [this] (type this))) 36 | 37 | (defmethod tokenize String 38 | [this] 39 | (parse-statement this nil)) 40 | 41 | (defmethod tokenize Query 42 | [{:keys [statement]}] 43 | (parse-statement statement nil)) 44 | -------------------------------------------------------------------------------- /test/jeesql/positional_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.positional-test 2 | (:require [expectations :refer :all] 3 | [clojure.java.jdbc :as jdbc] 4 | [jeesql.core :refer :all]) 5 | (:import [java.sql SQLException SQLSyntaxErrorException SQLDataException])) 6 | 7 | (def db {:subprotocol "derby" 8 | :subname (gensym "memory:") 9 | :create true}) 10 | 11 | (defqueries "jeesql/sample_files/acceptance_test_combined.sql" 12 | {:positional? true}) 13 | 14 | ;; Create 15 | (expect (create-person-table! db)) 16 | 17 | ;; Insert -> Select. 18 | (expect {:1 1M} (insert-person :age 21 | 22 | -- name: find-by-age 23 | SELECT * 24 | FROM person 25 | WHERE age IN (:age) 26 | 27 | -- name: update-age! 28 | UPDATE person 29 | SET age = :age 30 | WHERE name = :name 31 | 32 | -- name: delete-person! 33 | DELETE FROM person 34 | WHERE name = :name 35 | 36 | -- name: drop-person-table! 37 | DROP TABLE person 38 | 39 | -- name: find-by-name-and-age-range 40 | -- Test positional paremeter order with these 3 args 41 | SELECT * 42 | FROM person 43 | WHERE name LIKE :name AND age >= :age_min AND age <= :age_max 44 | 45 | -- name: find-by-name-and-age-is-not 46 | -- Test same positional parameter name isn't repeated 47 | SELECT * 48 | FROM person 49 | WHERE name LIKE :name AND (age < :age OR age > :age) 50 | 51 | -- name: count-people-older-than 52 | -- single?: true 53 | SELECT COUNT(*) FROM person WHERE age > :age 54 | 55 | 56 | -- name: insert-person-return-keys` marker, 11 | followed by optional comment lines (which form the docstring), followed by 12 | the query itself." 13 | ([filename] 14 | (defqueries filename {})) 15 | ([filename options] 16 | (let [file-url (resource-file-url filename) 17 | ns *ns* 18 | reload-fn (fn [content] 19 | (doall (->> content 20 | parse-tagged-queries 21 | (map #(generate-var ns % options)))))] 22 | (autoreload file-url reload-fn) 23 | (reload-fn (slurp file-url))))) 24 | 25 | (defmacro require-sql 26 | "Require-like behavior for jeesql, to prevent namespace pollution. 27 | Parameter is a list of [sql-source-file-name [:as alias] [:refer [var1 var2]]] 28 | At least one of :as or :refer is required 29 | Usage: (require-sql [\"sql/foo.sql\" :as foo-sql :refer [some-query-fn])" 30 | [[sql-file & {:keys [as refer]} :as require-args]] 31 | (when-not (or as refer) 32 | (throw (Exception. "Missing an :as or a :refer"))) 33 | (let [current-ns (ns-name *ns*) 34 | ;; Keep this .sql file's defqueries in a predictable place: 35 | target-ns (-> (str "jeesql/" sql-file) (str/replace #"/" ".") symbol)] 36 | `(do 37 | (ns-unalias *ns* '~as) 38 | (create-ns '~target-ns) 39 | (in-ns '~target-ns) 40 | (clojure.core/require '[jeesql.core]) 41 | (jeesql.core/defqueries ~sql-file) 42 | (clojure.core/in-ns '~current-ns) 43 | ~(when as 44 | `(clojure.core/alias '~as '~target-ns)) 45 | ~(when refer 46 | `(clojure.core/refer '~target-ns :only '~refer))))) 47 | -------------------------------------------------------------------------------- /test/jeesql/statement_parser_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.statement-parser-test 2 | (:require [expectations :refer :all] 3 | [clojure.template :refer [do-template]] 4 | [jeesql.types :refer [map->Query]] 5 | [jeesql.statement-parser :refer :all])) 6 | 7 | (do-template [statement _ split-result] 8 | (do (expect (quote split-result) 9 | (tokenize statement)) 10 | (expect (quote split-result) 11 | (tokenize (map->Query {:name "test" 12 | :doctstring "A test case." 13 | :statement statement})))) 14 | 15 | ;; Simple tests 16 | "SELECT 1 FROM dual" => ["SELECT 1 FROM dual"] 17 | 18 | "SELECT :value FROM dual" => ["SELECT " value " FROM dual"] 19 | "SELECT 'test'\nFROM dual" => ["SELECT 'test'\nFROM dual"] 20 | "SELECT :value, :other_value FROM dual" => ["SELECT " value ", " other_value " FROM dual"] 21 | 22 | ;; Tokenization rules 23 | "SELECT :age-5 FROM dual" 24 | => ["SELECT " age-5 " FROM dual"] 25 | 26 | ;; Escapes 27 | "SELECT :value, :other_value, '\\:not_a_value' FROM dual" 28 | => ["SELECT " value ", " other_value ", ':not_a_value' FROM dual"] 29 | 30 | "SELECT 'not \\' \\:a_value' FROM dual" 31 | => ["SELECT 'not \\' :a_value' FROM dual"] 32 | 33 | ;; Casting 34 | "SELECT :value, :other_value, 5::text FROM dual" 35 | => ["SELECT " value ", " other_value ", 5::text FROM dual"] 36 | 37 | ;; Newlines are preserved. 38 | "SELECT :value, :other_value, 5::text\nFROM dual" 39 | => ["SELECT " value ", " other_value ", 5::text\nFROM dual"] 40 | 41 | ;; Complex 42 | "SELECT :a+2*:b+age::int FROM users WHERE username = :user AND :b > 0" 43 | => ["SELECT " a "+2*" b "+age::int FROM users WHERE username = " user " AND " b " > 0"] 44 | 45 | "SELECT :value1 + :p1 + value2 + :p2 + :value1\nFROM SYSIBM.SYSDUMMY1" 46 | => ["SELECT " value1 " + " p1 " + value2 + " p2 " + " value1 "\nFROM SYSIBM.SYSDUMMY1"] 47 | 48 | "SELECT ARRAY [:value1] FROM dual" 49 | => ["SELECT ARRAY [" value1 "] FROM dual"] 50 | 51 | "SELECT id FROM table WHERE foo = :bar? AND date < :date-foo AND removed = false" 52 | => ["SELECT id FROM table WHERE foo = " bar? " AND date < " date-foo " AND removed = false"]) 53 | -------------------------------------------------------------------------------- /src/jeesql/queryfile_parser.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.queryfile-parser 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :refer [join trim]] 4 | [jeesql.types :refer [map->Query]] 5 | [jeesql.util :refer [str-non-nil]] 6 | [clojure.string :as str])) 7 | 8 | (def header-attribute #"^--\s*([^\s:]+):\s*(.+)$") 9 | (def header-comment #"^--.*") 10 | (def comment-line #"\s*--.*") 11 | 12 | (defn- attribute-line? [line] 13 | (re-matches header-attribute line)) 14 | 15 | (defn- header-comment-line? [line] 16 | (re-matches header-comment line)) 17 | 18 | (defn- comment-line? [line] 19 | (re-matches comment-line line)) 20 | 21 | (defn- parse-header [header-lines] 22 | (reduce 23 | (fn [headers line] 24 | (let [[_ name val] (re-matches header-attribute line)] 25 | (if (and name val) 26 | (assoc headers (keyword name) 27 | (read-string val)) 28 | headers))) 29 | {} 30 | header-lines)) 31 | 32 | (defn parse [lines] 33 | (let [lines (drop-while (comp not attribute-line?) lines) 34 | [header lines] (split-with attribute-line? lines) 35 | [comment lines] (split-with header-comment-line? lines) 36 | [statement lines] (split-with (comp not attribute-line?) lines) 37 | attributes (parse-header header)] 38 | (if-not (:name attributes) 39 | (throw (ex-info "Parse error: query must have a name" {})) 40 | [{:name (name (:name attributes)) 41 | :docstring (->> comment 42 | (map #(str/replace (str/trim %) #"^--\s*" "")) 43 | (str/join "\n")) 44 | :statement (->> statement 45 | (remove comment-line?) 46 | (str/join "\n") 47 | str/trim) 48 | :attributes (dissoc attributes :name)} 49 | lines]))) 50 | 51 | (defn parse-all [lines] 52 | (loop [queries [] 53 | lines lines] 54 | (if (empty? lines) 55 | queries 56 | (let [[query lines] (parse lines)] 57 | (recur (conj queries query) 58 | lines))))) 59 | 60 | 61 | 62 | (defn parse-tagged-queries 63 | "Parses a string with Jeesql's defqueries syntax into a sequence of maps." 64 | [text] 65 | (mapv map->Query (parse-all (str/split-lines text)))) 66 | -------------------------------------------------------------------------------- /src/jeesql/autoreload.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.autoreload 2 | "Support automatic reloading when in development mode. If classpath resources are 3 | file URLs, watches changes in them." 4 | (:require [clojure.java.io :as io]) 5 | (:import [java.util.concurrent Executors TimeUnit ThreadFactory])) 6 | 7 | ;; Tried using Java nio WatchService but the API was horrible and the 8 | ;; notifications arrived way too slowly... and it was polling on OS X. 9 | ;; So we might as well just implement our own simpler polling. 10 | 11 | 12 | ;; The watch files is a mapping of file url to 13 | ;; file information. :timestamp, :reload-fn 14 | (def watched-files (atom {})) 15 | 16 | (defn- watch-file 17 | "Ensure that the given SQL file is being watched" 18 | [files file-url reload-fn] 19 | (if (= "file" (.getScheme (.toURI file-url))) 20 | (assoc files file-url 21 | {:timestamp (System/currentTimeMillis) 22 | :reload-fn reload-fn}) 23 | 24 | ;; Not a file URI, I can't watch this 25 | files)) 26 | 27 | (defn- reload-if-changed [[file-url {:keys [timestamp reload-fn] :as file}]] 28 | (let [modified (.lastModified (io/as-file file-url))] 29 | (if (> modified timestamp) 30 | (do (try 31 | (println "RELOADING " (str file-url)) 32 | (reload-fn (slurp file-url)) 33 | (catch Exception e 34 | (println "ERROR RELOADING " file-url "\n" e))) 35 | [file-url (assoc file :timestamp (System/currentTimeMillis))]) 36 | [file-url file]))) 37 | 38 | (defn- check-for-reload [files] 39 | (into {} 40 | (map reload-if-changed) 41 | (seq files))) 42 | 43 | (defn autoreload 44 | "Register file URL for autoreload. When file changes the reload-fn will be invoked 45 | with the file contents. 46 | If the URL is not a file: URL, it cannot be reloaded and is ignored." 47 | [file-url reload-fn] 48 | (swap! watched-files watch-file file-url reload-fn)) 49 | 50 | (def reload-poll-ms 2000) 51 | (defonce reloader (atom nil)) 52 | 53 | (defn start-autoreload [] 54 | (swap! reloader 55 | (fn [reloader] 56 | (or reloader 57 | (doto (Executors/newSingleThreadScheduledExecutor) 58 | (.scheduleAtFixedRate #(swap! watched-files check-for-reload) 59 | reload-poll-ms reload-poll-ms 60 | TimeUnit/MILLISECONDS)))))) 61 | (defn stop-autoreload [] 62 | (swap! reloader 63 | #(when % 64 | (.shutdownNow %) 65 | nil))) 66 | -------------------------------------------------------------------------------- /test/jeesql/queryfile_parser_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.queryfile-parser-test 2 | (:require [clojure.string :refer [join]] 3 | [clojure.template :refer [do-template]] 4 | [expectations :refer :all] 5 | [jeesql.queryfile-parser :refer :all] 6 | [jeesql.types :refer [map->Query]] 7 | [jeesql.util :refer [slurp-from-classpath]]) 8 | (:import [clojure.lang ExceptionInfo])) 9 | 10 | (expect 11 | [(map->Query {:name "the-time" 12 | :docstring "This is another time query.\nExciting, huh?" 13 | :statement "SELECT CURRENT_TIMESTAMP\nFROM SYSIBM.SYSDUMMY1" 14 | :attributes {}}) 15 | (map->Query {:name "sums" 16 | :docstring "Just in case you've forgotten\nI made you a sum." 17 | :statement (join "\n" ["SELECT" 18 | " :a + 1 adder," 19 | " :b - 1 subtractor" 20 | "FROM SYSIBM.SYSDUMMY1"]) 21 | :attributes {}}) 22 | (map->Query {:name "edge" 23 | :docstring "And here's an edge case.\nComments in the middle of the query." 24 | :statement (join "\n" ["SELECT" 25 | " 1 + 1 AS two" 26 | "FROM SYSIBM.SYSDUMMY1"]) 27 | :attributes {}}) 28 | (map->Query {:name "query-with-default" 29 | :docstring "This query has default parameter foo" 30 | :statement "SELECT * FROM foobar WHERE foo = :foo AND bar = :bar" 31 | :attributes {:default-parameters {:foo 42}}}) 32 | ] 33 | (parse-tagged-queries (slurp-from-classpath "jeesql/sample_files/combined_file.sql"))) 34 | 35 | ;;; Failures. 36 | (expect #"Parse error" 37 | (try 38 | (parse-tagged-queries (slurp-from-classpath "jeesql/sample_files/tagged_no_name.sql")) 39 | (catch ExceptionInfo e (.getMessage e)))) 40 | 41 | (expect #"name: the-time2" 42 | (-> "jeesql/sample_files/tagged_two_names.sql" 43 | slurp-from-classpath 44 | parse-tagged-queries 45 | first :docstring)) 46 | 47 | ;;; Parsing edge cases. 48 | 49 | (expect ["this-has-trailing-whitespace"] 50 | (map :name 51 | (parse-tagged-queries (slurp-from-classpath "jeesql/sample_files/parser_edge_cases.sql")))) 52 | 53 | 54 | ;;; Parse queries with attributes 55 | (expect {:meaning-of-life 42 :result-type :single} 56 | (-> "jeesql/sample_files/queries_with_attributes.sql" 57 | slurp-from-classpath 58 | parse-tagged-queries 59 | first 60 | :attributes)) 61 | -------------------------------------------------------------------------------- /test/jeesql/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.core-test 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.string :refer [upper-case]] 4 | [expectations :refer :all] 5 | [jeesql.core :refer :all])) 6 | 7 | (def derby-db {:subprotocol "derby" 8 | :subname (gensym "memory:") 9 | :create true}) 10 | 11 | ;;; Test-environment check. Can we actually access the test DB? 12 | (expect (more-> java.sql.Timestamp (-> first :1)) 13 | (jdbc/query derby-db 14 | ["SELECT CURRENT_TIMESTAMP FROM SYSIBM.SYSDUMMY1"])) 15 | 16 | (defqueries "jeesql/sample_files/current_time.sql") 17 | 18 | (defqueries "jeesql/sample_files/mixed_parameters.sql") 19 | 20 | ;;; Test querying. 21 | (expect (more-> java.util.Date 22 | (-> first :time)) 23 | (current-time-query derby-db)) 24 | 25 | (expect (more-> java.util.Date 26 | (-> first :time)) 27 | (mixed-parameters-query derby-db 28 | {:value1 1 29 | :value2 2})) 30 | 31 | (expect empty? 32 | (mixed-parameters-query derby-db 33 | {:value1 1 34 | :value2 2 35 | :value3 0})) 36 | 37 | ;;; Test comment rules. 38 | (defqueries "jeesql/sample_files/inline_comments.sql") 39 | 40 | (expect (more-> java.util.Date :time 41 | "Not -- a comment" :string) 42 | (first (inline-comments-query derby-db))) 43 | 44 | ;;; Test Metadata. 45 | (expect (more-> "Just selects the current time.\nNothing fancy." :doc 46 | 'current-time-query :name 47 | (list '[connection]) :arglists) 48 | (meta (var current-time-query))) 49 | 50 | (expect (more-> "Here's a query with some named and some anonymous parameters.\n(...and some repeats.)" :doc 51 | 'mixed-parameters-query :name 52 | true (-> :arglists list?) 53 | ;; TODO We could improve the clarity of what this is testing. 54 | 1 (-> :arglists count) 55 | 56 | 2 (-> :arglists first count) 57 | #{'value1 'value2 'value3} (-> :arglists first second :keys set)) 58 | (meta (var mixed-parameters-query))) 59 | 60 | ;; Running a query in a transaction and using the result outside of it should work as expected. 61 | (expect-let [[{time :time}] (jdbc/with-db-transaction [connection derby-db] 62 | (current-time-query connection))] 63 | java.util.Date 64 | time) 65 | 66 | ;;; Check defqueries returns the list of defined vars. 67 | (expect-let [return-value (defqueries "jeesql/sample_files/combined_file.sql")] 68 | (repeat 4 clojure.lang.Var) 69 | (map type return-value)) 70 | 71 | ;;; SQL's quoting rules. 72 | (defqueries "jeesql/sample_files/quoting.sql") 73 | 74 | (expect "'can't'" 75 | (:word (first (quoting derby-db)))) 76 | 77 | ;;; Switch into a fresh namespace 78 | (ns jeesql.core-test.test-require-sql 79 | (:require [expectations :refer :all] 80 | [jeesql.core :refer :all])) 81 | 82 | (require-sql ["jeesql/sample_files/combined_file.sql" :as combined]) 83 | 84 | (expect var? #'combined/edge) 85 | 86 | (expect first (combined/edge jeesql.core-test/derby-db)) 87 | 88 | (require-sql ["jeesql/sample_files/combined_file.sql" :refer [the-time]]) 89 | 90 | (expect var? #'the-time) 91 | 92 | (expect first (the-time jeesql.core-test/derby-db)) 93 | -------------------------------------------------------------------------------- /test/jeesql/generate_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.generate-test 2 | (:require [expectations :refer :all] 3 | [clojure.template :refer [do-template]] 4 | [jeesql.statement-parser :refer [tokenize]] 5 | [jeesql.generate :refer :all])) 6 | 7 | (do-template [statement _ expected-parameters] 8 | (expect expected-parameters 9 | (expected-parameter-list statement)) 10 | 11 | "SELECT * FROM user" 12 | => #{} 13 | 14 | "SELECT * FROM user WHERE user_id = :id" 15 | => #{:id} 16 | 17 | "SELECT * FROM user WHERE user_id = :name" 18 | => #{:name} 19 | 20 | "SELECT * FROM user WHERE user_id = :name AND country = :country AND age IN (:ages)" 21 | => #{:name :country :ages}) 22 | 23 | ;;; Testing in-list-parmaeter for "IN-list" statements. 24 | (expect [true true true nil true] 25 | (mapv in-list-parameter? 26 | (list [] 27 | (list) 28 | (lazy-seq (cons 1 [2])) 29 | {:a 1} 30 | #{1 2 3}))) 31 | 32 | ;;; Testing reassemble-query 33 | (do-template [statement parameters _ rewritten-form] 34 | (expect rewritten-form 35 | (rewrite-query-for-jdbc (tokenize statement) 36 | parameters)) 37 | 38 | "SELECT age FROM users WHERE country = :country" 39 | {:country "gb"} 40 | => ["SELECT age FROM users WHERE country = ?" "gb"] 41 | 42 | "SELECT age FROM users WHERE (country = :c1 OR country = :c2) AND name = :name" 43 | {:c1 "gb" :c2 "us" 44 | :name "tom"} 45 | => ["SELECT age FROM users WHERE (country = ? OR country = ?) AND name = ?" "gb" "us" "tom"] 46 | 47 | ;;; Vectors trigger IN expansion 48 | "SELECT age FROM users WHERE country = :country AND name IN (:names)" 49 | {:country "gb" 50 | :names ["tom" "dick" "harry"]} 51 | => ["SELECT age FROM users WHERE country = ? AND name IN (?,?,?)" "gb" "tom" "dick" "harry"] 52 | 53 | ;;; Lists trigger IN expansion 54 | "SELECT age FROM users WHERE country = :country AND name IN (:names)" 55 | {:country "gb" 56 | :names (list "tom" "dick" "harry")} 57 | => ["SELECT age FROM users WHERE country = ? AND name IN (?,?,?)" "gb" "tom" "dick" "harry"] 58 | 59 | ;;; Lazy seqs of cons of vectors trigger IN expansion 60 | "SELECT age FROM users WHERE country = :country AND name IN (:names)" 61 | {:country "gb" 62 | :names (lazy-seq (cons "tom" ["dick" "harry"]))} 63 | => ["SELECT age FROM users WHERE country = ? AND name IN (?,?,?)" "gb" "tom" "dick" "harry"] 64 | 65 | ;;; Maps do not trigger IN expansion 66 | "INSERT INTO json (source, data) VALUES (:source, :data)" 67 | {:source "google" 68 | :data {:a 1}} 69 | => ["INSERT INTO json (source, data) VALUES (?, ?)" "google" {:a 1}] 70 | 71 | "INSERT INTO json (data, source) VALUES (:data, :source)" 72 | {:source "google" 73 | :data {:a 1}} 74 | => ["INSERT INTO json (data, source) VALUES (?, ?)" {:a 1} "google"] 75 | 76 | ;;; Empty IN-lists are allowed by Jeesql and output as NULL 77 | "SELECT age FROM users WHERE country = :country AND name IN (:names)" 78 | {:country "gb" 79 | :names []} 80 | => ["SELECT age FROM users WHERE country = ? AND name IN (NULL)" "gb"] 81 | 82 | "SELECT * FROM users WHERE group_ids IN(:group_ids) AND parent_id = :parent_id" 83 | {:group_ids [1 2] 84 | :parent_id 3} 85 | => ["SELECT * FROM users WHERE group_ids IN(?,?) AND parent_id = ?" 1 2 3]) 86 | 87 | ;;; Incorrect parameters. 88 | (expect AssertionError 89 | (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = :country AND name = :name") 90 | {:country "gb"})) 91 | 92 | (expect AssertionError 93 | (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = :c AND name = :n") 94 | {})) 95 | -------------------------------------------------------------------------------- /test/jeesql/acceptance_test.clj: -------------------------------------------------------------------------------- 1 | (ns jeesql.acceptance-test 2 | (:require [expectations :refer :all] 3 | [clojure.java.jdbc :as jdbc] 4 | [jeesql.core :refer :all]) 5 | (:import [java.sql SQLException SQLSyntaxErrorException SQLDataException])) 6 | 7 | (def derby-db {:subprotocol "derby" 8 | :subname (gensym "memory:") 9 | :create true}) 10 | 11 | (defqueries "jeesql/sample_files/current_time.sql") 12 | (expect java.util.Date 13 | (-> (current-time-query derby-db) 14 | first 15 | :time)) 16 | 17 | ;;; Multiple-query workflow. 18 | (defqueries 19 | "jeesql/sample_files/acceptance_test_combined.sql") 20 | 21 | ;; Create 22 | (expect (create-person-table! derby-db)) 23 | 24 | ;; Insert -> Select. 25 | (expect {:1 1M} (insert-person Select. 40 | (expect 1 (update-age! derby-db {:age 38 41 | :name "Alice"})) 42 | (expect 0 (update-age! derby-db {:age 38 43 | :name "David"})) 44 | 45 | (expect 3 (count (find-older-than derby-db {:age 10}))) 46 | (expect 2 (count (find-older-than derby-db {:age 30}))) 47 | (expect 0 (count (find-older-than derby-db {:age 50}))) 48 | 49 | 50 | ;; Test that default parameters work 51 | (expect {:name "Alice" :age 38} (first (find-people-with-name-or-alice derby-db {}))) 52 | (expect {:name "Bob" :age 25} (first (find-people-with-name-or-alice derby-db {:name "Bob"}))) 53 | 54 | 55 | ;; Delete -> Select. 56 | (expect 1 (delete-person! derby-db {:name "Alice"})) 57 | 58 | (expect 2 (count (find-older-than derby-db {:age 10}))) 59 | (expect 1 (count (find-older-than derby-db {:age 30}))) 60 | (expect 0 (count (find-older-than derby-db {:age 50}))) 61 | 62 | ;; Check that our query with {:single? true} attribute 63 | ;; returns a single value 64 | (expect 2 (count-people-older-than derby-db {:age 10})) 65 | 66 | (expect 2 (count (find-older-than derby-db {:age 10}))) 67 | 68 | 69 | ;; Failing transaction: Insert with abort. 70 | ;; Insert two rows in a transaction. The second throws a deliberate error, meaning no new rows created. 71 | 72 | 73 | (expect SQLException 74 | (jdbc/with-db-transaction [connection derby-db] 75 | (insert-person= :age_min AND age <= :age_max 38 | ``` 39 | 40 | Will generate a method that can be called in two ways: 41 | 42 | ```clojure 43 | 44 | ;; The map parameters still work as expected 45 | (find-by-name-and-age-range db {:name "Foo%" :age_min 20 :age_max 40}) 46 | 47 | ;; Positional arguments are provided in the order they appear in the SQL 48 | (find-by-name-and-age-range db "Foo%" 20 40) 49 | ``` 50 | 51 | ## Query attributes 52 | 53 | Jeesql adds the possibility to annotate queries with attributes that change the 54 | way the query is processed. I think this is better to do at the query site instead 55 | of at the call site (with yesql's call-options). 56 | 57 | Attributes are placed between the name line and the docstring. 58 | 59 | Currently two attributes are supported: single? and return-keys. 60 | 61 | 62 | ### single? 63 | 64 | Single? attribute (for selects) changes the result set processing to return a 65 | single value. 66 | 67 | ```SQL 68 | -- name: count-people-older-than 69 | -- single?: true 70 | -- Count how many people are older than the given age 71 | SELECT COUNT(*) FROM people WHERE age > :age 72 | ``` 73 | 74 | Will generate a function that returns a the count number as a single value when 75 | called, instead of a sequence of maps. 76 | 77 | ### return-keys 78 | 79 | Return keys (for insert) is a vector of strings for keys to return. 80 | This is mainly for Oracle users. Remember to use the postfix Query]] 7 | [jeesql.statement-parser :refer [tokenize]] 8 | [clojure.core.async :as async]) 9 | (:import [jeesql.types Query])) 10 | 11 | (def in-list-parameter? 12 | "Check if a type triggers IN-list expansion." 13 | (some-fn list? vector? seq? set?)) 14 | 15 | (defn- args-to-placeholders 16 | [args] 17 | (if (in-list-parameter? args) 18 | (if (empty? args) 19 | "NULL" 20 | (clojure.string/join "," (repeat (count args) "?"))) 21 | "?")) 22 | 23 | (defn- analyse-statement-tokens 24 | [tokens] 25 | {:expected-keys (set (map keyword (filter symbol? tokens))) 26 | ;; Positional ? parameters are no longer supported 27 | :expected-positional-count 0}) 28 | 29 | (defn- positional-parameter-list [tokens] 30 | (distinct (filter symbol? tokens))) 31 | 32 | (defn expected-parameter-list 33 | [query] 34 | (let [tokens (tokenize query) 35 | {:keys [expected-keys expected-positional-count]} (analyse-statement-tokens tokens)] 36 | expected-keys)) 37 | 38 | (defn rewrite-query-for-jdbc 39 | [tokens initial-args] 40 | (let [{:keys [expected-keys expected-positional-count]} (analyse-statement-tokens tokens) 41 | actual-keys (set (keys (dissoc initial-args :?))) 42 | actual-positional-count (count (:? initial-args)) 43 | missing-keys (set/difference expected-keys actual-keys)] 44 | (assert (empty? missing-keys) 45 | (format "Query argument mismatch.\nExpected keys: %s\nActual keys: %s\nMissing keys: %s" 46 | (str (seq expected-keys)) 47 | (str (seq actual-keys)) 48 | (str (seq missing-keys)))) 49 | (assert (= expected-positional-count actual-positional-count) 50 | (format (join "\n" 51 | ["Query argument mismatch." 52 | "Positional ? parameters are not supported! Got %d."]) 53 | actual-positional-count)) 54 | (let [[final-query final-parameters consumed-args] 55 | (reduce (fn [[query parameters args] token] 56 | (cond 57 | (string? token) [(str query token) 58 | parameters 59 | args] 60 | (symbol? token) (let [[arg new-args] (if (= '? token) 61 | [(first (:? args)) (update-in args [:?] rest)] 62 | [(get args (keyword token)) args])] 63 | [(str query (args-to-placeholders arg)) 64 | (vec (if (in-list-parameter? arg) 65 | (concat parameters arg) 66 | (conj parameters arg))) 67 | new-args]))) 68 | ["" [] initial-args] 69 | tokens)] 70 | (concat [final-query] final-parameters)))) 71 | 72 | ;; Maintainer's note: clojure.java.jdbc.execute! returns a list of 73 | ;; rowcounts, because it takes a list of parameter groups. In our 74 | ;; case, we only ever use one group, so we'll unpack the 75 | ;; single-element list with `first`. 76 | (defn execute-handler 77 | [db sql-and-params] 78 | (first (jdbc/execute! db sql-and-params))) 79 | 80 | (defn insert-handler 81 | [db statement-and-params] 82 | (jdbc/db-do-prepared-return-keys db statement-and-params)) 83 | 84 | (defn insert-handler-return-keys 85 | [return-keys db [statement & params]] 86 | (with-open [ps (jdbc/prepare-statement (jdbc/get-connection db) statement 87 | {:return-keys return-keys})] 88 | (jdbc/db-do-prepared-return-keys db (cons ps params)))) 89 | 90 | (defn query-handler 91 | [row-fn db sql-and-params] 92 | (jdbc/query db sql-and-params 93 | {:identifiers lower-case 94 | :row-fn row-fn 95 | :result-set-fn doall})) 96 | 97 | (defn query-handler-single-value 98 | [db sql-and-params] 99 | (jdbc/query db sql-and-params 100 | {:row-fn (comp val first seq) 101 | :result-set-fn first})) 102 | 103 | (defn query-handler-stream 104 | [fetch-size row-fn db result-channel sql-and-params] 105 | (jdbc/db-query-with-resultset 106 | db sql-and-params 107 | (fn [rs] 108 | (loop [[row & rows] (jdbc/result-set-seq rs)] 109 | (if-not row 110 | ;; No more rows, close the channel 111 | (async/close! result-channel) 112 | ;; have more rows to send 113 | (when (async/>!! result-channel (row-fn row)) 114 | ;; channel is not closed yet 115 | (recur rows))))) 116 | {:fetch-size fetch-size})) 117 | 118 | (def ^:private supported-attributes #{:single? :return-keys :default-parameters 119 | :fetch-size :row-fn}) 120 | 121 | (defn- check-attributes [attributes] 122 | (when attributes 123 | (doseq [key (keys attributes)] 124 | (assert (supported-attributes key) 125 | (str "Unsupported attribute " key 126 | ". Valid attributes are: " 127 | (join ", " supported-attributes)))))) 128 | 129 | (defn generate-query-fn 130 | "Generate a function to run a query. 131 | 132 | - If the query name ends in `!` it will call `clojure.java.jdbc/execute!`, 133 | - If the query name ends in ` time slow-query-threshold-ms)) 170 | (report-slow-queries operation-type time connection jdbc-query)) 171 | result)) 172 | [display-args generated-function] 173 | (let [default-parameters (or (:default-parameters attributes) {}) 174 | named-args (when-not (empty? required-arg-symbols) 175 | {:keys (vec required-arg-symbols)})] 176 | (cond 177 | (nil? named-args) 178 | [(list ['connection]) 179 | (fn query-wrapper-fn-noargs [connection] 180 | (real-fn connection {}))] 181 | 182 | stream? 183 | [(list ['connection 'result-channel named-args]) 184 | (fn query-wrapper-streaming 185 | [connection result-channel args] 186 | (jdbc-fn connection result-channel 187 | (rewrite-query-for-jdbc tokens 188 | (merge default-parameters args))))] 189 | 190 | (and (:positional? query-options) 191 | (< (count required-args) 20)) 192 | (let [params (positional-parameter-list tokens) 193 | keywords (map (comp keyword clojure.core/name) params)] 194 | [(list ['connection named-args] 195 | (vec (concat ['connection] params))) 196 | (fn query-wrapper-fn-positional 197 | [connection & args] 198 | (if (and (= 1 (count args)) 199 | (map? (first args))) 200 | ;; One argument that is a map 201 | (real-fn connection (merge default-parameters 202 | (first args))) 203 | 204 | ;; Given all positional args 205 | (real-fn connection (zipmap keywords args))))]) 206 | 207 | :default 208 | [(list ['connection named-args]) 209 | (fn query-wrapper-fn [connection args] 210 | (real-fn connection (merge default-parameters args)))]))] 211 | (with-meta generated-function 212 | (merge {:name name 213 | :arglists display-args 214 | ::source (str statement)} 215 | (when docstring 216 | {:doc docstring}))))) 217 | 218 | (defn generate-var 219 | ([this options] (generate-var *ns* this options)) 220 | ([ns this options] 221 | (create-root-var ns (:name this) 222 | (generate-query-fn ns this options)))) 223 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' from 19 | a Contributor if it was added to the Program by such Contributor itself or 20 | anyone acting on such Contributor's behalf. Contributions do not include 21 | additions to the Program which: (i) are separate modules of software 22 | distributed in conjunction with the Program under their own license 23 | agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement, 34 | including all Contributors. 35 | 36 | 2. GRANT OF RIGHTS 37 | a) Subject to the terms of this Agreement, each Contributor hereby grants 38 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 39 | reproduce, prepare derivative works of, publicly display, publicly perform, 40 | distribute and sublicense the Contribution of such Contributor, if any, and 41 | such derivative works, in source code and object code form. 42 | b) Subject to the terms of this Agreement, each Contributor hereby grants 43 | Recipient a non-exclusive, worldwide, royalty-free patent license under 44 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 45 | transfer the Contribution of such Contributor, if any, in source code and 46 | object code form. This patent license shall apply to the combination of the 47 | Contribution and the Program if, at the time the Contribution is added by 48 | the Contributor, such addition of the Contribution causes such combination 49 | to be covered by the Licensed Patents. The patent license shall not apply 50 | to any other combinations which include the Contribution. No hardware per 51 | se is licensed hereunder. 52 | c) Recipient understands that although each Contributor grants the licenses to 53 | its Contributions set forth herein, no assurances are provided by any 54 | Contributor that the Program does not infringe the patent or other 55 | intellectual property rights of any other entity. Each Contributor 56 | disclaims any liability to Recipient for claims brought by any other entity 57 | based on infringement of intellectual property rights or otherwise. As a 58 | condition to exercising the rights and licenses granted hereunder, each 59 | Recipient hereby assumes sole responsibility to secure any other 60 | intellectual property rights needed, if any. For example, if a third party 61 | patent license is required to allow Recipient to distribute the Program, it 62 | is Recipient's responsibility to acquire that license before distributing 63 | the Program. 64 | d) Each Contributor represents that to its knowledge it has sufficient 65 | copyright rights in its Contribution, if any, to grant the copyright 66 | license set forth in this Agreement. 67 | 68 | 3. REQUIREMENTS 69 | 70 | A Contributor may choose to distribute the Program in object code form under its 71 | own license agreement, provided that: 72 | 73 | a) it complies with the terms and conditions of this Agreement; and 74 | b) its license agreement: 75 | i) effectively disclaims on behalf of all Contributors all warranties and 76 | conditions, express and implied, including warranties or conditions of 77 | title and non-infringement, and implied warranties or conditions of 78 | merchantability and fitness for a particular purpose; 79 | ii) effectively excludes on behalf of all Contributors all liability for 80 | damages, including direct, indirect, special, incidental and 81 | consequential damages, such as lost profits; 82 | iii) states that any provisions which differ from this Agreement are offered 83 | by that Contributor alone and not by any other party; and 84 | iv) states that source code for the Program is available from such 85 | Contributor, and informs licensees how to obtain it in a reasonable 86 | manner on or through a medium customarily used for software exchange. 87 | 88 | When the Program is made available in source code form: 89 | 90 | a) it must be made available under this Agreement; and 91 | b) a copy of this Agreement must be included with each copy of the Program. 92 | Contributors may not remove or alter any copyright notices contained within 93 | the Program. 94 | 95 | Each Contributor must identify itself as the originator of its Contribution, if 96 | any, in a manner that reasonably allows subsequent Recipients to identify the 97 | originator of the Contribution. 98 | 99 | 4. COMMERCIAL DISTRIBUTION 100 | 101 | Commercial distributors of software may accept certain responsibilities with 102 | respect to end users, business partners and the like. While this license is 103 | intended to facilitate the commercial use of the Program, the Contributor who 104 | includes the Program in a commercial product offering should do so in a manner 105 | which does not create potential liability for other Contributors. Therefore, if 106 | a Contributor includes the Program in a commercial product offering, such 107 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 108 | every other Contributor ("Indemnified Contributor") against any losses, damages 109 | and costs (collectively "Losses") arising from claims, lawsuits and other legal 110 | actions brought by a third party against the Indemnified Contributor to the 111 | extent caused by the acts or omissions of such Commercial Contributor in 112 | connection with its distribution of the Program in a commercial product 113 | offering. The obligations in this section do not apply to any claims or Losses 114 | relating to any actual or alleged intellectual property infringement. In order 115 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 116 | Contributor in writing of such claim, and b) allow the Commercial Contributor to 117 | control, and cooperate with the Commercial Contributor in, the defense and any 118 | related settlement negotiations. The Indemnified Contributor may participate in 119 | any such claim at its own expense. 120 | 121 | For example, a Contributor might include the Program in a commercial product 122 | offering, Product X. That Contributor is then a Commercial Contributor. If that 123 | Commercial Contributor then makes performance claims, or offers warranties 124 | related to Product X, those performance claims and warranties are such 125 | Commercial Contributor's responsibility alone. Under this section, the 126 | Commercial Contributor would have to defend claims against the other 127 | Contributors related to those performance claims and warranties, and if a court 128 | requires any other Contributor to pay any damages as a result, the Commercial 129 | Contributor must pay those damages. 130 | 131 | 5. NO WARRANTY 132 | 133 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 134 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 135 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 136 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 137 | Recipient is solely responsible for determining the appropriateness of using and 138 | distributing the Program and assumes all risks associated with its exercise of 139 | rights under this Agreement , including but not limited to the risks and costs 140 | of program errors, compliance with applicable laws, damage to or loss of data, 141 | programs or equipment, and unavailability or interruption of operations. 142 | 143 | 6. DISCLAIMER OF LIABILITY 144 | 145 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 146 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 147 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 148 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 149 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 150 | OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 151 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 152 | 153 | 7. GENERAL 154 | 155 | If any provision of this Agreement is invalid or unenforceable under applicable 156 | law, it shall not affect the validity or enforceability of the remainder of the 157 | terms of this Agreement, and without further action by the parties hereto, such 158 | provision shall be reformed to the minimum extent necessary to make such 159 | provision valid and enforceable. 160 | 161 | If Recipient institutes patent litigation against any entity (including a 162 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 163 | (excluding combinations of the Program with other software or hardware) 164 | infringes such Recipient's patent(s), then such Recipient's rights granted under 165 | Section 2(b) shall terminate as of the date such litigation is filed. 166 | 167 | All Recipient's rights under this Agreement shall terminate if it fails to 168 | comply with any of the material terms or conditions of this Agreement and does 169 | not cure such failure in a reasonable period of time after becoming aware of 170 | such noncompliance. If all Recipient's rights under this Agreement terminate, 171 | Recipient agrees to cease use and distribution of the Program as soon as 172 | reasonably practicable. However, Recipient's obligations under this Agreement 173 | and any licenses granted by Recipient relating to the Program shall continue and 174 | survive. 175 | 176 | Everyone is permitted to copy and distribute copies of this Agreement, but in 177 | order to avoid inconsistency the Agreement is copyrighted and may only be 178 | modified in the following manner. The Agreement Steward reserves the right to 179 | publish new versions (including revisions) of this Agreement from time to time. 180 | No one other than the Agreement Steward has the right to modify this Agreement. 181 | The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation 182 | may assign the responsibility to serve as the Agreement Steward to a suitable 183 | separate entity. Each new version of the Agreement will be given a 184 | distinguishing version number. The Program (including Contributions) may always 185 | be distributed subject to the version of the Agreement under which it was 186 | received. In addition, after a new version of the Agreement is published, 187 | Contributor may elect to distribute the Program (including its Contributions) 188 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 189 | above, Recipient receives no rights or licenses to the intellectual property of 190 | any Contributor under this Agreement, whether expressly, by implication, 191 | estoppel or otherwise. All rights in the Program not expressly granted under 192 | this Agreement are reserved. 193 | 194 | This Agreement is governed by the laws of the State of New York and the 195 | intellectual property laws of the United States of America. No party to this 196 | Agreement will bring a legal action under this Agreement more than one year 197 | after the cause of action arose. Each party waives its rights to a jury trial in 198 | any resulting litigation. 199 | --------------------------------------------------------------------------------