From 873738f0e1dc01f2a75e5e60ae68094b4558ed07 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 20 Mar 2022 07:53:06 +0200 Subject: refactor: put SchemaVersion, GenMeta in their own modules For clarity, though these aren't yet used anywhere. That will happen soon. Sponsored-by: author --- src/genmeta.rs | 57 +++++++++++++++++++ src/lib.rs | 2 + src/schema.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 src/genmeta.rs create mode 100644 src/schema.rs diff --git a/src/genmeta.rs b/src/genmeta.rs new file mode 100644 index 0000000..2ce4c4c --- /dev/null +++ b/src/genmeta.rs @@ -0,0 +1,57 @@ +//! Backup generations metadata. + +use crate::schema::{SchemaVersion, VersionComponent}; +use serde::Serialize; +use std::collections::HashMap; + +/// Metadata about the local generation. +#[derive(Debug, Serialize)] +pub struct GenerationMeta { + schema_version: SchemaVersion, + extras: HashMap, +} + +impl GenerationMeta { + /// Create from a hash map. + pub fn from(mut map: HashMap) -> Result { + let major: VersionComponent = metaint(&mut map, "schema_version_major")?; + let minor: VersionComponent = metaint(&mut map, "schema_version_minor")?; + Ok(Self { + schema_version: SchemaVersion::new(major, minor), + extras: map, + }) + } + + /// Return schema version of local generation. + pub fn schema_version(&self) -> SchemaVersion { + self.schema_version + } +} + +fn metastr(map: &mut HashMap, key: &str) -> Result { + if let Some(v) = map.remove(key) { + Ok(v) + } else { + Err(GenerationMetaError::NoMetaKey(key.to_string())) + } +} + +fn metaint(map: &mut HashMap, key: &str) -> Result { + let v = metastr(map, key)?; + let v = v + .parse() + .map_err(|err| GenerationMetaError::BadMetaInteger(key.to_string(), err))?; + Ok(v) +} + +/// Possible errors from getting generation metadata. +#[derive(Debug, thiserror::Error)] +pub enum GenerationMetaError { + /// Missing from from 'meta' table. + #[error("Generation 'meta' table does not have a row {0}")] + NoMetaKey(String), + + /// Bad data in 'meta' table. + #[error("Generation 'meta' row {0} has badly formed integer: {1}")] + BadMetaInteger(String, std::num::ParseIntError), +} diff --git a/src/lib.rs b/src/lib.rs index 6a30334..fdb318f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,10 +25,12 @@ pub mod fsentry; pub mod fsiter; pub mod generation; pub mod genlist; +pub mod genmeta; pub mod index; pub mod indexedstore; pub mod passwords; pub mod policy; +pub mod schema; pub mod server; pub mod store; pub mod workqueue; diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..ae8c00b --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,173 @@ +//! Database schema versions. + +use serde::Serialize; + +/// The type of schema version components. +pub type VersionComponent = u32; + +/// Schema version of the database storing the generation. +/// +/// An Obnam client can restore a generation using schema version +/// (x,y), if the client supports a schema version (x,z). If z < y, +/// the client knows it may not be able to the generation faithfully, +/// and should warn the user about this. If z >= y, the client knows +/// it can restore the generation faithfully. If the client does not +/// support any schema version x, it knows it can't restore the backup +/// at all. +#[derive(Debug, Clone, Copy, Serialize)] +pub struct SchemaVersion { + /// Major version. + pub major: VersionComponent, + /// Minor version. + pub minor: VersionComponent, +} + +impl SchemaVersion { + /// Create a new schema version object. + pub fn new(major: VersionComponent, minor: VersionComponent) -> Self { + Self { major, minor } + } + + /// Return the major and minor version number component of a schema version. + pub fn version(&self) -> (VersionComponent, VersionComponent) { + (self.major, self.minor) + } + + /// Is this schema version compatible with another schema version? + pub fn is_compatible_with(&self, other: &Self) -> bool { + self.major == other.major && self.minor >= other.minor + } +} + +impl std::fmt::Display for SchemaVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +impl std::str::FromStr for SchemaVersion { + type Err = SchemaVersionError; + fn from_str(s: &str) -> Result { + if let Some(pos) = s.find('.') { + let major = parse_int(&s[..pos])?; + let minor = parse_int(&s[pos + 1..])?; + Ok(SchemaVersion::new(major, minor)) + } else { + Err(SchemaVersionError::Invalid(s.to_string())) + } + } +} + +fn parse_int(s: &str) -> Result { + if let Ok(i) = s.parse() { + Ok(i) + } else { + Err(SchemaVersionError::InvalidComponent(s.to_string())) + } +} + +/// Possible errors from parsing schema versions. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum SchemaVersionError { + /// Failed to parse a string as a schema version. + #[error("Invalid schema version {0:?}")] + Invalid(String), + + /// Failed to parse a string as a schema version component. + #[error("Invalid schema version component {0:?}")] + InvalidComponent(String), +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn from_string() { + let v = SchemaVersion::from_str("1.2").unwrap(); + assert_eq!(v.version(), (1, 2)); + } + + #[test] + fn from_string_fails_if_empty() { + match SchemaVersion::from_str("") { + Err(SchemaVersionError::Invalid(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_empty_major() { + match SchemaVersion::from_str(".2") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_empty_minor() { + match SchemaVersion::from_str("1.") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_just_major() { + match SchemaVersion::from_str("1") { + Err(SchemaVersionError::Invalid(s)) => assert_eq!(s, "1"), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_nonnumeric_major() { + match SchemaVersion::from_str("a.2") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, "a"), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_nonnumeric_minor() { + match SchemaVersion::from_str("1.a") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, "a"), + _ => unreachable!(), + } + } + + #[test] + fn compatible_with_self() { + let v = SchemaVersion::new(1, 2); + assert!(v.is_compatible_with(&v)); + } + + #[test] + fn compatible_with_older_minor_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(1, 3); + assert!(new.is_compatible_with(&old)); + } + + #[test] + fn not_compatible_with_newer_minor_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(1, 3); + assert!(!old.is_compatible_with(&new)); + } + + #[test] + fn not_compatible_with_older_major_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(2, 0); + assert!(!new.is_compatible_with(&old)); + } + + #[test] + fn not_compatible_with_newer_major_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(2, 0); + assert!(!old.is_compatible_with(&new)); + } +} -- cgit v1.2.1