Welcome! Please see the About page for a little more info on how this works.

0 votes
in Client API by

How does one handle the following use case: I have a complicated report that allows the user to filter/narrow the results in a multitude of different ways. Let's say for example it's an order report. The user might want to view only the orders placed between yesterday and today, or they might only include orders placed by a particular customer, etc...

Some of these query inputs have obvious defaults (eg the default date range can just be "orders placed today"), but others do not (the default customer should be all customers).

How does one handle the optionality of the latter case?

Here is a contrived example that demonstrates the problem:

  (d/q '[:find ?p
         :in $ ?name
         :where [?p :product/name ?name]]
    "My Product")

This is fine if the user actually provides the name of a product to filter by, but if they don't, I'd like to default to returning all products. I could easily just create two separate queries, one where the product name is an input, and the other where it isn't, but there would then be a combinatorial explosion of queries to cover all input possibilities.

Passing nil in as an input results in an exception.

Perhaps the best way to solve this is not within the query at all, and I should instead query for the lazy seq of all entities, and then just use Clojure's filter to trim that list down?

1 Answer

+1 vote
selected by
Best answer

Instead of doing it 'in' the query, use clojure's data manip to create the query you want.

This is where queries themselves being data is really cool. The map form may be more useful in this context. Here's a quick and dirty... Added assumed :product/id to make it clearer. IRL, you'd want to make sure more selective clauses are first, etc etc. If you have complex combos, you might want to look at something like meander

(defn product-q
    (cond-> '{:find [?p]
              :in [$]
              :where [[?p :product/id _]]}
      name (-> (update :in conj '?name)
             (update :where conj '[?p :product/name ?name]))))

(product-q nil)
=> {:find [?p]
    :in [$]
    :where [[?p :product/id _]]}
(product-q "cheetos")
=> {:find [?p]
    :in [$ ?name]
    :where [[?p :product/id _]
            [?p :product/name ?name]]
Thanks Erich. I’ve started doing this on several queries and it’s working really well. It makes it pretty simple to factor common query enhancers into reusable functions too, eg an “owns” enhancer that injects authorization clauses, or a date range enhancer that injects start-end clauses, etc…
One shouldn't get too liberal with this approach, though. Datomic caches queries, so you'd make sure that the set of possible queries generated by your code is at least finite and preferably small.