Switching a query's scope between graph-level and page-level with a single argument

Here’s a way to reuse the same advanced query as a global query (getting results from the whole graph) or as a page-specific query, by just changing one variable in the inputs.

Why?

Here’s my own purpose for doing this. You might find it useful, or repurpose it for some other use:

I was inspired by @alex0 's contextual sidebar topic, and wanted to have a set of queries for tasks on project pages that mirror the queries I have for my whole graph.

In my case, I have:

  • a page full of graph-level task management queries
  • a page full of page-level task management queries (for the sidebar, or to embed in whatever page)

A lot of these queries are identical, but their scope needs to be changed. I wanted to be able to keep one single list of the queries, and if I make an adjustment to it, the adjustment can easily apply to both versions of the query (graph-level and page-level).

I can have a page full of such queries, make changes to any of them, and when/if I want them to apply at the page level instead of the global level, just change one argument false to true.

Example

Starting with this query from @Siferiax in this post:

Example query: Before

#+BEGIN_QUERY
{:title ["📆 near TODOs (next 7 days, scheduled or deadline)"] 
 :query [:find (pull ?b [*])
   :in $ ?start ?next
   :where
   (or
     [?b :block/scheduled ?d]
     [?b :block/deadline ?d]
   )
   [(>= ?d ?start)]
   [(<= ?d ?next)]
 ]
 :result-transform (fn [result] (sort-by (juxt (fn [d] (get d :block/deadline) ) (fn [d] (get d :block/scheduled) ) ) result))
 :inputs [:today :7d-after]
 :collapsed? false}
#+END_QUERY

Example query: After

#+BEGIN_QUERY
{:title ["📆 near TODOs (next 7 days, scheduled or deadline)"] 
 :query [:find (pull ?b [*])
   :in $ ?start ?next ?inputpage ?q
   :where
   [?b :block/page ?p]  
   (or
     (not [(= ?q true)])
     (and [(= ?q true)]
        [?p :block/name ?inputpage]))
   (or
     [?b :block/scheduled ?d]
     [?b :block/deadline ?d]
   )
   [(>= ?d ?start)]
   [(<= ?d ?next)]
 ]
 :result-transform (fn [result] (sort-by (juxt (fn [d] (get d :block/deadline)) (fn [d] (get d :block/scheduled))) result))
 :inputs [:today :7d-after :current-page false] ; last argument (true or false): to query all pages, use false. to query current page only, use true.
 :collapsed? false}
#+END_QUERY

As it stands, the adapted query produces identical results to the first query. Now if we want it scoped to whatever page is in the main view of Logseq, we just change false to true in the :inputs line:

 :inputs [:today :7d-after :current-page true]

Step By Step

If you’re not adept at reading Datalog, here’s my best attempt at explaining how making these changes works, so that you can apply them to your own queries. (You may have to make adjustments depending on the details of whatever query you’re adapting):

  1. First we add new inputs :current-page false at the end of whatever inputs are already there. So in this query, the new line will be

    :inputs [:today :7d-after :current-page false]
    

    If your query doesn’t have an :input line, add one following the :where line with only

    :inputs [:current-page false]
    
  2. Next we need to accept the inputs, so to the :in line we append ?inputpage ?q:

    :in $ ?start ?next ?inputpage ?q
    

    If your query has rules and therefore a % at the end of the :in line, keep the % at the end.

    And if your query doesn’t have an :in line, add one with only

    :in $ ?inputpage ?q
    
  3. Now we add lines to select only blocks that belong to the :query-page, if the last input argument is true, or any page, if false:

    [?b :block/page ?p]  
    (or
      (not [(= ?q true)])
      (and [(= ?q true)]
         [?p :block/name ?inputpage]))
    

    If the argument is not true, the value of ?p is basically all pages. If it is true, ?p is equal to the :current-page. Thanks to @mentaloid for this improved logic over that of the original post. (This comment refers text from the original post which we’re no longer using, but it’s useful info nonetheless about using _ in queries)

    Some important notes:

    • The example query was already set up to return blocks, and those blocks were assigned the variable ?b: (:query [:find (pull ?b [*]). So the line in the code above will work, because ?b already referred to the blocks we’re looking for.
    • The example query didn’t already have the line [?b :block/page ?p] that finds the pages of blocks. In fact, it didn’t even filter for pages. So we’re safe to add that line. If the query did filter for :block/page somewhere (e.g., if it had a similar line like [?b :block/page ?page] we’d need to check carefully how variables are being used before
    • If your query uses a different variable than ?b for blocks and ?p for pages, adjust accordingly.
    • You’ll have to dig into the logic and figure things out if:
      • your query is more complex (for example, if it uses more than one variable for pages and/or for blocks)
      • it returns pages or something else instead of blocks
      • Datalog just doesn’t seem friendly to you today
  4. Change false to true, and now the query is scoped to the current page in main view.

  5. You can use the page-scoped version in several ways:

    • directly in a page
    • as a block embed in a page
    • in a page in the sidebar, while viewing the page you want results for

I love it! Just some extra clarification for people coming across this:

_ should be read as “any” value. Or not a defined value.
Which is to say with this usage ?p becomes all pages in the graph. As [?p :block/name _] simply means a page with any name.
So it isn’t necessarily empty as it is any.

2 Likes

Could avoid the rule with something like this:

(or
  (not [(= ?q true)])
  (and [(= ?q true)]
    [?p :block/name ?inputpage]))
1 Like

Thanks for that clarification! To realize that makes the behavior more logical, for sure. I updated the post.

I’m having trouble making that work with :current-page true in :input to scope to ?inputpage. Tried it with or-join [?q ?inputpage] but no luck. Could you share an example?

  • Here is an example:
    #+BEGIN_QUERY
    {:title ["📆 near TODOs (next 7 days, scheduled or deadline)"] 
     :query [:find (pull ?b [*])
       :in $ ?start ?next ?inputpage ?q
       :where
       [?b :block/page ?p]  
       (or
         (not [(= ?q true)])
         (and [(= ?q true)]
            [?p :block/name ?inputpage]))
       (or
         [?b :block/scheduled ?d]
         [?b :block/deadline ?d]
       )
       [(>= ?d ?start)]
       [(<= ?d ?next)]
     ]
     :result-transform (fn [result] (sort-by (juxt (fn [d] (get d :block/deadline)) (fn [d] (get d :block/scheduled))) result))
     :inputs [:today :7d-after :current-page true] ; last argument (true or false): to query all pages, use false. to query current page only, use true.
     :collapsed? false}
    #+END_QUERY
    
  • or-join would also need ?p, i.e. or-join [?q ?p ?inputpage]
2 Likes

ah, ok, that works! When I tried it, I mistakenly put [?b :block/page ?p] below the (or...). This is simpler. I’ll update the original post.

1 Like