✨ 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
|
@ -18,3 +18,6 @@ blake3 = { version = "1.8.2", features = ["traits-preview"] }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "2.0", features = ["full"] }
|
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
|
serde_json.workspace = true
|
||||||
blake3.workspace = true
|
blake3.workspace = true
|
||||||
vanth_derive = { path = "../vanth_derive" }
|
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 bevy_ecs::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
pub path: PathBuf,
|
backend: Backend,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
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)]
|
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||||
|
@ -20,11 +90,26 @@ pub struct Cache {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub enum Backend {
|
pub enum Backend {
|
||||||
Memory,
|
Memory(Memory),
|
||||||
Sqlite(Sqlite)
|
Sqlite(Sqlite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One table, key-value store. Keys and values are both blobs.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Sqlite {
|
pub struct Sqlite {
|
||||||
path: PathBuf,
|
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 derive;
|
||||||
mod fs;
|
mod fs;
|
||||||
|
mod store;
|
||||||
// #[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);
|
|
||||||
// }
|
|
||||||
|
|
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