query_dsl.cljs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. (ns frontend.db.query-dsl
  2. "Handles executing dsl queries a.k.a. simple queries"
  3. (:require [cljs-time.coerce :as tc]
  4. [cljs-time.core :as t]
  5. [cljs.reader :as reader]
  6. [clojure.set :as set]
  7. [clojure.string :as string]
  8. [clojure.walk :as walk]
  9. [frontend.state :as state]
  10. [frontend.date :as date]
  11. [frontend.db.model :as model]
  12. [frontend.db.query-react :as query-react]
  13. [frontend.db.utils :as db-utils]
  14. [frontend.db.rules :as rules]
  15. [frontend.template :as template]
  16. [logseq.graph-parser.text :as text]
  17. [frontend.util :as util]))
  18. ;; Query fields:
  19. ;; and
  20. ;; or
  21. ;; not
  22. ;; between
  23. ;; Example: (between -7d +7d)
  24. ;; (between created-at -1d today)
  25. ;; (between last-modified-at -1d today)
  26. ;; [[page-ref]]
  27. ;; property (block)
  28. ;; task (block)
  29. ;; priority (block)
  30. ;; page
  31. ;; page-property (page)
  32. ;; page-tags (page)
  33. ;; all-page-tags
  34. ;; project (block, TBD)
  35. ;; Sort by (field, asc/desc):
  36. ;; created_at
  37. ;; last_modified_at
  38. ;; (sort-by last_modified_at asc)
  39. ;; (between -7d +7d)
  40. ;; Time helpers
  41. ;; ============
  42. (defn- ->journal-day-int [input]
  43. (let [input (string/lower-case (name input))]
  44. (cond
  45. (= "today" input)
  46. (db-utils/date->int (t/today))
  47. (= "yesterday" input)
  48. (db-utils/date->int (t/yesterday))
  49. (= "tomorrow" input)
  50. (db-utils/date->int (t/plus (t/today) (t/days 1)))
  51. (text/page-ref? input)
  52. (let [input (-> (text/page-ref-un-brackets! input)
  53. (string/replace ":" "")
  54. (string/capitalize))]
  55. (when (date/valid-journal-title? input)
  56. (date/journal-title->int input)))
  57. :else
  58. (let [duration (util/parse-int (subs input 0 (dec (count input))))
  59. kind (last input)
  60. tf (case kind
  61. "y" t/years
  62. "m" t/months
  63. "w" t/weeks
  64. t/days)]
  65. (db-utils/date->int (t/plus (t/today) (tf duration)))))))
  66. (defn- ->timestamp [input]
  67. (let [input (string/lower-case (name input))]
  68. (cond
  69. (= "now" input)
  70. (util/time-ms)
  71. (= "today" input)
  72. (tc/to-long (t/today))
  73. (= "yesterday" input)
  74. (tc/to-long (t/yesterday))
  75. (= "tomorrow" input)
  76. (tc/to-long (t/plus (t/today) (t/days 1)))
  77. (text/page-ref? input)
  78. (let [input (-> (text/page-ref-un-brackets! input)
  79. (string/replace ":" "")
  80. (string/capitalize))]
  81. (when (date/valid-journal-title? input)
  82. (date/journal-title->long input)))
  83. :else
  84. (let [duration (util/parse-int (subs input 0 (dec (count input))))
  85. kind (last input)
  86. tf (case kind
  87. "y" t/years
  88. "m" t/months
  89. "w" t/weeks
  90. "h" t/hours
  91. "n" t/minutes ; min
  92. t/days)]
  93. (tc/to-long (t/plus (t/today) (tf duration)))))))
  94. ;; Boolean operator utils: and, or, not
  95. ;; ======================
  96. (defn- collect-vars
  97. [l]
  98. (let [vars (atom #{})]
  99. (walk/postwalk
  100. (fn [f]
  101. (when (and (symbol? f) (= \? (first (name f))))
  102. (swap! vars conj f))
  103. f)
  104. l)
  105. @vars))
  106. (defn- build-and-or-not-result
  107. [fe clauses current-filter nested-and?]
  108. (cond
  109. (= fe 'not)
  110. (if (every? list? clauses)
  111. (cons fe (seq clauses))
  112. (let [clauses (if (coll? (first clauses))
  113. (apply concat clauses)
  114. clauses)
  115. clauses (if (and (= 1 (count clauses))
  116. (= 'and (ffirst clauses)))
  117. ;; unflatten
  118. (rest (first clauses))
  119. clauses)]
  120. (cons fe (seq clauses))))
  121. (coll? (first clauses))
  122. (cond
  123. (= current-filter 'not)
  124. (cons 'and clauses)
  125. (or (= current-filter 'or)
  126. nested-and?)
  127. (cons 'and clauses)
  128. :else
  129. (->> clauses
  130. (map (fn [result]
  131. (if (list? result)
  132. result
  133. (let [result (if (vector? (ffirst result))
  134. (apply concat result)
  135. result)]
  136. (cons 'and (seq result))))))
  137. (apply list fe)))
  138. :else
  139. (apply list fe clauses)))
  140. (declare build-query)
  141. (defonce remove-nil? (partial remove nil?))
  142. (defn- build-and-or-not
  143. [e {:keys [current-filter vars] :as env} level fe]
  144. (let [raw-clauses (map (fn [form]
  145. (build-query form (assoc env :current-filter fe) (inc level)))
  146. (rest e))
  147. clauses (->> raw-clauses
  148. (map :query)
  149. remove-nil?
  150. (distinct))
  151. nested-and? (and (= fe 'and) (= current-filter 'and))]
  152. (when (seq clauses)
  153. (let [result (build-and-or-not-result
  154. fe clauses current-filter nested-and?)
  155. vars' (set/union (set @vars) (collect-vars result))
  156. query (cond
  157. nested-and?
  158. result
  159. (and (zero? level) (contains? #{'and 'or} fe))
  160. result
  161. (and (= 'not fe) (some? current-filter))
  162. result
  163. :else
  164. [result])]
  165. (reset! vars vars')
  166. {:query query
  167. :rules (distinct (mapcat :rules raw-clauses))}))))
  168. ;; build-query fns
  169. ;; ===============
  170. (defn- build-between-two-arg
  171. [e]
  172. (let [start (->journal-day-int (nth e 1))
  173. end (->journal-day-int (nth e 2))
  174. [start end] (sort [start end])]
  175. {:query (list 'between '?b start end)
  176. :rules [:between]}))
  177. (defn- build-between-three-arg
  178. [e]
  179. (let [k (-> (second e)
  180. (name)
  181. (string/lower-case)
  182. (string/replace "-" "_"))]
  183. (when (contains? #{"created_at" "last_modified_at"} k)
  184. (let [start (->timestamp (nth e 2))
  185. end (->timestamp (nth e 3))]
  186. (when (and start end)
  187. (let [[start end] (sort [start end])
  188. sym '?v]
  189. {:query [['?b :block/properties '?prop]
  190. [(list 'get '?prop k) sym]
  191. [(list '>= sym start)]
  192. [(list '< sym end)]]}))))))
  193. (defn- build-between
  194. [e]
  195. (cond
  196. (= 3 (count e))
  197. (build-between-two-arg e)
  198. ;; (between created_at -1d today)
  199. (= 4 (count e))
  200. (build-between-three-arg e)))
  201. (defn- build-property-two-arg
  202. [e]
  203. (let [k (string/replace (name (nth e 1)) "_" "-")
  204. v (nth e 2)
  205. v (if-not (nil? v)
  206. (text/parse-property k v (state/get-config))
  207. v)
  208. v (if (coll? v) (first v) v)]
  209. {:query (list 'property '?b (keyword k) v)
  210. :rules [:property]}))
  211. (defn- build-property-one-arg
  212. [e]
  213. (let [k (string/replace (name (nth e 1)) "_" "-")]
  214. {:query (list 'has-property '?b (keyword k))
  215. :rules [:has-property]}))
  216. (defn- build-property [e]
  217. (cond
  218. (= 3 (count e))
  219. (build-property-two-arg e)
  220. (= 2 (count e))
  221. (build-property-one-arg e)))
  222. (defn- build-task
  223. [e]
  224. (let [markers (if (coll? (first (rest e)))
  225. (first (rest e))
  226. (rest e))]
  227. (when (seq markers)
  228. (let [markers (set (map (comp string/upper-case name) markers))]
  229. {:query (list 'task '?b markers)
  230. :rules [:task]}))))
  231. (defn- build-priority
  232. [e]
  233. (let [priorities (if (coll? (first (rest e)))
  234. (first (rest e))
  235. (rest e))]
  236. (when (seq priorities)
  237. (let [priorities (set (map (comp string/upper-case name) priorities))]
  238. {:query (list 'priority '?b priorities)
  239. :rules [:priority]}))))
  240. (defn- build-page-property
  241. [e]
  242. (let [[k v] (rest e)
  243. k (string/replace (name k) "_" "-")]
  244. (if (some? v)
  245. (let [v' (text/parse-property k v (state/get-config))
  246. val (if (coll? v') (first v') v')]
  247. {:query (list 'page-property '?p (keyword k) val)
  248. :rules [:page-property]})
  249. {:query (list 'has-page-property '?p (keyword k))
  250. :rules [:has-page-property]})))
  251. (defn- build-page-tags
  252. [e]
  253. (let [tags (if (coll? (first (rest e)))
  254. (first (rest e))
  255. (rest e))
  256. tags (map (comp string/lower-case name) tags)]
  257. (when (seq tags)
  258. (let [tags (set (map (comp text/page-ref-un-brackets! string/lower-case name) tags))]
  259. {:query (list 'page-tags '?p tags)
  260. :rules [:page-tags]}))))
  261. (defn- build-all-page-tags
  262. []
  263. {:query (list 'all-page-tags '?p)
  264. :rules [:all-page-tags]} )
  265. (defn- build-sample
  266. [e sample]
  267. (when-let [num (second e)]
  268. (when (integer? num)
  269. (reset! sample num)
  270. {:query [['?p :block/uuid]]})))
  271. (defn- build-sort-by
  272. [e sort-by_]
  273. (let [[k order] (rest e)
  274. order (if (and order (contains? #{:asc :desc}
  275. (keyword (string/lower-case (name order)))))
  276. (keyword (string/lower-case (name order)))
  277. :desc)
  278. k (-> (string/lower-case (name k))
  279. (string/replace "_" "-"))
  280. get-value (cond
  281. (= k "created-at")
  282. :block/created-at
  283. (= k "updated-at")
  284. :block/updated-at
  285. :else
  286. #(get-in % [:block/properties k]))
  287. comp (if (= order :desc) >= <=)]
  288. (reset! sort-by_
  289. (fn [result]
  290. (->> result
  291. flatten
  292. (sort-by get-value comp))))
  293. nil))
  294. (defn- build-page
  295. [e]
  296. (let [page-name (text/page-ref-un-brackets! (str (first (rest e))))
  297. page-name (util/page-name-sanity-lc page-name)]
  298. {:query (list 'page '?b page-name)
  299. :rules [:page]}))
  300. (defn- build-namespace
  301. [e]
  302. (let [page-name (text/page-ref-un-brackets! (str (first (rest e))))
  303. page (util/page-name-sanity-lc page-name)]
  304. (when-not (string/blank? page)
  305. {:query (list 'namespace '?p page)
  306. :rules [:namespace]})))
  307. (defn- build-page-ref
  308. [e]
  309. (let [page-name (-> (text/page-ref-un-brackets! e)
  310. (util/page-name-sanity-lc))]
  311. {:query (list 'page-ref '?b page-name)
  312. :rules [:page-ref]}))
  313. (defn- build-block-content [e]
  314. {:query (list 'block-content '?b e)
  315. :rules [:block-content]})
  316. (defn build-query
  317. "This fn converts a form/list in a query e.g. `(operator arg1 arg2)` to its datalog
  318. equivalent. This fn is called recursively on sublists for boolean operators
  319. `and`, `or` and `not`. This fn should return a map with :query and :rules or nil.
  320. Some bindings in this fn:
  321. * e - the list being processed
  322. * fe - the query operator e.g. `property`"
  323. ([e env]
  324. (build-query e (assoc env :vars (atom {})) 0))
  325. ([e {:keys [sort-by blocks? sample] :as env :or {blocks? (atom nil)}} level]
  326. ; {:post [(or (nil? %) (map? %))]}
  327. (let [fe (first e)
  328. fe (when fe (symbol (string/lower-case (name fe))))
  329. page-ref? (text/page-ref? e)]
  330. (when (or (and page-ref?
  331. (not (contains? #{'page-property 'page-tags} (:current-filter env))))
  332. (contains? #{'between 'property 'todo 'task 'priority 'sort-by 'page} fe)
  333. (and (not page-ref?) (string? e)))
  334. (reset! blocks? true))
  335. (cond
  336. (nil? e)
  337. nil
  338. page-ref?
  339. (build-page-ref e)
  340. (string? e) ; block content full-text search, could be slow
  341. (build-block-content e)
  342. (contains? #{'and 'or 'not} fe)
  343. (build-and-or-not e env level fe)
  344. (= 'between fe)
  345. (build-between e)
  346. (= 'property fe)
  347. (build-property e)
  348. ;; task is the new name and todo is the old one
  349. (or (= 'todo fe) (= 'task fe))
  350. (build-task e)
  351. (= 'priority fe)
  352. (build-priority e)
  353. (= 'sort-by fe)
  354. (build-sort-by e sort-by)
  355. (= 'page fe)
  356. (build-page e)
  357. (= 'namespace fe)
  358. (build-namespace e)
  359. (= 'page-property fe)
  360. (build-page-property e)
  361. (= 'page-tags fe)
  362. (build-page-tags e)
  363. (= 'all-page-tags fe)
  364. (build-all-page-tags)
  365. (= 'sample fe)
  366. (build-sample e sample)
  367. :else
  368. nil))))
  369. ;; parse fns
  370. ;; =========
  371. (defn- pre-transform
  372. [s]
  373. (some-> s
  374. (string/replace text/page-ref-re "\"[[$1]]\"")
  375. (string/replace text/between-re (fn [[_ x]]
  376. (->> (string/split x #" ")
  377. (remove string/blank?)
  378. (map (fn [x]
  379. (if (or (contains? #{"+" "-"} (first x))
  380. (and (util/safe-re-find #"\d" (first x))
  381. (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
  382. (keyword (name x))
  383. x)))
  384. (string/join " ")
  385. (util/format "(between %s)"))))))
  386. (defn- add-bindings!
  387. [form q]
  388. (let [forms (set (flatten q))
  389. syms ['?b '?p 'not]
  390. [b? p? not?] (-> (set/intersection (set syms) forms)
  391. (map syms))
  392. or? (contains? (set (flatten form)) 'or)]
  393. (cond
  394. not?
  395. (cond
  396. (and b? p?)
  397. (concat [['?b :block/uuid] ['?p :block/name] ['?b :block/page '?p]] q)
  398. b?
  399. (concat [['?b :block/uuid]] q)
  400. p?
  401. (concat [['?p :block/name]] q)
  402. :else
  403. q)
  404. or?
  405. (cond
  406. (->> (flatten form)
  407. (remove text/page-ref?)
  408. (some string?)) ; block full-text search
  409. (concat [['?b :block/content '?content]] [q])
  410. :else
  411. q)
  412. (and b? p?)
  413. (concat [['?b :block/page '?p]] q)
  414. :else
  415. q)))
  416. (defn parse
  417. [s]
  418. (when (and (string? s)
  419. (not (string/blank? s)))
  420. (let [s (if (= \# (first s)) (util/format "[[%s]]" (subs s 1)) s)
  421. form (some-> s
  422. (pre-transform)
  423. (reader/read-string))
  424. sort-by (atom nil)
  425. blocks? (atom nil)
  426. sample (atom nil)
  427. {result :query rules :rules}
  428. (when form (build-query form {:sort-by sort-by
  429. :blocks? blocks?
  430. :sample sample}))
  431. result' (when (seq result)
  432. (let [key (if (coll? (first result))
  433. ;; Only queries for this branch are not's like:
  434. ;; [(not (page-ref ?b "page 2"))]
  435. (keyword (ffirst result))
  436. (keyword (first result)))]
  437. (add-bindings! form
  438. (if (= key :and) (rest result) result))))]
  439. {:query result'
  440. :rules (mapv rules/query-dsl-rules rules)
  441. :sort-by @sort-by
  442. :blocks? (boolean @blocks?)
  443. :sample sample})))
  444. ;; Main fns
  445. ;; ========
  446. (defn query-wrapper
  447. [where blocks?]
  448. (let [q (if blocks? ; FIXME: it doesn't need to be either blocks or pages
  449. `[:find (~'pull ~'?b ~model/block-attrs)
  450. :in ~'$ ~'%
  451. :where]
  452. '[:find (pull ?p [*])
  453. :in $ %
  454. :where])
  455. result (if (coll? (first where))
  456. (apply conj q where)
  457. (conj q where))]
  458. (prn "Datascript query: " result)
  459. result))
  460. (defn query
  461. "Runs a dsl query with query as a string. Primary use is from '{{query }}'"
  462. [repo query-string]
  463. (when (and (string? query-string) (not= "\"\"" query-string))
  464. (let [query-string' (template/resolve-dynamic-template! query-string)
  465. {:keys [query rules sort-by blocks? sample]} (parse query-string')]
  466. (when-let [query' (some-> query (query-wrapper blocks?))]
  467. (let [sort-by (or sort-by identity)
  468. random-samples (if @sample
  469. (fn [col]
  470. (take @sample (shuffle col)))
  471. identity)
  472. transform-fn (comp sort-by random-samples)]
  473. (query-react/react-query repo
  474. {:query query'
  475. :query-string query-string
  476. :rules rules}
  477. {:use-cache? false
  478. :transform-fn transform-fn}))))))
  479. (defn custom-query
  480. "Runs a dsl query with query as a seq. Primary use is from advanced query"
  481. [repo query-m query-opts]
  482. (when (seq (:query query-m))
  483. (let [query-string (template/resolve-dynamic-template! (pr-str (:query query-m)))
  484. {:keys [query sort-by blocks? rules]} (parse query-string)]
  485. (when-let [query' (some-> query (query-wrapper blocks?))]
  486. (query-react/react-query repo
  487. (merge
  488. query-m
  489. {:query query'
  490. :rules rules})
  491. (merge
  492. query-opts
  493. (when sort-by
  494. {:transform-fn sort-by})))))))
  495. (comment
  496. ;; {{query (and (page-property foo bar) [[hello]])}}
  497. (query "(and [[foo]] [[bar]])")
  498. (query "(or [[foo]] [[bar]])")
  499. (query "(not (or [[foo]] [[bar]]))")
  500. (query "(between -7d +7d)")
  501. (query "(between -7d today)")
  502. (query "(between created_at yesterday today)")
  503. (query "(and [[some page]] (property foo bar))")
  504. (query "(and [[some page]] (task now later))")
  505. (query "(and [[some page]] (priority A))")
  506. ;; nested query
  507. (query "(and [[baz]] (or [[foo]] [[bar]]))")
  508. (query "(and [[some page]] (sort-by created-at))")
  509. (query "(and (page-property foo bar) [[hello]])"))