diff --git a/CLAUDE.md b/CLAUDE.md index f5d5cf5..745d561 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,12 +99,14 @@ Semcode uses the following search order to locate the `.semcode.db` database dir **For semcode-index:** 1. **-d flag**: If provided, use the specified path (direct database path or parent directory containing `.semcode.db`) -2. **Source directory**: Look for `.semcode.db` in the source directory specified by `-s` -3. **Current directory**: Fall back to `./.semcode.db` in the current working directory +2. **SEMCODE_DB environment variable**: Same path semantics as `-d` +3. **Source directory**: Look for `.semcode.db` in the source directory specified by `-s` +4. **Current directory**: Fall back to `./.semcode.db` in the current working directory **For semcode (query tool), semcode-mcp, and semcode-lsp:** 1. **-d flag / configuration**: If provided, use the specified path (direct database path or parent directory containing `.semcode.db`) -2. **Workspace/Current directory**: Use `./.semcode.db` in the workspace or current working directory +2. **SEMCODE_DB environment variable**: Same path semantics as `-d` +3. **Workspace/Current directory**: Use `./.semcode.db` in the workspace or current working directory The `-d` flag can specify either: - A direct path to the database directory (e.g., `./my-custom.db`) diff --git a/Cargo.lock b/Cargo.lock index ff77c04..c8993b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -109,7 +109,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -120,9 +120,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -284,7 +284,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "lexical-core", "memchr", @@ -327,7 +327,7 @@ version = "57.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" dependencies = [ - "bitflags 2.11.0", + "bitflags", "serde_core", "serde_json", ] @@ -377,9 +377,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -444,17 +444,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -463,9 +452,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -473,9 +462,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -485,9 +474,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "base64 0.22.1", @@ -512,8 +501,8 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.28.0", - "tower 0.5.3", + "tokio-tungstenite", + "tower", "tower-layer", "tower-service", "tracing", @@ -565,15 +554,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitpacking" @@ -607,16 +590,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -653,6 +636,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "brotli" version = "8.0.2" @@ -726,9 +715,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -742,12 +731,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -796,9 +779,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -818,9 +801,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -854,9 +837,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -873,7 +856,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -913,9 +896,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -924,9 +907,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -1023,6 +1006,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1139,9 +1131,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.86+curl-8.19.0" +version = "0.4.88+curl-8.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1dd6a487cf4532ce0d801634b82aa2deb7c9c3ed930b9dadfce904df000745" +checksum = "644816de6547255eff4e491a1dda1c19b7237f00b62a61e6e64859ce4f2906d0" dependencies = [ "cc", "libc", @@ -1149,7 +1141,7 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1223,26 +1215,13 @@ dependencies = [ [[package]] name = "dary_heap" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" dependencies = [ "serde", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1259,15 +1238,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "datafusion" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c18ba387f9c05ac1f3be32a73f8f3cc6c1cfc43e5d4b7a8e5b0d3a5eb48dc7" +checksum = "7541353e77dc7262b71ca27be07d8393661737e3a73b5d1b1c6f7d814c64fa2a" dependencies = [ "arrow", "arrow-schema", @@ -1303,7 +1282,7 @@ dependencies = [ "log", "object_store", "parking_lot", - "rand 0.9.2", + "rand 0.9.4", "regex", "sqlparser", "tempfile", @@ -1314,13 +1293,13 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c75a4ce672b27fb8423810efb92a3600027717a1664d06a2c307eeeabcec694" +checksum = "9997731f90fa5398ef831ad0e69600f92c861b79c0d38bd1a29b6f0e3a0ce4c8" dependencies = [ "arrow", "async-trait", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", @@ -1339,9 +1318,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8b9a3795ffb46bf4957a34c67d89a67558b311ae455c8d4295ff2115eeea50" +checksum = "2b30a3dd50dec860c9559275c8d97d9de602e611237a6ecfbda0b3b63b872352" dependencies = [ "arrow", "async-trait", @@ -1362,9 +1341,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205dc1e20441973f470e6b7ef87626a3b9187970e5106058fef1b713047f770c" +checksum = "d551054acec0398ca604512310b77ce05c46f66e54b54d48200a686e385cca4e" dependencies = [ "ahash", "arrow", @@ -1372,7 +1351,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "libc", "log", "object_store", @@ -1384,9 +1363,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf5880c02ff6f5f11fb5bc19211789fb32fd3c53d79b7d6cb2b12e401312ba0" +checksum = "567d40e285f5b79f8737b576605721cd6c1133b5d2b00bdbd5d9838d90d0812f" dependencies = [ "futures", "log", @@ -1395,9 +1374,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc614d6e709450e29b7b032a42c1bdb705f166a6b2edef7bed7c7897eb905499" +checksum = "27d2668f51b3b30befae2207472569e37807fdedd1d14da58acc6f8ca6257eae" dependencies = [ "arrow", "async-trait", @@ -1417,16 +1396,16 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "rand 0.9.2", + "rand 0.9.4", "tokio", "url", ] [[package]] name = "datafusion-datasource-arrow" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e497d5fc48dac7ce86f6b4fb09a3a494385774af301ff20ec91aebfae9b05b4" +checksum = "e02e1b3e3a8ec55f1f62de4252b0407c8567363d056078769a197e24fc834a0f" dependencies = [ "arrow", "arrow-ipc", @@ -1448,9 +1427,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dfc250cad940d0327ca2e9109dc98830892d17a3d6b2ca11d68570e872cf379" +checksum = "b559d7bf87d4f900f847baba8509634f838d9718695389e903604cdcccdb01f3" dependencies = [ "arrow", "async-trait", @@ -1471,9 +1450,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91e9677ed62833b0e8129dec0d1a8f3c9bb7590bd6dd714a43e4c3b663e4aa0" +checksum = "250e2d7591ba8b638f063854650faa40bca4e8bd4059b2ece8836f6388d02db4" dependencies = [ "arrow", "async-trait", @@ -1493,36 +1472,36 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e13e5fe3447baa0584b61ee8644086e007e1ef6e58f4be48bc8a72417854729" +checksum = "b9496cb0db222dbb9a3735760ceca7fc56f35e1d5502c38d0caa77a81e9c1f6a" [[package]] name = "datafusion-execution" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48a6cc03e34899a54546b229235f7b192634c8e832f78a267f0989b18216c56d" +checksum = "dc45d23c516ed8d3637751e44e09e21b45b3f58b473c802dddd1f1ad4fe435ff" dependencies = [ "arrow", "async-trait", "chrono", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-expr", "futures", "log", "object_store", "parking_lot", - "rand 0.9.2", + "rand 0.9.4", "tempfile", "url", ] [[package]] name = "datafusion-expr" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3315d87eca7a7df58e52a1fb43b4c4171b545fd30ffc3102945c162a9f6ddb" +checksum = "63dd30526d2db4fda6440806a41e4676334a94bc0596cc9cc2a0efed20ef2c44" dependencies = [ "arrow", "async-trait", @@ -1533,7 +1512,7 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "paste", "serde_json", @@ -1542,22 +1521,22 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c6d83feae0753799f933a2c47dfd15980c6947960cb95ed60f5c1f885548b3" +checksum = "1b486b5f6255d40976b88bb83813b0d035a8333e0ec39864824e78068cf42fa6" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "paste", ] [[package]] name = "datafusion-functions" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b82962015cc3db4d7662459c9f7fcda0591b5edacb8af1cf3bc3031f274800" +checksum = "07356c94118d881130dd0ffbff127540407d969c8978736e324edcd6c41cd48f" dependencies = [ "arrow", "arrow-buffer", @@ -1577,7 +1556,7 @@ dependencies = [ "log", "md-5", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "regex", "sha2", "unicode-segmentation", @@ -1586,9 +1565,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e42c227d9e55a6c8041785d4a8a117e4de531033d480aae10984247ac62e27e" +checksum = "b644f9cf696df9233ce6958b9807666d78563b56f923267474dd6c07795f1f8f" dependencies = [ "ahash", "arrow", @@ -1607,9 +1586,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cead3cfed825b0b688700f4338d281cd7857e4907775a5b9554c083edd5f3f95" +checksum = "c1de2deaaabe8923ce9ea9f29c47bbb4ee14f67ea2fe1ab5398d9bbebcf86e56" dependencies = [ "ahash", "arrow", @@ -1620,9 +1599,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ea99612970aebab8cf864d02eb3d296bbab7f4881e1023d282b57fe431b201" +checksum = "552f8d92e4331ee91d23c02d12bb6acf32cbfd5215117e01c0fb63cd4b15af1a" dependencies = [ "arrow", "arrow-ord", @@ -1643,9 +1622,9 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83dbf3ab8b9af6f209b068825a7adbd3b88bf276f2a1ec14ba09567b97f5674" +checksum = "970fd0cdd3df8802b9a9975ff600998289ba9d46682a4f7285cba4820c9ada78" dependencies = [ "arrow", "async-trait", @@ -1659,9 +1638,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732edabe07496e2fc5a1e57a284d7a36edcea445a2821119770a0dea624b472c" +checksum = "40b4c21a7c8a986a1866c0a87ab756d0bbf7b5f41f306009fa2d9af79c52ed31" dependencies = [ "arrow", "datafusion-common", @@ -1677,9 +1656,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c6e30e09700799bd52adce8c377ab03dda96e73a623e4803a31ad94fe7ce14" +checksum = "b1210ad73b8b3211aeaf4a42bef9bd7a2b7fce3ec119a478831f18c6ff7f7b93" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1687,9 +1666,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402f2a8ed70fb99a18f71580a1fe338604222a3d32ddeac6e72c5b34feea2d4d" +checksum = "aaa566a963013a38681ad82a727a654bc7feb19632426aea8c3412d415d200c5" dependencies = [ "datafusion-doc", "quote", @@ -1698,9 +1677,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f32edb8ba12f08138f86c09b80fae3d4a320551262fa06b91d8a8cb3065a5b" +checksum = "ff9aa82b240252a88dee118372f9b9757c545ab9e53c0736bebab2e7da0ef1f2" dependencies = [ "arrow", "chrono", @@ -1708,7 +1687,7 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-physical-expr", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "log", "regex", @@ -1717,9 +1696,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987c5e29e96186589301b42e25aa7d11bbe319a73eb02ef8d755edc55b5b89fc" +checksum = "7d48022b8af9988c1d852644f9e8b5584c490659769a550c5e8d39457a1da0a5" dependencies = [ "ahash", "arrow", @@ -1730,7 +1709,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "parking_lot", "paste", @@ -1740,9 +1719,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de89d0afa08b6686697bd8a6bac4ba2cd44c7003356e1bce6114d5a93f94b5c" +checksum = "ae7a8abc0b4fe624000972a9b145b30b7f1b680bffaa950ea53f78d9b21c27c3" dependencies = [ "arrow", "datafusion-common", @@ -1755,9 +1734,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602d1970c0fe87f1c3a36665d131fbfe1c4379d35f8fc5ec43a362229ad2954d" +checksum = "147253ca3e6b9d59c162de64c02800973018660e13340dd1886dd038d17ac429" dependencies = [ "ahash", "arrow", @@ -1765,16 +1744,16 @@ dependencies = [ "datafusion-common", "datafusion-expr-common", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "parking_lot", ] [[package]] name = "datafusion-physical-optimizer" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b24d704b6385ebe27c756a12e5ba15684576d3b47aeca79cc9fb09480236dc32" +checksum = "689156bb2282107b6239db8d7ef44b4dab10a9b33d3491a0c74acac5e4fedd72" dependencies = [ "arrow", "datafusion-common", @@ -1790,9 +1769,9 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21d94141ea5043e98793f170798e9c1887095813b8291c5260599341e383a38" +checksum = "68253dc0ee5330aa558b2549c9b0da5af9fc17d753ae73022939014ad616fc28" dependencies = [ "ahash", "arrow", @@ -1811,7 +1790,7 @@ dependencies = [ "futures", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools 0.14.0", "log", "parking_lot", @@ -1821,9 +1800,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68cce43d18c0dfac95cacd74e70565f7e2fb12b9ed41e2d312f0fa837626b1" +checksum = "0fcad240a54d0b1d3e8f668398900260a53122d522b2102ab57218590decacd6" dependencies = [ "arrow", "datafusion-common", @@ -1838,9 +1817,9 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4e1c40a0b1896aed4a4504145c2eb7fa9b9da13c2d04b40a4767a09f076199" +checksum = "f58e83a68bb67007a8fcbf005c44cefe441270c7ee7f6dee10c0e0109b556f6d" dependencies = [ "async-trait", "datafusion-common", @@ -1852,16 +1831,16 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "52.4.0" +version = "52.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1891e5b106d1d73c7fe403bd8a265d19c3977edc17f60808daf26c2fe65ffb" +checksum = "be53e9eb55db0fbb8980bb6d87f2435b0524acf4c718ed54a57cabbb299b2ab3" dependencies = [ "arrow", "bigdecimal", "chrono", "datafusion-common", "datafusion-expr", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "regex", "sqlparser", @@ -1957,7 +1936,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2029,7 +2008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2049,9 +2028,9 @@ dependencies = [ [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "event-listener" @@ -2098,9 +2077,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fd-lock" @@ -2110,7 +2089,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2142,7 +2121,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.0", + "bitflags", "rustc_version", ] @@ -2156,6 +2135,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2201,12 +2191,12 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a32ddfc5478379cd1782bdd9d7d1411063f563e5b338fc73bafe5916451a5b9d" +checksum = "2195cc7f87e84bd695586137de99605e7e9579b26ec5e01b82960ddb4d0922f2" dependencies = [ "arrow-array", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -2438,7 +2428,7 @@ dependencies = [ "regex", "signal-hook", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2479,15 +2469,15 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror 2.0.18", + "thiserror", "unicode-bom", ] [[package]] name = "gix-bitmap" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7add20f40d060db8c9b1314d499bac6ed7480f33eb113ce3e1cf5d6ff85d989" +checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894" dependencies = [ "gix-error", ] @@ -2509,23 +2499,23 @@ dependencies = [ "gix-traverse", "gix-worktree", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-chunk" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1096b6608fbe5d27fb4984e20f992b4e76fb8c613f6acb87d07c5831b53a6959" +checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320" dependencies = [ "gix-error", ] [[package]] name = "gix-command" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849c65a609f50d02f8a2774fe371650b3384a743c79c2a070ce0da49b7fb7da" +checksum = "ae4bb9fa74c44c93f7238b08255f7f9afc158bafea4b95af665fa535352cd73c" dependencies = [ "bstr", "gix-path", @@ -2563,29 +2553,29 @@ dependencies = [ "gix-sec", "memchr", "smallvec", - "thiserror 2.0.18", + "thiserror", "unicode-bom", "winnow", ] [[package]] name = "gix-config-value" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441a300bc3645a1f45cba495b9175f90f47256ce43f2ee161da0031e3ac77c92" +checksum = "4378c53ec3db049919edf91ff76f56f28886a8b4b4a5a9dc633108d84afc3675" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bstr", "gix-path", "libc", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-credentials" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b2a34b8715e3bbd514f3d1705f5d51c4b250e5bfe506b9fb60b133c85c93d9" +checksum = "0493f25da3ce9e8f7d925e5b64dc6d0b6109c96add422a6bcada52416448147d" dependencies = [ "bstr", "gix-command", @@ -2596,14 +2586,14 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-date" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39acf819aa9fee65e4838a2eec5cb2506e47ebb89e02a5ab9918196e491571ea" +checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" dependencies = [ "bstr", "gix-error", @@ -2634,7 +2624,7 @@ dependencies = [ "gix-worktree", "imara-diff 0.1.8", "imara-diff 0.2.0", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2654,7 +2644,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2669,14 +2659,14 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-error" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e86d01da904d4a9265def43bd42a18c5e6dc7000a73af512946ba14579c9fbd" +checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" dependencies = [ "bstr", ] @@ -2698,7 +2688,7 @@ dependencies = [ "once_cell", "parking_lot", "prodash", - "thiserror 2.0.18", + "thiserror", "walkdir", "zlib-rs", ] @@ -2721,7 +2711,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2735,7 +2725,7 @@ dependencies = [ "gix-features", "gix-path", "gix-utils", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2744,7 +2734,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03e6cd88cc0dc1eafa1fddac0fb719e4e74b6ea58dd016e71125fde4a326bee" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bstr", "gix-features", "gix-path", @@ -2759,7 +2749,7 @@ dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2792,7 +2782,7 @@ version = "0.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bae54ab14e4e74d5dda60b82ea7afad7c8eb3be68283d6d5f29bd2e6d47fff7" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bstr", "filetime", "fnv", @@ -2811,7 +2801,7 @@ dependencies = [ "memmap2", "rustix 1.1.4", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2822,7 +2812,7 @@ checksum = "054fbd0989700c69dc5aa80bc66944f05df1e15aa7391a9e42aca7366337905f" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2860,7 +2850,7 @@ dependencies = [ "gix-worktree", "imara-diff 0.1.8", "nonempty", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2869,7 +2859,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea064c7595eea08fdd01c70748af747d9acc40f727b61f4c8a2145a5c5fc28c" dependencies = [ - "bitflags 2.11.0", + "bitflags", "gix-commitgraph", "gix-date", "gix-hash", @@ -2894,7 +2884,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror 2.0.18", + "thiserror", "winnow", ] @@ -2915,7 +2905,7 @@ dependencies = [ "gix-quote", "parking_lot", "tempfile", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2936,32 +2926,32 @@ dependencies = [ "memmap2", "parking_lot", "smallvec", - "thiserror 2.0.18", + "thiserror", "uluru", ] [[package]] name = "gix-packetline" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be19313dcdb7dff75a3ce2f99be00878458295bcc3b6c7f0005591597573345c" +checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6" dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-path" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c31d4373bda7fab9eb01822927b55185a378d6e1bf737e0a54c743ad806658" +checksum = "c8fd1fe596dc393b538e1d5492c5585971a9311475b3255f7b889023df208476" dependencies = [ "bstr", "gix-trace", "gix-validate", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2970,26 +2960,26 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89611f13544ca5ebeb68a502673814ef57200df60c24a61c2ce7b96f612f08b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bstr", "gix-attributes", "gix-config-value", "gix-glob", "gix-path", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-prompt" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f61f6264e1f6c5a951531fe127722c7522bc02ebda80c4528286bda4642055f" +checksum = "1de52c2570684db3eb8cebda77c1d0e3d03ddaad2d9bbb5055eba31fb9f8292a" dependencies = [ "gix-command", "gix-config-value", "parking_lot", "rustix 1.1.4", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3015,15 +3005,15 @@ dependencies = [ "gix-utils", "maybe-async", "nonempty", - "thiserror 2.0.18", + "thiserror", "winnow", ] [[package]] name = "gix-quote" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68533db71259c8776dd4e770d2b7b98696213ecdc1f5c9e3507119e274e0c578" +checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be" dependencies = [ "bstr", "gix-error", @@ -3047,7 +3037,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 2.0.18", + "thiserror", "winnow", ] @@ -3064,7 +3054,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3073,7 +3063,7 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c08f1ec5d1e6a524f8ba291c41f0ccaef64e48ed0e8cf790b3461cae45f6d3d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bstr", "gix-commitgraph", "gix-date", @@ -3099,16 +3089,16 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-sec" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf82ae037de9c62850ce67beaa92ec8e3e17785ea307cdde7618edc215603b4f" +checksum = "283f4a746c9bde8550be63e6f961ff4651f412ca12666e8f5615f39464960ab9" dependencies = [ - "bitflags 2.11.0", + "bitflags", "gix-path", "libc", "windows-sys 0.61.2", @@ -3124,7 +3114,7 @@ dependencies = [ "gix-hash", "gix-lock", "nonempty", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3147,7 +3137,7 @@ dependencies = [ "gix-pathspec", "gix-worktree", "portable-atomic", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3162,7 +3152,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3171,7 +3161,7 @@ version = "21.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22227f6b203f511ff451c33c89899e87e4f571fc596b06f68e6e613a6508528" dependencies = [ - "dashmap 6.1.0", + "dashmap", "gix-fs", "libc", "parking_lot", @@ -3182,9 +3172,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" +checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" [[package]] name = "gix-transport" @@ -3202,7 +3192,7 @@ dependencies = [ "gix-quote", "gix-sec", "gix-url", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3211,7 +3201,7 @@ version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "963dc2afcdb611092aa587c3f9365e749ac0a0892ff27662dbc75f26c953fbec" dependencies = [ - "bitflags 2.11.0", + "bitflags", "gix-commitgraph", "gix-date", "gix-hash", @@ -3219,26 +3209,26 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-url" -version = "0.35.2" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28e8af3d42581190da884f013caf254d2fd4d6ab102408f08d21bfa11de6c8d" +checksum = "1a61ead12e33fa52ae92b207ee27554f646a8e7a3dad8b78da1582ec91eda0a6" dependencies = [ "bstr", "gix-path", "percent-encoding", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "gix-utils" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" +checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" dependencies = [ "bstr", "fastrand", @@ -3247,9 +3237,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" +checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ "bstr", ] @@ -3287,7 +3277,7 @@ dependencies = [ "gix-path", "gix-worktree", "io-close", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3316,9 +3306,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -3326,7 +3316,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3388,6 +3378,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heapless" version = "0.8.0" @@ -3427,10 +3423,10 @@ dependencies = [ "indicatif 0.17.11", "libc", "log", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "ureq", "windows-sys 0.60.2", ] @@ -3509,9 +3505,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -3524,7 +3520,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3532,16 +3527,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3607,12 +3601,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3620,9 +3615,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3633,9 +3628,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3647,15 +3642,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3667,15 +3662,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3711,9 +3706,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3751,12 +3746,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -3805,9 +3800,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -3821,7 +3816,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3862,9 +3857,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3872,14 +3867,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -3903,27 +3898,32 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", ] [[package]] @@ -3957,19 +3957,21 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "jsonb" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a901f06163d352fbe41c3c2ff5e08b75330a003cc941e988fb501022f5421e6" +checksum = "eb98fb29636087c40ad0d1274d9a30c0c1e83e03ae93f6e7e89247b37fcc6953" dependencies = [ "byteorder", "ethnum", @@ -3979,10 +3981,10 @@ dependencies = [ "nom 8.0.0", "num-traits", "ordered-float", - "rand 0.9.2", - "ryu", + "rand 0.9.4", "serde", "serde_json", + "zmij", ] [[package]] @@ -3996,9 +3998,9 @@ dependencies = [ [[package]] name = "lance" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c5ce428fda0721f5c48bfde17a1921c4da2d2142b2f46a16c89abf5fce8003" +checksum = "efe6c3ddd79cdfd2b7e1c23cafae52806906bc40fbd97de9e8cf2f8c7a75fc04" dependencies = [ "arrow", "arrow-arith", @@ -4016,7 +4018,7 @@ dependencies = [ "bytes", "chrono", "crossbeam-skiplist", - "dashmap 6.1.0", + "dashmap", "datafusion", "datafusion-expr", "datafusion-functions", @@ -4045,7 +4047,7 @@ dependencies = [ "pin-project", "prost", "prost-types", - "rand 0.9.2", + "rand 0.9.4", "roaring", "semver", "serde", @@ -4062,9 +4064,9 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fdaf99863fa0d631e422881e88be4837d8b82f36a87143d723a9d285acec4b" +checksum = "5d9f5d95bdda2a2b790f1fb8028b5b6dcf661abeb3133a8bca0f3d24b054af87" dependencies = [ "arrow-array", "arrow-buffer", @@ -4079,14 +4081,14 @@ dependencies = [ "half", "jsonb", "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "lance-bitpacking" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b1634d38d94e8ab86fbcf238ac82dc8a5f72a4a6a90525f29899772e7cc7f" +checksum = "f827d6ab9f8f337a9509d5ad66a12f3314db8713868260521c344ef6135eb4e4" dependencies = [ "arrayref", "paste", @@ -4095,9 +4097,9 @@ dependencies = [ [[package]] name = "lance-core" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977c29f4e48c201c2806fe6ae117b65d0287eda236acd07357b556a54b0d5c5a" +checksum = "0f1e25df6a79bf72ee6bcde0851f19b1cd36c5848c1b7db83340882d3c9fdecb" dependencies = [ "arrow-array", "arrow-buffer", @@ -4120,7 +4122,7 @@ dependencies = [ "object_store", "pin-project", "prost", - "rand 0.9.2", + "rand 0.9.4", "roaring", "serde_json", "snafu 0.9.0", @@ -4134,9 +4136,9 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccc72695473f4207df4c6df3b347a63e84c32c0bc36bf42a7d86e8a7c0c67e2" +checksum = "93146de8ae720cb90edef81c2f2d0a1b065fc2f23ecff2419546f389b0fa70a4" dependencies = [ "arrow", "arrow-array", @@ -4166,9 +4168,9 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe84d76944acd834ded14d7562663af995556e0c6594f4b4ac69b0183f99c1a" +checksum = "ccec8ce4d8e0a87a99c431dab2364398029f2ffb649c1a693c60c79e05ed30dd" dependencies = [ "arrow", "arrow-array", @@ -4178,7 +4180,7 @@ dependencies = [ "futures", "half", "hex", - "rand 0.9.2", + "rand 0.9.4", "rand_distr 0.5.1", "rand_xoshiro", "random_word", @@ -4186,9 +4188,9 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1007242188e5d53c98717e7f2cb340dc80eb9c94c2b935587598919b3a36bd" +checksum = "5c1aec0bbbac6bce829bc10f1ba066258126100596c375fb71908ecf11c2c2a5" dependencies = [ "arrow-arith", "arrow-array", @@ -4214,7 +4216,7 @@ dependencies = [ "prost", "prost-build", "prost-types", - "rand 0.9.2", + "rand 0.9.4", "snafu 0.9.0", "strum", "tokio", @@ -4225,9 +4227,9 @@ dependencies = [ [[package]] name = "lance-file" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f80088e418941f39cf5599d166ae1a6ef498cc2d967652a0692477d4871a9277" +checksum = "14a8c548804f5b17486dc2d3282356ed1957095a852780283bc401fdd69e9075" dependencies = [ "arrow-arith", "arrow-array", @@ -4259,9 +4261,9 @@ dependencies = [ [[package]] name = "lance-index" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0011daf1ddde99becffd2ae235ad324576736a526c54ffbc4d7e583872f1215" +checksum = "2da212f0090ea59f79ac3686660f596520c167fe1cb5f408900cf71d215f0e03" dependencies = [ "arrow", "arrow-arith", @@ -4275,6 +4277,7 @@ dependencies = [ "bitpacking", "bitvec", "bytes", + "chrono", "crossbeam-queue", "datafusion", "datafusion-common", @@ -4305,7 +4308,7 @@ dependencies = [ "prost", "prost-build", "prost-types", - "rand 0.9.2", + "rand 0.9.4", "rand_distr 0.5.1", "rangemap", "rayon", @@ -4324,9 +4327,9 @@ dependencies = [ [[package]] name = "lance-io" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa8a74e93753d19a27ce3adaeb99e31227df13ad5926dd43572be76b43dd284" +checksum = "41d958eb4b56f03bbe0f5f85eb2b4e9657882812297b6f711f201ffc995f259f" dependencies = [ "arrow", "arrow-arith", @@ -4352,7 +4355,7 @@ dependencies = [ "path_abs", "pin-project", "prost", - "rand 0.9.2", + "rand 0.9.4", "serde", "snafu 0.9.0", "tempfile", @@ -4363,9 +4366,9 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2d8da8f6b8dd37ab3b8199896ee265817f86232e3727c0b0eeb3c9093b64d9" +checksum = "0285b70da35def7ed95e150fae1d5308089554e1290470403ed3c50cb235bc5e" dependencies = [ "arrow-array", "arrow-buffer", @@ -4376,28 +4379,29 @@ dependencies = [ "lance-arrow", "lance-core", "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "lance-namespace" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f176e427d9c35938d8a7097876114bc35dfd280b06077779753f2effe3e86aab" +checksum = "5f78e2a828b654e062a495462c6e3eb4fcf0e7e907d761b8f217fc09ccd3ceac" dependencies = [ "arrow", "async-trait", "bytes", "lance-core", "lance-namespace-reqwest-client", + "serde", "snafu 0.9.0", ] [[package]] name = "lance-namespace-impls" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663c32086ecfab311acb0813c65a4bb352a5b648ccf8b513c24697ce8d412039" +checksum = "a2392314f3da38f00d166295e44244208a65ccfc256e274fa8631849fc3f4d94" dependencies = [ "arrow", "arrow-ipc", @@ -4414,7 +4418,7 @@ dependencies = [ "lance-table", "log", "object_store", - "rand 0.9.2", + "rand 0.9.4", "serde_json", "snafu 0.9.0", "tokio", @@ -4423,9 +4427,9 @@ dependencies = [ [[package]] name = "lance-namespace-reqwest-client" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9008f9825066088178c10599130c8bb0b9c79a39a479e8c51201620c43864a" +checksum = "ee2e48de899e2931afb67fcddd0a08e439bf5d8b6ea2a2ed9cb8f4df669bd5cc" dependencies = [ "reqwest 0.12.28", "serde", @@ -4436,9 +4440,9 @@ dependencies = [ [[package]] name = "lance-table" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa189b3081481a97b64cf1161297947a63b8adb941b1950989d0269858703a43" +checksum = "3df9c4adca3eb2074b3850432a9fb34248a3d90c3d6427d158b13ff9355664ee" dependencies = [ "arrow", "arrow-array", @@ -4460,7 +4464,7 @@ dependencies = [ "prost", "prost-build", "prost-types", - "rand 0.9.2", + "rand 0.9.4", "rangemap", "roaring", "semver", @@ -4475,22 +4479,22 @@ dependencies = [ [[package]] name = "lance-testing" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a6f4ab0788ee82893bac5de4ff0d0d88bba96de87db4b6e18b1883616d4dbe" +checksum = "7ed7119bdd6983718387b4ac44af873a165262ca94f181b104cd6f97912eb3bf" dependencies = [ "arrow-array", "arrow-schema", "lance-arrow", "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "lancedb" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79dd30fddeb0f21d090c502f937aa4862b5a352e74c47e35c6c76fec9f31534" +checksum = "ce0f4d7f739dc30608fe8b202cbb40986c2937e1a5a189f98fb06d7b8543156a" dependencies = [ "ahash", "arrow", @@ -4535,7 +4539,7 @@ dependencies = [ "num-traits", "object_store", "pin-project", - "rand 0.9.2", + "rand 0.9.4", "regex", "semver", "serde", @@ -4625,9 +4629,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -4637,21 +4641,21 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -4673,9 +4677,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -4721,16 +4725,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "lsp-types" -version = "0.94.1" +name = "ls-types" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +checksum = "896e16b8e17d8732b9efe4d5b66cb0cc162b3023a2d8122f2aea6f7f185e0a67" dependencies = [ - "bitflags 1.3.2", + "bitflags", + "fluent-uri", + "percent-encoding", "serde", "serde_json", - "serde_repr", - "url", ] [[package]] @@ -4890,9 +4894,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -5033,7 +5037,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -5070,7 +5074,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5094,9 +5098,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -5148,7 +5152,7 @@ dependencies = [ "itertools 0.14.0", "parking_lot", "percent-encoding", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -5183,11 +5187,11 @@ checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" [[package]] name = "onig" -version = "6.5.1" +version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "once_cell", "onig_sys", @@ -5195,9 +5199,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.9.1" +version = "69.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" dependencies = [ "cc", "pkg-config", @@ -5217,9 +5221,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -5235,9 +5239,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -5328,7 +5332,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", ] @@ -5352,18 +5356,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", @@ -5376,17 +5380,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5402,18 +5400,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -5528,7 +5526,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -5544,13 +5542,13 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -5609,9 +5607,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5620,9 +5618,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5673,7 +5671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -5683,7 +5681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -5704,7 +5702,7 @@ dependencies = [ "ahash", "brotli", "paste", - "rand 0.9.2", + "rand 0.9.4", "unicase", ] @@ -5722,9 +5720,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -5757,16 +5755,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -5777,7 +5775,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -5864,7 +5862,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -5876,9 +5874,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -5905,7 +5903,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -5930,9 +5928,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -5950,9 +5948,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -5969,11 +5967,11 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5982,18 +5980,18 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -6019,9 +6017,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -6029,9 +6027,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -6045,7 +6043,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6056,9 +6054,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -6078,7 +6076,7 @@ version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "clipboard-win", "fd-lock", @@ -6176,7 +6174,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6210,7 +6208,7 @@ dependencies = [ "clap", "colored", "crossbeam-channel", - "dashmap 6.1.0", + "dashmap", "dirs", "flate2", "futures", @@ -6226,7 +6224,7 @@ dependencies = [ "owo-colors", "rayon", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "rustyline", "serde", "serde_bytes", @@ -6237,24 +6235,23 @@ dependencies = [ "streaming-iterator", "tempfile", "tokio", - "tokio-tungstenite 0.29.0", - "tower 0.5.3", - "tower-lsp", + "tokio-tungstenite", + "tower", + "tower-lsp-server", "tracing", "tracing-subscriber", "tree-sitter", "tree-sitter-c", "tree-sitter-python", "tree-sitter-rust", - "url", "walkdir", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "seq-macro" @@ -6308,7 +6305,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -6352,15 +6349,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -6371,9 +6368,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -6388,7 +6385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6409,7 +6406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6436,9 +6433,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -6456,9 +6453,19 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simdutf8" @@ -6474,9 +6481,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "sketches-ddsketch" @@ -6548,7 +6555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6726,7 +6733,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6793,7 +6800,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 2.0.18", + "thiserror", "time", "uuid", "winapi", @@ -6909,16 +6916,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", -] - -[[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", + "windows-sys 0.61.2", ] [[package]] @@ -6927,18 +6925,7 @@ 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", + "thiserror-impl", ] [[package]] @@ -7012,9 +6999,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -7056,7 +7043,7 @@ dependencies = [ "monostate", "onig", "paste", - "rand 0.9.2", + "rand 0.9.4", "rayon", "rayon-cond", "regex", @@ -7064,7 +7051,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -7072,9 +7059,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -7089,9 +7076,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -7119,18 +7106,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.28.0", -] - [[package]] name = "tokio-tungstenite" version = "0.29.0" @@ -7140,7 +7115,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.29.0", + "tungstenite", ] [[package]] @@ -7156,20 +7131,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower" version = "0.5.3" @@ -7193,7 +7154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.0", + "bitflags", "bytes", "futures-core", "futures-util", @@ -7204,7 +7165,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", ] @@ -7216,39 +7177,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "tower-lsp" -version = "0.20.0" +name = "tower-lsp-server" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +checksum = "2f0e711655c89181a6bc6a2cc348131fcd9680085f5b06b6af13427a393a6e72" dependencies = [ - "async-trait", - "auto_impl", "bytes", - "dashmap 5.5.3", + "dashmap", "futures", "httparse", - "lsp-types", + "ls-types", "memchr", "serde", "serde_json", "tokio", "tokio-util", - "tower 0.4.13", - "tower-lsp-macros", + "tower", "tracing", ] -[[package]] -name = "tower-lsp-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "tower-service" version = "0.3.3" @@ -7319,9 +7266,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a6592b1aec0109df37b6bafea77eb4e61466e37b0a5a98bef4f89bfb81b7a2" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" dependencies = [ "cc", "regex", @@ -7333,9 +7280,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3aad8f0129083a59fe8596157552d2bb7148c492d44c21558d68ca1c722707" +checksum = "a9b2eb57a55fed6b00812912e730b7a275cf4fe98bfd6a5d76263d4438371728" dependencies = [ "cc", "tree-sitter-language", @@ -7359,9 +7306,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f715f73a0687261ddb686f0d64a1e5af57bd199c4d12be5fdda6676ce1885bf9" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" dependencies = [ "cc", "tree-sitter-language", @@ -7373,23 +7320,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.29.0" @@ -7401,9 +7331,9 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -7412,14 +7342,14 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uluru" @@ -7468,9 +7398,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -7531,15 +7461,8 @@ dependencies = [ "idna", "percent-encoding", "serde", - "serde_derive", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8-ranges" version = "1.0.5" @@ -7560,9 +7483,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7615,11 +7538,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -7628,14 +7551,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -7646,23 +7569,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7670,9 +7589,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -7683,9 +7602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -7707,7 +7626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -7731,17 +7650,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -7759,9 +7678,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -7772,14 +7691,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -7806,7 +7725,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7885,15 +7804,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -7930,21 +7840,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -7978,12 +7873,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -7996,12 +7885,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -8014,12 +7897,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -8044,12 +7921,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -8062,12 +7933,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -8080,12 +7945,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -8098,12 +7957,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -8134,6 +7987,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -8153,7 +8012,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -8183,8 +8042,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -8203,7 +8062,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -8215,9 +8074,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -8236,9 +8095,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8247,9 +8106,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -8259,18 +8118,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -8279,18 +8138,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -8306,9 +8165,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -8317,9 +8176,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -8328,9 +8187,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index fd3ce2f..8f03437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ tokio = { version = "1.50.0", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } serde_bytes = "0.11" -clap = { version = "4.6", features = ["derive"] } +clap = { version = "4.6", features = ["derive", "env"] } anyhow = "1.0" walkdir = "2.5" indicatif = "0.18.4" @@ -51,8 +51,7 @@ anstream = "1.0" # Auto TTY/NO_COLOR handling; print!/println! and Write owo-colors = { version = "4", features = ["supports-colors"] } # .red().bold(), optional detection helpers [web:27] gix = { version = "0.81", features = ["blocking-network-client", "worktree-mutation", "blocking-http-transport-curl-openssl"] } model2vec-rs = "0.1.4" -tower-lsp = "0.20" # Language Server Protocol framework for LSP server -url = "2.5" # URL parsing for file URIs in LSP +tower-lsp-server = "0.23" # Language Server Protocol framework for LSP server similar = { version = "2.7", features = ["text"] } # Proper diff algorithm for commit diffs shlex = "1.3" # Shell-like parsing for quoted strings in CLI reqwest = { version = "0.13", features = ["blocking", "gzip"] } # HTTP client for manifest downloads diff --git a/docs/schema.md b/docs/schema.md index 963b005..8b9fb9c 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -363,6 +363,7 @@ branch_name (Utf8, NOT NULL) - Branch name (e.g., "main", "origin/de tip_commit (Utf8, NOT NULL) - Commit SHA at the tip when indexed (40-char hex) indexed_at (Int64, NOT NULL) - Unix timestamp of when branch was last indexed remote (Utf8, nullable) - Remote name if tracking branch (e.g., "origin") +repo_path (Utf8, nullable) - Canonical repository identity path for multi-repo scoping ``` **Purpose:** @@ -370,6 +371,7 @@ remote (Utf8, nullable) - Remote name if tracking branch (e.g., - Enables efficient multi-branch indexing by skipping already-current branches - Supports both local branches (e.g., "main") and remote-tracking branches (e.g., "origin/develop") - Stores indexing timestamp for freshness tracking +- Scopes branch references per repository in multi-repository shared database mode **Use Cases:** - Multi-branch indexing: `semcode-index --branches main,develop,feature-x` @@ -381,6 +383,7 @@ remote (Utf8, nullable) - Remote name if tracking branch (e.g., - BTree on `branch_name` (primary lookup by branch name) - BTree on `tip_commit` (find branches at specific commits) - BTree on `remote` (filter by remote) +- BTree on `repo_path` (fast repo-scoped branch query pruning) --- @@ -509,10 +512,20 @@ The git_commits table stores git commit history with enhanced metadata: - **Walk-back Symbol Extraction**: Fast O(modified_lines × 50) algorithm identifies changed functions, types, and macros - **Dual-file Analysis**: Extracts symbols from both additions and deletions - **Enhanced Hunk Headers**: Git-style `@@ ... @@ symbol` format for better context -- **Commit Traversal**: Parent relationships enable git history analysis -- **Tag Parsing**: Extracts structured metadata from commit messages (Signed-off-by, Reviewed-by, etc.) - **Use Cases**: Commit analysis, code evolution tracking, review assistance, git history search +### Multi-Repository Shared Database Support & Database Locality + +**Overview:** +When operating `semcode` with a shared central database (e.g., `SEMCODE_DB=~/.cache/semcode.db` or an external directory shared across multiple trees of a project), branch records are isolated by repository identity (`repo_path`) to prevent cross-repository branch pollution. + +**Database Locality Modes:** +- **Local Database Mode**: When the database resides inside a repository directory (such as `.git/` or `.semcode.db`), branch store queries use the fallback filter `(repo_path = '{repo_identity}' OR repo_path IS NULL)`. This ensures position-independent relocatability and full backward compatibility with unscoped legacy records while matching current repository branches. +- **Shared / Central Database Mode**: When the database resides in a central location outside all repositories, queries strictly filter `repo_path = '{repo_identity}'` to ensure clean separation between repositories. +- **Worktree Identity Consolidation**: Linked Git worktrees automatically resolve to their primary repository identity (`common_dir`), sharing the same branch index without duplication. +- **On-Write Lazy Migration**: Older database tables are lazily migrated on write operations (`record_branch_indexed`) by adding the `repo_path` column JIT without interrupting read queries. +- **Automatic Garbage Collection**: `DatabaseManager::garbage_collect()` automatically purges branch records corresponding to deleted filesystem paths in shared mode, and purges legacy unscoped `NULL` records when running in standalone database mode. + ## Query Patterns ### Basic Lookups diff --git a/docs/shared-db.md b/docs/shared-db.md new file mode 100644 index 0000000..ef1fd24 --- /dev/null +++ b/docs/shared-db.md @@ -0,0 +1,104 @@ +# Semcode Shared Database Architecture & Usage Guide + +`semcode` supports operating with a **Shared Central Database** shared across multiple repository trees, subsystem forks, and worktrees of the same project (such as Linux kernel trees: `linux-mainline`, `bpf-next`, `net-next`, `stable`). + +This document explains why and how Shared Database mode works, how cross-repository data deduplication and branch isolation are maintained, and how to use it. + +--- + +## 1. Why Use Shared Database Mode? + +When working on large projects like the Linux kernel, developers frequently maintain multiple repository checkouts: +- Mainline (`linux-mainline`) +- Subsystem trees (`bpf-next`, `net-next`, `sound`) +- Stable release trees (`linux-6.12.y`, `linux-6.1.y`) +- Multiple local Git worktrees (`git worktree add ...`) + +Because these trees share 90%+ of their Git commit graph and source code, indexing each tree into a separate database creates massive data duplication and wastes indexing time. + +### Key Benefits of Shared Database Mode: +1. **Near-Instant Incremental Indexing Across Trees**: When you index a new subsystem tree or worktree into a shared database, `semcode` reuses all existing commit objects, file parsing results, and symbol AST definitions already indexed from other trees. +2. **Dramatic Storage Savings (50-80%)**: Content-addressed tables (functions, types, commit diffs, Blake3 content shards) are globally deduplicated across all repositories. +3. **Clean Branch Isolation**: Branch references (`indexed_branches`) are scoped per repository tree, preventing branch name collisions (e.g. `main` or `v6.12` existing in different trees). +4. **Git Worktree Consolidation**: Linked worktrees automatically map to their primary repository identity (`common_dir`), sharing branch references seamlessly without duplicate worktree records. + +--- + +## 2. Core Architecture + +### Global Content Deduplication vs. Relational Scoping + +`semcode` divides its database tables into two distinct categories: + +#### 1. Global Content Tables +* **Tables**: `functions`, `types`, `processed_files`, `git_commits`, `commit_vectors`, `content_0..15` +* **Strategy**: **Globally Deduplicated** +* **Details**: Keyed on SHA-1 file blob hashes, Blake3 content hashes, or Git commit SHAs. Identical function definitions or commit diffs across different repository trees are stored **once**. + +#### 2. Scoped Identity Tables +* **Tables**: `indexed_branches` +* **Strategy**: **Repo-Scoped** +* **Details**: Stores `repo_path` (canonical repository identity path) to isolate branch references (`main`, `master`, `v6.12`) per repository checkout. + +--- + +## 3. Database Locality Modes (`Local` vs `Shared`) + +`semcode` automatically detects the database locality mode based on where the database file resides: + +### 1. Local Database Mode (`.semcode.db`) +* **Trigger**: The database file is located inside a repository directory (e.g., `/home/user/linux/.semcode.db` or `/home/user/linux/.git/semcode.db`). +* **Behavior**: + * Records `repo_path = NULL` or matches dynamically using `(repo_path = '{repo_identity}' OR repo_path IS NULL)`. + * **100% Relocatable**: Moving or renaming the repository directory on disk will **never** invalidate database records or break future queries. + +### 2. Shared Database Mode (`SEMCODE_DB`) +* **Trigger**: The database file resides in a central directory outside all repositories (e.g., `SEMCODE_DB=~/.cache/semcode.db` or `/var/shared/semcode.db`). +* **Behavior**: + * Enforces strict SQL filtering on branch queries: `repo_path = '{repo_identity}'`. + * Ensures complete isolation between different repository checkouts. + +--- + +## 4. Git Worktree Identity Consolidation + +Linked Git worktrees created via `git worktree add` share the same `.git` commit graph and refs as the parent repository. + +`semcode` automatically resolves worktrees to their parent repository's canonical identity (`common_dir`): +* Primary Checkout (`/home/user/linux`): Identity path `~/linux/.git` +* Linked Worktree (`/home/user/linux-feature-wt`): Identity path `~/linux/.git` + +Because both resolve to `~/linux/.git`, worktrees transparently share the same branch index without creating redundant branch records. + +--- + +## 5. Automatic Garbage Collection + +In Shared Database mode, deleting a repository directory from the filesystem leaves behind stale `indexed_branches` records for that path. + +`semcode-index` automatically invokes `DatabaseManager::garbage_collect()` at the end of every indexing run: +* **In Shared DB Mode**: Scans `indexed_branches` for `repo_path` entries whose paths no longer exist on the local filesystem and purges them via `table.delete()`. +* **In Local DB Mode**: Purges legacy unscoped `NULL` branch records if the database is running standalone outside of Git repositories. + +--- + +## 6. How to Use Shared Database Mode + +To use a shared central database across all your projects, set the `SEMCODE_DB` environment variable: + +```bash +# 1. Index your mainline checkout into the central database +export SEMCODE_DB=~/.cache/semcode.db +cd ~/projects/linux-mainline +semcode-index -s . + +# 2. Index a subsystem fork into the same central database (runs near-instantly!) +cd ~/projects/bpf-next +semcode-index -s . + +# 3. Query within your current checkout (queries are automatically scoped to the active repository you are in) +cd ~/projects/bpf-next +semcode find_function bpf_prog_load +``` + +You can also add `export SEMCODE_DB=~/.cache/semcode.db` to your `~/.bashrc` or `~/.zshrc` to make all `semcode`, `semcode-index`, and MCP server instances automatically use the shared central database! diff --git a/scripts/nomic2vec.py b/scripts/nomic2vec.py index 0025d4c..1977ab5 100755 --- a/scripts/nomic2vec.py +++ b/scripts/nomic2vec.py @@ -493,6 +493,17 @@ def tokens(self, value): # Allow setting but always return self pass + # NomicBertEmbedding doesn't implement {get,set}_input_embeddings, but + # it is required for distill_from_model. See: + # https://huggingface.co/nomic-ai/nomic-bert-2048/discussions/22 + import types + def get_input_embeddings(self): + return self.embeddings.word_embeddings + def set_input_embeddings(self, value): + self.embeddings.word_embeddings = value + model.get_input_embeddings = types.MethodType(get_input_embeddings, model) + model.set_input_embeddings = types.MethodType(set_input_embeddings, model) + try: # Get full stack trace to see where the error is coming from import traceback diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 675faf6..68b50f0 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -8,7 +8,7 @@ tokenizers>=0.13.0 numpy>=1.24.0 requests>=2.25.0 tqdm>=4.60.0 -model2vec>=0.1.0 +model2vec[distill]>=0.1.0 sentence-transformers>=2.2.0 sentencepiece>=0.1.97 einops>=0.8.1 diff --git a/src/bin/index.rs b/src/bin/index.rs index f1f8f17..c5995b1 100644 --- a/src/bin/index.rs +++ b/src/bin/index.rs @@ -1080,30 +1080,30 @@ async fn main() -> Result<()> { total_emails_all_archives ); - // Optimize before creating FTS indices so that compaction - // does not orphan the index data that was just built. if total_new_emails > 0 { - match db_manager.check_optimization_health().await { - Ok((needs_optimization, message)) => { - if needs_optimization { - println!("\n{}", message); - match db_manager.optimize_database().await { - Ok(_) => println!("Database optimization completed successfully"), - Err(e) => error!("Failed to optimize database: {}", e), - } - } else { - println!("\n{}", message); - } - } - Err(e) => { - error!("Failed to check database health: {}", e); - } + // Compact only lore tables. The full optimize_database() + // method processes every table in the database including + // code-index tables and content shards that a lore run + // never touches. On memory-constrained systems the + // combined working set triggers the OOM killer. + println!("\nCompacting lore tables..."); + match db_manager.compact_lore_tables().await { + Ok(_) => println!("Lore table compaction completed successfully"), + Err(e) => error!("Failed to compact lore tables: {}", e), } - println!("\nCreating FTS indices for lore table..."); - match db_manager.create_lore_fts_indices().await { - Ok(_) => println!("FTS indices created successfully"), - Err(e) => eprintln!("Warning: Failed to create FTS indices: {}", e), + // Create FTS indices on first run; merge new rows on + // subsequent runs. LanceDB's native FTS engine serves + // unindexed rows via brute-force fallback, so queries + // remain correct before optimize completes. + println!("\nUpdating FTS indices for lore table..."); + match db_manager.ensure_lore_fts_indices().await { + Ok(_) => {} + Err(e) => eprintln!("Warning: Failed to ensure FTS indices: {}", e), + } + match db_manager.optimize_lore_fts_indices().await { + Ok(_) => println!("FTS indices updated successfully"), + Err(e) => eprintln!("Warning: Failed to optimize FTS indices: {}", e), } } @@ -1248,30 +1248,21 @@ async fn main() -> Result<()> { } } - // Optimize before creating FTS indices so that compaction - // does not orphan the index data that was just built. if total_new_emails > 0 { - match db_manager.check_optimization_health().await { - Ok((needs_optimization, message)) => { - if needs_optimization { - println!("\n{}", message); - match db_manager.optimize_database().await { - Ok(_) => println!("Database optimization completed successfully"), - Err(e) => error!("Failed to optimize database: {}", e), - } - } else { - println!("\n{}", message); - } - } - Err(e) => { - error!("Failed to check database health: {}", e); - } + println!("\nCompacting lore tables..."); + match db_manager.compact_lore_tables().await { + Ok(_) => println!("Lore table compaction completed successfully"), + Err(e) => error!("Failed to compact lore tables: {}", e), } - println!("\nCreating FTS indices for lore table..."); - match db_manager.create_lore_fts_indices().await { - Ok(_) => println!("FTS indices created successfully"), - Err(e) => eprintln!("Warning: Failed to create FTS indices: {}", e), + println!("\nUpdating FTS indices for lore table..."); + match db_manager.ensure_lore_fts_indices().await { + Ok(_) => {} + Err(e) => eprintln!("Warning: Failed to ensure FTS indices: {}", e), + } + match db_manager.optimize_lore_fts_indices().await { + Ok(_) => println!("FTS indices updated successfully"), + Err(e) => eprintln!("Warning: Failed to optimize FTS indices: {}", e), } } @@ -1669,7 +1660,8 @@ async fn run_pipeline(args: Args) -> Result<()> { } } - // Optimization is now handled inside process_git_range for all modes + // Perform database garbage collection for stale branch records + let _ = db_manager.garbage_collect().await; // Drop and recreate if requested if args.drop_recreate { diff --git a/src/bin/query.rs b/src/bin/query.rs index ae8a374..7f92b47 100644 --- a/src/bin/query.rs +++ b/src/bin/query.rs @@ -49,7 +49,7 @@ struct Args { database: Option, /// Path to the git repository for git-aware queries - #[arg(long, default_value = ".")] + #[arg(long, env = "SEMCODE_GIT_REPO", default_value = ".")] git_repo: String, /// Path to local model directory (for semantic search) @@ -554,3 +554,29 @@ async fn main() -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use std::env; + + // Combined into a single test so the three cases don't race on the + // process-wide SEMCODE_GIT_REPO env var when cargo runs tests in parallel. + #[test] + fn test_git_repo_arg_env_default() { + let key = "SEMCODE_GIT_REPO"; + + env::set_var(key, "/tmp/test_repo"); + let args = Args::try_parse_from(["query"]).unwrap(); + assert_eq!(args.git_repo, "/tmp/test_repo"); + + env::set_var(key, "/tmp/env_repo"); + let args = Args::try_parse_from(["query", "--git-repo", "/tmp/arg_repo"]).unwrap(); + assert_eq!(args.git_repo, "/tmp/arg_repo"); + + env::remove_var(key); + let args = Args::try_parse_from(["query"]).unwrap(); + assert_eq!(args.git_repo, "."); + } +} diff --git a/src/bin/semcode-lsp.rs b/src/bin/semcode-lsp.rs index d8b5c8c..69d57c4 100644 --- a/src/bin/semcode-lsp.rs +++ b/src/bin/semcode-lsp.rs @@ -4,10 +4,9 @@ use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::Arc; use tokio::sync::Mutex; -use tower_lsp::jsonrpc::Result as LspResult; -use tower_lsp::lsp_types::*; -use tower_lsp::{Client, LanguageServer, LspService, Server}; -use url::Url; +use tower_lsp_server::jsonrpc::Result as LspResult; +use tower_lsp_server::ls_types::*; +use tower_lsp_server::{Client, LanguageServer, LspService, Server}; use semcode::{database_utils, DatabaseManager}; @@ -33,7 +32,7 @@ impl SemcodeLspBackend { } } - async fn ensure_database_connection(&self, workspace_uri: Option<&Url>) -> Result<()> { + async fn ensure_database_connection(&self, workspace_uri: Option<&Uri>) -> Result<()> { let mut db = self.database.lock().await; if db.is_some() { return Ok(()); @@ -41,6 +40,11 @@ impl SemcodeLspBackend { // Try to determine database path and git repo path from config or workspace let config = self.config.lock().await; + let env_db = std::env::var("SEMCODE_DB") + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + let (db_path, git_repo_path) = if let Some(path) = &config.database_path { // Custom database path provided let git_repo = std::env::current_dir() @@ -48,11 +52,18 @@ impl SemcodeLspBackend { .and_then(|p| p.to_str().map(|s| s.to_string())) .unwrap_or_else(|| ".".to_string()); (path.clone(), git_repo) + } else if let Some(env_path) = env_db { + // SEMCODE_DB environment variable + let git_repo = std::env::current_dir() + .ok() + .and_then(|p| p.to_str().map(|s| s.to_string())) + .unwrap_or_else(|| ".".to_string()); + (env_path, git_repo) } else if let Some(uri) = workspace_uri { // Use workspace directory (process_database_path will add .semcode.db) let workspace_path = uri .to_file_path() - .map_err(|_| anyhow::anyhow!("Failed to convert workspace URI to file path"))?; + .ok_or_else(|| anyhow::anyhow!("Failed to convert workspace URI to file path"))?; let workspace_str = workspace_path .to_str() .ok_or_else(|| anyhow::anyhow!("Invalid workspace path"))? @@ -163,7 +174,7 @@ impl SemcodeLspBackend { drop(repo_path_guard); // Convert absolute file path to URI - let file_uri = Url::from_file_path(&absolute_path).ok()?; + let file_uri = Uri::from_file_path(&absolute_path)?; // Create position (LSP uses 0-based line numbers) let position = Position { @@ -257,7 +268,7 @@ impl SemcodeLspBackend { drop(repo_path_guard); // Convert to URI - if let Ok(file_uri) = Url::from_file_path(&absolute_path) { + if let Some(file_uri) = Uri::from_file_path(&absolute_path) { let position = Position { line: line_start.saturating_sub(1), character: 0, @@ -322,19 +333,24 @@ impl SemcodeLspBackend { } } -#[tower_lsp::async_trait] impl LanguageServer for SemcodeLspBackend { async fn initialize(&self, params: InitializeParams) -> LspResult { - // Try to establish database connection - let _ = self - .ensure_database_connection(params.root_uri.as_ref()) - .await; + // Try to establish database connection using workspace folders (preferred) or root_uri (legacy) + let workspace_uri = params + .workspace_folders + .as_ref() + .and_then(|folders| folders.first()) + .map(|f| &f.uri); + #[allow(deprecated)] + let workspace_uri = workspace_uri.or(params.root_uri.as_ref()); + let _ = self.ensure_database_connection(workspace_uri).await; Ok(InitializeResult { server_info: Some(ServerInfo { name: "semcode-lsp".to_string(), version: Some("0.1.0".to_string()), }), + offset_encoding: None, capabilities: ServerCapabilities { definition_provider: Some(OneOf::Left(true)), references_provider: Some(OneOf::Left(true)), @@ -360,7 +376,11 @@ impl LanguageServer for SemcodeLspBackend { let position = ¶ms.text_document_position_params.position; // Get the document text to extract the function name - let document_text = match std::fs::read_to_string(uri.path()) { + let file_path = match uri.to_file_path() { + Some(p) => p.into_owned(), + None => return Ok(None), + }; + let document_text = match std::fs::read_to_string(&file_path) { Ok(text) => text, Err(_) => return Ok(None), }; @@ -385,7 +405,11 @@ impl LanguageServer for SemcodeLspBackend { let position = ¶ms.text_document_position.position; // Get the document text to extract the function name - let document_text = match std::fs::read_to_string(uri.path()) { + let file_path = match uri.to_file_path() { + Some(p) => p.into_owned(), + None => return Ok(None), + }; + let document_text = match std::fs::read_to_string(&file_path) { Ok(text) => text, Err(_) => return Ok(None), }; diff --git a/src/bin/semcode-mcp.rs b/src/bin/semcode-mcp.rs index 3ca0666..c8d4079 100644 --- a/src/bin/semcode-mcp.rs +++ b/src/bin/semcode-mcp.rs @@ -1814,7 +1814,7 @@ struct Args { database: Option, /// Path to the git repository for git-aware queries - #[arg(long, default_value = ".")] + #[arg(long, env = "SEMCODE_GIT_REPO", default_value = ".")] git_repo: String, /// Path to custom model directory (defaults to ~/.cache/semcode/models/) @@ -5680,6 +5680,28 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; + use clap::Parser; + use std::env; + + // Combined into a single test so the three cases don't race on the + // process-wide SEMCODE_GIT_REPO env var when cargo runs tests in parallel. + #[test] + fn test_git_repo_arg_env_default() { + let key = "SEMCODE_GIT_REPO"; + + env::set_var(key, "/tmp/test_repo_mcp"); + let args = Args::try_parse_from(["semcode-mcp"]).unwrap(); + assert_eq!(args.git_repo, "/tmp/test_repo_mcp"); + + env::set_var(key, "/tmp/env_repo_mcp"); + let args = + Args::try_parse_from(["semcode-mcp", "--git-repo", "/tmp/arg_repo_mcp"]).unwrap(); + assert_eq!(args.git_repo, "/tmp/arg_repo_mcp"); + + env::remove_var(key); + let args = Args::try_parse_from(["semcode-mcp"]).unwrap(); + assert_eq!(args.git_repo, "."); + } #[test] fn test_indexing_state_new() { diff --git a/src/database/branches.rs b/src/database/branches.rs index 335405f..a485740 100644 --- a/src/database/branches.rs +++ b/src/database/branches.rs @@ -11,6 +11,7 @@ use arrow::datatypes::{DataType, Field, Schema}; use futures::TryStreamExt; use lancedb::connection::Connection; use lancedb::query::{ExecutableQuery, QueryBase}; +use lancedb::index::{scalar::BTreeIndexBuilder, Index as LanceIndex}; use std::sync::Arc; /// Information about an indexed branch @@ -24,6 +25,8 @@ pub struct IndexedBranchInfo { pub indexed_at: i64, /// Remote name if this is a remote-tracking branch (e.g., "origin") pub remote: Option, + /// Repository canonical path identity (NULL for local DB, canonical path for shared DB) + pub repo_path: Option, } /// JSON-serializable version of IndexedBranchInfo @@ -33,6 +36,7 @@ pub struct IndexedBranchInfoJson { pub tip_commit: String, pub indexed_at: i64, pub remote: Option, + pub repo_path: Option, } impl From for IndexedBranchInfoJson { @@ -42,6 +46,7 @@ impl From for IndexedBranchInfoJson { tip_commit: info.tip_commit, indexed_at: info.indexed_at, remote: info.remote, + repo_path: info.repo_path, } } } @@ -53,6 +58,7 @@ impl From for IndexedBranchInfo { tip_commit: json.tip_commit, indexed_at: json.indexed_at, remote: json.remote, + repo_path: json.repo_path, } } } @@ -60,11 +66,64 @@ impl From for IndexedBranchInfo { /// Store for managing indexed branch records pub struct IndexedBranchStore { connection: Connection, + repo_identity: String, + is_local_db: bool, } impl IndexedBranchStore { - pub fn new(connection: Connection) -> Self { - Self { connection } + pub fn new(connection: Connection, repo_identity: String, is_local_db: bool) -> Self { + Self { + connection, + repo_identity, + is_local_db, + } + } + + /// Construct SQL filter string for LanceDB queries based on repository identity and database locality. + /// Returns "true" (noop) if the table schema on disk does not have a repo_path column (legacy schema). + pub fn build_sql_filter(&self, schema: &Schema) -> String { + if schema.column_with_name("repo_path").is_none() { + return "true".to_string(); + } + let escaped_rp = Self::escape_sql_literal(&self.repo_identity); + if self.is_local_db { + format!("(repo_path = '{escaped_rp}' OR repo_path IS NULL)") + } else { + format!("repo_path = '{escaped_rp}'") + } + } + + async fn open_table_with_schema(&self) -> Result<(lancedb::Table, Arc)> { + let table = self + .connection + .open_table("indexed_branches") + .execute() + .await?; + let schema = table.schema().await?; + Ok((table, schema)) + } + + async fn add_repo_path_column(&self, table: &lancedb::Table) -> Result<()> { + tracing::info!("Adding missing 'repo_path' column to indexed_branches table during write"); + table + .add_columns( + lancedb::table::NewColumnTransform::SqlExpressions(vec![( + "repo_path".into(), + "CAST(NULL AS string)".into(), + )]), + None, + ) + .await?; + + let _ = table + .create_index( + &["repo_path"], + LanceIndex::BTree(BTreeIndexBuilder::default()), + ) + .execute() + .await; + + Ok(()) } /// Get the Arrow schema for the indexed_branches table @@ -74,25 +133,32 @@ impl IndexedBranchStore { Field::new("tip_commit", DataType::Utf8, false), Field::new("indexed_at", DataType::Int64, false), Field::new("remote", DataType::Utf8, true), + Field::new("repo_path", DataType::Utf8, true), ])) } + fn escape_sql_literal(val: &str) -> String { + val.replace("'", "''") + } + /// Record that a branch has been indexed at a specific commit pub async fn record_branch_indexed(&self, info: &IndexedBranchInfo) -> Result<()> { - // First, remove any existing record for this branch + // First, remove any existing record for this branch under current repo scoping self.remove_branch(&info.branch_name).await?; - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; + let (table, schema) = self.open_table_with_schema().await?; + + // Migrate the schema to support scoping if writing to an older version + if schema.column_with_name("repo_path").is_none() { + self.add_repo_path_column(&table).await?; + } // Build arrays for each column let mut branch_name_builder = StringBuilder::new(); let mut tip_commit_builder = StringBuilder::new(); let mut indexed_at_builder = arrow::array::Int64Builder::new(); let mut remote_builder = StringBuilder::new(); + let mut repo_path_builder = StringBuilder::new(); branch_name_builder.append_value(&info.branch_name); tip_commit_builder.append_value(&info.tip_commit); @@ -101,6 +167,10 @@ impl IndexedBranchStore { Some(r) => remote_builder.append_value(r), None => remote_builder.append_null(), } + match &info.repo_path { + Some(rp) => repo_path_builder.append_value(rp), + None => repo_path_builder.append_null(), + } let batch = RecordBatch::try_from_iter(vec![ ( @@ -116,6 +186,10 @@ impl IndexedBranchStore { Arc::new(indexed_at_builder.finish()) as ArrayRef, ), ("remote", Arc::new(remote_builder.finish()) as ArrayRef), + ( + "repo_path", + Arc::new(repo_path_builder.finish()) as ArrayRef, + ), ])?; table.add(vec![batch]).execute().await?; @@ -131,45 +205,46 @@ impl IndexedBranchStore { /// Get full information about a specific branch pub async fn get_branch_info(&self, branch_name: &str) -> Result> { - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; + let (table, schema) = self.open_table_with_schema().await?; - let escaped_name = branch_name.replace("'", "''"); - let filter = format!("branch_name = '{escaped_name}'"); + let escaped_name = Self::escape_sql_literal(branch_name); + let repo_filter = self.build_sql_filter(&schema); + let sql_filter = format!("{repo_filter} AND branch_name = '{escaped_name}'"); let results = table .query() - .only_if(filter) - .limit(1) + .only_if(sql_filter) .execute() .await? .try_collect::>() .await?; - if results.is_empty() || results[0].num_rows() == 0 { + if results.is_empty() { return Ok(None); } - self.extract_record_from_batch(&results[0], 0) + // Disambiguate between legacy un-scoped (repo_path = NULL) and repo-scoped records + // by selecting the record with the most recent indexed_at timestamp. + let newest = results + .iter() + .flat_map(|batch| (0..batch.num_rows()).map(move |i| (batch, i))) + .map(|(batch, i)| self.extract_record_from_batch(batch, i)) + .collect::, _>>()? + .into_iter() + .flatten() + .max_by_key(|info| info.indexed_at); + + Ok(newest) } /// List all indexed branches pub async fn list_indexed_branches(&self) -> Result> { - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; + let (table, schema) = self.open_table_with_schema().await?; - let results = table - .query() - .execute() - .await? - .try_collect::>() - .await?; + let sql_filter = self.build_sql_filter(&schema); + let query = table.query().only_if(sql_filter); + + let results = query.execute().await?.try_collect::>().await?; let mut branches = Vec::new(); for batch in &results { @@ -197,16 +272,13 @@ impl IndexedBranchStore { /// Remove a branch record (used when branch is deleted or before updating) pub async fn remove_branch(&self, branch_name: &str) -> Result<()> { - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; + let (table, schema) = self.open_table_with_schema().await?; - let escaped_name = branch_name.replace("'", "''"); - let filter = format!("branch_name = '{escaped_name}'"); + let escaped_name = Self::escape_sql_literal(branch_name); + let repo_filter = self.build_sql_filter(&schema); + let sql_filter = format!("{repo_filter} AND branch_name = '{escaped_name}'"); - table.delete(&filter).await?; + table.delete(&sql_filter).await?; Ok(()) } @@ -227,18 +299,15 @@ impl IndexedBranchStore { /// Get all branches that point to a specific commit pub async fn get_branches_at_commit(&self, commit_sha: &str) -> Result> { - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; + let (table, schema) = self.open_table_with_schema().await?; - let escaped_sha = commit_sha.replace("'", "''"); - let filter = format!("tip_commit = '{escaped_sha}'"); + let escaped_sha = Self::escape_sql_literal(commit_sha); + let repo_filter = self.build_sql_filter(&schema); + let sql_filter = format!("{repo_filter} AND tip_commit = '{escaped_sha}'"); let results = table .query() - .only_if(filter) + .only_if(sql_filter) .execute() .await? .try_collect::>() @@ -256,18 +325,14 @@ impl IndexedBranchStore { Ok(branches) } - /// Get total count of indexed branches + /// Get total count of indexed branches matching repository filter pub async fn count(&self) -> Result { - let table = self - .connection - .open_table("indexed_branches") - .execute() - .await?; - Ok(table.count_rows(None).await?) + let branches = self.list_indexed_branches().await?; + Ok(branches.len()) } /// Extract an IndexedBranchInfo from a batch at the given row index - fn extract_record_from_batch( + pub fn extract_record_from_batch( &self, batch: &RecordBatch, row: usize, @@ -300,6 +365,13 @@ impl IndexedBranchStore { .downcast_ref::() .ok_or_else(|| anyhow::anyhow!("Invalid remote column type"))?; + // Extract optional repo_path if column exists and value at row is non-null + let repo_path = batch + .column_by_name("repo_path") + .and_then(|col| col.as_any().downcast_ref::()) + .filter(|arr| !arr.is_null(row)) + .map(|arr| arr.value(row).to_string()); + let branch_name = branch_name_array.value(row).to_string(); let tip_commit = tip_commit_array.value(row).to_string(); let indexed_at = indexed_at_array.value(row); @@ -314,6 +386,7 @@ impl IndexedBranchStore { tip_commit, indexed_at, remote, + repo_path, })) } } @@ -337,7 +410,7 @@ mod tests { .await .unwrap(); - let store = IndexedBranchStore::new(connection); + let store = IndexedBranchStore::new(connection, "~/projects/repoA/.git".to_string(), true); (tmpdir, store) } @@ -350,6 +423,7 @@ mod tests { tip_commit: "abc123def456".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -372,6 +446,7 @@ mod tests { tip_commit: "789abc123".to_string(), indexed_at: 1699900100, remote: Some("origin".to_string()), + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -392,6 +467,7 @@ mod tests { tip_commit: "commit1".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info1).await.unwrap(); @@ -401,6 +477,7 @@ mod tests { tip_commit: "commit2".to_string(), indexed_at: 1699900100, remote: None, + repo_path: None, }; store.record_branch_indexed(&info2).await.unwrap(); @@ -423,18 +500,21 @@ mod tests { tip_commit: "commit1".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }, IndexedBranchInfo { branch_name: "develop".to_string(), tip_commit: "commit2".to_string(), indexed_at: 1699900100, remote: None, + repo_path: None, }, IndexedBranchInfo { branch_name: "origin/feature".to_string(), tip_commit: "commit3".to_string(), indexed_at: 1699900200, remote: Some("origin".to_string()), + repo_path: None, }, ]; @@ -460,6 +540,7 @@ mod tests { tip_commit: "abc123".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -480,6 +561,7 @@ mod tests { tip_commit: "abc123".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -500,24 +582,28 @@ mod tests { tip_commit: "commit1".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }, IndexedBranchInfo { branch_name: "origin/main".to_string(), tip_commit: "commit2".to_string(), indexed_at: 1699900100, remote: Some("origin".to_string()), + repo_path: None, }, IndexedBranchInfo { branch_name: "origin/develop".to_string(), tip_commit: "commit3".to_string(), indexed_at: 1699900200, remote: Some("origin".to_string()), + repo_path: None, }, IndexedBranchInfo { branch_name: "upstream/main".to_string(), tip_commit: "commit4".to_string(), indexed_at: 1699900300, remote: Some("upstream".to_string()), + repo_path: None, }, ]; @@ -545,18 +631,21 @@ mod tests { tip_commit: shared_commit.to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }, IndexedBranchInfo { branch_name: "release".to_string(), tip_commit: shared_commit.to_string(), indexed_at: 1699900100, remote: None, + repo_path: None, }, IndexedBranchInfo { branch_name: "develop".to_string(), tip_commit: "different456".to_string(), indexed_at: 1699900200, remote: None, + repo_path: None, }, ]; @@ -579,6 +668,7 @@ mod tests { tip_commit: "abc123".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -599,6 +689,7 @@ mod tests { tip_commit: "abc123".to_string(), indexed_at: 1699900000, remote: None, + repo_path: None, }; store.record_branch_indexed(&info).await.unwrap(); @@ -609,4 +700,153 @@ mod tests { assert!(retrieved.is_some()); assert_eq!(retrieved.unwrap().branch_name, "feature/user's-branch"); } + + #[tokio::test] + async fn test_shared_db_branch_isolation() { + let tmpdir = tempfile::tempdir().unwrap(); + let connection = lancedb::connect(tmpdir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + + let schema = IndexedBranchStore::get_schema(); + let empty_batch = RecordBatch::new_empty(schema.clone()); + connection + .create_table("indexed_branches", vec![empty_batch]) + .execute() + .await + .unwrap(); + + let repo_a = "~/projects/repoA/.git"; + let repo_b = "~/projects/repoB/.git"; + + let store_a = IndexedBranchStore::new(connection.clone(), repo_a.to_string(), false); + let store_b = IndexedBranchStore::new(connection.clone(), repo_b.to_string(), false); + + let info_a = IndexedBranchInfo { + branch_name: "main".to_string(), + tip_commit: "commit_a_123".to_string(), + indexed_at: 1699900000, + remote: None, + repo_path: Some(repo_a.to_string()), + }; + store_a.record_branch_indexed(&info_a).await.unwrap(); + + let info_b = IndexedBranchInfo { + branch_name: "main".to_string(), + tip_commit: "commit_b_456".to_string(), + indexed_at: 1699900100, + remote: None, + repo_path: Some(repo_b.to_string()), + }; + store_b.record_branch_indexed(&info_b).await.unwrap(); + + // In shared mode, store A sees only repo A's main branch + let branch_a = store_a.get_branch_info("main").await.unwrap().unwrap(); + assert_eq!(branch_a.tip_commit, "commit_a_123"); + + // Store B sees only repo B's main branch + let branch_b = store_b.get_branch_info("main").await.unwrap().unwrap(); + assert_eq!(branch_b.tip_commit, "commit_b_456"); + + // Listing for Store A yields 1 branch + let list_a = store_a.list_indexed_branches().await.unwrap(); + assert_eq!(list_a.len(), 1); + assert_eq!(list_a[0].tip_commit, "commit_a_123"); + } + + #[tokio::test] + async fn test_local_db_matches_null_and_active() { + let tmpdir = tempfile::tempdir().unwrap(); + let connection = lancedb::connect(tmpdir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + + let schema = IndexedBranchStore::get_schema(); + let empty_batch = RecordBatch::new_empty(schema.clone()); + connection + .create_table("indexed_branches", vec![empty_batch]) + .execute() + .await + .unwrap(); + + let repo_a = "~/projects/repoA/.git"; + + let store_local = IndexedBranchStore::new(connection.clone(), repo_a.to_string(), true); + let store_shared = IndexedBranchStore::new(connection.clone(), repo_a.to_string(), false); + + // Record a legacy NULL branch entry + let info_null = IndexedBranchInfo { + branch_name: "legacy_branch".to_string(), + tip_commit: "legacy_sha".to_string(), + indexed_at: 1699900000, + remote: None, + repo_path: None, + }; + store_local.record_branch_indexed(&info_null).await.unwrap(); + + // In Local mode, the legacy NULL branch is returned + let branch = store_local + .get_branch_info("legacy_branch") + .await + .unwrap() + .unwrap(); + assert_eq!(branch.tip_commit, "legacy_sha"); + + // In Shared mode, the legacy NULL branch is NOT returned + let branch_shared = store_shared.get_branch_info("legacy_branch").await.unwrap(); + assert!(branch_shared.is_none()); + } + + #[tokio::test] + async fn test_read_old_indexed_branches_table_does_not_alter_schema() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().to_str().unwrap(); + + // 1. Manually create an old 4-column indexed_branches table FIRST + let connection = lancedb::connect(db_path).execute().await.unwrap(); + let old_schema = Arc::new(Schema::new(vec![ + Field::new("branch_name", DataType::Utf8, false), + Field::new("tip_commit", DataType::Utf8, false), + Field::new("indexed_at", DataType::Int64, false), + Field::new("remote", DataType::Utf8, true), + ])); + + let empty_batch = RecordBatch::new_empty(old_schema); + connection + .create_table("indexed_branches", vec![empty_batch]) + .execute() + .await + .unwrap(); + + // 2. NOW open IndexedBranchStore on the old schema + let store = IndexedBranchStore::new(connection, "~/projects/repoA/.git".to_string(), true); + + // Reading branches works cleanly without altering disk schema + let (_table, schema_read) = store.open_table_with_schema().await.unwrap(); + assert!( + schema_read.column_with_name("repo_path").is_none(), + "Read-only operations should not alter schema on disk" + ); + + let branches = store.list_indexed_branches().await.unwrap(); + assert!(branches.is_empty()); + + // Writing a new branch record triggers schema migration on demand + let info = IndexedBranchInfo { + branch_name: "main".to_string(), + tip_commit: "sha123".to_string(), + indexed_at: 1699900000, + remote: None, + repo_path: None, + }; + store.record_branch_indexed(&info).await.unwrap(); + + let (_table, schema_after_write) = store.open_table_with_schema().await.unwrap(); + assert!( + schema_after_write.column_with_name("repo_path").is_some(), + "Write operation should add repo_path column on demand" + ); + } } diff --git a/src/database/connection.rs b/src/database/connection.rs index 0542e0f..5477bbe 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -24,13 +24,22 @@ use crate::types::{FunctionInfo, TypeInfo, TypedefInfo}; use crate::vectorizer::CodeVectorizer; use crate::workdir::WorkdirIndex; use std::collections::HashSet; +use std::path::PathBuf; // Optimal batch size for LanceDB operations pub const OPTIMAL_BATCH_SIZE: usize = 65536; +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct GcStats { + pub deleted_stale_branch_records: usize, +} + pub struct DatabaseManager { connection: Connection, - git_repo_path: String, + db_path: String, + work_dir: String, + repo_identity: String, + is_local_db: bool, function_store: FunctionStore, type_store: TypeStore, typedef_store: TypedefStore, @@ -45,22 +54,26 @@ pub struct DatabaseManager { } impl DatabaseManager { - pub async fn new(db_path: &str, git_repo_path: String) -> Result { + pub async fn new(db_path: &str, work_dir: String) -> Result { let connection = lancedb::connect(db_path).execute().await?; + let (repo_identity, is_local_db) = crate::git::determine_db_locality(&work_dir, db_path); Ok(Self { connection: connection.clone(), - git_repo_path: git_repo_path.clone(), + db_path: db_path.to_string(), + work_dir: work_dir.clone(), + repo_identity: repo_identity.clone(), + is_local_db, function_store: FunctionStore::new(connection.clone()), type_store: TypeStore::new(connection.clone()), typedef_store: TypedefStore::new(connection.clone()), - search_manager: SearchManager::new(connection.clone(), git_repo_path), + search_manager: SearchManager::new(connection.clone(), work_dir), vector_search_manager: VectorSearchManager::new(connection.clone()), schema_manager: SchemaManager::new(connection.clone()), processed_file_store: ProcessedFileStore::new(connection.clone()), content_store: ContentStore::new(connection.clone()), symbol_filename_store: SymbolFilenameStore::new(connection.clone()), - branch_store: IndexedBranchStore::new(connection.clone()), + branch_store: IndexedBranchStore::new(connection.clone(), repo_identity, is_local_db), workdir_index: std::sync::RwLock::new(None), }) } @@ -504,16 +517,30 @@ impl DatabaseManager { self.schema_manager.compact_and_cleanup().await } + pub async fn compact_lore_tables(&self) -> Result<()> { + self.schema_manager.compact_lore_tables().await + } + /// Drop and recreate all tables for maximum space savings pub async fn drop_and_recreate_tables(&self) -> Result<()> { self.schema_manager.drop_and_recreate_tables().await } - /// Create FTS indices for lore table (must be called after data is inserted) + /// Drop and rebuild all FTS indices for the lore table from scratch. pub async fn create_lore_fts_indices(&self) -> Result<()> { self.schema_manager.create_lore_fts_indices().await } + /// Create FTS indices only if they do not already exist. + pub async fn ensure_lore_fts_indices(&self) -> Result<()> { + self.schema_manager.ensure_lore_fts_indices().await + } + + /// Merge newly-inserted rows into existing lore FTS indices. + pub async fn optimize_lore_fts_indices(&self) -> Result<()> { + self.schema_manager.optimize_lore_fts_indices().await + } + /// Get lore table information including row count and indices pub async fn get_lore_table_info(&self) -> Result { use std::fmt::Write; @@ -2592,14 +2619,14 @@ impl DatabaseManager { file_paths: &[String], git_sha: &str, ) -> Result> { - match crate::git::resolve_files_at_commit(&self.git_repo_path, git_sha, file_paths) { + match crate::git::resolve_files_at_commit(&self.work_dir, git_sha, file_paths) { Ok(resolved_hashes) => { // If no files were resolved, log this as a warning if resolved_hashes.is_empty() { tracing::warn!( "No files were resolved at commit {} in repository {}", git_sha, - self.git_repo_path + self.work_dir ); } @@ -2607,7 +2634,7 @@ impl DatabaseManager { } Err(e) => { tracing::error!("DatabaseManager::resolve_git_file_hashes: Failed to resolve git files at commit {}: {}", git_sha, e); - tracing::error!("Repository path: {}", self.git_repo_path); + tracing::error!("Repository path: {}", self.work_dir); tracing::error!("Requested file paths: {:?}", file_paths); Ok(std::collections::HashMap::new()) // Return empty map instead of failing, let caller handle } @@ -2699,6 +2726,14 @@ impl DatabaseManager { // ==================== Branch Management ==================== + pub fn repo_identity(&self) -> &str { + &self.repo_identity + } + + pub fn is_local_db(&self) -> bool { + self.is_local_db + } + /// Record that a branch has been indexed at a specific commit pub async fn record_branch_indexed( &self, @@ -2707,6 +2742,7 @@ impl DatabaseManager { remote: Option<&str>, ) -> Result<()> { use crate::database::branches::IndexedBranchInfo; + let repo_path = (!self.is_local_db).then(|| self.repo_identity.clone()); let info = IndexedBranchInfo { branch_name: branch_name.to_string(), tip_commit: tip_commit.to_string(), @@ -2715,6 +2751,7 @@ impl DatabaseManager { .unwrap() .as_secs() as i64, remote: remote.map(|s| s.to_string()), + repo_path, }; self.branch_store.record_branch_indexed(&info).await } @@ -2769,6 +2806,93 @@ impl DatabaseManager { self.branch_store.count().await } + /// Performs garbage collection on stale branch records belonging to deleted repository paths. + pub async fn garbage_collect(&self) -> Result { + let mut stats = GcStats::default(); + + let table = match self + .connection + .open_table("indexed_branches") + .execute() + .await + { + Ok(t) => t, + Err(_) => return Ok(stats), + }; + + use futures::TryStreamExt; + let results = table + .query() + .only_if("repo_path IS NOT NULL") + .execute() + .await? + .try_collect::>() + .await?; + + let mut known_repo_paths = HashSet::new(); + for batch in &results { + if let Some(col) = batch.column_by_name("repo_path") { + if let Some(arr) = col.as_any().downcast_ref::() { + for i in 0..batch.num_rows() { + if !arr.is_null(i) { + known_repo_paths.insert(arr.value(i).to_string()); + } + } + } + } + } + + let mut repo_paths_to_clean = Vec::new(); + for rp in known_repo_paths { + let expanded_path = if let Some(stripped) = rp.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + home.join(stripped) + } else { + PathBuf::from(&rp) + } + } else { + PathBuf::from(&rp) + }; + + if !expanded_path.exists() { + repo_paths_to_clean.push(rp); + } + } + + for stale_rp in repo_paths_to_clean { + let escaped = stale_rp.replace("'", "''"); + let filter = format!("repo_path = '{escaped}'"); + let _ = table.delete(&filter).await; + stats.deleted_stale_branch_records += 1; + } + + // If the database directory does not reside within any Git repository, + // it is a standalone/central database that can never be local to any repository. + // In this case, un-scoped legacy (repo_path IS NULL) records can never be read + // and are safe to clean up. + let db_dir = std::path::Path::new(&self.db_path) + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + let is_inside_any_git_repo = gix::discover(db_dir).is_ok(); + + if !is_inside_any_git_repo { + let null_results = table + .query() + .only_if("repo_path IS NULL") + .execute() + .await? + .try_collect::>() + .await?; + let null_count: usize = null_results.iter().map(|b| b.num_rows()).sum(); + if null_count > 0 { + let _ = table.delete("repo_path IS NULL").await; + stats.deleted_stale_branch_records += null_count; + } + } + + Ok(stats) + } + // ==================== End Branch Management ==================== pub async fn get_existing_function_names(&self) -> Result> { @@ -2826,7 +2950,7 @@ impl DatabaseManager { ); // Step 1: Get directly changed files between commits - let changed_files = get_changed_files(&self.git_repo_path, commit_a, commit_b)?; + let changed_files = get_changed_files(&self.work_dir, commit_a, commit_b)?; let mut files_to_scan = std::collections::HashSet::new(); // Add all directly changed files (except deleted ones) @@ -3288,7 +3412,7 @@ impl DatabaseManager { } None => { // Default to current commit - match crate::git::get_git_sha(&self.git_repo_path) { + match crate::git::get_git_sha(&self.work_dir) { Ok(Some(current_sha)) => { tracing::info!("Using current git commit SHA: {}", current_sha); current_sha @@ -3423,16 +3547,12 @@ impl DatabaseManager { let mut manifest = std::collections::HashMap::new(); // Use shared tree traversal utility - crate::git::walk_tree_at_commit( - &self.git_repo_path, - git_sha, - |relative_path, object_id| { - // Normalize path by removing any double slashes - let normalized_path = relative_path.replace("//", "/"); - manifest.insert(normalized_path, object_id.to_string()); - Ok(()) - }, - )?; + crate::git::walk_tree_at_commit(&self.work_dir, git_sha, |relative_path, object_id| { + // Normalize path by removing any double slashes + let normalized_path = relative_path.replace("//", "/"); + manifest.insert(normalized_path, object_id.to_string()); + Ok(()) + })?; // Merge with workdir overlay (adds dirty files, removes deleted files) Ok(self.workdir_merged_manifest(manifest)) @@ -3933,13 +4053,19 @@ impl DatabaseManager { Ok(()) } - /// Insert lore emails into the database - pub async fn insert_lore_emails(&self, emails: &[crate::types::LoreEmailInfo]) -> Result<()> { + /// Insert lore emails into the database, returning the indices of + /// any emails that could not be stored. Callers must not record + /// commit SHAs for failed indices in the lore_indexed_commits + /// table, otherwise those emails are permanently lost. + pub async fn insert_lore_emails( + &self, + emails: &[crate::types::LoreEmailInfo], + ) -> Result> { use arrow::datatypes::{DataType, Field, Schema}; use std::sync::Arc; if emails.is_empty() { - return Ok(()); + return Ok(Vec::new()); } tracing::info!( @@ -3961,10 +4087,10 @@ impl DatabaseManager { Field::new("symbols", DataType::Utf8, false), ])); - // Deduplicate by message_id within the batch. A lore archive - // can contain the same email in multiple git commits, and - // LanceDB merge_insert requires each target row to be matched - // by at most one source row. Keep the last occurrence. + // Deduplicate by message_id within the batch. A lore + // archive can contain the same email in multiple git commits; + // appending duplicates would create redundant rows. Keep the + // last occurrence. let mut seen = std::collections::HashMap::with_capacity(emails.len()); for (i, email) in emails.iter().enumerate() { seen.insert(&email.message_id, i); @@ -3983,38 +4109,50 @@ impl DatabaseManager { let table = self.connection.open_table("lore").execute().await?; - // Try inserting the full batch first -- a single merge_insert - // is far cheaper than many small ones because each call is a - // full read-modify-write cycle in LanceDB. Fall back to - // chunked insertion only when the batch exhausts DataFusion's - // memory pool (the RepartitionExec OOM described in the - // comment below). - // - // Retrying the full set of indices on failure is safe: - // merge_insert is not transactional, so a failed call may - // have persisted some rows before the error. Because the - // upsert key is message_id, re-inserting those rows is - // idempotent. - if let Err(e) = Self::merge_insert_lore_chunk(&table, emails, &dedup_indices, &schema).await - { + let mut failed_indices: Vec = Vec::new(); + + // Filter out emails whose message_id already exists in the + // table. Normally every email here is new because + // index_lore_archive skips already-indexed commits, but a + // partial failure on a previous run can leave rows in the + // table whose commit SHA was never recorded in + // lore_indexed_commits. Filtering avoids duplicates without + // relying on merge_insert, which hits a spurious null-column + // error in lance-file 3.0.1. + let new_indices = Self::filter_existing_lore_ids(&table, emails, &dedup_indices).await?; + + if new_indices.len() < dedup_indices.len() { + tracing::info!( + "insert_lore_emails: {} of {} already in table, inserting {}", + dedup_indices.len() - new_indices.len(), + dedup_indices.len(), + new_indices.len(), + ); + } + + if new_indices.is_empty() { + tracing::info!("insert_lore_emails: no new emails to insert"); + return Ok(failed_indices); + } + + // Use table.add() instead of merge_insert. merge_insert in + // lance-file 3.0.1 introduces spurious nulls during its + // internal join/write phase, causing every insert to fail + // with "subject contained null values". Plain append avoids + // the merge codepath entirely. Duplicates are prevented by + // the filter_existing_lore_ids check above. + if let Err(e) = Self::add_lore_chunk(&table, emails, &new_indices, &schema).await { tracing::warn!( "insert_lore_emails: full batch of {} failed ({}), \ falling back to chunked insertion", - dedup_indices.len(), + new_indices.len(), e ); - // Lore emails carry full bodies, so each row is large - // compared to code-analysis records. LanceDB - // merge_insert uses DataFusion's RepartitionExec, whose - // memory pool can be exhausted by a single oversized - // RecordBatch. Insert in sub-batches to bound peak - // memory per operation. const MAX_CHUNK: usize = 128; - for chunk in dedup_indices.chunks(MAX_CHUNK) { - if let Err(e) = Self::merge_insert_lore_chunk(&table, emails, chunk, &schema).await - { + for chunk in new_indices.chunks(MAX_CHUNK) { + if let Err(e) = Self::add_lore_chunk(&table, emails, chunk, &schema).await { tracing::warn!( "insert_lore_emails: chunk of {} failed ({}), \ retrying individually", @@ -4022,8 +4160,7 @@ impl DatabaseManager { e ); for &idx in chunk { - if let Err(e2) = - Self::merge_insert_lore_chunk(&table, emails, &[idx], &schema).await + if let Err(e2) = Self::add_lore_chunk(&table, emails, &[idx], &schema).await { tracing::warn!( "insert_lore_emails: skipping \ @@ -4031,20 +4168,80 @@ impl DatabaseManager { emails[idx].message_id, e2 ); + failed_indices.push(idx); } } } } } + if !failed_indices.is_empty() { + tracing::warn!( + "insert_lore_emails: {} of {} emails failed to insert", + failed_indices.len(), + new_indices.len() + ); + } + tracing::info!("insert_lore_emails: Batch insertion complete"); - Ok(()) + Ok(failed_indices) + } + + /// Return the subset of `indices` whose message_id does not + /// already exist in the lore table. + async fn filter_existing_lore_ids( + table: &lancedb::Table, + emails: &[crate::types::LoreEmailInfo], + indices: &[usize], + ) -> Result> { + use futures::TryStreamExt as _; + + // Build an IN-list predicate. message_ids are already + // validated to be non-empty, but escape single quotes for + // the SQL literal. + let id_list: Vec = indices + .iter() + .map(|&i| format!("'{}'", emails[i].message_id.replace('\'', "''"))) + .collect(); + + let predicate = format!("message_id IN ({})", id_list.join(", ")); + + let stream = table + .query() + .select(lancedb::query::Select::Columns(vec![ + "message_id".to_string() + ])) + .only_if(&predicate) + .execute() + .await?; + let batches: Vec<_> = stream.try_collect().await?; + + let mut existing = std::collections::HashSet::new(); + for batch in &batches { + if let Some(col) = batch.column_by_name("message_id") { + let arr = col + .as_any() + .downcast_ref::() + .expect("message_id column must be StringArray"); + for i in 0..arr.len() { + if !arr.is_null(i) { + existing.insert(arr.value(i).to_string()); + } + } + } + } + + Ok(indices + .iter() + .copied() + .filter(|&i| !existing.contains(&emails[i].message_id)) + .collect()) } /// Build a [`RecordBatch`] from the given email indices and - /// merge-insert it into the lore table. - async fn merge_insert_lore_chunk( + /// append it to the lore table. + async fn add_lore_chunk( table: &lancedb::Table, emails: &[crate::types::LoreEmailInfo], indices: &[usize], @@ -4098,15 +4295,7 @@ impl DatabaseManager { ]; let batch = RecordBatch::try_new(schema.clone(), columns)?; - let batches = vec![Ok(batch)]; - let batch_iterator = - arrow::record_batch::RecordBatchIterator::new(batches.into_iter(), schema.clone()); - - let mut merge_insert = table.merge_insert(&["message_id"]); - merge_insert - .when_matched_update_all(None) - .when_not_matched_insert_all(); - merge_insert.execute(Box::new(batch_iterator)).await?; + table.add(vec![batch]).execute().await?; Ok(()) } @@ -4293,18 +4482,42 @@ impl DatabaseManager { let mut first_iteration = true; loop { - let fts_query = - FullTextSearchQuery::new(fts_pattern.clone()).with_column(field.to_owned())?; + // When the FTS pattern is empty (e.g. regex ".*" has no + // alphanumeric tokens), skip FTS and use a plain table + // scan so the regex post-filter still runs. + let batches: Vec<_> = if fts_pattern.is_empty() { + tracing::info!( + "FTS pattern empty for field '{}', falling back to table scan", + field + ); + let mut query_builder = table.query(); + if let Some(ref filter) = date_filter { + query_builder = query_builder.only_if(filter); + } + query_builder + .limit(fts_limit) + .execute() + .await? + .try_collect() + .await? + } else { + let fts_query = + FullTextSearchQuery::new(fts_pattern.clone()).with_column(field.to_owned())?; - let mut query_builder = table.query().full_text_search(fts_query); + let mut query_builder = table.query().full_text_search(fts_query); - // Apply date filter at database level so limit applies to date-filtered results - if let Some(ref filter) = date_filter { - query_builder = query_builder.only_if(filter); - } + // Apply date filter at database level so limit applies to date-filtered results + if let Some(ref filter) = date_filter { + query_builder = query_builder.only_if(filter); + } - let stream = query_builder.limit(fts_limit).execute().await?; - let batches: Vec<_> = stream.try_collect().await?; + query_builder + .limit(fts_limit) + .execute() + .await? + .try_collect() + .await? + }; let fts_count: usize = batches.iter().map(|b| b.num_rows()).sum(); tracing::info!( @@ -4468,6 +4681,7 @@ impl DatabaseManager { /// Helper function to query lore emails by multiple fields and return intersection /// Uses regex alternation for OR within fields, intersection for AND across fields + #[allow(clippy::too_many_arguments)] pub(crate) async fn query_lore_by_fields_intersection( &self, from_patterns: Option<&[String]>, @@ -4475,18 +4689,35 @@ impl DatabaseManager { body_patterns: Option<&[String]>, recipients_patterns: Option<&[String]>, search_limit: usize, + since_date: Option<&str>, + until_date: Option<&str>, ) -> Result> { use std::collections::HashSet; let lore_table = self.connection.open_table("lore").execute().await?; let mut field_result_sets: Vec> = Vec::new(); - // Helper function to query a field using FTS with regex post-filtering + // Parse date filters into DateTime for temporal comparison + // in query_field_impl (RFC 2822 string comparison is not + // meaningful for date ordering). + let since_dt = since_date + .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)); + let until_dt = until_date + .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)); + + // Helper function to query a field using FTS with regex and + // date post-filtering. Selects the "date" column alongside + // the searched field so temporal filtering happens on the + // already-fetched FTS candidates without extra lookups. async fn query_field_impl( lore_table: &lancedb::Table, field_name: String, pattern: String, search_limit: usize, + since: Option>, + until: Option>, ) -> Result> { // FTS uses simple tokenizer - normalize pattern by stripping special chars let fts_pattern = pattern @@ -4502,28 +4733,52 @@ impl DatabaseManager { fts_pattern ); - let fts_query = - FullTextSearchQuery::new(fts_pattern).with_column(field_name.clone())?; - let mut query = lore_table.query().full_text_search(fts_query).select( - lancedb::query::Select::Columns(vec![ - "message_id".to_string(), - "_score".to_string(), - field_name.clone(), - ]), - ); - - // Apply limit - use large limit if search_limit is 0 (unlimited) - // FTS has a default limit of 10, so we must explicitly set a large limit let effective_limit = if search_limit > 0 { search_limit } else { 100000 }; - query = query.limit(effective_limit); - let results = query.execute().await?.try_collect::>().await?; + // When the FTS pattern is empty (e.g. regex ".*" has no + // alphanumeric tokens), skip FTS and fall back to a plain + // table scan so the regex post-filter still runs. + let results = if fts_pattern.is_empty() { + tracing::info!( + "FTS pattern empty for field '{}', falling back to table scan", + field_name + ); + lore_table + .query() + .select(lancedb::query::Select::Columns(vec![ + "message_id".to_string(), + field_name.clone(), + "date".to_string(), + ])) + .limit(effective_limit) + .execute() + .await? + .try_collect::>() + .await? + } else { + let fts_query = + FullTextSearchQuery::new(fts_pattern).with_column(field_name.clone())?; + lore_table + .query() + .full_text_search(fts_query) + .select(lancedb::query::Select::Columns(vec![ + "message_id".to_string(), + "_score".to_string(), + field_name.clone(), + "date".to_string(), + ])) + .limit(effective_limit) + .execute() + .await? + .try_collect::>() + .await? + }; - // Step 2: Post-filter with regex in memory + // Post-filter with regex and date range in memory let fts_result_count: usize = results.iter().map(|b| b.num_rows()).sum(); tracing::info!( "FTS returned {} candidates for field '{}'", @@ -4534,37 +4789,46 @@ impl DatabaseManager { let regex = regex::RegexBuilder::new(&pattern) .case_insensitive(true) .build()?; + let has_date_filter = since.is_some() || until.is_some(); let mut message_ids = HashSet::new(); - let mut first_candidate_logged = false; + let mut bad_dates: usize = 0; for batch in &results { - let msg_array = batch - .column(0) - .as_any() - .downcast_ref::() - .unwrap(); - let field_array = batch - .column(2) - .as_any() - .downcast_ref::() - .unwrap(); + let msg_array: &arrow::array::StringArray = super::get_column(batch, "message_id")?; + let field_array: &arrow::array::StringArray = + super::get_column(batch, &field_name)?; + let date_array: &arrow::array::StringArray = super::get_column(batch, "date")?; for i in 0..batch.num_rows() { - let field_value = field_array.value(i); - - // Log first candidate for debugging - if !first_candidate_logged { - tracing::info!("Sample {} value: '{}'", field_name, field_value); - tracing::info!("Regex pattern: '{}'", pattern); - first_candidate_logged = true; + if !regex.is_match(field_array.value(i)) { + continue; } - - if regex.is_match(field_value) { - message_ids.insert(msg_array.value(i).to_string()); + if has_date_filter { + if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(date_array.value(i)) { + let dt_utc = dt.with_timezone(&chrono::Utc); + if since.is_some_and(|s| dt_utc < s) { + continue; + } + if until.is_some_and(|u| dt_utc > u) { + continue; + } + } else { + bad_dates += 1; + continue; + } } + message_ids.insert(msg_array.value(i).to_string()); } } + if bad_dates > 0 { + tracing::warn!( + "Skipped {} candidates with unparseable dates for field '{}'", + bad_dates, + field_name + ); + } + tracing::info!( "Regex filter kept {} of {} FTS candidates", message_ids.len(), @@ -4574,71 +4838,90 @@ impl DatabaseManager { Ok(message_ids) } - // Query from field + // Query from field (OR across patterns, then push single set) if let Some(patterns) = from_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - let results = query_field_impl( - &lore_table, - "from".to_string(), - combined_pattern, - search_limit, - ) - .await?; - tracing::info!("lore from field returned {} results", results.len()); - field_result_sets.push(results); + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( + &lore_table, + "from".to_string(), + pattern.clone(), + search_limit, + since_dt, + until_dt, + ) + .await?; + field_union.extend(results); + } + tracing::info!("lore from field returned {} results", field_union.len()); + field_result_sets.push(field_union); } } - // Query subject field + // Query subject field (OR across patterns) if let Some(patterns) = subject_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - let results = query_field_impl( - &lore_table, - "subject".to_string(), - combined_pattern, - search_limit, - ) - .await?; - tracing::info!("lore subject field returned {} results", results.len()); - field_result_sets.push(results); + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( + &lore_table, + "subject".to_string(), + pattern.clone(), + search_limit, + since_dt, + until_dt, + ) + .await?; + field_union.extend(results); + } + tracing::info!("lore subject field returned {} results", field_union.len()); + field_result_sets.push(field_union); } } - // Query body field + // Query body field (OR across patterns) if let Some(patterns) = body_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - let results = query_field_impl( - &lore_table, - "body".to_string(), - combined_pattern, - search_limit, - ) - .await?; - tracing::info!("lore body field returned {} results", results.len()); - field_result_sets.push(results); + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( + &lore_table, + "body".to_string(), + pattern.clone(), + search_limit, + since_dt, + until_dt, + ) + .await?; + field_union.extend(results); + } + tracing::info!("lore body field returned {} results", field_union.len()); + field_result_sets.push(field_union); } } - // Query recipients field + // Query recipients field (OR across patterns) if let Some(patterns) = recipients_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - let results = query_field_impl( - &lore_table, - "recipients".to_string(), - combined_pattern, - search_limit, - ) - .await?; - tracing::info!("lore recipients field returned {} results", results.len()); - field_result_sets.push(results); + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( + &lore_table, + "recipients".to_string(), + pattern.clone(), + search_limit, + since_dt, + until_dt, + ) + .await?; + field_union.extend(results); + } + tracing::info!( + "lore recipients field returned {} results", + field_union.len() + ); + field_result_sets.push(field_union); } } @@ -4704,7 +4987,9 @@ impl DatabaseManager { let body_patterns = field_map.get("body").map(|v| v.as_slice()); let recipients_patterns = field_map.get("recipients").map(|v| v.as_slice()); - // Use helper to get intersection of message_ids + // Use helper to get intersection of message_ids. + // Date range is pushed into FTS queries so the candidate set + // is already bounded before intersection and fetching. let intersection = self .query_lore_by_fields_intersection( from_patterns, @@ -4712,6 +4997,8 @@ impl DatabaseManager { body_patterns, recipients_patterns, 0, // No limit for individual queries + since_date, + until_date, ) .await?; @@ -4721,54 +5008,15 @@ impl DatabaseManager { return Ok(Vec::new()); } - // Parse filter dates to Unix timestamps for database-level filtering - let since_timestamp = since_date - .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) - .map(|dt| dt.timestamp()); - let until_timestamp = until_date - .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) - .map(|dt| dt.timestamp()); + // Fetch full email records for the intersection. + // Date filtering was already pushed into the per-field FTS + // queries, so the intersection is already date-bounded. + let count_limit = if limit > 0 { limit } else { intersection.len() }; + let ids: Vec<&String> = intersection.iter().take(count_limit).collect(); + let final_emails = self.fetch_lore_emails_by_message_ids(&ids).await?; tracing::info!( - "lore multi_field search: since_timestamp={:?} until_timestamp={:?}", - since_timestamp, - until_timestamp - ); - - // Build date filter clause — only if the column exists in the table - let has_date_timestamp = { - let table = self.connection.open_table("lore").execute().await?; - table - .schema() - .await - .map(|s| s.field_with_name("date_timestamp").is_ok()) - .unwrap_or(false) - }; - let date_filter = if has_date_timestamp { - match (since_timestamp, until_timestamp) { - (Some(since), Some(until)) => Some(format!( - "date_timestamp >= {} AND date_timestamp <= {}", - since, until - )), - (Some(since), None) => Some(format!("date_timestamp >= {}", since)), - (None, Some(until)) => Some(format!("date_timestamp <= {}", until)), - (None, None) => None, - } - } else { - None - }; - - // Fetch emails with date filtering at database level - let final_emails = self - .fetch_lore_emails_by_message_ids_with_filter( - &intersection, - date_filter.as_deref(), - limit, - ) - .await?; - - tracing::info!( - "Fetched {} email records matching date filter from {} message_ids", + "Fetched {} email records from {} message_ids", final_emails.len(), intersection.len() ); @@ -4776,13 +5024,13 @@ impl DatabaseManager { Ok(final_emails) } - /// Fetch lore emails by message IDs with optional SQL filter and limit - /// This allows efficient database-level filtering (e.g., by date_timestamp) - async fn fetch_lore_emails_by_message_ids_with_filter( + /// Fetch lore emails by message IDs in batches. + /// + /// Builds `message_id IN (...)` predicates in chunks to avoid + /// per-ID round-trips while keeping predicate size bounded. + async fn fetch_lore_emails_by_message_ids( &self, - message_ids: &std::collections::HashSet, - filter: Option<&str>, - limit: usize, + message_ids: &[&String], ) -> Result> { use arrow::array::AsArray; use futures::TryStreamExt; @@ -4792,88 +5040,99 @@ impl DatabaseManager { } let table = self.connection.open_table("lore").execute().await?; + let mut emails = Vec::with_capacity(message_ids.len()); - // Build message_id IN clause - let escaped_ids: Vec = message_ids - .iter() - .map(|id| { - let escaped = id.replace('\'', "''"); - format!("'{}'", escaped) - }) - .collect(); - let in_clause = format!("message_id IN ({})", escaped_ids.join(", ")); - - // Combine with optional filter - let where_clause = match filter { - Some(f) => format!("{} AND {}", in_clause, f), - None => in_clause, - }; - - tracing::debug!("Lore query with filter: {}", where_clause); + for chunk in message_ids.chunks(500) { + let placeholders: Vec = chunk + .iter() + .map(|id| { + let escaped = id.replace('\'', "''"); + format!("'{}'", escaped) + }) + .collect(); + let predicate = format!("message_id IN ({})", placeholders.join(", ")); - // Query with filter, ordered by date_timestamp descending (newest first) - let effective_limit = if limit > 0 { limit } else { message_ids.len() }; - let results = table - .query() - .only_if(&where_clause) - .limit(effective_limit) - .execute() - .await? - .try_collect::>() - .await?; + let results = table + .query() + .only_if(&predicate) + .limit(chunk.len()) + .execute() + .await? + .try_collect::>() + .await?; - let mut emails = Vec::new(); - for batch in results { - let git_commit_sha_col = batch.column_by_name("git_commit_sha").unwrap(); - let from_col = batch.column_by_name("from").unwrap(); - let date_col = batch.column_by_name("date").unwrap(); - let date_timestamp_col = batch.column_by_name("date_timestamp"); - let message_id_col = batch.column_by_name("message_id").unwrap(); - let in_reply_to_col = batch.column_by_name("in_reply_to").unwrap(); - let subject_col = batch.column_by_name("subject").unwrap(); - let references_col = batch.column_by_name("references").unwrap(); - let recipients_col = batch.column_by_name("recipients").unwrap(); - let body_col = batch.column_by_name("body").unwrap(); - let symbols_col = batch.column_by_name("symbols").unwrap(); - - let git_commit_sha_arr = git_commit_sha_col.as_string::(); - let from_arr = from_col.as_string::(); - let date_arr = date_col.as_string::(); - let date_timestamp_arr = date_timestamp_col - .and_then(|c| c.as_any().downcast_ref::()); - let message_id_arr = message_id_col.as_string::(); - let in_reply_to_arr = in_reply_to_col.as_string::(); - let subject_arr = subject_col.as_string::(); - let references_arr = references_col.as_string::(); - let recipients_arr = recipients_col.as_string::(); - let body_arr = body_col.as_string::(); - let symbols_arr = symbols_col.as_string::(); + for batch in &results { + let git_commit_shas = batch + .column_by_name("git_commit_sha") + .ok_or_else(|| anyhow::anyhow!("Missing git_commit_sha column"))? + .as_string::(); + let from_addrs = batch + .column_by_name("from") + .ok_or_else(|| anyhow::anyhow!("Missing from column"))? + .as_string::(); + let dates = batch + .column_by_name("date") + .ok_or_else(|| anyhow::anyhow!("Missing date column"))? + .as_string::(); + let date_timestamps = batch + .column_by_name("date_timestamp") + .and_then(|c| c.as_any().downcast_ref::()); + let msg_ids = batch + .column_by_name("message_id") + .ok_or_else(|| anyhow::anyhow!("Missing message_id column"))? + .as_string::(); + let in_reply_tos = batch + .column_by_name("in_reply_to") + .ok_or_else(|| anyhow::anyhow!("Missing in_reply_to column"))? + .as_string::(); + let subjects = batch + .column_by_name("subject") + .ok_or_else(|| anyhow::anyhow!("Missing subject column"))? + .as_string::(); + let references_list = batch + .column_by_name("references") + .ok_or_else(|| anyhow::anyhow!("Missing references column"))? + .as_string::(); + let recipients_list = batch + .column_by_name("recipients") + .ok_or_else(|| anyhow::anyhow!("Missing recipients column"))? + .as_string::(); + let bodies = batch + .column_by_name("body") + .ok_or_else(|| anyhow::anyhow!("Missing body column"))? + .as_string::(); + let symbols_list = batch + .column_by_name("symbols") + .ok_or_else(|| anyhow::anyhow!("Missing symbols column"))? + .as_string::(); - for i in 0..batch.num_rows() { - let symbols_json = symbols_arr.value(i); - let symbols: Vec = serde_json::from_str(symbols_json).unwrap_or_default(); + for i in 0..batch.num_rows() { + let symbols_json = symbols_list.value(i); + let symbols: Vec = + serde_json::from_str(symbols_json).unwrap_or_default(); - emails.push(crate::types::LoreEmailInfo { - git_commit_sha: git_commit_sha_arr.value(i).to_string(), - from: from_arr.value(i).to_string(), - date: date_arr.value(i).to_string(), - date_timestamp: Self::get_date_timestamp(date_timestamp_arr, date_arr, i), - message_id: message_id_arr.value(i).to_string(), - in_reply_to: if in_reply_to_arr.is_null(i) { - None - } else { - Some(in_reply_to_arr.value(i).to_string()) - }, - subject: subject_arr.value(i).to_string(), - references: if references_arr.is_null(i) { - None - } else { - Some(references_arr.value(i).to_string()) - }, - recipients: recipients_arr.value(i).to_string(), - body: body_arr.value(i).to_string(), - symbols, - }); + emails.push(crate::types::LoreEmailInfo { + git_commit_sha: git_commit_shas.value(i).to_string(), + from: from_addrs.value(i).to_string(), + date: dates.value(i).to_string(), + date_timestamp: Self::get_date_timestamp(date_timestamps, dates, i), + message_id: msg_ids.value(i).to_string(), + in_reply_to: if in_reply_tos.is_null(i) { + None + } else { + Some(in_reply_tos.value(i).to_string()) + }, + subject: subjects.value(i).to_string(), + references: if references_list.is_null(i) { + None + } else { + Some(references_list.value(i).to_string()) + }, + recipients: recipients_list.value(i).to_string(), + body: bodies.value(i).to_string(), + symbols, + }); + } } } @@ -5781,3 +6040,53 @@ impl DatabaseManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::branches::{IndexedBranchInfo, IndexedBranchStore}; + use arrow::array::RecordBatch; + + #[tokio::test] + async fn test_garbage_collect_null_records_in_standalone_db() { + let tmpdir = tempfile::tempdir().unwrap(); + let db_path = tmpdir.path().join("standalone.db"); + + let git_dir = tempfile::tempdir().unwrap(); + let _ = std::process::Command::new("git") + .args(["init", git_dir.path().to_str().unwrap()]) + .output(); + + let db = DatabaseManager::new( + db_path.to_str().unwrap(), + git_dir.path().to_str().unwrap().to_string(), + ) + .await + .unwrap(); + + let schema = IndexedBranchStore::get_schema(); + let empty_batch = RecordBatch::new_empty(schema); + db.connection() + .create_table("indexed_branches", vec![empty_batch]) + .execute() + .await + .unwrap(); + + let store_local = IndexedBranchStore::new( + db.connection().clone(), + git_dir.path().to_str().unwrap().to_string(), + true, + ); + let info_null = IndexedBranchInfo { + branch_name: "legacy".to_string(), + tip_commit: "sha1".to_string(), + indexed_at: 100, + remote: None, + repo_path: None, + }; + store_local.record_branch_indexed(&info_null).await.unwrap(); + + let stats = db.garbage_collect().await.unwrap(); + assert_eq!(stats.deleted_stale_branch_records, 1); + } +} diff --git a/src/database/schema.rs b/src/database/schema.rs index 28977e2..b4a4026 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use anyhow::Result; +use arrow::array::Array; use arrow::datatypes::{DataType, Field, Schema}; use arrow::record_batch::RecordBatch; use futures::{stream, StreamExt, TryStreamExt}; @@ -14,7 +15,7 @@ use std::sync::Arc; pub enum OptimizeOutcome { /// Table was successfully optimized (all operations completed) Optimized, - /// Table was skipped (too small to benefit from optimization) + /// Table was skipped (e.g., too few rows to benefit) Skipped, /// Optimization was attempted but one or more operations failed PartialFailure, @@ -301,7 +302,7 @@ impl SchemaManager { tracing::info!("Lore table migration complete"); } OptimizeOutcome::Skipped => { - tracing::info!("Lore table compaction skipped (table too small)"); + tracing::info!("Lore table compaction skipped (preserving FTS indices)"); } OptimizeOutcome::PartialFailure => { tracing::warn!("Lore table compaction partially failed"); @@ -309,6 +310,109 @@ impl SchemaManager { } } + // Add the date_timestamp column if missing. Databases created + // before this column was introduced have a 10-column schema; + // merge_insert of 11-column batches silently fails, causing + // new emails to be skipped while their commit SHAs are still + // recorded as indexed. + if schema.column_with_name("date_timestamp").is_none() { + tracing::info!("Migrating lore table: adding 'date_timestamp' column"); + table + .add_columns( + lancedb::table::NewColumnTransform::SqlExpressions(vec![( + "date_timestamp".into(), + "CAST(0 AS BIGINT)".into(), + )]), + None, + ) + .await?; + + // Purge lore_indexed_commits so that previously-skipped + // emails are re-examined on the next --lore refresh. + self.reconcile_lore_indexed_commits().await?; + } + + Ok(()) + } + + /// Remove entries from lore_indexed_commits whose git_commit_sha + /// does not appear in the lore table. This recovers from the + /// schema-mismatch bug where SHAs were recorded as indexed but the + /// corresponding emails were never stored. + async fn reconcile_lore_indexed_commits(&self) -> Result<()> { + let lore = self.connection.open_table("lore").execute().await?; + let idx = self + .connection + .open_table("lore_indexed_commits") + .execute() + .await?; + + // Collect the set of SHAs actually present in the lore table. + let lore_stream = lore + .query() + .select(lancedb::query::Select::Columns(vec![ + "git_commit_sha".to_string() + ])) + .execute() + .await?; + let lore_batches: Vec<_> = lore_stream.try_collect().await?; + + let mut lore_shas = std::collections::HashSet::new(); + for batch in &lore_batches { + if let Some(col) = batch.column_by_name("git_commit_sha") { + if let Some(arr) = col.as_any().downcast_ref::() { + for i in 0..arr.len() { + lore_shas.insert(arr.value(i).to_string()); + } + } + } + } + + // Collect SHAs from lore_indexed_commits. + let idx_stream = idx + .query() + .select(lancedb::query::Select::Columns(vec![ + "git_commit_sha".to_string() + ])) + .execute() + .await?; + let idx_batches: Vec<_> = idx_stream.try_collect().await?; + + let mut orphaned: Vec = Vec::new(); + for batch in &idx_batches { + if let Some(col) = batch.column_by_name("git_commit_sha") { + if let Some(arr) = col.as_any().downcast_ref::() { + for i in 0..arr.len() { + let sha = arr.value(i); + if !lore_shas.contains(sha) { + orphaned.push(sha.to_string()); + } + } + } + } + } + + if orphaned.is_empty() { + tracing::info!("reconcile_lore_indexed_commits: no orphaned entries"); + return Ok(()); + } + + tracing::info!( + "reconcile_lore_indexed_commits: removing {} orphaned entries", + orphaned.len() + ); + + // Delete in chunks to avoid oversized SQL predicates. + for chunk in orphaned.chunks(500) { + let placeholders: Vec = chunk + .iter() + .map(|s| format!("'{}'", s.replace('\'', "''"))) + .collect(); + let predicate = format!("git_commit_sha IN ({})", placeholders.join(", ")); + idx.delete(&predicate).await?; + } + + tracing::info!("reconcile_lore_indexed_commits: done"); Ok(()) } @@ -788,38 +892,11 @@ impl SchemaManager { } } - async fn try_create_fts_index( - &self, - table: &lancedb::table::Table, - columns: &[&str], - description: &str, - ) { - // Configure FTS index optimized for source code and technical docs - let fts_config = FtsIndexBuilder::default() - .with_position(false) // Enable phrase queries for exact code snippets - .base_tokenizer("simple".to_string()) - .lower_case(true) // Case-insensitive search for better matching - .stem(false) // No stemming (run != running in code) - .remove_stop_words(false) // Keep all tokens (if, for, while are keywords) - .ascii_folding(true) // Preserve exact characters - .max_token_length(Some(100)); // Allow long identifiers/symbols - - tracing::info!("Starting {}", description); - match table - .create_index(columns, LanceIndex::FTS(fts_config)) - .execute() - .await - { - Ok(_) => tracing::info!("✓ Completed {}", description), - Err(e) => tracing::warn!("Failed to create {}: {}", description, e), - } - } - - /// Create FTS indices for lore table (must be called after data is inserted) + /// Drop and rebuild all FTS indices for the lore table from scratch. /// - /// Drops any existing FTS indices first, creates fresh ones, then - /// prunes so that the on-disk index directories from prior data - /// layouts are actually removed. + /// Intended for schema migrations and --clear rebuilds where the + /// table structure has changed. Normal incremental indexing should + /// use ensure_lore_fts_indices() + optimize_lore_fts_indices(). pub async fn create_lore_fts_indices(&self) -> Result<()> { let table = self.connection.open_table("lore").execute().await?; @@ -841,29 +918,9 @@ impl SchemaManager { } } - // Create FTS indices for text search on all searchable fields in parallel - let start_time = std::time::Instant::now(); - tracing::info!("Creating 5 FTS indices for lore table in parallel (from, subject, body, recipients, symbols)..."); - - // Create all 5 indices concurrently for faster indexing - tokio::join!( - self.try_create_fts_index(&table, &["from"], "FTS index on lore.from"), - self.try_create_fts_index(&table, &["subject"], "FTS index on lore.subject"), - self.try_create_fts_index(&table, &["body"], "FTS index on lore.body"), - self.try_create_fts_index(&table, &["recipients"], "FTS index on lore.recipients"), - self.try_create_fts_index(&table, &["symbols"], "FTS index on lore.symbols"), - ); + Self::create_all_fts_indices(&table).await?; - let elapsed = start_time.elapsed(); - tracing::info!( - "Completed creating 5 FTS indices in {:.1}s", - elapsed.as_secs_f64() - ); - - // Prune orphaned index data left behind by drop_index() - // and by OptimizeAction::Index in optimize_single_table(), - // which rebuilds all indices (including FTS) into new - // directories without removing the old ones. + // Prune orphaned index data left behind by drop_index(). if dropped { tracing::info!("Pruning orphaned index data from lore table..."); if let Err(e) = table @@ -883,6 +940,149 @@ impl SchemaManager { Ok(()) } + /// Create FTS indices only if they do not already exist. + /// + /// After the first full build, subsequent indexing runs call this + /// to ensure the indices are present, then optimize_lore_fts_indices() + /// to merge newly-inserted rows into the existing indices. + pub async fn ensure_lore_fts_indices(&self) -> Result<()> { + use lancedb::index::IndexType; + let table = self.connection.open_table("lore").execute().await?; + let indices: Vec = + (table.list_indices().await).unwrap_or_default(); + + let fts_count = indices + .iter() + .filter(|idx| idx.index_type == IndexType::FTS) + .count(); + + // All 5 FTS indices present — nothing to do. + if fts_count >= 5 { + tracing::info!( + "Lore FTS indices already present ({} indices), skipping creation", + fts_count + ); + return Ok(()); + } + + if fts_count > 0 { + tracing::info!( + "Only {} of 5 FTS indices present, rebuilding all", + fts_count + ); + // Drop the partial set so create_index does not collide. + for idx in &indices { + if idx.index_type == IndexType::FTS { + let _ = table.drop_index(&idx.name).await; + } + } + } else { + tracing::info!("No FTS indices found, creating initial set"); + } + + Self::create_all_fts_indices(&table).await + } + + /// Merge newly-inserted rows into existing lore FTS indices. + /// + /// LanceDB's native FTS engine serves unindexed rows via a + /// brute-force fallback at query time, so queries remain correct + /// even before this call. Running optimize merges those rows + /// into the inverted index structure, eliminating the scan cost. + pub async fn optimize_lore_fts_indices(&self) -> Result<()> { + // Guard against running on a table with a large _indices/ + // backlog. lance/index/append.rs opens every delta fragment + // for a column before merging any, so peak memory scales + // linearly with the number of fragments per column. On + // memory-constrained systems a backlog in the thousands + // drives semcode-index into swap and gets it OOM-killed. + // Query correctness is preserved regardless: unindexed rows + // still fall back to a brute-force scan. + const MAX_LORE_INDEX_FRAGMENTS: usize = 100; + let uri = self.connection.uri(); + let indices_dir = std::path::Path::new(uri) + .join("lore.lance") + .join("_indices"); + if let Ok(rd) = std::fs::read_dir(&indices_dir) { + let count = rd.count(); + if count > MAX_LORE_INDEX_FRAGMENTS { + tracing::warn!( + "Skipping lore FTS index optimization: \ + {} _indices/ fragments exceeds {} threshold. \ + Queries remain correct via brute-force fallback. \ + Rebuild the lore table on a larger host to recover.", + count, + MAX_LORE_INDEX_FRAGMENTS + ); + return Ok(()); + } + } + + let table = self.connection.open_table("lore").execute().await?; + let start_time = std::time::Instant::now(); + + tracing::info!("Optimizing lore FTS indices (incremental merge)..."); + table + .optimize(OptimizeAction::Index(Default::default())) + .await?; + + let elapsed = start_time.elapsed(); + tracing::info!( + "Lore FTS index optimization completed in {:.1}s", + elapsed.as_secs_f64() + ); + Ok(()) + } + + /// Shared helper: create all 5 FTS indices on an already-opened table. + async fn create_all_fts_indices(table: &lancedb::table::Table) -> Result<()> { + let start_time = std::time::Instant::now(); + tracing::info!( + "Creating 5 FTS indices for lore table (from, subject, body, recipients, symbols)..." + ); + + tokio::join!( + Self::create_one_fts_index(table, &["from"], "FTS index on lore.from"), + Self::create_one_fts_index(table, &["subject"], "FTS index on lore.subject"), + Self::create_one_fts_index(table, &["body"], "FTS index on lore.body"), + Self::create_one_fts_index(table, &["recipients"], "FTS index on lore.recipients"), + Self::create_one_fts_index(table, &["symbols"], "FTS index on lore.symbols"), + ); + + let elapsed = start_time.elapsed(); + tracing::info!( + "Completed creating 5 FTS indices in {:.1}s", + elapsed.as_secs_f64() + ); + Ok(()) + } + + /// Create a single FTS index on the given columns. + async fn create_one_fts_index( + table: &lancedb::table::Table, + columns: &[&str], + description: &str, + ) { + let fts_config = FtsIndexBuilder::default() + .with_position(false) + .base_tokenizer("simple".to_string()) + .lower_case(true) + .stem(false) + .remove_stop_words(false) + .ascii_folding(true) + .max_token_length(Some(100)); + + tracing::info!("Starting {}", description); + match table + .create_index(columns, LanceIndex::FTS(fts_config)) + .execute() + .await + { + Ok(_) => tracing::info!("Completed {}", description), + Err(e) => tracing::warn!("Failed to create {}: {}", description, e), + } + } + pub async fn rebuild_indices(&self) -> Result<()> { // Rebuild vector index if needed let table_names = self.connection.table_names().execute().await?; @@ -999,7 +1199,7 @@ impl SchemaManager { let skipped = tables_skipped.load(Ordering::Relaxed); let failed = tables_failed.load(Ordering::Relaxed); - println!( + tracing::info!( " Optimized {} tables{}{}", optimized, if skipped > 0 { @@ -1017,6 +1217,43 @@ impl SchemaManager { Ok(()) } + /// Compact only the tables modified by lore indexing. + /// + /// The full `compact_and_cleanup` method processes every table in + /// the database, including code-index tables and content shards + /// that a lore run never touches. On memory-constrained systems + /// the combined working set of those compactions triggers the OOM + /// killer. This method limits work to the two lore tables and + /// processes them sequentially to keep peak memory low. + pub async fn compact_lore_tables(&self) -> Result<()> { + tracing::info!("Running compaction for lore tables..."); + + let table_names = self.connection.table_names().execute().await?; + let lore_tables = ["lore", "lore_indexed_commits"]; + + for name in &lore_tables { + if !table_names.iter().any(|n| n == name) { + continue; + } + match Self::optimize_single_table(&self.connection, name).await { + Ok(OptimizeOutcome::Optimized) => { + tracing::info!("Compacted table {}", name); + } + Ok(OptimizeOutcome::Skipped) => { + tracing::info!("Skipped table {} (too small)", name); + } + Ok(OptimizeOutcome::PartialFailure) => { + tracing::warn!("Partial failure compacting table {}", name); + } + Err(e) => { + tracing::warn!("Failed to compact table {}: {}", name, e); + } + } + } + + Ok(()) + } + /// Optimize a single table - runs compact, prune, and index operations /// /// Tables with fewer than 1000 rows are skipped since the overhead of @@ -1025,6 +1262,24 @@ impl SchemaManager { connection: &Connection, table_name: &str, ) -> Result { + // The lore table is indexed incrementally via + // ensure_lore_fts_indices() + optimize_lore_fts_indices(). + // Running the generic optimize path here does no useful work + // that those helpers have not already done, and for large + // lore archives its Compact phase walks every delta index + // fragment under _indices/, holding per-fragment state until + // the operation completes. On memory-constrained systems the + // resident set grows into swap and the OOM killer terminates + // semcode-index before compaction finishes, leaving fresh + // delta fragments behind each time. Skip the table entirely. + if table_name == "lore" { + tracing::info!( + "Skipping generic optimization for lore table \ + (handled by optimize_lore_fts_indices)" + ); + return Ok(OptimizeOutcome::Skipped); + } + // Minimum row count for optimization to be worthwhile const MIN_ROWS_FOR_OPTIMIZATION: usize = 1000; diff --git a/src/database/search.rs b/src/database/search.rs index 5528489..7145177 100644 --- a/src/database/search.rs +++ b/src/database/search.rs @@ -34,16 +34,16 @@ pub struct FunctionMatch { pub struct SearchManager { connection: Connection, - git_repo_path: String, + work_dir: String, content_store: ContentStore, } impl SearchManager { - pub fn new(connection: Connection, git_repo_path: String) -> Self { + pub fn new(connection: Connection, work_dir: String) -> Self { let content_store = ContentStore::new(connection.clone()); Self { connection, - git_repo_path, + work_dir, content_store, } } @@ -62,7 +62,7 @@ impl SearchManager { git_sha ); - match crate::git::resolve_files_at_commit(&self.git_repo_path, git_sha, file_paths) { + match crate::git::resolve_files_at_commit(&self.work_dir, git_sha, file_paths) { Ok(resolved_hashes) => { tracing::debug!( "resolve_git_file_hashes: Successfully resolved {} out of {} file paths", @@ -1787,6 +1787,7 @@ impl VectorSearchManager { /// Helper to query lore by fields and return intersection of message_ids /// Optimized for large result sets with capacity pre-allocation + #[allow(clippy::too_many_arguments)] async fn query_lore_fields_intersection( &self, from_patterns: Option<&[String]>, @@ -1795,18 +1796,35 @@ impl VectorSearchManager { symbols_patterns: Option<&[String]>, recipients_patterns: Option<&[String]>, search_limit: usize, + since_date: Option<&str>, + until_date: Option<&str>, ) -> Result> { use std::collections::HashSet; let lore_table = self.connection.open_table("lore").execute().await?; let mut field_result_sets: Vec> = Vec::new(); - // Helper function to query a field using substring matching and collect message_ids efficiently + // Parse date filters into DateTime for temporal comparison + // in query_field_impl (RFC 2822 string comparison is not + // meaningful for date ordering). + let since_dt = since_date + .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)); + let until_dt = until_date + .and_then(|d| chrono::DateTime::parse_from_rfc2822(d).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)); + + // Helper function to query a field using FTS with regex and + // date post-filtering. Selects the "date" column alongside + // the searched field so temporal filtering happens on the + // already-fetched FTS candidates without extra lookups. async fn query_field_impl( lore_table: &lancedb::Table, field_name: String, pattern: String, search_limit: usize, + since: Option>, + until: Option>, ) -> Result> { let start = std::time::Instant::now(); @@ -1826,11 +1844,12 @@ impl VectorSearchManager { let fts_query = FullTextSearchQuery::new(fts_pattern).with_column(field_name.clone())?; - let mut query = lore_table.query().full_text_search(fts_query).select( + let query = lore_table.query().full_text_search(fts_query).select( lancedb::query::Select::Columns(vec![ "message_id".to_string(), "_score".to_string(), field_name.clone(), + "date".to_string(), ]), ); @@ -1841,38 +1860,56 @@ impl VectorSearchManager { } else { 100000 }; - query = query.limit(effective_limit); + let query = query.limit(effective_limit); let results = query.execute().await?.try_collect::>().await?; - // Step 2: Post-filter results with regex in memory (small result set) + // Post-filter with regex and date range in memory let regex = regex::RegexBuilder::new(&pattern) .case_insensitive(true) .build()?; + let has_date_filter = since.is_some() || until.is_some(); let mut message_ids = HashSet::new(); + let mut bad_dates: usize = 0; for batch in &results { - let msg_array = batch - .column(0) - .as_any() - .downcast_ref::() - .unwrap(); - let field_array = batch - .column(2) - .as_any() - .downcast_ref::() - .unwrap(); + let msg_array: &arrow::array::StringArray = super::get_column(batch, "message_id")?; + let field_array: &arrow::array::StringArray = + super::get_column(batch, &field_name)?; + let date_array: &arrow::array::StringArray = super::get_column(batch, "date")?; for i in 0..batch.num_rows() { - let field_value = field_array.value(i); - if regex.is_match(field_value) { - message_ids.insert(msg_array.value(i).to_string()); + if !regex.is_match(field_array.value(i)) { + continue; } + if has_date_filter { + if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(date_array.value(i)) { + let dt_utc = dt.with_timezone(&chrono::Utc); + if since.is_some_and(|s| dt_utc < s) { + continue; + } + if until.is_some_and(|u| dt_utc > u) { + continue; + } + } else { + bad_dates += 1; + continue; + } + } + message_ids.insert(msg_array.value(i).to_string()); } } + if bad_dates > 0 { + tracing::warn!( + "Skipped {} candidates with unparseable dates for field '{}'", + bad_dates, + field_name + ); + } + tracing::info!( - "vlore filter completed: FTS returned {} candidates, regex filtered to {} in {:?}", + "vlore filter completed: FTS returned {} candidates, regex+date filtered to {} in {:?}", results.iter().map(|b| b.num_rows()).sum::(), message_ids.len(), start.elapsed() @@ -1881,88 +1918,103 @@ impl VectorSearchManager { Ok(message_ids) } - // Query from field + // Query from field (OR across patterns) if let Some(patterns) = from_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - field_result_sets.push( - query_field_impl( + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( &lore_table, "from".to_string(), - combined_pattern, + pattern.clone(), search_limit, + since_dt, + until_dt, ) - .await?, - ); + .await?; + field_union.extend(results); + } + field_result_sets.push(field_union); } } - // Query subject field + // Query subject field (OR across patterns) if let Some(patterns) = subject_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - field_result_sets.push( - query_field_impl( + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( &lore_table, "subject".to_string(), - combined_pattern, + pattern.clone(), search_limit, + since_dt, + until_dt, ) - .await?, - ); + .await?; + field_union.extend(results); + } + field_result_sets.push(field_union); } } - // Query body field + // Query body field (OR across patterns) if let Some(patterns) = body_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - field_result_sets.push( - query_field_impl( + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( &lore_table, "body".to_string(), - combined_pattern, + pattern.clone(), search_limit, + since_dt, + until_dt, ) - .await?, - ); + .await?; + field_union.extend(results); + } + field_result_sets.push(field_union); } } - // Query symbols field + // Query symbols field (OR across patterns) if let Some(patterns) = symbols_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - field_result_sets.push( - query_field_impl( + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( &lore_table, "symbols".to_string(), - combined_pattern, + pattern.clone(), search_limit, + since_dt, + until_dt, ) - .await?, - ); + .await?; + field_union.extend(results); + } + field_result_sets.push(field_union); } } - // Query recipients field + // Query recipients field (OR across patterns) if let Some(patterns) = recipients_patterns { if !patterns.is_empty() { - // FTS: join patterns with spaces for multi-keyword search - let combined_pattern = patterns.join(" "); - field_result_sets.push( - query_field_impl( + let mut field_union = HashSet::new(); + for pattern in patterns { + let results = query_field_impl( &lore_table, "recipients".to_string(), - combined_pattern, + pattern.clone(), search_limit, + since_dt, + until_dt, ) - .await?, - ); + .await?; + field_union.extend(results); + } + field_result_sets.push(field_union); } } @@ -3232,7 +3284,7 @@ impl VectorSearchManager { ); // Separate field filters from date filters - // Field filters affect which emails to search (FTS/regex), date filters affect final results + // Field filters select emails via FTS/regex; date filters narrow candidates during FTS post-filtering let has_field_filters = filters.from_patterns.is_some() || filters.subject_patterns.is_some() || filters.body_patterns.is_some() @@ -3242,39 +3294,13 @@ impl VectorSearchManager { let lore_vectors_table = self.connection.open_table("lore_vectors").execute().await?; let lore_table = self.connection.open_table("lore").execute().await?; - // Build date filter clause if needed - let date_filter = match (filters.since_date, filters.until_date) { - (Some(since), Some(until)) => { - let escaped_since = since.replace("'", "''"); - let escaped_until = until.replace("'", "''"); - Some(format!( - "date >= '{}' AND date <= '{}'", - escaped_since, escaped_until - )) - } - (Some(since), None) => { - let escaped_since = since.replace("'", "''"); - Some(format!("date >= '{}'", escaped_since)) - } - (None, Some(until)) => { - let escaped_until = until.replace("'", "''"); - Some(format!("date <= '{}'", escaped_until)) - } - (None, None) => None, - }; - - if let Some(ref filter) = date_filter { - tracing::info!( - "vlore: Date filter will be applied during email fetch: {}", - filter - ); - } + let has_date_filter = filters.since_date.is_some() || filters.until_date.is_some(); // No field filters (but may have date filter): simple vector search if !has_field_filters { // Note: LanceDB vector search on lore_vectors table doesn't have date column // We must fetch more candidates and filter in fetch_emails_by_ids - let fetch_multiplier = if date_filter.is_some() { + let fetch_multiplier = if has_date_filter { 50 // Significantly increase to ensure we get enough results after date filtering } else { 2 @@ -3352,6 +3378,8 @@ impl VectorSearchManager { filters.symbols_patterns, filters.recipients_patterns, search_limit, + filters.since_date, + filters.until_date, ) .await?; @@ -3385,16 +3413,10 @@ impl VectorSearchManager { // 3. Build score map from vector results let mut score_map = HashMap::new(); for batch in &vector_results { - let message_id_array = batch - .column(0) - .as_any() - .downcast_ref::() - .unwrap(); - let distance_array = batch - .column(2) - .as_any() - .downcast_ref::() - .unwrap(); + let message_id_array: &arrow::array::StringArray = + super::get_column(batch, "message_id")?; + let distance_array: &arrow::array::Float32Array = + super::get_column(batch, "_distance")?; for i in 0..batch.num_rows() { let message_id = message_id_array.value(i).to_string(); @@ -3431,7 +3453,9 @@ impl VectorSearchManager { continue; // Try larger search if no intersection found } - // 5. Fetch full email data for intersection (only what we need, up to limit) + // 5. Fetch full email data for intersection (up to limit). + // Date filtering was already applied in the FTS phase, + // so no date re-filtering is needed here. let ids_to_fetch: Vec = intersection_ids .iter() .take(limit) @@ -3444,8 +3468,8 @@ impl VectorSearchManager { &score_map, lore_table.clone(), limit, - filters.since_date, - filters.until_date, + None, + None, ) .await?; @@ -3527,61 +3551,26 @@ impl VectorSearchManager { .await?; for batch in &batches { - let git_commit_sha_array = batch - .column(0) - .as_any() - .downcast_ref::() - .unwrap(); - let from_array = batch - .column(1) - .as_any() - .downcast_ref::() - .unwrap(); - let date_array = batch - .column(2) - .as_any() - .downcast_ref::() - .unwrap(); + let git_commit_sha_array: &arrow::array::StringArray = + super::get_column(batch, "git_commit_sha")?; + let from_array: &arrow::array::StringArray = super::get_column(batch, "from")?; + let date_array: &arrow::array::StringArray = super::get_column(batch, "date")?; let date_timestamp_array = batch - .column(3) - .as_any() - .downcast_ref::() - .unwrap(); - let message_id_array = batch - .column(4) - .as_any() - .downcast_ref::() - .unwrap(); - let in_reply_to_array = batch - .column(5) - .as_any() - .downcast_ref::() - .unwrap(); - let subject_array = batch - .column(6) - .as_any() - .downcast_ref::() - .unwrap(); - let references_array = batch - .column(7) - .as_any() - .downcast_ref::() - .unwrap(); - let recipients_array = batch - .column(8) - .as_any() - .downcast_ref::() - .unwrap(); - let body_array = batch - .column(9) - .as_any() - .downcast_ref::() - .unwrap(); - let symbols_array = batch - .column(10) - .as_any() - .downcast_ref::() - .unwrap(); + .column_by_name("date_timestamp") + .and_then(|c| c.as_any().downcast_ref::()); + let message_id_array: &arrow::array::StringArray = + super::get_column(batch, "message_id")?; + let in_reply_to_array: &arrow::array::StringArray = + super::get_column(batch, "in_reply_to")?; + let subject_array: &arrow::array::StringArray = + super::get_column(batch, "subject")?; + let references_array: &arrow::array::StringArray = + super::get_column(batch, "references")?; + let recipients_array: &arrow::array::StringArray = + super::get_column(batch, "recipients")?; + let body_array: &arrow::array::StringArray = super::get_column(batch, "body")?; + let symbols_array: &arrow::array::StringArray = + super::get_column(batch, "symbols")?; for i in 0..batch.num_rows() { if results.len() >= limit { @@ -3642,8 +3631,9 @@ impl VectorSearchManager { let symbols: Vec = serde_json::from_str(symbols_json).unwrap_or_default(); - // Get date_timestamp from the batch - let date_timestamp = date_timestamp_array.value(i); + let date_timestamp = date_timestamp_array + .map(|arr| arr.value(i)) + .unwrap_or_else(|| email_datetime.timestamp()); results.push(( crate::types::LoreEmailInfo { diff --git a/src/database_utils.rs b/src/database_utils.rs index 7512c47..c45535d 100644 --- a/src/database_utils.rs +++ b/src/database_utils.rs @@ -9,7 +9,9 @@ use std::path::Path; /// 1. If `database_arg` is provided: /// - If it's a directory, look for `.semcode.db` within it /// - Otherwise, use the path as-is (direct database path) -/// 2. If `database_arg` is None: +/// 2. If `database_arg` is None, check the `SEMCODE_DB` environment variable +/// (same directory/suffix semantics as the `-d` flag) +/// 3. If neither is set: /// - For indexing operations: prefer `source_dir/.semcode.db`, fallback to current directory /// - For query operations: use current directory `./.semcode.db` /// @@ -21,23 +23,17 @@ use std::path::Path; /// String representation of the database path to use pub fn process_database_path(database_arg: Option<&str>, source_dir: Option<&Path>) -> String { match database_arg { - Some(path) => { - let path_obj = Path::new(path); - - // If path already ends with .semcode.db, use it as-is (avoid double appending) - if path.ends_with(".semcode.db") { - path.to_string() - } else if path_obj.is_dir() { - // If the path is a directory, look for .semcode.db within it - let semcode_db_path = path_obj.join(".semcode.db"); - semcode_db_path.to_string_lossy().to_string() - } else { - // If it's a specific file path, use it as-is - path.to_string() - } - } + Some(path) => resolve_path(path), None => { - // No -d flag provided - behavior depends on whether we have a source directory + // Check SEMCODE_DB environment variable before falling back to + // source-dir or current-dir defaults. + if let Ok(env_path) = std::env::var("SEMCODE_DB") { + let env_path = env_path.trim(); + if !env_path.is_empty() { + return resolve_path(env_path); + } + } + match source_dir { Some(source_path) => { // For indexing operations: prefer source directory unless it's current directory @@ -58,10 +54,29 @@ pub fn process_database_path(database_arg: Option<&str>, source_dir: Option<&Pat } } +/// Normalize a database path: append `.semcode.db` to directories, pass +/// paths that already end with `.semcode.db` through unchanged, and +/// return anything else as-is. +fn resolve_path(path: &str) -> String { + let path_obj = Path::new(path); + + if path.ends_with(".semcode.db") { + path.to_string() + } else if path_obj.is_dir() { + path_obj.join(".semcode.db").to_string_lossy().to_string() + } else { + path.to_string() + } +} + #[cfg(test)] mod tests { use super::*; use std::path::Path; + use std::sync::Mutex; + + /// Serializes tests that read or write the SEMCODE_DB environment variable. + static ENV_LOCK: Mutex<()> = Mutex::new(()); #[test] fn test_process_database_path_with_explicit_path() { @@ -83,24 +98,94 @@ mod tests { #[test] fn test_process_database_path_no_args_no_source() { - // Test query mode (no source directory) + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::remove_var("SEMCODE_DB"); + let result = process_database_path(None, None); assert_eq!(result, "./.semcode.db"); + + if let Some(v) = saved { + std::env::set_var("SEMCODE_DB", v); + } } #[test] fn test_process_database_path_no_args_with_source() { - // Test index mode with source directory + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::remove_var("SEMCODE_DB"); + let source_path = Path::new("/source/code"); let result = process_database_path(None, Some(source_path)); assert_eq!(result, "/source/code/.semcode.db"); + + if let Some(v) = saved { + std::env::set_var("SEMCODE_DB", v); + } } #[test] fn test_process_database_path_current_dir_source() { - // Test index mode with current directory as source + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::remove_var("SEMCODE_DB"); + let source_path = Path::new("."); let result = process_database_path(None, Some(source_path)); assert_eq!(result, "./.semcode.db"); + + if let Some(v) = saved { + std::env::set_var("SEMCODE_DB", v); + } + } + + #[test] + fn test_env_var_used_when_no_flag() { + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::set_var("SEMCODE_DB", "/data/my-project.semcode.db"); + + let result = process_database_path(None, None); + assert_eq!(result, "/data/my-project.semcode.db"); + + // Also overrides source_dir fallback + let result = process_database_path(None, Some(Path::new("/source/code"))); + assert_eq!(result, "/data/my-project.semcode.db"); + + match saved { + Some(v) => std::env::set_var("SEMCODE_DB", v), + None => std::env::remove_var("SEMCODE_DB"), + } + } + + #[test] + fn test_flag_overrides_env_var() { + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::set_var("SEMCODE_DB", "/env/path.semcode.db"); + + let result = process_database_path(Some("/flag/path.semcode.db"), None); + assert_eq!(result, "/flag/path.semcode.db"); + + match saved { + Some(v) => std::env::set_var("SEMCODE_DB", v), + None => std::env::remove_var("SEMCODE_DB"), + } + } + + #[test] + fn test_empty_env_var_ignored() { + let _guard = ENV_LOCK.lock().unwrap(); + let saved = std::env::var("SEMCODE_DB").ok(); + std::env::set_var("SEMCODE_DB", ""); + + let result = process_database_path(None, None); + assert_eq!(result, "./.semcode.db"); + + match saved { + Some(v) => std::env::set_var("SEMCODE_DB", v), + None => std::env::remove_var("SEMCODE_DB"), + } } } diff --git a/src/git.rs b/src/git.rs index 777bd79..c1b1915 100644 --- a/src/git.rs +++ b/src/git.rs @@ -33,6 +33,122 @@ pub fn resolve_to_commit<'a>(repo: &'a gix::Repository, revspec: &str) -> Result .try_into_commit() .map_err(|_| anyhow::anyhow!("'{}' does not resolve to a commit", revspec)) } + +/// Normalize path string to use forward slashes and trim `$HOME` prefix to `~` if applicable. +pub fn normalize_repo_path>(path: P) -> String { + let path = path.as_ref(); + let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); + let home = dirs::home_dir().map(|h| std::fs::canonicalize(&h).unwrap_or(h)); + + let path_buf = match home.as_ref().and_then(|h| canonical.strip_prefix(h).ok()) { + Some(rel) => PathBuf::from("~").join(rel), + None => canonical, + }; + + path_buf.to_string_lossy().replace('\\', "/") +} + +/// Get canonical repository root identity string (`repo_identity`). +/// Resolves linked worktrees and bare repos to their shared core `common_dir` path, +/// with `$HOME` normalized to `~` and backslashes normalized to `/`. +pub fn get_repo_root>(path: P) -> String { + let path = path.as_ref(); + let abs_input = if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir().unwrap_or_default().join(path) + }; + + let p = gix::discover(&abs_input) + .map(|repo| repo.common_dir().to_path_buf()) + .unwrap_or_else(|_| abs_input.clone()); + + let abs_p = if p.is_absolute() { + p + } else { + abs_input.join(p) + }; + + normalize_repo_path(abs_p) +} + +/// Helper to canonicalize a path or return its original PathBuf if canonicalization fails. +fn canonicalize_or_original(path: &Path) -> PathBuf { + std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) +} + +/// Canonicalize a path even if the final file or subdirectories do not exist yet on disk. +fn canonicalize_existing_ancestors(path: &Path) -> PathBuf { + if let Ok(canon) = std::fs::canonicalize(path) { + return canon; + } + let mut missing = Vec::new(); + let mut curr = path; + while let Some(parent) = curr.parent() { + if let Some(name) = curr.file_name() { + missing.push(name); + } + if let Ok(canon_parent) = std::fs::canonicalize(parent) { + let mut result = canon_parent; + for component in missing.into_iter().rev() { + result.push(component); + } + return result; + } + curr = parent; + } + path.to_path_buf() +} + +/// Determine if a database path (`db_path`) is local to a repository (`repo_path`). +/// Returns `(repo_identity, is_local_db)`. +pub fn determine_db_locality>(repo_path: P, db_path: &str) -> (String, bool) { + let repo_path = repo_path.as_ref(); + let git_repo_identity = get_repo_root(repo_path); + let mut local_roots = Vec::new(); + + if let Ok(repo) = gix::discover(repo_path) { + let common = canonicalize_or_original(repo.common_dir()); + if !repo.is_bare() && common.ends_with(".git") { + if let Some(main_root) = common.parent() { + local_roots.push(main_root.to_path_buf()); + } + } + local_roots.push(common); + + if let Some(workdir) = repo.workdir() { + local_roots.push(canonicalize_or_original(workdir)); + } + } else { + local_roots.push(canonicalize_or_original(repo_path)); + } + + let db_path_buf = PathBuf::from(db_path); + let preferred_root = local_roots.iter().rev().find(|p| p.is_dir()); + + let db_abs = if db_path_buf.is_absolute() { + db_path_buf + } else if let Some(target_root) = preferred_root { + target_root.join(db_path_buf) + } else { + std::env::current_dir() + .unwrap_or_default() + .join(db_path_buf) + }; + + let real_target = std::fs::read_link(&db_abs).unwrap_or_else(|_| db_abs.clone()); + let real_target_abs = if real_target.is_absolute() { + real_target + } else { + db_abs.parent().unwrap_or(Path::new(".")).join(real_target) + }; + + let db_canonical = canonicalize_existing_ancestors(&real_target_abs); + let is_local_db = local_roots.iter().any(|r| db_canonical.starts_with(r)); + + (git_repo_identity, is_local_db) +} + use once_cell::sync::Lazy; use std::collections::HashSet; use std::path::{Path, PathBuf}; @@ -223,6 +339,146 @@ pub fn get_git_file_hash_with_fallback>(file_path: P) -> Result Result> { let (from_spec, to_spec) = if range.contains("..") { let parts: Vec<&str> = range.split("..").collect(); @@ -107,14 +107,20 @@ pub fn list_shas_in_range(repo: &gix::Repository, range: &str) -> Result Some(from_commit.id().detach()), + Ok(from_commit) => { + let from_id = from_commit.id().detach(); + repo.rev_walk([to_id]) + .with_hidden([from_id]) + .sorting(Sorting::ByCommitTime(Default::default())) + .all()? + } Err(e) => { - // Parent commit not found (shallow clone or root commit). - // Fall back to returning just the target commit. info!( "Could not resolve '{}' ({}); falling back to single-commit indexing", from_spec, e @@ -125,18 +131,10 @@ pub fn list_shas_in_range(repo: &gix::Repository, range: &str) -> Result MAX_COMMITS { return Err(anyhow::anyhow!( @@ -146,20 +144,7 @@ pub fn list_shas_in_range(repo: &gix::Repository, range: &str) -> Result c, - Err(_) => break, - }; - let parent_ids: Vec<_> = commit.parent_ids().collect(); - - if parent_ids.is_empty() { - // Root commit, no more parents - break; - } - - current_id = parent_ids[0].detach(); + shas.push(info.id().to_string()); } // Reverse to get chronological order (oldest first) @@ -985,42 +970,57 @@ pub async fn process_lore_commits_pipeline( match batch { Ok(emails) => { - let batch_len = emails.len(); - // Insert batch into database - if let Err(e) = db_manager_clone.insert_lore_emails(&emails).await { - error!("Inserter {} failed to insert batch: {}", inserter_id, e); - } else { - // Record processed commit SHAs so they are - // not re-examined on subsequent runs. - let shas: Vec = emails - .iter() - .map(|e| e.git_commit_sha.as_str()) - .collect::>() - .into_iter() - .map(String::from) - .collect(); - if let Err(e) = - db_manager_clone.insert_lore_indexed_commits(&shas).await - { - error!( - "Inserter {} failed to record indexed commits: {}", - inserter_id, e - ); + match db_manager_clone.insert_lore_emails(&emails).await { + Err(e) => { + error!("Inserter {} failed to insert batch: {}", inserter_id, e); } + Ok(failed_indices) => { + let failed_set: std::collections::HashSet = + failed_indices.into_iter().collect(); + let success_count = emails.len() - failed_set.len(); + + // Record commit SHAs only for emails that + // were actually stored, so that failed + // emails are retried on the next run. + let shas: Vec = emails + .iter() + .enumerate() + .filter(|(i, _)| !failed_set.contains(i)) + .map(|(_, e)| e.git_commit_sha.as_str()) + .collect::>() + .into_iter() + .map(String::from) + .collect(); + if !shas.is_empty() { + if let Err(e) = + db_manager_clone.insert_lore_indexed_commits(&shas).await + { + error!( + "Inserter {} failed to record indexed commits: {}", + inserter_id, e + ); + } + } - let count = inserted_clone.fetch_add(batch_len, Ordering::Relaxed); - pb_clone.set_message(format!("Inserted {} emails", count + batch_len)); - - // Track successful batch insertions and check for periodic optimization - let total_batches = batches_counter.fetch_add(1, Ordering::Relaxed) + 1; - check_and_optimize_if_needed( - &db_manager_clone, - inserter_id, - total_batches, - &optimization_check_timer, - ) - .await; + let count = + inserted_clone.fetch_add(success_count, Ordering::Relaxed); + pb_clone.set_message(format!( + "Inserted {} emails", + count + success_count + )); + + // Track successful batch insertions and check for periodic optimization + let total_batches = + batches_counter.fetch_add(1, Ordering::Relaxed) + 1; + check_and_optimize_if_needed( + &db_manager_clone, + inserter_id, + total_batches, + &optimization_check_timer, + ) + .await; + } } } Err(_) => { @@ -1136,3 +1136,153 @@ pub async fn index_git_commits( Ok(commit_count) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper: run a git command in a directory, panic on failure. + fn git(repo: &std::path::Path, args: &[&str]) -> String { + let out = std::process::Command::new("git") + .args(args) + .current_dir(repo) + .env("GIT_AUTHOR_NAME", "test") + .env("GIT_AUTHOR_EMAIL", "test@test.com") + .env("GIT_COMMITTER_NAME", "test") + .env("GIT_COMMITTER_EMAIL", "test@test.com") + .env("GIT_CONFIG_GLOBAL", "/dev/null") + .env("GIT_CONFIG_SYSTEM", "/dev/null") + .output() + .expect("git command failed to execute"); + assert!( + out.status.success(), + "git {:?} failed: {}", + args, + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8_lossy(&out.stdout).trim().to_string() + } + + /// Build a repo with merge topology and a tag on the side branch: + /// + /// A -- B -- M -- D (main) + /// \ / + /// C --- (tagged "v1") + /// + /// Returns (tempdir, repo, sha_a, sha_b, sha_c, sha_m, sha_d). + fn create_merge_repo() -> ( + tempfile::TempDir, + gix::Repository, + String, + String, + String, + String, + String, + ) { + let tmpdir = tempfile::tempdir().unwrap(); + let p = tmpdir.path(); + + git(p, &["init", "-b", "main"]); + + // Commit A + std::fs::write(p.join("a.txt"), "a").unwrap(); + git(p, &["add", "a.txt"]); + git(p, &["commit", "-m", "A"]); + let sha_a = git(p, &["rev-parse", "HEAD"]); + + // Commit B + std::fs::write(p.join("b.txt"), "b").unwrap(); + git(p, &["add", "b.txt"]); + git(p, &["commit", "-m", "B"]); + let sha_b = git(p, &["rev-parse", "HEAD"]); + + // Side branch with commit C, tagged "v1" + git(p, &["checkout", "-b", "side"]); + std::fs::write(p.join("c.txt"), "c").unwrap(); + git(p, &["add", "c.txt"]); + git(p, &["commit", "-m", "C"]); + let sha_c = git(p, &["rev-parse", "HEAD"]); + git(p, &["tag", "v1"]); + + // Back to main, merge side → creates merge commit M + git(p, &["checkout", "main"]); + git(p, &["merge", "side", "--no-ff", "-m", "M"]); + let sha_m = git(p, &["rev-parse", "HEAD"]); + + // Commit D on top + std::fs::write(p.join("d.txt"), "d").unwrap(); + git(p, &["add", "d.txt"]); + git(p, &["commit", "-m", "D"]); + let sha_d = git(p, &["rev-parse", "HEAD"]); + + let repo = gix::open(p).unwrap(); + (tmpdir, repo, sha_a, sha_b, sha_c, sha_m, sha_d) + } + + /// Regression test: a tag on a side branch (not on the first-parent + /// chain) must still correctly bound the range. A first-parent-only + /// walk would miss it and traverse the entire history. + #[test] + fn test_list_shas_tag_on_side_branch() { + let (_tmp, repo, _sha_a, _sha_b, _sha_c, sha_m, sha_d) = create_merge_repo(); + + let shas = list_shas_in_range(&repo, "v1..HEAD").unwrap(); + + // v1 points at C (on the side branch). The range v1..HEAD + // should include M and D only — NOT A, B, or C. + assert!( + shas.contains(&sha_m), + "merge commit M should be in v1..HEAD" + ); + assert!(shas.contains(&sha_d), "commit D should be in v1..HEAD"); + assert_eq!( + shas.len(), + 2, + "v1..HEAD should contain exactly 2 commits (M, D), got {}: {:?}", + shas.len(), + shas + ); + } + + /// Range where the start IS on the first-parent chain works too. + #[test] + fn test_list_shas_first_parent_boundary() { + let (_tmp, repo, sha_a, sha_b, sha_c, sha_m, sha_d) = create_merge_repo(); + + let shas = list_shas_in_range(&repo, &format!("{sha_a}..HEAD")).unwrap(); + + // A..HEAD should include B, C, M, D (everything after A). + let expected: std::collections::HashSet<&str> = [ + sha_b.as_str(), + sha_c.as_str(), + sha_m.as_str(), + sha_d.as_str(), + ] + .into_iter() + .collect(); + let got: std::collections::HashSet<&str> = shas.iter().map(|s| s.as_str()).collect(); + + assert_eq!(expected, got, "A..HEAD should be {{B, C, M, D}}"); + } + + /// No range separator — returns all reachable commits. + #[test] + fn test_list_shas_no_range() { + let (_tmp, repo, sha_a, sha_b, sha_c, sha_m, sha_d) = create_merge_repo(); + + let shas = list_shas_in_range(&repo, "HEAD").unwrap(); + + let expected: std::collections::HashSet<&str> = [ + sha_a.as_str(), + sha_b.as_str(), + sha_c.as_str(), + sha_m.as_str(), + sha_d.as_str(), + ] + .into_iter() + .collect(); + let got: std::collections::HashSet<&str> = shas.iter().map(|s| s.as_str()).collect(); + + assert_eq!(expected, got, "HEAD (no range) should return all 5 commits"); + } +}