/Projects/

In-memory non-relational db

A clojure DSL for a sparql-like language and query engine implementation

Skills: #clojure

Inspired by sparql query language, I implemented a toy, in-memory, relational db clojure DSL in order to experiment with clojure's metaprogramming features and see where can it be pushed.

The (schemaless) database is composed by (subject, predicate, object) triples

(def data
  '[[:john :age 42]
   [:mary :age 26]
   [:john :friend-with :mary]
   [:bob :age 42]])

We can query the db using the select function in conjunction with the where macro. For example, here's how we can find out john's age:

(def name :john)

(select '?age {:from data}
  (where name :age ?age)) ;; => (42)

The where clause acts as a triple of contraints. While name and :age are exact values (name is bound to :john) the ?age symbol is a logic variable: it binds the symbol to any value that is able to unify the constraint. This way, ?age can be used in the select's first argument, the projection value - but it could have been any expression, like '{:age ?age}, resulting in the ({:age 42}) list as a result.

But the interesting part is that we can specify multiple where clauses. The following queries all the people that have the same age as john's:

(select '?person {:from data}
  (where :john :age ?age))
  (where ?person :age ?age)) ;; => (:john :bob)

We can specify more complex queries using the guard macro:

(select '?person {:from data}
  (where :john :age ?age))
  (where ?person :age ?age)
  (guard (not (== ?person :john)))) ;; => (:bob)

The query doesn't return data in case no facts unify with the constraints:

(select '[?full-name ?age]
  {:from [[:john :full-name "John Doe"]
          [:bob :full-name "Bob Doe"]
          [:bob :age 42]]}

  (where ?person :full-name ?full-name))
  (where ?person :age ?age)) ;; => (["bob doe" 42])

So in case we want an optional result to be bound as nil, we can use the optional macro:

(select '[?full-name ?age]
  {:from [[:john :full-name "John Doe"]
          [:bob :full-name "Bob Doe"]
          [:bob :age 42]]}

  (guard ?person :full-name ?full-name))
  (optional
    (where ?person :age ?age))) ;; => (["John Doe" nil] ["bob doe" 42])

Finally, we can also aggregate resulting data:

(select '?x {:from data
             :distinct true
             :limit 4
             :order-by '?age}
  ;; ...
  )