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:

  • GraphQL is built atop strings, in contrast with Clojure’s rich data structure used by EQL.

  • EQL encourages the use of namespaced (aka qualified) keywords. In fact, you must use namespaced keywords with Walkable so it can infer which column is from which table. For instance, :person/id means the column id in the table person.

  • GraphQL is coupled with a type system. EQL is not.

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.

1. Properties

To query for an entity’s properties, use a vector of keywords denoting which properties you’re asking for.

For example, use this query

;; query of three prop keys:
[:person/id :person/name :person/age]

when you want to receive things like:

;; returned value
{:person/id   99
 :person/name "Alice"
 :person/age  40}

the thing that receives a query and returns the above is called a query resolver (or simply query parser in Fulcro and Pathom’s documentation).

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.

  • Without params

  • With params

;; 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:

  • Without params

  • With params

;; 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]}]
  • Without params

  • With params

simple query:

'[{:people/list [:person/name :person/age]}]

v.s. modified query with params {:offset 20 :limit 10} added to the query root :people/list

[{(:people/list {:offset 20 :limit 10}) [: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:

  • Code

  • Output

(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:

  • Code

  • Output

(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]}]}]