...
This commit is contained in:
@@ -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"
|
||||
|
@@ -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)
|
@@ -1,227 +1,116 @@
|
||||
|
||||
# HeroDB Redis Protocol Support: Commands & Client Usage
|
||||
## Backend Support
|
||||
|
||||
HeroDB is a Redis-compatible database built using the `redb` database backend.
|
||||
HeroDB supports two storage backends, both with full encryption support:
|
||||
|
||||
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.
|
||||
- **redb** (default): Full-featured, optimized for production use
|
||||
- **sled**: Alternative embedded database with encryption support
|
||||
|
||||
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`:
|
||||
### Starting HeroDB with Different Backends
|
||||
|
||||
```bash
|
||||
# Build HeroDB
|
||||
cargo build --release
|
||||
# Use default redb backend
|
||||
./target/release/herodb --dir /tmp/herodb_redb --port 6379
|
||||
|
||||
# Start HeroDB server
|
||||
./target/release/herodb --dir /tmp/herodb_data --port 6381 --debug
|
||||
# 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
|
||||
```
|
||||
|
||||
## Using Standard Redis Clients
|
||||
### Command Support by Backend
|
||||
|
||||
### With `redis-cli`
|
||||
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
|
||||
redis-cli -p 6381 SET mykey "hello"
|
||||
redis-cli -p 6381 GET mykey
|
||||
# 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
|
||||
```
|
||||
|
||||
### With Python (`redis-py`)
|
||||
### Migration Between Backends
|
||||
|
||||
```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
|
||||
To migrate data between backends, use Redis replication or dump/restore:
|
||||
|
||||
```bash
|
||||
redis-cli -p 6381 SET greeting "Hello, HeroDB!"
|
||||
redis-cli -p 6381 GET greeting
|
||||
# → "Hello, HeroDB!"
|
||||
# Export from redb
|
||||
redis-cli -p 6379 --rdb dump.rdb
|
||||
|
||||
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"
|
||||
```
|
||||
# Import to sled
|
||||
redis-cli -p 6381 --pipe < dump.rdb
|
||||
```
|
0
herodb/specs/backgroundinfo/tantivy.md
Normal file
0
herodb/specs/backgroundinfo/tantivy.md
Normal 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,
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
@@ -123,4 +124,160 @@ 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 clone_arc(&self) -> Arc<dyn StorageBackend> {
|
||||
unimplemented!("Storage cloning not yet implemented for redb backend")
|
||||
}
|
||||
}
|
837
herodb/src/storage_sled/mod.rs
Normal file
837
herodb/src/storage_sled/mod.rs
Normal file
@@ -0,0 +1,837 @@
|
||||
// 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 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(),
|
||||
})
|
||||
}
|
||||
}
|
57
herodb/src/storage_trait.rs
Normal file
57
herodb/src/storage_trait.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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;
|
||||
|
||||
// Clone to Arc for sharing
|
||||
fn clone_arc(&self) -> Arc<dyn StorageBackend>;
|
||||
}
|
Reference in New Issue
Block a user