mirror of
https://github.com/logseq/logseq.git
synced 2026-05-18 09:52:22 +00:00
enhance: export-edn :build/class-properties sorts properties
both when building and exporting
This commit is contained in:
82
deps/db/src/logseq/db/sqlite/build.cljs
vendored
82
deps/db/src/logseq/db/sqlite/build.cljs
vendored
@@ -233,9 +233,10 @@
|
||||
:block/refs block-refs})))))))
|
||||
|
||||
(defn- build-property-tx
|
||||
[properties page-uuids all-idents property-db-ids options
|
||||
[properties page-uuids all-idents property-db-ids class-property-orders options
|
||||
[prop-name {:build/keys [property-classes] :as prop-m}]]
|
||||
(let [[new-block & additional-tx]
|
||||
(let [class-property-order (get class-property-orders prop-name)
|
||||
[new-block & additional-tx]
|
||||
(if-let [closed-values (seq (map #(merge {:uuid (random-uuid)} %) (:build/closed-values prop-m)))]
|
||||
(let [db-ident (get-ident all-idents prop-name)]
|
||||
(db-property-build/build-closed-values
|
||||
@@ -245,14 +246,18 @@
|
||||
{:property-attributes
|
||||
(merge {:db/id (or (property-db-ids prop-name)
|
||||
(throw (ex-info "No :db/id for property" {:property prop-name})))}
|
||||
(when class-property-order
|
||||
{:block/order class-property-order})
|
||||
(select-keys prop-m [:build/properties-ref-types :block/created-at :block/updated-at :block/collapsed?]))}))
|
||||
[(merge (sqlite-util/build-new-property (get-ident all-idents prop-name)
|
||||
(db-property/get-property-schema prop-m)
|
||||
{:block-uuid (:block/uuid prop-m)
|
||||
:title (:block/title prop-m)})
|
||||
{:db/id (or (property-db-ids prop-name)
|
||||
(throw (ex-info "No :db/id for property" {:property prop-name})))}
|
||||
(select-keys prop-m [:build/properties-ref-types :block/created-at :block/updated-at :block/collapsed?]))])
|
||||
[(cond-> (merge (sqlite-util/build-new-property (get-ident all-idents prop-name)
|
||||
(db-property/get-property-schema prop-m)
|
||||
{:block-uuid (:block/uuid prop-m)
|
||||
:title (:block/title prop-m)})
|
||||
{:db/id (or (property-db-ids prop-name)
|
||||
(throw (ex-info "No :db/id for property" {:property prop-name})))}
|
||||
(select-keys prop-m [:build/properties-ref-types :block/created-at :block/updated-at :block/collapsed?]))
|
||||
class-property-order
|
||||
(assoc :block/order class-property-order))])
|
||||
pvalue-tx-m
|
||||
(->property-value-tx-m new-block (:build/properties prop-m) properties all-idents)]
|
||||
(cond-> []
|
||||
@@ -272,17 +277,70 @@
|
||||
true
|
||||
(into additional-tx))))
|
||||
|
||||
(defn- build-properties-tx [properties page-uuids all-idents {:keys [build-existing-tx?] :as options}]
|
||||
(defn- class-properties->ordered-properties
|
||||
"Returns a deterministic property order inferred from :build/class-properties, using topological sorting"
|
||||
[classes]
|
||||
(let [class-properties (->> (vals classes)
|
||||
(map :build/class-properties)
|
||||
(filter seq))
|
||||
;; Create first-seen unique property ids for use as a stable tie-break order
|
||||
all-properties (vec (distinct (mapcat identity class-properties)))
|
||||
property-index (zipmap all-properties (range))
|
||||
sort-by-input-order #(sort-by property-index %)
|
||||
;; Adjacent pairs encode ordering e.g. [:p2 :p1 :p3]: #{[:p2 :p1] [:p1 :p3]}
|
||||
edges (->> class-properties
|
||||
(mapcat #(partition 2 1 %))
|
||||
(remove (fn [[left right]] (= left right)))
|
||||
set)
|
||||
;; Adjacency list by source node
|
||||
;; Example: #{[:p2 :p1] [:p2 :p3] [:p1 :p3]} -> {:p2 [[:p2 :p1] [:p2 :p3]], :p1 [[:p1 :p3]]}
|
||||
outgoing (group-by first edges)
|
||||
;; Count inbound edges for each property e.g. {:p2 0, :p1 1, :p3 2}
|
||||
incoming-counts (reduce (fn [m [_left right]]
|
||||
(update m right inc))
|
||||
(zipmap all-properties (repeat 0))
|
||||
edges)]
|
||||
(loop [ordered-properties []
|
||||
;; Kahn queue: nodes with zero incoming edges, stably sorted
|
||||
queue (->> all-properties
|
||||
(filter #(zero? (incoming-counts %)))
|
||||
sort-by-input-order
|
||||
vec)
|
||||
remaining-incoming incoming-counts]
|
||||
(if-let [property (first queue)]
|
||||
;; Consume one zero-incoming node, then decrement incoming counts for its neighbors
|
||||
(let [[next-incoming unlocked]
|
||||
(reduce (fn [[incoming unlocked*] [_left next-property]]
|
||||
(let [next-count (dec (incoming next-property))]
|
||||
[(assoc incoming next-property next-count)
|
||||
(if (zero? next-count) (conj unlocked* next-property) unlocked*)]))
|
||||
[remaining-incoming []]
|
||||
(get outgoing property))
|
||||
;; Merge newly unlocked nodes into queue with deterministic ordering
|
||||
next-queue (->> (concat (rest queue) unlocked)
|
||||
sort-by-input-order
|
||||
vec)]
|
||||
(recur (conj ordered-properties property) next-queue next-incoming))
|
||||
(do
|
||||
(assert (= (count ordered-properties) (count all-properties))
|
||||
(str "Cycle detected in :build/class-properties constraints. Ordered "
|
||||
(count ordered-properties) " of " (count all-properties) " properties."))
|
||||
ordered-properties)))))
|
||||
|
||||
(defn- build-properties-tx [properties classes page-uuids all-idents {:keys [build-existing-tx?] :as options}]
|
||||
(let [properties' (if build-existing-tx?
|
||||
(->> properties
|
||||
(remove (fn [[_ v]] (and (:block/uuid v) (not (:build/keep-uuid? v)))))
|
||||
(into {}))
|
||||
properties)
|
||||
class-property-orders (->> classes
|
||||
class-properties->ordered-properties
|
||||
(#(zipmap % (db-order/gen-n-keys (count %) nil nil))))
|
||||
property-db-ids (->> (keys properties')
|
||||
(map #(vector % (new-db-id)))
|
||||
(into {}))
|
||||
new-properties-tx (vec
|
||||
(mapcat (partial build-property-tx properties' page-uuids all-idents property-db-ids options)
|
||||
(mapcat (partial build-property-tx properties' page-uuids all-idents property-db-ids class-property-orders options)
|
||||
properties'))]
|
||||
new-properties-tx))
|
||||
|
||||
@@ -725,7 +783,7 @@
|
||||
page-uuids (create-page-uuids pages-and-blocks')
|
||||
{:keys [classes properties]} (if auto-create-ontology? (auto-create-ontology options) options)
|
||||
all-idents (create-all-idents properties classes options)
|
||||
properties-tx (build-properties-tx properties page-uuids all-idents options)
|
||||
properties-tx (build-properties-tx properties classes page-uuids all-idents options)
|
||||
classes-tx (build-classes-tx classes properties page-uuids all-idents options)
|
||||
class-ident->id (->> classes-tx (map (juxt :db/ident :db/id)) (into {}))
|
||||
;; Replace idents with db-ids to avoid any upsert issues
|
||||
|
||||
8
deps/db/src/logseq/db/sqlite/export.cljs
vendored
8
deps/db/src/logseq/db/sqlite/export.cljs
vendored
@@ -184,7 +184,9 @@
|
||||
(merge (select-keys class-ent [:block/created-at :block/updated-at]))
|
||||
(and (:logseq.property.class/properties class-ent) (not shallow-copy?))
|
||||
(assoc :build/class-properties
|
||||
(mapv :db/ident (:logseq.property.class/properties class-ent)))
|
||||
(->> (:logseq.property.class/properties class-ent)
|
||||
(sort-by :block/order)
|
||||
(mapv :db/ident)))
|
||||
(and (not shallow-copy?) include-alias? (:block/alias class-ent))
|
||||
(assoc :block/alias (set (map #(vector :block/uuid (:block/uuid %)) (:block/alias class-ent))))
|
||||
;; It's caller's responsibility to ensure parent is included in final export
|
||||
@@ -1138,9 +1140,7 @@
|
||||
(update :classes update-vals (fn [m]
|
||||
(cond-> m
|
||||
(:build/class-extends m)
|
||||
(update :build/class-extends sort)
|
||||
(:build/class-properties m)
|
||||
(update :build/class-properties sort))))
|
||||
(update :build/class-extends sort))))
|
||||
(update :properties update-vals (fn [m]
|
||||
(cond-> m
|
||||
(:build/property-classes m)
|
||||
|
||||
30
deps/db/test/logseq/db/sqlite/build_test.cljs
vendored
30
deps/db/test/logseq/db/sqlite/build_test.cljs
vendored
@@ -6,6 +6,7 @@
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.sqlite.build :as sqlite-build]
|
||||
[logseq.db.sqlite.export :as sqlite-export]
|
||||
[logseq.db.test.helper :as db-test]))
|
||||
|
||||
(deftest build-tags
|
||||
@@ -273,4 +274,31 @@
|
||||
(is (entity-util/property? (d/entity @conn :user.property/p1)))
|
||||
(is (entity-util/property? (d/entity @conn :other.property/p1)))
|
||||
(is (entity-util/class? (d/entity @conn :user.class/C1)))
|
||||
(is (entity-util/class? (d/entity @conn :other.class/C1)))))
|
||||
(is (entity-util/class? (d/entity @conn :other.class/C1)))))
|
||||
|
||||
(deftest build-preserves-class-property-ordering-for-export
|
||||
(let [class-properties-c1 [:user.property/p2 :user.property/p1 :user.property/p3]
|
||||
class-properties-c2 [:user.property/p4 :user.property/p2 :user.property/p3]
|
||||
another-class-properties-c1 [:user.property/p5]
|
||||
another-class-properties-c2 [:user.property/p6]
|
||||
another-class-properties-c3 [:user.property/p6 :user.property/p5]
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/p1 {:logseq.property/type :default}
|
||||
:user.property/p2 {:logseq.property/type :default}
|
||||
:user.property/p3 {:logseq.property/type :default}
|
||||
:user.property/p4 {:logseq.property/type :default}
|
||||
:user.property/p5 {:logseq.property/type :default}
|
||||
:user.property/p6 {:logseq.property/type :default}}
|
||||
:classes {:user.class/C1 {:build/class-properties class-properties-c1}
|
||||
:user.class/C2 {:build/class-properties class-properties-c2}
|
||||
:user.class/AnotherC1 {:build/class-properties another-class-properties-c1}
|
||||
:user.class/AnotherC2 {:build/class-properties another-class-properties-c2}
|
||||
:user.class/AnotherC3 {:build/class-properties another-class-properties-c3}}})
|
||||
export-map (sqlite-export/build-export @conn {:export-type :graph-ontology})]
|
||||
(is (= class-properties-c1
|
||||
(get-in export-map [:classes :user.class/C1 :build/class-properties])))
|
||||
(is (= class-properties-c2
|
||||
(get-in export-map [:classes :user.class/C2 :build/class-properties])))
|
||||
(is (= another-class-properties-c3
|
||||
(get-in export-map [:classes :user.class/AnotherC3 :build/class-properties]))
|
||||
"Later class-level ordering constraint :p6 before :p5 is preserved")))
|
||||
Reference in New Issue
Block a user