#![allow(deprecated)] #![allow(clippy::expect_used)]
use super::{constraint::BoxedConstraint, Constraint, Level};
use crate::constraints::{
ApproxCountDistinctConstraint, Assertion, ColumnCountConstraint, CorrelationConstraint,
CustomSqlConstraint, DataTypeConstraint, FormatConstraint, FormatOptions, FormatType,
HistogramAssertion, HistogramConstraint, NullHandling, QuantileConstraint, SizeConstraint,
UniquenessConstraint, UniquenessOptions, UniquenessType,
};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Check {
name: String,
level: Level,
description: Option<String>,
constraints: Vec<Arc<dyn Constraint>>,
}
impl Check {
pub fn builder(name: impl Into<String>) -> CheckBuilder {
CheckBuilder::new(name)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn level(&self) -> Level {
self.level
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn constraints(&self) -> &[Arc<dyn Constraint>] {
&self.constraints
}
}
#[derive(Debug)]
pub struct CheckBuilder {
name: String,
level: Level,
description: Option<String>,
constraints: Vec<Arc<dyn Constraint>>,
}
impl CheckBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
level: Level::default(),
description: None,
constraints: Vec::new(),
}
}
pub fn level(mut self, level: Level) -> Self {
self.level = level;
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn constraint(mut self, constraint: impl Constraint + 'static) -> Self {
self.constraints.push(Arc::new(constraint));
self
}
pub fn boxed_constraint(mut self, constraint: BoxedConstraint) -> Self {
self.constraints.push(Arc::from(constraint));
self
}
pub fn arc_constraint(mut self, constraint: Arc<dyn Constraint>) -> Self {
self.constraints.push(constraint);
self
}
pub fn constraints<I>(mut self, constraints: I) -> Self
where
I: IntoIterator<Item = BoxedConstraint>,
{
self.constraints
.extend(constraints.into_iter().map(Arc::from));
self
}
pub fn has_size(mut self, assertion: Assertion) -> Self {
self.constraints
.push(Arc::new(SizeConstraint::new(assertion)));
self
}
pub fn has_column_count(mut self, assertion: Assertion) -> Self {
self.constraints
.push(Arc::new(ColumnCountConstraint::new(assertion)));
self
}
pub fn has_approx_count_distinct(
mut self,
column: impl Into<String>,
assertion: Assertion,
) -> Self {
self.constraints
.push(Arc::new(ApproxCountDistinctConstraint::new(
column, assertion,
)));
self
}
pub fn has_approx_quantile(
mut self,
column: impl Into<String>,
quantile: f64,
assertion: Assertion,
) -> Self {
self.constraints.push(Arc::new(
QuantileConstraint::percentile(column, quantile, assertion)
.expect("Invalid quantile parameters"),
));
self
}
pub fn has_mutual_information(
mut self,
column1: impl Into<String>,
column2: impl Into<String>,
assertion: Assertion,
) -> Self {
self.constraints.push(Arc::new(
CorrelationConstraint::mutual_information(column1, column2, 10, assertion)
.expect("Invalid mutual information parameters"),
));
self
}
pub fn has_correlation(
mut self,
column1: impl Into<String>,
column2: impl Into<String>,
assertion: Assertion,
) -> Self {
self.constraints.push(Arc::new(
CorrelationConstraint::pearson(column1, column2, assertion)
.expect("Invalid correlation parameters"),
));
self
}
pub fn has_min_length(mut self, column: impl Into<String>, min_length: usize) -> Self {
use crate::constraints::LengthConstraint;
self.constraints
.push(Arc::new(LengthConstraint::min(column, min_length)));
self
}
pub fn has_max_length(mut self, column: impl Into<String>, max_length: usize) -> Self {
use crate::constraints::LengthConstraint;
self.constraints
.push(Arc::new(LengthConstraint::max(column, max_length)));
self
}
pub fn has_length_between(
mut self,
column: impl Into<String>,
min_length: usize,
max_length: usize,
) -> Self {
use crate::constraints::LengthConstraint;
self.constraints.push(Arc::new(LengthConstraint::between(
column, min_length, max_length,
)));
self
}
pub fn has_exact_length(mut self, column: impl Into<String>, length: usize) -> Self {
use crate::constraints::LengthConstraint;
self.constraints
.push(Arc::new(LengthConstraint::exactly(column, length)));
self
}
pub fn is_not_empty(mut self, column: impl Into<String>) -> Self {
use crate::constraints::LengthConstraint;
self.constraints
.push(Arc::new(LengthConstraint::not_empty(column)));
self
}
pub fn has_consistent_data_type(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
DataTypeConstraint::type_consistency(column, threshold)
.expect("Invalid data type consistency parameters"),
));
self
}
pub fn satisfies(
mut self,
sql_expression: impl Into<String>,
hint: Option<impl Into<String>>,
) -> Self {
self.constraints.push(Arc::new(
CustomSqlConstraint::new(sql_expression, hint).expect("Invalid SQL expression"),
));
self
}
pub fn has_histogram(
mut self,
column: impl Into<String>,
assertion: HistogramAssertion,
) -> Self {
self.constraints
.push(Arc::new(HistogramConstraint::new(column, assertion)));
self
}
pub fn has_histogram_with_description(
mut self,
column: impl Into<String>,
assertion: HistogramAssertion,
description: impl Into<String>,
) -> Self {
self.constraints
.push(Arc::new(HistogramConstraint::new_with_description(
column,
assertion,
description,
)));
self
}
pub fn has_format(
mut self,
column: impl Into<String>,
format: FormatType,
threshold: f64,
options: FormatOptions,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::new(column, format, threshold, options)
.expect("Invalid column, format, threshold, or options"),
));
self
}
pub fn validates_regex(
mut self,
column: impl Into<String>,
pattern: impl Into<String>,
threshold: f64,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::regex(column, pattern, threshold)
.expect("Invalid column, pattern, or threshold"),
));
self
}
pub fn validates_email(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::email(column, threshold).expect("Invalid column or threshold"),
));
self
}
pub fn validates_url(
mut self,
column: impl Into<String>,
threshold: f64,
allow_localhost: bool,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::url(column, threshold, allow_localhost)
.expect("Invalid column or threshold"),
));
self
}
pub fn validates_credit_card(
mut self,
column: impl Into<String>,
threshold: f64,
detect_only: bool,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::credit_card(column, threshold, detect_only)
.expect("Invalid column or threshold"),
));
self
}
pub fn validates_phone(
mut self,
column: impl Into<String>,
threshold: f64,
country: Option<&str>,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::phone(column, threshold, country.map(|s| s.to_string()))
.expect("Invalid column or threshold"),
));
self
}
pub fn validates_postal_code(
mut self,
column: impl Into<String>,
threshold: f64,
country: &str,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::postal_code(column, threshold, country)
.expect("Invalid column or threshold"),
));
self
}
pub fn validates_uuid(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::uuid(column, threshold).expect("Invalid column or threshold"),
));
self
}
pub fn validates_ipv4(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::ipv4(column, threshold).expect("Invalid column or threshold"),
));
self
}
pub fn validates_ipv6(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::ipv6(column, threshold).expect("Invalid column or threshold"),
));
self
}
pub fn validates_json(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::json(column, threshold).expect("Invalid column or threshold"),
));
self
}
pub fn validates_iso8601_datetime(mut self, column: impl Into<String>, threshold: f64) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::iso8601_datetime(column, threshold)
.expect("Invalid column or threshold"),
));
self
}
pub fn validates_email_with_options(
mut self,
column: impl Into<String>,
threshold: f64,
options: FormatOptions,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::new(column, FormatType::Email, threshold, options)
.expect("Invalid column, threshold, or options"),
));
self
}
pub fn validates_url_with_options(
mut self,
column: impl Into<String>,
threshold: f64,
allow_localhost: bool,
options: FormatOptions,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::new(
column,
FormatType::Url { allow_localhost },
threshold,
options,
)
.expect("Invalid column, threshold, or options"),
));
self
}
pub fn validates_phone_with_options(
mut self,
column: impl Into<String>,
threshold: f64,
country: Option<String>,
options: FormatOptions,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::new(column, FormatType::Phone { country }, threshold, options)
.expect("Invalid column, threshold, or options"),
));
self
}
pub fn validates_regex_with_options(
mut self,
column: impl Into<String>,
pattern: impl Into<String>,
threshold: f64,
options: FormatOptions,
) -> Self {
self.constraints.push(Arc::new(
FormatConstraint::new(
column,
FormatType::Regex(pattern.into()),
threshold,
options,
)
.expect("Invalid column, pattern, threshold, or options"),
));
self
}
pub fn uniqueness<I, S>(
mut self,
columns: I,
uniqueness_type: UniquenessType,
options: UniquenessOptions,
) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(columns, uniqueness_type, options)
.expect("Invalid columns, uniqueness type, or options"),
));
self
}
pub fn validates_uniqueness<I, S>(mut self, columns: I, threshold: f64) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(
columns,
UniquenessType::FullUniqueness { threshold },
UniquenessOptions::default(),
)
.expect("Invalid columns or threshold"),
));
self
}
pub fn validates_distinctness<I, S>(mut self, columns: I, assertion: Assertion) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(
columns,
UniquenessType::Distinctness(assertion),
UniquenessOptions::default(),
)
.expect("Invalid columns"),
));
self
}
pub fn validates_unique_value_ratio<I, S>(mut self, columns: I, assertion: Assertion) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(
columns,
UniquenessType::UniqueValueRatio(assertion),
UniquenessOptions::default(),
)
.expect("Invalid columns"),
));
self
}
pub fn validates_primary_key<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(
columns,
UniquenessType::PrimaryKey,
UniquenessOptions::default(),
)
.expect("Invalid columns"),
));
self
}
pub fn validates_uniqueness_with_nulls<I, S>(
mut self,
columns: I,
threshold: f64,
null_handling: NullHandling,
) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.constraints.push(Arc::new(
UniquenessConstraint::new(
columns,
UniquenessType::UniqueWithNulls {
threshold,
null_handling,
},
UniquenessOptions::new().with_null_handling(null_handling),
)
.expect("Invalid columns or threshold"),
));
self
}
pub fn completeness(
mut self,
columns: impl Into<crate::core::ColumnSpec>,
options: crate::core::ConstraintOptions,
) -> Self {
use crate::constraints::CompletenessConstraint;
self.constraints
.push(Arc::new(CompletenessConstraint::new(columns, options)));
self
}
pub fn length(
mut self,
column: impl Into<String>,
assertion: crate::constraints::LengthAssertion,
) -> Self {
use crate::constraints::LengthConstraint;
self.constraints
.push(Arc::new(LengthConstraint::new(column, assertion)));
self
}
pub fn statistic(
mut self,
column: impl Into<String>,
statistic: crate::constraints::StatisticType,
assertion: Assertion,
) -> Self {
use crate::constraints::StatisticalConstraint;
self.constraints.push(Arc::new(
StatisticalConstraint::new(column, statistic, assertion)
.expect("Invalid column name or statistic"),
));
self
}
pub fn has_min(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(column, crate::constraints::StatisticType::Min, assertion)
}
pub fn has_max(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(column, crate::constraints::StatisticType::Max, assertion)
}
pub fn has_mean(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(column, crate::constraints::StatisticType::Mean, assertion)
}
pub fn has_sum(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(column, crate::constraints::StatisticType::Sum, assertion)
}
pub fn has_standard_deviation(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(
column,
crate::constraints::StatisticType::StandardDeviation,
assertion,
)
}
pub fn has_variance(self, column: impl Into<String>, assertion: Assertion) -> Self {
self.statistic(
column,
crate::constraints::StatisticType::Variance,
assertion,
)
}
pub fn foreign_key(
mut self,
child_column: impl Into<String>,
parent_column: impl Into<String>,
) -> Self {
use crate::constraints::ForeignKeyConstraint;
self.constraints.push(Arc::new(ForeignKeyConstraint::new(
child_column,
parent_column,
)));
self
}
pub fn cross_table_sum(
mut self,
left_column: impl Into<String>,
right_column: impl Into<String>,
) -> Self {
use crate::constraints::CrossTableSumConstraint;
self.constraints.push(Arc::new(CrossTableSumConstraint::new(
left_column,
right_column,
)));
self
}
pub fn join_coverage(
mut self,
left_table: impl Into<String>,
right_table: impl Into<String>,
) -> Self {
use crate::constraints::JoinCoverageConstraint;
self.constraints.push(Arc::new(JoinCoverageConstraint::new(
left_table,
right_table,
)));
self
}
pub fn temporal_ordering(mut self, table_name: impl Into<String>) -> Self {
use crate::constraints::TemporalOrderingConstraint;
self.constraints
.push(Arc::new(TemporalOrderingConstraint::new(table_name)));
self
}
pub fn with_constraint(mut self, constraint: impl crate::core::Constraint + 'static) -> Self {
self.constraints.push(Arc::new(constraint));
self
}
pub fn any_complete<I, S>(self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
use crate::core::{ConstraintOptions, LogicalOperator};
let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
self.completeness(
cols,
ConstraintOptions::new()
.with_operator(LogicalOperator::Any)
.with_threshold(1.0),
)
}
pub fn at_least_complete<I, S>(self, n: usize, columns: I, threshold: f64) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
use crate::core::{ConstraintOptions, LogicalOperator};
let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
self.completeness(
cols,
ConstraintOptions::new()
.with_operator(LogicalOperator::AtLeast(n))
.with_threshold(threshold),
)
}
pub fn exactly_complete<I, S>(self, n: usize, columns: I, threshold: f64) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
use crate::core::{ConstraintOptions, LogicalOperator};
let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
self.completeness(
cols,
ConstraintOptions::new()
.with_operator(LogicalOperator::Exactly(n))
.with_threshold(threshold),
)
}
pub fn build(self) -> Check {
Check {
name: self.name,
level: self.level,
description: self.description,
constraints: self.constraints,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use async_trait::async_trait;
use datafusion::prelude::*;
#[derive(Debug)]
struct DummyConstraint {
name: String,
}
#[async_trait]
impl Constraint for DummyConstraint {
async fn evaluate(&self, _ctx: &SessionContext) -> Result<crate::core::ConstraintResult> {
Ok(crate::core::ConstraintResult::success())
}
fn name(&self) -> &str {
&self.name
}
fn metadata(&self) -> crate::core::ConstraintMetadata {
crate::core::ConstraintMetadata::new()
}
}
#[test]
fn test_check_builder() {
let check = Check::builder("test_check")
.level(Level::Error)
.description("Test check description")
.constraint(DummyConstraint {
name: "constraint1".to_string(),
})
.build();
assert_eq!(check.name(), "test_check");
assert_eq!(check.level(), Level::Error);
assert_eq!(check.description(), Some("Test check description"));
assert_eq!(check.constraints().len(), 1);
}
#[test]
fn test_check_default_level() {
let check = Check::builder("test_check").build();
assert_eq!(check.level(), Level::Warning);
}
#[test]
fn test_check_builder_completeness() {
use crate::core::ConstraintOptions;
let check = Check::builder("completeness_check")
.level(Level::Error)
.completeness("user_id", ConstraintOptions::new().with_threshold(1.0))
.completeness("email", ConstraintOptions::new().with_threshold(0.95))
.build();
assert_eq!(check.name(), "completeness_check");
assert_eq!(check.level(), Level::Error);
assert_eq!(check.constraints().len(), 2);
}
#[test]
fn test_check_builder_uniqueness() {
let check = Check::builder("uniqueness_check")
.validates_uniqueness(vec!["user_id"], 1.0)
.validates_uniqueness(vec!["first_name", "last_name"], 1.0)
.validates_uniqueness(vec!["email"], 0.99)
.build();
assert_eq!(check.constraints().len(), 3);
}
#[test]
fn test_check_builder_method_chaining() {
use crate::core::ConstraintOptions;
let check = Check::builder("comprehensive_check")
.level(Level::Error)
.description("Comprehensive data quality check")
.completeness("id", ConstraintOptions::new().with_threshold(1.0))
.completeness("name", ConstraintOptions::new().with_threshold(0.9))
.validates_uniqueness(vec!["id"], 1.0)
.validates_uniqueness(vec!["email", "phone"], 1.0)
.build();
assert_eq!(check.name(), "comprehensive_check");
assert_eq!(check.level(), Level::Error);
assert_eq!(
check.description(),
Some("Comprehensive data quality check")
);
assert_eq!(check.constraints().len(), 4);
}
#[test]
fn test_check_builder_multiple_completeness() {
use crate::core::{ConstraintOptions, LogicalOperator};
let check = Check::builder("multi_completeness_check")
.completeness(
vec!["user_id", "email", "name"],
ConstraintOptions::new()
.with_operator(LogicalOperator::All)
.with_threshold(1.0),
)
.any_complete(vec!["phone", "mobile", "fax"])
.build();
assert_eq!(check.constraints().len(), 2);
}
#[test]
#[should_panic(expected = "Threshold must be between 0.0 and 1.0")]
fn test_check_builder_invalid_completeness_threshold() {
use crate::core::ConstraintOptions;
Check::builder("test")
.completeness("column", ConstraintOptions::new().with_threshold(1.5))
.build();
}
#[test]
#[should_panic(expected = "Invalid columns or threshold")]
fn test_check_builder_invalid_uniqueness_threshold() {
Check::builder("test")
.validates_uniqueness(vec!["column"], -0.1)
.build();
}
#[test]
fn test_check_builder_string_length() {
let check = Check::builder("string_length_check")
.has_min_length("password", 8)
.has_max_length("username", 20)
.build();
assert_eq!(check.constraints().len(), 2);
}
#[test]
fn test_unified_completeness_api() {
use crate::core::{ConstraintOptions, LogicalOperator};
let check = Check::builder("unified_completeness_test")
.completeness("email", ConstraintOptions::new().with_threshold(0.95))
.completeness(
vec!["phone", "email", "address"],
ConstraintOptions::new()
.with_operator(LogicalOperator::Any)
.with_threshold(1.0),
)
.completeness(
vec!["a", "b", "c", "d"],
ConstraintOptions::new()
.with_operator(LogicalOperator::AtLeast(2))
.with_threshold(0.9),
)
.build();
assert_eq!(check.constraints().len(), 3);
}
#[test]
fn test_unified_length_api() {
use crate::constraints::LengthAssertion;
let check = Check::builder("unified_length_test")
.length("password", LengthAssertion::Min(8))
.length("username", LengthAssertion::Between(3, 20))
.length("code", LengthAssertion::Exactly(6))
.length("name", LengthAssertion::NotEmpty)
.build();
assert_eq!(check.constraints().len(), 4);
}
#[test]
fn test_unified_statistics_api() {
use crate::constraints::{Assertion, StatisticType};
let check = Check::builder("unified_statistics_test")
.statistic(
"age",
StatisticType::Min,
Assertion::GreaterThanOrEqual(0.0),
)
.statistic("age", StatisticType::Max, Assertion::LessThanOrEqual(120.0))
.statistic(
"salary",
StatisticType::Mean,
Assertion::Between(50000.0, 100000.0),
)
.statistic(
"response_time",
StatisticType::StandardDeviation,
Assertion::LessThan(100.0),
)
.build();
assert_eq!(check.constraints().len(), 4);
}
#[test]
fn test_convenience_methods() {
let check = Check::builder("convenience_test")
.any_complete(vec!["phone", "email", "address"])
.at_least_complete(2, vec!["a", "b", "c", "d"], 0.9)
.exactly_complete(1, vec!["primary", "secondary"], 1.0)
.build();
assert_eq!(check.constraints().len(), 3);
}
#[test]
fn test_with_constraint_method() {
use crate::constraints::{LengthAssertion, LengthConstraint};
let constraint = LengthConstraint::new("test", LengthAssertion::Between(5, 50));
let check = Check::builder("with_constraint_test")
.with_constraint(constraint)
.build();
assert_eq!(check.constraints().len(), 1);
}
#[test]
fn test_enhanced_format_validation_methods() {
let check = Check::builder("enhanced_format_test")
.validates_email_with_options(
"email",
0.95,
FormatOptions::new()
.case_sensitive(false)
.trim_before_check(true)
.null_is_valid(false),
)
.validates_url_with_options(
"website",
0.90,
true, FormatOptions::new()
.case_sensitive(false)
.trim_before_check(true),
)
.validates_phone_with_options(
"phone",
0.95,
Some("US".to_string()),
FormatOptions::new().trim_before_check(true),
)
.validates_regex_with_options(
"product_code",
r"^[A-Z]{2}\d{4}$",
0.98,
FormatOptions::new()
.case_sensitive(false)
.trim_before_check(true),
)
.build();
assert_eq!(check.constraints().len(), 4);
assert_eq!(check.name(), "enhanced_format_test");
}
#[test]
fn test_enhanced_vs_basic_format_methods() {
let check = Check::builder("mixed_format_test")
.validates_email("basic_email", 0.90)
.validates_url("basic_url", 0.85, false)
.validates_phone("basic_phone", 0.80, None)
.validates_regex("basic_pattern", r"^\d+$", 0.75)
.validates_email_with_options(
"enhanced_email",
0.95,
FormatOptions::new().case_sensitive(false),
)
.validates_url_with_options(
"enhanced_url",
0.90,
true,
FormatOptions::new().trim_before_check(true),
)
.build();
assert_eq!(check.constraints().len(), 6);
}
#[test]
fn test_cross_table_sum_builder_method() {
let check = Check::builder("cross_table_sum_test")
.level(Level::Error)
.cross_table_sum("orders.total", "payments.amount")
.cross_table_sum("inventory.quantity", "transactions.quantity")
.build();
assert_eq!(check.constraints().len(), 2);
assert_eq!(check.name(), "cross_table_sum_test");
assert_eq!(check.level(), Level::Error);
for constraint in check.constraints() {
assert_eq!(constraint.name(), "cross_table_sum");
}
}
}