S-expressions is the way Walkable allow you to write arbitrary SQL expressions in your paredit/parinfer-powered editors without compromising security.
{% hint style="info" %}
Note about SQL examples:
- S-expressions can end up as SQL strings in either
SELECT
statements orWHERE
conditions. For demonstrating purpose, the strings are wrapped inSELECT ... as q
so the SQL outputs are executable, except ones with tables and columns. - SQL output may differ when you
require
different implementations (ie(require 'walkable.sql-query-builder.impl.postgres)
vs(require 'walkable.sql-query-builder.impl.sqlite)
).
{% endhint %}
{% mdtabs title="S-expression" %}
123
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT 123 AS q"])
{% mdtab title="result" %}
[{:q 123}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
nil
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT NULL AS q"])
{% mdtab title="result" %}
[{:q nil}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
"hello world"
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT ? AS q" "hello world"])
{% mdtab title="result" %}
[{:q "hello world"}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
"hello\"; DROP TABLE users"
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT ? AS q" "hello\"; DROP TABLE users"])
{% mdtab title="result" %}
[{:q "hello\"; DROP TABLE users"}]
{% endmdtabs %}
{% hint style="info" %} Note
The examples just use backticks as quote marks. Depending on your emitter configuration, Walkable will emit SQL strings using whatever quote marks you specified.
{% endhint %}
{% mdtabs title="S-expression" %}
:my-table/a-column
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT `my_table`.`a_column` AS `my-table/a-column` FROM `my_table`"])
{% mdtab title="result" %}
[{:my-table/a-column 42}, ...other records...]
{% endmdtabs %}
Walkable comes with some comparison operators: :=
, :<
, :>
,
:<=
, :>=
. They will result in SQL operators with the same name,
but also handle multiple arity mimicking their Clojure equivalents.
{% mdtabs title="S-expression" %}
[:= 1 2]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (1 = 2) AS q"])
{% mdtab title="result" %}
[{:q false}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:<= 1 2]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (1 <= 2) AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:< 1 2 3 1]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT ((1 < 2) AND (2 < 3) AND (3 < 1)) AS q"])
{% mdtab title="result" %}
[{:q false}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:= 0]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT true AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:>= 1000]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT true AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
String comparison operators: =
, like
, match
, glob
:
{% mdtabs title="S-expression" %}
[:= "hello" "hi"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (? = ?) AS q" "hello" "hi"])
{% mdtab title="result" %}
[{:q false}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:like "abcd" "abc%"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (? LIKE ?) AS q" "abcd" "abc%"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
Use them on some columns, too:
{% mdtabs title="S-expression" %}
[:= :my-table/its-column "hi"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (`my_table`.`its_column` = ?) AS q FROM `my_table`" "hi"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
Basic math operators work just like their Clojure equivalents: :+
, :-
, :*
, :/
:
{% mdtabs title="S-expression" %}
[:+ 1 2 4 8]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (1 + 2 + 4 + 8) AS q"])
{% mdtab title="result" %}
[{:q 15}]
{% endmdtabs %}
Feel free to mix them:
{% mdtabs title="S-expression" %}
[:+ [:*] [:* 2 4 7] [:/ 0.25]]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (1 + (2 * 4 * 7) + (1/0.25)) AS q"])
{% mdtab title="result" %}
[{:q 61.0}]
{% endmdtabs %}
{% hint style="info" %}
:*
with no argument result in 1
{% endhint %}
{% mdtabs title="S-expression" %}
[:str "hello " nil "world" 123]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CONCAT(?, NULL, ?, 123) AS q" "hello " "world"])
{% mdtab title="result" %}
[{:q "hello world123"}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:subs "hello world"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CONCAT(?, NULL, ?, 123) AS q" "hello " "world"])
{% mdtab title="result" %}
[{:q "hello world123"}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:str "hello " nil "world" 123]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CONCAT(?, NULL, ?, 123) AS q" "hello " "world"])
{% mdtab title="result" %}
[{:q "hello world123"}]
{% endmdtabs %}
Use the :cast
operator:
{% mdtabs title="S-expression" %}
[:cast "2" :integer]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT CAST(? as INTEGER) AS q" "2"])
{% mdtab title="result" %}
[{:q 2}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:cast 3 :text]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT CAST(3 as TEXT) AS q"])
{% mdtab title="result" %}
[{:q "3"}]
{% endmdtabs %}
:and
and :or
accept many arguments like in Clojure:
{% mdtabs title="S-expression" %}
[:and true true false]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (true AND true AND false) AS q"])
{% mdtab title="result" %}
[{:q false}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:and]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (true) AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:or]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (NULL) AS q"])
{% mdtab title="result" %}
[{:q nil}]
{% endmdtabs %}
:not
accepts exactly one argument:
{% mdtabs title="S-expression" %}
[:not true]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (NOT true) AS q"])
{% mdtab title="result" %}
[{:q false}]
{% endmdtabs %}
Party time! Mix them as you wish:
{% mdtabs title="S-expression" %}
[:and [:= 4 [:* 2 2]] [:not [:> 1 2]] [:or [:= 2 3] [:= 4 4]]]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (((4)=((2)*(2))) AND (NOT ((1)>(2))) AND (((2)=(3)) OR ((4)=(4)))) AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
Please note that Walkable S-expressions are translated directly to SQL equivalent. Your DBMS may throw an exception if you ask for this:
{% mdtabs title="S-expression" %}
[:or 2 true]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (2 OR true) AS q"])
{% mdtab title="result" %}
ERROR: argument of OR must be type boolean, not type integer
{% endmdtabs %}
Don't be surprised if you see [:not nil]
is ... nil
!
{% mdtabs title="S-expression" %}
[:not nil]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (NOT NULL) AS q"])
{% mdtab title="result" %}
[{:q nil}]
{% endmdtabs %}
nil
can not be checked with :=
. Use :nil?
instead
{% mdtabs title="S-expression" %}
[:= nil nil]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (NULL = NULL) AS q"])
{% mdtab title="result" %}
[{:q nil}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:nil? nil]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (NULL IS NULL) AS q"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
:when
, :if
, :case
and :cond
look like in Clojure...
{% mdtabs title="S-expression" %}
[:when true "yay"] ;; or [:if true "yay"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE WHEN ( true ) THEN ( ? ) END) AS q" "yay"])
{% mdtab title="result" %}
[{:q "yay"}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:if [:= 1 2] "yep" "nope"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE WHEN ((1)=(2)) THEN ( ? ) ELSE ( ? ) END) AS q" "yay" "nope"])
{% mdtab title="result" %}
[{:q "nope"}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:case [:+ 0 1] 2 3]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE (0+1) WHEN (2) THEN (3) END) AS q"])
{% mdtab title="result" %}
[{:q nil}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:case [:+ 0 1] 2 3 4]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE (0+1) WHEN (2) THEN (3) ELSE (4) END) AS q"])
{% mdtab title="result" %}
[{:q 4}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:cond [:= 0 1] "wrong" [:< 2 3] "right"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE WHEN ((0)=(1)) THEN ( ? ) WHEN ((2)<(3)) THEN ( ? ) END) AS q" "wrong" "right"])
{% mdtab title="result" %}
[{:q "right"}]
{% endmdtabs %}
...except the fact that you must supply real booleans to them, not just some truthy values.
{% mdtabs title="S-expression" %}
[:cond
[:= 0 1]
"wrong"
[:> 2 3]
"wrong again"
true ;; <= must be literally `true`, not `:default` or something else
"default"]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (CASE WHEN ((0)=(1)) THEN ( ? ) WHEN ((2)>(3)) THEN ( ? ) WHEN ( true ) THEN ( ? ) END) AS q" "wrong" "wrong again" "default"])
{% mdtab title="result" %}
[{:q "default"}]
{% endmdtabs %}
In your floor-plan you can define so-called pseudo columns that look just like normal columns from client-side view:
;; floor-plan
;; :person/yob is a real column
{:pseudo-columns {:person/age [:- 2018 :person/yob]}}
You can't tell the difference from client-side:
{% mdtabs title="Query for a real column" %}
[{[:person/by-id 9]
[:person/yob]}]
{% mdtab title="Query for a pseudo column" %}
[{[:person/by-id 9]
[:person/age]}]
{% endmdtabs %}
{% mdtabs title="Filter with a real column" %}
[{(:people/all {:filters [:= 1988 :person/yob]})
[:person/name]}]
{% mdtab title="Filter with a pseudo column" %}
[{(:people/all {:filters [:= 30 :person/age]})
[:person/name]}]
{% endmdtabs %}
Behind the scenes, Walkable will expand the pseudo columns to whatever they are defined. You can also use pseudo columns in other pseudo columns' definition, but be careful as Walkable won't check circular dependencies for you.
Please note you can only use true columns from the same table in the definition of pseudo columns. For instance, the following doesn't make sense:
;; floor-plan
{:pseudo-columns {:person/age [:- 2018 :pet/yob]}}
Your RDMS will throw an exception in that case anyway.
There are some convenient marcros to help you "import" SQL functions/operators: walkable.sql-query-builder.expressions/import-functions
and walkable.sql-query-builder.expressions/import-infix-operators
.
More complex operators may require implementing multimethod walkable.sql-query-builder.expressions/process-operator
or even a harder one walkable.sql-query-builder.expressions/process-unsafe-expression
.
Todo: more docs.
The following expressions work in Postgresql:
{% mdtabs title="S-expression" %}
[:= 1
[:cast [:get-as-text [:jsonb {:a 1}] "a"] :integer]]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT ((1)=(CAST ((?::jsonb)->>( ? ) AS INTEGER))) AS q" "{\"a\" :1}" "a"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}
{% mdtabs title="S-expression" %}
[:or [:= 2 [:array-length [:array 1 2 3 4] 1]]
[:contains [:jsonb {:a 1 :b 2}]
[:jsonb {:a 1}]]
[:jsonb-exists [:jsonb {:a 1 :b 2}]
"a"]]
{% mdtab title="SQL output" %}
(jdbc/query your-db ["SELECT (((2)=(array_length (ARRAY[1, 2, 3, 4], 1)))
OR ((?::jsonb)@>(?::jsonb))
OR (jsonb_exists (?::jsonb, ? )))
AS q"
"{\"a\":1,\"b\":2}" "{\"a\":1}" "{\"a\":1,\"b\":2}" "a"])
{% mdtab title="result" %}
[{:q true}]
{% endmdtabs %}