10 Commits

25 changed files with 2225 additions and 65 deletions

123
Cargo.lock generated
View File

@@ -206,6 +206,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.2"
@@ -358,6 +364,30 @@ dependencies = [
"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]]
name = "crypto-common"
version = "0.1.6"
@@ -406,7 +436,7 @@ dependencies = [
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.11",
]
[[package]]
@@ -533,6 +563,16 @@ dependencies = [
"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]]
name = "futures"
version = "0.3.31"
@@ -622,6 +662,15 @@ dependencies = [
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -682,6 +731,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"sled",
"thiserror",
"tokio",
]
@@ -732,7 +782,7 @@ dependencies = [
"intl-memoizer",
"lazy_static",
"log",
"parking_lot",
"parking_lot 0.12.4",
"rust-embed",
"thiserror",
"unic-langid",
@@ -889,6 +939,15 @@ dependencies = [
"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]]
name = "intl-memoizer"
version = "0.5.3"
@@ -914,7 +973,7 @@ version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags",
"bitflags 2.9.2",
"cfg-if",
"libc",
]
@@ -1040,6 +1099,17 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "parking_lot"
version = "0.12.4"
@@ -1047,7 +1117,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"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]]
@@ -1058,7 +1142,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.17",
"smallvec",
"windows-targets 0.52.6",
]
@@ -1252,13 +1336,22 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags",
"bitflags 2.9.2",
]
[[package]]
@@ -1466,6 +1559,22 @@ version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "smallvec"
version = "1.15.1"
@@ -1599,7 +1708,7 @@ dependencies = [
"io-uring",
"libc",
"mio",
"parking_lot",
"parking_lot 0.12.4",
"pin-project-lite",
"signal-hook-registry",
"slab",

View File

@@ -12,10 +12,11 @@ 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.3"
bincode = "1.3"
chacha20poly1305 = "0.10.1"
rand = "0.8"
sha2 = "0.10"

View File

@@ -0,0 +1,85 @@
# HeroDB
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:
```bash
cargo build --release
```
### 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)

View File

@@ -16,33 +16,40 @@ Note: Database-at-rest encryption flags in the test harness are unrelated to AGE
## Quick start
Assuming the server is running on localhost on some PORT:
Assuming the server is running on localhost on some $PORT:
```bash
~/code/git.ourworld.tf/herocode/herodb/herodb/build.sh
~/code/git.ourworld.tf/herocode/herodb/target/release/herodb --dir /tmp/data --debug --$PORT 6381 --encryption-key 1234 --encrypt
```
```bash
export PORT=6381
# Generate an ephemeral keypair and encrypt/decrypt a message (stateless mode)
redis-cli -p PORT AGE GENENC
redis-cli -p $PORT AGE GENENC
# → returns an array: [recipient, identity]
redis-cli -p PORT AGE ENCRYPT <recipient> "hello world"
redis-cli -p $PORT AGE ENCRYPT <recipient> "hello world"
# → returns ciphertext (base64 in a bulk string)
redis-cli -p PORT AGE DECRYPT <identity> <ciphertext_b64>
redis-cli -p $PORT AGE DECRYPT <identity> <ciphertext_b64>
# → returns "hello world"
```
For keymanaged mode, generate a named key once and reference it by name afterwards:
```bash
redis-cli -p PORT AGE KEYGEN app1
redis-cli -p $PORT AGE KEYGEN app1
# → persists encryption keypair under name "app1"
redis-cli -p PORT AGE ENCRYPTNAME app1 "hello"
redis-cli -p PORT AGE DECRYPTNAME app1 <ciphertext_b64>
redis-cli -p $PORT AGE ENCRYPTNAME app1 "hello"
redis-cli -p $PORT AGE DECRYPTNAME app1 <ciphertext_b64>
```
## Stateless AGE (ephemeral)
Characteristics
- No serverside storage of keys.
- You pass the actual key material with every call.
- Not listable via AGE LIST.
@@ -52,36 +59,40 @@ Commands and examples
1) Ephemeral encryption keys
```bash
# Generate an ephemeral encryption keypair
redis-cli -p PORT AGE GENENC
# Generate an ephemeral encryption keypair
redis-cli -p $PORT AGE GENENC
# Example output (abridged):
# 1) "age1qz..." # recipient (public)
# 2) "AGE-SECRET-KEY-1..." # identity (secret)
# 1) "age1qz..." # recipient (public key) = can be used by others e.g. to verify what I sign
# 2) "AGE-SECRET-KEY-1..." # identity (secret) = is like my private, cannot lose this one
# Encrypt with the recipient
redis-cli -p PORT AGE ENCRYPT "age1qz..." "hello world"
# → returns bulk string payload: base64 ciphertext
# Encrypt with the recipient public key
redis-cli -p $PORT AGE ENCRYPT "age1qz..." "hello world"
# Decrypt with the identity (secret)
redis-cli -p PORT AGE DECRYPT "AGE-SECRET-KEY-1..." "<ciphertext_b64>"
# → returns bulk string payload: base64 ciphertext (encrypted content)
# Decrypt with the identity (secret) in other words your private key
redis-cli -p $PORT AGE DECRYPT "AGE-SECRET-KEY-1..." "<ciphertext_b64>"
# → "hello world"
```
2) Ephemeral signing keys
> ? is this same as my private key
```bash
# Generate an ephemeral signing keypair
redis-cli -p PORT AGE GENSIGN
redis-cli -p $PORT AGE GENSIGN
# Example output:
# 1) "<verify_pub_b64>"
# 2) "<sign_secret_b64>"
# Sign a message with the secret
redis-cli -p PORT AGE SIGN "<sign_secret_b64>" "msg"
redis-cli -p $PORT AGE SIGN "<sign_secret_b64>" "msg"
# → returns "<signature_b64>"
# Verify with the public key
redis-cli -p PORT AGE VERIFY "<verify_pub_b64>" "msg" "<signature_b64>"
redis-cli -p $PORT AGE VERIFY "<verify_pub_b64>" "msg" "<signature_b64>"
# → 1 (valid) or 0 (invalid)
```
@@ -105,15 +116,17 @@ Commands and examples
```bash
# Create/persist a named encryption keypair
redis-cli -p PORT AGE KEYGEN app1
redis-cli -p $PORT AGE KEYGEN app1
# → returns [recipient, identity] but also stores them under name "app1"
> TODO: should not return identity (security, but there can be separate function to export it e.g. AGE EXPORTKEY app1)
# Encrypt using the stored public key
redis-cli -p PORT AGE ENCRYPTNAME app1 "hello"
redis-cli -p $PORT AGE ENCRYPTNAME app1 "hello"
# → returns bulk string payload: base64 ciphertext
# Decrypt using the stored secret
redis-cli -p PORT AGE DECRYPTNAME app1 "<ciphertext_b64>"
redis-cli -p $PORT AGE DECRYPTNAME app1 "<ciphertext_b64>"
# → "hello"
```
@@ -121,22 +134,24 @@ redis-cli -p PORT AGE DECRYPTNAME app1 "<ciphertext_b64>"
```bash
# Create/persist a named signing keypair
redis-cli -p PORT AGE SIGNKEYGEN app1
redis-cli -p $PORT AGE SIGNKEYGEN app1
# → returns [verify_pub_b64, sign_secret_b64] and stores under name "app1"
> TODO: should not return sign_secret_b64 (for security, but there can be separate function to export it e.g. AGE EXPORTSIGNKEY app1)
# Sign using the stored secret
redis-cli -p PORT AGE SIGNNAME app1 "msg"
redis-cli -p $PORT AGE SIGNNAME app1 "msg"
# → returns "<signature_b64>"
# Verify using the stored public key
redis-cli -p PORT AGE VERIFYNAME app1 "msg" "<signature_b64>"
redis-cli -p $PORT AGE VERIFYNAME app1 "msg" "<signature_b64>"
# → 1 (valid) or 0 (invalid)
```
3) List stored AGE keys
```bash
redis-cli -p PORT AGE LIST
redis-cli -p $PORT AGE LIST
# Example output includes labels such as "encpub" and your key names (e.g., "app1")
```

