|
9 | 9 | [logseq.db.common.entity-plus :as entity-plus] |
10 | 10 | [logseq.db.common.initial-data :as common-initial-data] |
11 | 11 | [logseq.db.frontend.class :as db-class] |
12 | | - [logseq.db.frontend.entity-util :as entity-util] |
13 | | - [logseq.db.frontend.rules :as rules])) |
| 12 | + [logseq.db.frontend.entity-util :as entity-util])) |
| 13 | + |
| 14 | +;; ----------------------------------------------------------------------------- |
| 15 | +;; Helpers (fast datoms access) |
| 16 | +;; ----------------------------------------------------------------------------- |
| 17 | + |
| 18 | +(defn- has-datom? [db e a v] |
| 19 | + (boolean (seq (d/datoms db :eavt e a v)))) |
| 20 | + |
| 21 | +(defn- entid |
| 22 | + "Normalize a datom value into an entity id. |
| 23 | + In DataScript, ref values are often raw entids (numbers), not maps." |
| 24 | + [v] |
| 25 | + (cond |
| 26 | + (nil? v) nil |
| 27 | + (number? v) v |
| 28 | + (and (map? v) (contains? v :db/id)) (:db/id v) |
| 29 | + :else v)) |
| 30 | + |
| 31 | +(defn- datom-v |
| 32 | + "Return the first value for attr a on entity e, normalized to entid." |
| 33 | + [db e a] |
| 34 | + (some-> (first (d/datoms db :eavt e a)) |
| 35 | + :v |
| 36 | + entid)) |
| 37 | + |
| 38 | +(defn- datom-vs |
| 39 | + "Return all values for attr a on entity e, normalized to entids, as a set." |
| 40 | + [db e a] |
| 41 | + (into #{} |
| 42 | + (comp (map :v) (map entid)) |
| 43 | + (d/datoms db :eavt e a))) |
| 44 | + |
| 45 | +(defn- get-path-refs |
| 46 | + [db entity] |
| 47 | + (let [refs (mapcat :block/refs (ldb/get-block-parents db (:block/uuid entity))) |
| 48 | + block-page (:block/page entity)] |
| 49 | + (->> (cond->> refs (some? block-page) (cons block-page)) |
| 50 | + distinct))) |
| 51 | + |
| 52 | +(defn- get-ref-pages-count |
| 53 | + [db id ref-blocks children-ids] |
| 54 | + (when (seq ref-blocks) |
| 55 | + (let [children (->> children-ids |
| 56 | + (map (fn [id] (d/entity db id))))] |
| 57 | + (->> (concat (mapcat #(get-path-refs db %) ref-blocks) |
| 58 | + (mapcat :block/refs (concat ref-blocks children))) |
| 59 | + frequencies |
| 60 | + (keep (fn [[ref size]] |
| 61 | + (when (and (ldb/page? ref) |
| 62 | + (not= (:db/id ref) id) |
| 63 | + (not= :block/tags (:db/ident ref)) |
| 64 | + (not (common-initial-data/hidden-ref? db ref id))) |
| 65 | + [(:block/title ref) size]))) |
| 66 | + (sort-by second #(> %1 %2)))))) |
| 67 | + |
| 68 | +(defn- child-ids |
| 69 | + "Direct children via AVET on :block/parent." |
| 70 | + [db parent-eid] |
| 71 | + (into #{} (map :e) (d/datoms db :avet :block/parent parent-eid))) |
| 72 | + |
| 73 | +(defn- own-refs |
| 74 | + "Refs contributed by this block itself: |
| 75 | + - direct :block/refs |
| 76 | + - implicit :block/page as a ref" |
| 77 | + [db eid] |
| 78 | + (let [direct (datom-vs db eid :block/refs) |
| 79 | + page (datom-v db eid :block/page)] |
| 80 | + (cond-> direct |
| 81 | + page (conj page)))) |
| 82 | + |
| 83 | +(defn- effective-refs-fn |
| 84 | + "effective-refs(eid) = own-refs(eid) ∪ effective-refs(parent(eid))" |
| 85 | + [db] |
| 86 | + (let [memo (volatile! {})] |
| 87 | + (letfn [(eff [eid] |
| 88 | + (if (contains? @memo eid) |
| 89 | + (get @memo eid) |
| 90 | + (let [own (own-refs db eid) |
| 91 | + res (if-let [p (datom-v db eid :block/parent)] |
| 92 | + (into own (eff p)) |
| 93 | + own)] |
| 94 | + (vswap! memo assoc eid res) |
| 95 | + res)))] |
| 96 | + eff))) |
| 97 | + |
| 98 | +(defn- allowed-subtree-refs-fn |
| 99 | + "Like subtree-refs-fn but PRUNES branches that are under an excluded ref. |
| 100 | + This implements: |
| 101 | + - parent should not be excluded just because a child has an excluded ref |
| 102 | + - BUT refs (e.g. [[bar]]) that appear under an excluded block (e.g. [[baz]]) |
| 103 | + should NOT satisfy includes for ancestors. |
| 104 | +
|
| 105 | + Concretely: if (effective-refs node) contains any excludes, this node and its |
| 106 | + descendants contribute NOTHING to the include reachability set." |
| 107 | + [db eff excludes] |
| 108 | + (let [memo (volatile! {})] |
| 109 | + (letfn [(blocked? [eid] |
| 110 | + (and (seq excludes) |
| 111 | + (some #(contains? (eff eid) %) excludes))) |
| 112 | + (sub [eid] |
| 113 | + (if (contains? @memo eid) |
| 114 | + (get @memo eid) |
| 115 | + (let [res (if (blocked? eid) |
| 116 | + #{} |
| 117 | + (reduce (fn [acc c] (into acc (sub c))) |
| 118 | + (own-refs db eid) |
| 119 | + (child-ids db eid)))] |
| 120 | + (vswap! memo assoc eid res) |
| 121 | + res)))] |
| 122 | + sub))) |
| 123 | + |
| 124 | +(defn- matches-filters? |
| 125 | + "Include semantics: AND (must all be present) against include-set. |
| 126 | + Exclude semantics: NONE (must not contain any) against exclude-set." |
| 127 | + [include-set exclude-set includes excludes] |
| 128 | + (and |
| 129 | + (or (empty? includes) |
| 130 | + (every? #(contains? include-set %) includes)) |
| 131 | + (or (empty? excludes) |
| 132 | + (not (some #(contains? exclude-set %) excludes))))) |
| 133 | + |
| 134 | +(defn- matched-ref-block-ids-under-top |
| 135 | + "Return the set of block ids (top or child) that match filters. |
| 136 | +
|
| 137 | + Semantics (matches your example): |
| 138 | + - Includes (AND) may be satisfied by descendants, BUT only from descendants |
| 139 | + that are NOT under an excluded ref. |
| 140 | + Example: |
| 141 | + - [[foo]] |
| 142 | + - [[baz]] |
| 143 | + - [[bar]] |
| 144 | + With include=bar, exclude=baz => foo should NOT match because bar is under baz. |
| 145 | +
|
| 146 | + - Excludes are checked ONLY against effective-refs(node) (self + parents), |
| 147 | + so a parent isn't excluded just because a child mentions an excluded page. |
| 148 | +
|
| 149 | + Pruning: |
| 150 | + - If includes cannot be satisfied anywhere below (effective + allowed-subtree), |
| 151 | + prune subtree." |
| 152 | + [db top-ref-block-ids includes excludes class-ids] |
| 153 | + (let [eff (effective-refs-fn db) |
| 154 | + ;; allowed subtree refs for includes (prunes excluded branches) |
| 155 | + allowed-subrefs (allowed-subtree-refs-fn db eff (->> excludes (remove nil?) vec)) |
| 156 | + includes (->> includes (remove nil?) vec) |
| 157 | + excludes (->> excludes (remove nil?) vec) |
| 158 | + class-ids (when (seq class-ids) (->> class-ids (remove nil?) vec)) |
| 159 | + |
| 160 | + can-satisfy-includes? (fn [eff-refs node] |
| 161 | + (or (empty? includes) |
| 162 | + (let [possible (into eff-refs (allowed-subrefs node))] |
| 163 | + (every? #(contains? possible %) includes)))) |
| 164 | + |
| 165 | + class-ok? (fn [eid] |
| 166 | + (or (empty? class-ids) |
| 167 | + (not (some #(has-datom? db eid :block/tags %) class-ids))))] |
| 168 | + |
| 169 | + (loop [stack (vec top-ref-block-ids) |
| 170 | + visited #{} |
| 171 | + out #{}] |
| 172 | + (if (empty? stack) |
| 173 | + out |
| 174 | + (let [eid (peek stack) |
| 175 | + stack (pop stack)] |
| 176 | + (if (contains? visited eid) |
| 177 | + (recur stack visited out) |
| 178 | + (let [visited (conj visited eid) |
| 179 | + eff-refs (eff eid)] |
| 180 | + (cond |
| 181 | + ;; don't match this node, but still traverse |
| 182 | + (not (class-ok? eid)) |
| 183 | + (recur (into stack (child-ids db eid)) visited out) |
| 184 | + |
| 185 | + ;; prune: AND includes cannot be satisfied anywhere below |
| 186 | + (not (can-satisfy-includes? eff-refs eid)) |
| 187 | + (recur stack visited out) |
| 188 | + |
| 189 | + :else |
| 190 | + (let [include-set (into eff-refs (allowed-subrefs eid)) |
| 191 | + exclude-set eff-refs |
| 192 | + out' (if (matches-filters? include-set exclude-set includes excludes) |
| 193 | + (conj out eid) |
| 194 | + out)] |
| 195 | + (recur (into stack (child-ids db eid)) visited out')))))))))) |
| 196 | + |
| 197 | +;; ----------------------------------------------------------------------------- |
| 198 | +;; Expand matched refs up to top refs |
| 199 | +;; ----------------------------------------------------------------------------- |
| 200 | + |
| 201 | +(defn- expand-to-top-refs |
| 202 | + "Given matched refs, add ancestors until reaching a top ref in `top-ref-ids`. |
| 203 | + Uses cached parent lookups (datoms only)." |
| 204 | + [db top-ref-ids matched-ref-ids] |
| 205 | + (let [parent-cache (volatile! {}) ;; eid -> parent-eid|nil |
| 206 | + result (volatile! #{}) |
| 207 | + top? (fn [eid] (contains? top-ref-ids eid)) |
| 208 | + parent-of (fn [eid] |
| 209 | + (if (contains? @parent-cache eid) |
| 210 | + (get @parent-cache eid) |
| 211 | + (let [p (some-> (first (d/datoms db :eavt eid :block/parent)) |
| 212 | + :v |
| 213 | + entid)] |
| 214 | + (vswap! parent-cache assoc eid p) |
| 215 | + p)))] |
| 216 | + (doseq [start matched-ref-ids] |
| 217 | + (loop [eid start] |
| 218 | + (when eid |
| 219 | + (cond |
| 220 | + (contains? @result eid) |
| 221 | + nil |
| 222 | + (top? eid) |
| 223 | + (vswap! result conj eid) |
| 224 | + :else |
| 225 | + (do |
| 226 | + (vswap! result conj eid) |
| 227 | + (recur (parent-of eid))))))) |
| 228 | + @result)) |
| 229 | + |
| 230 | +;; ----------------------------------------------------------------------------- |
| 231 | +;; Public API |
| 232 | +;; ----------------------------------------------------------------------------- |
14 | 233 |
|
15 | 234 | (defn get-filters |
16 | 235 | [db page] |
|
37 | 256 | (catch :default e |
38 | 257 | (log/error :syntax/filters e))))))) |
39 | 258 |
|
40 | | -(defn- build-include-exclude-query |
41 | | - [includes excludes] |
42 | | - (concat |
43 | | - (for [include includes] |
44 | | - (list 'has-ref '?b include)) |
45 | | - (for [exclude excludes] |
46 | | - (list 'not (list 'has-ref '?b exclude))))) |
47 | | - |
48 | | -(defn- filter-refs-query |
49 | | - [includes excludes class-ids] |
50 | | - (let [clauses (concat |
51 | | - (build-include-exclude-query includes excludes) |
52 | | - (for [class-id class-ids] |
53 | | - (list 'not ['?b :block/tags class-id])))] |
54 | | - (into [:find '[?b ...] |
55 | | - :in '$ '% '[?id ...] |
56 | | - :where |
57 | | - (list 'has-ref '?b '?id)] |
58 | | - clauses))) |
| 259 | +(defn get-linked-references [db id] |
| 260 | + (let [entity (d/entity db id) |
| 261 | + ids (set (cons id (ldb/get-block-alias db id))) |
| 262 | + page-filters (get-filters db entity) |
| 263 | + excludes (map :db/id (:excluded page-filters)) |
| 264 | + includes (map :db/id (:included page-filters)) |
| 265 | + has-filters? (or (seq excludes) (seq includes)) |
59 | 266 |
|
60 | | -(defn- get-path-refs |
61 | | - [db entity] |
62 | | - (let [refs (mapcat :block/refs (ldb/get-block-parents db (:block/uuid entity))) |
63 | | - block-page (:block/page entity)] |
64 | | - (->> (cond->> refs (some? block-page) (cons block-page)) |
65 | | - distinct))) |
| 267 | + class-ids (when (ldb/class? entity) |
| 268 | + (let [class-children (db-class/get-structured-children db id)] |
| 269 | + (set (conj class-children id)))) |
66 | 270 |
|
67 | | -(defn- get-ref-pages-count |
68 | | - [db id ref-blocks children-ids] |
69 | | - (when (seq ref-blocks) |
70 | | - (let [children (->> children-ids |
71 | | - (map (fn [id] (d/entity db id))))] |
72 | | - (->> (concat (mapcat #(get-path-refs db %) ref-blocks) |
73 | | - (mapcat :block/refs (concat ref-blocks children))) |
74 | | - frequencies |
75 | | - (keep (fn [[ref size]] |
76 | | - (when (and (ldb/page? ref) |
77 | | - (not= (:db/id ref) id) |
78 | | - (not= :block/tags (:db/ident ref)) |
79 | | - (not (common-initial-data/hidden-ref? db ref id))) |
80 | | - [(:block/title ref) size]))) |
81 | | - (sort-by second #(> %1 %2)))))) |
| 271 | + ;; Collect all top ref blocks that directly reference the page (or any alias). |
| 272 | + full-ref-block-ids |
| 273 | + (->> ids |
| 274 | + (mapcat (fn [pid] (:block/_refs (d/entity db pid)))) |
| 275 | + (remove (fn [ref] |
| 276 | + (or |
| 277 | + (when class-ids |
| 278 | + (some class-ids (map :db/id (:block/tags ref)))) |
| 279 | + (entity-util/hidden? ref) |
| 280 | + (entity-util/hidden? (:block/page ref))))) |
| 281 | + (map :db/id) |
| 282 | + set) |
82 | 283 |
|
83 | | -(defn- get-block-parents-until-top-ref |
84 | | - [db id ref-id ref-block-ids *result] |
85 | | - (loop [eid ref-id |
86 | | - parents' []] |
87 | | - (when eid |
88 | | - (cond |
89 | | - (contains? @*result eid) |
90 | | - (swap! *result into parents') |
91 | | - |
92 | | - (contains? ref-block-ids eid) |
93 | | - (when-not (common-initial-data/hidden-ref? db (d/entity db eid) id) |
94 | | - (swap! *result into (conj parents' eid))) |
95 | | - :else |
96 | | - (let [e (d/entity db eid)] |
97 | | - (recur (:db/id (:block/parent e)) (conj parents' eid))))))) |
98 | | - |
99 | | -;; TODO(perf): recursive datascript rule is still too slow for filters for large graphs |
100 | | -(defn get-linked-references |
101 | | - [db id] |
102 | | - (let [entity (d/entity db id) |
103 | | - ids (set (cons id (ldb/get-block-alias db id))) |
104 | | - page-filters (get-filters db entity) |
105 | | - excludes (map :db/id (:excluded page-filters)) |
106 | | - includes (map :db/id (:included page-filters)) |
107 | | - has-filters? (or (seq excludes) (seq includes)) |
108 | | - class-ids (when (ldb/class? entity) |
109 | | - (let [class-children (db-class/get-structured-children db id)] |
110 | | - (set (conj class-children id)))) |
111 | | - full-ref-block-ids (->> ids |
112 | | - (mapcat (fn [id] (:block/_refs (d/entity db id)))) |
113 | | - (remove (fn [ref] |
114 | | - (or |
115 | | - (when class-ids |
116 | | - (some class-ids (map :db/id (:block/tags ref)))) |
117 | | - (entity-util/hidden? ref) |
118 | | - (entity-util/hidden? (:block/page ref))))) |
119 | | - (map :db/id) |
120 | | - set) |
121 | | - matched-ref-block-ids (when has-filters? |
122 | | - (let [ref-ids (d/q (filter-refs-query includes excludes class-ids) |
123 | | - db |
124 | | - (rules/extract-rules rules/db-query-dsl-rules |
125 | | - [:has-ref] |
126 | | - {:deps rules/rules-dependencies}) |
127 | | - ids)] |
128 | | - (set ref-ids))) |
129 | | - matched-refs-with-children-ids (when has-filters? |
130 | | - (let [*result (atom #{})] |
131 | | - (doseq [ref-id matched-ref-block-ids] |
132 | | - (get-block-parents-until-top-ref db id ref-id full-ref-block-ids *result)) |
133 | | - @*result)) |
134 | | - ref-blocks (->> (if has-filters? |
135 | | - (set/intersection full-ref-block-ids matched-refs-with-children-ids) |
136 | | - full-ref-block-ids) |
137 | | - (map (fn [id] (d/entity db id)))) |
138 | | - children-ids (if has-filters? |
139 | | - (set (remove full-ref-block-ids matched-refs-with-children-ids)) |
140 | | - (->> (mapcat (fn [ref] (ldb/get-block-children-ids db (:db/id ref))) ref-blocks) |
141 | | - set))] |
| 284 | + ;; matched can be top or child ids |
| 285 | + matched-ref-block-ids |
| 286 | + (when has-filters? |
| 287 | + (matched-ref-block-ids-under-top db full-ref-block-ids includes excludes class-ids)) |
| 288 | + |
| 289 | + ;; Expand matches up to top refs so we can show parent chains and matched children. |
| 290 | + matched-refs-with-children-ids |
| 291 | + (when has-filters? |
| 292 | + (expand-to-top-refs db full-ref-block-ids matched-ref-block-ids)) |
| 293 | + |
| 294 | + final-ref-ids |
| 295 | + (if has-filters? |
| 296 | + (set/intersection full-ref-block-ids matched-refs-with-children-ids) |
| 297 | + full-ref-block-ids) |
| 298 | + |
| 299 | + ;; Materialize only at the end. |
| 300 | + ref-blocks (map #(d/entity db %) final-ref-ids) |
| 301 | + |
| 302 | + children-ids |
| 303 | + (if has-filters? |
| 304 | + (set (remove full-ref-block-ids matched-refs-with-children-ids)) |
| 305 | + (->> ref-blocks |
| 306 | + (mapcat (fn [ref] (ldb/get-block-children-ids db (:db/id ref)))) |
| 307 | + set))] |
142 | 308 | {:ref-blocks ref-blocks |
143 | 309 | :ref-pages-count (get-ref-pages-count db id ref-blocks children-ids) |
144 | 310 | :ref-matched-children-ids (when has-filters? children-ids)})) |
|
150 | 316 | (when-not (string/blank? title) |
151 | 317 | (let [ids (->> (d/datoms db :avet :block/title) |
152 | 318 | (keep (fn [d] |
153 | | - (when (and (not= id (:e d)) (string/includes? (string/lower-case (:v d)) title)) |
| 319 | + (when (and (not= id (:e d)) |
| 320 | + (string/includes? (string/lower-case (:v d)) title)) |
154 | 321 | (:e d)))))] |
155 | 322 | (keep |
156 | 323 | (fn [eid] |
|
0 commit comments