Skip to content

Instantly share code, notes, and snippets.

@olivergeorge
Last active February 11, 2022 10:59
Show Gist options
  • Save olivergeorge/e086543f519d5e0559c1179545e20c68 to your computer and use it in GitHub Desktop.
Save olivergeorge/e086543f519d5e0559c1179545e20c68 to your computer and use it in GitHub Desktop.

I was struggling to compose datomic queries neatly. In the end I googled and found a blogpost which proposed a sane solution.

My motivation was an API endpoint with optional filters. I'd like my query include additional filters based on the args present.

Anyway, here's how my code ended up looking...

And the article: http://grishaev.me/en/datomic-query

(ns olivergeorge.v1)
(defmethod reader :movies
[{:keys [pull args]}]
(let [db (db/get-db)
{:keys [since]} args
q (cond-> '{:find [[?e ...]]
:in [$]
:where [[?e :movie/title]]
:args []}
; Do this here so the function all isn't quoted. ` & ~ would change symbols.
; e.g. $ would become ethics_prototype.core/$
true
(-> (update :args conj (db/get-db)))
; Bit brittle since there's a shared namespace of variables
since
(-> (update :in conj '?since)
(update :args conj since)
(update :where conj
'[?e :movie/release-year ?release-year]
'[(>= ?release-year ?since)])))
results (apply d/q (dissoc q :args) (:args q))]
(d/pull-many db pull results)))
(ns olivergeorge.v2)
(defmethod reader :movies
[{:keys [pull args]}]
(let [db (db/get-db)
; Using rules keeps query simpler and avoids risk of reusing variables
rules '[[[released-since ?e ?s]
[?e :movie/release-year ?y]
[(< ?s ?y)]]]
{:keys [since]} args
q (cond-> '{:find [[?e ...]]
:in [$ %]
:where [[?e :movie/title]]
:args []}
true
(-> (update :args conj db rules))
since
(-> (update :in conj '?since)
(update :args conj since)
(update :where conj '(released-since ?e ?since))))
results (apply d/q (dissoc q :args) (:args q))]
(d/pull-many db pull results)))
(ns olivergeorge.v3)
;; Using a local function gives more freedom in how the logic is expressed. Logic in where clauses require a very specific structure.
(defn crazy-logic [since year] ...)
(defmethod reader :movies
[{:keys [pull args]}]
(let [db (db/get-db)
rules '[[[released-since ?e ?s]
[?e :movie/release-year ?y]
[(olivergeorge.v3/crazy-logic ?s ?y)]]]
{:keys [since]} args
q (cond-> '{:find [[?e ...]]
:in [$ %]
:where [[?e :movie/title]]
:args []}
true
(-> (update :args conj db rules))
since
(-> (update :in conj '?since)
(update :args conj since)
(update :where conj '(released-since ?e ?since))))
results (apply d/q (dissoc q :args) (:args q))]
(d/pull-many db pull results)))
(ns olivergeorge.v4
"This doesn't quite work yet")
(defn filter?
[a v [op a1 a2]]
(case op
:= (= (string/lower-case v) (string/lower-case a1))
:contains (string/includes? v a1)
:icontains (string/includes? (string/lower-case v) (string/lower-case a1))
:in (contains? a1 v)
:>= (>= v a1)
:<= (<= v a1)
:> (> v a1)
:< (< v a1)
:startswith (string/starts-with? v a1)
:istartswith (string/starts-with? (string/lower-case v) (string/lower-case a1))
:endswith (string/ends-with? v a1)
:iendswith (string/ends-with? (string/lower-case v) (string/lower-case a1))
:range (< a1 v a2)
;:date (date v a1)
;:year (year v a1)
;:month (month v a1)
;:day (day v a1)
;:week (week v a1)
;:week_day (week_day v a1)
;:quarter (quarter v a1)
;:time (time v a1)
;:hour (hour v a1)
;:minute (minute v a1)
;:second (second v a1)
:regex (re-matches (re-pattern a1) v)
:iregex (re-matches (re-pattern (str "(?i)" a1)) v)))
(defmulti reader (fn [{:keys [query]}] query))
(defmethod reader :movies
[{:keys [pull args]}]
(let [db (db/get-db)
rules '[[[filter? ?e ?a ?test]
[?e ?a ?v]
[(ethics-prototype.api/filter? ?a ?v ?test)]]]
q (cond-> '{:find [[?e ...]]
:in [$ %]
:where [[?e :movie/title]]
:args []}
true
(-> (update :args conj db rules))
(seq args)
(-> (update :in conj '[[?a ?v]])
(update :where conj '(filter? ?e ?a ?v))
(update :args conj args)))
results (apply d/q (dissoc q :args) (:args q))]
(d/pull-many db pull results)))
(comment
(reader {:query :movies :args {} :pull [:movie/title :movie/release-year]})
(reader {:query :movies :args {:movie/release-year [:= 1985]
:movie/title [:= "The Goonies"]} :pull [:movie/title :movie/release-year]}))
(ns olivergeorge.v4
"Use recursive step to expand over filters")
(defn filter?
[a v [op a1 a2]]
(case op
:= (= (string/lower-case v) (string/lower-case a1))
:contains (string/includes? v a1)
:icontains (string/includes? (string/lower-case v) (string/lower-case a1))
:in (contains? a1 v)
:>= (>= v a1)
:<= (<= v a1)
:> (> v a1)
:< (< v a1)
:startswith (string/starts-with? v a1)
:istartswith (string/starts-with? (string/lower-case v) (string/lower-case a1))
:endswith (string/ends-with? v a1)
:iendswith (string/ends-with? (string/lower-case v) (string/lower-case a1))
:range (< a1 v a2)
:regex (re-matches (re-pattern a1) v)
:iregex (re-matches (re-pattern (str "(?i)" a1)) v)))
(defmulti reader (fn [{:keys [query]}] query))
(defmethod reader :movies
[{:keys [pull args]}]
(let [db (db/get-db)
rules '[[[filter? ?e ?tests]
[(empty? ?tests)]]
[[filter? ?e ?tests]
[(seq ?tests)]
[(first ?tests) [?a ?test]]
[?e ?a ?v]
[(ethics-prototype.api/filter? ?a ?v ?test)]
[(rest ?tests) ?rest]
(filter? ?e ?rest)]]
q '[:find [?e ...]
:in $ % ?tests
:where
[?e :movie/title]
(filter? ?e ?tests)]
results (d/q q db rules args)]
(d/pull-many db pull results)))
(comment
(reader {:query :movies :args {} :pull [:movie/title :movie/release-year]})
(reader {:query :movies :args {:movie/release-year [:= 1985]
:movie/title [:= "The Goonies"]} :pull [:movie/title :movie/release-year]}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment