Query language
Walkable uses EQL, a Clojure native query language to describe what data you want from your SQL DBMS. The query language was inspired by Datomic’s Pull API and Graphql and first introduced in om.next. However, you don’t have to use om.next (or its successor, Fulcro) to make use of the query language.
If you know GraphQL, then the main differences between the two query languages are:
|
If you’re familiar with building apps with Fulcro, please note: you often specify a query for each component and compose them up. From server-side perspective (which is that of Walkable’s), you don’t know (or care) about those query fragments: you just return the right data in the right shape for the big final top-level query. The reconciler in the client-side will build up the query and break down the result. |
This page is not very well-written and hard to follow. Feedback and suggestion are welcome. |
1. Columns
To query for some table’s columns, use a vector of keywords denoting which columns you’re asking for.
For example, put this vector somewhere:
;; vector of three column keys:
[:person/id :person/name :person/age]
when you want to receive things like:
;; (part of) result
{:person/id 99
:person/name "Alice"
:person/age 40}
2. Joins
Sometimes you want to include another entity that has some kind of relationship with the current entity.
;; returned value
{:person/friends [{:person/id 97
:person/name "Jon"}
{:person/id 98
:person/name "Mary"}]
:person/spouse {:person/id 100
:person/name "Bob"}
:person/pets [{:pet/id 10
:pet/name "Nyan cat"}
{:pet/id 20
:pet/name "Ceiling cat"}
{:pet/id 30
:pet/name "Invisible Bike Cat"}]}
to achieve that, the query should be:
;; query of three join keys:
[:person/friends :person/mate :person/pets]
which is the same as:
;; query
[{:person/friends [*]}
{:person/mate [*]}
{:person/pets [*]}]
which means you let the query resolver dictate which child properties to return for each join. Or you may explicitly tell your own list:
;; query
[{:person/friends [:person/id :person/name]}
{:person/mate [:person/id :person/name]}
{:person/pets [:pet/id :pet/name]}]
Wait, isn’t this list the vector syntax I’ve learned in Properties section? Yup.
You may also notice that the query syntax is the same for to-many
relationships (ie :person/friends
and :person/pets
) as well as
to-one one (:person/mate
). Well, the query resolver, which owns the
data, will decide if it will return an item (as a map) or a vector of
zero or more such item.
3. Roots
Actually the properties and joins above can’t stand alone themselves. They must stem from somewhere: enter roots. Roots are, well, the root of all queries (or to put it in Lisp terms, the root of all evals :D)
I lied in the examples in section 1 and 2: such queries are not enough for the query resolver to return such results. So what’s missing? You guess… Roots! |
Let’s see some examples:
Some roots look just like joins:
;; query
[{:user/profile [:person/name :person/age]}]
of course roots can have joins nested inside, too:
;; query
[{:user/profile [:person/name :person/age
{:person/pets [:pet/name :pet/id]}]}]
4. Idents
At first glance, idents look a bit weird. Unlike roots which are keywords, idents consist of a vector of a keyword indicating the entity type followed by exactly one argument specifying how to identify those entities.
First, look at the an ident:
;; queries
[:person/id 1]
;; or
[:thing/uuid "03157713-28f8-4f2b-9aa2-3fc52451369a"]
Okay, now see them in context:
[{[:person/id 1] [:person/name :person/age]}]
[:person/id 1]
is called the ident. [:person/name
and
:person/age
are the child properties.
Allow yourself some time to grasp the syntax. Once you’re comfortable, here is the above query again with a child join added:
[{[:person/id 1] [:person/name :person/age
{:person/pets [:pet/name :pet/id]}]}]
You may notice idents also live inside a vector, which means you can have many of them at the same level in your query:
These two queries:
[{[:person/id 1] [:person/name :person/age]}]
[{[:person/id 2] [:person/name :person/age]}]
can be merged into one:
[{[:person/id 1] [:person/name :person/age]}
{[:person/id 2] [:person/name :person/age]}]
actually, idents can stem from anywhere, like this:
[{[:person/id 1] [:person/name :person/age
{[:person/id 2] [:person/name :person/age]}]}]
Here [:person/id 2]
looks just like a child join (such as
:person/mate
, :person/pets
), but it has nothing to do with the
entity [:person/id 1]
.
5. Parameters
Parameter is the way you attach extra data to a property.
Parameters must be implemented from the query resolver’s side in order to have effect. The parameters in the examples below are provided to explain the syntax so you get the idea. |
;; simple query
'[:person/name :person/height]
;; vs modified query with params in the property `:person/height`
'[:person/name (:person/height {:unit :cm})]
Just like a Clojure function’s list of arguments, parameters may contain zero or more items. Personally, I prefer the use of exactly one hash-map. For instance, with Walkable you can use some pre-defined parameters:
;; simple query
'[{:people/list [:person/name :person/age]}]
;; vs modified query with params `{:order-by :person/name}` added to the ident `:people/list`
'[{(:people/list {:order-by :person/name}) [:person/name :person/age]}]
Practice
It’s recommended to get acquainted with the query language in the
REPL. Give some desired output to data->shape
and see the what the
equivalent query is. For example:
(require '[com.wsscode.pathom.connect :as pc])
(pc/data->shape {:a [1 2 3] :b {:x 1 :y 2} :c [{:m 4 :n 5} {:p 6 :q 7}]})
[:a {:b [:x :y]} {:c [:m :n :p :q]}]
A more realistic example:
(pc/data->shape
{:people/list
[{:person/id 1
:person/name "jon"
:person/pets [{:pet/name "kitty"
:pet/species "cat"}
{:pet/name "canny"
:pet/species "dog"}]}
{:person/id 2
:person/name "snow"
:person/spouse {:person/id 3
:person/name "buddy"
:person/yob 2000}}]})
[{:people/list
[:person/id
:person/name
{:person/pets [:pet/name
:pet/species]}
{:person/spouse [:person/id
:person/name
:person/yob]}]}]