replay_sync_sqlite.cljs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. (ns replay-sync-sqlite
  2. "Replay db-sync rebase/apply flow directly from sqlite artifacts.
  3. It replays:
  4. 1) server tx_log baseline up to before remote window,
  5. 2) local client ops as applied local changes,
  6. 3) reverse local ops,
  7. 4) apply remote txs,
  8. 5) reapply local ops.
  9. Supports `legacy` vs `fixed` reapply fallback logic to verify behavior."
  10. (:require ["better-sqlite3" :as sqlite3]
  11. ["fs" :as fs]
  12. ["path" :as node-path]
  13. [babashka.cli :as cli]
  14. [clojure.string :as string]
  15. [datascript.core :as d]
  16. [logseq.db :as ldb]
  17. [logseq.db.common.sqlite-cli :as sqlite-cli]
  18. [logseq.db.frontend.property.type :as db-property-type]
  19. [logseq.db.frontend.schema :as db-schema]
  20. [logseq.outliner.core :as outliner-core]
  21. [logseq.outliner.op :as outliner-op]
  22. [logseq.outliner.page :as outliner-page]
  23. [logseq.outliner.property :as outliner-property]
  24. [logseq.outliner.recycle :as outliner-recycle]
  25. [nbb.core :as nbb]))
  26. (def sqlite (if (find-ns 'nbb.core) (aget sqlite3 "default") sqlite3))
  27. (def cli-spec
  28. {:help {:alias :h
  29. :desc "Show help"}
  30. :server-db {:alias :s
  31. :desc "Path to server graph db.sqlite containing tx_log"
  32. :coerce :string}
  33. :client-ops-db {:alias :c
  34. :desc "Path to client ops sqlite"
  35. :coerce :string}
  36. :from-t {:alias :f
  37. :desc "Replay remote txs from this t (inclusive). If omitted, use --auto-from-conflict"
  38. :coerce :long}
  39. :to-t {:alias :t
  40. :desc "Replay remote txs to this t (inclusive). Default: all after from-t"
  41. :coerce :long}
  42. :remote-limit {:alias :l
  43. :desc "Cap remote tx count after from-t"
  44. :coerce :long}
  45. :mode {:alias :m
  46. :desc "fixed | legacy | both (default both)"
  47. :coerce :string}
  48. :pending-only {:desc "Only include rows where :db-sync/pending? true"}
  49. :local-created-at-min {:desc "Filter local ops by created-at >= value"
  50. :coerce :long}
  51. :local-created-at-max {:desc "Filter local ops by created-at <= value"
  52. :coerce :long}
  53. :local-tx-id {:desc "Repeatable tx-id UUID filter"
  54. :coerce []}
  55. :auto-from-conflict {:desc "Infer from-t from first remote tx referencing uuids deleted by selected local ops"}
  56. :inspect-only {:desc "Only print inferred conflict info without replay"}
  57. :pretty {:desc "Pretty-print JSON output"}})
  58. (def local-op-keys
  59. [:db/id
  60. :db-sync/tx-id
  61. :db-sync/created-at
  62. :db-sync/pending?
  63. :db-sync/failed?
  64. :db-sync/outliner-op
  65. :db-sync/forward-outliner-ops
  66. :db-sync/inverse-outliner-ops
  67. :db-sync/inferred-outliner-ops?
  68. :db-sync/normalized-tx-data
  69. :db-sync/reversed-tx-data])
  70. (defn usage []
  71. (str "Usage:\n"
  72. " yarn -s nbb-logseq -cp src:../outliner/src:../common/src:../graph-parser/src script/replay_sync_sqlite.cljs \\\n"
  73. " --server-db <server-db.sqlite> --client-ops-db <client-ops.sqlite> [--from-t <n>] [--to-t <n>] [--mode both] [--pretty]\n"
  74. "\n"
  75. "Examples:\n"
  76. " yarn -s nbb-logseq -cp src:../outliner/src:../common/src:../graph-parser/src script/replay_sync_sqlite.cljs \\\n"
  77. " --server-db /path/server/db.sqlite --client-ops-db /path/electron_client_ops.sqlite \\\n"
  78. " --auto-from-conflict --local-created-at-max 1775717890000 --mode both --pretty\n"
  79. "\n"
  80. "Options:\n"
  81. (cli/format-opts {:spec cli-spec})))
  82. (defn resolve-path
  83. [path*]
  84. (if (node-path/isAbsolute path*)
  85. path*
  86. (node-path/join (or js/process.env.ORIGINAL_PWD ".") path*)))
  87. (defn parse-uuid
  88. [value]
  89. (cond
  90. (uuid? value)
  91. value
  92. (string? value)
  93. (try
  94. (uuid value)
  95. (catch :default _
  96. nil))
  97. :else
  98. nil))
  99. (defn uuid-ref
  100. [value]
  101. (cond
  102. (uuid? value)
  103. value
  104. (string? value)
  105. (parse-uuid value)
  106. (and (vector? value)
  107. (= :block/uuid (first value)))
  108. (parse-uuid (second value))
  109. (map? value)
  110. (some-> (:block/uuid value) parse-uuid)
  111. :else
  112. nil))
  113. (defn collect-uuids
  114. [value]
  115. (cond
  116. (nil? value)
  117. []
  118. (uuid? value)
  119. [value]
  120. (string? value)
  121. (if-let [u (parse-uuid value)] [u] [])
  122. (vector? value)
  123. (if (and (= :block/uuid (first value))
  124. (some? (second value)))
  125. (if-let [u (parse-uuid (second value))]
  126. [u]
  127. [])
  128. (mapcat collect-uuids value))
  129. (set? value)
  130. (mapcat collect-uuids value)
  131. (sequential? value)
  132. (mapcat collect-uuids value)
  133. (map? value)
  134. (mapcat collect-uuids (vals value))
  135. :else
  136. []))
  137. (defn read-server-tx-log
  138. [server-db-path]
  139. (let [db (new sqlite server-db-path nil)]
  140. (try
  141. (->> (.all (.prepare db "select t, tx, outliner_op from tx_log order by t asc"))
  142. (mapv (fn [row]
  143. (let [t (aget row "t")
  144. tx-str (aget row "tx")
  145. outliner-op (aget row "outliner_op")]
  146. {:t t
  147. :outliner-op (when (string? outliner-op)
  148. (keyword outliner-op))
  149. :tx-data (ldb/read-transit-str tx-str)}))))
  150. (finally
  151. (.close db)))))
  152. (defn entity->local-op
  153. [db eid]
  154. (let [ent (d/entity db eid)
  155. m (into {} ent)]
  156. {:db/id (:db/id ent)
  157. :tx-id (:db-sync/tx-id ent)
  158. :created-at (:db-sync/created-at ent)
  159. :pending? (:db-sync/pending? ent)
  160. :failed? (:db-sync/failed? ent)
  161. :outliner-op (:db-sync/outliner-op ent)
  162. :forward-outliner-ops (:db-sync/forward-outliner-ops ent)
  163. :inverse-outliner-ops (:db-sync/inverse-outliner-ops ent)
  164. :inferred-outliner-ops? (:db-sync/inferred-outliner-ops? ent)
  165. :tx (:db-sync/normalized-tx-data ent)
  166. :reversed-tx (:db-sync/reversed-tx-data ent)
  167. :raw (select-keys m local-op-keys)}))
  168. (defn read-client-ops
  169. [client-ops-db-path]
  170. (let [{:keys [conn sqlite]} (sqlite-cli/open-sqlite-datascript! client-ops-db-path)]
  171. (try
  172. (let [db @conn]
  173. (->> (d/q '[:find ?e ?created-at
  174. :where
  175. [?e :db-sync/tx-id _]
  176. [?e :db-sync/created-at ?created-at]]
  177. db)
  178. (sort-by (fn [[e created-at]] [created-at e]))
  179. (mapv (fn [[e _]]
  180. (entity->local-op db e)))))
  181. (finally
  182. (when sqlite
  183. (.close sqlite))))))
  184. (defn parse-tx-id-set
  185. [tx-id-values]
  186. (let [values (if (sequential? tx-id-values) tx-id-values [])]
  187. (->> values
  188. (mapcat (fn [v]
  189. (if (string? v)
  190. (remove string/blank? (string/split v #","))
  191. [])))
  192. (map parse-uuid)
  193. (remove nil?)
  194. set)))
  195. (defn filter-local-ops
  196. [ops {:keys [pending-only local-created-at-min local-created-at-max local-tx-id]}]
  197. (let [tx-id-set (parse-tx-id-set local-tx-id)]
  198. (->> ops
  199. (filter (fn [op]
  200. (and
  201. (if pending-only
  202. (true? (:pending? op))
  203. true)
  204. (if (some? local-created-at-min)
  205. (>= (or (:created-at op) -1) local-created-at-min)
  206. true)
  207. (if (some? local-created-at-max)
  208. (<= (or (:created-at op) js/Number.MAX_SAFE_INTEGER) local-created-at-max)
  209. true)
  210. (if (seq tx-id-set)
  211. (contains? tx-id-set (:tx-id op))
  212. true))))
  213. vec)))
  214. (defn delete-op-uuids
  215. [local-ops]
  216. (->> local-ops
  217. (mapcat :forward-outliner-ops)
  218. (filter (fn [op] (= :delete-blocks (first op))))
  219. (mapcat (fn [[_ args]]
  220. (let [[ids] args]
  221. (if (sequential? ids) ids []))))
  222. (map uuid-ref)
  223. (remove nil?)
  224. set))
  225. (defn row-referenced-uuids
  226. [row]
  227. (->> (:tx-data row)
  228. (mapcat collect-uuids)
  229. set))
  230. (defn infer-conflicts
  231. [server-rows local-ops]
  232. (let [deleted-uuids (delete-op-uuids local-ops)
  233. conflicts (when (seq deleted-uuids)
  234. (->> server-rows
  235. (keep (fn [row]
  236. (let [refs (row-referenced-uuids row)
  237. hit (seq (clojure.set/intersection deleted-uuids refs))]
  238. (when hit
  239. {:t (:t row)
  240. :outliner-op (:outliner-op row)
  241. :hits (vec (take 10 hit))}))))
  242. vec))]
  243. {:deleted-uuids (vec deleted-uuids)
  244. :conflicts conflicts
  245. :first-conflict-t (some-> conflicts first :t)}))
  246. (defn select-remote-rows
  247. [server-rows from-t to-t remote-limit]
  248. (let [rows (->> server-rows
  249. (filter (fn [{:keys [t]}]
  250. (and (if (some? from-t) (>= t from-t) true)
  251. (if (some? to-t) (<= t to-t) true))))
  252. vec)]
  253. (if (some? remote-limit)
  254. (vec (take remote-limit rows))
  255. rows)))
  256. (defn baseline-rows
  257. [server-rows from-t]
  258. (->> server-rows
  259. (filter (fn [{:keys [t]}]
  260. (if (some? from-t) (< t from-t) true)))
  261. vec))
  262. (defn usable-history-ops
  263. [ops]
  264. ops)
  265. (defn entity-id->block-uuid
  266. [db id]
  267. (or (uuid-ref id)
  268. (some-> (d/entity db id) :block/uuid)))
  269. (defn delete-block-root-uuids
  270. [db forward-outliner-ops]
  271. (->> (usable-history-ops forward-outliner-ops)
  272. (mapcat (fn [[op args]]
  273. (if (= :delete-blocks op)
  274. (let [[ids] args]
  275. (if (sequential? ids)
  276. (keep #(entity-id->block-uuid db %) ids)
  277. []))
  278. [])))
  279. distinct
  280. vec))
  281. (defn raw-restored-block-uuids
  282. [raw-tx-data]
  283. (->> raw-tx-data
  284. (keep (fn [datom]
  285. (when (and (vector? datom)
  286. (>= (count datom) 4)
  287. (= :db/add (nth datom 0))
  288. (= :block/uuid (nth datom 2)))
  289. (uuid-ref (nth datom 3)))))
  290. distinct
  291. vec))
  292. (defn assert-delete-block-roots-restored!
  293. [conn local-tx]
  294. (let [root-uuids (delete-block-root-uuids @conn (:forward-outliner-ops local-tx))]
  295. (when (seq root-uuids)
  296. (let [missing-root-uuids (->> root-uuids
  297. (remove #(d/entity @conn [:block/uuid %]))
  298. vec)]
  299. (when (seq missing-root-uuids)
  300. (throw (ex-info "incomplete delete-blocks reverse"
  301. {:error :db-sync/incomplete-delete-blocks-reverse
  302. :tx-id (:tx-id local-tx)
  303. :outliner-op (:outliner-op local-tx)
  304. :root-uuids root-uuids
  305. :missing-root-uuids missing-root-uuids})))))))
  306. (defn assert-raw-restored-block-uuids!
  307. [conn local-tx raw-tx-data]
  308. (let [restored-uuids (raw-restored-block-uuids raw-tx-data)]
  309. (when (seq restored-uuids)
  310. (let [missing-restored-uuids (->> restored-uuids
  311. (remove #(d/entity @conn [:block/uuid %]))
  312. vec)]
  313. (when (seq missing-restored-uuids)
  314. (throw (ex-info "incomplete raw restored uuids"
  315. {:error :db-sync/incomplete-raw-restored-uuids
  316. :tx-id (:tx-id local-tx)
  317. :outliner-op (:outliner-op local-tx)
  318. :restored-uuids restored-uuids
  319. :missing-restored-uuids missing-restored-uuids})))))))
  320. (defn invalid-rebase-op!
  321. [op data]
  322. (throw (ex-info "invalid rebase op" (assoc data :op op))))
  323. (defn replay-entity-id-value
  324. [db v]
  325. (cond
  326. (number? v)
  327. v
  328. (uuid? v)
  329. (some-> (d/entity db [:block/uuid v]) :db/id)
  330. (or (vector? v) (qualified-keyword? v))
  331. (some-> (d/entity db v) :db/id)
  332. :else
  333. v))
  334. (defn stable-entity-ref-like?
  335. [v]
  336. (or (qualified-keyword? v)
  337. (and (vector? v)
  338. (or (= :block/uuid (first v))
  339. (= :db/ident (first v))))))
  340. (defn replay-property-value
  341. [db property-id v]
  342. (let [property-type (some-> (d/entity db property-id) :logseq.property/type)]
  343. (if (contains? db-property-type/all-ref-property-types property-type)
  344. (cond
  345. (stable-entity-ref-like? v)
  346. (replay-entity-id-value db v)
  347. (set? v)
  348. (->> v
  349. (map #(if (stable-entity-ref-like? %)
  350. (replay-entity-id-value db %)
  351. %))
  352. set)
  353. (sequential? v)
  354. (mapv #(if (stable-entity-ref-like? %)
  355. (replay-entity-id-value db %)
  356. %)
  357. v)
  358. :else
  359. v)
  360. v)))
  361. (defn replay-entity-id-coll
  362. [db ids]
  363. (mapv #(or (replay-entity-id-value db %) %) ids))
  364. (defn rebase-find-existing-left-sibling
  365. [current-db target]
  366. (loop [sibling (ldb/get-left-sibling target)]
  367. (if (nil? sibling)
  368. nil
  369. (if-let [current-sibling (and sibling (d/entity current-db [:block/uuid (:block/uuid sibling)]))]
  370. current-sibling
  371. (recur (ldb/get-left-sibling sibling))))))
  372. (defn rebase-resolve-target-and-sibling
  373. [current-db rebase-db-before target-id sibling?]
  374. (let [target (d/entity current-db target-id)
  375. target-before (when rebase-db-before
  376. (d/entity rebase-db-before target-id))
  377. parent-before (when rebase-db-before
  378. (:block/parent (d/entity rebase-db-before target-id)))]
  379. (cond
  380. target
  381. [target sibling?]
  382. (and target-before parent-before sibling?)
  383. (if-let [left-sibling (rebase-find-existing-left-sibling current-db target-before)]
  384. [left-sibling true]
  385. (when-let [parent (d/entity current-db [:block/uuid (:block/uuid parent-before)])]
  386. [parent false]))
  387. :else
  388. nil)))
  389. (defn replay-canonical-outliner-op!
  390. [conn [op args] rebase-db-before]
  391. (case op
  392. :save-block
  393. (let [[block opts] args
  394. db @conn
  395. block-uuid (:block/uuid block)
  396. block-ent (when block-uuid
  397. (d/entity db [:block/uuid block-uuid]))
  398. block-base (dissoc block :db/id :block/order)
  399. block' block-base]
  400. (when (nil? block-ent)
  401. (invalid-rebase-op! op {:args args
  402. :reason :missing-block}))
  403. (outliner-core/save-block! conn block' opts))
  404. :insert-blocks
  405. (let [[blocks target-id opts] args
  406. db @conn
  407. [target sibling?] (rebase-resolve-target-and-sibling db rebase-db-before target-id (:sibling? opts))]
  408. (when-not (and target (seq blocks))
  409. (invalid-rebase-op! op {:args args}))
  410. (outliner-core/insert-blocks! conn blocks target (assoc opts :sibling? sibling?)))
  411. :apply-template
  412. (let [[template-id target-id opts] args
  413. template-id' (replay-entity-id-value @conn template-id)
  414. target-id' (replay-entity-id-value @conn target-id)
  415. [target sibling?] (rebase-resolve-target-and-sibling @conn rebase-db-before target-id' (:sibling? opts))]
  416. (when-not (and template-id' (d/entity @conn template-id') target)
  417. (invalid-rebase-op! op {:args args
  418. :reason :missing-template-or-target-block}))
  419. (outliner-op/apply-ops!
  420. conn
  421. [[:apply-template [template-id'
  422. target-id'
  423. (assoc opts :sibling? sibling?)]]]
  424. {:gen-undo-ops? false}))
  425. :move-blocks
  426. (let [[ids target-id opts] args
  427. ids' (replay-entity-id-coll @conn ids)
  428. target-id' (replay-entity-id-value @conn target-id)
  429. blocks (keep #(d/entity @conn %) ids')
  430. [target sibling?] (rebase-resolve-target-and-sibling @conn rebase-db-before target-id' (:sibling? opts))]
  431. (when (or (empty? blocks) (nil? target))
  432. (invalid-rebase-op! op {:args args}))
  433. (when (seq blocks)
  434. (outliner-core/move-blocks! conn blocks target (assoc opts :sibling? sibling?))))
  435. :move-blocks-up-down
  436. (let [[ids up?] args
  437. ids' (replay-entity-id-coll @conn ids)
  438. blocks (keep #(d/entity @conn %) ids')]
  439. (when (seq blocks)
  440. (outliner-core/move-blocks-up-down! conn blocks up?)))
  441. :indent-outdent-blocks
  442. (let [[ids indent? opts] args
  443. ids' (replay-entity-id-coll @conn ids)
  444. blocks (keep #(d/entity @conn %) ids')]
  445. (when (empty? blocks)
  446. (invalid-rebase-op! op {:args args}))
  447. (when (seq blocks)
  448. (outliner-core/indent-outdent-blocks! conn blocks indent? opts)))
  449. :delete-blocks
  450. (let [[ids opts] args
  451. ids' (replay-entity-id-coll @conn ids)
  452. blocks (keep #(d/entity @conn %) ids')]
  453. (when (seq blocks)
  454. (outliner-core/delete-blocks! conn blocks opts)))
  455. :create-page
  456. (let [[title opts] args]
  457. (outliner-page/create! conn title opts))
  458. :delete-page
  459. (let [[page-uuid opts] args]
  460. (outliner-page/delete! conn page-uuid opts))
  461. :restore-recycled
  462. (let [[root-id] args
  463. root-ref (cond
  464. (and (vector? root-id)
  465. (= :block/uuid (first root-id)))
  466. root-id
  467. (uuid? root-id)
  468. [:block/uuid root-id]
  469. :else
  470. root-id)
  471. root (d/entity @conn root-ref)
  472. tx-data (when root
  473. (seq (outliner-recycle/restore-tx-data @conn root)))]
  474. (when-not tx-data
  475. (invalid-rebase-op! op {:args args
  476. :reason :invalid-restore-target}))
  477. (ldb/transact! conn tx-data
  478. {:outliner-op :restore-recycled}))
  479. :recycle-delete-permanently
  480. (let [[root-id] args
  481. root-ref (cond
  482. (and (vector? root-id)
  483. (= :block/uuid (first root-id)))
  484. root-id
  485. (uuid? root-id)
  486. [:block/uuid root-id]
  487. :else
  488. root-id)
  489. root (d/entity @conn root-ref)
  490. tx-data (when root
  491. (seq (outliner-recycle/permanently-delete-tx-data @conn root)))]
  492. (when (seq tx-data)
  493. (ldb/transact! conn tx-data
  494. {:outliner-op :recycle-delete-permanently})))
  495. :set-block-property
  496. (let [[block-eid property-id v] args
  497. block-eid' (or (replay-entity-id-value @conn block-eid)
  498. block-eid)
  499. block (d/entity @conn block-eid')
  500. property (d/entity @conn property-id)
  501. _ (when-not (and block property)
  502. (invalid-rebase-op! op {:args args
  503. :reason :missing-block-or-property}))
  504. v' (replay-property-value @conn property-id v)]
  505. (when (and (stable-entity-ref-like? v) (nil? v'))
  506. (invalid-rebase-op! op {:args args}))
  507. (outliner-property/set-block-property! conn block-eid' property-id v'))
  508. :remove-block-property
  509. (apply outliner-property/remove-block-property! conn args)
  510. :batch-set-property
  511. (let [[block-ids property-id v opts] args
  512. block-ids' (replay-entity-id-coll @conn block-ids)
  513. property (d/entity @conn property-id)
  514. _ (when-not (and property
  515. (seq block-ids')
  516. (every? #(some? (d/entity @conn %)) block-ids'))
  517. (invalid-rebase-op! op {:args args
  518. :reason :missing-block-or-property}))
  519. v' (replay-property-value @conn property-id v)]
  520. (when (and (stable-entity-ref-like? v) (nil? v'))
  521. (invalid-rebase-op! op {:args args}))
  522. (outliner-property/batch-set-property! conn block-ids' property-id v' opts))
  523. :batch-remove-property
  524. (let [[block-ids property-id] args
  525. block-ids' (replay-entity-id-coll @conn block-ids)]
  526. (outliner-property/batch-remove-property! conn block-ids' property-id))
  527. :delete-property-value
  528. (let [[block-eid property-id property-value] args
  529. block (d/entity @conn block-eid)
  530. property (d/entity @conn property-id)
  531. _ (when-not (and block property)
  532. (invalid-rebase-op! op {:args args
  533. :reason :missing-block-or-property}))
  534. property-value' (replay-property-value @conn property-id property-value)]
  535. (when (and (stable-entity-ref-like? property-value) (nil? property-value'))
  536. (invalid-rebase-op! op {:args args}))
  537. (outliner-property/delete-property-value! conn block-eid property-id property-value'))
  538. :batch-delete-property-value
  539. (let [[block-eids property-id property-value] args
  540. block-eids' (replay-entity-id-coll @conn block-eids)
  541. property (d/entity @conn property-id)
  542. _ (when-not (and property
  543. (seq block-eids')
  544. (every? #(some? (d/entity @conn %)) block-eids'))
  545. (invalid-rebase-op! op {:args args
  546. :reason :missing-block-or-property}))
  547. property-value' (replay-property-value @conn property-id property-value)]
  548. (when (and (stable-entity-ref-like? property-value) (nil? property-value'))
  549. (invalid-rebase-op! op {:args args}))
  550. (outliner-property/batch-delete-property-value! conn block-eids' property-id property-value'))
  551. :create-property-text-block
  552. (apply outliner-property/create-property-text-block! conn args)
  553. :upsert-property
  554. (apply outliner-property/upsert-property! conn args)
  555. :class-add-property
  556. (apply outliner-property/class-add-property! conn args)
  557. :class-remove-property
  558. (apply outliner-property/class-remove-property! conn args)
  559. :upsert-closed-value
  560. (apply outliner-property/upsert-closed-value! conn args)
  561. :add-existing-values-to-closed-values
  562. (apply outliner-property/add-existing-values-to-closed-values! conn args)
  563. :delete-closed-value
  564. (apply outliner-property/delete-closed-value! conn args)
  565. (let [tx-data (:tx args)]
  566. (when-let [tx-data (seq tx-data)]
  567. (ldb/transact! conn tx-data {:outliner-op :transact})))))
  568. (defn replace-uuid-str-with-eid
  569. [db v]
  570. (if-let [u (and (string? v) (parse-uuid v))]
  571. (if-let [entity (d/entity db [:block/uuid u])]
  572. (:db/id entity)
  573. v)
  574. v))
  575. (defn resolve-temp-id
  576. [db datom-v]
  577. (if (and (vector? datom-v)
  578. (= (count datom-v) 5)
  579. (= (first datom-v) :db/add))
  580. (let [[op e a v t] datom-v
  581. e' (replace-uuid-str-with-eid db e)
  582. v' (replace-uuid-str-with-eid db v)]
  583. [op e' a v' t])
  584. datom-v))
  585. (defn reverse-local-tx!
  586. [conn local-tx]
  587. (let [preserve-during-rebase? (or (= :create-page (:outliner-op local-tx))
  588. (and (= 1 (count (:forward-outliner-ops local-tx)))
  589. (= :create-page (ffirst (:forward-outliner-ops local-tx)))))
  590. inferred-history? (true? (:inferred-outliner-ops? local-tx))
  591. inverse-ops (usable-history-ops (:inverse-outliner-ops local-tx))
  592. raw-tx-data (seq (:reversed-tx local-tx))]
  593. (cond
  594. preserve-during-rebase?
  595. {:tx-id (:tx-id local-tx)
  596. :status :preserved}
  597. (and inferred-history? raw-tx-data)
  598. (try
  599. (ldb/transact! conn raw-tx-data
  600. {:outliner-op (:outliner-op local-tx)
  601. :reverse? true})
  602. (catch :default error
  603. (if (seq inverse-ops)
  604. (do
  605. (doseq [op inverse-ops]
  606. (replay-canonical-outliner-op! conn op nil))
  607. (assert-delete-block-roots-restored! conn local-tx)
  608. (assert-raw-restored-block-uuids! conn local-tx raw-tx-data))
  609. (throw error))))
  610. (seq inverse-ops)
  611. (try
  612. (doseq [op inverse-ops]
  613. (replay-canonical-outliner-op! conn op nil))
  614. (assert-delete-block-roots-restored! conn local-tx)
  615. (assert-raw-restored-block-uuids! conn local-tx raw-tx-data)
  616. (catch :default error
  617. (if raw-tx-data
  618. (ldb/transact! conn raw-tx-data
  619. {:outliner-op (:outliner-op local-tx)
  620. :reverse? true})
  621. (throw error))))
  622. raw-tx-data
  623. (ldb/transact! conn raw-tx-data
  624. {:outliner-op (:outliner-op local-tx)
  625. :reverse? true})
  626. :else
  627. (invalid-rebase-op! :reverse-history-action
  628. {:reason :missing-reversed-tx-data
  629. :tx-id (:tx-id local-tx)
  630. :outliner-op (:outliner-op local-tx)}))))
  631. (defn rebase-history-ops
  632. [_mode local-tx]
  633. (let [forward-outliner-ops (seq (:forward-outliner-ops local-tx))
  634. inverse-outliner-ops (seq (:inverse-outliner-ops local-tx))
  635. forward-ops forward-outliner-ops
  636. inverse-ops inverse-outliner-ops]
  637. {:forward-ops forward-ops
  638. :inverse-ops inverse-ops
  639. :fallback? false}))
  640. (defn transact-remote-txs!
  641. [conn remote-rows]
  642. (loop [remaining remote-rows
  643. idx 0]
  644. (let [db @conn]
  645. (if-let [row (first remaining)]
  646. (let [tx-data (->> (:tx-data row)
  647. (map (partial resolve-temp-id db))
  648. seq)
  649. pre-missing-entity-id (when-let [entity-id (some-> tx-data first second)]
  650. (when (and (vector? entity-id)
  651. (every? (fn [datom]
  652. (= entity-id (second datom)))
  653. tx-data)
  654. (nil? (d/entity db entity-id)))
  655. entity-id))]
  656. (if pre-missing-entity-id
  657. nil
  658. (try
  659. (when tx-data
  660. (ldb/transact! conn tx-data {:transact-remote? true}))
  661. (catch :default e
  662. (let [error-data (or (ex-data e) {})
  663. missing-entity-id (:entity-id error-data)
  664. missing-entity-only-tx? (and (= :entity-id/missing (:error error-data))
  665. (vector? missing-entity-id)
  666. (seq tx-data)
  667. (every? (fn [datom]
  668. (= missing-entity-id (second datom)))
  669. tx-data))]
  670. (when-not missing-entity-only-tx?
  671. (throw (ex-info "remote transact failed"
  672. {:stage :remote
  673. :index idx
  674. :t (:t row)
  675. :outliner-op (:outliner-op row)}
  676. e)))))))
  677. (recur (next remaining) (inc idx)))
  678. nil))))
  679. (defn seed-local-txs!
  680. [conn local-ops]
  681. (doseq [local local-ops]
  682. (let [forward-ops (usable-history-ops (:forward-outliner-ops local))
  683. raw-tx (seq (:tx local))]
  684. (try
  685. (cond
  686. (seq forward-ops)
  687. (doseq [op forward-ops]
  688. (replay-canonical-outliner-op! conn op nil))
  689. raw-tx
  690. (ldb/transact! conn raw-tx {:seed-local? true
  691. :outliner-op (:outliner-op local)})
  692. :else
  693. nil)
  694. (catch :default e
  695. (throw (ex-info "seed local tx failed"
  696. {:stage :seed-local
  697. :tx-id (:tx-id local)
  698. :outliner-op (:outliner-op local)
  699. :seed-source (cond
  700. (seq forward-ops) :semantic
  701. raw-tx :raw
  702. :else :none)}
  703. e)))))))
  704. (defn reapply-local-tx!
  705. [mode conn local-tx rebase-db-before preserved-tx-ids]
  706. (if (contains? preserved-tx-ids (:tx-id local-tx))
  707. {:tx-id (:tx-id local-tx)
  708. :status :preserved
  709. :fallback? false
  710. :forward-op-count 0
  711. :inverse-op-count 0}
  712. (let [{:keys [forward-ops inverse-ops fallback?]} (rebase-history-ops mode local-tx)]
  713. (if (seq forward-ops)
  714. (try
  715. (doseq [op forward-ops]
  716. (replay-canonical-outliner-op! conn op rebase-db-before))
  717. {:tx-id (:tx-id local-tx)
  718. :status :rebased
  719. :fallback? fallback?
  720. :inverse-op-count (count inverse-ops)
  721. :forward-op-count (count forward-ops)}
  722. (catch :default error
  723. {:tx-id (:tx-id local-tx)
  724. :status :failed
  725. :fallback? fallback?
  726. :error (ex-message error)
  727. :error-data (select-keys (or (ex-data error) {})
  728. [:op :reason :error :entity-id])}))
  729. {:tx-id (:tx-id local-tx)
  730. :status :skipped
  731. :fallback? false
  732. :forward-op-count 0
  733. :inverse-op-count (count inverse-ops)}))))
  734. (defn replay-mode!
  735. [{:keys [mode baseline remote local]}]
  736. (let [conn (d/create-conn db-schema/schema)
  737. *stage (atom :baseline)
  738. *current (atom nil)]
  739. (try
  740. ;; Replay can pass through transient intermediate states; disable strict
  741. ;; validation so we can observe end-to-end conflict handling behavior.
  742. (swap! conn assoc :skip-validate-db? true)
  743. (doseq [row baseline]
  744. (when-let [tx-data (seq (:tx-data row))]
  745. (reset! *current {:t (:t row)})
  746. (ldb/transact! conn tx-data {:server-baseline? true :t (:t row)})))
  747. (reset! *stage :seed-local)
  748. (seed-local-txs! conn local)
  749. (let [rebase-db-before @conn]
  750. (reset! *stage :reverse-local)
  751. (let [reverse-results (mapv (fn [local-tx]
  752. (reset! *current {:tx-id (:tx-id local-tx)})
  753. (reverse-local-tx! conn local-tx))
  754. (reverse local))
  755. preserved-tx-ids (->> reverse-results
  756. (filter #(= :preserved (:status %)))
  757. (keep :tx-id)
  758. set)]
  759. (reset! *stage :remote)
  760. (transact-remote-txs! conn remote)
  761. (reset! *stage :reapply-local)
  762. (let [reapply-results (mapv (fn [local-tx]
  763. (reset! *current {:tx-id (:tx-id local-tx)})
  764. (reapply-local-tx! mode conn local-tx rebase-db-before preserved-tx-ids))
  765. local)]
  766. {:mode (name mode)
  767. :ok? true
  768. :baseline-count (count baseline)
  769. :remote-count (count remote)
  770. :local-count (count local)
  771. :reapply-results reapply-results
  772. :reapply-failed-count (count (filter #(= :failed (:status %)) reapply-results))
  773. :final-datom-count (count (d/datoms @conn :eavt))})))
  774. (catch :default e
  775. {:mode (name mode)
  776. :ok? false
  777. :stage @*stage
  778. :current @*current
  779. :error (ex-message e)
  780. :error-data (select-keys (or (ex-data e) {})
  781. [:stage :t :index :tx-id :op :reason :error :entity-id])}))))
  782. (defn mode-seq
  783. [mode-option]
  784. (case (some-> mode-option string/lower-case)
  785. "legacy" [:legacy]
  786. "fixed" [:fixed]
  787. "both" [:legacy :fixed]
  788. nil [:legacy :fixed]
  789. [:legacy :fixed]))
  790. (defn ensure-exists!
  791. [label path]
  792. (when-not (.existsSync fs path)
  793. (binding [*print-fn* *print-err-fn*]
  794. (println (str label " does not exist: " path)))
  795. (js/process.exit 1)))
  796. (defn -main
  797. [argv]
  798. (let [{:keys [opts]} (cli/parse-args argv {:spec cli-spec})
  799. {:keys [server-db client-ops-db from-t to-t remote-limit
  800. auto-from-conflict inspect-only pretty] :as opts} opts]
  801. (when (:help opts)
  802. (println (usage))
  803. (js/process.exit 0))
  804. (when (or (string/blank? server-db) (string/blank? client-ops-db))
  805. (binding [*print-fn* *print-err-fn*]
  806. (println "Missing required --server-db and/or --client-ops-db"))
  807. (println (usage))
  808. (js/process.exit 1))
  809. (let [server-db' (resolve-path server-db)
  810. client-ops-db' (resolve-path client-ops-db)]
  811. (ensure-exists! "server-db" server-db')
  812. (ensure-exists! "client-ops-db" client-ops-db')
  813. (let [server-rows (read-server-tx-log server-db')
  814. all-local-ops (read-client-ops client-ops-db')
  815. local-ops (filter-local-ops all-local-ops opts)
  816. conflicts (infer-conflicts server-rows local-ops)
  817. inferred-from-t (when auto-from-conflict
  818. (:first-conflict-t conflicts))
  819. effective-from-t (or from-t inferred-from-t)]
  820. (when (and (not inspect-only) (nil? effective-from-t))
  821. (binding [*print-fn* *print-err-fn*]
  822. (println "Missing --from-t and failed to infer via --auto-from-conflict"))
  823. (js/process.exit 1))
  824. (if inspect-only
  825. (let [payload {:server-db server-db'
  826. :client-ops-db client-ops-db'
  827. :server-tx-count (count server-rows)
  828. :local-op-count (count local-ops)
  829. :deleted-uuids (mapv str (:deleted-uuids conflicts))
  830. :first-conflict-t (:first-conflict-t conflicts)
  831. :conflicts (mapv (fn [c]
  832. (update c :hits #(mapv str %)))
  833. (or (:conflicts conflicts) []))}]
  834. (if pretty
  835. (println (js/JSON.stringify (clj->js payload) nil 2))
  836. (println (js/JSON.stringify (clj->js payload)))))
  837. (let [remote (select-remote-rows server-rows effective-from-t to-t remote-limit)
  838. baseline (baseline-rows server-rows effective-from-t)
  839. results (mapv (fn [mode]
  840. (replay-mode! {:mode mode
  841. :baseline baseline
  842. :remote remote
  843. :local local-ops}))
  844. (mode-seq (:mode opts)))
  845. payload {:server-db server-db'
  846. :client-ops-db client-ops-db'
  847. :input {:from-t from-t
  848. :effective-from-t effective-from-t
  849. :to-t to-t
  850. :remote-limit remote-limit
  851. :mode (:mode opts)
  852. :pending-only (boolean (:pending-only opts))
  853. :local-created-at-min (:local-created-at-min opts)
  854. :local-created-at-max (:local-created-at-max opts)
  855. :local-tx-id (:local-tx-id opts)
  856. :auto-from-conflict (boolean auto-from-conflict)}
  857. :counts {:server-tx-total (count server-rows)
  858. :baseline (count baseline)
  859. :remote (count remote)
  860. :local-selected (count local-ops)
  861. :local-total (count all-local-ops)}
  862. :conflicts {:deleted-uuids (mapv str (:deleted-uuids conflicts))
  863. :first-conflict-t (:first-conflict-t conflicts)
  864. :conflict-count (count (:conflicts conflicts))
  865. :sample (->> (:conflicts conflicts)
  866. (take 10)
  867. (mapv (fn [c]
  868. (update c :hits #(mapv str %)))))}
  869. :results results}]
  870. (if pretty
  871. (println (js/JSON.stringify (clj->js payload) nil 2))
  872. (println (js/JSON.stringify (clj->js payload))))))))))
  873. (when (= nbb/*file* (nbb/invoked-file))
  874. (-main *command-line-args*))