623
herodb/docs/basics.md Normal file
View File

@@ -0,0 +1,623 @@
Here's an expanded version of the cmds.md documentation to include the list commands:
# HeroDB Commands
HeroDB implements a subset of Redis commands over the Redis protocol. This document describes the available commands and their usage.
## String Commands
### PING
Ping the server to test connectivity.
```bash
redis-cli -p $PORT PING
# → PONG
```
### ECHO
Echo the given message.
```bash
redis-cli -p $PORT ECHO "hello"
# → hello
```
### SET
Set a key to hold a string value.
```bash
redis-cli -p $PORT SET key value
# → OK
```
Options:
- EX seconds: Set expiration in seconds
- PX milliseconds: Set expiration in milliseconds
- NX: Only set if key doesn't exist
- XX: Only set if key exists
- GET: Return old value
Examples:
```bash
redis-cli -p $PORT SET key value EX 60
redis-cli -p $PORT SET key value PX 1000
redis-cli -p $PORT SET key value NX
redis-cli -p $PORT SET key value XX
redis-cli -p $PORT SET key value GET
```
### GET
Get the value of a key.
```bash
redis-cli -p $PORT GET key
# → value
```
### MGET
Get values of multiple keys.
```bash
redis-cli -p $PORT MGET key1 key2 key3
# → 1) "value1"
# 2) "value2"
# 3) (nil)
```
### MSET
Set multiple key-value pairs.
```bash
redis-cli -p $PORT MSET key1 value1 key2 value2
# → OK
```
### INCR
Increment the integer value of a key by 1.
```bash
redis-cli -p $PORT SET counter 10
redis-cli -p $PORT INCR counter
# → 11
```
### DEL
Delete a key.
```bash
redis-cli -p $PORT DEL key
# → 1
```
For multiple keys:
```bash
redis-cli -p $PORT DEL key1 key2 key3
# → number of keys deleted
```
### TYPE
Determine the type of a key.
```bash
redis-cli -p $PORT TYPE key
# → string
```
### EXISTS
Check if a key exists.
```bash
redis-cli -p $PORT EXISTS key
# → 1 (exists) or 0 (doesn't exist)
```
For multiple keys:
```bash
redis-cli -p $PORT EXISTS key1 key2 key3
# → count of existing keys
```
### EXPIRE / PEXPIRE
Set expiration time for a key.
```bash
redis-cli -p $PORT EXPIRE key 60
# → 1 (timeout set) or 0 (timeout not set)
redis-cli -p $PORT PEXPIRE key 1000
# → 1 (timeout set) or 0 (timeout not set)
```
### EXPIREAT / PEXPIREAT
Set expiration timestamp for a key.
```bash
redis-cli -p $PORT EXPIREAT key 1672531200
# → 1 (timeout set) or 0 (timeout not set)
redis-cli -p $PORT PEXPIREAT key 1672531200000
# → 1 (timeout set) or 0 (timeout not set)
```
### TTL
Get the time to live for a key.
```bash
redis-cli -p $PORT TTL key
# → remaining time in seconds
```
### PERSIST
Remove expiration from a key.
```bash
redis-cli -p $PORT PERSIST key
# → 1 (timeout removed) or 0 (key has no timeout)
```
## Hash Commands
### HSET
Set field-value pairs in a hash.
```bash
redis-cli -p $PORT HSET hashkey field1 value1 field2 value2
# → number of fields added
```
### HGET
Get value of a field in a hash.
```bash
redis-cli -p $PORT HGET hashkey field1
# → value1
```
### HGETALL
Get all field-value pairs in a hash.
```bash
redis-cli -p $PORT HGETALL hashkey
# → 1) "field1"
# 2) "value1"
# 3) "field2"
# 4) "value2"
```
### HDEL
Delete fields from a hash.
```bash
redis-cli -p $PORT HDEL hashkey field1 field2
# → number of fields deleted
```
### HEXISTS
Check if a field exists in a hash.
```bash
redis-cli -p $PORT HEXISTS hashkey field1
# → 1 (exists) or 0 (doesn't exist)
```
### HKEYS
Get all field names in a hash.
```bash
redis-cli -p $PORT HKEYS hashkey
# → 1) "field1"
# 2) "field2"
```
### HVALS
Get all values in a hash.
```bash
redis-cli -p $PORT HVALS hashkey
# → 1) "value1"
# 2) "value2"
```
### HLEN
Get number of fields in a hash.
```bash
redis-cli -p $PORT HLEN hashkey
# → number of fields
```
### HMGET
Get values of multiple fields in a hash.
```bash
redis-cli -p $PORT HMGET hashkey field1 field2 field3
# → 1) "value1"
# 2) "value2"
# 3) (nil)
```
### HSETNX
Set field-value pair in hash only if field doesn't exist.
```bash
redis-cli -p $PORT HSETNX hashkey field1 value1
# → 1 (field set) or 0 (field not set)
```
### HINCRBY
Increment integer value of a field in a hash.
```bash
redis-cli -p $PORT HINCRBY hashkey field1 5
# → new value
```
### HINCRBYFLOAT
Increment float value of a field in a hash.
```bash
redis-cli -p $PORT HINCRBYFLOAT hashkey field1 3.14
# → new value
```
### HSCAN
Incrementally iterate over fields in a hash.
```bash
redis-cli -p $PORT HSCAN hashkey 0
# → 1) "next_cursor"
# 2) 1) "field1"
# 2) "value1"
# 3) "field2"
# 4) "value2"
```
Options:
- MATCH pattern: Filter fields by pattern
- COUNT number: Suggest number of fields to return
Examples:
```bash
redis-cli -p $PORT HSCAN hashkey 0 MATCH f*
redis-cli -p $PORT HSCAN hashkey 0 COUNT 10
redis-cli -p $PORT HSCAN hashkey 0 MATCH f* COUNT 10
```
## List Commands
### LPUSH
Insert elements at the head of a list.
```bash
redis-cli -p $PORT LPUSH listkey element1 element2 element3
# → number of elements in the list
```
### RPUSH
Insert elements at the tail of a list.
```bash
redis-cli -p $PORT RPUSH listkey element1 element2 element3
# → number of elements in the list
```
### LPOP
Remove and return elements from the head of a list.
```bash
redis-cli -p $PORT LPOP listkey
# → element1
```
With count:
```bash
redis-cli -p $PORT LPOP listkey 2
# → 1) "element1"
# 2) "element2"
```
### RPOP
Remove and return elements from the tail of a list.
```bash
redis-cli -p $PORT RPOP listkey
# → element3
```
With count:
```bash
redis-cli -p $PORT RPOP listkey 2
# → 1) "element3"
# 2) "element2"
```
### LLEN
Get the length of a list.
```bash
redis-cli -p $PORT LLEN listkey
# → number of elements in the list
```
### LINDEX
Get element at index in a list.
```bash
redis-cli -p $PORT LINDEX listkey 0
# → first element
```
Negative indices count from the end:
```bash
redis-cli -p $PORT LINDEX listkey -1
# → last element
```
### LRANGE
Get a range of elements from a list.
```bash
redis-cli -p $PORT LRANGE listkey 0 -1
# → 1) "element1"
# 2) "element2"
# 3) "element3"
```
### LTRIM
Trim a list to specified range.
```bash
redis-cli -p $PORT LTRIM listkey 0 1
# → OK (list now contains only first 2 elements)
```
### LREM
Remove elements from a list.
```bash
redis-cli -p $PORT LREM listkey 2 element1
# → number of elements removed
```
Count values:
- Positive: Remove from head
- Negative: Remove from tail
- Zero: Remove all
### LINSERT
Insert element before or after pivot element.
```bash
redis-cli -p $PORT LINSERT listkey BEFORE pivot newelement
# → number of elements in the list
```
### BLPOP
Blocking remove and return elements from the head of a list.
```bash
redis-cli -p $PORT BLPOP listkey1 listkey2 5
# → 1) "listkey1"
# 2) "element1"
```
If no elements are available, blocks for specified timeout (in seconds) until an element is pushed to one of the lists.
### BRPOP
Blocking remove and return elements from the tail of a list.
```bash
redis-cli -p $PORT BRPOP listkey1 listkey2 5
# → 1) "listkey1"
# 2) "element1"
```
If no elements are available, blocks for specified timeout (in seconds) until an element is pushed to one of the lists.
## Keyspace Commands
### KEYS
Get all keys matching pattern.
```bash
redis-cli -p $PORT KEYS *
# → 1) "key1"
# 2) "key2"
```
### SCAN
Incrementally iterate over keys.
```bash
redis-cli -p $PORT SCAN 0
# → 1) "next_cursor"
# 2) 1) "key1"
# 2) "key2"
```
Options:
- MATCH pattern: Filter keys by pattern
- COUNT number: Suggest number of keys to return
Examples:
```bash
redis-cli -p $PORT SCAN 0 MATCH k*
redis-cli -p $PORT SCAN 0 COUNT 10
redis-cli -p $PORT SCAN 0 MATCH k* COUNT 10
```
### DBSIZE
Get number of keys in current database.
```bash
redis-cli -p $PORT DBSIZE
# → number of keys
```
### FLUSHDB
Remove all keys from current database.
```bash
redis-cli -p $PORT FLUSHDB
# → OK
```
## Configuration Commands
### CONFIG GET
Get configuration parameter.
```bash
redis-cli -p $PORT CONFIG GET dir
# → 1) "dir"
# 2) "/path/to/db"
redis-cli -p $PORT CONFIG GET dbfilename
# → 1) "dbfilename"
# 2) "0.db"
```
## Client Commands
### CLIENT SETNAME
Set current connection name.
```bash
redis-cli -p $PORT CLIENT SETNAME myconnection
# → OK
```
### CLIENT GETNAME
Get current connection name.
```bash
redis-cli -p $PORT CLIENT GETNAME
# → myconnection
```
## Transaction Commands
### MULTI
Start a transaction block.
```bash
redis-cli -p $PORT MULTI
# → OK
```
### EXEC
Execute all commands in transaction block.
```bash
redis-cli -p $PORT MULTI
redis-cli -p $PORT SET key1 value1
redis-cli -p $PORT SET key2 value2
redis-cli -p $PORT EXEC
# → 1) OK
# 2) OK
```
### DISCARD
Discard all commands in transaction block.
```bash
redis-cli -p $PORT MULTI
redis-cli -p $PORT SET key1 value1
redis-cli -p $PORT DISCARD
# → OK
```
## AGE Commands
### AGE GENENC
Generate ephemeral encryption keypair.
```bash
redis-cli -p $PORT AGE GENENC
# → 1) "recipient_public_key"
# 2) "identity_secret_key"
```
### AGE ENCRYPT
Encrypt message with recipient public key.
```bash
redis-cli -p $PORT AGE ENCRYPT recipient_public_key "message"
# → base64_encoded_ciphertext
```
### AGE DECRYPT
Decrypt ciphertext with identity secret key.
```bash
redis-cli -p $PORT AGE DECRYPT identity_secret_key base64_encoded_ciphertext
# → decrypted_message
```
### AGE GENSIGN
Generate ephemeral signing keypair.
```bash
redis-cli -p $PORT AGE GENSIGN
# → 1) "verify_public_key"
# 2) "sign_secret_key"
```
### AGE SIGN
Sign message with signing secret key.
```bash
redis-cli -p $PORT AGE SIGN sign_secret_key "message"
# → base64_encoded_signature
```
### AGE VERIFY
Verify signature with verify public key.
```bash
redis-cli -p $PORT AGE VERIFY verify_public_key "message" base64_encoded_signature
# → 1 (valid) or 0 (invalid)
```
### AGE KEYGEN
Generate and persist named encryption keypair.
```bash
redis-cli -p $PORT AGE KEYGEN keyname
# → 1) "recipient_public_key"
# 2) "identity_secret_key"
```
### AGE SIGNKEYGEN
Generate and persist named signing keypair.
```bash
redis-cli -p $PORT AGE SIGNKEYGEN keyname
# → 1) "verify_public_key"
# 2) "sign_secret_key"
```
### AGE ENCRYPTNAME
Encrypt message with named key.
```bash
redis-cli -p $PORT AGE ENCRYPTNAME keyname "message"
# → base64_encoded_ciphertext
```
### AGE DECRYPTNAME
Decrypt ciphertext with named key.
```bash
redis-cli -p $PORT AGE DECRYPTNAME keyname base64_encoded_ciphertext
# → decrypted_message
```
### AGE SIGNNAME
Sign message with named signing key.
```bash
redis-cli -p $PORT AGE SIGNNAME keyname "message"
# → base64_encoded_signature
```
### AGE VERIFYNAME
Verify signature with named verify key.
```bash
redis-cli -p $PORT AGE VERIFYNAME keyname "message" base64_encoded_signature
# → 1 (valid) or 0 (invalid)
```
### AGE LIST
List all stored AGE keys.
```bash
redis-cli -p $PORT AGE LIST
# → 1) "keyname1"
# 2) "keyname2"
```
## Server Information Commands
### INFO
Get server information.
```bash
redis-cli -p $PORT INFO
# → Server information
```
With section:
```bash
redis-cli -p $PORT INFO replication
# → Replication information
```
### COMMAND
Get command information (stub implementation).
```bash
redis-cli -p $PORT COMMAND
# → Empty array (stub)
```
## Database Selection
### SELECT
Select database by index.
```bash
redis-cli -p $PORT SELECT 0
# → OK
```
```
This expanded documentation includes all the list commands that were implemented in the cmd.rs file:
1. LPUSH - push elements to the left (head) of a list
2. RPUSH - push elements to the right (tail) of a list
3. LPOP - pop elements from the left (head) of a list
4. RPOP - pop elements from the right (tail) of a list
5. BLPOP - blocking pop from the left with timeout
6. BRPOP - blocking pop from the right with timeout
7. LLEN - get list length
8. LREM - remove elements from list
9. LTRIM - trim list to range
10. LINDEX - get element by index
11. LRANGE - get range of elements

116
herodb/docs/cmds.md Normal file
View File

@@ -0,0 +1,116 @@
## 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 |
### 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
```

