Files
logseq/src/main/frontend/db/query_dsl.cljs
Gabriel Horner 15b8e5e9df Setup CI jobs that are only for graph-parser
Also move all non graph-parser fns to frontend thanks to carve lint
2022-05-26 18:04:45 -04:00

609 lines
18 KiB
Clojure

(ns frontend.db.query-dsl
"Handles executing dsl queries a.k.a. simple queries"
(:require [cljs-time.coerce :as tc]
[cljs-time.core :as t]
[cljs.reader :as reader]
[clojure.set :as set]
[clojure.string :as string]
[clojure.walk :as walk]
[frontend.state :as state]
[frontend.date :as date]
[frontend.db.model :as model]
[frontend.db.query-react :as query-react]
[frontend.db.utils :as db-utils]
[frontend.db.rules :as rules]
[frontend.template :as template]
[logseq.graph-parser.text :as text]
[frontend.util.text :as text-util]
[frontend.util :as util]))
;; Query fields:
;; and
;; or
;; not
;; between
;; Example: (between -7d +7d)
;; (between created-at -1d today)
;; (between last-modified-at -1d today)
;; [[page-ref]]
;; property (block)
;; task (block)
;; priority (block)
;; page
;; page-property (page)
;; page-tags (page)
;; all-page-tags
;; project (block, TBD)
;; Sort by (field, asc/desc):
;; created_at
;; last_modified_at
;; (sort-by last_modified_at asc)
;; (between -7d +7d)
;; Time helpers
;; ============
(defn- ->journal-day-int [input]
(let [input (string/lower-case (name input))]
(cond
(= "today" input)
(db-utils/date->int (t/today))
(= "yesterday" input)
(db-utils/date->int (t/yesterday))
(= "tomorrow" input)
(db-utils/date->int (t/plus (t/today) (t/days 1)))
(text/page-ref? input)
(let [input (-> (text/page-ref-un-brackets! input)
(string/replace ":" "")
(string/capitalize))]
(when (date/valid-journal-title? input)
(date/journal-title->int input)))
:else
(let [duration (parse-long (subs input 0 (dec (count input))))
kind (last input)
tf (case kind
"y" t/years
"m" t/months
"w" t/weeks
t/days)]
(db-utils/date->int (t/plus (t/today) (tf duration)))))))
(defn- ->timestamp [input]
(let [input (string/lower-case (name input))]
(cond
(= "now" input)
(util/time-ms)
(= "today" input)
(tc/to-long (t/today))
(= "yesterday" input)
(tc/to-long (t/yesterday))
(= "tomorrow" input)
(tc/to-long (t/plus (t/today) (t/days 1)))
(text/page-ref? input)
(let [input (-> (text/page-ref-un-brackets! input)
(string/replace ":" "")
(string/capitalize))]
(when (date/valid-journal-title? input)
(date/journal-title->long input)))
:else
(let [duration (parse-long (subs input 0 (dec (count input))))
kind (last input)
tf (case kind
"y" t/years
"m" t/months
"w" t/weeks
"h" t/hours
"n" t/minutes ; min
t/days)]
(tc/to-long (t/plus (t/today) (tf duration)))))))
;; Boolean operator utils: and, or, not
;; ======================
(defn- collect-vars
[l]
(let [vars (atom #{})]
(walk/postwalk
(fn [f]
(when (and (symbol? f) (= \? (first (name f))))
(swap! vars conj f))
f)
l)
@vars))
(defn- build-and-or-not-result
[fe clauses current-filter nested-and?]
(cond
(= fe 'not)
(if (every? list? clauses)
(cons fe (seq clauses))
(let [clauses (if (coll? (first clauses))
(apply concat clauses)
clauses)
clauses (if (and (= 1 (count clauses))
(= 'and (ffirst clauses)))
;; unflatten
(rest (first clauses))
clauses)]
(cons fe (seq clauses))))
(coll? (first clauses))
(cond
(= current-filter 'not)
(cons 'and clauses)
(or (= current-filter 'or)
nested-and?)
(cons 'and clauses)
:else
(->> clauses
(map (fn [result]
(if (list? result)
result
(let [result (if (vector? (ffirst result))
(apply concat result)
result)]
(cons 'and (seq result))))))
(apply list fe)))
:else
(apply list fe clauses)))
(declare build-query)
(defonce remove-nil? (partial remove nil?))
(defn- build-and-or-not
[e {:keys [current-filter vars] :as env} level fe]
(let [raw-clauses (map (fn [form]
(build-query form (assoc env :current-filter fe) (inc level)))
(rest e))
clauses (->> raw-clauses
(map :query)
remove-nil?
(distinct))
nested-and? (and (= fe 'and) (= current-filter 'and))]
(when (seq clauses)
(let [result (build-and-or-not-result
fe clauses current-filter nested-and?)
vars' (set/union (set @vars) (collect-vars result))
query (cond
nested-and?
result
(and (zero? level) (contains? #{'and 'or} fe))
result
(and (= 'not fe) (some? current-filter))
result
:else
[result])]
(reset! vars vars')
{:query query
:rules (distinct (mapcat :rules raw-clauses))}))))
;; build-query fns
;; ===============
(defn- build-between-two-arg
[e]
(let [start (->journal-day-int (nth e 1))
end (->journal-day-int (nth e 2))
[start end] (sort [start end])]
{:query (list 'between '?b start end)
:rules [:between]}))
(defn- build-between-three-arg
[e]
(let [k (-> (second e)
(name)
(string/lower-case)
(string/replace "-" "_"))]
(when (contains? #{"created_at" "last_modified_at"} k)
(let [start (->timestamp (nth e 2))
end (->timestamp (nth e 3))]
(when (and start end)
(let [[start end] (sort [start end])
sym '?v]
{:query [['?b :block/properties '?prop]
[(list 'get '?prop k) sym]
[(list '>= sym start)]
[(list '< sym end)]]}))))))
(defn- build-between
[e]
(cond
(= 3 (count e))
(build-between-two-arg e)
;; (between created_at -1d today)
(= 4 (count e))
(build-between-three-arg e)))
(defn- build-property-two-arg
[e]
(let [k (string/replace (name (nth e 1)) "_" "-")
v (nth e 2)
v (if-not (nil? v)
(text/parse-property k v (state/get-config))
v)
v (if (coll? v) (first v) v)]
{:query (list 'property '?b (keyword k) v)
:rules [:property]}))
(defn- build-property-one-arg
[e]
(let [k (string/replace (name (nth e 1)) "_" "-")]
{:query (list 'has-property '?b (keyword k))
:rules [:has-property]}))
(defn- build-property [e]
(cond
(= 3 (count e))
(build-property-two-arg e)
(= 2 (count e))
(build-property-one-arg e)))
(defn- build-task
[e]
(let [markers (if (coll? (first (rest e)))
(first (rest e))
(rest e))]
(when (seq markers)
(let [markers (set (map (comp string/upper-case name) markers))]
{:query (list 'task '?b markers)
:rules [:task]}))))
(defn- build-priority
[e]
(let [priorities (if (coll? (first (rest e)))
(first (rest e))
(rest e))]
(when (seq priorities)
(let [priorities (set (map (comp string/upper-case name) priorities))]
{:query (list 'priority '?b priorities)
:rules [:priority]}))))
(defn- build-page-property
[e]
(let [[k v] (rest e)
k (string/replace (name k) "_" "-")]
(if (some? v)
(let [v' (text/parse-property k v (state/get-config))
val (if (coll? v') (first v') v')]
{:query (list 'page-property '?p (keyword k) val)
:rules [:page-property]})
{:query (list 'has-page-property '?p (keyword k))
:rules [:has-page-property]})))
(defn- build-page-tags
[e]
(let [tags (if (coll? (first (rest e)))
(first (rest e))
(rest e))
tags (map (comp string/lower-case name) tags)]
(when (seq tags)
(let [tags (set (map (comp text/page-ref-un-brackets! string/lower-case name) tags))]
{:query (list 'page-tags '?p tags)
:rules [:page-tags]}))))
(defn- build-all-page-tags
[]
{:query (list 'all-page-tags '?p)
:rules [:all-page-tags]} )
(defn- build-sample
[e sample]
(when-let [num (second e)]
(when (integer? num)
(reset! sample num)
{:query [['?p :block/uuid]]})))
(defn- build-sort-by
[e sort-by_]
(let [[k order] (rest e)
order (if (and order (contains? #{:asc :desc}
(keyword (string/lower-case (name order)))))
(keyword (string/lower-case (name order)))
:desc)
k (-> (string/lower-case (name k))
(string/replace "_" "-"))
get-value (cond
(= k "created-at")
:block/created-at
(= k "updated-at")
:block/updated-at
:else
#(get-in % [:block/properties k]))
comp (if (= order :desc) >= <=)]
(reset! sort-by_
(fn [result]
(->> result
flatten
(sort-by get-value comp))))
nil))
(defn- build-page
[e]
(let [page-name (text/page-ref-un-brackets! (str (first (rest e))))
page-name (util/page-name-sanity-lc page-name)]
{:query (list 'page '?b page-name)
:rules [:page]}))
(defn- build-namespace
[e]
(let [page-name (text/page-ref-un-brackets! (str (first (rest e))))
page (util/page-name-sanity-lc page-name)]
(when-not (string/blank? page)
{:query (list 'namespace '?p page)
:rules [:namespace]})))
(defn- build-page-ref
[e]
(let [page-name (-> (text/page-ref-un-brackets! e)
(util/page-name-sanity-lc))]
{:query (list 'page-ref '?b page-name)
:rules [:page-ref]}))
(defn- build-block-content [e]
{:query (list 'block-content '?b e)
:rules [:block-content]})
(defn build-query
"This fn converts a form/list in a query e.g. `(operator arg1 arg2)` to its datalog
equivalent. This fn is called recursively on sublists for boolean operators
`and`, `or` and `not`. This fn should return a map with :query and :rules or nil.
Some bindings in this fn:
* e - the list being processed
* fe - the query operator e.g. `property`"
([e env]
(build-query e (assoc env :vars (atom {})) 0))
([e {:keys [sort-by blocks? sample] :as env :or {blocks? (atom nil)}} level]
; {:post [(or (nil? %) (map? %))]}
(let [fe (first e)
fe (when fe (symbol (string/lower-case (name fe))))
page-ref? (text/page-ref? e)]
(when (or (and page-ref?
(not (contains? #{'page-property 'page-tags} (:current-filter env))))
(contains? #{'between 'property 'todo 'task 'priority 'sort-by 'page} fe)
(and (not page-ref?) (string? e)))
(reset! blocks? true))
(cond
(nil? e)
nil
page-ref?
(build-page-ref e)
(string? e) ; block content full-text search, could be slow
(build-block-content e)
(contains? #{'and 'or 'not} fe)
(build-and-or-not e env level fe)
(= 'between fe)
(build-between e)
(= 'property fe)
(build-property e)
;; task is the new name and todo is the old one
(or (= 'todo fe) (= 'task fe))
(build-task e)
(= 'priority fe)
(build-priority e)
(= 'sort-by fe)
(build-sort-by e sort-by)
(= 'page fe)
(build-page e)
(= 'namespace fe)
(build-namespace e)
(= 'page-property fe)
(build-page-property e)
(= 'page-tags fe)
(build-page-tags e)
(= 'all-page-tags fe)
(build-all-page-tags)
(= 'sample fe)
(build-sample e sample)
:else
nil))))
;; parse fns
;; =========
(defn- pre-transform
[s]
(some-> s
(string/replace text/page-ref-re "\"[[$1]]\"")
(string/replace text-util/between-re
(fn [[_ x]]
(->> (string/split x #" ")
(remove string/blank?)
(map (fn [x]
(if (or (contains? #{"+" "-"} (first x))
(and (util/safe-re-find #"\d" (first x))
(some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
(keyword (name x))
x)))
(string/join " ")
(util/format "(between %s)"))))))
(defn- add-bindings!
[form q]
(let [forms (set (flatten q))
syms ['?b '?p 'not]
[b? p? not?] (-> (set/intersection (set syms) forms)
(map syms))
or? (contains? (set (flatten form)) 'or)]
(cond
not?
(cond
(and b? p?)
(concat [['?b :block/uuid] ['?p :block/name] ['?b :block/page '?p]] q)
b?
(concat [['?b :block/uuid]] q)
p?
(concat [['?p :block/name]] q)
:else
q)
or?
(cond
(->> (flatten form)
(remove text/page-ref?)
(some string?)) ; block full-text search
(concat [['?b :block/content '?content]] [q])
:else
q)
(and b? p?)
(concat [['?b :block/page '?p]] q)
:else
q)))
(defn parse
[s]
(when (and (string? s)
(not (string/blank? s)))
(let [s (if (= \# (first s)) (util/format "[[%s]]" (subs s 1)) s)
form (some-> s
(pre-transform)
(reader/read-string))
sort-by (atom nil)
blocks? (atom nil)
sample (atom nil)
{result :query rules :rules}
(when form (build-query form {:sort-by sort-by
:blocks? blocks?
:sample sample}))
result' (when (seq result)
(let [key (if (coll? (first result))
;; Only queries for this branch are not's like:
;; [(not (page-ref ?b "page 2"))]
(keyword (ffirst result))
(keyword (first result)))]
(add-bindings! form
(if (= key :and) (rest result) result))))]
{:query result'
:rules (mapv rules/query-dsl-rules rules)
:sort-by @sort-by
:blocks? (boolean @blocks?)
:sample sample})))
;; Main fns
;; ========
(defn query-wrapper
[where blocks?]
(let [q (if blocks? ; FIXME: it doesn't need to be either blocks or pages
`[:find (~'pull ~'?b ~model/block-attrs)
:in ~'$ ~'%
:where]
'[:find (pull ?p [*])
:in $ %
:where])
result (if (coll? (first where))
(apply conj q where)
(conj q where))]
(prn "Datascript query: " result)
result))
(defn query
"Runs a dsl query with query as a string. Primary use is from '{{query }}'"
[repo query-string]
(when (and (string? query-string) (not= "\"\"" query-string))
(let [query-string' (template/resolve-dynamic-template! query-string)
{:keys [query rules sort-by blocks? sample]} (parse query-string')]
(when-let [query' (some-> query (query-wrapper blocks?))]
(let [sort-by (or sort-by identity)
random-samples (if @sample
(fn [col]
(take @sample (shuffle col)))
identity)
transform-fn (comp sort-by random-samples)]
(query-react/react-query repo
{:query query'
:query-string query-string
:rules rules}
{:use-cache? false
:transform-fn transform-fn}))))))
(defn custom-query
"Runs a dsl query with query as a seq. Primary use is from advanced query"
[repo query-m query-opts]
(when (seq (:query query-m))
(let [query-string (template/resolve-dynamic-template! (pr-str (:query query-m)))
{:keys [query sort-by blocks? rules]} (parse query-string)]
(when-let [query' (some-> query (query-wrapper blocks?))]
(query-react/react-query repo
(merge
query-m
{:query query'
:rules rules})
(merge
query-opts
(when sort-by
{:transform-fn sort-by})))))))
(comment
;; {{query (and (page-property foo bar) [[hello]])}}
(query "(and [[foo]] [[bar]])")
(query "(or [[foo]] [[bar]])")
(query "(not (or [[foo]] [[bar]]))")
(query "(between -7d +7d)")
(query "(between -7d today)")
(query "(between created_at yesterday today)")
(query "(and [[some page]] (property foo bar))")
(query "(and [[some page]] (task now later))")
(query "(and [[some page]] (priority A))")
;; nested query
(query "(and [[baz]] (or [[foo]] [[bar]]))")
(query "(and [[some page]] (sort-by created-at))")
(query "(and (page-property foo bar) [[hello]])"))