From 68dcedd2e8efa28a52510fe4c930967728f03d7f Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 1 Mar 2026 16:20:04 -0700 Subject: [PATCH] build loro storage layer primitives --- Cargo.lock | 1187 +++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 3 +- src/db.rs | 807 ++++++++++++++++++++++---------- src/migrate.rs | 203 +-------- 4 files changed, 1718 insertions(+), 482 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b88477759168a9baede7b52e2c5090b99623eb47..445a9d49f35d5a03594087fcacf0f0372f1bf395 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,19 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -76,6 +89,33 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "append-only-bytes" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac436601d6bdde674a0d7fb593e829ffe7b3387c351b356dd20e2d40f5bf3ee5" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ccd462b64c3c72f1be8305905a85d85403d768e8690c9b8bd3b9009a5761679" + [[package]] name = "assert_cmd" version = "2.1.2" @@ -91,6 +131,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -103,6 +152,24 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -120,6 +187,18 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.56" @@ -175,10 +254,10 @@ version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -187,6 +266,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -210,6 +298,21 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossterm" version = "0.29.0" @@ -233,12 +336,84 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "document-features" version = "0.2.12" @@ -248,6 +423,66 @@ dependencies = [ "litrs", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "ensure-cov" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33753185802e107b8fa907192af1f0eca13b1fb33327a59266d650fef29b2b4e" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -264,18 +499,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.3.0" @@ -297,12 +520,82 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-btree" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c1bce85c110ab718fd139e0cc89c51b63bd647b14a767e24bdfc77c83df79b" +dependencies = [ + "arref", + "heapless 0.9.2", + "itertools 0.11.0", + "loro-thunderdome", + "proc-macro2", + "rustc-hash", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -316,6 +609,24 @@ dependencies = [ "wasip3", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -332,14 +643,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "hashlink" -version = "0.10.0" +name = "heapless" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ - "hashbrown 0.15.5", + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -376,6 +718,27 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -394,6 +757,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -410,6 +791,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -422,17 +815,6 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" -[[package]] -name = "libsqlite3-sys" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -460,18 +842,274 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "loro" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75216d8f99725531a30f7b00901ee154a4f8a9b7f125bfe032e197d4c7ffb8c" +dependencies = [ + "enum-as-inner 0.6.1", + "generic-btree", + "loro-common", + "loro-delta", + "loro-internal", + "loro-kv-store", + "rustc-hash", + "tracing", +] + +[[package]] +name = "loro-common" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70363ea05a9c507fd9d58b65dc414bf515f636d69d8ab53e50ecbe8d27eef90c" +dependencies = [ + "arbitrary", + "enum-as-inner 0.6.1", + "leb128", + "loro-rle", + "nonmax", + "rustc-hash", + "serde", + "serde_columnar", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "loro-delta" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eafa788a72c1cbf0b7dc08a862cd7cc31b96d99c2ef749cdc94c2330f9494d3" +dependencies = [ + "arrayvec", + "enum-as-inner 0.5.1", + "generic-btree", + "heapless 0.8.0", +] + +[[package]] +name = "loro-internal" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f447044ec3d3ba572623859add3334bd87b84340ee5fdf00315bfee0e3ad3e3f" +dependencies = [ + "append-only-bytes", + "arref", + "bytes", + "either", + "ensure-cov", + "enum-as-inner 0.6.1", + "enum_dispatch", + "generic-btree", + "getrandom 0.2.17", + "im", + "itertools 0.12.1", + "leb128", + "loom", + "loro-common", + "loro-delta", + "loro-kv-store", + "loro-rle", + "loro_fractional_index", + "md5", + "nonmax", + "num", + "num-traits", + "once_cell", + "parking_lot", + "pest", + "pest_derive", + "postcard", + "pretty_assertions", + "rand 0.8.5", + "rustc-hash", + "serde", + "serde_columnar", + "serde_json", + "smallvec", + "thiserror 1.0.69", + "thread_local", + "tracing", + "wasm-bindgen", + "xxhash-rust", +] + +[[package]] +name = "loro-kv-store" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78beebc933a33c26495c9a98f05b38bc0a4c0a337ecfbd3146ce1f9437eec71f" +dependencies = [ + "bytes", + "ensure-cov", + "loro-common", + "lz4_flex", + "once_cell", + "quick_cache", + "rustc-hash", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "loro-rle" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76400c3eea6bb39b013406acce964a8db39311534e308286c8d8721baba8ee20" +dependencies = [ + "append-only-bytes", + "num", + "smallvec", +] + +[[package]] +name = "loro-thunderdome" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" + +[[package]] +name = "loro_fractional_index" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c8ea186958094052b971fe7e322a934b034c3bf62f0458ccea04fcd687ba1" +dependencies = [ + "once_cell", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -517,10 +1155,75 @@ dependencies = [ ] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "predicates" @@ -552,6 +1255,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -559,7 +1272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -571,6 +1284,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick_cache" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + [[package]] name = "quote" version = "1.0.44" @@ -586,6 +1311,74 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -625,17 +1418,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "rusqlite" -version = "0.34.0" +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", + "semver", ] [[package]] @@ -657,6 +1451,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -679,6 +1479,31 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_columnar" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a16e404f17b16d0273460350e29b02d76ba0d70f34afdc9a4fa034c97d6c6eb" +dependencies = [ + "itertools 0.11.0", + "postcard", + "serde", + "serde_columnar_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_columnar_derive" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45958fce4903f67e871fbf15ac78e289269b21ebd357d6fecacdba233629112e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -696,7 +1521,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -712,17 +1537,65 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -730,6 +1603,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -748,7 +1632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys", @@ -760,6 +1644,144 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -791,10 +1813,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -805,6 +1833,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -855,7 +1889,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -902,6 +1936,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -945,7 +1989,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -956,7 +2000,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1008,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -1019,10 +2063,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1038,7 +2082,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1080,6 +2124,18 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yatd" version = "0.1.0" @@ -1089,11 +2145,32 @@ dependencies = [ "chrono", "clap", "comfy-table", + "loro", "predicates", - "rusqlite", "serde", "serde_json", "tempfile", + "ulid", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 27239288aca701119cfee2d85c87cd0ae3c69a2a..5ed8ee32605a446962ee5c4e7973a475e7ca62a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,10 @@ anyhow = "1" chrono = { version = "0.4", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive"] } comfy-table = "7.2.2" -rusqlite = { version = "0.34", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +loro = "1" +ulid = "1" [dev-dependencies] assert_cmd = "2" diff --git a/src/db.rs b/src/db.rs index 34b87032512cc91f8ec99db14e1f842cb88928e9..7be0ba8fe9ef1340304a42e8ebe08dc2d38c79b1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,257 +1,371 @@ -use anyhow::{bail, Result}; -use rusqlite::Connection; +use anyhow::{anyhow, bail, Context, Result}; +use loro::{ExportMode, LoroDoc, PeerID}; use serde::Serialize; -use std::hash::{DefaultHasher, Hash, Hasher}; +use serde_json::Value; +use std::fmt; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::SystemTime; +use ulid::Ulid; const TD_DIR: &str = ".td"; -const DB_FILE: &str = "tasks.db"; +const PROJECTS_DIR: &str = "projects"; +const CHANGES_DIR: &str = "changes"; +const BASE_FILE: &str = "base.loro"; +const TMP_SUFFIX: &str = ".tmp"; +const SCHEMA_VERSION: u32 = 1; -/// A task record. -#[derive(Debug, Serialize)] -pub struct Task { - pub id: String, - pub title: String, - pub description: String, - #[serde(rename = "type")] - pub task_type: String, - pub priority: i32, - pub status: String, - pub effort: i32, - pub parent: String, - pub created: String, - pub updated: String, -} - -static ID_COUNTER: AtomicU64 = AtomicU64::new(0); - -/// Generate a short unique ID like `td-a1b2c3`. -pub fn gen_id() -> String { - let mut hasher = DefaultHasher::new(); - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_nanos() - .hash(&mut hasher); - std::process::id().hash(&mut hasher); - ID_COUNTER.fetch_add(1, Ordering::Relaxed).hash(&mut hasher); - format!("td-{:06x}", hasher.finish() & 0xffffff) -} - -/// A task with its labels and blockers. -#[derive(Debug, Serialize)] -pub struct TaskDetail { - #[serde(flatten)] - pub task: Task, - pub labels: Vec, - pub blockers: Vec, +/// Current UTC time in ISO 8601 format. +pub fn now_utc() -> String { + chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string() } -/// A work log entry attached to a task. -#[derive(Debug, Serialize)] -pub struct LogEntry { - pub id: i64, - pub task_id: String, - pub timestamp: String, - pub body: String, -} - -/// Parse a priority label to its integer value. -/// -/// Accepts "low" (3), "medium" (2), or "high" (1). -pub fn parse_priority(s: &str) -> anyhow::Result { - match s { - "high" => Ok(1), - "medium" => Ok(2), - "low" => Ok(3), - _ => bail!("invalid priority '{s}': expected low, medium, or high"), +/// Lifecycle state for a task. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Status { + Open, + InProgress, + Closed, +} + +impl Status { + fn as_str(self) -> &'static str { + match self { + Status::Open => "open", + Status::InProgress => "in_progress", + Status::Closed => "closed", + } + } + + fn parse(raw: &str) -> Result { + match raw { + "open" => Ok(Self::Open), + "in_progress" => Ok(Self::InProgress), + "closed" => Ok(Self::Closed), + _ => bail!("invalid status '{raw}'"), + } } } -/// Convert a priority integer back to its label. -pub fn priority_label(val: i32) -> &'static str { - match val { - 1 => "high", - 2 => "medium", - 3 => "low", - _ => "unknown", +/// Priority for task ordering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Priority { + High, + Medium, + Low, +} + +impl Priority { + fn as_str(self) -> &'static str { + match self { + Priority::High => "high", + Priority::Medium => "medium", + Priority::Low => "low", + } + } + + fn parse(raw: &str) -> Result { + match raw { + "high" => Ok(Self::High), + "medium" => Ok(Self::Medium), + "low" => Ok(Self::Low), + _ => bail!("invalid priority '{raw}'"), + } } } -/// Parse an effort label to its integer value. -/// -/// Accepts "low" (1), "medium" (2), or "high" (3). -pub fn parse_effort(s: &str) -> anyhow::Result { - match s { - "low" => Ok(1), - "medium" => Ok(2), - "high" => Ok(3), - _ => bail!("invalid effort '{s}': expected low, medium, or high"), +/// Estimated effort for a task. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Effort { + Low, + Medium, + High, +} + +impl Effort { + fn as_str(self) -> &'static str { + match self { + Effort::Low => "low", + Effort::Medium => "medium", + Effort::High => "high", + } + } + + fn parse(raw: &str) -> Result { + match raw { + "low" => Ok(Self::Low), + "medium" => Ok(Self::Medium), + "high" => Ok(Self::High), + _ => bail!("invalid effort '{raw}'"), + } } } -/// Convert an effort integer back to its label. -pub fn effort_label(val: i32) -> &'static str { - match val { - 1 => "low", - 2 => "medium", - 3 => "high", - _ => "unknown", +/// A stable task identifier backed by a ULID. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(transparent)] +pub struct TaskId(String); + +impl TaskId { + pub fn new(id: Ulid) -> Self { + Self(id.to_string()) + } + + pub fn parse(raw: &str) -> Result { + let id = Ulid::from_string(raw).with_context(|| format!("invalid task id '{raw}'"))?; + Ok(Self::new(id)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn short(&self) -> String { + self.0.chars().take(7).collect() } } -/// Current UTC time in ISO 8601 format. -pub fn now_utc() -> String { - chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string() +impl fmt::Display for TaskId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.short()) + } } -/// Read a single `Task` row from a query result. -pub fn row_to_task(row: &rusqlite::Row) -> rusqlite::Result { - Ok(Task { - id: row.get("id")?, - title: row.get("title")?, - description: row.get("description")?, - task_type: row.get("type")?, - priority: row.get("priority")?, - status: row.get("status")?, - effort: row.get("effort")?, - parent: row.get("parent")?, - created: row.get("created")?, - updated: row.get("updated")?, - }) +/// A task log entry embedded in a task record. +#[derive(Debug, Clone, Serialize)] +pub struct LogEntry { + pub id: TaskId, + pub timestamp: String, + pub message: String, } -/// Load labels for a task. -pub fn load_labels(conn: &Connection, task_id: &str) -> Result> { - let mut stmt = conn.prepare("SELECT label FROM labels WHERE task_id = ?1")?; - let labels = stmt - .query_map([task_id], |r| r.get(0))? - .collect::>>()?; - Ok(labels) -} - -/// Load blockers for a task. -pub fn load_blockers(conn: &Connection, task_id: &str) -> Result> { - let mut stmt = conn.prepare("SELECT blocker_id FROM blockers WHERE task_id = ?1")?; - let blockers = stmt - .query_map([task_id], |r| r.get(0))? - .collect::>>()?; - Ok(blockers) -} - -/// Load log entries for a task in chronological order. -pub fn load_logs(conn: &Connection, task_id: &str) -> Result> { - let mut stmt = conn.prepare( - "SELECT id, task_id, timestamp, body - FROM task_logs - WHERE task_id = ?1 - ORDER BY timestamp ASC, id ASC", - )?; - let logs = stmt - .query_map([task_id], |r| { - Ok(LogEntry { - id: r.get("id")?, - task_id: r.get("task_id")?, - timestamp: r.get("timestamp")?, - body: r.get("body")?, - }) - })? - .collect::>>()?; - Ok(logs) -} - -/// Load blockers for a task, partitioned by whether they are resolved. -/// -/// Returns `(open, resolved)` where open blockers have a non-closed status -/// and resolved blockers are closed. -pub fn load_blockers_partitioned( - conn: &Connection, - task_id: &str, -) -> Result<(Vec, Vec)> { - let mut stmt = conn.prepare( - "SELECT b.blocker_id, COALESCE(t.status, 'open') - FROM blockers b - LEFT JOIN tasks t ON b.blocker_id = t.id - WHERE b.task_id = ?1", - )?; - let mut open = Vec::new(); - let mut resolved = Vec::new(); - let rows: Vec<(String, String)> = stmt - .query_map([task_id], |r| Ok((r.get(0)?, r.get(1)?)))? - .collect::>()?; - for (id, status) in rows { - if status == "closed" { - resolved.push(id); - } else { - open.push(id); +/// Hydrated task data from the CRDT document. +#[derive(Debug, Clone, Serialize)] +pub struct Task { + pub id: TaskId, + pub title: String, + pub description: String, + #[serde(rename = "type")] + pub task_type: String, + pub priority: Priority, + pub status: Status, + pub effort: Effort, + pub parent: Option, + pub created_at: String, + pub updated_at: String, + pub deleted_at: Option, + pub labels: Vec, + pub blockers: Vec, + pub logs: Vec, +} + +/// Result type for partitioning blockers by task state. +#[derive(Debug, Default, Clone, Serialize)] +pub struct BlockerPartition { + pub open: Vec, + pub resolved: Vec, +} + +/// Storage wrapper around one project's Loro document and disk layout. +#[derive(Debug, Clone)] +pub struct Store { + root: PathBuf, + project: String, + doc: LoroDoc, +} + +impl Store { + /// Create a new store rooted at the current project path. + pub fn init(root: &Path) -> Result { + let project = project_name(root)?; + let project_dir = project_dir(root, &project); + fs::create_dir_all(project_dir.join(CHANGES_DIR))?; + + let doc = LoroDoc::new(); + let peer_id = load_or_create_device_peer_id()?; + doc.set_peer_id(peer_id)?; + + doc.get_map("tasks"); + let meta = doc.get_map("meta"); + meta.insert("schema_version", SCHEMA_VERSION as i64)?; + meta.insert("project_id", Ulid::new().to_string())?; + meta.insert("created_at", now_utc())?; + + let snapshot = doc + .export(ExportMode::Snapshot) + .context("failed to export initial loro snapshot")?; + atomic_write_file(&project_dir.join(BASE_FILE), &snapshot)?; + + Ok(Self { + root: root.to_path_buf(), + project, + doc, + }) + } + + /// Open an existing store and replay deltas. + pub fn open(root: &Path) -> Result { + let project = project_name(root)?; + let project_dir = project_dir(root, &project); + let base_path = project_dir.join(BASE_FILE); + + if !base_path.exists() { + bail!("not initialized. Run 'td init'"); + } + + let base = fs::read(&base_path) + .with_context(|| format!("failed to read loro snapshot '{}'", base_path.display()))?; + + let doc = LoroDoc::from_snapshot(&base).context("failed to load loro snapshot")?; + doc.set_peer_id(load_or_create_device_peer_id()?)?; + + let mut deltas = collect_delta_paths(&project_dir)?; + deltas.sort_by_key(|path| { + path.file_stem() + .and_then(|s| s.to_str()) + .and_then(|s| Ulid::from_string(s).ok()) + }); + + for delta_path in deltas { + let bytes = fs::read(&delta_path) + .with_context(|| format!("failed to read loro delta '{}'", delta_path.display()))?; + doc.import(&bytes).with_context(|| { + format!("failed to import loro delta '{}'", delta_path.display()) + })?; } + + Ok(Self { + root: root.to_path_buf(), + project, + doc, + }) } - Ok((open, resolved)) -} -/// Check whether `from` can reach `to` by following blocker edges. -/// -/// Returns `true` if there is a path from `from` to `to` in the blocker -/// graph (i.e. adding an edge `to → from` would create a cycle). -/// Uses a visited-set so it terminates even if the graph already contains -/// a cycle from bad data. -pub fn would_cycle(conn: &Connection, from: &str, to: &str) -> Result { - use std::collections::{HashSet, VecDeque}; + pub fn root(&self) -> &Path { + &self.root + } - if from == to { - return Ok(true); + pub fn project_name(&self) -> &str { + &self.project } - let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); - queue.push_back(from.to_string()); - visited.insert(from.to_string()); + pub fn doc(&self) -> &LoroDoc { + &self.doc + } - let mut stmt = conn.prepare("SELECT blocker_id FROM blockers WHERE task_id = ?1")?; + /// Export all current state to a fresh base snapshot. + pub fn write_snapshot(&self) -> Result { + let out = project_dir(&self.root, &self.project).join(BASE_FILE); + let bytes = self + .doc + .export(ExportMode::Snapshot) + .context("failed to export loro snapshot")?; + atomic_write_file(&out, &bytes)?; + Ok(out) + } - while let Some(current) = queue.pop_front() { - let neighbors: Vec = stmt - .query_map([¤t], |r| r.get(0))? - .collect::>()?; + /// Apply a local mutation and persist only the resulting delta. + pub fn apply_and_persist(&self, mutator: F) -> Result + where + F: FnOnce(&LoroDoc) -> Result<()>, + { + let before = self.doc.oplog_vv(); + mutator(&self.doc)?; + self.doc.commit(); - for neighbor in neighbors { - if neighbor == to { - return Ok(true); - } - if visited.insert(neighbor.clone()) { - queue.push_back(neighbor); + let delta = self + .doc + .export(ExportMode::updates(&before)) + .context("failed to export loro update delta")?; + + let filename = format!("{}.loro", Ulid::new()); + let path = project_dir(&self.root, &self.project) + .join(CHANGES_DIR) + .join(filename); + atomic_write_file(&path, &delta)?; + Ok(path) + } + + /// Return hydrated tasks, excluding tombstones. + pub fn list_tasks(&self) -> Result> { + self.list_tasks_inner(false) + } + + /// Return hydrated tasks, including tombstoned rows. + pub fn list_tasks_unfiltered(&self) -> Result> { + self.list_tasks_inner(true) + } + + /// Find a task by exact ULID string. + pub fn get_task(&self, id: &TaskId, include_deleted: bool) -> Result> { + let tasks = if include_deleted { + self.list_tasks_unfiltered()? + } else { + self.list_tasks()? + }; + Ok(tasks.into_iter().find(|task| task.id == *id)) + } + + fn list_tasks_inner(&self, include_deleted: bool) -> Result> { + let root = serde_json::to_value(self.doc.get_deep_value())?; + let tasks_obj = root + .get("tasks") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("missing root tasks map"))?; + + let mut tasks = Vec::with_capacity(tasks_obj.len()); + for (task_id_raw, task_json) in tasks_obj { + let task = hydrate_task(task_id_raw, task_json)?; + if include_deleted || task.deleted_at.is_none() { + tasks.push(task); } } + + tasks.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str())); + Ok(tasks) } - Ok(false) + /// Return current schema version from root meta map. + pub fn schema_version(&self) -> Result { + let root = serde_json::to_value(self.doc.get_deep_value())?; + let meta = root + .get("meta") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("missing root meta map"))?; + let n = meta + .get("schema_version") + .and_then(Value::as_u64) + .ok_or_else(|| anyhow!("invalid or missing meta.schema_version"))?; + Ok(n as u32) + } } -/// Check whether a task with the given ID exists. -pub fn task_exists(conn: &Connection, id: &str) -> Result { - let count: i32 = conn.query_row("SELECT COUNT(*) FROM tasks WHERE id = ?1", [id], |r| { - r.get(0) - })?; - Ok(count > 0) -} - -/// Load a full task with labels and blockers. -pub fn load_task_detail(conn: &Connection, id: &str) -> Result { - let task = conn.query_row( - "SELECT id, title, description, type, priority, status, effort, parent, created, updated - FROM tasks WHERE id = ?1", - [id], - row_to_task, - )?; - let labels = load_labels(conn, id)?; - let blockers = load_blockers(conn, id)?; - Ok(TaskDetail { - task, - labels, - blockers, - }) +/// Generate a new task ULID. +pub fn gen_id() -> TaskId { + TaskId::new(Ulid::new()) +} + +/// Parse a priority string value. +pub fn parse_priority(s: &str) -> Result { + Priority::parse(s) +} + +/// Parse an effort string value. +pub fn parse_effort(s: &str) -> Result { + Effort::parse(s) +} + +/// Convert a priority value to its storage label. +pub fn priority_label(p: Priority) -> &'static str { + p.as_str() +} + +/// Convert an effort value to its storage label. +pub fn effort_label(e: Effort) -> &'static str { + e.as_str() } /// Walk up from `start` looking for a `.td/` directory. @@ -267,29 +381,250 @@ pub fn find_root(start: &Path) -> Result { } } -/// Create the `.td/` directory and initialise the database via migrations. -pub fn init(root: &Path) -> Result { - let td = root.join(TD_DIR); - std::fs::create_dir_all(&td)?; - let mut conn = Connection::open(td.join(DB_FILE))?; - conn.execute_batch("PRAGMA foreign_keys = ON;")?; - crate::migrate::migrate_up(&mut conn)?; - Ok(conn) +/// Return the path to the `.td/` directory under `root`. +pub fn td_dir(root: &Path) -> PathBuf { + root.join(TD_DIR) +} + +/// Initialize on-disk project storage and return the opened store. +pub fn init(root: &Path) -> Result { + fs::create_dir_all(td_dir(root))?; + Store::init(root) +} + +/// Open an existing project's storage. +pub fn open(root: &Path) -> Result { + Store::open(root) +} + +fn hydrate_task(task_id_raw: &str, value: &Value) -> Result { + let obj = value + .as_object() + .ok_or_else(|| anyhow!("task '{task_id_raw}' is not an object"))?; + + let id = TaskId::parse(task_id_raw)?; + + let title = get_required_string(obj, "title")?; + let description = get_required_string(obj, "description")?; + let task_type = get_required_string(obj, "type")?; + let status = Status::parse(&get_required_string(obj, "status")?)?; + let priority = Priority::parse(&get_required_string(obj, "priority")?)?; + let effort = Effort::parse(&get_required_string(obj, "effort")?)?; + let parent = match obj.get("parent").and_then(Value::as_str) { + Some("") | None => None, + Some(raw) => Some(TaskId::parse(raw)?), + }; + + let created_at = get_required_string(obj, "created_at")?; + let updated_at = get_required_string(obj, "updated_at")?; + let deleted_at = obj + .get("deleted_at") + .and_then(Value::as_str) + .map(str::to_owned) + .filter(|s| !s.is_empty()); + + let labels = obj + .get("labels") + .and_then(Value::as_object) + .map(|m| m.keys().cloned().collect()) + .unwrap_or_else(Vec::new); + + let blockers = obj + .get("blockers") + .and_then(Value::as_object) + .map(|m| { + m.keys() + .map(|raw| TaskId::parse(raw)) + .collect::>>() + }) + .transpose()? + .unwrap_or_else(Vec::new); + + let mut logs = obj + .get("logs") + .and_then(Value::as_object) + .map(|logs| { + logs.iter() + .map(|(log_id_raw, payload)| { + let payload_obj = payload.as_object().ok_or_else(|| { + anyhow!("log '{log_id_raw}' on task '{task_id_raw}' is not an object") + })?; + Ok(LogEntry { + id: TaskId::parse(log_id_raw)?, + timestamp: get_required_string(payload_obj, "timestamp")?, + message: get_required_string(payload_obj, "message")?, + }) + }) + .collect::>>() + }) + .transpose()? + .unwrap_or_else(Vec::new); + + logs.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str())); + + Ok(Task { + id, + title, + description, + task_type, + priority, + status, + effort, + parent, + created_at, + updated_at, + deleted_at, + labels, + blockers, + logs, + }) } -/// Open an existing database, applying any pending migrations. -pub fn open(root: &Path) -> Result { - let path = root.join(TD_DIR).join(DB_FILE); - if !path.exists() { - bail!("not initialized. Run 'td init'"); +fn get_required_string(map: &serde_json::Map, key: &str) -> Result { + map.get(key) + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| anyhow!("missing or non-string key '{key}'")) +} + +fn collect_delta_paths(project_dir: &Path) -> Result> { + let mut paths = Vec::new(); + + collect_changes_from_dir(&project_dir.join(CHANGES_DIR), &mut paths)?; + + for entry in fs::read_dir(project_dir)? { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if name.starts_with("changes.compacting.") { + collect_changes_from_dir(&path, &mut paths)?; + } } - let mut conn = Connection::open(path)?; - conn.execute_batch("PRAGMA foreign_keys = ON;")?; - crate::migrate::migrate_up(&mut conn)?; - Ok(conn) + + Ok(paths) } -/// Return the path to the `.td/` directory under `root`. -pub fn td_dir(root: &Path) -> PathBuf { - root.join(TD_DIR) +fn collect_changes_from_dir(dir: &Path, out: &mut Vec) -> Result<()> { + if !dir.exists() { + return Ok(()); + } + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if !path.is_file() { + continue; + } + + let Some(filename) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if filename.ends_with(TMP_SUFFIX) { + continue; + } + if !filename.ends_with(".loro") { + continue; + } + + let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { + continue; + }; + if Ulid::from_string(stem).is_err() { + continue; + } + + out.push(path); + } + + Ok(()) +} + +fn project_name(root: &Path) -> Result { + root.file_name() + .and_then(|n| n.to_str()) + .map(str::to_owned) + .ok_or_else(|| { + anyhow!( + "could not infer project name from path '{}'", + root.display() + ) + }) +} + +fn project_dir(root: &Path, project: &str) -> PathBuf { + td_dir(root).join(PROJECTS_DIR).join(project) +} + +fn load_or_create_device_peer_id() -> Result { + let home = std::env::var("HOME").context("HOME is not set")?; + let path = PathBuf::from(home) + .join(".local") + .join("share") + .join("td") + .join("device_id"); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let device_ulid = if path.exists() { + let content = fs::read_to_string(&path) + .with_context(|| format!("failed reading device id from '{}'", path.display()))?; + Ulid::from_string(content.trim()).context("invalid persisted device id ULID")? + } else { + let id = Ulid::new(); + atomic_write_file(&path, id.to_string().as_bytes())?; + id + }; + + Ok((device_ulid.to_u128() & u64::MAX as u128) as u64) +} + +fn atomic_write_file(path: &Path, bytes: &[u8]) -> Result<()> { + let parent = path + .parent() + .ok_or_else(|| anyhow!("cannot atomically write root path '{}'", path.display()))?; + fs::create_dir_all(parent)?; + + let tmp_name = format!( + "{}.{}{}", + path.file_name().and_then(|n| n.to_str()).unwrap_or("write"), + Ulid::new(), + TMP_SUFFIX + ); + let tmp_path = parent.join(tmp_name); + + { + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&tmp_path) + .with_context(|| format!("failed to open temp file '{}'", tmp_path.display()))?; + file.write_all(bytes)?; + file.sync_all()?; + } + + fs::rename(&tmp_path, path).with_context(|| { + format!( + "failed to atomically rename '{}' to '{}'", + tmp_path.display(), + path.display() + ) + })?; + + sync_dir(parent)?; + Ok(()) +} + +fn sync_dir(path: &Path) -> Result<()> { + let dir = + File::open(path).with_context(|| format!("failed opening dir '{}'", path.display()))?; + dir.sync_all() + .with_context(|| format!("failed fsync on dir '{}'", path.display()))?; + Ok(()) } diff --git a/src/migrate.rs b/src/migrate.rs index 44183413ca7f13f7aea88131e7047200c1518d0d..fa36137769e5dc8d4603fae3fca572f30b737eec 100644 --- a/src/migrate.rs +++ b/src/migrate.rs @@ -1,152 +1,18 @@ -//! Versioned schema migrations for the task database. +//! Loro-backed storage does not use SQL schema migrations. //! -//! Each migration has up/down SQL and optional post-hook functions that run -//! inside the same transaction. Schema version is tracked via SQLite's built-in -//! `PRAGMA user_version`. +//! The old SQLite migration flow has been replaced by document-level metadata +//! (`meta.schema_version`) in the Loro snapshot. This module remains only as a +//! compatibility shim for call sites that still invoke migration entry points. -use anyhow::{bail, Context, Result}; -use rusqlite::Connection; +use anyhow::Result; -/// A single schema migration step. -struct Migration { - up_sql: &'static str, - down_sql: &'static str, - post_hook_up: Option Result<()>>, - post_hook_down: Option Result<()>>, -} - -/// All migrations in order. The array index is the version the database will -/// be at *after* the migration runs (1-indexed: migration 0 brings the DB to -/// version 1). -static MIGRATIONS: &[Migration] = &[ - // 0 → 1: initial schema - Migration { - up_sql: include_str!("migrations/0001_initial_schema.up.sql"), - down_sql: include_str!("migrations/0001_initial_schema.down.sql"), - post_hook_up: None, - post_hook_down: None, - }, - // 1 → 2: add effort column (integer-backed, default medium) - Migration { - up_sql: include_str!("migrations/0002_add_effort.up.sql"), - down_sql: include_str!("migrations/0002_add_effort.down.sql"), - post_hook_up: None, - post_hook_down: None, - }, - Migration { - up_sql: include_str!("migrations/0003_blocker_fk.up.sql"), - down_sql: include_str!("migrations/0003_blocker_fk.down.sql"), - post_hook_up: None, - post_hook_down: None, - }, - Migration { - up_sql: include_str!("migrations/0004_task_logs.up.sql"), - down_sql: include_str!("migrations/0004_task_logs.down.sql"), - post_hook_up: None, - post_hook_down: None, - }, - Migration { - up_sql: include_str!("migrations/0005_cascade_fks.up.sql"), - down_sql: include_str!("migrations/0005_cascade_fks.down.sql"), - post_hook_up: None, - post_hook_down: None, - }, -]; - -/// Read the current schema version from the database. -fn get_version(conn: &Connection) -> Result { - let v: u32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; - Ok(v) -} - -/// Set the schema version inside an open transaction. -fn set_version(tx: &rusqlite::Transaction, version: u32) -> Result<()> { - // PRAGMA cannot be parameterised, but the value is a u32 we control. - tx.pragma_update(None, "user_version", version)?; +/// No-op compatibility function for legacy call sites. +pub fn migrate_up(_conn: &mut T) -> Result<()> { Ok(()) } -/// Apply all pending up-migrations to bring the database to the latest version. -pub fn migrate_up(conn: &mut Connection) -> Result<()> { - let current = get_version(conn)?; - let latest = MIGRATIONS.len() as u32; - - if current > latest { - bail!( - "database is at version {current} but this binary only knows up to {latest}; \ - upgrade td or use a matching version" - ); - } - - for (idx, m) in MIGRATIONS.iter().enumerate().skip(current as usize) { - let target_version = (idx + 1) as u32; - - let tx = conn - .transaction() - .context("failed to begin migration transaction")?; - - if !m.up_sql.is_empty() { - tx.execute_batch(m.up_sql) - .with_context(|| format!("migration {target_version} up SQL failed"))?; - } - - if let Some(hook) = m.post_hook_up { - hook(&tx) - .with_context(|| format!("migration {target_version} post-hook (up) failed"))?; - } - - set_version(&tx, target_version)?; - - tx.commit() - .with_context(|| format!("failed to commit migration {target_version}"))?; - } - - Ok(()) -} - -/// Roll back migrations down to `target_version` (inclusive — the database -/// will be at `target_version` when this returns). -pub fn migrate_down(conn: &mut Connection, target_version: u32) -> Result<()> { - let current = get_version(conn)?; - - if target_version >= current { - bail!("target version {target_version} is not below current version {current}"); - } - - if target_version > MIGRATIONS.len() as u32 { - bail!("target version {target_version} exceeds known migrations"); - } - - // Walk backwards: if we're at version 3 and want version 1, we undo - // migration index 2 (v3→v2) then index 1 (v2→v1). - for (idx, m) in MIGRATIONS - .iter() - .enumerate() - .rev() - .filter(|(i, _)| *i >= target_version as usize && *i < current as usize) - { - let from_version = (idx + 1) as u32; - - let tx = conn - .transaction() - .context("failed to begin down-migration transaction")?; - - if let Some(hook) = m.post_hook_down { - hook(&tx) - .with_context(|| format!("migration {from_version} post-hook (down) failed"))?; - } - - if !m.down_sql.is_empty() { - tx.execute_batch(m.down_sql) - .with_context(|| format!("migration {from_version} down SQL failed"))?; - } - - set_version(&tx, idx as u32)?; - - tx.commit() - .with_context(|| format!("failed to commit down-migration {from_version}"))?; - } - +/// No-op compatibility function for legacy call sites. +pub fn migrate_down(_conn: &mut T, _target_version: u32) -> Result<()> { Ok(()) } @@ -155,52 +21,9 @@ mod tests { use super::*; #[test] - fn migrate_up_from_empty() { - let mut conn = Connection::open_in_memory().unwrap(); - migrate_up(&mut conn).unwrap(); - - let version = get_version(&conn).unwrap(); - assert_eq!(version, MIGRATIONS.len() as u32); - - // Verify tables exist by querying them. - conn.execute_batch("SELECT id FROM tasks LIMIT 0").unwrap(); - conn.execute_batch("SELECT task_id FROM labels LIMIT 0") - .unwrap(); - conn.execute_batch("SELECT task_id FROM blockers LIMIT 0") - .unwrap(); - conn.execute_batch("SELECT task_id FROM task_logs LIMIT 0") - .unwrap(); - } - - #[test] - fn migrate_up_is_idempotent() { - let mut conn = Connection::open_in_memory().unwrap(); - migrate_up(&mut conn).unwrap(); - // Running again should be a no-op, not an error. - migrate_up(&mut conn).unwrap(); - assert_eq!(get_version(&conn).unwrap(), MIGRATIONS.len() as u32); - } - - #[test] - fn migrate_down_to_zero() { - let mut conn = Connection::open_in_memory().unwrap(); - migrate_up(&mut conn).unwrap(); - migrate_down(&mut conn, 0).unwrap(); - assert_eq!(get_version(&conn).unwrap(), 0); - - // Tables should be gone. - let result = conn.execute_batch("SELECT id FROM tasks LIMIT 0"); - assert!(result.is_err()); - } - - #[test] - fn rejects_future_version() { - let mut conn = Connection::open_in_memory().unwrap(); - conn.pragma_update(None, "user_version", 999).unwrap(); - let err = migrate_up(&mut conn).unwrap_err(); - assert!( - err.to_string().contains("999"), - "error should mention the version: {err}" - ); + fn compatibility_noops_succeed() { + let mut placeholder = (); + migrate_up(&mut placeholder).unwrap(); + migrate_down(&mut placeholder, 0).unwrap(); } }