Advanced query for sorted tasks with custom table view

This has been a rough one - I’ve been trying to built a table view of my tasks sorted by urgency and finally got a solution that I’m satisfied with.

As there aren’t many resources on advanced queries (especially custom view funcitons), I decided to share the result. Let me know if you found it useful:

#+BEGIN_QUERY
{:title "BACKLOG"
:query [:find ?status (pull ?todo [*])
  :keys status task
  :where
  [?todo :block/marker ?status]
  [(contains? #{"NOW" "LATER" "DOING" "TODO" "IN-PROGRESS" "WAIT" "WAITING"} ?status)]
  ; I keep all my templates in a page called "templates" and this is how I filter out TODOS defined insde them. You can delete it if you don't have such a page
  (not [?todo :block/page [:block/name "templates"]])]
 :result-transform (fn [result] 
  (sort-by
    (min (fn [d] (get-in d [:task :block/deadline] 99999999)) (fn [d] (get-in d [:task :block/scheduled] 99999999)))
    result
  )
)
:view (fn [rows] 
  (defn dateformat [datestr]
    (let [year (subs datestr 0 4)]
      (let [month (subs datestr 4 6)]
        (let [day (subs datestr 6 8)]
          (str year "-" month "-" day)
        )
      )
    )
  )
  [:div.overflow-x-auto.query-table {:width "100%"} [:table.table-auto 
    [:thead [:tr 
      [:th.whitespace-nowrap ">"] 
      [:th.whitespace-nowrap  {:width "50%"} "Task"] 
      [:th.whitespace-nowrap "Project"] 
      [:th.whitespace-nowrap "Status"] 
      [:th.whitespace-nowrap "Scheduled"] 
      [:th.whitespace-nowrap "Deadline"]
    ]]
    [:tbody (for [r rows] [:tr 
      [:td.whitespace-nowrap [:a {:href (str "#/page/" (get-in r [:task :block/uuid]))} ">" ] ]
      [:td.whitespace-nowrap (clojure.string/replace (first (str/split-lines (get-in r [:task :block/content]))) (re-pattern "^[^ ]+ ") "")]
      [:td.whitespace-nowrap (if (not (nil? (get-in r [:task :block/properties :project]))) [:a {:href (str "#/page/" (first (get-in r [:task :block/properties :project])) )} (str "[[" (first (get-in r [:task :block/properties :project])) "]]") ]) ]
      [:td.whitespace-nowrap (str (get-in r [:task :block/marker])) ]
      [:td.whitespace-nowrap (dateformat (str (get-in r [:task :block/scheduled]))) ]
      [:td.whitespace-nowrap (dateformat (str (get-in r [:task :block/deadline]))) ]
    ]) ]
  ] ]
)
:collapsed? false}
#+END_QUERY

Result:

Full (updated) example:

Great work @theCalcaholic :clap:t2:

Could you please explain the role/function of the following line:

:keys status task

Including :keys in an advanced query changes the structure of the query results.

When I say the structure changes, I mean is that the query result content remains the same, but the way Logseq organizes the data changes. This change isn’t visible to the user.

Let’s explore the specific changes that occur.

Essentially, Logseq changes the data types used to contain the query results from one type to another. Specifically, the query result structure changes in this way:

  • from a list of the data specified by the :find clause expressions.
  • into a list of maps. In the list, each map represents a single evaluation of the :find clause. Within each map there are a number of keys containing the result values, with as many keys as there are symbols specified after :keys.

In the :keys result, each map gets its key name from the values specified after :keys. These values are referred to as symbols. For example, the symbols from OP’s query are status and task.

Let’s peek at the result of the original advanced query to compare the result with and without :keys. This can be done by changing the :view function to :view :pprint

Advanced Query

Advanced query used to see the results. Notice the modified :view function.

{:query
 [:find ?status (pull ?todo [*])
  :keys status task ;; comment this out
  :where
  [?todo :block/marker ?status]
  [(contains? #{"NOW" "LATER"
                "DOING" "TODO"
                "IN-PROGRESS" "WAIT"
                "WAITING"}
              ?status)]]
 :result-transform
   ;; same as above
 
 :view :pprint
 }

query results

Comparing the advanced query result both with and without the :keys feature.

Without :keys. This is the “default” query result structure. It doesn’t use :keys status task.

(
 "TODO"

 {:block/uuid #uuid "6682d233-653e-47b4-9196-9fd5b19e60c8",
  :block/content "TODO do laundry",
  :db/id 8926,
  :block/marker "TODO"}

 "TODO"
 
 {:block/uuid #uuid "6682d234-ad2f-4584-b660-60c22d1074bd",
  :block/content "TODO canned tomato paste",
  :db/id 9542,
  :block/marker "TODO"}
 )

With :keys shows how the structure of the query result changes when including :keys status task. The content is the same but the data structure is different. Try to notice what changed and what stayed the same.

(
 {:status "TODO",
  :task {:block/uuid #uuid "6682d233-653e-47b4-9196-9fd5b19e60c8",
         :block/content "TODO do laundry",
         :db/id 8926,
         :block/marker "TODO"}}

 {:status "TODO",
  :task {:block/uuid #uuid "6682d234-ad2f-4584-b660-60c22d1074bd",
         :block/content "TODO canned tomato paste",
         :db/id 9542,
         :block/marker "TODO"}}
)

Interpretation

By using :keys in an advanced query, instead of a getting… ( "a list of strings" {:and "maps"} ), logseq returns a ( {:list "of nested"} {:maps "and strings"} ).

Aside: About data structures
In Clojure, the syntax indicates the data type:
"This is a string"
"This is a
    multi-line string"
0 <- number
1.7 <- number
:i-am-a-keyword <- keyword
symbol <- used for variables, evaluates to something else
(1 2 3 4 5) <- parentheses make lists
{:keyword "string"} <- curly braces make maps storing key-value pairs.
{"string" {:another "map"}} <- Maps can contain other data types
Anyway…

In the result with :keys, each component of the :find clause corresponds to the symbol from :keys in the same position. Each :key symbol becomes the map keyword for that :find clause return value.

Annotated results

Without :keys. Query results without using :keys with data type annotations:

(       ;; list data type
 "TODO" ;; string data type   ;; :find ?status
 {      ;; map data type      ;; :find (pull ?todo [*])
  :block/uuid #uuid "6682d233-653e-47b4-9196-9fd5b19e60c8",
  :block/content "TODO do laundry",
  :db/id 8926,
  :block/marker "TODO"}
 
 "TODO"

 {:block/uuid #uuid "6682d234-ad2f-4584-b660-60c22d1074bd",
  :block/content "TODO canned tomato paste",
  :db/id 9542,
  :block/marker "TODO"}
 )

With :keys status task. Query results when using :keys status task:

(  ;; list data type
 { ;; map data type
  :status "TODO",   ;; :find ?status :keys status
  :task {           ;; :find (pull ?todo [*]) :keys task
         :block/uuid #uuid "6682d233-653e-47b4-9196-9fd5b19e60c8",
         :block/content "TODO do laundry",
         :db/id 8926,
         :block/marker "TODO"
        }
  }
 
 {:status "TODO",
  :task {:block/uuid #uuid "6682d234-ad2f-4584-b660-60c22d1074bd",
         :block/content "TODO canned tomato paste",
         :db/id 9542,
         :block/marker "TODO"}
  }
 )

Details

return map

The name for the :keys syntax is a return map in the Datomic query language. From the Datomic documentation:

Return Maps
Supplying a return-map will cause the query to return maps instead of tuples. Each entry in the :keys / :strs / :syms clause will become a key mapped to the corresponding item in the :find clause.

Return maps also preserve the order of the :find clause.

Datomic grammar
The Datomic EBNF grammar specification shows the relationship between return maps and return-keys. Here’s a simplified version showing the relationship.

Datomic Query Argument Grammar
(simplified)

EBNF rule definition
query [find-spec return-map-spec? with-clause? inputs? where-clauses?]
find-spec ‘:find’ (variable OR pull-expr OR aggregate)
return-map-spec (return-keys OR return-syms OR return-strs)
return-keys ‘:keys’ plain-symbol+
return-syms ‘:syms’ plain-symbol+
return-strs ‘:strs’ plain-symbol+
plain-symbol symbol that does not begin with “$”, “?”, or “%”
with-clause ‘:with’ variable+

Syntax used in grammar

syntax symbol meaning
‘’ literal
() grouping
+ one or more
4 Likes

Wow, thank you for your detailed analysis. Way better than I could’ve put it. Also, I didn’t know about :pprint, I was looking very long for something like that.

To be honest, I wrote part of the query so long ago that I didn’t know completely why I did everything (which is probably why I wasn’t the status key in the view function but :task/:block/marker directly), so your explanation is also very helpful for me as a beginner when it comes to datomic (and the LogSeq APIs available to queries).

1 Like

Really really great query. Would it somehow be possible, to add the parent page of the task, to see some context, instead of just having a project property? I tried to figure it out myself, but have so little knowledge in advanced queries that all i could get was a id of sorts.

Welcome.

  • Get the name of the task’s page by adding (inside :where) these clauses:
      [?todo :block/page ?p]
      [?p :block/original-name ?p-name]
    
    • By the way, I would rename all ?todo with ?task
  • Update the beginning of the query to pass the name like this:
    :query [:find ?p-name ?status (pull ?todo [*])
      :keys page status task
    
  • Render the name by replacing (inside :view):
    • the first ">" (in title :th) with "Page"
    • the second ">" (in link :a) with (:page r)

Thank you so much. I have been trying to figure this out, but without any background coding knowledge it is rather difficult. I have another query with scheduled dates, and in that one I can click on the task and it opens up the task in the sidebar view. But in this query here I have to click the page link and it goes to the page, and not just the task block, and not in sidebar view. Would something like that be possible, so you could click on the page view to go to the page where the task is, but on the task itself to open just the task in the sidebar view.
The code I refer to is this:
#+BEGIN_QUERY
{:title “scheduled”
:query [:find (pull ?b [*])
:where
[?b :block/scheduled _]

 [?b :block/marker ?m]
 (not [(contains? #{"DONE" "CANCELED"} ?m)] )

]
:result-transform (fn [result] (map (fn [m] (update m :block/properties (fn [u] (assoc u :sched (get-in m [:block/scheduled]) ) ) ) ) result ) )
}
#+END_QUERY

  • Queries are usually opened in the sidebar, so as links in them redirect the main pane.
  • Try modifying the first two table-rows like this:
          [:td.whitespace-nowrap [:a {:href (str "#/page/" (clojure.string/replace (:page r) "/" "%2F"))} (:page r) ] ]
          [:td.whitespace-nowrap [:a {:href (str "#/page/" (get-in r [:task :block/uuid]))} (clojure.string/replace (first (str/split-lines (get-in r [:task :block/content]))) (re-pattern "^[^ ]+ ") "") ] ]
    
  • By the way, the sorting seems in need of some change as well:
      (sort-by
        (fn [d] (min (get-in d [:task :block/deadline] 99999999) (get-in d [:task :block/scheduled] 99999999)))
        result
      )
    

So great. Thank you again.