diff --git a/Cargo.toml b/Cargo.toml index a078724..baee4b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,9 +31,14 @@ optional = true version = "0.19.0" optional = true +[dependencies.rusqlite] +version = "0.28.0" +optional = true + [features] -default = [] +default = ["cbor"] cbor = ["dep:ciborium"] blake2 = ["dep:blake2"] +sqlite = ["dep:rusqlite", "cbor", "blake2"] rusty-leveldb = ["dep:rusty-leveldb", "blake2", "cbor"] rocksdb = ["dep:rocksdb", "blake2", "cbor"] diff --git a/src/lib.rs b/src/lib.rs index a8a2c76..70732cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ pub mod node; pub mod prelude; #[cfg(feature = "rocksdb")] pub mod rocksdb; +#[cfg(feature = "sqlite")] +pub mod sqlite; pub mod store; #[cfg(test)] diff --git a/src/sqlite/mod.rs b/src/sqlite/mod.rs new file mode 100644 index 0000000..306a26a --- /dev/null +++ b/src/sqlite/mod.rs @@ -0,0 +1,101 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::path::Path; + +use crate::{ + hash::HashWriter, + node::Node, + store::{Result as StoreResult, Store, StoreError}, +}; + +use ciborium; +use rusqlite::{self, OptionalExtension}; + +pub struct SqliteStore { + conn: rusqlite::Connection, +} + +impl SqliteStore { + pub fn connect>(path: P) -> Result { + Ok(Self { + conn: rusqlite::Connection::open(path)?, + }) + } + + pub fn in_memory() -> Result { + let me = Self { + conn: rusqlite::Connection::open_in_memory()?, + }; + me.init_db()?; + Ok(me) + } + + pub fn init_db(&self) -> Result<(), rusqlite::Error> { + self.conn.execute_batch( + "BEGIN; + CREATE TABLE content_store(content_id BLOB PRIMARY KEY, node BLOB NOT NULL); + COMMIT;", + )?; + Ok(()) + } +} + +impl Store for SqliteStore +where + HW: HashWriter, +{ + fn contains(&self, id: &[u8]) -> StoreResult { + let val: Option> = self + .conn + .query_row( + "select node from content_store where content_id = ?", + [id], + |r| r.get(0), + ) + .optional()?; + Ok(val.is_some()) + } + + fn get(&self, id: &[u8]) -> StoreResult>> { + let result: Option> = self + .conn + .query_row( + "select node from content_store where content_id = ?", + [id], + |r| r.get(0), + ) + .optional()?; + Ok(match result { + Some(bs) => ciborium::de::from_reader(bs.as_slice()) + .map_err(|e| StoreError::StoreFailure(format!("Invalid serialization {:?}", e)))?, + None => None, + }) + } + + fn store(&mut self, node: Node) -> StoreResult<()> { + let mut buf = Vec::new(); + ciborium::ser::into_writer(&node, &mut buf).unwrap(); + self.conn.execute( + "insert into content_store (content_id, node) values (?, ?)", + [node.id(), buf.as_slice()], + )?; + Ok(()) + } +} + +impl From for StoreError { + fn from(e: rusqlite::Error) -> Self { + StoreError::StoreFailure(format!("{:?}", e)) + } +}