✨♻️➕ cli, vanth: Implement CLI commands and enhance store
- Added `write`, `get`, `get-all`, `delete`, and `delete-all` subcommands to the CLI. - Refactored store operations to support raw type handling and removed `Vanth` trait requirement where possible. - Implemented tracing-based logging with console output for improved observability. - Improved SQLite store error handling with specific cases for missing tables. - Added new dependencies including `tracing`, `tracing-subscriber`, `nu-ansi-term`, `sharded-slab`, and `valuable`. - Removed `.zed/settings.json` editor configuration file.
This commit is contained in:
parent
9e7979931c
commit
a91d92b6ff
10 changed files with 361 additions and 77 deletions
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"lsp": {
|
|
||||||
"rust-analyzer": {
|
|
||||||
"initialization_options": {
|
|
||||||
"rustfmt": {
|
|
||||||
"overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
84
Cargo.lock
generated
84
Cargo.lock
generated
|
@ -1166,6 +1166,16 @@ version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
|
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -1225,6 +1235,12 @@ version = "1.70.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
@ -1536,6 +1552,15 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -1946,6 +1971,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
|
dependencies = [
|
||||||
|
"nu-ansi-term",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2028,6 +2079,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vanth"
|
name = "vanth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -2041,6 +2098,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"tracing",
|
||||||
"vanth_derive",
|
"vanth_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2049,6 +2107,10 @@ name = "vanth_cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"vanth",
|
"vanth",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2211,6 +2273,28 @@ dependencies = [
|
||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
@ -23,3 +23,5 @@ rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||||
tempfile = "3.12.0"
|
tempfile = "3.12.0"
|
||||||
rand_core = "0.6.4"
|
rand_core = "0.6.4"
|
||||||
rand_chacha = { version = "0.3.1", features = ["serde1"] }
|
rand_chacha = { version = "0.3.1", features = ["serde1"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
|
@ -5,4 +5,10 @@ edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
vanth = { path = "../vanth" }
|
vanth = { path = "../vanth" }
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
use vanth::{ContentHash, store::Store, Ty};
|
||||||
|
use vanth::hash as vanth_hash;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "vanth")]
|
#[command(name = "vanth")]
|
||||||
|
@ -10,38 +15,187 @@ pub struct Cli {
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
#[command(about = "File system operations")]
|
#[command(about = "Write a value to the store")]
|
||||||
Fs {
|
Write(WriteArgs),
|
||||||
#[command(subcommand)]
|
#[command(about = "Get a value from the store")]
|
||||||
command: FsCommands,
|
Get(GetArgs),
|
||||||
},
|
#[command(about = "Get all values of a type from the store")]
|
||||||
|
GetAll(GetAllArgs),
|
||||||
|
#[command(about = "Delete a value by hash from all types")]
|
||||||
|
Delete(DeleteArgs),
|
||||||
|
#[command(about = "Delete all values of a type")]
|
||||||
|
DeleteAll(DeleteAllArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub enum FsCommands {
|
pub struct WriteArgs {
|
||||||
#[command(about = "Read from path and print JSON to stdout")]
|
#[arg(long, help = "Database file path")]
|
||||||
Open {
|
db: PathBuf,
|
||||||
#[arg(help = "Path to read from")]
|
#[arg(long, help = "Type name, e.g., path::to::Type")]
|
||||||
path: String,
|
ty: String,
|
||||||
},
|
#[arg(long, help = "JSON value to write, optional (read from stdin if omitted)")]
|
||||||
#[command(about = "Take JSON from stdin and write to path")]
|
value: Option<String>,
|
||||||
Save {
|
}
|
||||||
#[arg(help = "Path to write to")]
|
|
||||||
path: String,
|
#[derive(Args, Debug)]
|
||||||
},
|
pub struct GetArgs {
|
||||||
|
#[arg(long, help = "Database file path")]
|
||||||
|
db: PathBuf,
|
||||||
|
#[arg(long, help = "Type name, e.g., path::to::Type")]
|
||||||
|
ty: String,
|
||||||
|
#[arg(help = "Content hash as 64-character hex string")]
|
||||||
|
content_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct GetAllArgs {
|
||||||
|
#[arg(long, help = "Database file path")]
|
||||||
|
db: PathBuf,
|
||||||
|
#[arg(long, help = "Type name, e.g., path::to::Type")]
|
||||||
|
ty: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct DeleteArgs {
|
||||||
|
#[arg(long, help = "Database file path")]
|
||||||
|
db: PathBuf,
|
||||||
|
#[arg(help = "Content hash as 64-character hex string")]
|
||||||
|
content_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct DeleteAllArgs {
|
||||||
|
#[arg(long, help = "Database file path")]
|
||||||
|
db: PathBuf,
|
||||||
|
#[arg(long, help = "Type name, e.g., path::to::Type")]
|
||||||
|
ty: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(cli: Cli) {
|
pub fn execute(cli: Cli) {
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Fs { command } => match command {
|
Commands::Write(args) => handle_write(&args),
|
||||||
FsCommands::Open { path } => {
|
Commands::Get(args) => handle_get(&args),
|
||||||
// TODO: implement fs open functionality
|
Commands::GetAll(args) => handle_get_all(&args),
|
||||||
println!("fs open: {}", path);
|
Commands::Delete(args) => handle_delete(&args),
|
||||||
}
|
Commands::DeleteAll(args) => handle_delete_all(&args),
|
||||||
FsCommands::Save { path } => {
|
|
||||||
// TODO: implement fs save functionality
|
|
||||||
println!("fs save: {}", path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_ty(s: &str) -> Ty {
|
||||||
|
Ty {
|
||||||
|
path: s.split("::").map(|p| p.to_string()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hash(s: &str) -> ContentHash {
|
||||||
|
if s.len() != 64 {
|
||||||
|
eprintln!("Hash must be exactly 64 hexadecimal characters");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
let mut hash = [0u8; 32];
|
||||||
|
for (i, byte) in hash.iter_mut().enumerate() {
|
||||||
|
let hex_slice = &s[i * 2..i * 2 + 2];
|
||||||
|
*byte = u8::from_str_radix(hex_slice, 16).unwrap_or_else(|_| {
|
||||||
|
eprintln!("Invalid hexadecimal in hash: {}", hex_slice);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ContentHash { hash }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_write(args: &WriteArgs) {
|
||||||
|
let mut store = Store::sqlite_from_path(args.db.clone()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error opening store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let ty = parse_ty(&args.ty);
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
if let Some(val) = &args.value {
|
||||||
|
content = val.clone();
|
||||||
|
} else {
|
||||||
|
io::stdin().read_to_string(&mut content).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error reading from stdin: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: serde_json::Value = serde_json::from_str(&content).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Invalid JSON: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let data = serde_json::to_vec(&value).unwrap();
|
||||||
|
let content_hash = vanth_hash(&value);
|
||||||
|
|
||||||
|
store.write_raw(ty, content_hash, data).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error writing to store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get(args: &GetArgs) {
|
||||||
|
let mut store = Store::sqlite_from_path(args.db.clone()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error opening store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let ty = parse_ty(&args.ty);
|
||||||
|
let content_hash = parse_hash(&args.content_hash);
|
||||||
|
|
||||||
|
let raw = store.get_from_hash_raw(ty, content_hash).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error getting from store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
match raw {
|
||||||
|
Some(data) => {
|
||||||
|
let output = String::from_utf8(data).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Invalid UTF-8 in data: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
println!("{}", output);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get_all(args: &GetAllArgs) {
|
||||||
|
let mut store = Store::sqlite_from_path(args.db.clone()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error opening store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let ty = parse_ty(&args.ty);
|
||||||
|
|
||||||
|
let items = store.get_all_of_type_raw(ty).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error getting all from store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
for (_, data) in items {
|
||||||
|
let output = String::from_utf8(data).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Invalid UTF-8 in data: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
println!("{}", output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_delete(args: &DeleteArgs) {
|
||||||
|
let mut store = Store::sqlite_from_path(args.db.clone()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error opening store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let content_hash = parse_hash(&args.content_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_delete_all(args: &DeleteAllArgs) {
|
||||||
|
let mut store = Store::sqlite_from_path(args.db.clone()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error opening store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let ty = parse_ty(&args.ty);
|
||||||
|
|
||||||
|
store.delete_all_raw(ty).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Error deleting all from store: {:?}", e);
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -2,9 +2,13 @@ use clap::Parser;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
use cli::Cli;
|
pub use cli::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::TRACE)
|
||||||
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
cli::execute(cli);
|
cli::execute(cli);
|
||||||
}
|
}
|
||||||
|
|
0
crates/cli/tests/integration/main.rs
Normal file
0
crates/cli/tests/integration/main.rs
Normal file
|
@ -15,7 +15,8 @@ serde_json.workspace = true
|
||||||
blake3.workspace = true
|
blake3.workspace = true
|
||||||
vanth_derive = { path = "../vanth_derive" }
|
vanth_derive = { path = "../vanth_derive" }
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
rusqlite = { workspace = true }
|
rusqlite.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
@ -4,6 +4,7 @@ use rusqlite::{Connection, named_params, params};
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::{ComponentContents, ContentHash, Ty, Vanth, hash};
|
use crate::{ComponentContents, ContentHash, Ty, Vanth, hash};
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ type Result<T> = std::result::Result<T, Error>;
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Serializiation(String),
|
Serializiation(String),
|
||||||
Database(String),
|
SqliteTableDoesNotExist { table_name: String },
|
||||||
|
SqliteUnknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
impl From<serde_json::Error> for Error {
|
||||||
|
@ -28,13 +30,36 @@ impl From<serde_json::Error> for Error {
|
||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
impl From<rusqlite::Error> for Error {
|
||||||
fn from(err: rusqlite::Error) -> Self {
|
fn from(err: rusqlite::Error) -> Self {
|
||||||
Error::Database(err.to_string())
|
if let rusqlite::Error::SqliteFailure(_, Some(ref message)) = err
|
||||||
|
&& let Some(table_name) = message.strip_prefix("no such table: ")
|
||||||
|
{
|
||||||
|
return Error::SqliteTableDoesNotExist {
|
||||||
|
table_name: table_name.into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Error::SqliteUnknown(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct StoreParams {
|
||||||
|
pub create_if_not_exists: bool,
|
||||||
|
pub read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StoreParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
create_if_not_exists: true,
|
||||||
|
read_only: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
/// Use an SQLite backend with a database file at the provided path.
|
/// Use an SQLite backend with a database file at the provided path.
|
||||||
pub fn from_path(path: PathBuf) -> Result<Self> {
|
// TODO: Params
|
||||||
|
pub fn sqlite_from_path(path: PathBuf) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
backend: Box::new(Sqlite::new(path)?),
|
backend: Box::new(Sqlite::new(path)?),
|
||||||
})
|
})
|
||||||
|
@ -48,7 +73,7 @@ impl Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_from_hash<T: Vanth + DeserializeOwned>(&mut self, content_hash: ContentHash) -> Result<Option<T>> {
|
pub fn get_from_hash<T: Vanth + DeserializeOwned>(&mut self, content_hash: ContentHash) -> Result<Option<T>> {
|
||||||
let Some(raw) = self.get_from_hash_raw::<T>(content_hash)? else {
|
let Some(raw) = self.get_from_hash_raw(T::ty(), content_hash)? else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,8 +81,8 @@ impl Store {
|
||||||
Ok(Some(deserialized))
|
Ok(Some(deserialized))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_from_hash_raw<T: Vanth>(&mut self, content_hash: ContentHash) -> Result<Option<Vec<u8>>> {
|
pub fn get_from_hash_raw(&mut self, ty: Ty, content_hash: ContentHash) -> Result<Option<Vec<u8>>> {
|
||||||
self.backend.get_from_hash(T::ty(), content_hash)
|
self.backend.get_from_hash(ty, content_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_of_type<T: Vanth>(&mut self) -> Result<Vec<ComponentContents<T>>> {
|
pub fn get_all_of_type<T: Vanth>(&mut self) -> Result<Vec<ComponentContents<T>>> {
|
||||||
|
@ -79,8 +104,8 @@ impl Store {
|
||||||
self.backend.write(T::ty(), content_hash, data)
|
self.backend.write(T::ty(), content_hash, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_raw<T: Vanth>(&mut self, content_hash: ContentHash, content: Vec<u8>) -> Result<()> {
|
pub fn write_raw(&mut self, ty: Ty, content_hash: ContentHash, content: Vec<u8>) -> Result<()> {
|
||||||
self.backend.write(T::ty(), content_hash, content)
|
self.backend.write(ty, content_hash, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete<T: Vanth>(&mut self, content_hash: ContentHash) -> Result<()> {
|
pub fn delete<T: Vanth>(&mut self, content_hash: ContentHash) -> Result<()> {
|
||||||
|
@ -90,6 +115,18 @@ impl Store {
|
||||||
pub fn delete_all<T: Vanth>(&mut self) -> Result<()> {
|
pub fn delete_all<T: Vanth>(&mut self) -> Result<()> {
|
||||||
self.backend.delete_all_of_ty(T::ty())
|
self.backend.delete_all_of_ty(T::ty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_all_of_type_raw(&mut self, ty: Ty) -> Result<Vec<(ContentHash, Vec<u8>)>> {
|
||||||
|
self.backend.get_all_of_ty(ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_raw(&mut self, ty: Ty, content_hash: ContentHash) -> Result<()> {
|
||||||
|
self.backend.delete_by_hash(ty, content_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_all_raw(&mut self, ty: Ty) -> Result<()> {
|
||||||
|
self.backend.delete_all_of_ty(ty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||||
|
@ -113,26 +150,18 @@ pub trait Backend: std::fmt::Debug {
|
||||||
/// One table per type. Keys and values are both blobs.
|
/// One table per type. Keys and values are both blobs.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Sqlite {
|
pub struct Sqlite {
|
||||||
conn: Connection,
|
connection: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sqlite {
|
impl Sqlite {
|
||||||
pub fn new(path: PathBuf) -> Result<Self> {
|
pub fn new(path: PathBuf) -> Result<Self> {
|
||||||
let conn = Connection::open(path)?;
|
use rusqlite::OpenFlags;
|
||||||
Ok(Self { conn })
|
// Remove the `SQLITE_OPEN_CREATE` flag because we do not want to create databases if they don't exist.
|
||||||
}
|
let connection = Connection::open_with_flags(
|
||||||
|
path,
|
||||||
fn ensure_table_exists(&self, ty: &Ty) -> Result<()> {
|
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||||
let table_name = Self::table_name(ty);
|
)?;
|
||||||
let query = format!(
|
Ok(Self { connection })
|
||||||
"CREATE TABLE IF NOT EXISTS \"{}\" (
|
|
||||||
content_hash BLOB PRIMARY KEY,
|
|
||||||
content BLOB NOT NULL
|
|
||||||
)",
|
|
||||||
table_name
|
|
||||||
);
|
|
||||||
self.conn.execute(&query, [])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn table_name(ty: &Ty) -> String {
|
fn table_name(ty: &Ty) -> String {
|
||||||
|
@ -142,12 +171,11 @@ impl Sqlite {
|
||||||
|
|
||||||
impl Backend for Sqlite {
|
impl Backend for Sqlite {
|
||||||
fn get_from_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<Option<Vec<u8>>> {
|
fn get_from_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<Option<Vec<u8>>> {
|
||||||
self.ensure_table_exists(&ty)?;
|
|
||||||
let table_name = Self::table_name(&ty);
|
let table_name = Self::table_name(&ty);
|
||||||
let query = format!("SELECT content FROM \"{}\" WHERE content_hash = :hash", table_name);
|
let query = format!("SELECT content FROM \"{}\" WHERE content_hash = :hash", table_name);
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.conn
|
.connection
|
||||||
.query_row(&query, named_params! {":hash": content_hash.hash.as_slice()}, |row| {
|
.query_row(&query, named_params! {":hash": content_hash.hash.as_slice()}, |row| {
|
||||||
row.get::<_, Vec<u8>>(0)
|
row.get::<_, Vec<u8>>(0)
|
||||||
}) {
|
}) {
|
||||||
|
@ -158,12 +186,18 @@ impl Backend for Sqlite {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_all_of_ty(&mut self, ty: Ty) -> Result<Vec<(ContentHash, Vec<u8>)>> {
|
fn get_all_of_ty(&mut self, ty: Ty) -> Result<Vec<(ContentHash, Vec<u8>)>> {
|
||||||
self.ensure_table_exists(&ty)?;
|
let transaction = self.connection.transaction()?;
|
||||||
let table_name = Self::table_name(&ty);
|
let table_name = Self::table_name(&ty);
|
||||||
let query = format!("SELECT content_hash, content FROM \"{}\"", table_name);
|
let query = format!("SELECT content_hash, content FROM \"{}\"", table_name);
|
||||||
|
|
||||||
let mut stmt = self.conn.prepare(&query)?;
|
trace!("Reading table {}", table_name);
|
||||||
let rows = stmt.query_map([], |row| {
|
|
||||||
|
let mut statement = match transaction.prepare(&query).map_err(Into::into) {
|
||||||
|
Err(Error::SqliteTableDoesNotExist { .. }) => return Ok(Vec::new()),
|
||||||
|
other => other?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = statement.query_map([], |row| {
|
||||||
let hash_bytes: Vec<u8> = row.get(0)?;
|
let hash_bytes: Vec<u8> = row.get(0)?;
|
||||||
let content: Vec<u8> = row.get(1)?;
|
let content: Vec<u8> = row.get(1)?;
|
||||||
let mut hash_array = [0u8; 32];
|
let mut hash_array = [0u8; 32];
|
||||||
|
@ -175,17 +209,28 @@ impl Backend for Sqlite {
|
||||||
for row in rows {
|
for row in rows {
|
||||||
results.push(row?);
|
results.push(row?);
|
||||||
}
|
}
|
||||||
Ok(results)
|
|
||||||
|
drop(statement);
|
||||||
|
|
||||||
|
transaction.commit()?;
|
||||||
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, ty: Ty, content_hash: ContentHash, content: Vec<u8>) -> Result<()> {
|
fn write(&mut self, ty: Ty, content_hash: ContentHash, content: Vec<u8>) -> Result<()> {
|
||||||
self.ensure_table_exists(&ty)?;
|
|
||||||
let table_name = Self::table_name(&ty);
|
let table_name = Self::table_name(&ty);
|
||||||
|
let create_table_query = format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS \"{}\" (
|
||||||
|
content_hash BLOB PRIMARY KEY,
|
||||||
|
content BLOB NOT NULL
|
||||||
|
)",
|
||||||
|
table_name
|
||||||
|
);
|
||||||
|
self.connection.execute(&create_table_query, [])?;
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"INSERT OR REPLACE INTO \"{}\" (content_hash, content) VALUES (:hash, :content)",
|
"INSERT OR REPLACE INTO \"{}\" (content_hash, content) VALUES (:hash, :content)",
|
||||||
table_name
|
table_name
|
||||||
);
|
);
|
||||||
self.conn.execute(
|
self.connection.execute(
|
||||||
&query,
|
&query,
|
||||||
named_params! {":hash": content_hash.hash.as_slice(), ":content": content},
|
named_params! {":hash": content_hash.hash.as_slice(), ":content": content},
|
||||||
)?;
|
)?;
|
||||||
|
@ -193,10 +238,9 @@ impl Backend for Sqlite {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_by_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<()> {
|
fn delete_by_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<()> {
|
||||||
self.ensure_table_exists(&ty)?;
|
|
||||||
let table_name = Self::table_name(&ty);
|
let table_name = Self::table_name(&ty);
|
||||||
let query = format!("DELETE FROM \"{}\" WHERE content_hash = :hash", table_name);
|
let query = format!("DELETE FROM \"{}\" WHERE content_hash = :hash", table_name);
|
||||||
self.conn
|
self.connection
|
||||||
.execute(&query, named_params! {":hash": content_hash.hash.as_slice()})?;
|
.execute(&query, named_params! {":hash": content_hash.hash.as_slice()})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -204,7 +248,7 @@ impl Backend for Sqlite {
|
||||||
fn delete_all_of_ty(&mut self, ty: Ty) -> Result<()> {
|
fn delete_all_of_ty(&mut self, ty: Ty) -> Result<()> {
|
||||||
let table_name = Self::table_name(&ty);
|
let table_name = Self::table_name(&ty);
|
||||||
let query = format!("DROP TABLE IF EXISTS \"{}\"", table_name);
|
let query = format!("DROP TABLE IF EXISTS \"{}\"", table_name);
|
||||||
self.conn.execute(&query, [])?;
|
self.connection.execute(&query, [])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct Bar {
|
||||||
fn test_sqlite_store() {
|
fn test_sqlite_store() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let path = dir.path().join("test.db");
|
let path = dir.path().join("test.db");
|
||||||
let mut store = Store::from_path(path.clone()).unwrap();
|
let mut store = Store::sqlite_from_path(path.clone()).unwrap();
|
||||||
|
|
||||||
let foo_1 = Foo { inner: 1 };
|
let foo_1 = Foo { inner: 1 };
|
||||||
let foo_2 = Foo { inner: 2 };
|
let foo_2 = Foo { inner: 2 };
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue