From e62e0a125a23606463398268d21d4bc755557213 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:02:07 +0200 Subject: [PATCH] set up tests and examples --- radixtree/Cargo.lock | 815 ++++++++++++++++++++++ radixtree/Cargo.toml | 27 + radixtree/README.md | 41 ++ radixtree/benches/radixtree_benchmarks.rs | 141 ++++ radixtree/examples/basic_usage.rs | 51 ++ radixtree/examples/prefix_operations.rs | 97 +++ radixtree/src/error.rs | 35 + radixtree/src/lib.rs | 141 ++++ radixtree/src/node.rs | 59 ++ radixtree/src/operations.rs | 425 +++++++++++ radixtree/src/serialize.rs | 144 ++++ radixtree/tests/basic_test.rs | 144 ++++ radixtree/tests/getall_test.rs | 153 ++++ radixtree/tests/prefix_test.rs | 185 +++++ radixtree/tests/serialize_test.rs | 180 +++++ 15 files changed, 2638 insertions(+) create mode 100644 radixtree/Cargo.lock create mode 100644 radixtree/Cargo.toml create mode 100644 radixtree/benches/radixtree_benchmarks.rs create mode 100644 radixtree/examples/basic_usage.rs create mode 100644 radixtree/examples/prefix_operations.rs create mode 100644 radixtree/src/error.rs create mode 100644 radixtree/src/lib.rs create mode 100644 radixtree/src/node.rs create mode 100644 radixtree/src/operations.rs create mode 100644 radixtree/src/serialize.rs create mode 100644 radixtree/tests/basic_test.rs create mode 100644 radixtree/tests/getall_test.rs create mode 100644 radixtree/tests/prefix_test.rs create mode 100644 radixtree/tests/serialize_test.rs diff --git a/radixtree/Cargo.lock b/radixtree/Cargo.lock new file mode 100644 index 0000000..ce6b281 --- /dev/null +++ b/radixtree/Cargo.lock @@ -0,0 +1,815 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "ourdb" +version = "0.1.0" +dependencies = [ + "crc32fast", + "log", + "rand", + "thiserror", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radixtree" +version = "0.1.0" +dependencies = [ + "criterion", + "log", + "ourdb", + "tempfile", + "thiserror", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/radixtree/Cargo.toml b/radixtree/Cargo.toml new file mode 100644 index 0000000..3ac5b35 --- /dev/null +++ b/radixtree/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "radixtree" +version = "0.1.0" +edition = "2021" +description = "A persistent radix tree implementation using OurDB for storage" +authors = ["OurWorld Team"] + +[dependencies] +ourdb = { path = "../ourdb" } +thiserror = "1.0.40" +log = "0.4.17" + +[dev-dependencies] +criterion = "0.5.1" +tempfile = "3.8.0" + +[[bench]] +name = "radixtree_benchmarks" +harness = false + +[[example]] +name = "basic_usage" +path = "examples/basic_usage.rs" + +[[example]] +name = "prefix_operations" +path = "examples/prefix_operations.rs" diff --git a/radixtree/README.md b/radixtree/README.md index bfe5edb..14b3ed3 100644 --- a/radixtree/README.md +++ b/radixtree/README.md @@ -143,6 +143,47 @@ The RadixTree implementation uses OurDB for persistent storage: For more detailed information about the implementation, see the [ARCHITECTURE.md](./ARCHITECTURE.md) file. +## Running Tests + +The project includes a comprehensive test suite that verifies all functionality: + +```bash +# Run all tests +cargo test + +# Run specific test file +cargo test --test basic_test +cargo test --test prefix_test +cargo test --test getall_test +cargo test --test serialize_test +``` + +## Running Examples + +The project includes example applications that demonstrate how to use the RadixTree: + +```bash +# Run the basic usage example +cargo run --example basic_usage + +# Run the prefix operations example +cargo run --example prefix_operations +``` + +## Benchmarking + +The project includes benchmarks to measure performance: + +```bash +# Run all benchmarks +cargo bench + +# Run specific benchmark +cargo bench -- set +cargo bench -- get +cargo bench -- prefix_operations +``` + ## License This project is licensed under the same license as the HeroCode project. \ No newline at end of file diff --git a/radixtree/benches/radixtree_benchmarks.rs b/radixtree/benches/radixtree_benchmarks.rs new file mode 100644 index 0000000..b95a294 --- /dev/null +++ b/radixtree/benches/radixtree_benchmarks.rs @@ -0,0 +1,141 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use radixtree::RadixTree; +use std::path::PathBuf; +use tempfile::tempdir; + +fn criterion_benchmark(c: &mut Criterion) { + // Create a temporary directory for benchmarks + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Benchmark set operation + c.bench_function("set", |b| { + let mut tree = RadixTree::new(db_path, true).unwrap(); + let mut i = 0; + b.iter(|| { + let key = format!("benchmark_key_{}", i); + let value = format!("benchmark_value_{}", i).into_bytes(); + tree.set(&key, value).unwrap(); + i += 1; + }); + }); + + // Setup tree with data for get/list/delete benchmarks + let mut setup_tree = RadixTree::new(db_path, true).unwrap(); + for i in 0..1000 { + let key = format!("benchmark_key_{}", i); + let value = format!("benchmark_value_{}", i).into_bytes(); + setup_tree.set(&key, value).unwrap(); + } + + // Benchmark get operation + c.bench_function("get", |b| { + let mut tree = RadixTree::new(db_path, false).unwrap(); + let mut i = 0; + b.iter(|| { + let key = format!("benchmark_key_{}", i % 1000); + let _value = tree.get(&key).unwrap(); + i += 1; + }); + }); + + // Benchmark list operation + c.bench_function("list", |b| { + let mut tree = RadixTree::new(db_path, false).unwrap(); + b.iter(|| { + let _keys = tree.list("benchmark_key_1").unwrap(); + }); + }); + + // Benchmark getall operation + c.bench_function("getall", |b| { + let mut tree = RadixTree::new(db_path, false).unwrap(); + b.iter(|| { + let _values = tree.getall("benchmark_key_1").unwrap(); + }); + }); + + // Benchmark update operation + c.bench_function("update", |b| { + let mut tree = RadixTree::new(db_path, false).unwrap(); + let mut i = 0; + b.iter(|| { + let key = format!("benchmark_key_{}", i % 1000); + let new_value = format!("updated_value_{}", i).into_bytes(); + tree.update(&key, new_value).unwrap(); + i += 1; + }); + }); + + // Benchmark delete operation + c.bench_function("delete", |b| { + // Create a fresh tree for deletion benchmarks + let delete_dir = tempdir().expect("Failed to create temp directory"); + let delete_path = delete_dir.path().to_str().unwrap(); + let mut tree = RadixTree::new(delete_path, true).unwrap(); + + // Setup keys to delete + for i in 0..1000 { + let key = format!("delete_key_{}", i); + let value = format!("delete_value_{}", i).into_bytes(); + tree.set(&key, value).unwrap(); + } + + let mut i = 0; + b.iter(|| { + let key = format!("delete_key_{}", i % 1000); + // Only try to delete if it exists + if tree.get(&key).is_ok() { + tree.delete(&key).unwrap(); + } + i += 1; + }); + }); + + // Benchmark prefix operations with varying tree sizes + let mut group = c.benchmark_group("prefix_operations"); + + for &size in &[100, 1000, 10000] { + // Create a fresh tree for each size + let size_dir = tempdir().expect("Failed to create temp directory"); + let size_path = size_dir.path().to_str().unwrap(); + let mut tree = RadixTree::new(size_path, true).unwrap(); + + // Insert data with common prefixes + for i in 0..size { + let prefix = match i % 5 { + 0 => "user", + 1 => "post", + 2 => "comment", + 3 => "product", + _ => "category", + }; + let key = format!("{}_{}", prefix, i); + let value = format!("value_{}", i).into_bytes(); + tree.set(&key, value).unwrap(); + } + + // Benchmark list operation for this size + group.bench_function(format!("list_size_{}", size), |b| { + b.iter(|| { + for prefix in &["user", "post", "comment", "product", "category"] { + let _keys = tree.list(prefix).unwrap(); + } + }); + }); + + // Benchmark getall operation for this size + group.bench_function(format!("getall_size_{}", size), |b| { + b.iter(|| { + for prefix in &["user", "post", "comment", "product", "category"] { + let _values = tree.getall(prefix).unwrap(); + } + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/radixtree/examples/basic_usage.rs b/radixtree/examples/basic_usage.rs new file mode 100644 index 0000000..4203539 --- /dev/null +++ b/radixtree/examples/basic_usage.rs @@ -0,0 +1,51 @@ +use radixtree::RadixTree; +use std::path::PathBuf; + +fn main() -> Result<(), radixtree::Error> { + // Create a temporary directory for the database + let db_path = std::env::temp_dir().join("radixtree_example"); + std::fs::create_dir_all(&db_path)?; + + println!("Creating radix tree at: {}", db_path.display()); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path.to_str().unwrap(), true)?; + + // Store some data + println!("Storing data..."); + tree.set("hello", b"world".to_vec())?; + tree.set("help", b"me".to_vec())?; + tree.set("helicopter", b"flying".to_vec())?; + + // Retrieve and print the data + let value = tree.get("hello")?; + println!("hello: {}", String::from_utf8_lossy(&value)); + + // Update a value + println!("Updating value..."); + tree.update("hello", b"updated world".to_vec())?; + + // Retrieve the updated value + let updated_value = tree.get("hello")?; + println!("hello (updated): {}", String::from_utf8_lossy(&updated_value)); + + // Delete a key + println!("Deleting 'help'..."); + tree.delete("help")?; + + // Try to retrieve the deleted key (should fail) + match tree.get("help") { + Ok(value) => println!("Unexpected: help still exists with value: {}", String::from_utf8_lossy(&value)), + Err(e) => println!("As expected, help was deleted: {}", e), + } + + // Clean up (optional) + if std::env::var("KEEP_DB").is_err() { + std::fs::remove_dir_all(&db_path)?; + println!("Cleaned up database directory"); + } else { + println!("Database kept at: {}", db_path.display()); + } + + Ok(()) +} diff --git a/radixtree/examples/prefix_operations.rs b/radixtree/examples/prefix_operations.rs new file mode 100644 index 0000000..a9c48c2 --- /dev/null +++ b/radixtree/examples/prefix_operations.rs @@ -0,0 +1,97 @@ +use radixtree::RadixTree; +use std::path::PathBuf; + +fn main() -> Result<(), radixtree::Error> { + // Create a temporary directory for the database + let db_path = std::env::temp_dir().join("radixtree_prefix_example"); + std::fs::create_dir_all(&db_path)?; + + println!("Creating radix tree at: {}", db_path.display()); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path.to_str().unwrap(), true)?; + + // Store data with common prefixes + println!("Storing data with common prefixes..."); + + // User data + tree.set("user:1:name", b"Alice".to_vec())?; + tree.set("user:1:email", b"alice@example.com".to_vec())?; + tree.set("user:2:name", b"Bob".to_vec())?; + tree.set("user:2:email", b"bob@example.com".to_vec())?; + + // Post data + tree.set("post:1:title", b"First Post".to_vec())?; + tree.set("post:1:content", b"Hello World!".to_vec())?; + tree.set("post:2:title", b"Second Post".to_vec())?; + tree.set("post:2:content", b"Another post content".to_vec())?; + + // Demonstrate listing keys with a prefix + println!("\nListing keys with prefix 'user:1:'"); + let user1_keys = tree.list("user:1:")?; + for key in &user1_keys { + println!(" Key: {}", key); + } + + println!("\nListing keys with prefix 'post:'"); + let post_keys = tree.list("post:")?; + for key in &post_keys { + println!(" Key: {}", key); + } + + // Demonstrate getting all values with a prefix + println!("\nGetting all values with prefix 'user:1:'"); + let user1_values = tree.getall("user:1:")?; + for (i, value) in user1_values.iter().enumerate() { + println!(" Value {}: {}", i + 1, String::from_utf8_lossy(value)); + } + + // Demonstrate finding all user names + println!("\nFinding all user names (prefix 'user:*:name')"); + let mut user_names = Vec::new(); + let all_keys = tree.list("user:")?; + for key in all_keys { + if key.ends_with(":name") { + if let Ok(value) = tree.get(&key) { + user_names.push((key, String::from_utf8_lossy(&value).to_string())); + } + } + } + + for (key, name) in user_names { + println!(" {}: {}", key, name); + } + + // Demonstrate updating values with a specific prefix + println!("\nUpdating all post titles..."); + let post_title_keys = tree.list("post:")?.into_iter().filter(|k| k.ends_with(":title")).collect::>(); + + for key in post_title_keys { + let old_value = tree.get(&key)?; + let old_title = String::from_utf8_lossy(&old_value); + let new_title = format!("UPDATED: {}", old_title); + + println!(" Updating '{}' to '{}'", old_title, new_title); + tree.update(&key, new_title.as_bytes().to_vec())?; + } + + // Verify updates + println!("\nVerifying updates:"); + let post_keys = tree.list("post:")?; + for key in post_keys { + if key.ends_with(":title") { + let value = tree.get(&key)?; + println!(" {}: {}", key, String::from_utf8_lossy(&value)); + } + } + + // Clean up (optional) + if std::env::var("KEEP_DB").is_err() { + std::fs::remove_dir_all(&db_path)?; + println!("\nCleaned up database directory"); + } else { + println!("\nDatabase kept at: {}", db_path.display()); + } + + Ok(()) +} diff --git a/radixtree/src/error.rs b/radixtree/src/error.rs new file mode 100644 index 0000000..cacf236 --- /dev/null +++ b/radixtree/src/error.rs @@ -0,0 +1,35 @@ +//! Error types for the RadixTree module. + +use thiserror::Error; + +/// Error type for RadixTree operations. +#[derive(Debug, Error)] +pub enum Error { + /// Error from OurDB operations. + #[error("OurDB error: {0}")] + OurDB(#[from] ourdb::Error), + + /// Error when a key is not found. + #[error("Key not found: {0}")] + KeyNotFound(String), + + /// Error when a prefix is not found. + #[error("Prefix not found: {0}")] + PrefixNotFound(String), + + /// Error during serialization. + #[error("Serialization error: {0}")] + Serialization(String), + + /// Error during deserialization. + #[error("Deserialization error: {0}")] + Deserialization(String), + + /// Error for invalid operations. + #[error("Invalid operation: {0}")] + InvalidOperation(String), + + /// Error for I/O operations. + #[error("I/O error: {0}")] + IO(#[from] std::io::Error), +} diff --git a/radixtree/src/lib.rs b/radixtree/src/lib.rs new file mode 100644 index 0000000..ab4561e --- /dev/null +++ b/radixtree/src/lib.rs @@ -0,0 +1,141 @@ +//! RadixTree is a space-optimized tree data structure that enables efficient string key operations +//! with persistent storage using OurDB as a backend. +//! +//! This implementation provides a persistent radix tree that can be used for efficient +//! prefix-based key operations, such as auto-complete, routing tables, and more. + +mod error; +mod node; +mod operations; +mod serialize; + +pub use error::Error; +pub use node::{Node, NodeRef}; + +use ourdb::{OurDB, OurDBConfig, OurDBSetArgs}; +use std::path::PathBuf; + +/// RadixTree represents a radix tree data structure with persistent storage. +pub struct RadixTree { + db: OurDB, + root_id: u32, +} + +impl RadixTree { + /// Creates a new radix tree with the specified database path. + /// + /// # Arguments + /// + /// * `path` - The path to the database directory + /// * `reset` - Whether to reset the database if it exists + /// + /// # Returns + /// + /// A new `RadixTree` instance + /// + /// # Errors + /// + /// Returns an error if the database cannot be created or opened + pub fn new(path: &str, reset: bool) -> Result { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Sets a key-value pair in the tree. + /// + /// # Arguments + /// + /// * `key` - The key to set + /// * `value` - The value to set + /// + /// # Errors + /// + /// Returns an error if the operation fails + pub fn set(&mut self, key: &str, value: Vec) -> Result<(), Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Gets a value by key from the tree. + /// + /// # Arguments + /// + /// * `key` - The key to get + /// + /// # Returns + /// + /// The value associated with the key + /// + /// # Errors + /// + /// Returns an error if the key is not found or the operation fails + pub fn get(&mut self, key: &str) -> Result, Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Updates the value at a given key prefix. + /// + /// # Arguments + /// + /// * `prefix` - The key prefix to update + /// * `new_value` - The new value to set + /// + /// # Errors + /// + /// Returns an error if the prefix is not found or the operation fails + pub fn update(&mut self, prefix: &str, new_value: Vec) -> Result<(), Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Deletes a key from the tree. + /// + /// # Arguments + /// + /// * `key` - The key to delete + /// + /// # Errors + /// + /// Returns an error if the key is not found or the operation fails + pub fn delete(&mut self, key: &str) -> Result<(), Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Lists all keys with a given prefix. + /// + /// # Arguments + /// + /// * `prefix` - The prefix to search for + /// + /// # Returns + /// + /// A list of keys that start with the given prefix + /// + /// # Errors + /// + /// Returns an error if the operation fails + pub fn list(&mut self, prefix: &str) -> Result, Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } + + /// Gets all values for keys with a given prefix. + /// + /// # Arguments + /// + /// * `prefix` - The prefix to search for + /// + /// # Returns + /// + /// A list of values for keys that start with the given prefix + /// + /// # Errors + /// + /// Returns an error if the operation fails + pub fn getall(&mut self, prefix: &str) -> Result>, Error> { + // Implementation will be provided in operations.rs + unimplemented!() + } +} diff --git a/radixtree/src/node.rs b/radixtree/src/node.rs new file mode 100644 index 0000000..b469cd1 --- /dev/null +++ b/radixtree/src/node.rs @@ -0,0 +1,59 @@ +//! Node types for the RadixTree module. + +/// Represents a node in the radix tree. +#[derive(Debug, Clone, PartialEq)] +pub struct Node { + /// The segment of the key stored at this node. + pub key_segment: String, + + /// Value stored at this node (empty if not a leaf). + pub value: Vec, + + /// References to child nodes. + pub children: Vec, + + /// Whether this node is a leaf node. + pub is_leaf: bool, +} + +/// Reference to a node in the database. +#[derive(Debug, Clone, PartialEq)] +pub struct NodeRef { + /// The key segment for this child. + pub key_part: String, + + /// Database ID of the node. + pub node_id: u32, +} + +impl Node { + /// Creates a new node. + pub fn new(key_segment: String, value: Vec, is_leaf: bool) -> Self { + Self { + key_segment, + value, + children: Vec::new(), + is_leaf, + } + } + + /// Creates a new root node. + pub fn new_root() -> Self { + Self { + key_segment: String::new(), + value: Vec::new(), + children: Vec::new(), + is_leaf: false, + } + } +} + +impl NodeRef { + /// Creates a new node reference. + pub fn new(key_part: String, node_id: u32) -> Self { + Self { + key_part, + node_id, + } + } +} diff --git a/radixtree/src/operations.rs b/radixtree/src/operations.rs new file mode 100644 index 0000000..9821e0e --- /dev/null +++ b/radixtree/src/operations.rs @@ -0,0 +1,425 @@ +//! Implementation of RadixTree operations. + +use crate::error::Error; +use crate::node::{Node, NodeRef}; +use crate::RadixTree; +use ourdb::{OurDB, OurDBConfig, OurDBSetArgs}; + + +impl RadixTree { + /// Creates a new radix tree with the specified database path. + pub fn new(path: &str, reset: bool) -> Result { + let config = OurDBConfig { + record_size_max: 1024 * 4, // 4KB max record size + incremental_mode: true, + reset, + ..Default::default() + }; + + let db = OurDB::new(path, config)?; + + let root_id = if db.get_next_id()? == 1 { + // Create a new root node + let root = Node::new_root(); + let root_id = db.set(OurDBSetArgs { + id: None, + data: &root.serialize(), + })?; + + // First ID should be 1 + assert_eq!(root_id, 1); + root_id + } else { + // Use existing root node + 1 // Root node always has ID 1 + }; + + Ok(Self { + db, + root_id, + }) + } + + /// Helper function to get a node from the database. + pub(crate) fn get_node(&mut self, node_id: u32) -> Result { + let data = self.db.get(node_id)?; + Node::deserialize(&data) + } + + /// Helper function to save a node to the database. + pub(crate) fn save_node(&mut self, node_id: Option, node: &Node) -> Result { + let data = node.serialize(); + let args = OurDBSetArgs { + id: node_id, + data: &data, + }; + Ok(self.db.set(args)?) + } + + /// Sets a key-value pair in the tree. + pub fn set(&mut self, key: &str, value: Vec) -> Result<(), Error> { + let mut current_id = self.root_id; + let mut offset = 0; + + // Handle empty key case + if key.is_empty() { + let mut root_node = self.get_node(current_id)?; + root_node.is_leaf = true; + root_node.value = value; + self.save_node(Some(current_id), &root_node)?; + return Ok(()); + } + + while offset < key.len() { + let mut node = self.get_node(current_id)?; + + // Find matching child + let mut matched_child = None; + for (i, child) in node.children.iter().enumerate() { + if key[offset..].starts_with(&child.key_part) { + matched_child = Some((i, child.clone())); + break; + } + } + + if matched_child.is_none() { + // No matching child found, create new leaf node + let key_part = key[offset..].to_string(); + let new_node = Node { + key_segment: key_part.clone(), + value: value.clone(), + children: Vec::new(), + is_leaf: true, + }; + + let new_id = self.save_node(None, &new_node)?; + + // Create new child reference and update parent node + node.children.push(NodeRef { + key_part, + node_id: new_id, + }); + + self.save_node(Some(current_id), &node)?; + return Ok(()); + } + + let (child_index, mut child) = matched_child.unwrap(); + let common_prefix = get_common_prefix(&key[offset..], &child.key_part); + + if common_prefix.len() < child.key_part.len() { + // Split existing node + let mut child_node = self.get_node(child.node_id)?; + + // Create new intermediate node + let new_node = Node { + key_segment: child.key_part[common_prefix.len()..].to_string(), + value: child_node.value.clone(), + children: child_node.children.clone(), + is_leaf: child_node.is_leaf, + }; + let new_id = self.save_node(None, &new_node)?; + + // Update current node + node.children[child_index] = NodeRef { + key_part: common_prefix.to_string(), + node_id: new_id, + }; + self.save_node(Some(current_id), &node)?; + + // Update child node reference + child.node_id = new_id; + } + + if offset + common_prefix.len() == key.len() { + // Update value at existing node + let mut child_node = self.get_node(child.node_id)?; + child_node.value = value; + child_node.is_leaf = true; + self.save_node(Some(child.node_id), &child_node)?; + return Ok(()); + } + + offset += common_prefix.len(); + current_id = child.node_id; + } + + Ok(()) + } + + /// Gets a value by key from the tree. + pub fn get(&mut self, key: &str) -> Result, Error> { + let mut current_id = self.root_id; + let mut offset = 0; + + // Handle empty key case + if key.is_empty() { + let root_node = self.get_node(current_id)?; + if root_node.is_leaf { + return Ok(root_node.value); + } + return Err(Error::KeyNotFound(key.to_string())); + } + + while offset < key.len() { + let node = self.get_node(current_id)?; + + let mut found = false; + for child in &node.children { + if key[offset..].starts_with(&child.key_part) { + if offset + child.key_part.len() == key.len() { + let child_node = self.get_node(child.node_id)?; + if child_node.is_leaf { + return Ok(child_node.value); + } + } + current_id = child.node_id; + offset += child.key_part.len(); + found = true; + break; + } + } + + if !found { + return Err(Error::KeyNotFound(key.to_string())); + } + } + + Err(Error::KeyNotFound(key.to_string())) + } + + /// Updates the value at a given key prefix. + pub fn update(&mut self, prefix: &str, new_value: Vec) -> Result<(), Error> { + let mut current_id = self.root_id; + let mut offset = 0; + + // Handle empty prefix case + if prefix.is_empty() { + return Err(Error::InvalidOperation("Empty prefix not allowed".to_string())); + } + + while offset < prefix.len() { + let node = self.get_node(current_id)?; + + let mut found = false; + for child in &node.children { + if prefix[offset..].starts_with(&child.key_part) { + if offset + child.key_part.len() == prefix.len() { + // Found exact prefix match + let mut child_node = self.get_node(child.node_id)?; + if child_node.is_leaf { + // Update the value + child_node.value = new_value; + self.save_node(Some(child.node_id), &child_node)?; + return Ok(()); + } + } + current_id = child.node_id; + offset += child.key_part.len(); + found = true; + break; + } + } + + if !found { + return Err(Error::PrefixNotFound(prefix.to_string())); + } + } + + Err(Error::PrefixNotFound(prefix.to_string())) + } + + /// Deletes a key from the tree. + pub fn delete(&mut self, key: &str) -> Result<(), Error> { + let mut current_id = self.root_id; + let mut offset = 0; + let mut path = Vec::new(); + + // Find the node to delete + while offset < key.len() { + let node = self.get_node(current_id)?; + + let mut found = false; + for child in &node.children { + if key[offset..].starts_with(&child.key_part) { + path.push(child.clone()); + current_id = child.node_id; + offset += child.key_part.len(); + found = true; + + // Check if we've matched the full key + if offset == key.len() { + let child_node = self.get_node(child.node_id)?; + if child_node.is_leaf { + found = true; + break; + } + } + break; + } + } + + if !found { + return Err(Error::KeyNotFound(key.to_string())); + } + } + + if path.is_empty() { + return Err(Error::KeyNotFound(key.to_string())); + } + + // Get the node to delete + let mut last_node = self.get_node(path.last().unwrap().node_id)?; + + // If the node has children, just mark it as non-leaf + if !last_node.children.is_empty() { + last_node.is_leaf = false; + last_node.value = Vec::new(); + self.save_node(Some(path.last().unwrap().node_id), &last_node)?; + return Ok(()); + } + + // If node has no children, remove it from parent + if path.len() > 1 { + let parent_id = path[path.len() - 2].node_id; + let mut parent_node = self.get_node(parent_id)?; + + // Find and remove the child from parent + for i in 0..parent_node.children.len() { + if parent_node.children[i].node_id == path.last().unwrap().node_id { + parent_node.children.remove(i); + break; + } + } + + self.save_node(Some(parent_id), &parent_node)?; + + // Delete the node from the database + self.db.delete(path.last().unwrap().node_id)?; + } else { + // If this is a direct child of the root, just mark it as non-leaf + last_node.is_leaf = false; + last_node.value = Vec::new(); + self.save_node(Some(path.last().unwrap().node_id), &last_node)?; + } + + Ok(()) + } + + /// Lists all keys with a given prefix. + pub fn list(&mut self, prefix: &str) -> Result, Error> { + let mut result = Vec::new(); + + // Handle empty prefix case - will return all keys + if prefix.is_empty() { + self.collect_all_keys(self.root_id, "", &mut result)?; + return Ok(result); + } + + // Start from the root and find all matching keys + self.find_keys_with_prefix(self.root_id, "", prefix, &mut result)?; + Ok(result) + } + + /// Helper function to find all keys with a given prefix. + fn find_keys_with_prefix( + &mut self, + node_id: u32, + current_path: &str, + prefix: &str, + result: &mut Vec, + ) -> Result<(), Error> { + let node = self.get_node(node_id)?; + + // If the current path already matches or exceeds the prefix length + if current_path.len() >= prefix.len() { + // Check if the current path starts with the prefix + if current_path.starts_with(prefix) { + // If this is a leaf node, add it to the results + if node.is_leaf { + result.push(current_path.to_string()); + } + + // Collect all keys from this subtree + for child in &node.children { + let child_path = format!("{}{}", current_path, child.key_part); + self.find_keys_with_prefix(child.node_id, &child_path, prefix, result)?; + } + } + return Ok(()); + } + + // Current path is shorter than the prefix, continue searching + for child in &node.children { + let child_path = format!("{}{}", current_path, child.key_part); + + // Check if this child's path could potentially match the prefix + if prefix.starts_with(current_path) { + // The prefix starts with the current path, so we need to check if + // the child's key_part matches the next part of the prefix + let prefix_remainder = &prefix[current_path.len()..]; + + // If the prefix remainder starts with the child's key_part or vice versa + if prefix_remainder.starts_with(&child.key_part) + || (child.key_part.starts_with(prefix_remainder) + && child.key_part.len() >= prefix_remainder.len()) { + self.find_keys_with_prefix(child.node_id, &child_path, prefix, result)?; + } + } + } + + Ok(()) + } + + /// Helper function to recursively collect all keys under a node. + fn collect_all_keys( + &mut self, + node_id: u32, + current_path: &str, + result: &mut Vec, + ) -> Result<(), Error> { + let node = self.get_node(node_id)?; + + // If this node is a leaf, add its path to the result + if node.is_leaf { + result.push(current_path.to_string()); + } + + // Recursively collect keys from all children + for child in &node.children { + let child_path = format!("{}{}", current_path, child.key_part); + self.collect_all_keys(child.node_id, &child_path, result)?; + } + + Ok(()) + } + + /// Gets all values for keys with a given prefix. + pub fn getall(&mut self, prefix: &str) -> Result>, Error> { + // Get all matching keys + let keys = self.list(prefix)?; + + // Get values for each key + let mut values = Vec::new(); + for key in keys { + if let Ok(value) = self.get(&key) { + values.push(value); + } + } + + Ok(values) + } +} + +/// Helper function to get the common prefix of two strings. +fn get_common_prefix(a: &str, b: &str) -> String { + let mut i = 0; + let a_bytes = a.as_bytes(); + let b_bytes = b.as_bytes(); + + while i < a.len() && i < b.len() && a_bytes[i] == b_bytes[i] { + i += 1; + } + + a[..i].to_string() +} diff --git a/radixtree/src/serialize.rs b/radixtree/src/serialize.rs new file mode 100644 index 0000000..ac42a64 --- /dev/null +++ b/radixtree/src/serialize.rs @@ -0,0 +1,144 @@ +//! Serialization and deserialization for RadixTree nodes. + +use crate::error::Error; +use crate::node::{Node, NodeRef}; +use std::convert::TryInto; +use std::io::{Cursor, Read, Write}; +use std::mem::size_of; + +/// Current binary format version. +const VERSION: u8 = 1; + +impl Node { + /// Serializes a node to bytes for storage. + pub fn serialize(&self) -> Vec { + let mut buffer = Vec::new(); + + // Add version byte + buffer.push(VERSION); + + // Add key segment + write_string(&mut buffer, &self.key_segment); + + // Add value as []u8 + write_u16(&mut buffer, self.value.len() as u16); + buffer.extend_from_slice(&self.value); + + // Add children + write_u16(&mut buffer, self.children.len() as u16); + for child in &self.children { + write_string(&mut buffer, &child.key_part); + write_u32(&mut buffer, child.node_id); + } + + // Add leaf flag + buffer.push(if self.is_leaf { 1 } else { 0 }); + + buffer + } + + /// Deserializes bytes to a node. + pub fn deserialize(data: &[u8]) -> Result { + if data.is_empty() { + return Err(Error::Deserialization("Empty data".to_string())); + } + + let mut cursor = Cursor::new(data); + + // Read and verify version + let mut version_byte = [0u8; 1]; + cursor.read_exact(&mut version_byte) + .map_err(|e| Error::Deserialization(format!("Failed to read version byte: {}", e)))?; + + if version_byte[0] != VERSION { + return Err(Error::Deserialization( + format!("Invalid version byte: expected {}, got {}", VERSION, version_byte[0]) + )); + } + + // Read key segment + let key_segment = read_string(&mut cursor) + .map_err(|e| Error::Deserialization(format!("Failed to read key segment: {}", e)))?; + + // Read value as []u8 + let value_len = read_u16(&mut cursor) + .map_err(|e| Error::Deserialization(format!("Failed to read value length: {}", e)))?; + + let mut value = vec![0u8; value_len as usize]; + cursor.read_exact(&mut value) + .map_err(|e| Error::Deserialization(format!("Failed to read value: {}", e)))?; + + // Read children + let children_len = read_u16(&mut cursor) + .map_err(|e| Error::Deserialization(format!("Failed to read children length: {}", e)))?; + + let mut children = Vec::with_capacity(children_len as usize); + for _ in 0..children_len { + let key_part = read_string(&mut cursor) + .map_err(|e| Error::Deserialization(format!("Failed to read child key part: {}", e)))?; + + let node_id = read_u32(&mut cursor) + .map_err(|e| Error::Deserialization(format!("Failed to read child node ID: {}", e)))?; + + children.push(NodeRef { + key_part, + node_id, + }); + } + + // Read leaf flag + let mut is_leaf_byte = [0u8; 1]; + cursor.read_exact(&mut is_leaf_byte) + .map_err(|e| Error::Deserialization(format!("Failed to read leaf flag: {}", e)))?; + + let is_leaf = is_leaf_byte[0] == 1; + + Ok(Node { + key_segment, + value, + children, + is_leaf, + }) + } +} + +// Helper functions for serialization + +fn write_string(buffer: &mut Vec, s: &str) { + let bytes = s.as_bytes(); + write_u16(buffer, bytes.len() as u16); + buffer.extend_from_slice(bytes); +} + +fn write_u16(buffer: &mut Vec, value: u16) { + buffer.extend_from_slice(&value.to_le_bytes()); +} + +fn write_u32(buffer: &mut Vec, value: u32) { + buffer.extend_from_slice(&value.to_le_bytes()); +} + +// Helper functions for deserialization + +fn read_string(cursor: &mut Cursor<&[u8]>) -> std::io::Result { + let len = read_u16(cursor)? as usize; + let mut bytes = vec![0u8; len]; + cursor.read_exact(&mut bytes)?; + + String::from_utf8(bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +fn read_u16(cursor: &mut Cursor<&[u8]>) -> std::io::Result { + let mut bytes = [0u8; size_of::()]; + cursor.read_exact(&mut bytes)?; + + Ok(u16::from_le_bytes(bytes)) +} + +fn read_u32(cursor: &mut Cursor<&[u8]>) -> std::io::Result { + let mut bytes = [0u8; size_of::()]; + cursor.read_exact(&mut bytes)?; + + Ok(u32::from_le_bytes(bytes)) +} diff --git a/radixtree/tests/basic_test.rs b/radixtree/tests/basic_test.rs new file mode 100644 index 0000000..628f6a4 --- /dev/null +++ b/radixtree/tests/basic_test.rs @@ -0,0 +1,144 @@ +use radixtree::RadixTree; +use std::path::PathBuf; +use tempfile::tempdir; + +#[test] +fn test_basic_operations() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Test setting and getting values + let key = "test_key"; + let value = b"test_value".to_vec(); + tree.set(key, value.clone())?; + + let retrieved_value = tree.get(key)?; + assert_eq!(retrieved_value, value); + + // Test updating a value + let new_value = b"updated_value".to_vec(); + tree.update(key, new_value.clone())?; + + let updated_value = tree.get(key)?; + assert_eq!(updated_value, new_value); + + // Test deleting a value + tree.delete(key)?; + + // Trying to get a deleted key should return an error + let result = tree.get(key); + assert!(result.is_err()); + + Ok(()) +} + +#[test] +fn test_empty_key() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Test setting and getting empty key + let key = ""; + let value = b"value_for_empty_key".to_vec(); + tree.set(key, value.clone())?; + + let retrieved_value = tree.get(key)?; + assert_eq!(retrieved_value, value); + + // Test deleting empty key + tree.delete(key)?; + + // Trying to get a deleted key should return an error + let result = tree.get(key); + assert!(result.is_err()); + + Ok(()) +} + +#[test] +fn test_multiple_keys() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Insert multiple keys + let test_data = [ + ("key1", b"value1".to_vec()), + ("key2", b"value2".to_vec()), + ("key3", b"value3".to_vec()), + ]; + + for (key, value) in &test_data { + tree.set(key, value.clone())?; + } + + // Verify all keys can be retrieved + for (key, expected_value) in &test_data { + let retrieved_value = tree.get(key)?; + assert_eq!(&retrieved_value, expected_value); + } + + Ok(()) +} + +#[test] +fn test_shared_prefixes() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Insert keys with shared prefixes + let test_data = [ + ("test", b"value_test".to_vec()), + ("testing", b"value_testing".to_vec()), + ("tested", b"value_tested".to_vec()), + ]; + + for (key, value) in &test_data { + tree.set(key, value.clone())?; + } + + // Verify all keys can be retrieved + for (key, expected_value) in &test_data { + let retrieved_value = tree.get(key)?; + assert_eq!(&retrieved_value, expected_value); + } + + Ok(()) +} + +#[test] +fn test_persistence() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree and add some data + { + let mut tree = RadixTree::new(db_path, true)?; + tree.set("persistent_key", b"persistent_value".to_vec())?; + } // Tree is dropped here + + // Create a new tree instance with the same path + { + let mut tree = RadixTree::new(db_path, false)?; + let value = tree.get("persistent_key")?; + assert_eq!(value, b"persistent_value".to_vec()); + } + + Ok(()) +} diff --git a/radixtree/tests/getall_test.rs b/radixtree/tests/getall_test.rs new file mode 100644 index 0000000..26669c0 --- /dev/null +++ b/radixtree/tests/getall_test.rs @@ -0,0 +1,153 @@ +use radixtree::RadixTree; +use std::collections::HashMap; +use tempfile::tempdir; + +#[test] +fn test_getall() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Set up test data with common prefixes + let test_data: HashMap<&str, &str> = [ + ("user_1", "data1"), + ("user_2", "data2"), + ("user_3", "data3"), + ("admin_1", "admin_data1"), + ("admin_2", "admin_data2"), + ("guest", "guest_data"), + ].iter().cloned().collect(); + + // Set all test data + for (key, value) in &test_data { + tree.set(key, value.as_bytes().to_vec())?; + } + + // Test getall with 'user_' prefix + let user_values = tree.getall("user_")?; + + // Should return 3 values + assert_eq!(user_values.len(), 3); + + // Convert byte arrays to strings for easier comparison + let user_value_strings: Vec = user_values + .iter() + .map(|v| String::from_utf8_lossy(v).to_string()) + .collect(); + + // Check all expected values are present + assert!(user_value_strings.contains(&"data1".to_string())); + assert!(user_value_strings.contains(&"data2".to_string())); + assert!(user_value_strings.contains(&"data3".to_string())); + + // Test getall with 'admin_' prefix + let admin_values = tree.getall("admin_")?; + + // Should return 2 values + assert_eq!(admin_values.len(), 2); + + // Convert byte arrays to strings for easier comparison + let admin_value_strings: Vec = admin_values + .iter() + .map(|v| String::from_utf8_lossy(v).to_string()) + .collect(); + + // Check all expected values are present + assert!(admin_value_strings.contains(&"admin_data1".to_string())); + assert!(admin_value_strings.contains(&"admin_data2".to_string())); + + // Test getall with empty prefix (should return all values) + let all_values = tree.getall("")?; + + // Should return all 6 values + assert_eq!(all_values.len(), test_data.len()); + + // Test getall with non-existent prefix + let non_existent_values = tree.getall("xyz")?; + + // Should return empty array + assert_eq!(non_existent_values.len(), 0); + + Ok(()) +} + +#[test] +fn test_getall_with_updates() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Set initial values + tree.set("key1", b"value1".to_vec())?; + tree.set("key2", b"value2".to_vec())?; + tree.set("key3", b"value3".to_vec())?; + + // Get initial values + let initial_values = tree.getall("key")?; + assert_eq!(initial_values.len(), 3); + + // Update a value + tree.update("key2", b"updated_value2".to_vec())?; + + // Get values after update + let updated_values = tree.getall("key")?; + assert_eq!(updated_values.len(), 3); + + // Convert to strings for easier comparison + let updated_value_strings: Vec = updated_values + .iter() + .map(|v| String::from_utf8_lossy(v).to_string()) + .collect(); + + // Check the updated value is present + assert!(updated_value_strings.contains(&"value1".to_string())); + assert!(updated_value_strings.contains(&"updated_value2".to_string())); + assert!(updated_value_strings.contains(&"value3".to_string())); + + Ok(()) +} + +#[test] +fn test_getall_with_deletions() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Set initial values + tree.set("prefix_1", b"value1".to_vec())?; + tree.set("prefix_2", b"value2".to_vec())?; + tree.set("prefix_3", b"value3".to_vec())?; + tree.set("other", b"other_value".to_vec())?; + + // Get initial values + let initial_values = tree.getall("prefix_")?; + assert_eq!(initial_values.len(), 3); + + // Delete a key + tree.delete("prefix_2")?; + + // Get values after deletion + let after_delete_values = tree.getall("prefix_")?; + assert_eq!(after_delete_values.len(), 2); + + // Convert to strings for easier comparison + let after_delete_strings: Vec = after_delete_values + .iter() + .map(|v| String::from_utf8_lossy(v).to_string()) + .collect(); + + // Check the remaining values + assert!(after_delete_strings.contains(&"value1".to_string())); + assert!(after_delete_strings.contains(&"value3".to_string())); + + Ok(()) +} diff --git a/radixtree/tests/prefix_test.rs b/radixtree/tests/prefix_test.rs new file mode 100644 index 0000000..0b89355 --- /dev/null +++ b/radixtree/tests/prefix_test.rs @@ -0,0 +1,185 @@ +use radixtree::RadixTree; +use std::collections::HashMap; +use tempfile::tempdir; + +#[test] +fn test_list() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Insert keys with various prefixes + let test_data: HashMap<&str, &str> = [ + ("apple", "fruit1"), + ("application", "software1"), + ("apply", "verb1"), + ("banana", "fruit2"), + ("ball", "toy1"), + ("cat", "animal1"), + ("car", "vehicle1"), + ("cargo", "shipping1"), + ].iter().cloned().collect(); + + // Set all test data + for (key, value) in &test_data { + tree.set(key, value.as_bytes().to_vec())?; + } + + // Test prefix 'app' - should return apple, application, apply + let app_keys = tree.list("app")?; + assert_eq!(app_keys.len(), 3); + assert!(app_keys.contains(&"apple".to_string())); + assert!(app_keys.contains(&"application".to_string())); + assert!(app_keys.contains(&"apply".to_string())); + + // Test prefix 'ba' - should return banana, ball + let ba_keys = tree.list("ba")?; + assert_eq!(ba_keys.len(), 2); + assert!(ba_keys.contains(&"banana".to_string())); + assert!(ba_keys.contains(&"ball".to_string())); + + // Test prefix 'car' - should return car, cargo + let car_keys = tree.list("car")?; + assert_eq!(car_keys.len(), 2); + assert!(car_keys.contains(&"car".to_string())); + assert!(car_keys.contains(&"cargo".to_string())); + + // Test prefix 'z' - should return empty list + let z_keys = tree.list("z")?; + assert_eq!(z_keys.len(), 0); + + // Test empty prefix - should return all keys + let all_keys = tree.list("")?; + assert_eq!(all_keys.len(), test_data.len()); + for key in test_data.keys() { + assert!(all_keys.contains(&key.to_string())); + } + + // Test exact key as prefix - should return just that key + let exact_key = tree.list("apple")?; + assert_eq!(exact_key.len(), 1); + assert_eq!(exact_key[0], "apple"); + + Ok(()) +} + +#[test] +fn test_list_with_deletion() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Set keys with common prefixes + tree.set("test1", b"value1".to_vec())?; + tree.set("test2", b"value2".to_vec())?; + tree.set("test3", b"value3".to_vec())?; + tree.set("other", b"value4".to_vec())?; + + // Initial check + let test_keys = tree.list("test")?; + assert_eq!(test_keys.len(), 3); + assert!(test_keys.contains(&"test1".to_string())); + assert!(test_keys.contains(&"test2".to_string())); + assert!(test_keys.contains(&"test3".to_string())); + + // Delete one key + tree.delete("test2")?; + + // Check after deletion + let test_keys_after = tree.list("test")?; + assert_eq!(test_keys_after.len(), 2); + assert!(test_keys_after.contains(&"test1".to_string())); + assert!(!test_keys_after.contains(&"test2".to_string())); + assert!(test_keys_after.contains(&"test3".to_string())); + + // Check all keys + let all_keys = tree.list("")?; + assert_eq!(all_keys.len(), 3); + assert!(all_keys.contains(&"other".to_string())); + + Ok(()) +} + +#[test] +fn test_list_edge_cases() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Test with empty tree + let empty_result = tree.list("any")?; + assert_eq!(empty_result.len(), 0); + + // Set a single key + tree.set("single", b"value".to_vec())?; + + // Test with prefix that's longer than any key + let long_prefix = tree.list("singlelonger")?; + assert_eq!(long_prefix.len(), 0); + + // Test with partial prefix match + let partial = tree.list("sing")?; + assert_eq!(partial.len(), 1); + assert_eq!(partial[0], "single"); + + // Test with very long keys + let long_key1 = "a".repeat(100) + "key1"; + let long_key2 = "a".repeat(100) + "key2"; + + tree.set(&long_key1, b"value1".to_vec())?; + tree.set(&long_key2, b"value2".to_vec())?; + + let long_prefix_result = tree.list(&"a".repeat(100))?; + assert_eq!(long_prefix_result.len(), 2); + assert!(long_prefix_result.contains(&long_key1)); + assert!(long_prefix_result.contains(&long_key2)); + + Ok(()) +} + +#[test] +fn test_list_performance() -> Result<(), radixtree::Error> { + // Create a temporary directory for the test + let temp_dir = tempdir().expect("Failed to create temp directory"); + let db_path = temp_dir.path().to_str().unwrap(); + + // Create a new radix tree + let mut tree = RadixTree::new(db_path, true)?; + + // Insert a large number of keys with different prefixes + let prefixes = ["user", "post", "comment", "like", "share"]; + + // Set 100 keys for each prefix (500 total) + for prefix in &prefixes { + for i in 0..100 { + let key = format!("{}_{}", prefix, i); + tree.set(&key, format!("value_{}", key).as_bytes().to_vec())?; + } + } + + // Test retrieving by each prefix + for prefix in &prefixes { + let keys = tree.list(prefix)?; + assert_eq!(keys.len(), 100); + + // Verify all keys have the correct prefix + for key in &keys { + assert!(key.starts_with(prefix)); + } + } + + // Test retrieving all keys + let all_keys = tree.list("")?; + assert_eq!(all_keys.len(), 500); + + Ok(()) +} diff --git a/radixtree/tests/serialize_test.rs b/radixtree/tests/serialize_test.rs new file mode 100644 index 0000000..867b843 --- /dev/null +++ b/radixtree/tests/serialize_test.rs @@ -0,0 +1,180 @@ +use radixtree::{Node, NodeRef}; + +#[test] +fn test_node_serialization() { + // Create a node with some data + let node = Node { + key_segment: "test".to_string(), + value: b"test_value".to_vec(), + children: vec![ + NodeRef { + key_part: "child1".to_string(), + node_id: 1, + }, + NodeRef { + key_part: "child2".to_string(), + node_id: 2, + }, + ], + is_leaf: true, + }; + + // Serialize the node + let serialized = node.serialize(); + + // Deserialize the node + let deserialized = Node::deserialize(&serialized).expect("Failed to deserialize node"); + + // Verify the deserialized node matches the original + assert_eq!(deserialized.key_segment, node.key_segment); + assert_eq!(deserialized.value, node.value); + assert_eq!(deserialized.is_leaf, node.is_leaf); + assert_eq!(deserialized.children.len(), node.children.len()); + + for (i, child) in node.children.iter().enumerate() { + assert_eq!(deserialized.children[i].key_part, child.key_part); + assert_eq!(deserialized.children[i].node_id, child.node_id); + } +} + +#[test] +fn test_empty_node_serialization() { + // Create an empty node + let node = Node { + key_segment: "".to_string(), + value: vec![], + children: vec![], + is_leaf: false, + }; + + // Serialize the node + let serialized = node.serialize(); + + // Deserialize the node + let deserialized = Node::deserialize(&serialized).expect("Failed to deserialize node"); + + // Verify the deserialized node matches the original + assert_eq!(deserialized.key_segment, node.key_segment); + assert_eq!(deserialized.value, node.value); + assert_eq!(deserialized.is_leaf, node.is_leaf); + assert_eq!(deserialized.children.len(), node.children.len()); +} + +#[test] +fn test_node_with_many_children() { + // Create a node with many children + let mut children = Vec::new(); + for i in 0..100 { + children.push(NodeRef { + key_part: format!("child{}", i), + node_id: i as u32, + }); + } + + let node = Node { + key_segment: "parent".to_string(), + value: b"parent_value".to_vec(), + children, + is_leaf: true, + }; + + // Serialize the node + let serialized = node.serialize(); + + // Deserialize the node + let deserialized = Node::deserialize(&serialized).expect("Failed to deserialize node"); + + // Verify the deserialized node matches the original + assert_eq!(deserialized.key_segment, node.key_segment); + assert_eq!(deserialized.value, node.value); + assert_eq!(deserialized.is_leaf, node.is_leaf); + assert_eq!(deserialized.children.len(), node.children.len()); + + for (i, child) in node.children.iter().enumerate() { + assert_eq!(deserialized.children[i].key_part, child.key_part); + assert_eq!(deserialized.children[i].node_id, child.node_id); + } +} + +#[test] +fn test_node_with_large_value() { + // Create a node with a large value + let large_value = vec![0u8; 4096]; // 4KB value + + let node = Node { + key_segment: "large_value".to_string(), + value: large_value.clone(), + children: vec![], + is_leaf: true, + }; + + // Serialize the node + let serialized = node.serialize(); + + // Deserialize the node + let deserialized = Node::deserialize(&serialized).expect("Failed to deserialize node"); + + // Verify the deserialized node matches the original + assert_eq!(deserialized.key_segment, node.key_segment); + assert_eq!(deserialized.value, node.value); + assert_eq!(deserialized.is_leaf, node.is_leaf); + assert_eq!(deserialized.children.len(), node.children.len()); +} + +#[test] +fn test_version_compatibility() { + // This test ensures that the serialization format is compatible with version 1 + + // Create a node + let node = Node { + key_segment: "test".to_string(), + value: b"test_value".to_vec(), + children: vec![ + NodeRef { + key_part: "child".to_string(), + node_id: 1, + }, + ], + is_leaf: true, + }; + + // Serialize the node + let serialized = node.serialize(); + + // Verify the first byte is the version byte (1) + assert_eq!(serialized[0], 1); + + // Deserialize the node + let deserialized = Node::deserialize(&serialized).expect("Failed to deserialize node"); + + // Verify the deserialized node matches the original + assert_eq!(deserialized.key_segment, node.key_segment); + assert_eq!(deserialized.value, node.value); + assert_eq!(deserialized.is_leaf, node.is_leaf); + assert_eq!(deserialized.children.len(), node.children.len()); +} + +#[test] +fn test_invalid_serialization() { + // Test with empty data + let result = Node::deserialize(&[]); + assert!(result.is_err()); + + // Test with invalid version + let result = Node::deserialize(&[2, 0, 0, 0, 0]); + assert!(result.is_err()); + + // Test with truncated data + let node = Node { + key_segment: "test".to_string(), + value: b"test_value".to_vec(), + children: vec![], + is_leaf: true, + }; + + let serialized = node.serialize(); + let truncated = &serialized[0..serialized.len() / 2]; + + let result = Node::deserialize(truncated); + assert!(result.is_err()); +}