diff --git a/.gitignore b/.gitignore index 9cb116f..14e0629 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +*~ +.sw* +*.swp /target /lib /classes diff --git a/project.clj b/project.clj index 2019952..f5c4321 100644 --- a/project.clj +++ b/project.clj @@ -5,4 +5,23 @@ :url "https://github.com/jkk/honeysql" :scm {:name "git" :url "https://github.com/jkk/honeysql"} - :dependencies [[org.clojure/clojure "1.8.0"]]) + :dependencies [[org.clojure/clojure "1.8.0"]] + :cljsbuild {:builds {:release {:source-paths ["src"] + :compiler {:output-to "dist/honeysql.js" + :optimizations :advanced + :output-wrapper false + :parallel-build true + :pretty-print false}} + :test {:source-paths ["src" "test"] + :compiler {:output-to "target/test/honeysql.js" + :output-dir "target/test" + :source-map true + :main honeysql.test + :parallel-build true + :target :nodejs}}}} + :doo {:build "test"} + :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"] + [org.clojure/clojurescript "1.9.89"] + [cljsbuild "1.1.3"]] + :plugins [[lein-cljsbuild "1.1.3"] + [lein-doo "0.1.6"]]}}) diff --git a/src/honeysql/core.clj b/src/honeysql/core.cljc similarity index 81% rename from src/honeysql/core.clj rename to src/honeysql/core.cljc index cb4f03a..827ac58 100644 --- a/src/honeysql/core.clj +++ b/src/honeysql/core.cljc @@ -3,15 +3,15 @@ (:require [honeysql.format :as format] [honeysql.types :as types] [honeysql.helpers :refer [build-clause]] - [honeysql.util :refer [defalias]] + #?(:clj [honeysql.util :refer [defalias]]) [clojure.string :as string])) -(defalias call types/call) -(defalias raw types/raw) -(defalias param types/param) -(defalias format format/format) -(defalias format-predicate format/format-predicate) -(defalias quote-identifier format/quote-identifier) +(#?(:clj defalias :cljs def) call types/call) +(#?(:clj defalias :cljs def) raw types/raw) +(#?(:clj defalias :cljs def) param types/param) +(#?(:clj defalias :cljs def) format format/format) +(#?(:clj defalias :cljs def) format-predicate format/format-predicate) +(#?(:clj defalias :cljs def) quote-identifier format/quote-identifier) (defn qualify "Takes one or more keyword or string qualifers and name. Returns diff --git a/src/honeysql/format.clj b/src/honeysql/format.cljc similarity index 86% rename from src/honeysql/format.clj rename to src/honeysql/format.cljc index 974819d..d9509d9 100644 --- a/src/honeysql/format.clj +++ b/src/honeysql/format.cljc @@ -1,8 +1,9 @@ (ns honeysql.format (:refer-clojure :exclude [format]) - (:require [honeysql.types :refer [call raw param param-name]] + (:require [honeysql.types :refer [call raw param param-name + #?@(:cljs [SqlCall SqlRaw SqlParam SqlArray])]] [clojure.string :as string]) - (:import [honeysql.types SqlCall SqlRaw SqlParam SqlArray])) + #?(:clj (:import [honeysql.types SqlCall SqlRaw SqlParam SqlArray]))) ;;(set! *warn-on-reflection* true) @@ -203,7 +204,10 @@ (defn sort-clauses [clauses] (let [m @clause-store] - (sort-by #(m % Long/MAX_VALUE) clauses))) + (sort-by + (fn [c] + (m c #?(:clj Long/MAX_VALUE :cljs js/Number.MAX_VALUE))) + clauses))) (defn format "Takes a SQL map and optional input parameters and returns a vector @@ -243,11 +247,22 @@ (defprotocol Parameterizable (to-params [value pname])) +(defn to-params-seq [s pname] + (paren-wrap (comma-join (mapv #(to-params % pname) s)))) + +(defn to-params-default [value pname] + (swap! *params* conj value) + (swap! *param-names* conj pname) + (*parameterizer*)) + (extend-protocol Parameterizable - clojure.lang.Sequential - (to-params [value pname] - (paren-wrap (comma-join (mapv #(to-params % pname) value)))) - clojure.lang.IPersistentSet + #?@(:clj + [clojure.lang.Sequential + + (to-params [value pname] + (to-params-seq value pname))]) + #?(:clj clojure.lang.IPersistentSet + :cljs cljs.core/PersistentHashSet) (to-params [value pname] (to-params (seq value) pname)) nil @@ -255,11 +270,14 @@ (swap! *params* conj value) (swap! *param-names* conj pname) (*parameterizer*)) - java.lang.Object + #?(:clj Object :cljs default) (to-params [value pname] - (swap! *params* conj value) - (swap! *param-names* conj pname) - (*parameterizer*))) + #?(:clj + (to-params-default value pname) + :cljs + (if (sequential? value) + (to-params-seq value pname) + (to-params-default value pname))))) (defn add-param [pname pval] (to-params pval pname)) @@ -279,8 +297,34 @@ (declare -format-clause) +(defn map->sql [m] + (let [clause-ops (sort-clauses (keys m)) + sql-str (binding [*subquery?* true + *fn-context?* false] + (space-join + (map (comp #(-format-clause % m) #(find m %)) + clause-ops)))] + (if *subquery?* + (paren-wrap sql-str) + sql-str))) + +(defn seq->sql [x] + (if *fn-context?* + ;; list argument in fn call + (paren-wrap (comma-join (map to-sql x))) + ;; alias + (str (to-sql (first x)) + ; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it + (if (= :select *clause*) + " AS " + " ") + (if (string? (second x)) + (quote-identifier (second x)) + (to-sql (second x)))))) + (extend-protocol ToSql - clojure.lang.Keyword + #?(:clj clojure.lang.Keyword + :cljs cljs.core/Keyword) (to-sql [x] (let [s (name x)] (case (.charAt s 0) @@ -288,25 +332,15 @@ (to-sql (apply call (map keyword call-args)))) \? (to-sql (param (keyword (subs s 1)))) (quote-identifier x)))) - clojure.lang.Symbol + #?(:clj clojure.lang.Symbol + :cljs cljs.core/Symbol) (to-sql [x] (quote-identifier x)) - java.lang.Boolean + #?(:clj java.lang.Boolean :cljs boolean) (to-sql [x] (if x "TRUE" "FALSE")) - clojure.lang.Sequential - (to-sql [x] - (if *fn-context?* - ;; list argument in fn call - (paren-wrap (comma-join (map to-sql x))) - ;; alias - (str (to-sql (first x)) - ; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it - (if (= :select *clause*) - " AS " - " ") - (if (string? (second x)) - (quote-identifier (second x)) - (to-sql (second x)))))) + #?@(:clj + [clojure.lang.Sequential + (to-sql [x] (seq->sql x))]) SqlCall (to-sql [x] (binding [*fn-context?* true] @@ -315,18 +349,12 @@ (apply fn-handler fn-name (.-args x))))) SqlRaw (to-sql [x] (.-s x)) - clojure.lang.IPersistentMap + #?(:clj clojure.lang.IPersistentMap + :cljs cljs.core/PersistentArrayMap) (to-sql [x] - (let [clause-ops (sort-clauses (keys x)) - sql-str (binding [*subquery?* true - *fn-context?* false] - (space-join - (map (comp #(-format-clause % x) #(find x %)) - clause-ops)))] - (if *subquery?* - (paren-wrap sql-str) - sql-str))) - clojure.lang.IPersistentSet + (map->sql x)) + #?(:clj clojure.lang.IPersistentSet + :cljs cljs.core/PersistentHashSet) (to-sql [x] (to-sql (seq x))) nil @@ -342,9 +370,15 @@ SqlArray (to-sql [x] (str "ARRAY[" (comma-join (map to-sql (.-values x))) "]")) - Object + #?(:clj Object :cljs default) (to-sql [x] - (add-anon-param x))) + #?(:clj (add-anon-param x) + :cljs (if (sequential? x) + (seq->sql x) + (add-anon-param x)))) + #?@(:cljs + [cljs.core/PersistentHashMap + (to-sql [x] (map->sql x))])) (defn sqlable? [x] (satisfies? ToSql x)) diff --git a/src/honeysql/helpers.clj b/src/honeysql/helpers.cljc similarity index 87% rename from src/honeysql/helpers.clj rename to src/honeysql/helpers.cljc index 69dde96..1d3b517 100644 --- a/src/honeysql/helpers.clj +++ b/src/honeysql/helpers.cljc @@ -1,5 +1,6 @@ (ns honeysql.helpers - (:refer-clojure :exclude [update])) + (:refer-clojure :exclude [update]) + #?(:cljs (:require-macros [honeysql.helpers :refer [defhelper]]))) (defmulti build-clause (fn [name & args] name)) @@ -10,24 +11,27 @@ (defn plain-map? [m] (and (map? m) - (not (instance? clojure.lang.IRecord m)))) + (not (record? m)))) -(defmacro defhelper [helper arglist & more] - (let [kw (keyword (name helper))] - `(do - (defmethod build-clause ~kw ~(into ['_] arglist) ~@more) - (doto (defn ~helper [& args#] - (let [[m# args#] (if (plain-map? (first args#)) - [(first args#) (rest args#)] - [{} args#])] - (build-clause ~kw m# args#))) - ;; maintain the original arglist instead of getting - ;; ([& args__6880__auto__]) - (alter-meta! - assoc - :arglists - '(~(into [] (rest arglist)) - ~(into [(first arglist)] (rest arglist)))))))) +#?(:clj + (defmacro defhelper [helper arglist & more] + (let [kw (keyword (name helper))] + `(do + (defmethod build-clause ~kw ~(into ['_] arglist) ~@more) + (defn ~helper [& args#] + (let [[m# args#] (if (plain-map? (first args#)) + [(first args#) (rest args#)] + [{} args#])] + (build-clause ~kw m# args#))) + + ;; maintain the original arglist instead of getting + ;; ([& args__6880__auto__]) + (alter-meta! + (var ~helper) + assoc + :arglists + '(~(into [] (rest arglist)) + ~(into [(first arglist)] (rest arglist)))))))) (defn collify [x] (if (coll? x) x [x])) @@ -233,7 +237,7 @@ (defn delete-from ([table] (delete-from nil table)) ([m table] (build-clause :delete-from m table))) - + (defmethod build-clause :with [_ m ctes] (assoc m :with ctes)) diff --git a/src/honeysql/types.clj b/src/honeysql/types.clj deleted file mode 100644 index bc746d7..0000000 --- a/src/honeysql/types.clj +++ /dev/null @@ -1,81 +0,0 @@ -(ns honeysql.types) - -(defrecord SqlCall [name args]) - -(defn call - "Represents a SQL function call. Name should be a keyword." - [name & args] - (SqlCall. name args)) - -(defn read-sql-call [form] - ;; late bind so that we get new class on REPL reset - (apply (resolve `call) form)) - -(defmethod print-method SqlCall [^SqlCall o ^java.io.Writer w] - (.write w (str "#sql/call " (pr-str (into [(.-name o)] (.-args o)))))) - -(defmethod print-dup SqlCall [o w] - (print-method o w)) - -;;;; - -(defrecord SqlRaw [s]) - -(defn raw - "Represents a raw SQL string" - [s] - (SqlRaw. (str s))) - -(defn read-sql-raw [form] - ;; late bind, as above - ((resolve `raw) form)) - -(defmethod print-method SqlRaw [^SqlRaw o ^java.io.Writer w] - (.write w (str "#sql/raw " (pr-str (.-s o))))) - -(defmethod print-dup SqlRaw [o w] - (print-method o w)) - -;;;; - -(defrecord SqlParam [name]) - -(defn param - "Represents a SQL parameter which can be filled in later" - [name] - (SqlParam. name)) - -(defn param-name [^SqlParam param] - (.-name param)) - -(defn read-sql-param [form] - ;; late bind, as above - ((resolve `param) form)) - -(defmethod print-method SqlParam [^SqlParam o ^java.io.Writer w] - (.write w (str "#sql/param " (pr-str (.-name o))))) - -(defmethod print-dup SqlParam [o w] - (print-method o w)) - -;;;; - -(defrecord SqlArray [values]) - -(defn array - "Represents a SQL array." - [values] - (SqlArray. values)) - -(defn array-vals [^SqlArray a] - (.-values a)) - -(defn read-sql-array [form] - ;; late bind, as above - ((resolve `array) form)) - -(defmethod print-method SqlArray [^SqlArray a ^java.io.Writer w] - (.write w (str "#sql/array " (pr-str (.-values a))))) - -(defmethod print-dup SqlArray [a w] - (print-method a w)) diff --git a/src/honeysql/types.cljc b/src/honeysql/types.cljc new file mode 100644 index 0000000..35344fc --- /dev/null +++ b/src/honeysql/types.cljc @@ -0,0 +1,84 @@ +(ns honeysql.types + (:refer-clojure :exclude [array])) + +(defrecord SqlCall [name args]) + +(defn call + "Represents a SQL function call. Name should be a keyword." + [name & args] + (SqlCall. name args)) + +(defn read-sql-call [form] + ;; late bind so that we get new class on REPL reset + (apply #?(:clj (resolve `call) :cljs call) form)) + +;;;; + +(defrecord SqlRaw [s]) + +(defn raw + "Represents a raw SQL string" + [s] + (SqlRaw. (str s))) + +(defn read-sql-raw [form] + ;; late bind, as above + (#?(:clj (resolve `raw) :cljs raw) form)) + +;;;; + +(defrecord SqlParam [name]) + +(defn param + "Represents a SQL parameter which can be filled in later" + [name] + (SqlParam. name)) + +(defn param-name [^SqlParam param] + (.-name param)) + +(defn read-sql-param [form] + ;; late bind, as above + (#?(:clj (resolve `param) :cljs param) form)) + +;;;; + +(defrecord SqlArray [values]) + +(defn array + "Represents a SQL array." + [values] + (SqlArray. values)) + +(defn array-vals [^SqlArray a] + (.-values a)) + +(defn read-sql-array [form] + ;; late bind, as above + (#?(:clj (resolve `array) :cljs array) form)) + +#?(:clj + (do + (defmethod print-method SqlCall [^SqlCall o ^java.io.Writer w] + (.write w (str "#sql/call " (pr-str (into [(.-name o)] (.-args o)))))) + + (defmethod print-dup SqlCall [o w] + (print-method o w)) + + (defmethod print-method SqlRaw [^SqlRaw o ^java.io.Writer w] + (.write w (str "#sql/raw " (pr-str (.s o))))) + + (defmethod print-dup SqlRaw [o w] + (print-method o w)) + + (defmethod print-method SqlParam [^SqlParam o ^java.io.Writer w] + (.write w (str "#sql/param " (pr-str (.name o))))) + + (defmethod print-dup SqlParam [o w] + (print-method o w)) + + (defmethod print-method SqlArray [^SqlArray a ^java.io.Writer w] + (.write w (str "#sql/array " (pr-str (.values a))))) + + (defmethod print-dup SqlArray [a w] + (print-method a w)))) diff --git a/test/honeysql/core_test.clj b/test/honeysql/core_test.cljc similarity index 93% rename from test/honeysql/core_test.clj rename to test/honeysql/core_test.cljc index 61e720b..8a87a45 100644 --- a/test/honeysql/core_test.clj +++ b/test/honeysql/core_test.cljc @@ -1,8 +1,13 @@ (ns honeysql.core-test (:refer-clojure :exclude [format update]) - (:require [clojure.test :refer [deftest testing is]] + (:require [#?@(:clj [clojure.test :refer] + :cljs [cljs.test :refer-macros]) [deftest testing is]] [honeysql.core :as sql] - [honeysql.helpers :refer :all])) + [honeysql.helpers :refer [select modifiers from join left-join + right-join full-join where group having + order-by limit offset values columns + insert-into]] + honeysql.format-test)) ;; TODO: more tests @@ -55,8 +60,8 @@ (is (= ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? " "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] (sql/format m1 {:param1 "gabba" :param2 2})))) - (testing "SQL data prints and reads correctly" - (is (= m1 (read-string (pr-str m1))))) + #?(:clj (testing "SQL data prints and reads correctly" + (is (= m1 (read-string (pr-str m1)))))) (testing "SQL data formats correctly with alternate param naming" (is (= (sql/format m1 :params {:param1 "gabba" :param2 2} :parameterizer :postgresql) ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $1 AND b.baz <> $2) OR ($3 < $4 AND $5 < $6) OR (f.e in ($7, $8, $9)) OR f.e BETWEEN $10 AND $11) GROUP BY f.a HAVING $12 < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT $13 OFFSET $14 " @@ -174,3 +179,5 @@ (from :foo) (join :x [:= :foo.id :x.id] :y nil) sql/format))))) + +#?(:cljs (cljs.test/run-all-tests)) diff --git a/test/honeysql/format_test.clj b/test/honeysql/format_test.cljc similarity index 90% rename from test/honeysql/format_test.clj rename to test/honeysql/format_test.cljc index d35df79..5338eea 100644 --- a/test/honeysql/format_test.clj +++ b/test/honeysql/format_test.cljc @@ -1,7 +1,10 @@ (ns honeysql.format-test (:refer-clojure :exclude [format]) - (:require [clojure.test :refer [deftest testing is are]] - [honeysql.format :refer :all])) + (:require [#?@(:clj [clojure.test :refer] + :cljs [cljs.test :refer-macros]) [deftest testing is are]] + [honeysql.types :as sql] + [honeysql.format :refer + [*allow-dashed-names?* quote-identifier format-clause format]])) (deftest test-quote (are @@ -66,11 +69,11 @@ (deftest array-test (is (= (format {:insert-into :foo :columns [:baz] - :values [[#sql/array [1 2 3 4]]]}) + :values [[(sql/array [1 2 3 4])]]}) ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4])) (is (= (format {:insert-into :foo :columns [:baz] - :values [[#sql/array ["one" "two" "three"]]]}) + :values [[(sql/array ["one" "two" "three"])]]}) ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?])" "one" "two" "three"]))) (deftest union-test diff --git a/test/honeysql/test.cljs b/test/honeysql/test.cljs new file mode 100644 index 0000000..f1642ba --- /dev/null +++ b/test/honeysql/test.cljs @@ -0,0 +1,9 @@ +(ns honeysql.test + (:require + [doo.runner :refer-macros [doo-tests]] + [cljs.test :as t :refer-macros [is are deftest testing]] + honeysql.core-test + honeysql.format-test)) + +(doo-tests 'honeysql.core-test + 'honeysql.format-test)