✨ vanth/store, tests: Add persistent SQLite and in-memory store backends with CRUD operations
- Added `rusqlite` and `sqlx` dependencies to support SQLite backend functionality for the store. - Implemented `Store` struct with backend switching between in-memory (`HashMap`) and SQLite-based storage. - Added CRUD operations (`read`, `write`, `delete`) to the `Store` API with error handling. - Created integration tests for SQLite store persistence and basic operations.
This commit is contained in:
parent
b36f178999
commit
a1cc9b6e04
6 changed files with 1375 additions and 74 deletions
1267
Cargo.lock
generated
1267
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -17,4 +17,7 @@ digest = "0.10.7"
|
|||
blake3 = { version = "1.8.2", features = ["traits-preview"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
proc-macro2 = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
sqlx = "0.8.6"
|
||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||
tempfile = "3.12.0"
|
||||
|
|
|
@ -14,3 +14,8 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
blake3.workspace = true
|
||||
vanth_derive = { path = "../vanth_derive" }
|
||||
sqlx.workspace = true
|
||||
rusqlite = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
@ -1,15 +1,85 @@
|
|||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
pub struct Store {
|
||||
pub path: PathBuf,
|
||||
backend: Backend,
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, String>;
|
||||
|
||||
impl Store {
|
||||
pub fn from_path(path: PathBuf) -> Result<Self> {
|
||||
let conn = Connection::open(&path).map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS kv (key BLOB PRIMARY KEY, value BLOB)",
|
||||
params![],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
backend: Backend::Sqlite(Sqlite { path }),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn in_memory() -> Result<Self> {
|
||||
Ok(Self {
|
||||
backend: Backend::Memory(Memory::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read(&mut self, key: impl AsRef<[u8]>) -> Result<Option<Vec<u8>>> {
|
||||
match &mut self.backend {
|
||||
Backend::Memory(mem) => Ok(mem.values.get(key.as_ref()).cloned()),
|
||||
Backend::Sqlite(sql) => {
|
||||
let conn = Connection::open(&sql.path).map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT value FROM kv WHERE key = ?1").map_err(|e| e.to_string())?;
|
||||
let mut rows = stmt.query(params![key.as_ref()]).map_err(|e| e.to_string())?;
|
||||
if let Some(row) = rows.next().map_err(|e| e.to_string())? {
|
||||
let value: Vec<u8> = row.get(0).map_err(|e| e.to_string())?;
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<()> {
|
||||
match &mut self.backend {
|
||||
Backend::Memory(mem) => {
|
||||
mem.values.insert(key.as_ref().to_vec(), value.as_ref().to_vec());
|
||||
Ok(())
|
||||
}
|
||||
Backend::Sqlite(sql) => {
|
||||
let conn = Connection::open(&sql.path).map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO kv (key, value) VALUES (?1, ?2)",
|
||||
params![key.as_ref(), value.as_ref()],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, key: impl AsRef<[u8]>) -> Result<()> {
|
||||
match &mut self.backend {
|
||||
Backend::Memory(mem) => {
|
||||
mem.values.remove(key.as_ref());
|
||||
Ok(())
|
||||
}
|
||||
Backend::Sqlite(sql) => {
|
||||
let conn = Connection::open(&sql.path).map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"DELETE FROM kv WHERE key = ?1",
|
||||
params![key.as_ref()],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
|
@ -20,11 +90,26 @@ pub struct Cache {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Backend {
|
||||
Memory,
|
||||
Memory(Memory),
|
||||
Sqlite(Sqlite)
|
||||
}
|
||||
|
||||
/// One table, key-value store. Keys and values are both blobs.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Sqlite {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
/// One table, key-value store. Keys and values are both blobs.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Memory {
|
||||
values: HashMap<Vec<u8>, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,57 +3,4 @@ use vanth::{Component, Node, Reference};
|
|||
|
||||
mod derive;
|
||||
mod fs;
|
||||
|
||||
// #[test]
|
||||
// fn test_store() {
|
||||
// #[derive(Clone, Deserialize, Serialize)]
|
||||
// struct Foo {
|
||||
// bar: Reference<Bar>,
|
||||
// }
|
||||
|
||||
// #[derive(Clone, Deserialize, Serialize)]
|
||||
// struct Bar {
|
||||
// foo: Reference<Foo>,
|
||||
// }
|
||||
|
||||
// impl Component for Foo {
|
||||
// fn id() -> String {
|
||||
// "foo".into()
|
||||
// }
|
||||
// }
|
||||
|
||||
// let node = Node::in_memory();
|
||||
|
||||
// let entity_id = "entity_1";
|
||||
// let entity_components = (Foo { a: 5, b: 6.0 },);
|
||||
|
||||
// node.save("entity_1", entity_components);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_store() {
|
||||
// #[derive(Deserialize, Serialize)]
|
||||
// struct Foo {
|
||||
// a: u32,
|
||||
// b: f32,
|
||||
// }
|
||||
|
||||
// impl Component for Foo {
|
||||
// fn id() -> String {
|
||||
// "foo".into()
|
||||
// }
|
||||
// }
|
||||
|
||||
// let node = Node::in_memory();
|
||||
|
||||
// let entity_id = "entity_1";
|
||||
// let entity_components = (Foo { a: 5, b: 6.0 },);
|
||||
|
||||
// node.save("entity_1", entity_components);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_entity_count_zero() {
|
||||
// let mut node = Node::new();
|
||||
// assert_eq!(node.entity_count(), 0);
|
||||
// }
|
||||
mod store;
|
||||
|
|
26
crates/vanth/tests/integration/store.rs
Normal file
26
crates/vanth/tests/integration/store.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use vanth::store::Store;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_sqlite_store() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
let mut store = Store::from_path(path.clone()).unwrap();
|
||||
|
||||
assert_eq!(store.read(b"key_1"), Ok(None));
|
||||
assert_eq!(store.write(b"key_1", b"value_1"), Ok(()));
|
||||
|
||||
let value = store.read(b"key_1").unwrap();
|
||||
assert_eq!(value.as_deref(), Some(b"value_1" as &[u8]));
|
||||
|
||||
drop(store);
|
||||
|
||||
let mut store = Store::from_path(path.clone()).unwrap();
|
||||
|
||||
let value = store.read(b"key_1").unwrap();
|
||||
assert_eq!(value.as_deref(), Some(b"value_1" as &[u8]));
|
||||
|
||||
store.delete(b"key_1").unwrap();
|
||||
assert_eq!(store.read(b"key_1"), Ok(None));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue