diff --git a/README.md b/README.md index 3981f49..4657971 100644 --- a/README.md +++ b/README.md @@ -522,58 +522,81 @@ OFFSET ? ## Extensibility -_This needs a rewrite!_ +Any keyword (or symbol) that appears as the first element of a vector will be treated as a generic function unless it is declared to be an operator or "special syntax". Any keyword (or symbol) that appears as a key in a hash map will be treated as a SQL clause -- and must either be built-in or must be registered as new clauses. -You can define your own function handlers for use in `where`: +If your database supports `<=>` as an operator, you can tell HoneySQL about it using the `register-op!` function (which should be called before the first call to `honey.sql/format`): ```clojure -(defmethod fmt/fn-handler "betwixt" [_ field lower upper] - (str (fmt/to-sql field) " BETWIXT " - (fmt/to-sql lower) " AND " (fmt/to-sql upper))) +(sql/register-op! :<=>) +;; default is a binary operator: +(-> (select :a) (where [:<=> :a "foo"]) sql/format) +=> ["SELECT a WHERE a <=> ?" "foo"] +;; you can declare that an operator is variadic: +(sql/register-op! :<=> :variadic? true) +(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format) +=> ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"] +``` +Sometimes you want an operator to ignore `nil` clauses (`:and` and `:or` are declared that way): + +```clojure +(sql/register-op! :<=> :ignore-nil? true) +``` + +Or perhaps your database supports syntax like `a BETWIXT b AND c`, in which case you can use `register-fn!` to tell HoneySQL about it (again, called before the first call to `honey.sql/format`): + +```clojure +;; the formatter will be passed your new operator (function) and a +;; sequence of the arguments provided to it (so you can write any arity ops): +(sql/register-fn! :betwixt + (fn [op [a b c]] + (let [[sql-a & params-a] (sql/format-expr a) + [sql-b & params-b] (sql/format-expr b) + [sql-c & params-c] (sql/format-expr c)] + (-> [(str sql-a " " (sql/sql-kw op) " " + sql-b " AND " sql-c)] + (into params-a) + (into params-b) + (into params-c))))) +;; example usage: (-> (select :a) (where [:betwixt :a 1 10]) sql/format) => ["SELECT a WHERE a BETWIXT ? AND ?" 1 10] ``` -You can also define your own clauses: +You can also register SQL clauses, specifying the keyword, the formatting function, and an existing clause that this new clause should be processed before: ```clojure -;; Takes a MapEntry of the operator & clause data, plus the entire SQL map -(defmethod fmt/format-clause :foobar [[op v] sqlmap] - (str "FOOBAR " (fmt/to-sql v))) -``` -```clojure +;; the formatter will be passed your new clause and the value associated +;; with that clause in the DSL (which is often a sequence but does not +;; need to be -- it can be whatever syntax you desire in the DSL): +(sql/register-clause! :foobar + (fn [clause x] + (let [[sql & params] + (if (keyword? x) + (sql/format-expr x) + (sql/format-dsl x))] + (into [(str (sql/sql-kw clause) " " sql)] params))) + :from) ; SELECT ... FOOBAR ... FROM ... +;; example usage: (sql/format {:select [:a :b] :foobar :baz}) => ["SELECT a, b FOOBAR baz"] -``` -```clojure -(require '[honeysql.helpers :refer [defhelper]]) - -;; Defines a helper function, and allows 'build' to recognize your clause -(defhelper foobar [m args] - (assoc m :foobar (first args))) -``` -```clojure -(-> (select :a :b) (foobar :baz) sql/format) -=> ["SELECT a, b FOOBAR baz"] - +(sql/format {:select [:a :b] :foobar {:where [:= :id 1]}}) +=> ["SELECT a, b FOOBAR WHERE id = ?" 1] ``` -When adding a new clause, you may also need to register it with a specific priority so that it formats correctly, for example: - -```clojure -(fmt/register-clause! :foobar 110) -``` - -If you do implement a clause or function handler for an ANSI SQL, consider submitting a pull request so others can use it, too. For non-standard clauses and/or functions, look for a library that extends `honeysql` for that specific database or create one, if no such library exists. +If you find yourself registering an operator, a function (syntax), or a new clause, consider submitting a [pull request to HoneySQL](https://github.com/seancorfield/honeysql/pulls) so others can use it, too. If it is dialect-specific, let me know in the pull request. ## Why does my parameter get emitted as `()`? +_Need to investigate whether this is still true in 2.0!_ + If you want to use your own datatype as a parameter then the idiomatic approach of implementing `next.jdbc`'s [`SettableParameter`](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.prepare#SettableParameter) -or `clojure.java.jdbc`'s [`ISQLValue`](https://clojure.github.io/java.jdbc/#clojure.java.jdbc/ISQLValue) protocol isn't enough as `honeysql` won't correct pass through your datatype, rather it will interpret it incorrectly. +or `clojure.java.jdbc`'s [`ISQLValue`](https://clojure.github.io/java.jdbc/#clojure.java.jdbc/ISQLValue) protocol isn't enough as HoneySQL won't correct pass through your datatype, rather it will interpret it incorrectly. -To teach `honeysql` how to handle your datatype you need to implement [`honeysql.format/ToSql`](https://github.com/seancorfield/honeysql/blob/a9dffec632be62c961be7d9e695d0b2b85732c53/src/honeysql/format.cljc#L94). For example: +_This bit no longer exists:_ + +To teach HoneySQL how to handle your datatype you need to implement [`honeysql.format/ToSql`](https://github.com/seancorfield/honeysql/blob/a9dffec632be62c961be7d9e695d0b2b85732c53/src/honeysql/format.cljc#L94). For example: ``` clojure ;; given: (defrecord MyDateWrapper [...] @@ -599,7 +622,7 @@ To teach `honeysql` how to handle your datatype you need to implement [`honeysql ## Extensions -* [For PostgreSQL-specific extensions falling outside of ANSI SQL](https://github.com/nilenso/honeysql-postgres) +* [For PostgreSQL-specific extensions falling outside of ANSI SQL](https://github.com/nilenso/honeysql-postgres) -- these will all be core in 2.0! ## License diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 633cea4..1d3511f 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -82,7 +82,7 @@ #?(:clj (fn [^String s] (.. s toString (toUpperCase (java.util.Locale/US)))) :cljs str/upper-case)) -(defn- sql-kw [k] +(defn sql-kw [k] (-> k (name) (upper-case) (str/replace "-" " "))) (defn- format-entity [x & [{:keys [aliased? drop-ns?]}]] @@ -444,7 +444,7 @@ ;; ;do-nothing ;:returning -(defn- format-dsl [x & [{:keys [aliased? nested? pretty?]}]] +(defn format-dsl [x & [{:keys [aliased? nested? pretty?]}]] (let [[sqls params leftover] (reduce (fn [[sql params leftover] k] (if-let [xs (or (k x) (let [s (symbol (name k))] (get x s)))] diff --git a/src/readme.clj b/src/readme.clj deleted file mode 100644 index 871f71a..0000000 --- a/src/readme.clj +++ /dev/null @@ -1,607 +0,0 @@ -(ns readme (:require [seancorfield.readme])) - - - - - - - - - - - - - - - - - - - - - - - -(seancorfield.readme/defreadme readme-25 -(refer-clojure :exclude '[for group-by set update]) -(require '[honey.sql :as sql] - ;; caution: this overwrites for, group-by, set, and update - '[honey.sql.helpers :refer :all :as helpers]) -) - - - -(seancorfield.readme/defreadme readme-34 -(def sqlmap {:select [:a :b :c] - :from [:foo] - :where [:= :f.a "baz"]}) -) - - - - - - - -(seancorfield.readme/defreadme readme-46 -(sql/format sqlmap) -=> ["SELECT a, b, c FROM foo WHERE f.a = ?" "baz"] -) - - - - - - - - - - - - - - - - - -(seancorfield.readme/defreadme readme-67 -(def q-sqlmap {:select [:foo/a :foo/b :foo/c] - :from [:foo] - :where [:= :foo/a "baz"]}) -(sql/format q-sqlmap) -=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"] -) - - - - - - - -(seancorfield.readme/defreadme readme-81 -(-> (select :a :b :c) - (from :foo) - (where [:= :f.a "baz"])) -) - - - -(seancorfield.readme/defreadme readme-89 -(= (-> (select :*) (from :foo)) - (-> (from :foo) (select :*))) -=> true -) - - - -(seancorfield.readme/defreadme readme-97 -(-> sqlmap (select :d)) -=> '{:from [:foo], :where [:= :f.a "baz"], :select [:a :b :c :d]} -) - - - -(seancorfield.readme/defreadme readme-104 -(-> sqlmap - (dissoc :select) - (select :*) - (where [:> :b 10]) - sql/format) -=> ["SELECT * FROM foo WHERE (f.a = ?) AND (b > ?)" "baz" 10] -) - - - -(seancorfield.readme/defreadme readme-115 -(-> (select :*) - (from :foo) - (where [:= :a 1] [:< :b 100]) - sql/format) -=> ["SELECT * FROM foo WHERE (a = ?) AND (b < ?)" 1 100] -) - - - - -(seancorfield.readme/defreadme readme-126 -(-> (select :a [:b :bar] :c [:d :x]) - (from [:foo :quux]) - (where [:= :quux.a 1] [:< :bar 100]) - sql/format) -=> ["SELECT a, b AS bar, c, d AS x FROM foo quux WHERE (quux.a = ?) AND (bar < ?)" 1 100] -) - - - - - - - - - - -(seancorfield.readme/defreadme readme-143 -(-> (insert-into :properties) - (columns :name :surname :age) - (values - [["Jon" "Smith" 34] - ["Andrew" "Cooper" 12] - ["Jane" "Daniels" 56]]) - (sql/format {:pretty? true})) -=> [" -INSERT INTO properties (name, surname, age) -VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) -" -"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56] -) - - - - - -(seancorfield.readme/defreadme readme-162 -(-> (insert-into :properties) - (values [{:name "John" :surname "Smith" :age 34} - {:name "Andrew" :surname "Cooper" :age 12} - {:name "Jane" :surname "Daniels" :age 56}]) - (sql/format {:pretty? true})) -=> [" -INSERT INTO properties (name, surname, age) -VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) -" -"John" "Smith" 34 -"Andrew" "Cooper" 12 -"Jane" "Daniels" 56] -) - - - - - -(seancorfield.readme/defreadme readme-181 -(let [user-id 12345 - role-name "user"] - (-> (insert-into :user_profile_to_role) - (values [{:user_profile_id user-id - :role_id (-> (select :id) - (from :role) - (where [:= :name role-name]))}]) - (sql/format {:pretty? true}))) - -=> [" -INSERT INTO user_profile_to_role (user_profile_id, role_id) -VALUES (?, (SELECT id FROM role WHERE name = ?)) -" -12345 -"user"] -) - -(seancorfield.readme/defreadme readme-199 -(-> (select :*) - (from :foo) - (where [:in :foo.a (-> (select :a) (from :bar))]) - sql/format) -=> ["SELECT * FROM foo WHERE (foo.a in (SELECT a FROM bar))"] -) - - - - - -(seancorfield.readme/defreadme readme-211 -(-> (insert-into :comp_table) - (columns :name :comp_column) - (values - [["small" (composite 1 "inch")] - ["large" (composite 10 "feet")]]) - (sql/format {:pretty? true})) -=> [" -INSERT INTO comp_table (name, comp_column) -VALUES (?, (?, ?)), (?, (?, ?)) -" -"small" 1 "inch" "large" 10 "feet"] -) - - - - - -(seancorfield.readme/defreadme readme-229 -(-> (helpers/update :films) - (set {:kind "dramatic" - :watched [:+ :watched 1]}) - (where [:= :kind "drama"]) - (sql/format {:pretty? true})) -=> [" -UPDATE films SET kind = ?, watched = (watched + ?) -WHERE kind = ? -" -"dramatic" -1 -"drama"] -) - - - - - - - - - - - - -(seancorfield.readme/defreadme readme-255 -(-> (delete-from :films) - (where [:<> :kind "musical"]) - (sql/format)) -=> ["DELETE FROM films WHERE kind <> ?" "musical"] -) - - - -(seancorfield.readme/defreadme readme-264 -(-> (delete [:films :directors]) - (from :films) - (join :directors [:= :films.director_id :directors.id]) - (where [:<> :kind "musical"]) - (sql/format {:pretty? true})) -=> [" -DELETE films, directors -FROM films -INNER JOIN directors ON films.director_id = directors.id -WHERE kind <> ? -" -"musical"] -) - - - -(seancorfield.readme/defreadme readme-281 -(-> (truncate :films) - (sql/format)) -=> ["TRUNCATE films"] -) - - - - - -(seancorfield.readme/defreadme readme-291 -(sql/format {:union [(-> (select :*) (from :foo)) - (-> (select :*) (from :bar))]}) -=> ["SELECT * FROM foo UNION SELECT * FROM bar"] -) - - - - - -(seancorfield.readme/defreadme readme-301 -(-> (select :%count.*) (from :foo) sql/format) -=> ["SELECT count(*) FROM foo"] -) -(seancorfield.readme/defreadme readme-305 -(-> (select :%max.id) (from :foo) sql/format) -=> ["SELECT max(id) FROM foo"] -) - - - - - - - -(seancorfield.readme/defreadme readme-316 -(-> (select :id) - (from :foo) - (where [:= :a :?baz]) - (sql/format {:params {:baz "BAZ"}})) -=> ["SELECT id FROM foo WHERE (a = ?)" "BAZ"] -) - - - - - - -(seancorfield.readme/defreadme readme-329 -(def call-qualify-map - (-> (select [[:foo :bar]] [[:raw "@var := foo.bar"]]) - (from :foo) - (where [:= :a [:param :baz]] [:= :b [:inline 42]]))) -) -(seancorfield.readme/defreadme readme-335 -call-qualify-map -=> '{:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]] - :from (:foo) - :select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]} -) -(seancorfield.readme/defreadme readme-341 -(sql/format call-qualify-map {:params {:baz "BAZ"}}) -=> ["SELECT foo(bar), @var := foo.bar FROM foo WHERE (a = ?) AND (b = 42)" "BAZ"] -) - - - - - - -(seancorfield.readme/defreadme readme-351 -(-> (insert-into :sample) - (values [{:location [:ST_SetSRID - [:ST_MakePoint 0.291 32.621] - [:cast 4325 :integer]]}]) - (sql/format {:pretty? true})) -=> [" -INSERT INTO sample (location) -VALUES (ST_SetSRID(ST_MakePoint(?, ?), CAST(? AS integer))) -" -0.291 32.621 4326] -) - - - - - - - - - - - - - - -(seancorfield.readme/defreadme readme-377 -(-> (select :*) - (from :foo) - (where [:< :expired_at [:raw ["now() - '" 5 " seconds'"]]]) - (sql/format {:foo 5})) -=> ["SELECT * FROM foo WHERE expired_at < now() - '? seconds'" 5] -) - -(seancorfield.readme/defreadme readme-385 -(-> (select :*) - (from :foo) - (where [:< :expired_at [:raw ["now() - '" [:param :t] " seconds'"]]]) - (sql/format {:t 5})) -=> ["SELECT * FROM foo WHERE expired_at < now() - '? seconds'" 5] -) - - - - - - - - - - -(seancorfield.readme/defreadme readme-402 -(-> (select :foo.a) - (from :foo) - (where [:= :foo.a "baz"]) - (sql/format {:dialect :mysql})) -=> ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"] -) - - - - - - - - - - - - - - - - - -(seancorfield.readme/defreadme readme-426 -(-> (select :foo.a) - (from :foo) - (where [:= :foo.a "baz"]) - (for :update) - (format)) -=> ["SELECT foo.a FROM foo WHERE (foo.a = ?) FOR UPDATE" "baz"] -) - - - -(seancorfield.readme/defreadme readme-437 -(sql/format {:select [:*] :from :foo - :where [:= :name [:inline "Jones"]] - :lock [:in-share-mode]} - {:dialect :mysql :quoted false}) -=> ["SELECT * FROM foo WHERE name = 'Jones' LOCK IN SHARE MODE"] -) - - -(seancorfield.readme/defreadme readme-446 -(sql/format - {:select [:f.foo-id :f.foo-name] - :from [[:foo-bar :f]] - :where [:= :f.foo-id 12345]} - {:allow-dashed-names? true ; not implemented yet - :quoted true}) -=> ["SELECT \"f\".\"foo-id\", \"f\".\"foo-name\" FROM \"foo-bar\" \"f\" WHERE \"f\".\"foo-id\" = ?" 12345] -) - - - - - -(seancorfield.readme/defreadme readme-460 -(def big-complicated-map - (-> (select :f.* :b.baz :c.quux [:b.bla "bla-bla"] - [[:now]] [[:raw "@x := 10"]]) - #_(modifiers :distinct) ; this is not implemented yet - (from [:foo :f] [:baz :b]) - (join :draq [:= :f.b :draq.x]) - (left-join [:clod :c] [:= :f.a :c.d]) - (right-join :bock [:= :bock.z :c.e]) - (where [:or - [:and [:= :f.a "bort"] [:not= :b.baz [:param :param1]]] - [:< 1 2 3] - [:in :f.e [1 [:param :param2] 3]] - [:between :f.e 10 20]]) - (group-by :f.a :c.e) - (having [:< 0 :f.e]) - (order-by [:b.baz :desc] :c.quux [:f.a :nulls-first]) - (limit 50) - (offset 10))) -) -(seancorfield.readme/defreadme readme-480 -big-complicated-map -=> {:select [:f.* :b.baz :c.quux [:b.bla "bla-bla"] - [[:now]] [[:raw "@x := 10"]]] - :modifiers [:distinct] - :from [[:foo :f] [:baz :b]] - :join [:draq [:= :f.b :draq.x]] - :left-join [[:clod :c] [:= :f.a :c.d]] - :right-join [:bock [:= :bock.z :c.e]] - :where [:or - [:and [:= :f.a "bort"] [:not= :b.baz [:param :param1]]] - [:< 1 2 3] - [:in :f.e [1 [:param :param2] 3]] - [:between :f.e 10 20]] - :group-by [:f.a :c.e] - :having [:< 0 :f.e] - :order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]] - :limit 50 - :offset 10} -) -(seancorfield.readme/defreadme readme-500 -(sql/format big-complicated-map {:param1 "gabba" :param2 2}) -=> [" -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 -WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) -GROUP BY f.a, c.e -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] -) -(seancorfield.readme/defreadme readme-517 -;; Printable and readable -(= big-complicated-map (read-string (pr-str big-complicated-map))) -=> true -) - - - - - - - -(seancorfield.readme/defreadme readme-529 -(defmethod fmt/fn-handler "betwixt" [_ field lower upper] - (str (fmt/to-sql field) " BETWIXT " - (fmt/to-sql lower) " AND " (fmt/to-sql upper))) - -(-> (select :a) (where [:betwixt :a 1 10]) sql/format) -=> ["SELECT a WHERE a BETWIXT ? AND ?" 1 10] -) - - - -(seancorfield.readme/defreadme readme-540 -;; Takes a MapEntry of the operator & clause data, plus the entire SQL map -(defmethod fmt/format-clause :foobar [[op v] sqlmap] - (str "FOOBAR " (fmt/to-sql v))) -) -(seancorfield.readme/defreadme readme-545 -(sql/format {:select [:a :b] :foobar :baz}) -=> ["SELECT a, b FOOBAR baz"] -) -(seancorfield.readme/defreadme readme-549 -(require '[honeysql.helpers :refer [defhelper]]) - -;; Defines a helper function, and allows 'build' to recognize your clause -(defhelper foobar [m args] - (assoc m :foobar (first args))) -) -(seancorfield.readme/defreadme readme-556 -(-> (select :a :b) (foobar :baz) sql/format) -=> ["SELECT a, b FOOBAR baz"] - -) - - - -(seancorfield.readme/defreadme readme-564 -(fmt/register-clause! :foobar 110) -) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -