Merge branch 'append'
This commit is contained in:
		
							
								
								
									
										165
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										165
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -208,9 +208,15 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "bitflags" | name = "bitflags" | ||||||
| version = "2.9.2" | version = "1.3.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "bitflags" | ||||||
|  | version = "2.9.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "block-buffer" | name = "block-buffer" | ||||||
| @@ -235,9 +241,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "cfg-if" | name = "cfg-if" | ||||||
| version = "1.0.1" | version = "1.0.3" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "chacha20" | name = "chacha20" | ||||||
| @@ -358,6 +364,30 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "crc32fast" | ||||||
|  | version = "1.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[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]] | [[package]] | ||||||
| name = "crypto-common" | name = "crypto-common" | ||||||
| version = "0.1.6" | version = "0.1.6" | ||||||
| @@ -406,7 +436,7 @@ dependencies = [ | |||||||
|  "hashbrown", |  "hashbrown", | ||||||
|  "lock_api", |  "lock_api", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  "parking_lot_core", |  "parking_lot_core 0.9.11", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -526,13 +556,23 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "form_urlencoded" | name = "form_urlencoded" | ||||||
| version = "1.2.1" | version = "1.2.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "fs2" | ||||||
|  | version = "0.4.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" | ||||||
|  | dependencies = [ | ||||||
|  |  "libc", | ||||||
|  |  "winapi", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "futures" | name = "futures" | ||||||
| version = "0.3.31" | version = "0.3.31" | ||||||
| @@ -622,6 +662,15 @@ dependencies = [ | |||||||
|  "slab", |  "slab", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "fxhash" | ||||||
|  | version = "0.2.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" | ||||||
|  | dependencies = [ | ||||||
|  |  "byteorder", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "generic-array" | name = "generic-array" | ||||||
| version = "0.14.7" | version = "0.14.7" | ||||||
| @@ -682,6 +731,7 @@ dependencies = [ | |||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "sha2", |  "sha2", | ||||||
|  |  "sled", | ||||||
|  "thiserror", |  "thiserror", | ||||||
|  "tokio", |  "tokio", | ||||||
| ] | ] | ||||||
| @@ -732,7 +782,7 @@ dependencies = [ | |||||||
|  "intl-memoizer", |  "intl-memoizer", | ||||||
|  "lazy_static", |  "lazy_static", | ||||||
|  "log", |  "log", | ||||||
|  "parking_lot", |  "parking_lot 0.12.4", | ||||||
|  "rust-embed", |  "rust-embed", | ||||||
|  "thiserror", |  "thiserror", | ||||||
|  "unic-langid", |  "unic-langid", | ||||||
| @@ -861,9 +911,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "idna" | name = "idna" | ||||||
| version = "1.0.3" | version = "1.1.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "idna_adapter", |  "idna_adapter", | ||||||
|  "smallvec", |  "smallvec", | ||||||
| @@ -889,6 +939,15 @@ dependencies = [ | |||||||
|  "generic-array", |  "generic-array", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "instant" | ||||||
|  | version = "0.1.13" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "intl-memoizer" | name = "intl-memoizer" | ||||||
| version = "0.5.3" | version = "0.5.3" | ||||||
| @@ -910,11 +969,11 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "io-uring" | name = "io-uring" | ||||||
| version = "0.7.9" | version = "0.7.10" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" | checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bitflags", |  "bitflags 2.9.3", | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| @@ -1040,6 +1099,17 @@ version = "0.3.1" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "parking_lot" | ||||||
|  | version = "0.11.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" | ||||||
|  | dependencies = [ | ||||||
|  |  "instant", | ||||||
|  |  "lock_api", | ||||||
|  |  "parking_lot_core 0.8.6", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "parking_lot" | name = "parking_lot" | ||||||
| version = "0.12.4" | version = "0.12.4" | ||||||
| @@ -1047,7 +1117,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "lock_api", |  "lock_api", | ||||||
|  "parking_lot_core", |  "parking_lot_core 0.9.11", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "parking_lot_core" | ||||||
|  | version = "0.8.6" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  |  "instant", | ||||||
|  |  "libc", | ||||||
|  |  "redox_syscall 0.2.16", | ||||||
|  |  "smallvec", | ||||||
|  |  "winapi", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -1058,7 +1142,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  "libc", |  "libc", | ||||||
|  "redox_syscall", |  "redox_syscall 0.5.17", | ||||||
|  "smallvec", |  "smallvec", | ||||||
|  "windows-targets 0.52.6", |  "windows-targets 0.52.6", | ||||||
| ] | ] | ||||||
| @@ -1075,9 +1159,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "percent-encoding" | name = "percent-encoding" | ||||||
| version = "2.3.1" | version = "2.3.2" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pin-project" | name = "pin-project" | ||||||
| @@ -1176,9 +1260,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "proc-macro2" | name = "proc-macro2" | ||||||
| version = "1.0.97" | version = "1.0.101" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "unicode-ident", |  "unicode-ident", | ||||||
| ] | ] | ||||||
| @@ -1252,13 +1336,22 @@ dependencies = [ | |||||||
|  "url", |  "url", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "redox_syscall" | ||||||
|  | version = "0.2.16" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 1.3.2", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "redox_syscall" | name = "redox_syscall" | ||||||
| version = "0.5.17" | version = "0.5.17" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" | checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bitflags", |  "bitflags 2.9.3", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -1415,9 +1508,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde_json" | name = "serde_json" | ||||||
| version = "1.0.142" | version = "1.0.143" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" | checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "itoa", |  "itoa", | ||||||
|  "memchr", |  "memchr", | ||||||
| @@ -1466,6 +1559,22 @@ version = "0.4.11" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "sled" | ||||||
|  | version = "0.34.7" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" | ||||||
|  | dependencies = [ | ||||||
|  |  "crc32fast", | ||||||
|  |  "crossbeam-epoch", | ||||||
|  |  "crossbeam-utils", | ||||||
|  |  "fs2", | ||||||
|  |  "fxhash", | ||||||
|  |  "libc", | ||||||
|  |  "log", | ||||||
|  |  "parking_lot 0.11.2", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "smallvec" | name = "smallvec" | ||||||
| version = "1.15.1" | version = "1.15.1" | ||||||
| @@ -1599,7 +1708,7 @@ dependencies = [ | |||||||
|  "io-uring", |  "io-uring", | ||||||
|  "libc", |  "libc", | ||||||
|  "mio", |  "mio", | ||||||
|  "parking_lot", |  "parking_lot 0.12.4", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  "signal-hook-registry", |  "signal-hook-registry", | ||||||
|  "slab", |  "slab", | ||||||
| @@ -1693,9 +1802,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "url" | name = "url" | ||||||
| version = "2.5.4" | version = "2.5.6" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" | checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "form_urlencoded", |  "form_urlencoded", | ||||||
|  "idna", |  "idna", | ||||||
| @@ -1754,11 +1863,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "winapi-util" | name = "winapi-util" | ||||||
| version = "0.1.9" | version = "0.1.10" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" | checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "windows-sys 0.59.0", |  "windows-sys 0.60.2", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,9 +1,29 @@ | |||||||
| [workspace] | [package] | ||||||
| members = ["herodb"] | name = "herodb" | ||||||
| resolver = "2" | version = "0.0.1" | ||||||
|  | authors = ["ThreeFold Tech NV"] | ||||||
|  | edition = "2024" | ||||||
|  |  | ||||||
| # You can define shared profiles for all workspace members here | [dependencies] | ||||||
| [profile.release] | anyhow = "1.0.59" | ||||||
| lto = true | bytes = "1.3.0" | ||||||
| codegen-units = 1 | thiserror = "1.0.32" | ||||||
| strip = true | tokio = { version = "1.23.0", features = ["full"] } | ||||||
|  | clap = { version = "4.5.20", features = ["derive"] } | ||||||
|  | byteorder = "1.4.3" | ||||||
|  | futures = "0.3" | ||||||
|  | sled = "0.34" | ||||||
|  | redb = "2.1.3" | ||||||
|  | serde = { version = "1.0", features = ["derive"] } | ||||||
|  | serde_json = "1.0" | ||||||
|  | bincode = "1.3" | ||||||
|  | chacha20poly1305 = "0.10.1" | ||||||
|  | rand = "0.8" | ||||||
|  | sha2 = "0.10" | ||||||
|  | age = "0.10" | ||||||
|  | secrecy = "0.8" | ||||||
|  | ed25519-dalek = "2" | ||||||
|  | base64 = "0.22" | ||||||
|  |  | ||||||
|  | [dev-dependencies] | ||||||
|  | redis = { version = "0.24", features = ["aio", "tokio-comp"] } | ||||||
|   | |||||||
							
								
								
									
										86
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										86
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| # HeroDB | # HeroDB | ||||||
|  |  | ||||||
|  | <<<<<<< HEAD | ||||||
| Redis-compatible database server with encryption and AGE cryptographic operations. | Redis-compatible database server with encryption and AGE cryptographic operations. | ||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
| @@ -13,11 +14,37 @@ Redis-compatible database server with encryption and AGE cryptographic operation | |||||||
| - Persistent storage using redb | - Persistent storage using redb | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  | ======= | ||||||
|  | HeroDB is a Redis-compatible database built with Rust, offering a flexible and secure storage solution. It supports two primary storage backends: `redb` (default) and `sled`, both with full encryption capabilities. HeroDB aims to provide a robust and performant key-value store with advanced features like data-at-rest encryption, hash operations, list operations, and cursor-based scanning. | ||||||
|  |  | ||||||
|  | ## Purpose | ||||||
|  |  | ||||||
|  | The main purpose of HeroDB is to offer a lightweight, embeddable, and Redis-compatible database that prioritizes data security through transparent encryption. It's designed for applications that require fast, reliable data storage with the option for strong cryptographic protection, without the overhead of a full-fledged Redis server. | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - **Redis Compatibility**: Supports a subset of Redis commands over RESP (Redis Serialization Protocol) via TCP. | ||||||
|  | - **Dual Backend Support**: | ||||||
|  |     - `redb` (default): Optimized for concurrent access and high-throughput scenarios. | ||||||
|  |     - `sled`: A lock-free, log-structured database, excellent for specific workloads. | ||||||
|  | - **Data-at-Rest Encryption**: Transparent encryption for both backends using the `age` encryption library. | ||||||
|  | - **Key-Value Operations**: Full support for basic string, hash, and list operations. | ||||||
|  | - **Expiration**: Time-to-live (TTL) functionality for keys. | ||||||
|  | - **Scanning**: Cursor-based iteration for keys and hash fields (`SCAN`, `HSCAN`). | ||||||
|  | - **AGE Cryptography Commands**: HeroDB-specific extensions for cryptographic operations. | ||||||
|  |  | ||||||
|  | ## Quick Start | ||||||
|  |  | ||||||
|  | ### Building HeroDB | ||||||
|  |  | ||||||
|  | To build HeroDB, navigate to the project root and run: | ||||||
|  | >>>>>>> append | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| cargo build --release | cargo build --release | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | <<<<<<< HEAD | ||||||
| ## Usage | ## Usage | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| @@ -80,4 +107,61 @@ redis-cli -p 6379 AGE GENENC | |||||||
| - **Storage**: redb embedded database | - **Storage**: redb embedded database | ||||||
| - **Protocol**: Redis RESP protocol over TCP | - **Protocol**: Redis RESP protocol over TCP | ||||||
| - **Encryption**: ChaCha20-Poly1305 for data, AGE for operations | - **Encryption**: ChaCha20-Poly1305 for data, AGE for operations | ||||||
| - **Concurrency**: Tokio async runtime | - **Concurrency**: Tokio async runtime | ||||||
|  | ======= | ||||||
|  | ### Running HeroDB | ||||||
|  |  | ||||||
|  | You can start HeroDB with different backends and encryption options: | ||||||
|  |  | ||||||
|  | #### Default `redb` Backend | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_redb --port 6379 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### `sled` Backend | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_sled --port 6379 --sled | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### `redb` with Encryption | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_encrypted --port 6379 --encrypt --key mysecretkey | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### `sled` with Encryption | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_sled_encrypted --port 6379 --sled --encrypt --key mysecretkey | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Usage with Redis Clients | ||||||
|  |  | ||||||
|  | HeroDB can be interacted with using any standard Redis client, such as `redis-cli`, `redis-py` (Python), or `ioredis` (Node.js). | ||||||
|  |  | ||||||
|  | ### Example with `redis-cli` | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | redis-cli -p 6379 SET mykey "Hello from HeroDB!" | ||||||
|  | redis-cli -p 6379 GET mykey | ||||||
|  | # → "Hello from HeroDB!" | ||||||
|  |  | ||||||
|  | redis-cli -p 6379 HSET user:1 name "Alice" age "30" | ||||||
|  | redis-cli -p 6379 HGET user:1 name | ||||||
|  | # → "Alice" | ||||||
|  |  | ||||||
|  | redis-cli -p 6379 SCAN 0 MATCH user:* COUNT 10 | ||||||
|  | # → 1) "0" | ||||||
|  | #    2) 1) "user:1" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Documentation | ||||||
|  |  | ||||||
|  | For more detailed information on commands, features, and advanced usage, please refer to the documentation: | ||||||
|  |  | ||||||
|  | - [Basics](docs/basics.md) | ||||||
|  | - [Supported Commands](docs/cmds.md) | ||||||
|  | - [AGE Cryptography](docs/age.md) | ||||||
|  | >>>>>>> append | ||||||
|   | |||||||
							
								
								
									
										125
									
								
								docs/cmds.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								docs/cmds.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  |  | ||||||
|  | ## Backend Support | ||||||
|  |  | ||||||
|  | HeroDB supports two storage backends, both with full encryption support: | ||||||
|  |  | ||||||
|  | - **redb** (default): Full-featured, optimized for production use | ||||||
|  | - **sled**: Alternative embedded database with encryption support | ||||||
|  |  | ||||||
|  | ### Starting HeroDB with Different Backends | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Use default redb backend | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_redb --port 6379 | ||||||
|  |  | ||||||
|  | # Use sled backend | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_sled --port 6379 --sled | ||||||
|  |  | ||||||
|  | # Use redb with encryption | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_encrypted --port 6379 --encrypt --key mysecretkey | ||||||
|  |  | ||||||
|  | # Use sled with encryption | ||||||
|  | ./target/release/herodb --dir /tmp/herodb_sled_encrypted --port 6379 --sled --encrypt --key mysecretkey | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Command Support by Backend | ||||||
|  |  | ||||||
|  | Command Category | redb | sled | Notes | | ||||||
|  | |-----------------|------|------|-------| | ||||||
|  | **Strings** | | | | | ||||||
|  | SET | ✅ | ✅ | Full support | | ||||||
|  | GET | ✅ | ✅ | Full support | | ||||||
|  | DEL | ✅ | ✅ | Full support | | ||||||
|  | EXISTS | ✅ | ✅ | Full support | | ||||||
|  | INCR/DECR | ✅ | ✅ | Full support | | ||||||
|  | MGET/MSET | ✅ | ✅ | Full support | | ||||||
|  | **Hashes** | | | | | ||||||
|  | HSET | ✅ | ✅ | Full support | | ||||||
|  | HGET | ✅ | ✅ | Full support | | ||||||
|  | HGETALL | ✅ | ✅ | Full support | | ||||||
|  | HDEL | ✅ | ✅ | Full support | | ||||||
|  | HEXISTS | ✅ | ✅ | Full support | | ||||||
|  | HKEYS | ✅ | ✅ | Full support | | ||||||
|  | HVALS | ✅ | ✅ | Full support | | ||||||
|  | HLEN | ✅ | ✅ | Full support | | ||||||
|  | HMGET | ✅ | ✅ | Full support | | ||||||
|  | HSETNX | ✅ | ✅ | Full support | | ||||||
|  | HINCRBY/HINCRBYFLOAT | ✅ | ✅ | Full support | | ||||||
|  | HSCAN | ✅ | ✅ | Full support with pattern matching | | ||||||
|  | **Lists** | | | | | ||||||
|  | LPUSH/RPUSH | ✅ | ✅ | Full support | | ||||||
|  | LPOP/RPOP | ✅ | ✅ | Full support | | ||||||
|  | LLEN | ✅ | ✅ | Full support | | ||||||
|  | LRANGE | ✅ | ✅ | Full support | | ||||||
|  | LINDEX | ✅ | ✅ | Full support | | ||||||
|  | LTRIM | ✅ | ✅ | Full support | | ||||||
|  | LREM | ✅ | ✅ | Full support | | ||||||
|  | BLPOP/BRPOP | ✅ | ❌ | Blocking operations not in sled | | ||||||
|  | **Expiration** | | | | | ||||||
|  | EXPIRE | ✅ | ✅ | Full support in both | | ||||||
|  | TTL | ✅ | ✅ | Full support in both | | ||||||
|  | PERSIST | ✅ | ✅ | Full support in both | | ||||||
|  | SETEX/PSETEX | ✅ | ✅ | Full support in both | | ||||||
|  | EXPIREAT/PEXPIREAT | ✅ | ✅ | Full support in both | | ||||||
|  | **Scanning** | | | | | ||||||
|  | KEYS | ✅ | ✅ | Full support with patterns | | ||||||
|  | SCAN | ✅ | ✅ | Full cursor-based iteration | | ||||||
|  | HSCAN | ✅ | ✅ | Full cursor-based iteration | | ||||||
|  | **Transactions** | | | | | ||||||
|  | MULTI/EXEC/DISCARD | ✅ | ❌ | Only supported in redb | | ||||||
|  | **Encryption** | | | | | ||||||
|  | Data-at-rest encryption | ✅ | ✅ | Both support [age](age.tech) encryption | | ||||||
|  | AGE commands | ✅ | ✅ | Both support AGE crypto commands | | ||||||
|  | **Full-Text Search** | | | | | ||||||
|  | FT.CREATE | ✅ | ✅ | Create search index with schema | | ||||||
|  | FT.ADD | ✅ | ✅ | Add document to search index | | ||||||
|  | FT.SEARCH | ✅ | ✅ | Search documents with query | | ||||||
|  | FT.DEL | ✅ | ✅ | Delete document from index | | ||||||
|  | FT.INFO | ✅ | ✅ | Get index information | | ||||||
|  | FT.DROP | ✅ | ✅ | Drop search index | | ||||||
|  | FT.ALTER | ✅ | ✅ | Alter index schema | | ||||||
|  | FT.AGGREGATE | ✅ | ✅ | Aggregate search results | | ||||||
|  |  | ||||||
|  | ### Performance Considerations | ||||||
|  |  | ||||||
|  | - **redb**: Optimized for concurrent access, better for high-throughput scenarios | ||||||
|  | - **sled**: Lock-free architecture, excellent for specific workloads | ||||||
|  |  | ||||||
|  | ### Encryption Features | ||||||
|  |  | ||||||
|  | Both backends support: | ||||||
|  | - Transparent data-at-rest encryption using the `age` encryption library | ||||||
|  | - Per-database encryption (databases >= 10 are encrypted when `--encrypt` flag is used) | ||||||
|  | - Secure key derivation using the master key | ||||||
|  |  | ||||||
|  | ### Backend Selection Examples | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Example: Testing both backends | ||||||
|  | redis-cli -p 6379 SET mykey "redb value" | ||||||
|  | redis-cli -p 6381 SET mykey "sled value" | ||||||
|  |  | ||||||
|  | # Example: Using encryption with both | ||||||
|  | ./target/release/herodb --port 6379 --encrypt --key secret123 | ||||||
|  | ./target/release/herodb --port 6381 --sled --encrypt --key secret123 | ||||||
|  |  | ||||||
|  | # Both support the same Redis commands | ||||||
|  | redis-cli -p 6379 HSET user:1 name "Alice" age "30" | ||||||
|  | redis-cli -p 6381 HSET user:1 name "Alice" age "30" | ||||||
|  |  | ||||||
|  | # Both support SCAN operations | ||||||
|  | redis-cli -p 6379 SCAN 0 MATCH user:* COUNT 10 | ||||||
|  | redis-cli -p 6381 SCAN 0 MATCH user:* COUNT 10 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Migration Between Backends | ||||||
|  |  | ||||||
|  | To migrate data between backends, use Redis replication or dump/restore: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Export from redb | ||||||
|  | redis-cli -p 6379 --rdb dump.rdb | ||||||
|  |  | ||||||
|  | # Import to sled | ||||||
|  | redis-cli -p 6381 --pipe < dump.rdb | ||||||
|  | ``` | ||||||
							
								
								
									
										397
									
								
								docs/search.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										397
									
								
								docs/search.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,397 @@ | |||||||
|  | # Full-Text Search with Tantivy | ||||||
|  |  | ||||||
|  | HeroDB includes powerful full-text search capabilities powered by [Tantivy](https://github.com/quickwit-oss/tantivy), a fast full-text search engine library written in Rust. This provides Redis-compatible search commands similar to RediSearch. | ||||||
|  |  | ||||||
|  | ## Overview | ||||||
|  |  | ||||||
|  | The search functionality allows you to: | ||||||
|  | - Create search indexes with custom schemas | ||||||
|  | - Index documents with multiple field types | ||||||
|  | - Perform complex queries with filters | ||||||
|  | - Support for text, numeric, date, and geographic data | ||||||
|  | - Real-time search with high performance | ||||||
|  |  | ||||||
|  | ## Search Commands | ||||||
|  |  | ||||||
|  | ### FT.CREATE - Create Search Index | ||||||
|  |  | ||||||
|  | Create a new search index with a defined schema. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.CREATE index_name SCHEMA field_name field_type [options] [field_name field_type [options] ...] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Field Types:** | ||||||
|  | - `TEXT` - Full-text searchable text fields | ||||||
|  | - `NUMERIC` - Numeric fields (integers, floats) | ||||||
|  | - `TAG` - Tag fields for exact matching | ||||||
|  | - `GEO` - Geographic coordinates (lat,lon) | ||||||
|  | - `DATE` - Date/timestamp fields | ||||||
|  |  | ||||||
|  | **Field Options:** | ||||||
|  | - `STORED` - Store field value for retrieval | ||||||
|  | - `INDEXED` - Make field searchable | ||||||
|  | - `TOKENIZED` - Enable tokenization for text fields | ||||||
|  | - `FAST` - Enable fast access for numeric fields | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | # Create a product search index | ||||||
|  | FT.CREATE products SCHEMA  | ||||||
|  |   title TEXT STORED INDEXED TOKENIZED | ||||||
|  |   description TEXT STORED INDEXED TOKENIZED   | ||||||
|  |   price NUMERIC STORED INDEXED FAST | ||||||
|  |   category TAG STORED | ||||||
|  |   location GEO STORED | ||||||
|  |   created_date DATE STORED INDEXED | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.ADD - Add Document to Index | ||||||
|  |  | ||||||
|  | Add a document to a search index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.ADD index_name doc_id [SCORE score] FIELDS field_name field_value [field_name field_value ...] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | # Add a product document | ||||||
|  | FT.ADD products product:1 SCORE 1.0 FIELDS  | ||||||
|  |   title "Wireless Headphones"  | ||||||
|  |   description "High-quality wireless headphones with noise cancellation" | ||||||
|  |   price 199.99 | ||||||
|  |   category "electronics" | ||||||
|  |   location "37.7749,-122.4194" | ||||||
|  |   created_date 1640995200000 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.SEARCH - Search Documents | ||||||
|  |  | ||||||
|  | Search for documents in an index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.SEARCH index_name query [LIMIT offset count] [FILTER field min max] [RETURN field [field ...]] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Query Syntax:** | ||||||
|  | - Simple terms: `wireless headphones` | ||||||
|  | - Phrase queries: `"noise cancellation"` | ||||||
|  | - Field-specific: `title:wireless` | ||||||
|  | - Boolean operators: `wireless AND headphones` | ||||||
|  | - Wildcards: `head*` | ||||||
|  |  | ||||||
|  | **Examples:** | ||||||
|  | ```bash | ||||||
|  | # Simple text search | ||||||
|  | FT.SEARCH products "wireless headphones" | ||||||
|  |  | ||||||
|  | # Search with filters | ||||||
|  | FT.SEARCH products "headphones" FILTER price 100 300 LIMIT 0 10 | ||||||
|  |  | ||||||
|  | # Field-specific search | ||||||
|  | FT.SEARCH products "title:wireless AND category:electronics" | ||||||
|  |  | ||||||
|  | # Return specific fields only | ||||||
|  | FT.SEARCH products "*" RETURN title price | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.DEL - Delete Document | ||||||
|  |  | ||||||
|  | Remove a document from the search index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.DEL index_name doc_id | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | FT.DEL products product:1 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.INFO - Get Index Information | ||||||
|  |  | ||||||
|  | Get information about a search index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.INFO index_name | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Returns:** | ||||||
|  | - Index name and document count | ||||||
|  | - Field definitions and types | ||||||
|  | - Index configuration | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | FT.INFO products | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.DROP - Drop Index | ||||||
|  |  | ||||||
|  | Delete an entire search index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.DROP index_name | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | FT.DROP products | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.ALTER - Alter Index Schema | ||||||
|  |  | ||||||
|  | Add new fields to an existing index. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.ALTER index_name SCHEMA ADD field_name field_type [options] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | FT.ALTER products SCHEMA ADD brand TAG STORED | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### FT.AGGREGATE - Aggregate Search Results | ||||||
|  |  | ||||||
|  | Perform aggregations on search results. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | FT.AGGREGATE index_name query [GROUPBY field] [REDUCE function field AS alias] | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Example:** | ||||||
|  | ```bash | ||||||
|  | # Group products by category and count | ||||||
|  | FT.AGGREGATE products "*" GROUPBY category REDUCE COUNT 0 AS count | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Field Types in Detail | ||||||
|  |  | ||||||
|  | ### TEXT Fields | ||||||
|  | - **Purpose**: Full-text search on natural language content | ||||||
|  | - **Features**: Tokenization, stemming, stop-word removal | ||||||
|  | - **Options**: `STORED`, `INDEXED`, `TOKENIZED` | ||||||
|  | - **Example**: Product titles, descriptions, content | ||||||
|  |  | ||||||
|  | ### NUMERIC Fields   | ||||||
|  | - **Purpose**: Numeric data for range queries and sorting | ||||||
|  | - **Types**: I64, U64, F64 | ||||||
|  | - **Options**: `STORED`, `INDEXED`, `FAST` | ||||||
|  | - **Example**: Prices, quantities, ratings | ||||||
|  |  | ||||||
|  | ### TAG Fields | ||||||
|  | - **Purpose**: Exact-match categorical data | ||||||
|  | - **Features**: No tokenization, exact string matching | ||||||
|  | - **Options**: `STORED`, case sensitivity control | ||||||
|  | - **Example**: Categories, brands, status values | ||||||
|  |  | ||||||
|  | ### GEO Fields | ||||||
|  | - **Purpose**: Geographic coordinates | ||||||
|  | - **Format**: "latitude,longitude" (e.g., "37.7749,-122.4194") | ||||||
|  | - **Features**: Geographic distance queries | ||||||
|  | - **Options**: `STORED` | ||||||
|  |  | ||||||
|  | ### DATE Fields | ||||||
|  | - **Purpose**: Timestamp and date data | ||||||
|  | - **Format**: Unix timestamp in milliseconds | ||||||
|  | - **Features**: Range queries, temporal filtering | ||||||
|  | - **Options**: `STORED`, `INDEXED`, `FAST` | ||||||
|  |  | ||||||
|  | ## Search Query Syntax | ||||||
|  |  | ||||||
|  | ### Basic Queries | ||||||
|  | ```bash | ||||||
|  | # Single term | ||||||
|  | FT.SEARCH products "wireless" | ||||||
|  |  | ||||||
|  | # Multiple terms (AND by default) | ||||||
|  | FT.SEARCH products "wireless headphones" | ||||||
|  |  | ||||||
|  | # Phrase query | ||||||
|  | FT.SEARCH products "\"noise cancellation\"" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Field-Specific Queries | ||||||
|  | ```bash | ||||||
|  | # Search in specific field | ||||||
|  | FT.SEARCH products "title:wireless" | ||||||
|  |  | ||||||
|  | # Multiple field queries | ||||||
|  | FT.SEARCH products "title:wireless AND description:bluetooth" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Boolean Operators | ||||||
|  | ```bash | ||||||
|  | # AND operator | ||||||
|  | FT.SEARCH products "wireless AND headphones" | ||||||
|  |  | ||||||
|  | # OR operator   | ||||||
|  | FT.SEARCH products "wireless OR bluetooth" | ||||||
|  |  | ||||||
|  | # NOT operator | ||||||
|  | FT.SEARCH products "headphones NOT wired" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Wildcards and Fuzzy Search | ||||||
|  | ```bash | ||||||
|  | # Wildcard search | ||||||
|  | FT.SEARCH products "head*" | ||||||
|  |  | ||||||
|  | # Fuzzy search (approximate matching) | ||||||
|  | FT.SEARCH products "%headphone%" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Range Queries | ||||||
|  | ```bash | ||||||
|  | # Numeric range in query | ||||||
|  | FT.SEARCH products "@price:[100 300]" | ||||||
|  |  | ||||||
|  | # Date range | ||||||
|  | FT.SEARCH products "@created_date:[1640995200000 1672531200000]" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Filtering and Sorting | ||||||
|  |  | ||||||
|  | ### FILTER Clause | ||||||
|  | ```bash | ||||||
|  | # Numeric filter | ||||||
|  | FT.SEARCH products "headphones" FILTER price 100 300 | ||||||
|  |  | ||||||
|  | # Multiple filters | ||||||
|  | FT.SEARCH products "*" FILTER price 100 500 FILTER rating 4 5 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### LIMIT Clause | ||||||
|  | ```bash | ||||||
|  | # Pagination | ||||||
|  | FT.SEARCH products "wireless" LIMIT 0 10    # First 10 results | ||||||
|  | FT.SEARCH products "wireless" LIMIT 10 10   # Next 10 results | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### RETURN Clause | ||||||
|  | ```bash | ||||||
|  | # Return specific fields | ||||||
|  | FT.SEARCH products "*" RETURN title price | ||||||
|  |  | ||||||
|  | # Return all stored fields (default) | ||||||
|  | FT.SEARCH products "*" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Performance Considerations | ||||||
|  |  | ||||||
|  | ### Indexing Strategy | ||||||
|  | - Only index fields you need to search on | ||||||
|  | - Use `FAST` option for frequently filtered numeric fields | ||||||
|  | - Consider storage vs. search performance trade-offs | ||||||
|  |  | ||||||
|  | ### Query Optimization | ||||||
|  | - Use specific field queries when possible | ||||||
|  | - Combine filters with text queries for better performance | ||||||
|  | - Use pagination with LIMIT for large result sets | ||||||
|  |  | ||||||
|  | ### Memory Usage | ||||||
|  | - Tantivy indexes are memory-mapped for performance | ||||||
|  | - Index size depends on document count and field configuration | ||||||
|  | - Monitor disk space for index storage | ||||||
|  |  | ||||||
|  | ## Integration with Redis Commands | ||||||
|  |  | ||||||
|  | Search indexes work alongside regular Redis data: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Store product data in Redis hash | ||||||
|  | HSET product:1 title "Wireless Headphones" price "199.99" | ||||||
|  |  | ||||||
|  | # Index the same data for search | ||||||
|  | FT.ADD products product:1 FIELDS title "Wireless Headphones" price 199.99 | ||||||
|  |  | ||||||
|  | # Search returns document IDs that can be used with Redis commands | ||||||
|  | FT.SEARCH products "wireless" | ||||||
|  | # Returns: product:1 | ||||||
|  |  | ||||||
|  | # Retrieve full data using Redis | ||||||
|  | HGETALL product:1 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Example Use Cases | ||||||
|  |  | ||||||
|  | ### E-commerce Product Search | ||||||
|  | ```bash | ||||||
|  | # Create product catalog index | ||||||
|  | FT.CREATE catalog SCHEMA  | ||||||
|  |   name TEXT STORED INDEXED TOKENIZED | ||||||
|  |   description TEXT INDEXED TOKENIZED | ||||||
|  |   price NUMERIC STORED INDEXED FAST | ||||||
|  |   category TAG STORED | ||||||
|  |   brand TAG STORED | ||||||
|  |   rating NUMERIC STORED FAST | ||||||
|  |  | ||||||
|  | # Add products | ||||||
|  | FT.ADD catalog prod:1 FIELDS name "iPhone 14" price 999 category "phones" brand "apple" rating 4.5 | ||||||
|  | FT.ADD catalog prod:2 FIELDS name "Samsung Galaxy" price 899 category "phones" brand "samsung" rating 4.3 | ||||||
|  |  | ||||||
|  | # Search queries | ||||||
|  | FT.SEARCH catalog "iPhone" | ||||||
|  | FT.SEARCH catalog "phones" FILTER price 800 1000 | ||||||
|  | FT.SEARCH catalog "@brand:apple" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Content Management | ||||||
|  | ```bash | ||||||
|  | # Create content index | ||||||
|  | FT.CREATE content SCHEMA | ||||||
|  |   title TEXT STORED INDEXED TOKENIZED | ||||||
|  |   body TEXT INDEXED TOKENIZED | ||||||
|  |   author TAG STORED | ||||||
|  |   published DATE STORED INDEXED | ||||||
|  |   tags TAG STORED | ||||||
|  |  | ||||||
|  | # Search content | ||||||
|  | FT.SEARCH content "machine learning" | ||||||
|  | FT.SEARCH content "@author:john AND @tags:ai" | ||||||
|  | FT.SEARCH content "*" FILTER published 1640995200000 1672531200000 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Geographic Search | ||||||
|  | ```bash | ||||||
|  | # Create location-based index | ||||||
|  | FT.CREATE places SCHEMA | ||||||
|  |   name TEXT STORED INDEXED TOKENIZED | ||||||
|  |   location GEO STORED | ||||||
|  |   type TAG STORED | ||||||
|  |  | ||||||
|  | # Add locations | ||||||
|  | FT.ADD places place:1 FIELDS name "Golden Gate Bridge" location "37.8199,-122.4783" type "landmark" | ||||||
|  |  | ||||||
|  | # Geographic queries (future feature) | ||||||
|  | FT.SEARCH places "@location:[37.7749 -122.4194 10 km]" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Error Handling | ||||||
|  |  | ||||||
|  | Common error responses: | ||||||
|  | - `ERR index not found` - Index doesn't exist | ||||||
|  | - `ERR field not found` - Field not defined in schema | ||||||
|  | - `ERR invalid query syntax` - Malformed query | ||||||
|  | - `ERR document not found` - Document ID doesn't exist | ||||||
|  |  | ||||||
|  | ## Best Practices | ||||||
|  |  | ||||||
|  | 1. **Schema Design**: Plan your schema carefully - changes require reindexing | ||||||
|  | 2. **Field Selection**: Only store and index fields you actually need | ||||||
|  | 3. **Batch Operations**: Add multiple documents efficiently | ||||||
|  | 4. **Query Testing**: Test queries for performance with realistic data | ||||||
|  | 5. **Monitoring**: Monitor index size and query performance | ||||||
|  | 6. **Backup**: Include search indexes in backup strategies | ||||||
|  |  | ||||||
|  | ## Future Enhancements | ||||||
|  |  | ||||||
|  | Planned features: | ||||||
|  | - Geographic distance queries | ||||||
|  | - Advanced aggregations and faceting | ||||||
|  | - Highlighting of search results | ||||||
|  | - Synonyms and custom analyzers | ||||||
|  | - Real-time suggestions and autocomplete | ||||||
|  | - Index replication and sharding | ||||||
							
								
								
									
										171
									
								
								examples/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								examples/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | |||||||
|  | # HeroDB Tantivy Search Examples | ||||||
|  |  | ||||||
|  | This directory contains examples demonstrating HeroDB's full-text search capabilities powered by Tantivy. | ||||||
|  |  | ||||||
|  | ## Tantivy Search Demo (Bash Script) | ||||||
|  |  | ||||||
|  | ### Overview | ||||||
|  | The `tantivy_search_demo.sh` script provides a comprehensive demonstration of HeroDB's search functionality using Redis commands. It showcases various search scenarios including basic text search, filtering, sorting, geographic queries, and more. | ||||||
|  |  | ||||||
|  | ### Prerequisites | ||||||
|  | 1. **HeroDB Server**: The server must be running on port 6381 | ||||||
|  | 2. **Redis CLI**: The `redis-cli` tool must be installed and available in your PATH | ||||||
|  |  | ||||||
|  | ### Running the Demo | ||||||
|  |  | ||||||
|  | #### Step 1: Start HeroDB Server | ||||||
|  | ```bash | ||||||
|  | # From the project root directory | ||||||
|  | cargo run -- --port 6381 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Step 2: Run the Demo (in a new terminal) | ||||||
|  | ```bash | ||||||
|  | # From the project root directory | ||||||
|  | ./examples/tantivy_search_demo.sh | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### What the Demo Covers | ||||||
|  |  | ||||||
|  | The script demonstrates 15 different search scenarios: | ||||||
|  |  | ||||||
|  | 1. **Index Creation** - Creating a search index with various field types | ||||||
|  | 2. **Data Insertion** - Adding sample products to the index | ||||||
|  | 3. **Basic Text Search** - Simple keyword searches | ||||||
|  | 4. **Filtered Search** - Combining text search with category filters | ||||||
|  | 5. **Numeric Range Search** - Finding products within price ranges | ||||||
|  | 6. **Sorting Results** - Ordering results by different fields | ||||||
|  | 7. **Limited Results** - Pagination and result limiting | ||||||
|  | 8. **Complex Queries** - Multi-field searches with sorting | ||||||
|  | 9. **Geographic Search** - Location-based queries | ||||||
|  | 10. **Index Information** - Getting statistics about the search index | ||||||
|  | 11. **Search Comparison** - Tantivy vs simple pattern matching | ||||||
|  | 12. **Fuzzy Search** - Typo tolerance and approximate matching | ||||||
|  | 13. **Phrase Search** - Exact phrase matching | ||||||
|  | 14. **Boolean Queries** - AND, OR, NOT operators | ||||||
|  | 15. **Cleanup** - Removing test data | ||||||
|  |  | ||||||
|  | ### Sample Data | ||||||
|  |  | ||||||
|  | The demo uses a product catalog with the following fields: | ||||||
|  | - **title** (TEXT) - Product name with higher search weight | ||||||
|  | - **description** (TEXT) - Detailed product description | ||||||
|  | - **category** (TAG) - Comma-separated categories | ||||||
|  | - **price** (NUMERIC) - Product price for range queries | ||||||
|  | - **rating** (NUMERIC) - Customer rating for sorting | ||||||
|  | - **location** (GEO) - Geographic coordinates for location searches | ||||||
|  |  | ||||||
|  | ### Key Redis Commands Demonstrated | ||||||
|  |  | ||||||
|  | #### Index Management | ||||||
|  | ```bash | ||||||
|  | # Create search index | ||||||
|  | FT.CREATE product_catalog ON HASH PREFIX 1 product: SCHEMA title TEXT WEIGHT 2.0 SORTABLE description TEXT category TAG SEPARATOR , price NUMERIC SORTABLE rating NUMERIC SORTABLE location GEO | ||||||
|  |  | ||||||
|  | # Get index information | ||||||
|  | FT.INFO product_catalog | ||||||
|  |  | ||||||
|  | # Drop index | ||||||
|  | FT.DROPINDEX product_catalog | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Search Queries | ||||||
|  | ```bash | ||||||
|  | # Basic text search | ||||||
|  | FT.SEARCH product_catalog wireless | ||||||
|  |  | ||||||
|  | # Filtered search | ||||||
|  | FT.SEARCH product_catalog 'organic @category:{food}' | ||||||
|  |  | ||||||
|  | # Numeric range | ||||||
|  | FT.SEARCH product_catalog '@price:[50 150]' | ||||||
|  |  | ||||||
|  | # Sorted results | ||||||
|  | FT.SEARCH product_catalog '@category:{electronics}' SORTBY price ASC | ||||||
|  |  | ||||||
|  | # Geographic search | ||||||
|  | FT.SEARCH product_catalog '@location:[37.7749 -122.4194 50 km]' | ||||||
|  |  | ||||||
|  | # Boolean queries | ||||||
|  | FT.SEARCH product_catalog 'wireless AND audio' | ||||||
|  | FT.SEARCH product_catalog 'coffee OR tea' | ||||||
|  |  | ||||||
|  | # Phrase search | ||||||
|  | FT.SEARCH product_catalog '"noise canceling"' | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Interactive Features | ||||||
|  |  | ||||||
|  | The demo script includes: | ||||||
|  | - **Colored output** for better readability | ||||||
|  | - **Pause between steps** to review results | ||||||
|  | - **Error handling** with clear error messages | ||||||
|  | - **Automatic cleanup** of test data | ||||||
|  | - **Progress indicators** showing what each step demonstrates | ||||||
|  |  | ||||||
|  | ### Troubleshooting | ||||||
|  |  | ||||||
|  | #### HeroDB Not Running | ||||||
|  | ``` | ||||||
|  | ✗ HeroDB is not running on port 6381 | ||||||
|  | ℹ Please start HeroDB with: cargo run -- --port 6381 | ||||||
|  | ``` | ||||||
|  | **Solution**: Start the HeroDB server in a separate terminal. | ||||||
|  |  | ||||||
|  | #### Redis CLI Not Found | ||||||
|  | ``` | ||||||
|  | redis-cli: command not found | ||||||
|  | ``` | ||||||
|  | **Solution**: Install Redis tools or use an alternative Redis client. | ||||||
|  |  | ||||||
|  | #### Connection Refused | ||||||
|  | ``` | ||||||
|  | Could not connect to Redis at localhost:6381: Connection refused | ||||||
|  | ``` | ||||||
|  | **Solution**: Ensure HeroDB is running and listening on the correct port. | ||||||
|  |  | ||||||
|  | ### Manual Testing | ||||||
|  |  | ||||||
|  | You can also run individual commands manually: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Connect to HeroDB | ||||||
|  | redis-cli -h localhost -p 6381 | ||||||
|  |  | ||||||
|  | # Create a simple index | ||||||
|  | FT.CREATE myindex ON HASH SCHEMA title TEXT description TEXT | ||||||
|  |  | ||||||
|  | # Add a document | ||||||
|  | HSET doc:1 title "Hello World" description "This is a test document" | ||||||
|  |  | ||||||
|  | # Search | ||||||
|  | FT.SEARCH myindex hello | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Performance Notes | ||||||
|  |  | ||||||
|  | - **Indexing**: Documents are indexed in real-time as they're added | ||||||
|  | - **Search Speed**: Full-text search is much faster than pattern matching on large datasets | ||||||
|  | - **Memory Usage**: Tantivy indexes are memory-efficient and disk-backed | ||||||
|  | - **Scalability**: Supports millions of documents with sub-second search times | ||||||
|  |  | ||||||
|  | ### Advanced Features | ||||||
|  |  | ||||||
|  | The demo showcases advanced Tantivy features: | ||||||
|  | - **Relevance Scoring** - Results ranked by relevance | ||||||
|  | - **Fuzzy Matching** - Handles typos and approximate matches | ||||||
|  | - **Field Weighting** - Title field has higher search weight | ||||||
|  | - **Multi-field Search** - Search across multiple fields simultaneously | ||||||
|  | - **Geographic Queries** - Distance-based location searches | ||||||
|  | - **Numeric Ranges** - Efficient range queries on numeric fields | ||||||
|  | - **Tag Filtering** - Fast categorical filtering | ||||||
|  |  | ||||||
|  | ### Next Steps | ||||||
|  |  | ||||||
|  | After running the demo, explore: | ||||||
|  | 1. **Custom Schemas** - Define your own field types and configurations | ||||||
|  | 2. **Large Datasets** - Test with thousands or millions of documents | ||||||
|  | 3. **Real Applications** - Integrate search into your applications | ||||||
|  | 4. **Performance Tuning** - Optimize for your specific use case | ||||||
|  |  | ||||||
|  | For more information, see the [search documentation](../herodb/docs/search.md). | ||||||
							
								
								
									
										186
									
								
								examples/simple_demo.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								examples/simple_demo.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | # Simple HeroDB Demo - Basic Redis Commands | ||||||
|  | # This script demonstrates basic Redis functionality that's currently implemented | ||||||
|  |  | ||||||
|  | set -e  # Exit on any error | ||||||
|  |  | ||||||
|  | # Configuration | ||||||
|  | REDIS_HOST="localhost" | ||||||
|  | REDIS_PORT="6381" | ||||||
|  | REDIS_CLI="redis-cli -h $REDIS_HOST -p $REDIS_PORT" | ||||||
|  |  | ||||||
|  | # Colors for output | ||||||
|  | RED='\033[0;31m' | ||||||
|  | GREEN='\033[0;32m' | ||||||
|  | BLUE='\033[0;34m' | ||||||
|  | YELLOW='\033[1;33m' | ||||||
|  | NC='\033[0m' # No Color | ||||||
|  |  | ||||||
|  | # Function to print colored output | ||||||
|  | print_header() { | ||||||
|  |     echo -e "${BLUE}=== $1 ===${NC}" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | print_success() { | ||||||
|  |     echo -e "${GREEN}✓ $1${NC}" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | print_info() { | ||||||
|  |     echo -e "${YELLOW}ℹ $1${NC}" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | print_error() { | ||||||
|  |     echo -e "${RED}✗ $1${NC}" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Function to check if HeroDB is running | ||||||
|  | check_herodb() { | ||||||
|  |     print_info "Checking if HeroDB is running on port $REDIS_PORT..." | ||||||
|  |     if ! $REDIS_CLI ping > /dev/null 2>&1; then | ||||||
|  |         print_error "HeroDB is not running on port $REDIS_PORT" | ||||||
|  |         print_info "Please start HeroDB with: cargo run -- --port $REDIS_PORT" | ||||||
|  |         exit 1 | ||||||
|  |     fi | ||||||
|  |     print_success "HeroDB is running and responding" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Function to execute Redis command with error handling | ||||||
|  | execute_cmd() { | ||||||
|  |     local cmd="$1" | ||||||
|  |     local description="$2" | ||||||
|  |      | ||||||
|  |     echo -e "${YELLOW}Command:${NC} $cmd" | ||||||
|  |     if result=$($REDIS_CLI $cmd 2>&1); then | ||||||
|  |         echo -e "${GREEN}Result:${NC} $result" | ||||||
|  |         return 0 | ||||||
|  |     else | ||||||
|  |         print_error "Failed: $description" | ||||||
|  |         echo "Error: $result" | ||||||
|  |         return 1 | ||||||
|  |     fi | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Main demo function | ||||||
|  | main() { | ||||||
|  |     clear | ||||||
|  |     print_header "HeroDB Basic Functionality Demo" | ||||||
|  |     echo "This demo shows basic Redis commands that are currently implemented" | ||||||
|  |     echo "HeroDB runs on port $REDIS_PORT (instead of Redis default 6379)" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     # Check if HeroDB is running | ||||||
|  |     check_herodb | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 1: Basic Key-Value Operations" | ||||||
|  |      | ||||||
|  |     execute_cmd "SET greeting 'Hello HeroDB!'" "Setting a simple key-value pair" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "GET greeting" "Getting the value" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "SET counter 42" "Setting a numeric value" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "INCR counter" "Incrementing the counter" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "GET counter" "Getting the incremented value" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 2: Hash Operations" | ||||||
|  |      | ||||||
|  |     execute_cmd "HSET user:1 name 'John Doe' email 'john@example.com' age 30" "Setting hash fields" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "HGET user:1 name" "Getting a specific field" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "HGETALL user:1" "Getting all fields" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "HLEN user:1" "Getting hash length" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 3: List Operations" | ||||||
|  |      | ||||||
|  |     execute_cmd "LPUSH tasks 'Write code' 'Test code' 'Deploy code'" "Adding items to list" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "LLEN tasks" "Getting list length" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "LRANGE tasks 0 -1" "Getting all list items" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "LPOP tasks" "Popping from left" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "LRANGE tasks 0 -1" "Checking remaining items" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 4: Key Management" | ||||||
|  |      | ||||||
|  |     execute_cmd "KEYS *" "Listing all keys" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "EXISTS greeting" "Checking if key exists" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "TYPE user:1" "Getting key type" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "DBSIZE" "Getting database size" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 5: Expiration" | ||||||
|  |      | ||||||
|  |     execute_cmd "SET temp_key 'temporary value'" "Setting temporary key" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "EXPIRE temp_key 5" "Setting 5 second expiration" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "TTL temp_key" "Checking time to live" | ||||||
|  |     echo | ||||||
|  |     print_info "Waiting 2 seconds..." | ||||||
|  |     sleep 2 | ||||||
|  |     execute_cmd "TTL temp_key" "Checking TTL again" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 6: Multiple Operations" | ||||||
|  |      | ||||||
|  |     execute_cmd "MSET key1 'value1' key2 'value2' key3 'value3'" "Setting multiple keys" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "MGET key1 key2 key3" "Getting multiple values" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "DEL key1 key2" "Deleting multiple keys" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "EXISTS key1 key2 key3" "Checking existence of multiple keys" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 7: Search Commands (Placeholder)" | ||||||
|  |     print_info "Testing FT.CREATE command (currently returns placeholder response)" | ||||||
|  |      | ||||||
|  |     execute_cmd "FT.CREATE test_index SCHEMA title TEXT description TEXT" "Creating search index" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 8: Server Information" | ||||||
|  |      | ||||||
|  |     execute_cmd "INFO" "Getting server information" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "CONFIG GET dir" "Getting configuration" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Step 9: Cleanup" | ||||||
|  |      | ||||||
|  |     execute_cmd "FLUSHDB" "Clearing database" | ||||||
|  |     echo | ||||||
|  |     execute_cmd "DBSIZE" "Confirming database is empty" | ||||||
|  |     echo | ||||||
|  |  | ||||||
|  |     print_header "Demo Summary" | ||||||
|  |     echo "This demonstration showed:" | ||||||
|  |     echo "• Basic key-value operations (GET, SET, INCR)" | ||||||
|  |     echo "• Hash operations (HSET, HGET, HGETALL)" | ||||||
|  |     echo "• List operations (LPUSH, LPOP, LRANGE)" | ||||||
|  |     echo "• Key management (KEYS, EXISTS, TYPE, DEL)" | ||||||
|  |     echo "• Expiration handling (EXPIRE, TTL)" | ||||||
|  |     echo "• Multiple key operations (MSET, MGET)" | ||||||
|  |     echo "• Server information commands" | ||||||
|  |     echo | ||||||
|  |     print_success "HeroDB basic functionality demo completed successfully!" | ||||||
|  |     echo | ||||||
|  |     print_info "Note: Full-text search (FT.*) commands are defined but not yet fully implemented" | ||||||
|  |     print_info "To run HeroDB server: cargo run -- --port 6381" | ||||||
|  |     print_info "To connect with redis-cli: redis-cli -h localhost -p 6381" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Run the demo | ||||||
|  | main "$@" | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| [package] |  | ||||||
| name = "herodb" |  | ||||||
| version = "0.0.1" |  | ||||||
| authors = ["ThreeFold Tech"] |  | ||||||
| edition = "2024" |  | ||||||
|  |  | ||||||
| [dependencies] |  | ||||||
| anyhow = "1.0.59" |  | ||||||
| bytes = "1.3.0" |  | ||||||
| thiserror = "1.0.32" |  | ||||||
| tokio = { version = "1.23.0", features = ["full"] } |  | ||||||
| clap = { version = "4.5.20", features = ["derive"] } |  | ||||||
| byteorder = "1.4.3" |  | ||||||
| futures = "0.3" |  | ||||||
| redb = "2.1.3" |  | ||||||
| serde = { version = "1.0", features = ["derive"] } |  | ||||||
| serde_json = "1.0" |  | ||||||
| bincode = "1.3.3" |  | ||||||
| chacha20poly1305 = "0.10.1" |  | ||||||
| rand = "0.8" |  | ||||||
| sha2 = "0.10" |  | ||||||
| age = "0.10" |  | ||||||
| secrecy = "0.8" |  | ||||||
| ed25519-dalek = "2" |  | ||||||
| base64 = "0.22" |  | ||||||
|  |  | ||||||
| [dev-dependencies] |  | ||||||
| redis = { version = "0.24", features = ["aio", "tokio-comp"] } |  | ||||||
| @@ -1,227 +0,0 @@ | |||||||
|  |  | ||||||
| # HeroDB Redis Protocol Support: Commands & Client Usage |  | ||||||
|  |  | ||||||
| HeroDB is a Redis-compatible database built using the `redb` database backend.  |  | ||||||
|  |  | ||||||
| It supports a subset of Redis commands over the standard RESP (Redis Serialization Protocol) via TCP, allowing you to interact with it using standard Redis clients like `redis-cli`, Python's `redis-py`, Node.js's `ioredis`, etc. |  | ||||||
|  |  | ||||||
| This document provides: |  | ||||||
| - A list of all currently supported Redis commands. |  | ||||||
| - Example usage with standard Redis clients. |  | ||||||
| - Bash and Rust test-inspired usage examples. |  | ||||||
|  |  | ||||||
| ## Quick Start |  | ||||||
|  |  | ||||||
| Assuming the server is running on localhost at port `$PORT`: |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| # Build HeroDB |  | ||||||
| cargo build --release |  | ||||||
|  |  | ||||||
| # Start HeroDB server |  | ||||||
| ./target/release/herodb --dir /tmp/herodb_data --port 6381 --debug |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Using Standard Redis Clients |  | ||||||
|  |  | ||||||
| ### With `redis-cli` |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 SET mykey "hello" |  | ||||||
| redis-cli -p 6381 GET mykey |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### With Python (`redis-py`) |  | ||||||
|  |  | ||||||
| ```python |  | ||||||
| import redis |  | ||||||
|  |  | ||||||
| r = redis.Redis(host='localhost', port=6381, db=0) |  | ||||||
| r.set('mykey', 'hello') |  | ||||||
| print(r.get('mykey').decode()) |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### With Node.js (`ioredis`) |  | ||||||
|  |  | ||||||
| ```js |  | ||||||
| const Redis = require("ioredis"); |  | ||||||
| const redis = new Redis({ port: 6381, host: "localhost" }); |  | ||||||
|  |  | ||||||
| await redis.set("mykey", "hello"); |  | ||||||
| const value = await redis.get("mykey"); |  | ||||||
| console.log(value); // "hello" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Supported Redis Commands |  | ||||||
|  |  | ||||||
| ### String Commands |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `SET`         | Set a key to a string value              | `SET name "Alice"`                        | |  | ||||||
| | `GET`         | Get the value of a key                   | `GET name`                                | |  | ||||||
| | `DEL`         | Delete one or more keys                  | `DEL name age`                            | |  | ||||||
| | `INCR`        | Increment the integer value of a key     | `INCR counter`                            | |  | ||||||
| | `DECR`        | Decrement the integer value of a key     | `DECR counter`                            | |  | ||||||
| | `INCRBY`      | Increment key by a given integer         | `INCRBY counter 5`                        | |  | ||||||
| | `DECRBY`      | Decrement key by a given integer         | `DECRBY counter 3`                        | |  | ||||||
| | `EXISTS`      | Check if a key exists                    | `EXISTS name`                             | |  | ||||||
| | `TYPE`        | Return the type of a key                 | `TYPE name`                               | |  | ||||||
|  |  | ||||||
| ### Hash Commands |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `HSET`        | Set field in hash stored at key          | `HSET user:1 name "Alice"`                | |  | ||||||
| | `HGET`        | Get value of a field in hash             | `HGET user:1 name`                        | |  | ||||||
| | `HGETALL`     | Get all fields and values in a hash      | `HGETALL user:1`                          | |  | ||||||
| | `HDEL`        | Delete one or more fields from hash      | `HDEL user:1 name age`                    | |  | ||||||
| | `HEXISTS`     | Check if field exists in hash            | `HEXISTS user:1 name`                     | |  | ||||||
| | `HKEYS`       | Get all field names in a hash            | `HKEYS user:1`                            | |  | ||||||
| | `HVALS`       | Get all values in a hash                 | `HVALS user:1`                            | |  | ||||||
| | `HLEN`        | Get number of fields in a hash           | `HLEN user:1`                             | |  | ||||||
| | `HMGET`       | Get values of multiple fields            | `HMGET user:1 name age`                   | |  | ||||||
| | `HSETNX`      | Set field only if it does not exist      | `HSETNX user:1 email alice@example.com`   | |  | ||||||
|  |  | ||||||
| ### List Commands |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `LPUSH`       | Insert elements at the head of a list    | `LPUSH mylist "item1" "item2"`            | |  | ||||||
| | `RPUSH`       | Insert elements at the tail of a list    | `RPUSH mylist "item3" "item4"`            | |  | ||||||
| | `LPOP`        | Remove and return element from head      | `LPOP mylist`                             | |  | ||||||
| | `RPOP`        | Remove and return element from tail      | `RPOP mylist`                             | |  | ||||||
| | `BLPOP`       | Blocking remove from head with timeout   | `BLPOP mylist1 mylist2 5`                 | |  | ||||||
| | `BRPOP`       | Blocking remove from tail with timeout   | `BRPOP mylist1 mylist2 5`                 | |  | ||||||
| | `LLEN`        | Get the length of a list                 | `LLEN mylist`                             | |  | ||||||
| | `LREM`        | Remove elements from list                | `LREM mylist 2 "item"`                    | |  | ||||||
| | `LTRIM`       | Trim list to specified range             | `LTRIM mylist 0 5`                         | |  | ||||||
| | `LINDEX`      | Get element by index                     | `LINDEX mylist 0`                         | |  | ||||||
| | `LRANGE`      | Get range of elements                    | `LRANGE mylist 0 -1`                      | |  | ||||||
|  |  | ||||||
| ### Keys & Scanning |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `KEYS`        | Find all keys matching a pattern         | `KEYS user:*`                             | |  | ||||||
| | `SCAN`        | Incrementally iterate keys               | `SCAN 0 MATCH user:* COUNT 10`            | |  | ||||||
|  |  | ||||||
| ### Expiration |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `EXPIRE`      | Set a key's time to live in seconds      | `EXPIRE tempkey 60`                       | |  | ||||||
| | `TTL`         | Get the time to live for a key           | `TTL tempkey`                             | |  | ||||||
| | `PERSIST`     | Remove the expiration from a key         | `PERSIST tempkey`                         | |  | ||||||
|  |  | ||||||
| ### Transactions |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `MULTI`       | Start a transaction block                | `MULTI`                                   | |  | ||||||
| | `EXEC`        | Execute all commands in a transaction    | `EXEC`                                    | |  | ||||||
| | `DISCARD`     | Discard all commands in a transaction    | `DISCARD`                                 | |  | ||||||
|  |  | ||||||
| ### Configuration |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `CONFIG GET`  | Get configuration parameters             | `CONFIG GET dir`                           | |  | ||||||
| | `CONFIG SET`  | Set configuration parameters             | `CONFIG SET maxmemory 100mb`               | |  | ||||||
|  |  | ||||||
| ### Info & Monitoring |  | ||||||
|  |  | ||||||
| | Command       | Description                              | Example Usage                             | |  | ||||||
| |---------------|------------------------------------------|-------------------------------------------| |  | ||||||
| | `INFO`        | Get information and statistics about server | `INFO`                                 | |  | ||||||
| | `PING`        | Ping the server                          | `PING`                                    | |  | ||||||
|  |  | ||||||
| ### AGE Cryptography Commands |  | ||||||
|  |  | ||||||
| | Command            | Description                                   | Example Usage                                 | |  | ||||||
| |--------------------|-----------------------------------------------|-----------------------------------------------| |  | ||||||
| | `AGE GENENC`       | Generate ephemeral encryption keypair         | `AGE GENENC`                                  | |  | ||||||
| | `AGE GENSIGN`      | Generate ephemeral signing keypair            | `AGE GENSIGN`                                 | |  | ||||||
| | `AGE ENCRYPT`      | Encrypt a message using a public key          | `AGE ENCRYPT <recipient> "msg"`               | |  | ||||||
| | `AGE DECRYPT`      | Decrypt a message using a secret key          | `AGE DECRYPT <identity> <ciphertext>`        | |  | ||||||
| | `AGE SIGN`         | Sign a message using a secret key            | `AGE SIGN <sign_secret> "msg"`                | |  | ||||||
| | `AGE VERIFY`       | Verify a signature using a public key         | `AGE VERIFY <pubkey> "msg" <signature>`      | |  | ||||||
| | `AGE KEYGEN`       | Create and persist a named encryption key    | `AGE KEYGEN app1`                             | |  | ||||||
| | `AGE SIGNKEYGEN`   | Create and persist a named signing key       | `AGE SIGNKEYGEN app1`                          | |  | ||||||
| | `AGE ENCRYPTNAME`  | Encrypt using a named key                     | `AGE ENCRYPTNAME app1 "msg"`                   | |  | ||||||
| | `AGE DECRYPTNAME`  | Decrypt using a named key                     | `AGE DECRYPTNAME app1 <ciphertext>`           | |  | ||||||
| | `AGE SIGNNAME`     | Sign using a named key                       | `AGE SIGNNAME app1 "msg"`                      | |  | ||||||
| | `AGE VERIFYNAME`   | Verify using a named key                      | `AGE VERIFYNAME app1 "msg" <signature>`       | |  | ||||||
| | `AGE LIST`         | List all persisted named keys                | `AGE LIST`                                    | |  | ||||||
|  |  | ||||||
| > Note: AGE commands are not part of standard Redis. They are HeroDB-specific extensions for cryptographic operations. |  | ||||||
|  |  | ||||||
| ## Example Usage |  | ||||||
|  |  | ||||||
| ### Basic String Operations |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 SET greeting "Hello, HeroDB!" |  | ||||||
| redis-cli -p 6381 GET greeting |  | ||||||
| # → "Hello, HeroDB!" |  | ||||||
|  |  | ||||||
| redis-cli -p 6381 INCR visits |  | ||||||
| redis-cli -p 6381 INCR visits |  | ||||||
| redis-cli -p 6381 GET visits |  | ||||||
| # → "2" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Hash Operations |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 HSET user:1000 name "Alice" age "30" city "NYC" |  | ||||||
| redis-cli -p 6381 HGET user:1000 name |  | ||||||
| # → "Alice" |  | ||||||
|  |  | ||||||
| redis-cli -p 6381 HGETALL user:1000 |  | ||||||
| # → 1) "name" |  | ||||||
| #    2) "Alice" |  | ||||||
| #    3) "age" |  | ||||||
| #    4) "30" |  | ||||||
| #    5) "city" |  | ||||||
| #    6) "NYC" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Expiration |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 SET tempkey "temporary" |  | ||||||
| redis-cli -p 6381 EXPIRE tempkey 5 |  | ||||||
| redis-cli -p 6381 TTL tempkey |  | ||||||
| # → (integer) 4 |  | ||||||
|  |  | ||||||
| # After 5 seconds: |  | ||||||
| redis-cli -p 6381 GET tempkey |  | ||||||
| # → (nil) |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Transactions |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 MULTI |  | ||||||
| redis-cli -p 6381 SET txkey1 "value1" |  | ||||||
| redis-cli -p 6381 SET txkey2 "value2" |  | ||||||
| redis-cli -p 6381 INCR counter |  | ||||||
| redis-cli -p 6381 EXEC |  | ||||||
| # → 1) OK |  | ||||||
| #    2) OK |  | ||||||
| #    3) (integer) 3 |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Scanning Keys |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| redis-cli -p 6381 SET scankey1 "val1" |  | ||||||
| redis-cli -p 6381 SET scankey2 "val2" |  | ||||||
| redis-cli -p 6381 HSET scanhash field1 "val1" |  | ||||||
|  |  | ||||||
| redis-cli -p 6381 SCAN 0 MATCH scankey* |  | ||||||
| # → 1) "0" |  | ||||||
| #    2) 1) "scankey1" |  | ||||||
| #       2) "scankey2" |  | ||||||
| ``` |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| #[derive(Clone)] |  | ||||||
| pub struct DBOption { |  | ||||||
|     pub dir: String, |  | ||||||
|     pub port: u16, |  | ||||||
|     pub debug: bool, |  | ||||||
|     pub encrypt: bool, |  | ||||||
|     pub encryption_key: Option<String>, // Master encryption key |  | ||||||
| } |  | ||||||
| @@ -1,126 +0,0 @@ | |||||||
| use std::{ |  | ||||||
|     path::Path, |  | ||||||
|     time::{SystemTime, UNIX_EPOCH}, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use redb::{Database, TableDefinition}; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
|  |  | ||||||
| use crate::crypto::CryptoFactory; |  | ||||||
| use crate::error::DBError; |  | ||||||
|  |  | ||||||
| // Re-export modules |  | ||||||
| mod storage_basic; |  | ||||||
| mod storage_hset; |  | ||||||
| mod storage_lists; |  | ||||||
| mod storage_extra; |  | ||||||
|  |  | ||||||
| // Re-export implementations |  | ||||||
| // Note: These imports are used by the impl blocks in the submodules |  | ||||||
| // The compiler shows them as unused because they're not directly used in this file |  | ||||||
| // but they're needed for the Storage struct methods to be available |  | ||||||
| pub use storage_extra::*; |  | ||||||
|  |  | ||||||
| // Table definitions for different Redis data types |  | ||||||
| const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types"); |  | ||||||
| const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings"); |  | ||||||
| const HASHES_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("hashes"); |  | ||||||
| const LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("lists"); |  | ||||||
| const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta"); |  | ||||||
| const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data"); |  | ||||||
| const ENCRYPTED_TABLE: TableDefinition<&str, u8> = TableDefinition::new("encrypted"); |  | ||||||
| const EXPIRATION_TABLE: TableDefinition<&str, u64> = TableDefinition::new("expiration"); |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone)] |  | ||||||
| pub struct StreamEntry { |  | ||||||
|     pub fields: Vec<(String, String)>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone)] |  | ||||||
| pub struct ListValue { |  | ||||||
|     pub elements: Vec<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[inline] |  | ||||||
| pub fn now_in_millis() -> u128 { |  | ||||||
|     let start = SystemTime::now(); |  | ||||||
|     let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap(); |  | ||||||
|     duration_since_epoch.as_millis() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub struct Storage { |  | ||||||
|     db: Database, |  | ||||||
|     crypto: Option<CryptoFactory>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Storage { |  | ||||||
|     pub fn new(path: impl AsRef<Path>, should_encrypt: bool, master_key: Option<&str>) -> Result<Self, DBError> { |  | ||||||
|         let db = Database::create(path)?; |  | ||||||
|          |  | ||||||
|         // Create tables if they don't exist |  | ||||||
|         let write_txn = db.begin_write()?; |  | ||||||
|         { |  | ||||||
|             let _ = write_txn.open_table(TYPES_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(STRINGS_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(HASHES_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(LISTS_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(STREAMS_META_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(STREAMS_DATA_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(ENCRYPTED_TABLE)?; |  | ||||||
|             let _ = write_txn.open_table(EXPIRATION_TABLE)?; |  | ||||||
|         } |  | ||||||
|         write_txn.commit()?; |  | ||||||
|          |  | ||||||
|         // Check if database was previously encrypted |  | ||||||
|         let read_txn = db.begin_read()?; |  | ||||||
|         let encrypted_table = read_txn.open_table(ENCRYPTED_TABLE)?; |  | ||||||
|         let was_encrypted = encrypted_table.get("encrypted")?.map(|v| v.value() == 1).unwrap_or(false); |  | ||||||
|         drop(read_txn); |  | ||||||
|          |  | ||||||
|         let crypto = if should_encrypt || was_encrypted { |  | ||||||
|             if let Some(key) = master_key { |  | ||||||
|                 Some(CryptoFactory::new(key.as_bytes())) |  | ||||||
|             } else { |  | ||||||
|                 return Err(DBError("Encryption requested but no master key provided".to_string())); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             None |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         // If we're enabling encryption for the first time, mark it |  | ||||||
|         if should_encrypt && !was_encrypted { |  | ||||||
|             let write_txn = db.begin_write()?; |  | ||||||
|             { |  | ||||||
|                 let mut encrypted_table = write_txn.open_table(ENCRYPTED_TABLE)?; |  | ||||||
|                 encrypted_table.insert("encrypted", &1u8)?; |  | ||||||
|             } |  | ||||||
|             write_txn.commit()?; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         Ok(Storage { |  | ||||||
|             db, |  | ||||||
|             crypto, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     pub fn is_encrypted(&self) -> bool { |  | ||||||
|         self.crypto.is_some() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Helper methods for encryption |  | ||||||
|     fn encrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { |  | ||||||
|         if let Some(crypto) = &self.crypto { |  | ||||||
|             Ok(crypto.encrypt(data)) |  | ||||||
|         } else { |  | ||||||
|             Ok(data.to_vec()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     fn decrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { |  | ||||||
|         if let Some(crypto) = &self.crypto { |  | ||||||
|             Ok(crypto.decrypt(data)?) |  | ||||||
|         } else { |  | ||||||
|             Ok(data.to_vec()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										1251
									
								
								specs/backgroundinfo/lance.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1251
									
								
								specs/backgroundinfo/lance.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6847
									
								
								specs/backgroundinfo/lancedb.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6847
									
								
								specs/backgroundinfo/lancedb.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										113
									
								
								specs/backgroundinfo/sled.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								specs/backgroundinfo/sled.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | ======================== | ||||||
|  | CODE SNIPPETS | ||||||
|  | ======================== | ||||||
|  | TITLE: Basic Database Operations with sled in Rust | ||||||
|  | DESCRIPTION: This snippet demonstrates fundamental operations using the `sled` embedded database in Rust. It covers opening a database tree, inserting and retrieving key-value pairs, performing range queries, deleting entries, and executing an atomic compare-and-swap operation. It also shows how to flush changes to disk for durability. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/README.md#_snippet_0 | ||||||
|  |  | ||||||
|  | LANGUAGE: Rust | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | let tree = sled::open("/tmp/welcome-to-sled")?; | ||||||
|  |  | ||||||
|  | // insert and get, similar to std's BTreeMap | ||||||
|  | let old_value = tree.insert("key", "value")?; | ||||||
|  |  | ||||||
|  | assert_eq!( | ||||||
|  |   tree.get(&"key")?, | ||||||
|  |   Some(sled::IVec::from("value")), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | // range queries | ||||||
|  | for kv_result in tree.range("key_1".."key_9") {} | ||||||
|  |  | ||||||
|  | // deletion | ||||||
|  | let old_value = tree.remove(&"key")?; | ||||||
|  |  | ||||||
|  | // atomic compare and swap | ||||||
|  | tree.compare_and_swap( | ||||||
|  |   "key", | ||||||
|  |   Some("current_value"), | ||||||
|  |   Some("new_value"), | ||||||
|  | )?; | ||||||
|  |  | ||||||
|  | // block until all operations are stable on disk | ||||||
|  | // (flush_async also available to get a Future) | ||||||
|  | tree.flush()?; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ---------------------------------------- | ||||||
|  |  | ||||||
|  | TITLE: Subscribing to sled Events Asynchronously (Rust) | ||||||
|  | DESCRIPTION: This snippet demonstrates how to asynchronously subscribe to events on key prefixes in a `sled` database. It initializes a `sled` database, creates a `Subscriber` for all key prefixes, inserts a key-value pair to trigger an event, and then uses `extreme::run` to await and process incoming events. The `Subscriber` struct implements `Future<Output=Option<Event>>`, allowing it to be awaited in an async context. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/README.md#_snippet_1 | ||||||
|  |  | ||||||
|  | LANGUAGE: Rust | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | let sled = sled::open("my_db").unwrap(); | ||||||
|  |  | ||||||
|  | let mut sub = sled.watch_prefix(""); | ||||||
|  |  | ||||||
|  | sled.insert(b"a", b"a").unwrap(); | ||||||
|  |  | ||||||
|  | extreme::run(async move { | ||||||
|  |     while let Some(event) = (&mut sub).await { | ||||||
|  |         println!("got event {:?}", event); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ---------------------------------------- | ||||||
|  |  | ||||||
|  | TITLE: Iterating Subscriber Events with Async/Await in Rust | ||||||
|  | DESCRIPTION: This snippet demonstrates how to asynchronously iterate over events from a `Subscriber` instance in Rust. Since `Subscriber` now implements `Future`, it can be awaited in a loop to process incoming events, enabling efficient prefix watching. The loop continues as long as new events are available. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/CHANGELOG.md#_snippet_0 | ||||||
|  |  | ||||||
|  | LANGUAGE: Rust | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | while let Some(event) = (&mut subscriber).await {} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ---------------------------------------- | ||||||
|  |  | ||||||
|  | TITLE: Suppressing TSAN Race on Arc::drop in Rust | ||||||
|  | DESCRIPTION: This suppression addresses a false positive race detection by ThreadSanitizer in Rust's `Arc::drop` implementation. TSAN fails to correctly reason about the raw atomic `Acquire` fence used after the strong-count atomic subtraction with a `Release` fence in the `Drop` implementation, leading to an erroneous race report. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/tsan_suppressions.txt#_snippet_0 | ||||||
|  |  | ||||||
|  | LANGUAGE: TSAN Suppression | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | race:Arc*drop | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ---------------------------------------- | ||||||
|  |  | ||||||
|  | TITLE: Suppressing TSAN Race on std::thread::local in Rust | ||||||
|  | DESCRIPTION: This suppression addresses ThreadSanitizer false positives when using Rust's `std::thread::local`. Similar to `lazy_static`, `std::thread::local` utilizes implicit barriers that TSAN fails to recognize, leading to incorrect race condition reports. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/tsan_suppressions.txt#_snippet_2 | ||||||
|  |  | ||||||
|  | LANGUAGE: TSAN Suppression | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | race:std::thread::local | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ---------------------------------------- | ||||||
|  |  | ||||||
|  | TITLE: Suppressing TSAN Race on lazy_static in Rust | ||||||
|  | DESCRIPTION: This suppression targets ThreadSanitizer false positives related to the `lazy_static` crate in Rust. `lazy_static` relies on implicit memory barriers that TSAN does not correctly detect, causing it to report races where none exist. | ||||||
|  |  | ||||||
|  | SOURCE: https://github.com/spacejam/sled/blob/main/tsan_suppressions.txt#_snippet_1 | ||||||
|  |  | ||||||
|  | LANGUAGE: TSAN Suppression | ||||||
|  | CODE: | ||||||
|  | ``` | ||||||
|  | race:lazy_static | ||||||
|  | ``` | ||||||
							
								
								
									
										735
									
								
								specs/lance_implementation.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										735
									
								
								specs/lance_implementation.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,735 @@ | |||||||
|  | Looking at your request to ensure embedding happens with an external embedding provider configured in Redis at `config:core:aiembed:url`, I'll modify the implementation to use an external embedding service instead of local models. | ||||||
|  |  | ||||||
|  | ## Updated Implementation with External Embedding Provider | ||||||
|  |  | ||||||
|  | ### 1. Update Dependencies in `Cargo.toml` | ||||||
|  |  | ||||||
|  | ```toml | ||||||
|  | [dependencies] | ||||||
|  | # ... existing dependencies ... | ||||||
|  | lance = "0.18" | ||||||
|  | lance-index = "0.18" | ||||||
|  | lance-linalg = "0.18" | ||||||
|  | arrow = "52" | ||||||
|  | arrow-array = "52" | ||||||
|  | arrow-schema = "52" | ||||||
|  | parquet = "52" | ||||||
|  | uuid = { version = "1.10", features = ["v4"] } | ||||||
|  | reqwest = { version = "0.11", features = ["json"] } | ||||||
|  | serde = { version = "1.0", features = ["derive"] } | ||||||
|  | serde_json = "1.0" | ||||||
|  | base64 = "0.22" | ||||||
|  | image = "0.25" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 2. Create Enhanced Lance Module with External Embedding | ||||||
|  |  | ||||||
|  | Create `src/lance_store.rs`: | ||||||
|  |  | ||||||
|  | ```rust | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use std::sync::Arc; | ||||||
|  | use tokio::sync::RwLock; | ||||||
|  |  | ||||||
|  | use arrow::array::{Float32Array, StringArray, BinaryArray, ArrayRef}; | ||||||
|  | use arrow::datatypes::{DataType, Field, Schema}; | ||||||
|  | use arrow::record_batch::RecordBatch; | ||||||
|  | use lance::dataset::{Dataset, WriteParams, WriteMode}; | ||||||
|  | use lance::index::vector::VectorIndexParams; | ||||||
|  | use lance_index::vector::pq::PQBuildParams; | ||||||
|  | use lance_index::vector::ivf::IvfBuildParams; | ||||||
|  |  | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use crate::error::DBError; | ||||||
|  | use crate::cmd::Protocol; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | struct EmbeddingRequest { | ||||||
|  |     texts: Option<Vec<String>>, | ||||||
|  |     images: Option<Vec<String>>, // base64 encoded | ||||||
|  |     model: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | struct EmbeddingResponse { | ||||||
|  |     embeddings: Vec<Vec<f32>>, | ||||||
|  |     model: String, | ||||||
|  |     usage: Option<HashMap<String, u32>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct LanceStore { | ||||||
|  |     datasets: Arc<RwLock<HashMap<String, Arc<Dataset>>>>, | ||||||
|  |     data_dir: PathBuf, | ||||||
|  |     http_client: reqwest::Client, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl LanceStore { | ||||||
|  |     pub async fn new(data_dir: PathBuf) -> Result<Self, DBError> { | ||||||
|  |         // Create data directory if it doesn't exist | ||||||
|  |         std::fs::create_dir_all(&data_dir) | ||||||
|  |             .map_err(|e| DBError(format!("Failed to create Lance data directory: {}", e)))?; | ||||||
|  |          | ||||||
|  |         let http_client = reqwest::Client::builder() | ||||||
|  |             .timeout(std::time::Duration::from_secs(30)) | ||||||
|  |             .build() | ||||||
|  |             .map_err(|e| DBError(format!("Failed to create HTTP client: {}", e)))?; | ||||||
|  |          | ||||||
|  |         Ok(Self { | ||||||
|  |             datasets: Arc::new(RwLock::new(HashMap::new())), | ||||||
|  |             data_dir, | ||||||
|  |             http_client, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /// Get embedding service URL from Redis config | ||||||
|  |     async fn get_embedding_url(&self, server: &crate::server::Server) -> Result<String, DBError> { | ||||||
|  |         // Get the embedding URL from Redis config | ||||||
|  |         let key = "config:core:aiembed:url"; | ||||||
|  |          | ||||||
|  |         // Use HGET to retrieve the URL from Redis hash | ||||||
|  |         let cmd = crate::cmd::Cmd::HGet { | ||||||
|  |             key: key.to_string(), | ||||||
|  |             field: "url".to_string(), | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Execute command to get the config | ||||||
|  |         let result = cmd.run(server).await?; | ||||||
|  |          | ||||||
|  |         match result { | ||||||
|  |             Protocol::BulkString(url) => Ok(url), | ||||||
|  |             Protocol::SimpleString(url) => Ok(url), | ||||||
|  |             Protocol::Nil => Err(DBError( | ||||||
|  |                 "Embedding service URL not configured. Set it with: HSET config:core:aiembed:url url <YOUR_EMBEDDING_SERVICE_URL>".to_string() | ||||||
|  |             )), | ||||||
|  |             _ => Err(DBError("Invalid embedding URL configuration".to_string())), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /// Call external embedding service | ||||||
|  |     async fn call_embedding_service( | ||||||
|  |         &self, | ||||||
|  |         server: &crate::server::Server, | ||||||
|  |         texts: Option<Vec<String>>, | ||||||
|  |         images: Option<Vec<String>>, | ||||||
|  |     ) -> Result<Vec<Vec<f32>>, DBError> { | ||||||
|  |         let url = self.get_embedding_url(server).await?; | ||||||
|  |          | ||||||
|  |         let request = EmbeddingRequest { | ||||||
|  |             texts, | ||||||
|  |             images, | ||||||
|  |             model: None, // Let the service use its default | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let response = self.http_client | ||||||
|  |             .post(&url) | ||||||
|  |             .json(&request) | ||||||
|  |             .send() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| DBError(format!("Failed to call embedding service: {}", e)))?; | ||||||
|  |          | ||||||
|  |         if !response.status().is_success() { | ||||||
|  |             let status = response.status(); | ||||||
|  |             let error_text = response.text().await.unwrap_or_default(); | ||||||
|  |             return Err(DBError(format!( | ||||||
|  |                 "Embedding service returned error {}: {}",  | ||||||
|  |                 status, error_text | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let embedding_response: EmbeddingResponse = response | ||||||
|  |             .json() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| DBError(format!("Failed to parse embedding response: {}", e)))?; | ||||||
|  |          | ||||||
|  |         Ok(embedding_response.embeddings) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn embed_text( | ||||||
|  |         &self,  | ||||||
|  |         server: &crate::server::Server, | ||||||
|  |         texts: Vec<String> | ||||||
|  |     ) -> Result<Vec<Vec<f32>>, DBError> { | ||||||
|  |         if texts.is_empty() { | ||||||
|  |             return Ok(Vec::new()); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         self.call_embedding_service(server, Some(texts), None).await | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn embed_image( | ||||||
|  |         &self, | ||||||
|  |         server: &crate::server::Server, | ||||||
|  |         image_bytes: Vec<u8> | ||||||
|  |     ) -> Result<Vec<f32>, DBError> { | ||||||
|  |         // Convert image bytes to base64 | ||||||
|  |         let base64_image = base64::encode(&image_bytes); | ||||||
|  |          | ||||||
|  |         let embeddings = self.call_embedding_service( | ||||||
|  |             server,  | ||||||
|  |             None,  | ||||||
|  |             Some(vec![base64_image]) | ||||||
|  |         ).await?; | ||||||
|  |          | ||||||
|  |         embeddings.into_iter() | ||||||
|  |             .next() | ||||||
|  |             .ok_or_else(|| DBError("No embedding returned for image".to_string())) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn create_dataset( | ||||||
|  |         &self, | ||||||
|  |         name: &str, | ||||||
|  |         schema: Schema, | ||||||
|  |     ) -> Result<(), DBError> { | ||||||
|  |         let dataset_path = self.data_dir.join(format!("{}.lance", name)); | ||||||
|  |          | ||||||
|  |         // Create empty dataset with schema | ||||||
|  |         let write_params = WriteParams { | ||||||
|  |             mode: WriteMode::Create, | ||||||
|  |             ..Default::default() | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Create an empty RecordBatch with the schema | ||||||
|  |         let empty_batch = RecordBatch::new_empty(Arc::new(schema)); | ||||||
|  |         let batches = vec![empty_batch]; | ||||||
|  |          | ||||||
|  |         let dataset = Dataset::write( | ||||||
|  |             batches, | ||||||
|  |             dataset_path.to_str().unwrap(), | ||||||
|  |             Some(write_params) | ||||||
|  |         ).await | ||||||
|  |         .map_err(|e| DBError(format!("Failed to create dataset: {}", e)))?; | ||||||
|  |          | ||||||
|  |         let mut datasets = self.datasets.write().await; | ||||||
|  |         datasets.insert(name.to_string(), Arc::new(dataset)); | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn write_vectors( | ||||||
|  |         &self, | ||||||
|  |         dataset_name: &str, | ||||||
|  |         vectors: Vec<Vec<f32>>, | ||||||
|  |         metadata: Option<HashMap<String, Vec<String>>>, | ||||||
|  |     ) -> Result<usize, DBError> { | ||||||
|  |         let dataset_path = self.data_dir.join(format!("{}.lance", dataset_name)); | ||||||
|  |          | ||||||
|  |         // Open or get cached dataset | ||||||
|  |         let dataset = self.get_or_open_dataset(dataset_name).await?; | ||||||
|  |          | ||||||
|  |         // Build RecordBatch | ||||||
|  |         let num_vectors = vectors.len(); | ||||||
|  |         if num_vectors == 0 { | ||||||
|  |             return Ok(0); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let dim = vectors.first() | ||||||
|  |             .ok_or_else(|| DBError("Empty vectors".to_string()))? | ||||||
|  |             .len(); | ||||||
|  |          | ||||||
|  |         // Flatten vectors | ||||||
|  |         let flat_vectors: Vec<f32> = vectors.into_iter().flatten().collect(); | ||||||
|  |         let vector_array = Float32Array::from(flat_vectors); | ||||||
|  |         let vector_array = arrow::array::FixedSizeListArray::try_new_from_values( | ||||||
|  |             vector_array,  | ||||||
|  |             dim as i32 | ||||||
|  |         ).map_err(|e| DBError(format!("Failed to create vector array: {}", e)))?; | ||||||
|  |          | ||||||
|  |         let mut arrays: Vec<ArrayRef> = vec![Arc::new(vector_array)]; | ||||||
|  |         let mut fields = vec![Field::new( | ||||||
|  |             "vector", | ||||||
|  |             DataType::FixedSizeList( | ||||||
|  |                 Arc::new(Field::new("item", DataType::Float32, true)), | ||||||
|  |                 dim as i32 | ||||||
|  |             ), | ||||||
|  |             false | ||||||
|  |         )]; | ||||||
|  |          | ||||||
|  |         // Add metadata columns if provided | ||||||
|  |         if let Some(metadata) = metadata { | ||||||
|  |             for (key, values) in metadata { | ||||||
|  |                 if values.len() != num_vectors { | ||||||
|  |                     return Err(DBError(format!( | ||||||
|  |                         "Metadata field '{}' has {} values but expected {}",  | ||||||
|  |                         key, values.len(), num_vectors | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |                 let array = StringArray::from(values); | ||||||
|  |                 arrays.push(Arc::new(array)); | ||||||
|  |                 fields.push(Field::new(&key, DataType::Utf8, true)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let schema = Arc::new(Schema::new(fields)); | ||||||
|  |         let batch = RecordBatch::try_new(schema, arrays) | ||||||
|  |             .map_err(|e| DBError(format!("Failed to create RecordBatch: {}", e)))?; | ||||||
|  |          | ||||||
|  |         // Append to dataset | ||||||
|  |         let write_params = WriteParams { | ||||||
|  |             mode: WriteMode::Append, | ||||||
|  |             ..Default::default() | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         Dataset::write( | ||||||
|  |             vec![batch], | ||||||
|  |             dataset_path.to_str().unwrap(), | ||||||
|  |             Some(write_params) | ||||||
|  |         ).await | ||||||
|  |         .map_err(|e| DBError(format!("Failed to write to dataset: {}", e)))?; | ||||||
|  |          | ||||||
|  |         // Refresh cached dataset | ||||||
|  |         let mut datasets = self.datasets.write().await; | ||||||
|  |         datasets.remove(dataset_name); | ||||||
|  |          | ||||||
|  |         Ok(num_vectors) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn search_vectors( | ||||||
|  |         &self, | ||||||
|  |         dataset_name: &str, | ||||||
|  |         query_vector: Vec<f32>, | ||||||
|  |         k: usize, | ||||||
|  |         nprobes: Option<usize>, | ||||||
|  |         refine_factor: Option<usize>, | ||||||
|  |     ) -> Result<Vec<(f32, HashMap<String, String>)>, DBError> { | ||||||
|  |         let dataset = self.get_or_open_dataset(dataset_name).await?; | ||||||
|  |          | ||||||
|  |         // Build query | ||||||
|  |         let mut query = dataset.scan(); | ||||||
|  |         query = query.nearest( | ||||||
|  |             "vector", | ||||||
|  |             &query_vector, | ||||||
|  |             k, | ||||||
|  |         ).map_err(|e| DBError(format!("Failed to build search query: {}", e)))?; | ||||||
|  |          | ||||||
|  |         if let Some(nprobes) = nprobes { | ||||||
|  |             query = query.nprobes(nprobes); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if let Some(refine) = refine_factor { | ||||||
|  |             query = query.refine_factor(refine); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Execute search | ||||||
|  |         let results = query | ||||||
|  |             .try_into_stream() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| DBError(format!("Failed to execute search: {}", e)))? | ||||||
|  |             .try_collect::<Vec<_>>() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| DBError(format!("Failed to collect results: {}", e)))?; | ||||||
|  |          | ||||||
|  |         // Process results | ||||||
|  |         let mut output = Vec::new(); | ||||||
|  |         for batch in results { | ||||||
|  |             // Get distances | ||||||
|  |             let distances = batch | ||||||
|  |                 .column_by_name("_distance") | ||||||
|  |                 .ok_or_else(|| DBError("No distance column".to_string()))? | ||||||
|  |                 .as_any() | ||||||
|  |                 .downcast_ref::<Float32Array>() | ||||||
|  |                 .ok_or_else(|| DBError("Invalid distance type".to_string()))?; | ||||||
|  |              | ||||||
|  |             // Get metadata | ||||||
|  |             for i in 0..batch.num_rows() { | ||||||
|  |                 let distance = distances.value(i); | ||||||
|  |                 let mut metadata = HashMap::new(); | ||||||
|  |                  | ||||||
|  |                 for field in batch.schema().fields() { | ||||||
|  |                     if field.name() != "vector" && field.name() != "_distance" { | ||||||
|  |                         if let Some(col) = batch.column_by_name(field.name()) { | ||||||
|  |                             if let Some(str_array) = col.as_any().downcast_ref::<StringArray>() { | ||||||
|  |                                 if !str_array.is_null(i) { | ||||||
|  |                                     metadata.insert( | ||||||
|  |                                         field.name().to_string(), | ||||||
|  |                                         str_array.value(i).to_string() | ||||||
|  |                                     ); | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 output.push((distance, metadata)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(output) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn store_multimodal( | ||||||
|  |         &self, | ||||||
|  |         server: &crate::server::Server, | ||||||
|  |         dataset_name: &str, | ||||||
|  |         text: Option<String>, | ||||||
|  |         image_bytes: Option<Vec<u8>>, | ||||||
|  |         metadata: HashMap<String, String>, | ||||||
|  |     ) -> Result<String, DBError> { | ||||||
|  |         // Generate ID | ||||||
|  |         let id = uuid::Uuid::new_v4().to_string(); | ||||||
|  |          | ||||||
|  |         // Generate embeddings using external service | ||||||
|  |         let embedding = if let Some(text) = text.as_ref() { | ||||||
|  |             self.embed_text(server, vec![text.clone()]).await? | ||||||
|  |                 .into_iter() | ||||||
|  |                 .next() | ||||||
|  |                 .ok_or_else(|| DBError("No embedding returned".to_string()))? | ||||||
|  |         } else if let Some(img) = image_bytes.as_ref() { | ||||||
|  |             self.embed_image(server, img.clone()).await? | ||||||
|  |         } else { | ||||||
|  |             return Err(DBError("No text or image provided".to_string())); | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Prepare metadata | ||||||
|  |         let mut full_metadata = metadata; | ||||||
|  |         full_metadata.insert("id".to_string(), id.clone()); | ||||||
|  |         if let Some(text) = text { | ||||||
|  |             full_metadata.insert("text".to_string(), text); | ||||||
|  |         } | ||||||
|  |         if let Some(img) = image_bytes { | ||||||
|  |             full_metadata.insert("image_base64".to_string(), base64::encode(img)); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Convert metadata to column vectors | ||||||
|  |         let mut metadata_cols = HashMap::new(); | ||||||
|  |         for (key, value) in full_metadata { | ||||||
|  |             metadata_cols.insert(key, vec![value]); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Write to dataset | ||||||
|  |         self.write_vectors(dataset_name, vec![embedding], Some(metadata_cols)).await?; | ||||||
|  |          | ||||||
|  |         Ok(id) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn search_with_text( | ||||||
|  |         &self, | ||||||
|  |         server: &crate::server::Server, | ||||||
|  |         dataset_name: &str, | ||||||
|  |         query_text: String, | ||||||
|  |         k: usize, | ||||||
|  |         nprobes: Option<usize>, | ||||||
|  |         refine_factor: Option<usize>, | ||||||
|  |     ) -> Result<Vec<(f32, HashMap<String, String>)>, DBError> { | ||||||
|  |         // Embed the query text using external service | ||||||
|  |         let embeddings = self.embed_text(server, vec![query_text]).await?; | ||||||
|  |         let query_vector = embeddings.into_iter() | ||||||
|  |             .next() | ||||||
|  |             .ok_or_else(|| DBError("No embedding returned for query".to_string()))?; | ||||||
|  |          | ||||||
|  |         // Search with the embedding | ||||||
|  |         self.search_vectors(dataset_name, query_vector, k, nprobes, refine_factor).await | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn create_index( | ||||||
|  |         &self, | ||||||
|  |         dataset_name: &str, | ||||||
|  |         index_type: &str, | ||||||
|  |         num_partitions: Option<usize>, | ||||||
|  |         num_sub_vectors: Option<usize>, | ||||||
|  |     ) -> Result<(), DBError> { | ||||||
|  |         let dataset = self.get_or_open_dataset(dataset_name).await?; | ||||||
|  |          | ||||||
|  |         let mut params = VectorIndexParams::default(); | ||||||
|  |          | ||||||
|  |         match index_type.to_uppercase().as_str() { | ||||||
|  |             "IVF_PQ" => { | ||||||
|  |                 params.ivf = IvfBuildParams { | ||||||
|  |                     num_partitions: num_partitions.unwrap_or(256), | ||||||
|  |                     ..Default::default() | ||||||
|  |                 }; | ||||||
|  |                 params.pq = PQBuildParams { | ||||||
|  |                     num_sub_vectors: num_sub_vectors.unwrap_or(16), | ||||||
|  |                     ..Default::default() | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |             _ => return Err(DBError(format!("Unsupported index type: {}", index_type))), | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         dataset.create_index( | ||||||
|  |             &["vector"], | ||||||
|  |             lance::index::IndexType::Vector, | ||||||
|  |             None, | ||||||
|  |             ¶ms, | ||||||
|  |             true | ||||||
|  |         ).await | ||||||
|  |         .map_err(|e| DBError(format!("Failed to create index: {}", e)))?; | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     async fn get_or_open_dataset(&self, name: &str) -> Result<Arc<Dataset>, DBError> { | ||||||
|  |         let mut datasets = self.datasets.write().await; | ||||||
|  |          | ||||||
|  |         if let Some(dataset) = datasets.get(name) { | ||||||
|  |             return Ok(dataset.clone()); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let dataset_path = self.data_dir.join(format!("{}.lance", name)); | ||||||
|  |         if !dataset_path.exists() { | ||||||
|  |             return Err(DBError(format!("Dataset '{}' does not exist", name))); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let dataset = Dataset::open(dataset_path.to_str().unwrap()) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| DBError(format!("Failed to open dataset: {}", e)))?; | ||||||
|  |          | ||||||
|  |         let dataset = Arc::new(dataset); | ||||||
|  |         datasets.insert(name.to_string(), dataset.clone()); | ||||||
|  |          | ||||||
|  |         Ok(dataset) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn list_datasets(&self) -> Result<Vec<String>, DBError> { | ||||||
|  |         let mut datasets = Vec::new(); | ||||||
|  |          | ||||||
|  |         let entries = std::fs::read_dir(&self.data_dir) | ||||||
|  |             .map_err(|e| DBError(format!("Failed to read data directory: {}", e)))?; | ||||||
|  |          | ||||||
|  |         for entry in entries { | ||||||
|  |             let entry = entry.map_err(|e| DBError(format!("Failed to read entry: {}", e)))?; | ||||||
|  |             let path = entry.path(); | ||||||
|  |              | ||||||
|  |             if path.is_dir() { | ||||||
|  |                 if let Some(name) = path.file_name() { | ||||||
|  |                     if let Some(name_str) = name.to_str() { | ||||||
|  |                         if name_str.ends_with(".lance") { | ||||||
|  |                             let dataset_name = name_str.trim_end_matches(".lance"); | ||||||
|  |                             datasets.push(dataset_name.to_string()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(datasets) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn drop_dataset(&self, name: &str) -> Result<(), DBError> { | ||||||
|  |         // Remove from cache | ||||||
|  |         let mut datasets = self.datasets.write().await; | ||||||
|  |         datasets.remove(name); | ||||||
|  |          | ||||||
|  |         // Delete from disk | ||||||
|  |         let dataset_path = self.data_dir.join(format!("{}.lance", name)); | ||||||
|  |         if dataset_path.exists() { | ||||||
|  |             std::fs::remove_dir_all(dataset_path) | ||||||
|  |                 .map_err(|e| DBError(format!("Failed to delete dataset: {}", e)))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub async fn get_dataset_info(&self, name: &str) -> Result<HashMap<String, String>, DBError> { | ||||||
|  |         let dataset = self.get_or_open_dataset(name).await?; | ||||||
|  |          | ||||||
|  |         let mut info = HashMap::new(); | ||||||
|  |         info.insert("name".to_string(), name.to_string()); | ||||||
|  |         info.insert("version".to_string(), dataset.version().to_string()); | ||||||
|  |         info.insert("num_rows".to_string(), dataset.count_rows().await?.to_string()); | ||||||
|  |          | ||||||
|  |         // Get schema info | ||||||
|  |         let schema = dataset.schema(); | ||||||
|  |         let fields: Vec<String> = schema.fields() | ||||||
|  |             .iter() | ||||||
|  |             .map(|f| format!("{}:{}", f.name(), f.data_type())) | ||||||
|  |             .collect(); | ||||||
|  |         info.insert("schema".to_string(), fields.join(", ")); | ||||||
|  |          | ||||||
|  |         Ok(info) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 3. Update Command Implementations | ||||||
|  |  | ||||||
|  | Update the command implementations to pass the server reference for embedding service access: | ||||||
|  |  | ||||||
|  | ```rust | ||||||
|  | // In cmd.rs, update the lance command implementations | ||||||
|  |  | ||||||
|  | async fn lance_store_cmd( | ||||||
|  |     server: &Server, | ||||||
|  |     dataset: &str, | ||||||
|  |     text: Option<String>, | ||||||
|  |     image_base64: Option<String>, | ||||||
|  |     metadata: HashMap<String, String>, | ||||||
|  | ) -> Result<Protocol, DBError> { | ||||||
|  |     let lance_store = server.lance_store()?; | ||||||
|  |      | ||||||
|  |     // Decode image if provided | ||||||
|  |     let image_bytes = if let Some(b64) = image_base64 { | ||||||
|  |         Some(base64::decode(b64).map_err(|e|  | ||||||
|  |             DBError(format!("Invalid base64 image: {}", e)))?) | ||||||
|  |     } else { | ||||||
|  |         None | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // Pass server reference for embedding service access | ||||||
|  |     let id = lance_store.store_multimodal( | ||||||
|  |         server,  // Pass server to access Redis config | ||||||
|  |         dataset, | ||||||
|  |         text, | ||||||
|  |         image_bytes, | ||||||
|  |         metadata, | ||||||
|  |     ).await?; | ||||||
|  |      | ||||||
|  |     Ok(Protocol::BulkString(id)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn lance_embed_text_cmd( | ||||||
|  |     server: &Server, | ||||||
|  |     texts: &[String], | ||||||
|  | ) -> Result<Protocol, DBError> { | ||||||
|  |     let lance_store = server.lance_store()?; | ||||||
|  |      | ||||||
|  |     // Pass server reference for embedding service access | ||||||
|  |     let embeddings = lance_store.embed_text(server, texts.to_vec()).await?; | ||||||
|  |      | ||||||
|  |     // Return as array of vectors | ||||||
|  |     let mut output = Vec::new(); | ||||||
|  |     for embedding in embeddings { | ||||||
|  |         let vector_str = format!("[{}]",  | ||||||
|  |             embedding.iter() | ||||||
|  |                 .map(|f| f.to_string()) | ||||||
|  |                 .collect::<Vec<_>>() | ||||||
|  |                 .join(",") | ||||||
|  |         ); | ||||||
|  |         output.push(Protocol::BulkString(vector_str)); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     Ok(Protocol::Array(output)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn lance_search_text_cmd( | ||||||
|  |     server: &Server, | ||||||
|  |     dataset: &str, | ||||||
|  |     query_text: &str, | ||||||
|  |     k: usize, | ||||||
|  |     nprobes: Option<usize>, | ||||||
|  |     refine_factor: Option<usize>, | ||||||
|  | ) -> Result<Protocol, DBError> { | ||||||
|  |     let lance_store = server.lance_store()?; | ||||||
|  |      | ||||||
|  |     // Search using text query (will be embedded automatically) | ||||||
|  |     let results = lance_store.search_with_text( | ||||||
|  |         server, | ||||||
|  |         dataset, | ||||||
|  |         query_text.to_string(), | ||||||
|  |         k, | ||||||
|  |         nprobes, | ||||||
|  |         refine_factor, | ||||||
|  |     ).await?; | ||||||
|  |      | ||||||
|  |     // Format results | ||||||
|  |     let mut output = Vec::new(); | ||||||
|  |     for (distance, metadata) in results { | ||||||
|  |         let metadata_json = serde_json::to_string(&metadata) | ||||||
|  |             .unwrap_or_else(|_| "{}".to_string()); | ||||||
|  |          | ||||||
|  |         output.push(Protocol::Array(vec![ | ||||||
|  |             Protocol::BulkString(distance.to_string()), | ||||||
|  |             Protocol::BulkString(metadata_json), | ||||||
|  |         ])); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     Ok(Protocol::Array(output)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Add new command for text-based search | ||||||
|  | pub enum Cmd { | ||||||
|  |     // ... existing commands ... | ||||||
|  |     LanceSearchText { | ||||||
|  |         dataset: String, | ||||||
|  |         query_text: String, | ||||||
|  |         k: usize, | ||||||
|  |         nprobes: Option<usize>, | ||||||
|  |         refine_factor: Option<usize>, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Usage Examples | ||||||
|  |  | ||||||
|  | ### 1. Configure the Embedding Service | ||||||
|  |  | ||||||
|  | First, users need to configure the embedding service URL: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Configure the embedding service endpoint | ||||||
|  | redis-cli> HSET config:core:aiembed:url url "http://localhost:8000/embeddings" | ||||||
|  | OK | ||||||
|  |  | ||||||
|  | # Or use a cloud service | ||||||
|  | redis-cli> HSET config:core:aiembed:url url "https://api.openai.com/v1/embeddings" | ||||||
|  | OK | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 2. Use Lance Commands with Automatic External Embedding | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Create a dataset | ||||||
|  | redis-cli> LANCE.CREATE products DIM 1536 SCHEMA name:string price:float category:string | ||||||
|  | OK | ||||||
|  |  | ||||||
|  | # Store text with automatic embedding (calls external service) | ||||||
|  | redis-cli> LANCE.STORE products TEXT "Wireless noise-canceling headphones with 30-hour battery" name:AirPods price:299.99 category:Electronics | ||||||
|  | "uuid-123-456" | ||||||
|  |  | ||||||
|  | # Search using text query (automatically embeds the query) | ||||||
|  | redis-cli> LANCE.SEARCH.TEXT products "best headphones for travel" K 5 | ||||||
|  | 1) "0.92"  | ||||||
|  | 2) "{\"id\":\"uuid-123\",\"name\":\"AirPods\",\"price\":\"299.99\"}" | ||||||
|  |  | ||||||
|  | # Get embeddings directly | ||||||
|  | redis-cli> LANCE.EMBED.TEXT "This text will be embedded" | ||||||
|  | 1) "[0.123, 0.456, 0.789, ...]" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## External Embedding Service API Specification | ||||||
|  |  | ||||||
|  | The external embedding service should accept POST requests with this format: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  | // Request | ||||||
|  | { | ||||||
|  |   "texts": ["text1", "text2"],  // Optional | ||||||
|  |   "images": ["base64_img1"],    // Optional | ||||||
|  |   "model": "text-embedding-ada-002"  // Optional | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Response | ||||||
|  | { | ||||||
|  |   "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]], | ||||||
|  |   "model": "text-embedding-ada-002", | ||||||
|  |   "usage": { | ||||||
|  |     "prompt_tokens": 100, | ||||||
|  |     "total_tokens": 100 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Error Handling | ||||||
|  |  | ||||||
|  | The implementation includes comprehensive error handling: | ||||||
|  |  | ||||||
|  | 1. **Missing Configuration**: Clear error message if embedding URL not configured | ||||||
|  | 2. **Service Failures**: Graceful handling of embedding service errors | ||||||
|  | 3. **Timeout Protection**: 30-second timeout for embedding requests | ||||||
|  | 4. **Retry Logic**: Could be added for resilience | ||||||
|  |  | ||||||
|  | ## Benefits of This Approach | ||||||
|  |  | ||||||
|  | 1. **Flexibility**: Supports any embedding service with compatible API | ||||||
|  | 2. **Cost Control**: Use your preferred embedding provider | ||||||
|  | 3. **Scalability**: Embedding service can be scaled independently | ||||||
|  | 4. **Consistency**: All embeddings use the same configured service | ||||||
|  | 5. **Security**: API keys and endpoints stored securely in Redis | ||||||
|  |  | ||||||
|  | This implementation ensures that all embedding operations go through the external service configured in Redis, providing a clean separation between the vector database functionality and the embedding generation. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | TODO EXTRA: | ||||||
|  |  | ||||||
|  | - secret for the embedding service API key | ||||||
|  |  | ||||||
| @@ -1,5 +1,4 @@ | |||||||
| use crate::{error::DBError, protocol::Protocol, server::Server}; | use crate::{error::DBError, protocol::Protocol, server::Server}; | ||||||
| use serde::Serialize; |  | ||||||
| use tokio::time::{timeout, Duration}; | use tokio::time::{timeout, Duration}; | ||||||
| use futures::future::select_all; | use futures::future::select_all; | ||||||
| 
 | 
 | ||||||
| @@ -1093,26 +1092,23 @@ async fn dbsize_cmd(server: &Server) -> Result<Protocol, DBError> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize)] |  | ||||||
| struct ServerInfo { |  | ||||||
|     redis_version: String, |  | ||||||
|     encrypted: bool, |  | ||||||
|     selected_db: u64, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async fn info_cmd(server: &Server, section: &Option<String>) -> Result<Protocol, DBError> { | async fn info_cmd(server: &Server, section: &Option<String>) -> Result<Protocol, DBError> { | ||||||
|     let info = ServerInfo { |     let storage_info = server.current_storage()?.info()?; | ||||||
|         redis_version: "7.0.0".to_string(), |     let mut info_map: std::collections::HashMap<String, String> = storage_info.into_iter().collect(); | ||||||
|         encrypted: server.current_storage()?.is_encrypted(), | 
 | ||||||
|         selected_db: server.selected_db, |     info_map.insert("redis_version".to_string(), "7.0.0".to_string()); | ||||||
|     }; |     info_map.insert("selected_db".to_string(), server.selected_db.to_string()); | ||||||
|  |     info_map.insert("backend".to_string(), format!("{:?}", server.option.backend)); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     let mut info_string = String::new(); |     let mut info_string = String::new(); | ||||||
|     info_string.push_str(&format!("# Server\n")); |     info_string.push_str("# Server\n"); | ||||||
|     info_string.push_str(&format!("redis_version:{}\n", info.redis_version)); |     info_string.push_str(&format!("redis_version:{}\n", info_map.get("redis_version").unwrap())); | ||||||
|     info_string.push_str(&format!("encrypted:{}\n", if info.encrypted { 1 } else { 0 })); |     info_string.push_str(&format!("backend:{}\n", info_map.get("backend").unwrap())); | ||||||
|     info_string.push_str(&format!("# Keyspace\n")); |     info_string.push_str(&format!("encrypted:{}\n", info_map.get("is_encrypted").unwrap())); | ||||||
|     info_string.push_str(&format!("db{}:keys=0,expires=0,avg_ttl=0\n", info.selected_db)); |     
 | ||||||
|  |     info_string.push_str("# Keyspace\n"); | ||||||
|  |     info_string.push_str(&format!("db{}:keys={},expires=0,avg_ttl=0\n", info_map.get("selected_db").unwrap(), info_map.get("db_size").unwrap())); | ||||||
| 
 | 
 | ||||||
|     match section { |     match section { | ||||||
|         Some(s) => { |         Some(s) => { | ||||||
| @@ -23,6 +23,7 @@ impl From<CryptoError> for crate::error::DBError { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
 | /// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
 | ||||||
|  | #[derive(Clone)] | ||||||
| pub struct CryptoFactory { | pub struct CryptoFactory { | ||||||
|     key: chacha20poly1305::Key, |     key: chacha20poly1305::Key, | ||||||
| } | } | ||||||
| @@ -6,3 +6,5 @@ pub mod options; | |||||||
| pub mod protocol; | pub mod protocol; | ||||||
| pub mod server; | pub mod server; | ||||||
| pub mod storage; | pub mod storage; | ||||||
|  | pub mod storage_trait;  // Add this
 | ||||||
|  | pub mod storage_sled;   // Add this
 | ||||||
| @@ -30,6 +30,10 @@ struct Args { | |||||||
|     /// Encrypt the database
 |     /// Encrypt the database
 | ||||||
|     #[arg(long)] |     #[arg(long)] | ||||||
|     encrypt: bool, |     encrypt: bool, | ||||||
|  | 
 | ||||||
|  |     /// Use the sled backend
 | ||||||
|  |     #[arg(long)] | ||||||
|  |     sled: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| @@ -51,6 +55,11 @@ async fn main() { | |||||||
|         debug: args.debug, |         debug: args.debug, | ||||||
|         encryption_key: args.encryption_key, |         encryption_key: args.encryption_key, | ||||||
|         encrypt: args.encrypt, |         encrypt: args.encrypt, | ||||||
|  |         backend: if args.sled { | ||||||
|  |             herodb::options::BackendType::Sled | ||||||
|  |         } else { | ||||||
|  |             herodb::options::BackendType::Redb | ||||||
|  |         }, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // new server
 |     // new server
 | ||||||
							
								
								
									
										15
									
								
								src/options.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/options.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum BackendType { | ||||||
|  |     Redb, | ||||||
|  |     Sled, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub struct DBOption { | ||||||
|  |     pub dir: String, | ||||||
|  |     pub port: u16, | ||||||
|  |     pub debug: bool, | ||||||
|  |     pub encrypt: bool, | ||||||
|  |     pub encryption_key: Option<String>, | ||||||
|  |     pub backend: BackendType, | ||||||
|  | } | ||||||
| @@ -12,10 +12,12 @@ use crate::error::DBError; | |||||||
| use crate::options; | use crate::options; | ||||||
| use crate::protocol::Protocol; | use crate::protocol::Protocol; | ||||||
| use crate::storage::Storage; | use crate::storage::Storage; | ||||||
|  | use crate::storage_sled::SledStorage; | ||||||
|  | use crate::storage_trait::StorageBackend; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct Server { | pub struct Server { | ||||||
|     pub db_cache: std::sync::Arc<std::sync::RwLock<HashMap<u64, Arc<Storage>>>>, |     pub db_cache: std::sync::Arc<std::sync::RwLock<HashMap<u64, Arc<dyn StorageBackend>>>>, | ||||||
|     pub option: options::DBOption, |     pub option: options::DBOption, | ||||||
|     pub client_name: Option<String>, |     pub client_name: Option<String>, | ||||||
|     pub selected_db: u64, // Changed from usize to u64
 |     pub selected_db: u64, // Changed from usize to u64
 | ||||||
| @@ -52,7 +54,7 @@ impl Server { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn current_storage(&self) -> Result<Arc<Storage>, DBError> { |     pub fn current_storage(&self) -> Result<Arc<dyn StorageBackend>, DBError> { | ||||||
|         let mut cache = self.db_cache.write().unwrap(); |         let mut cache = self.db_cache.write().unwrap(); | ||||||
|         
 |         
 | ||||||
|         if let Some(storage) = cache.get(&self.selected_db) { |         if let Some(storage) = cache.get(&self.selected_db) { | ||||||
| @@ -73,11 +75,22 @@ impl Server { | |||||||
|         
 |         
 | ||||||
|         println!("Creating new db file: {}", db_file_path.display()); |         println!("Creating new db file: {}", db_file_path.display()); | ||||||
|         
 |         
 | ||||||
|         let storage = Arc::new(Storage::new( |         let storage: Arc<dyn StorageBackend> = match self.option.backend { | ||||||
|             db_file_path, |             options::BackendType::Redb => { | ||||||
|             self.should_encrypt_db(self.selected_db), |                 Arc::new(Storage::new( | ||||||
|             self.option.encryption_key.as_deref() |                     db_file_path, | ||||||
|         )?); |                     self.should_encrypt_db(self.selected_db), | ||||||
|  |                     self.option.encryption_key.as_deref() | ||||||
|  |                 )?) | ||||||
|  |             } | ||||||
|  |             options::BackendType::Sled => { | ||||||
|  |                 Arc::new(SledStorage::new( | ||||||
|  |                     db_file_path, | ||||||
|  |                     self.should_encrypt_db(self.selected_db), | ||||||
|  |                     self.option.encryption_key.as_deref() | ||||||
|  |                 )?) | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|         
 |         
 | ||||||
|         cache.insert(self.selected_db, storage.clone()); |         cache.insert(self.selected_db, storage.clone()); | ||||||
|         Ok(storage) |         Ok(storage) | ||||||
							
								
								
									
										287
									
								
								src/storage/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/storage/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | |||||||
|  | use std::{ | ||||||
|  |     path::Path, | ||||||
|  |     sync::Arc, | ||||||
|  |     time::{SystemTime, UNIX_EPOCH}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use redb::{Database, TableDefinition}; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | use crate::crypto::CryptoFactory; | ||||||
|  | use crate::error::DBError; | ||||||
|  |  | ||||||
|  | // Re-export modules | ||||||
|  | mod storage_basic; | ||||||
|  | mod storage_hset; | ||||||
|  | mod storage_lists; | ||||||
|  | mod storage_extra; | ||||||
|  |  | ||||||
|  | // Re-export implementations | ||||||
|  | // Note: These imports are used by the impl blocks in the submodules | ||||||
|  | // The compiler shows them as unused because they're not directly used in this file | ||||||
|  | // but they're needed for the Storage struct methods to be available | ||||||
|  | pub use storage_extra::*; | ||||||
|  |  | ||||||
|  | // Table definitions for different Redis data types | ||||||
|  | const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types"); | ||||||
|  | const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings"); | ||||||
|  | const HASHES_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("hashes"); | ||||||
|  | const LISTS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("lists"); | ||||||
|  | const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta"); | ||||||
|  | const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data"); | ||||||
|  | const ENCRYPTED_TABLE: TableDefinition<&str, u8> = TableDefinition::new("encrypted"); | ||||||
|  | const EXPIRATION_TABLE: TableDefinition<&str, u64> = TableDefinition::new("expiration"); | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | pub struct StreamEntry { | ||||||
|  |     pub fields: Vec<(String, String)>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | pub struct ListValue { | ||||||
|  |     pub elements: Vec<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[inline] | ||||||
|  | pub fn now_in_millis() -> u128 { | ||||||
|  |     let start = SystemTime::now(); | ||||||
|  |     let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap(); | ||||||
|  |     duration_since_epoch.as_millis() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct Storage { | ||||||
|  |     db: Database, | ||||||
|  |     crypto: Option<CryptoFactory>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Storage { | ||||||
|  |     pub fn new(path: impl AsRef<Path>, should_encrypt: bool, master_key: Option<&str>) -> Result<Self, DBError> { | ||||||
|  |         let db = Database::create(path)?; | ||||||
|  |          | ||||||
|  |         // Create tables if they don't exist | ||||||
|  |         let write_txn = db.begin_write()?; | ||||||
|  |         { | ||||||
|  |             let _ = write_txn.open_table(TYPES_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(STRINGS_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(HASHES_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(LISTS_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(STREAMS_META_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(STREAMS_DATA_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(ENCRYPTED_TABLE)?; | ||||||
|  |             let _ = write_txn.open_table(EXPIRATION_TABLE)?; | ||||||
|  |         } | ||||||
|  |         write_txn.commit()?; | ||||||
|  |          | ||||||
|  |         // Check if database was previously encrypted | ||||||
|  |         let read_txn = db.begin_read()?; | ||||||
|  |         let encrypted_table = read_txn.open_table(ENCRYPTED_TABLE)?; | ||||||
|  |         let was_encrypted = encrypted_table.get("encrypted")?.map(|v| v.value() == 1).unwrap_or(false); | ||||||
|  |         drop(read_txn); | ||||||
|  |          | ||||||
|  |         let crypto = if should_encrypt || was_encrypted { | ||||||
|  |             if let Some(key) = master_key { | ||||||
|  |                 Some(CryptoFactory::new(key.as_bytes())) | ||||||
|  |             } else { | ||||||
|  |                 return Err(DBError("Encryption requested but no master key provided".to_string())); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // If we're enabling encryption for the first time, mark it | ||||||
|  |         if should_encrypt && !was_encrypted { | ||||||
|  |             let write_txn = db.begin_write()?; | ||||||
|  |             { | ||||||
|  |                 let mut encrypted_table = write_txn.open_table(ENCRYPTED_TABLE)?; | ||||||
|  |                 encrypted_table.insert("encrypted", &1u8)?; | ||||||
|  |             } | ||||||
|  |             write_txn.commit()?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(Storage { | ||||||
|  |             db, | ||||||
|  |             crypto, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     pub fn is_encrypted(&self) -> bool { | ||||||
|  |         self.crypto.is_some() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Helper methods for encryption | ||||||
|  |     fn encrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { | ||||||
|  |         if let Some(crypto) = &self.crypto { | ||||||
|  |             Ok(crypto.encrypt(data)) | ||||||
|  |         } else { | ||||||
|  |             Ok(data.to_vec()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn decrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { | ||||||
|  |         if let Some(crypto) = &self.crypto { | ||||||
|  |             Ok(crypto.decrypt(data)?) | ||||||
|  |         } else { | ||||||
|  |             Ok(data.to_vec()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | use crate::storage_trait::StorageBackend; | ||||||
|  |  | ||||||
|  | impl StorageBackend for Storage { | ||||||
|  |     fn get(&self, key: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         self.get(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn set(&self, key: String, value: String) -> Result<(), DBError> { | ||||||
|  |         self.set(key, value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn setx(&self, key: String, value: String, expire_ms: u128) -> Result<(), DBError> { | ||||||
|  |         self.setx(key, value, expire_ms) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn del(&self, key: String) -> Result<(), DBError> { | ||||||
|  |         self.del(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn exists(&self, key: &str) -> Result<bool, DBError> { | ||||||
|  |         self.exists(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn keys(&self, pattern: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.keys(pattern) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn dbsize(&self) -> Result<i64, DBError> { | ||||||
|  |         self.dbsize() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn flushdb(&self) -> Result<(), DBError> { | ||||||
|  |         self.flushdb() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_key_type(&self, key: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         self.get_key_type(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn scan(&self, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> { | ||||||
|  |         self.scan(cursor, pattern, count) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hscan(&self, key: &str, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> { | ||||||
|  |         self.hscan(key, cursor, pattern, count) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hset(&self, key: &str, pairs: Vec<(String, String)>) -> Result<i64, DBError> { | ||||||
|  |         self.hset(key, pairs) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hget(&self, key: &str, field: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         self.hget(key, field) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> { | ||||||
|  |         self.hgetall(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hdel(&self, key: &str, fields: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         self.hdel(key, fields) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hexists(&self, key: &str, field: &str) -> Result<bool, DBError> { | ||||||
|  |         self.hexists(key, field) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hkeys(&self, key: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.hkeys(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hvals(&self, key: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.hvals(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hlen(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         self.hlen(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hmget(&self, key: &str, fields: Vec<String>) -> Result<Vec<Option<String>>, DBError> { | ||||||
|  |         self.hmget(key, fields) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn hsetnx(&self, key: &str, field: &str, value: &str) -> Result<bool, DBError> { | ||||||
|  |         self.hsetnx(key, field, value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn lpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         self.lpush(key, elements) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn rpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         self.rpush(key, elements) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn lpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.lpop(key, count) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn rpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.rpop(key, count) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn llen(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         self.llen(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn lindex(&self, key: &str, index: i64) -> Result<Option<String>, DBError> { | ||||||
|  |         self.lindex(key, index) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn lrange(&self, key: &str, start: i64, stop: i64) -> Result<Vec<String>, DBError> { | ||||||
|  |         self.lrange(key, start, stop) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ltrim(&self, key: &str, start: i64, stop: i64) -> Result<(), DBError> { | ||||||
|  |         self.ltrim(key, start, stop) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn lrem(&self, key: &str, count: i64, element: &str) -> Result<i64, DBError> { | ||||||
|  |         self.lrem(key, count, element) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ttl(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         self.ttl(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn expire_seconds(&self, key: &str, secs: u64) -> Result<bool, DBError> { | ||||||
|  |         self.expire_seconds(key, secs) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn pexpire_millis(&self, key: &str, ms: u128) -> Result<bool, DBError> { | ||||||
|  |         self.pexpire_millis(key, ms) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn persist(&self, key: &str) -> Result<bool, DBError> { | ||||||
|  |         self.persist(key) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn expire_at_seconds(&self, key: &str, ts_secs: i64) -> Result<bool, DBError> { | ||||||
|  |         self.expire_at_seconds(key, ts_secs) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn pexpire_at_millis(&self, key: &str, ts_ms: i64) -> Result<bool, DBError> { | ||||||
|  |         self.pexpire_at_millis(key, ts_ms) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn is_encrypted(&self) -> bool { | ||||||
|  |         self.is_encrypted() | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn info(&self) -> Result<Vec<(String, String)>, DBError> { | ||||||
|  |         self.info() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn clone_arc(&self) -> Arc<dyn StorageBackend> { | ||||||
|  |         unimplemented!("Storage cloning not yet implemented for redb backend") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -208,6 +208,14 @@ impl Storage { | |||||||
|         write_txn.commit()?; |         write_txn.commit()?; | ||||||
|         Ok(applied) |         Ok(applied) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub fn info(&self) -> Result<Vec<(String, String)>, DBError> { | ||||||
|  |         let dbsize = self.dbsize()?; | ||||||
|  |         Ok(vec![ | ||||||
|  |             ("db_size".to_string(), dbsize.to_string()), | ||||||
|  |             ("is_encrypted".to_string(), self.is_encrypted().to_string()), | ||||||
|  |         ]) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Utility function for glob pattern matching
 | // Utility function for glob pattern matching
 | ||||||
							
								
								
									
										845
									
								
								src/storage_sled/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										845
									
								
								src/storage_sled/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,845 @@ | |||||||
|  | // src/storage_sled/mod.rs | ||||||
|  | use std::path::Path; | ||||||
|  | use std::sync::Arc; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::time::{SystemTime, UNIX_EPOCH}; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use crate::error::DBError; | ||||||
|  | use crate::storage_trait::StorageBackend; | ||||||
|  | use crate::crypto::CryptoFactory; | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | enum ValueType { | ||||||
|  |     String(String), | ||||||
|  |     Hash(HashMap<String, String>), | ||||||
|  |     List(Vec<String>), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | struct StorageValue { | ||||||
|  |     value: ValueType, | ||||||
|  |     expires_at: Option<u128>, // milliseconds since epoch | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct SledStorage { | ||||||
|  |     db: sled::Db, | ||||||
|  |     types: sled::Tree, | ||||||
|  |     crypto: Option<CryptoFactory>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl SledStorage { | ||||||
|  |     pub fn new(path: impl AsRef<Path>, should_encrypt: bool, master_key: Option<&str>) -> Result<Self, DBError> { | ||||||
|  |         let db = sled::open(path).map_err(|e| DBError(format!("Failed to open sled: {}", e)))?; | ||||||
|  |         let types = db.open_tree("types").map_err(|e| DBError(format!("Failed to open types tree: {}", e)))?; | ||||||
|  |          | ||||||
|  |         // Check if database was previously encrypted | ||||||
|  |         let encrypted_tree = db.open_tree("encrypted").map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         let was_encrypted = encrypted_tree.get("encrypted") | ||||||
|  |             .map_err(|e| DBError(e.to_string()))? | ||||||
|  |             .map(|v| v[0] == 1) | ||||||
|  |             .unwrap_or(false); | ||||||
|  |          | ||||||
|  |         let crypto = if should_encrypt || was_encrypted { | ||||||
|  |             if let Some(key) = master_key { | ||||||
|  |                 Some(CryptoFactory::new(key.as_bytes())) | ||||||
|  |             } else { | ||||||
|  |                 return Err(DBError("Encryption requested but no master key provided".to_string())); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Mark database as encrypted if enabling encryption | ||||||
|  |         if should_encrypt && !was_encrypted { | ||||||
|  |             encrypted_tree.insert("encrypted", &[1u8]) | ||||||
|  |                 .map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             encrypted_tree.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(SledStorage { db, types, crypto }) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn now_millis() -> u128 { | ||||||
|  |         SystemTime::now() | ||||||
|  |             .duration_since(UNIX_EPOCH) | ||||||
|  |             .unwrap() | ||||||
|  |             .as_millis() | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn encrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { | ||||||
|  |         if let Some(crypto) = &self.crypto { | ||||||
|  |             Ok(crypto.encrypt(data)) | ||||||
|  |         } else { | ||||||
|  |             Ok(data.to_vec()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn decrypt_if_needed(&self, data: &[u8]) -> Result<Vec<u8>, DBError> { | ||||||
|  |         if let Some(crypto) = &self.crypto { | ||||||
|  |             Ok(crypto.decrypt(data)?) | ||||||
|  |         } else { | ||||||
|  |             Ok(data.to_vec()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn get_storage_value(&self, key: &str) -> Result<Option<StorageValue>, DBError> { | ||||||
|  |         match self.db.get(key).map_err(|e| DBError(e.to_string()))? { | ||||||
|  |             Some(encrypted_data) => { | ||||||
|  |                 let decrypted = self.decrypt_if_needed(&encrypted_data)?; | ||||||
|  |                 let storage_val: StorageValue = bincode::deserialize(&decrypted) | ||||||
|  |                     .map_err(|e| DBError(format!("Deserialization error: {}", e)))?; | ||||||
|  |                  | ||||||
|  |                 // Check expiration | ||||||
|  |                 if let Some(expires_at) = storage_val.expires_at { | ||||||
|  |                     if Self::now_millis() > expires_at { | ||||||
|  |                         // Expired, remove it | ||||||
|  |                         self.db.remove(key).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |                         self.types.remove(key).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |                         return Ok(None); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 Ok(Some(storage_val)) | ||||||
|  |             } | ||||||
|  |             None => Ok(None) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn set_storage_value(&self, key: &str, storage_val: StorageValue) -> Result<(), DBError> { | ||||||
|  |         let data = bincode::serialize(&storage_val) | ||||||
|  |             .map_err(|e| DBError(format!("Serialization error: {}", e)))?; | ||||||
|  |         let encrypted = self.encrypt_if_needed(&data)?; | ||||||
|  |         self.db.insert(key, encrypted).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |          | ||||||
|  |         // Store type info (unencrypted for efficiency) | ||||||
|  |         let type_str = match &storage_val.value { | ||||||
|  |             ValueType::String(_) => "string", | ||||||
|  |             ValueType::Hash(_) => "hash", | ||||||
|  |             ValueType::List(_) => "list", | ||||||
|  |         }; | ||||||
|  |         self.types.insert(key, type_str.as_bytes()).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn glob_match(pattern: &str, text: &str) -> bool { | ||||||
|  |         if pattern == "*" { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let pattern_chars: Vec<char> = pattern.chars().collect(); | ||||||
|  |         let text_chars: Vec<char> = text.chars().collect(); | ||||||
|  |          | ||||||
|  |         fn match_recursive(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool { | ||||||
|  |             if pi >= pattern.len() { | ||||||
|  |                 return ti >= text.len(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if ti >= text.len() { | ||||||
|  |                 return pattern[pi..].iter().all(|&c| c == '*'); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             match pattern[pi] { | ||||||
|  |                 '*' => { | ||||||
|  |                     for i in ti..=text.len() { | ||||||
|  |                         if match_recursive(pattern, text, pi + 1, i) { | ||||||
|  |                             return true; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |                 '?' => match_recursive(pattern, text, pi + 1, ti + 1), | ||||||
|  |                 c => { | ||||||
|  |                     if text[ti] == c { | ||||||
|  |                         match_recursive(pattern, text, pi + 1, ti + 1) | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         match_recursive(&pattern_chars, &text_chars, 0, 0) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl StorageBackend for SledStorage { | ||||||
|  |     fn get(&self, key: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::String(s) => Ok(Some(s)), | ||||||
|  |                 _ => Ok(None) | ||||||
|  |             } | ||||||
|  |             None => Ok(None) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn set(&self, key: String, value: String) -> Result<(), DBError> { | ||||||
|  |         let storage_val = StorageValue { | ||||||
|  |             value: ValueType::String(value), | ||||||
|  |             expires_at: None, | ||||||
|  |         }; | ||||||
|  |         self.set_storage_value(&key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn setx(&self, key: String, value: String, expire_ms: u128) -> Result<(), DBError> { | ||||||
|  |         let storage_val = StorageValue { | ||||||
|  |             value: ValueType::String(value), | ||||||
|  |             expires_at: Some(Self::now_millis() + expire_ms), | ||||||
|  |         }; | ||||||
|  |         self.set_storage_value(&key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn del(&self, key: String) -> Result<(), DBError> { | ||||||
|  |         self.db.remove(&key).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         self.types.remove(&key).map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn exists(&self, key: &str) -> Result<bool, DBError> { | ||||||
|  |         // Check with expiration | ||||||
|  |         Ok(self.get_storage_value(key)?.is_some()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn keys(&self, pattern: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         let mut keys = Vec::new(); | ||||||
|  |         for item in self.types.iter() { | ||||||
|  |             let (key_bytes, _) = item.map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             let key = String::from_utf8_lossy(&key_bytes).to_string(); | ||||||
|  |              | ||||||
|  |             // Check if key is expired | ||||||
|  |             if self.get_storage_value(&key)?.is_some() { | ||||||
|  |                 if Self::glob_match(pattern, &key) { | ||||||
|  |                     keys.push(key); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(keys) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn scan(&self, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> { | ||||||
|  |         let mut result = Vec::new(); | ||||||
|  |         let mut current_cursor = 0u64; | ||||||
|  |         let limit = count.unwrap_or(10) as usize; | ||||||
|  |          | ||||||
|  |         for item in self.types.iter() { | ||||||
|  |             if current_cursor >= cursor { | ||||||
|  |                 let (key_bytes, type_bytes) = item.map_err(|e| DBError(e.to_string()))?; | ||||||
|  |                 let key = String::from_utf8_lossy(&key_bytes).to_string(); | ||||||
|  |                  | ||||||
|  |                 // Check pattern match | ||||||
|  |                 let matches = if let Some(pat) = pattern { | ||||||
|  |                     Self::glob_match(pat, &key) | ||||||
|  |                 } else { | ||||||
|  |                     true | ||||||
|  |                 }; | ||||||
|  |                  | ||||||
|  |                 if matches { | ||||||
|  |                     // Check if key is expired and get value | ||||||
|  |                     if let Some(storage_val) = self.get_storage_value(&key)? { | ||||||
|  |                         let value = match storage_val.value { | ||||||
|  |                             ValueType::String(s) => s, | ||||||
|  |                             _ => String::from_utf8_lossy(&type_bytes).to_string(), | ||||||
|  |                         }; | ||||||
|  |                         result.push((key, value)); | ||||||
|  |                          | ||||||
|  |                         if result.len() >= limit { | ||||||
|  |                             current_cursor += 1; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             current_cursor += 1; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let next_cursor = if result.len() < limit { 0 } else { current_cursor }; | ||||||
|  |         Ok((next_cursor, result)) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn dbsize(&self) -> Result<i64, DBError> { | ||||||
|  |         let mut count = 0i64; | ||||||
|  |         for item in self.types.iter() { | ||||||
|  |             let (key_bytes, _) = item.map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             let key = String::from_utf8_lossy(&key_bytes).to_string(); | ||||||
|  |             if self.get_storage_value(&key)?.is_some() { | ||||||
|  |                 count += 1; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(count) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn flushdb(&self) -> Result<(), DBError> { | ||||||
|  |         self.db.clear().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         self.types.clear().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn get_key_type(&self, key: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         // First check if key exists (handles expiration) | ||||||
|  |         if self.get_storage_value(key)?.is_some() { | ||||||
|  |             match self.types.get(key).map_err(|e| DBError(e.to_string()))? { | ||||||
|  |                 Some(data) => Ok(Some(String::from_utf8_lossy(&data).to_string())), | ||||||
|  |                 None => Ok(None) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Ok(None) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Hash operations | ||||||
|  |     fn hset(&self, key: &str, pairs: Vec<(String, String)>) -> Result<i64, DBError> { | ||||||
|  |         let mut storage_val = self.get_storage_value(key)?.unwrap_or(StorageValue { | ||||||
|  |             value: ValueType::Hash(HashMap::new()), | ||||||
|  |             expires_at: None, | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         let hash = match &mut storage_val.value { | ||||||
|  |             ValueType::Hash(h) => h, | ||||||
|  |             _ => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())), | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut new_fields = 0i64; | ||||||
|  |         for (field, value) in pairs { | ||||||
|  |             if !hash.contains_key(&field) { | ||||||
|  |                 new_fields += 1; | ||||||
|  |             } | ||||||
|  |             hash.insert(field, value); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(new_fields) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hget(&self, key: &str, field: &str) -> Result<Option<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.get(field).cloned()), | ||||||
|  |                 _ => Ok(None) | ||||||
|  |             } | ||||||
|  |             None => Ok(None) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.into_iter().collect()), | ||||||
|  |                 _ => Ok(Vec::new()) | ||||||
|  |             } | ||||||
|  |             None => Ok(Vec::new()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hscan(&self, key: &str, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => { | ||||||
|  |                     let mut result = Vec::new(); | ||||||
|  |                     let mut current_cursor = 0u64; | ||||||
|  |                     let limit = count.unwrap_or(10) as usize; | ||||||
|  |                      | ||||||
|  |                     for (field, value) in h.iter() { | ||||||
|  |                         if current_cursor >= cursor { | ||||||
|  |                             let matches = if let Some(pat) = pattern { | ||||||
|  |                                 Self::glob_match(pat, field) | ||||||
|  |                             } else { | ||||||
|  |                                 true | ||||||
|  |                             }; | ||||||
|  |                              | ||||||
|  |                             if matches { | ||||||
|  |                                 result.push((field.clone(), value.clone())); | ||||||
|  |                                 if result.len() >= limit { | ||||||
|  |                                     current_cursor += 1; | ||||||
|  |                                     break; | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         current_cursor += 1; | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     let next_cursor = if result.len() < limit { 0 } else { current_cursor }; | ||||||
|  |                     Ok((next_cursor, result)) | ||||||
|  |                 } | ||||||
|  |                 _ => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())) | ||||||
|  |             } | ||||||
|  |             None => Ok((0, Vec::new())) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hdel(&self, key: &str, fields: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(0) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let hash = match &mut storage_val.value { | ||||||
|  |             ValueType::Hash(h) => h, | ||||||
|  |             _ => return Ok(0) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut deleted = 0i64; | ||||||
|  |         for field in fields { | ||||||
|  |             if hash.remove(&field).is_some() { | ||||||
|  |                 deleted += 1; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if hash.is_empty() { | ||||||
|  |             self.del(key.to_string())?; | ||||||
|  |         } else { | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(deleted) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hexists(&self, key: &str, field: &str) -> Result<bool, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.contains_key(field)), | ||||||
|  |                 _ => Ok(false) | ||||||
|  |             } | ||||||
|  |             None => Ok(false) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hkeys(&self, key: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.keys().cloned().collect()), | ||||||
|  |                 _ => Ok(Vec::new()) | ||||||
|  |             } | ||||||
|  |             None => Ok(Vec::new()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hvals(&self, key: &str) -> Result<Vec<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.values().cloned().collect()), | ||||||
|  |                 _ => Ok(Vec::new()) | ||||||
|  |             } | ||||||
|  |             None => Ok(Vec::new()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hlen(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => Ok(h.len() as i64), | ||||||
|  |                 _ => Ok(0) | ||||||
|  |             } | ||||||
|  |             None => Ok(0) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hmget(&self, key: &str, fields: Vec<String>) -> Result<Vec<Option<String>>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::Hash(h) => { | ||||||
|  |                     Ok(fields.into_iter().map(|f| h.get(&f).cloned()).collect()) | ||||||
|  |                 } | ||||||
|  |                 _ => Ok(fields.into_iter().map(|_| None).collect()) | ||||||
|  |             } | ||||||
|  |             None => Ok(fields.into_iter().map(|_| None).collect()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn hsetnx(&self, key: &str, field: &str, value: &str) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = self.get_storage_value(key)?.unwrap_or(StorageValue { | ||||||
|  |             value: ValueType::Hash(HashMap::new()), | ||||||
|  |             expires_at: None, | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         let hash = match &mut storage_val.value { | ||||||
|  |             ValueType::Hash(h) => h, | ||||||
|  |             _ => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())), | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         if hash.contains_key(field) { | ||||||
|  |             Ok(false) | ||||||
|  |         } else { | ||||||
|  |             hash.insert(field.to_string(), value.to_string()); | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             Ok(true) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // List operations | ||||||
|  |     fn lpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         let mut storage_val = self.get_storage_value(key)?.unwrap_or(StorageValue { | ||||||
|  |             value: ValueType::List(Vec::new()), | ||||||
|  |             expires_at: None, | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())), | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         for element in elements.into_iter().rev() { | ||||||
|  |             list.insert(0, element); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let len = list.len() as i64; | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(len) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn rpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError> { | ||||||
|  |         let mut storage_val = self.get_storage_value(key)?.unwrap_or(StorageValue { | ||||||
|  |             value: ValueType::List(Vec::new()), | ||||||
|  |             expires_at: None, | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())), | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         list.extend(elements); | ||||||
|  |         let len = list.len() as i64; | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(len) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn lpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(Vec::new()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Ok(Vec::new()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut result = Vec::new(); | ||||||
|  |         for _ in 0..count.min(list.len() as u64) { | ||||||
|  |             if let Some(elem) = list.first() { | ||||||
|  |                 result.push(elem.clone()); | ||||||
|  |                 list.remove(0); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if list.is_empty() { | ||||||
|  |             self.del(key.to_string())?; | ||||||
|  |         } else { | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn rpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(Vec::new()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Ok(Vec::new()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut result = Vec::new(); | ||||||
|  |         for _ in 0..count.min(list.len() as u64) { | ||||||
|  |             if let Some(elem) = list.pop() { | ||||||
|  |                 result.push(elem); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if list.is_empty() { | ||||||
|  |             self.del(key.to_string())?; | ||||||
|  |         } else { | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn llen(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::List(l) => Ok(l.len() as i64), | ||||||
|  |                 _ => Ok(0) | ||||||
|  |             } | ||||||
|  |             None => Ok(0) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn lindex(&self, key: &str, index: i64) -> Result<Option<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::List(list) => { | ||||||
|  |                     let actual_index = if index < 0 { | ||||||
|  |                         list.len() as i64 + index | ||||||
|  |                     } else { | ||||||
|  |                         index | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     if actual_index >= 0 && (actual_index as usize) < list.len() { | ||||||
|  |                         Ok(Some(list[actual_index as usize].clone())) | ||||||
|  |                     } else { | ||||||
|  |                         Ok(None) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 _ => Ok(None) | ||||||
|  |             } | ||||||
|  |             None => Ok(None) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn lrange(&self, key: &str, start: i64, stop: i64) -> Result<Vec<String>, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => match storage_val.value { | ||||||
|  |                 ValueType::List(list) => { | ||||||
|  |                     if list.is_empty() { | ||||||
|  |                         return Ok(Vec::new()); | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     let len = list.len() as i64; | ||||||
|  |                     let start_idx = if start < 0 {  | ||||||
|  |                         std::cmp::max(0, len + start)  | ||||||
|  |                     } else {  | ||||||
|  |                         std::cmp::min(start, len)  | ||||||
|  |                     }; | ||||||
|  |                     let stop_idx = if stop < 0 {  | ||||||
|  |                         std::cmp::max(-1, len + stop)  | ||||||
|  |                     } else {  | ||||||
|  |                         std::cmp::min(stop, len - 1)  | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     if start_idx > stop_idx || start_idx >= len { | ||||||
|  |                         return Ok(Vec::new()); | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     let start_usize = start_idx as usize; | ||||||
|  |                     let stop_usize = (stop_idx + 1) as usize; | ||||||
|  |                      | ||||||
|  |                     Ok(list[start_usize..std::cmp::min(stop_usize, list.len())].to_vec()) | ||||||
|  |                 } | ||||||
|  |                 _ => Ok(Vec::new()) | ||||||
|  |             } | ||||||
|  |             None => Ok(Vec::new()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn ltrim(&self, key: &str, start: i64, stop: i64) -> Result<(), DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Ok(()) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         if list.is_empty() { | ||||||
|  |             return Ok(()); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         let len = list.len() as i64; | ||||||
|  |         let start_idx = if start < 0 {  | ||||||
|  |             std::cmp::max(0, len + start)  | ||||||
|  |         } else {  | ||||||
|  |             std::cmp::min(start, len)  | ||||||
|  |         }; | ||||||
|  |         let stop_idx = if stop < 0 {  | ||||||
|  |             std::cmp::max(-1, len + stop)  | ||||||
|  |         } else {  | ||||||
|  |             std::cmp::min(stop, len - 1)  | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         if start_idx > stop_idx || start_idx >= len { | ||||||
|  |             self.del(key.to_string())?; | ||||||
|  |         } else { | ||||||
|  |             let start_usize = start_idx as usize; | ||||||
|  |             let stop_usize = (stop_idx + 1) as usize; | ||||||
|  |             *list = list[start_usize..std::cmp::min(stop_usize, list.len())].to_vec(); | ||||||
|  |              | ||||||
|  |             if list.is_empty() { | ||||||
|  |                 self.del(key.to_string())?; | ||||||
|  |             } else { | ||||||
|  |                 self.set_storage_value(key, storage_val)?; | ||||||
|  |                 self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn lrem(&self, key: &str, count: i64, element: &str) -> Result<i64, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(0) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let list = match &mut storage_val.value { | ||||||
|  |             ValueType::List(l) => l, | ||||||
|  |             _ => return Ok(0) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let mut removed = 0i64; | ||||||
|  |          | ||||||
|  |         if count == 0 { | ||||||
|  |             // Remove all occurrences | ||||||
|  |             let original_len = list.len(); | ||||||
|  |             list.retain(|x| x != element); | ||||||
|  |             removed = (original_len - list.len()) as i64; | ||||||
|  |         } else if count > 0 { | ||||||
|  |             // Remove first count occurrences | ||||||
|  |             let mut to_remove = count as usize; | ||||||
|  |             list.retain(|x| { | ||||||
|  |                 if x == element && to_remove > 0 { | ||||||
|  |                     to_remove -= 1; | ||||||
|  |                     removed += 1; | ||||||
|  |                     false | ||||||
|  |                 } else { | ||||||
|  |                     true | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } else { | ||||||
|  |             // Remove last |count| occurrences | ||||||
|  |             let mut to_remove = (-count) as usize; | ||||||
|  |             for i in (0..list.len()).rev() { | ||||||
|  |                 if list[i] == element && to_remove > 0 { | ||||||
|  |                     list.remove(i); | ||||||
|  |                     to_remove -= 1; | ||||||
|  |                     removed += 1; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if list.is_empty() { | ||||||
|  |             self.del(key.to_string())?; | ||||||
|  |         } else { | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(removed) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Expiration | ||||||
|  |     fn ttl(&self, key: &str) -> Result<i64, DBError> { | ||||||
|  |         match self.get_storage_value(key)? { | ||||||
|  |             Some(storage_val) => { | ||||||
|  |                 if let Some(expires_at) = storage_val.expires_at { | ||||||
|  |                     let now = Self::now_millis(); | ||||||
|  |                     if now >= expires_at { | ||||||
|  |                         Ok(-2) // Key has expired | ||||||
|  |                     } else { | ||||||
|  |                         Ok(((expires_at - now) / 1000) as i64) // TTL in seconds | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Ok(-1) // Key exists but has no expiration | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             None => Ok(-2) // Key does not exist | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn expire_seconds(&self, key: &str, secs: u64) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(false) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         storage_val.expires_at = Some(Self::now_millis() + (secs as u128) * 1000); | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn pexpire_millis(&self, key: &str, ms: u128) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(false) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         storage_val.expires_at = Some(Self::now_millis() + ms); | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn persist(&self, key: &str) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(false) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         if storage_val.expires_at.is_some() { | ||||||
|  |             storage_val.expires_at = None; | ||||||
|  |             self.set_storage_value(key, storage_val)?; | ||||||
|  |             self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |             Ok(true) | ||||||
|  |         } else { | ||||||
|  |             Ok(false) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn expire_at_seconds(&self, key: &str, ts_secs: i64) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(false) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let expires_at_ms: u128 = if ts_secs <= 0 { 0 } else { (ts_secs as u128) * 1000 }; | ||||||
|  |         storage_val.expires_at = Some(expires_at_ms); | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn pexpire_at_millis(&self, key: &str, ts_ms: i64) -> Result<bool, DBError> { | ||||||
|  |         let mut storage_val = match self.get_storage_value(key)? { | ||||||
|  |             Some(sv) => sv, | ||||||
|  |             None => return Ok(false) | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let expires_at_ms: u128 = if ts_ms <= 0 { 0 } else { ts_ms as u128 }; | ||||||
|  |         storage_val.expires_at = Some(expires_at_ms); | ||||||
|  |         self.set_storage_value(key, storage_val)?; | ||||||
|  |         self.db.flush().map_err(|e| DBError(e.to_string()))?; | ||||||
|  |         Ok(true) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn is_encrypted(&self) -> bool { | ||||||
|  |         self.crypto.is_some() | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     fn info(&self) -> Result<Vec<(String, String)>, DBError> { | ||||||
|  |         let dbsize = self.dbsize()?; | ||||||
|  |         Ok(vec![ | ||||||
|  |             ("db_size".to_string(), dbsize.to_string()), | ||||||
|  |             ("is_encrypted".to_string(), self.is_encrypted().to_string()), | ||||||
|  |         ]) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn clone_arc(&self) -> Arc<dyn StorageBackend> { | ||||||
|  |         // Note: This is a simplified clone - in production you might want to | ||||||
|  |         // handle this differently as sled::Db is already Arc internally | ||||||
|  |         Arc::new(SledStorage { | ||||||
|  |             db: self.db.clone(), | ||||||
|  |             types: self.types.clone(), | ||||||
|  |             crypto: self.crypto.clone(), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								src/storage_trait.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/storage_trait.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | // src/storage_trait.rs | ||||||
|  | use crate::error::DBError; | ||||||
|  | use std::sync::Arc; | ||||||
|  |  | ||||||
|  | pub trait StorageBackend: Send + Sync { | ||||||
|  |     // Basic key operations | ||||||
|  |     fn get(&self, key: &str) -> Result<Option<String>, DBError>; | ||||||
|  |     fn set(&self, key: String, value: String) -> Result<(), DBError>; | ||||||
|  |     fn setx(&self, key: String, value: String, expire_ms: u128) -> Result<(), DBError>; | ||||||
|  |     fn del(&self, key: String) -> Result<(), DBError>; | ||||||
|  |     fn exists(&self, key: &str) -> Result<bool, DBError>; | ||||||
|  |     fn keys(&self, pattern: &str) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn dbsize(&self) -> Result<i64, DBError>; | ||||||
|  |     fn flushdb(&self) -> Result<(), DBError>; | ||||||
|  |     fn get_key_type(&self, key: &str) -> Result<Option<String>, DBError>; | ||||||
|  |      | ||||||
|  |     // Scanning | ||||||
|  |     fn scan(&self, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError>; | ||||||
|  |     fn hscan(&self, key: &str, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<(String, String)>), DBError>; | ||||||
|  |      | ||||||
|  |     // Hash operations | ||||||
|  |     fn hset(&self, key: &str, pairs: Vec<(String, String)>) -> Result<i64, DBError>; | ||||||
|  |     fn hget(&self, key: &str, field: &str) -> Result<Option<String>, DBError>; | ||||||
|  |     fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError>; | ||||||
|  |     fn hdel(&self, key: &str, fields: Vec<String>) -> Result<i64, DBError>; | ||||||
|  |     fn hexists(&self, key: &str, field: &str) -> Result<bool, DBError>; | ||||||
|  |     fn hkeys(&self, key: &str) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn hvals(&self, key: &str) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn hlen(&self, key: &str) -> Result<i64, DBError>; | ||||||
|  |     fn hmget(&self, key: &str, fields: Vec<String>) -> Result<Vec<Option<String>>, DBError>; | ||||||
|  |     fn hsetnx(&self, key: &str, field: &str, value: &str) -> Result<bool, DBError>; | ||||||
|  |      | ||||||
|  |     // List operations | ||||||
|  |     fn lpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError>; | ||||||
|  |     fn rpush(&self, key: &str, elements: Vec<String>) -> Result<i64, DBError>; | ||||||
|  |     fn lpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn rpop(&self, key: &str, count: u64) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn llen(&self, key: &str) -> Result<i64, DBError>; | ||||||
|  |     fn lindex(&self, key: &str, index: i64) -> Result<Option<String>, DBError>; | ||||||
|  |     fn lrange(&self, key: &str, start: i64, stop: i64) -> Result<Vec<String>, DBError>; | ||||||
|  |     fn ltrim(&self, key: &str, start: i64, stop: i64) -> Result<(), DBError>; | ||||||
|  |     fn lrem(&self, key: &str, count: i64, element: &str) -> Result<i64, DBError>; | ||||||
|  |      | ||||||
|  |     // Expiration | ||||||
|  |     fn ttl(&self, key: &str) -> Result<i64, DBError>; | ||||||
|  |     fn expire_seconds(&self, key: &str, secs: u64) -> Result<bool, DBError>; | ||||||
|  |     fn pexpire_millis(&self, key: &str, ms: u128) -> Result<bool, DBError>; | ||||||
|  |     fn persist(&self, key: &str) -> Result<bool, DBError>; | ||||||
|  |     fn expire_at_seconds(&self, key: &str, ts_secs: i64) -> Result<bool, DBError>; | ||||||
|  |     fn pexpire_at_millis(&self, key: &str, ts_ms: i64) -> Result<bool, DBError>; | ||||||
|  |      | ||||||
|  |     // Metadata | ||||||
|  |     fn is_encrypted(&self) -> bool; | ||||||
|  |     fn info(&self) -> Result<Vec<(String, String)>, DBError>; | ||||||
|  |      | ||||||
|  |     // Clone to Arc for sharing | ||||||
|  |     fn clone_arc(&self) -> Arc<dyn StorageBackend>; | ||||||
|  | } | ||||||
| @@ -27,6 +27,7 @@ async fn debug_hset_simple() { | |||||||
|         debug: false, |         debug: false, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
|     
 |     
 | ||||||
|     let mut server = Server::new(option).await; |     let mut server = Server::new(option).await; | ||||||
| @@ -18,6 +18,7 @@ async fn debug_hset_return_value() { | |||||||
|         debug: false, |         debug: false, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
|     
 |     
 | ||||||
|     let mut server = Server::new(option).await; |     let mut server = Server::new(option).await; | ||||||
| @@ -22,6 +22,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | |||||||
|         debug: true, |         debug: true, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
|     
 |     
 | ||||||
|     let server = Server::new(option).await; |     let server = Server::new(option).await; | ||||||
| @@ -24,6 +24,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | |||||||
|         debug: true, |         debug: true, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
|     
 |     
 | ||||||
|     let server = Server::new(option).await; |     let server = Server::new(option).await; | ||||||
| @@ -22,6 +22,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | |||||||
|         debug: false, |         debug: false, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
|     
 |     
 | ||||||
|     let server = Server::new(option).await; |     let server = Server::new(option).await; | ||||||
| @@ -22,6 +22,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | |||||||
|         debug: false, |         debug: false, | ||||||
|         encrypt: false, |         encrypt: false, | ||||||
|         encryption_key: None, |         encryption_key: None, | ||||||
|  |         backend: herodb::options::BackendType::Redb, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let server = Server::new(option).await; |     let server = Server::new(option).await; | ||||||
| @@ -500,11 +501,11 @@ async fn test_07_age_stateless_suite() { | |||||||
|     let mut s = connect(port).await; |     let mut s = connect(port).await; | ||||||
| 
 | 
 | ||||||
|     // GENENC -> [recipient, identity]
 |     // GENENC -> [recipient, identity]
 | ||||||
|     let gen_result = send_cmd(&mut s, &["AGE", "GENENC"]).await; |     let genenc = send_cmd(&mut s, &["AGE", "GENENC"]).await; | ||||||
|     assert!( |     assert!( | ||||||
|         gen_result.starts_with("*2\r\n$"), |         genenc.starts_with("*2\r\n$"), | ||||||
|         "AGE GENENC should return array [recipient, identity], got:\n{}", |         "AGE GENENC should return array [recipient, identity], got:\n{}", | ||||||
|         gen_result |         genenc | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Parse simple RESP array of two bulk strings to extract keys
 |     // Parse simple RESP array of two bulk strings to extract keys
 | ||||||
| @@ -519,7 +520,7 @@ async fn test_07_age_stateless_suite() { | |||||||
|         let ident = lines.next().unwrap_or("").to_string(); |         let ident = lines.next().unwrap_or("").to_string(); | ||||||
|         (recip, ident) |         (recip, ident) | ||||||
|     } |     } | ||||||
|     let (recipient, identity) = parse_two_bulk_array(&gen_result); |     let (recipient, identity) = parse_two_bulk_array(&genenc); | ||||||
|     assert!( |     assert!( | ||||||
|         recipient.starts_with("age1") && identity.starts_with("AGE-SECRET-KEY-1"), |         recipient.starts_with("age1") && identity.starts_with("AGE-SECRET-KEY-1"), | ||||||
|         "Unexpected AGE key formats.\nrecipient: {}\nidentity: {}", |         "Unexpected AGE key formats.\nrecipient: {}\nidentity: {}", | ||||||
		Reference in New Issue
	
	Block a user