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:
Markus Scully 2025-08-05 18:26:08 +03:00
parent b36f178999
commit a1cc9b6e04
Signed by: mascully
GPG key ID: 93CA5814B698101C
6 changed files with 1375 additions and 74 deletions

1267
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,3 +18,6 @@ blake3 = { version = "1.8.2", features = ["traits-preview"] }
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
proc-macro2 = "1.0"
sqlx = "0.8.6"
rusqlite = { version = "0.32.1", features = ["bundled"] }
tempfile = "3.12.0"

View file

@ -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 }

View file

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

View file

@ -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;

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