Eql
EQL is a declarative way to make hierarchical (and possibly nested) selections of information about data requirements. This repository contains the base specs and definitions for EQL parsing, AST, etc.
Install / Use
/learn @edn-query-language/EqlREADME
:source-highlighter: coderay :source-language: clojure :toc: :toc-placement: preamble :sectlinks: :sectanchors: :sectnums: ifdef::env-github,env-cljdoc[] :tip-caption: :bulb: :note-caption: :information_source: :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: endif::[]
image:./assets/eql_logo_github.png[]
image:https://img.shields.io/clojars/v/edn-query-language/eql.svg[link=https://clojars.org/edn-query-language/eql] image:https://cljdoc.xyz/badge/edn-query-language/eql[link=https://cljdoc.xyz/d/edn-query-language/eql/CURRENT] image:https://github.com/edn-query-language/eql/actions/workflows/test.yml/badge.svg["GitHub actions", link="https://github.com/edn-query-language/eql/actions/workflows/test.yml"]
This repository contains a specification for EQL and a support library to help the development of tools using it.
== What is EQL?
EQL is a declarative way to make hierarchical (and possibly nested) selections of information about data requirements.
EQL doesn't have its own language; it uses EDN to express the request, taking advantage of the rich set of primitives provided by it.
==== EQL for selections
An easy way to get started is to think of a map and try to describe its shape. For example:
[source,clojure]
{:album/name "Magical Mystery Tour" :album/year 1967}
By describing the shape, we mean describing the structure but without the values, the previous example can be described using EQL as:
[source,clojure]
[:album/name :album/year]
Like using select-keys to specify which fields to extract from a map. Now let's see
what it looks like for nested structures:
[source,clojure]
{:album/name "Magical Mystery Tour" :album/artist {:artist/name "The Beatles"}}
; can be described as:
[:album/name ; note the use of a map to express nesting {:album/artist [:artist/name]}]
It works the same to represent nested sequences:
[source,clojure]
{:album/name "Magical Mystery Tour" :album/tracks [{:track/name "The Fool On The Hill"} {:track/name "All You Need Is Love"}]}
; can be described as:
[:album/name {:album/tracks [:track/name]}]
TIP: Although with just EQL you can't know if a key value is expected to be a single item or a sequence, you will know this as EQL is a model of your domain. If required, you can have this information setup out of band using Clojure specs, if you do so you can introspect the spec and detect that, this is not a feature of EQL in any way, just a suggested approach in case you need to know if the response of a key is a single item or a sequence.
==== EQL for operations
EQL also supports mutations, which are like side effect calls to an API. Mutations can
appear on EQL transaction, and they look like Clojure function calls, example:
[source,clojure]
[(call-mutation {:data "value"})]
More details in <<Mutations>>.
=== Datomic Pull Syntax comparison
On top of the link:https://docs.datomic.com/on-prem/pull.html[Datomic Pull Syntax] expression, EQL also supports:
- <<Parameters>>
- <<Mutations>>
- <<Unions,Union Queries>>
Check the links on each for more details.
The link:https://docs.datomic.com/on-prem/pull.html#attribute-with-options[attribute with options] feature from the Datomic Pull Syntax is not present in EQL; instead, we provide the parameterized attributes that can handle arbitrary data to go along the base property.
=== GraphQL comparison
Similar to GraphQL, EQL works as a language to client libraries to communicate data requirements and operations, there is a good set of intersection in features between both, as:
- a language to describe arbitrarily nested structures
- support for mutations (operations to side effect the world)
- support for parametrization
- union queries for query branching (select query according to some custom definition based on the data)
GraphQL has a type system in its definition, and it is required for a GraphQL system to work. EQL has no such thing, and it dictates only the syntax but not the semantics. Some features in GraphQL just doesn't make sense in EQL, like fragments, since EQL is already a data format (EDN), it's easy to compose data as you would do in a regular Clojure program, for that reason many features are not necessary because EQL is a parseable data format with all the Clojure tools already available to operate on top of it. Also read the <<AST Encode/Decode,AST>> section for more information on how to programmatically manipulate EQL data structures.
== Specification
The following sections explain the features provided by this library, including code examples. We show how you can work with an AST representation of an EQL expression, which is often an easier form to manipulate programatically. Of course, we'll also cover how to convert between EQL and an AST representation (bidirectionally).
In the end of this section you will also find the Clojure Spec formal specification for the EQL syntax.
=== Query / Transactions
An EQL transaction is represented by an EDN vector.
Examples:
[source,clojure]
[] ; empty transaction
; ast
{:type :root, :children []}
A transaction that only contains reads is commonly called a query, but notice that
at the syntax level, it has no difference.
=== Properties
Properties in EQL are expressed as Clojure keywords; they can be simple or qualified
keywords, and they express the property been requested.
Examples:
[source,clojure]
[:album/name :album/year]
; ast
{:type :root :children [{:type :prop, :dispatch-key :album/name, :key :album/name} {:type :prop, :dispatch-key :album/year, :key :album/year}]}
=== Joins
Joins are used to describe nesting in the request transaction. They are represented as EDN maps, always with a single entry, the entry key is the property to join on, and the entry value is a sub-query to run.
Examples:
[source,clojure]
[{:favorite-albums [:album/name :album/year]}]
; ast
{:type :root :children [{:type :join :dispatch-key :favorite-albums :key :favorite-albums :query [:album/name :album/year] :children [{:type :prop, :dispatch-key :album/name, :key :album/name} {:type :prop, :dispatch-key :album/year, :key :album/year}]}]}
Nested joins example:
[source,clojure]
[{:favorite-albums [:album/name :album/year {:album/tracks [:track/name :track/duration]}]}]
; ast
{:type :root :children [{:type :join :dispatch-key :favorite-albums :key :favorite-albums
:query [:album/name :album/year {:album/tracks [:track/name :track/duration]}]
:children [{:type :prop, :dispatch-key :album/name, :key :album/name} {:type :prop, :dispatch-key :album/year, :key :album/year} {:type :join :dispatch-key :album/tracks :key :album/tracks :query [:track/name :track/duration] :children [{:type :prop, :dispatch-key :track/name, :key :track/name} {:type :prop :dispatch-key :track/duration :key :track/duration}]}]}]}
=== Idents
Idents are represented by a vector with two elements, where the first is a keyword and the second can be anything. They are like link:http://blog.datomic.com/2014/02/datomic-lookup-refs.html[lookup refs on Datomic], in general, they can provide an address-like thing, and their use and semantic might vary from system to system.
Examples:
[source,clojure]
[[:customer/id 123]]
; ast
{:type :root :children [{:type :prop, :dispatch-key :customer/id, :key [:customer/id 123]}]}
Note that this time in the AST the :dispatch-key and :key got different values this
time, the :dispatch-key been just the ident key while the :key contains the
full thing.
It's common to use an ident as a join key to start a query for some entity:
[source,clojure]
[{[:customer/id 123] [:customer/name :customer/email]}]
; ast
{:type :root :children [{:type :join :dispatch-key :customer/id :key [:customer/id 123] :query [:customer/name :customer/email] :children [{:type :prop, :dispatch-key :customer/name, :key :customer/name} {:type :prop :dispatch-key :customer/email :key :customer/email}]}]}
=== Parameters
EQL properties, joins, and idents have support for parameters. This allows the query to provide an extra dimension of information about the requested data. A parameter is expressed by wrapping the thing with an EDN list, like so:
[source,clojure]
; without params [:foo]
; with params [(:foo {:with "params"})]
; ast
{:type :root :children [{:type :prop :dispatch-key :foo :key :foo :params {:with "params"} :meta {:line 1, :column 15}}]}
Note on the AST side it gets a new :params key. Params must always be maps, the
map values can be anything. Here are more examples of adding parameters to queries:
[source,clojure]
; ident with params
[([:ident "value"] {:with "param"})]
{:type :root :children [{:type :prop :dispatch-key :ident :key [:ident "value"] :params {:with "param"} :meta {:line 1, :column 15}}]}
; join with params wrap the key with the list
[{(:join-key {:with "params"}) [:sub-query]}]
{:type :root :children [{:type :join :dispatch-key :join-key :key
