| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- (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 :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 (util/parse-int (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 (util/parse-int (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/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]])"))
|