Skip to main content

vrtb_core/
lib.rs

1pub mod conformance;
2pub mod engine;
3pub mod error;
4pub mod format;
5
6use engine::{ColumnSchema, ComparePlan, DiffRow, Engine, TableRef, TableSchema};
7use error::Result;
8
9/// Resolve shared columns, fail on incomparable types. With `cols = None`,
10/// defaults to every non-key column of `src` (each must also exist in `dst`).
11pub fn build_plan(
12    src: &TableSchema,
13    dst: &TableSchema,
14    cols: Option<&[String]>,
15    key: &str,
16) -> Result<ComparePlan> {
17    // Default: every non-key column from src
18    let col_names: Vec<String> = match cols {
19        Some(cols) => cols.to_vec(),
20        None => src
21            .columns
22            .iter()
23            .filter(|c| c.name != key)
24            .map(|c| c.name.clone())
25            .collect(),
26    };
27
28    let mut shared_columns = Vec::new();
29    for col in &col_names {
30        let src_col: Option<&ColumnSchema> = src.columns.iter().find(|c| c.name == *col);
31        let dst_col: Option<&ColumnSchema> = dst.columns.iter().find(|c| c.name == *col);
32        match (src_col, dst_col) {
33            (Some(src_col), Some(dst_col)) => {
34                if src_col.ty != dst_col.ty {
35                    return Err(error::VeritableError::Schema(format!(
36                        "Column {} has different types in source and destination",
37                        col
38                    )));
39                }
40                shared_columns.push(src_col.clone());
41            }
42            _ => {
43                return Err(error::VeritableError::Schema(format!(
44                    "Column {} not found in both source and destination",
45                    col
46                )));
47            }
48        }
49    }
50
51    // Find the key column
52    let key_column: &ColumnSchema =
53        src.columns.iter().find(|c| c.name == key).ok_or_else(|| {
54            error::VeritableError::Schema(format!("Key column {} not found in source", key))
55        })?;
56    Ok(ComparePlan {
57        key: key_column.clone(),
58        columns: shared_columns,
59    })
60}
61
62/// Introspect both sides and plan the key plus every shared non-key column.
63pub fn plan(
64    src: &dyn Engine,
65    src_t: &TableRef,
66    dst: &dyn Engine,
67    dst_t: &TableRef,
68    key: &str,
69) -> Result<ComparePlan> {
70    let src_schema = src.introspect(src_t)?;
71    let dst_schema = dst.introspect(dst_t)?;
72    build_plan(&src_schema, &dst_schema, None, key)
73}
74
75/// Not yet implemented — see docs/STATUS.md §5. Signatures are the intended API.
76pub fn joindiff(
77    _engine: &dyn Engine,
78    _a: &TableRef,
79    _b: &TableRef,
80    _plan: &ComparePlan,
81) -> Result<Vec<DiffRow>> {
82    todo!()
83}
84
85pub fn hashdiff(
86    _src: &dyn Engine,
87    _dst: &dyn Engine,
88    _src_t: &TableRef,
89    _dst_t: &TableRef,
90    _plan: &ComparePlan,
91    _segment_size: usize,
92    _leaf_threshold: usize,
93) -> Result<Vec<DiffRow>> {
94    todo!()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use engine::LogicalType;
101
102    fn col(name: &str, ty: LogicalType) -> ColumnSchema {
103        ColumnSchema {
104            name: name.into(),
105            ty,
106            nullable: true,
107            default_value: None,
108            primary_key: false,
109        }
110    }
111
112    #[test]
113    fn none_columns_defaults_to_all_non_key() {
114        let src = TableSchema {
115            columns: vec![
116                col("id", LogicalType::Int),
117                col("a", LogicalType::String),
118                col("b", LogicalType::Boolean),
119            ],
120        };
121        let dst = TableSchema {
122            columns: vec![
123                col("id", LogicalType::Int),
124                col("a", LogicalType::String),
125                col("b", LogicalType::Boolean),
126            ],
127        };
128        let p = build_plan(&src, &dst, None, "id").unwrap();
129        assert_eq!(p.key.name, "id");
130        let names: Vec<&str> = p.columns.iter().map(|c| c.name.as_str()).collect();
131        assert_eq!(names, ["a", "b"]);
132    }
133
134    #[test]
135    fn default_column_missing_in_dst_is_error() {
136        let src = TableSchema {
137            columns: vec![col("id", LogicalType::Int), col("a", LogicalType::String)],
138        };
139        let dst = TableSchema {
140            columns: vec![col("id", LogicalType::Int)],
141        };
142        assert!(build_plan(&src, &dst, None, "id").is_err());
143    }
144
145    #[test]
146    fn mismatched_types_are_an_error() {
147        let src = TableSchema {
148            columns: vec![col("id", LogicalType::Int), col("a", LogicalType::String)],
149        };
150        let dst = TableSchema {
151            columns: vec![col("id", LogicalType::Int), col("a", LogicalType::Boolean)],
152        };
153        assert!(build_plan(&src, &dst, None, "id").is_err());
154    }
155}