diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index b348933..9691209 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -1,11 +1,5 @@ {:cljdoc.doc/tree [["Readme" {:file "README.md"}] ["Changes" {:file "CHANGELOG.md"}] ["Getting Started" {:file "doc/getting-started.md"} - #_["Friendly SQL Functions" {:file "doc/friendly-sql-functions.md"}] - #_["Tips & Tricks" {:file "doc/tips-and-tricks.md"}] - #_["Result Set Builders" {:file "doc/result-set-builders.md"}] - #_["Prepared Statements" {:file "doc/prepared-statements.md"}] - #_["Transactions" {:file "doc/transactions.md"}]] - #_["All The Options" {:file "doc/all-the-options.md"}] - #_["datafy, nav, and :schema" {:file "doc/datafy-nav-and-schema.md"}] + ["Extending HoneySQL" {:file "doc/extending-honeysql.md"}]] ["Differences from 1.x" {:file "doc/difference-from-1-x.md"}]]} diff --git a/doc/extending-honeysql.md b/doc/extending-honeysql.md new file mode 100644 index 0000000..6732325 --- /dev/null +++ b/doc/extending-honeysql.md @@ -0,0 +1,99 @@ +# Extending HoneySQL + +Out of the box, HoneySQL supports most standard ANSI SQL clauses +and expressions but where it doesn't support something you need +you can add new clauses, new operators, and new "functions" (or +"special syntax"). + +There are three extension points in `honey.sql` that let you +register formatters or behavior corresponding to clauses, +operators, and functions. + +Built in clauses include: `:select`, `:from`, `:where` and +many more. Built in operators include: `:=`, `:+`, `:mod`. +Built in functions (special syntax) include: `:array`, `:case`, +`:cast`, `:inline`, `:raw` and many more. + +## Registering a New Clause Formatter + +## Registering a New Operator + +`honey.sql/register-op!` accepts a keyword (or a symbol) that +should be treated as a new infix operator. + +By default, operators are treated as strictly binary -- +accepting just two arguments -- and an exception will be +thrown if they are provided less than two or more than +two arguments. You can optionally specify that an operator +can take any number of arguments with `:variadic true`: + +```clojure +(sql/register-op! :<=> :variadic true) +;; and then use the new operator: +(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]}) +;; will produce: +;;=> ["SELECT * FROM table WHERE ? <=> x <=> ?" 13 42] +``` + +If you are building expressions programmatically, you +may want your new operator to ignore "empty" expressions, +i.e., where your expression-building code might produce +`nil`. The built-in operators `:and` and `:or` ignore +such `nil` expressions. You can specify `:ignore-nil true` +to achieve that: + +```clojure +(sql/register-op! :<=> :variadic true :ignore-nil true) +;; and then use the new operator: +(sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]}) +;; will produce: +;;=> ["SELECT * FROM table WHERE x <=> ?" 42] +``` + +## Registering a New Function (Special Syntax) + +`honey.sql/register-fn!` accepts a keyword (or a symbol) +that should be treated as new syntax (as a function call), +and a "formatter". The formatter can either be a function +of two arguments or a previously registered "function" (so +that you can easily reuse formatters). + +The formatter will be called with: +* The function name (always as a keyword), +* The sequence of arguments provided. + +For example: + +```clojure +(sql/register-fn! :foo (fn [f args] ..)) + +(sql/format {:select [:*], :from [:table], :where [:foo 1 2 3]}) +``` + +Your formatter function will be called with `:foo` and `(1 2 3)`. +It should return a vector containing a SQL string followed by +any parameters: + +```clojure +(sql/register-fn! :foo (fn [f args] ["FOO(?)" (first args)])) + +(sql/format {:select [:*], :from [:table], :where [:foo 1 2 3]}) +;; produces: +;;=> ["SELECT * FROM table WHERE FOO(?)" 1] +``` + +In practice, it is likely that your formatter would call +`sql/sql-kw` on the function name to produce a SQL representation +of it and would call `sql/format-expr` on each argument: + +```clojure +(defn- foo-formatter [f [x]] + (let [[sql & params] (sql/format-expr x)] + (into [(str (sql/sql-kw f) "(" sql ")")] params))) + +(sql/register-fn! :foo foo-formatter) + +(sql/format {:select [:*], :from [:table], :where [:foo [:+ :a 1]]}) +;; produces: +;;=> ["SELECT * FROM table WHERE FOO(a + ?)" 1] +``` diff --git a/doc/getting-started.md b/doc/getting-started.md index 9fba60b..8941caa 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -1,3 +1,125 @@ # Getting Started with HoneySQL -tbd +HoneySQL lets you build complex SQL statements by constructing +and composing Clojure data structures and then formatting that +data to a SQL statement (string) and any parameters it needs. + +## Installation + +For the Clojure CLI, add the following dependency to your `deps.edn` file: + +```clojure + seancorfield/honeysql {:mvn/version "2.0.0-alpha1"} +``` + +For Leiningen, add the following dependency to your `project.clj` file: + +```clojure + [seancorfield/honeysql "2.0.0-alpha1"] +``` + +> Note: 2.0.0-alpha1 will be released shortly! + +HoneySQL produces SQL statements but does not execute them. +To execute SQL statements, you will also need a JDBC wrapper like +[`seancorfield/next.jdbc`](https://github.com/seancorfield/next-jdbc) and a JDBC driver for the database you use. + +## Basic Concepts + +SQL statements are represented as hash maps, with keys that +represent clauses in SQL. SQL expressions are generally +represented as vectors, where the first element identifies +the function or operator and the remaining elements are the +arguments or operands. + +`honey.sql/format` takes a hash map representing a SQL +statement and produces a vector, suitable for use with +`next.jdbc` or `clojure.java.jdbc`, that has the generated +SQL string as the first element followed by any parameter +values identified in the SQL expressions: + +```clojure +(ns my.example + (:require [honey.sql :as sql])) + +(sql/format {:select [:*], :from [:table], :where [:= :id 1]}) +;; produces: +;;=> ["SELECT * FROM table WHERE id = ?" 1] +``` + +Any values found in the data structure, that are not keywords +or symbols, are treated as positional parameters and replaced +by `?` in the SQL string and lifted out into the vector that +is returned from `format`. + +Nearly all clauses expect a vector as their value, containing +either a list of SQL entities or the representation of a SQL +expression. + +A SQL entity can be a simple keyword (or symbol) or a pair +that represents a SQL entity and its alias: + +```clojure +(sql/format {:select [:t.id [:name :item]], :from [[:table :t]], :where [:= :id 1]}) +;; produces: +;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] +``` + +The `FROM` clause now has a pair that identifies the SQL entity +`table` and its alias `t`. Columns can be identified either by +their qualified name (as in `:t.id`) or their unqualified name +(as in `:name`). The `SELECT` clause here identifies two SQL +entities: `t.id` and `name` with the latter aliased to `item`. + +Symbols can also be used, but you need to quote them to +avoid evaluation: + +```clojure +(sql/format '{select [t.id [name item]], from [[table t]], where [= id 1]}) +;; also produces: +;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] +``` + +If you wish, you can specify SQL entities as namespace-qualified +keywords (or symbols) and the namespace portion will treated as +the table name, i.e., `:foo/bar` instead of `:foo.bar`: + +```clojure +(sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]}) +;; and +(sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]}) +;; both produce: +;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] +``` + +In addition to the hash map (and vectors) approach of building +SQL queries with raw Clojure data structures, a namespace full +of helper functions is also available. These functions are +generally variadic and threadable: + +```clojure +(ns my.example + (:require [honey.sql :as sql] + [honey.sql.helpers :refer [select from where]])) + +(-> (select :t/id [:name :item]) + (from [:table :t]) + (where [:= :id 1]) + (sql/format)) +;; produces: +;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] +``` + +In addition to being variadic -- which often lets you omit one +level of `[`..`]` -- the helper functions merge clauses, which +can make it easier to build queries programmatically: + +```clojure +(-> (select :t/id) + (from [:table :t]) + (where [:= :id 1]) + (select [:name :item]) + (sql/format)) +;; produces: +;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] +``` diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 4e51f5a..05c0997 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -107,11 +107,33 @@ "Given a keyword, return a SQL representation of it as a string. A `:kebab-case` keyword becomes a `KEBAB CASE` (uppercase) string - with hyphens replaced by spaces, e.g., `:insert-into` => `INSERT INTO`." + with hyphens replaced by spaces, e.g., `:insert-into` => `INSERT INTO`. + + Any namespace qualifier is ignored." [k] (-> k (name) (upper-case) (as-> s (if (= "-" s) s (str/replace s "-" " "))))) +(defn- sym->kw + "Given a symbol, produce a keyword, retaining the namespace + qualifier, if any." + [s] + (if (symbol? s) + (if-let [n (namespace s)] + (keyword n (name s)) + (keyword (name s))) + s)) + +(defn- kw->sym + "Given a keyword, produce a symbol, retaining the namespace + qualifier, if any." + [k] + (if (keyword? k) + (if-let [n (namespace k)] + (symbol n (name k)) + (symbol (name k))) + k)) + (defn- namespace-_ [x] (some-> (namespace x) (str/replace "-" "_"))) (defn- name-_ [x] (str/replace (name x) "-" "_")) @@ -493,13 +515,13 @@ (let [[sqls params leftover] (reduce (fn [[sql params leftover] k] (if-let [xs (or (k statement-map) - (let [s (symbol (name k))] + (let [s (kw->sym k)] (get statement-map s)))] (let [formatter (k @clause-format) [sql' & params'] (formatter k xs)] [(conj sql sql') (if params' (into params params') params) - (dissoc leftover k (symbol (name k)))]) + (dissoc leftover k (kw->sym k))]) [sql params leftover])) [[] [] statement-map] *clause-order*)] @@ -638,7 +660,7 @@ (if (sequential? s) (let [[sqls params] (reduce (fn [[sqls params] s] - (if (coll? s) + (if (sequential? s) (let [[sql & params'] (format-expr s)] [(conj sqls sql) (into params params')]) @@ -663,10 +685,7 @@ (format-dsl expr (assoc opts :nested true)) (sequential? expr) - (let [op (first expr) - ;; normalize symbols to keywords here -- makes the subsequent - ;; logic easier since we use op to lookup things in hash maps: - op (if (symbol? op) (keyword (name op)) op)] + (let [op (sym->kw (first expr))] (if (keyword? op) (cond (contains? @infix-ops op) (if (contains? @op-variadic op) ; no aliases here, no special semantics @@ -791,16 +810,18 @@ only clause so far where that would matter is `:set` which differs in MySQL." [clause formatter before] - (assert (keyword? clause)) - (let [f (if (keyword? formatter) - (get @clause-format formatter) - formatter)] - (when-not (and f (fn? f)) - (throw (ex-info "The formatter must be a function or existing clause" - {:type (type formatter)}))) - (swap! base-clause-order add-clause-before clause before) - (swap! current-clause-order add-clause-before clause before) - (swap! clause-format assoc clause f))) + (let [clause (sym->kw clause)] + (assert (keyword? clause)) + (let [k (sym->kw formatter) + f (if (keyword? k) + (get @clause-format k) + formatter)] + (when-not (and f (fn? f)) + (throw (ex-info "The formatter must be a function or existing clause" + {:type (type formatter)}))) + (swap! base-clause-order add-clause-before clause before) + (swap! current-clause-order add-clause-before clause before) + (swap! clause-format assoc clause f)))) (defn register-fn! "Register a new function (as special syntax). The `formatter` is either @@ -810,26 +831,29 @@ of the function (as a keyword) and a sequence of the arguments from the DSL." [function formatter] - (assert (keyword? function)) - (let [f (if (keyword? formatter) - (get @special-syntax formatter) - formatter)] - (when-not (and f (fn? f)) - (throw (ex-info "The formatter must be a function or existing fn name" - {:type (type formatter)}))) - (swap! special-syntax assoc function f))) + (let [function (sym->kw function)] + (assert (keyword? function)) + (let [k (sym->kw formatter) + f (if (keyword? k) + (get @special-syntax k) + formatter)] + (when-not (and f (fn? f)) + (throw (ex-info "The formatter must be a function or existing fn name" + {:type (type formatter)}))) + (swap! special-syntax assoc function f)))) (defn register-op! "Register a new infix operator. Operators can be defined to be variadic (the default is that they are binary) and may choose to ignore `nil` arguments (this can make it easier to programmatically construct the DSL)." [op & {:keys [variadic ignore-nil]}] - (assert (keyword? op)) - (swap! infix-ops conj op) - (when variadic - (swap! op-variadic conj op)) - (when ignore-nil - (swap! op-ignore-nil conj op))) + (let [op (sym->kw op)] + (assert (keyword? op)) + (swap! infix-ops conj op) + (when variadic + (swap! op-variadic conj op)) + (when ignore-nil + (swap! op-ignore-nil conj op)))) (comment (format {:truncate :foo}) @@ -868,7 +892,35 @@ ;; while working on the docs (require '[honey.sql :as sql]) (sql/format {:select [:*] :from [:table] :where [:= :id 1]}) + (sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]}) + (sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]}) + (sql/format '{select * from table where (= id 1)}) + (require '[honey.sql.helpers :refer [select from where]]) + (-> (select :t/id [:name :item]) + (from [:table :t]) + (where [:= :id 1]) + (sql/format)) + (-> (select :t/id) + (from [:table :t]) + (where [:= :id 1]) + (select [:name :item]) + (sql/format)) (sql/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}) (sql/format {:select [:foo/bar] :from [:q-u-u-x]} {:quoted true}) (sql/format {:select ["foo/bar"] :from [:q-u-u-x]} {:quoted true}) + (sql/format-expr [:primary-key]) + (sql/register-op! 'y) + (sql/format {:where '[y 2 3]}) + (sql/register-op! :<=> :variadic true :ignore-nil true) + ;; and then use the new operator: + (sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]}) + (sql/register-fn! :foo (fn [f args] ["FOO(?)" (first args)])) + (sql/format {:select [:*], :from [:table], :where [:foo 1 2 3]}) + (defn- foo-formatter [f [x]] + (let [[sql & params] (sql/format-expr x)] + (into [(str (sql/sql-kw f) "(" sql ")")] params))) + + (sql/register-fn! :foo foo-formatter) + + (sql/format {:select [:*], :from [:table], :where [:foo [:+ :a 1]]}) ,)