Add SQLite schema metadata for DB resources
This commit is contained in:
parent
1a0f1e7671
commit
527090fd4c
8 changed files with 119 additions and 14 deletions
|
|
@ -6,6 +6,11 @@ rust-version.workspace = true
|
|||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
blake3.workspace = true
|
||||
rusqlite.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use geth_types::{DbId, ResourceId};
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DbResource {
|
||||
|
|
@ -17,6 +19,8 @@ pub struct DbResource {
|
|||
pub enum DbError {
|
||||
#[error("invalid db resource name: {0}")]
|
||||
InvalidName(String),
|
||||
#[error("sqlite error: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
}
|
||||
|
||||
pub fn validate_db_name(name: &str) -> Result<(), DbError> {
|
||||
|
|
@ -30,6 +34,50 @@ pub fn validate_db_name(name: &str) -> Result<(), DbError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn schema_metadata(path: &Path) -> Result<String, DbError> {
|
||||
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
|
||||
let mut stmt = conn.prepare(
|
||||
r#"SELECT type, name, COALESCE(sql, '')
|
||||
FROM sqlite_master
|
||||
WHERE name NOT LIKE 'sqlite_%'
|
||||
ORDER BY type, name"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut canonical = String::new();
|
||||
let mut table_count = 0_usize;
|
||||
let mut index_count = 0_usize;
|
||||
let mut view_count = 0_usize;
|
||||
let mut trigger_count = 0_usize;
|
||||
for row in rows {
|
||||
let (kind, name, sql) = row?;
|
||||
match kind.as_str() {
|
||||
"table" => table_count += 1,
|
||||
"index" => index_count += 1,
|
||||
"view" => view_count += 1,
|
||||
"trigger" => trigger_count += 1,
|
||||
_ => {}
|
||||
}
|
||||
canonical.push_str(&kind);
|
||||
canonical.push('\0');
|
||||
canonical.push_str(&name);
|
||||
canonical.push('\0');
|
||||
canonical.push_str(&sql);
|
||||
canonical.push('\n');
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"tables={table_count} indexes={index_count} views={view_count} triggers={trigger_count} schema_hash={}",
|
||||
blake3::hash(canonical.as_bytes())
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn crsqlite_sync_roadmap() -> &'static str {
|
||||
"future db sync reads crsql_changes, exchanges changes over Iroh, and applies through crsql_changes"
|
||||
|
|
@ -48,4 +96,27 @@ mod tests {
|
|||
assert!(validate_db_name("notes/main").is_err());
|
||||
assert!(validate_db_name("notes main").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_metadata_reports_counts_and_stable_hash() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("notes.sqlite");
|
||||
let conn = Connection::open(&path).expect("open sqlite");
|
||||
conn.execute(
|
||||
"CREATE TABLE notes(id INTEGER PRIMARY KEY, body TEXT NOT NULL)",
|
||||
[],
|
||||
)
|
||||
.expect("create table");
|
||||
conn.execute("CREATE INDEX notes_body ON notes(body)", [])
|
||||
.expect("create index");
|
||||
drop(conn);
|
||||
|
||||
let first = schema_metadata(&path).expect("schema metadata");
|
||||
let second = schema_metadata(&path).expect("schema metadata again");
|
||||
|
||||
assert_eq!(first, second);
|
||||
assert!(first.contains("tables=1"));
|
||||
assert!(first.contains("indexes=1"));
|
||||
assert!(first.contains("schema_hash="));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue