Initial Getting Started/Extending HoneySQL docs

This commit is contained in:
Sean Corfield 2021-02-02 12:25:26 -08:00
parent 25acc53b24
commit 80c137949e
4 changed files with 307 additions and 40 deletions

View file

@ -1,11 +1,5 @@
{:cljdoc.doc/tree [["Readme" {:file "README.md"}] {:cljdoc.doc/tree [["Readme" {:file "README.md"}]
["Changes" {:file "CHANGELOG.md"}] ["Changes" {:file "CHANGELOG.md"}]
["Getting Started" {:file "doc/getting-started.md"} ["Getting Started" {:file "doc/getting-started.md"}
#_["Friendly SQL Functions" {:file "doc/friendly-sql-functions.md"}] ["Extending HoneySQL" {:file "doc/extending-honeysql.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"}]
["Differences from 1.x" {:file "doc/difference-from-1-x.md"}]]} ["Differences from 1.x" {:file "doc/difference-from-1-x.md"}]]}

99
doc/extending-honeysql.md Normal file
View file

@ -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]
```

View file

@ -1,3 +1,125 @@
# Getting Started with HoneySQL # 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]
```

View file

@ -107,11 +107,33 @@
"Given a keyword, return a SQL representation of it as a string. "Given a keyword, return a SQL representation of it as a string.
A `:kebab-case` keyword becomes a `KEBAB CASE` (uppercase) 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]
(-> k (name) (upper-case) (-> k (name) (upper-case)
(as-> s (if (= "-" s) s (str/replace s "-" " "))))) (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- namespace-_ [x] (some-> (namespace x) (str/replace "-" "_")))
(defn- name-_ [x] (str/replace (name x) "-" "_")) (defn- name-_ [x] (str/replace (name x) "-" "_"))
@ -493,13 +515,13 @@
(let [[sqls params leftover] (let [[sqls params leftover]
(reduce (fn [[sql params leftover] k] (reduce (fn [[sql params leftover] k]
(if-let [xs (or (k statement-map) (if-let [xs (or (k statement-map)
(let [s (symbol (name k))] (let [s (kw->sym k)]
(get statement-map s)))] (get statement-map s)))]
(let [formatter (k @clause-format) (let [formatter (k @clause-format)
[sql' & params'] (formatter k xs)] [sql' & params'] (formatter k xs)]
[(conj sql sql') [(conj sql sql')
(if params' (into params params') params) (if params' (into params params') params)
(dissoc leftover k (symbol (name k)))]) (dissoc leftover k (kw->sym k))])
[sql params leftover])) [sql params leftover]))
[[] [] statement-map] [[] [] statement-map]
*clause-order*)] *clause-order*)]
@ -638,7 +660,7 @@
(if (sequential? s) (if (sequential? s)
(let [[sqls params] (let [[sqls params]
(reduce (fn [[sqls params] s] (reduce (fn [[sqls params] s]
(if (coll? s) (if (sequential? s)
(let [[sql & params'] (format-expr s)] (let [[sql & params'] (format-expr s)]
[(conj sqls sql) [(conj sqls sql)
(into params params')]) (into params params')])
@ -663,10 +685,7 @@
(format-dsl expr (assoc opts :nested true)) (format-dsl expr (assoc opts :nested true))
(sequential? expr) (sequential? expr)
(let [op (first expr) (let [op (sym->kw (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)]
(if (keyword? op) (if (keyword? op)
(cond (contains? @infix-ops op) (cond (contains? @infix-ops op)
(if (contains? @op-variadic op) ; no aliases here, no special semantics (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 only clause so far where that would matter is `:set` which differs in
MySQL." MySQL."
[clause formatter before] [clause formatter before]
(assert (keyword? clause)) (let [clause (sym->kw clause)]
(let [f (if (keyword? formatter) (assert (keyword? clause))
(get @clause-format formatter) (let [k (sym->kw formatter)
formatter)] f (if (keyword? k)
(when-not (and f (fn? f)) (get @clause-format k)
(throw (ex-info "The formatter must be a function or existing clause" formatter)]
{:type (type formatter)}))) (when-not (and f (fn? f))
(swap! base-clause-order add-clause-before clause before) (throw (ex-info "The formatter must be a function or existing clause"
(swap! current-clause-order add-clause-before clause before) {:type (type formatter)})))
(swap! clause-format assoc clause f))) (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! (defn register-fn!
"Register a new function (as special syntax). The `formatter` is either "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 of the function (as a keyword) and a sequence of the arguments from the
DSL." DSL."
[function formatter] [function formatter]
(assert (keyword? function)) (let [function (sym->kw function)]
(let [f (if (keyword? formatter) (assert (keyword? function))
(get @special-syntax formatter) (let [k (sym->kw formatter)
formatter)] f (if (keyword? k)
(when-not (and f (fn? f)) (get @special-syntax k)
(throw (ex-info "The formatter must be a function or existing fn name" formatter)]
{:type (type formatter)}))) (when-not (and f (fn? f))
(swap! special-syntax assoc function 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! (defn register-op!
"Register a new infix operator. Operators can be defined to be variadic (the "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 default is that they are binary) and may choose to ignore `nil` arguments
(this can make it easier to programmatically construct the DSL)." (this can make it easier to programmatically construct the DSL)."
[op & {:keys [variadic ignore-nil]}] [op & {:keys [variadic ignore-nil]}]
(assert (keyword? op)) (let [op (sym->kw op)]
(swap! infix-ops conj op) (assert (keyword? op))
(when variadic (swap! infix-ops conj op)
(swap! op-variadic conj op)) (when variadic
(when ignore-nil (swap! op-variadic conj op))
(swap! op-ignore-nil conj op))) (when ignore-nil
(swap! op-ignore-nil conj op))))
(comment (comment
(format {:truncate :foo}) (format {:truncate :foo})
@ -868,7 +892,35 @@
;; while working on the docs ;; while working on the docs
(require '[honey.sql :as sql]) (require '[honey.sql :as sql])
(sql/format {:select [:*] :from [:table] :where [:= :id 1]}) (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 [:*] :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 {: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]]})
,) ,)