Initial Getting Started/Extending HoneySQL docs
This commit is contained in:
parent
25acc53b24
commit
80c137949e
4 changed files with 307 additions and 40 deletions
|
|
@ -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"}]]}
|
||||
|
|
|
|||
99
doc/extending-honeysql.md
Normal file
99
doc/extending-honeysql.md
Normal 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]
|
||||
```
|
||||
|
|
@ -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]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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]]})
|
||||
,)
|
||||
|
|
|
|||
Loading…
Reference in a new issue