View 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
```

View File

View File

@@ -1,5 +1,4 @@
use crate::{error::DBError, protocol::Protocol, server::Server};
use serde::Serialize;
use tokio::time::{timeout, Duration};
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> {
let info = ServerInfo {
redis_version: "7.0.0".to_string(),
encrypted: server.current_storage()?.is_encrypted(),
selected_db: server.selected_db,
};
let storage_info = server.current_storage()?.info()?;
let mut info_map: std::collections::HashMap<String, String> = storage_info.into_iter().collect();
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();
info_string.push_str(&format!("# Server\n"));
info_string.push_str(&format!("redis_version:{}\n", info.redis_version));
info_string.push_str(&format!("encrypted:{}\n", if info.encrypted { 1 } else { 0 }));
info_string.push_str(&format!("# Keyspace\n"));
info_string.push_str(&format!("db{}:keys=0,expires=0,avg_ttl=0\n", info.selected_db));
info_string.push_str("# Server\n");
info_string.push_str(&format!("redis_version:{}\n", info_map.get("redis_version").unwrap()));
info_string.push_str(&format!("backend:{}\n", info_map.get("backend").unwrap()));
info_string.push_str(&format!("encrypted:{}\n", info_map.get("is_encrypted").unwrap()));
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 {
Some(s) => {

View File

@@ -23,6 +23,7 @@ impl From<CryptoError> for crate::error::DBError {
}
/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
#[derive(Clone)]
pub struct CryptoFactory {
key: chacha20poly1305::Key,
}

View File

@@ -6,3 +6,5 @@ pub mod options;
pub mod protocol;
pub mod server;
pub mod storage;
pub mod storage_trait; // Add this
pub mod storage_sled; // Add this

View File

@@ -30,6 +30,10 @@ struct Args {
/// Encrypt the database
#[arg(long)]
encrypt: bool,
/// Use the sled backend
#[arg(long)]
sled: bool,
}
#[tokio::main]
@@ -47,10 +51,14 @@ async fn main() {
// new DB option
let option = herodb::options::DBOption {
dir: args.dir,
port,
debug: args.debug,
encryption_key: args.encryption_key,
encrypt: args.encrypt,
backend: if args.sled {
herodb::options::BackendType::Sled
} else {
herodb::options::BackendType::Redb
},
};
// new server

View File

@@ -1,8 +1,14 @@
#[derive(Clone)]
#[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>, // Master encryption key
pub encryption_key: Option<String>,
pub backend: BackendType,
}

View File

@@ -12,10 +12,12 @@ use crate::error::DBError;
use crate::options;
use crate::protocol::Protocol;
use crate::storage::Storage;
use crate::storage_sled::SledStorage;
use crate::storage_trait::StorageBackend;
#[derive(Clone)]
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 client_name: Option<String>,
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();
if let Some(storage) = cache.get(&self.selected_db) {
@@ -73,11 +75,22 @@ impl Server {
println!("Creating new db file: {}", db_file_path.display());
let storage = Arc::new(Storage::new(
db_file_path,
self.should_encrypt_db(self.selected_db),
self.option.encryption_key.as_deref()
)?);
let storage: Arc<dyn StorageBackend> = match self.option.backend {
options::BackendType::Redb => {
Arc::new(Storage::new(
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());
Ok(storage)

View File

@@ -1,5 +1,6 @@
use std::{
path::Path,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
@@ -123,4 +124,164 @@ impl Storage {
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")
}
}

View File

@@ -208,6 +208,14 @@ impl Storage {
write_txn.commit()?;
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

View 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(),
})
}
}

View 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>;
}

View File

@@ -298,7 +298,7 @@ main() {
# Start the server
print_status "Starting HeroDB server..."
./target/release/herodb --dir "$DB_DIR" --port $PORT &
../target/release/herodb --dir "$DB_DIR" --port $PORT &
SERVER_PID=$!
# Wait for server to start
@@ -352,4 +352,4 @@ check_dependencies() {
# Run dependency check and main function
check_dependencies
main "$@"
main "$@"