Skip to main content

qubit_metadata/schema/
filter_validation.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Filter validation support for [`MetadataSchema`].
10
11use qubit_common::DataType;
12use qubit_value::Value;
13
14use super::metadata_field::MetadataField;
15use super::metadata_schema::MetadataSchema;
16use crate::{Condition, MetadataError, MetadataFilter, MetadataResult};
17
18impl MetadataSchema {
19    /// Validates a metadata filter against this schema.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error when the filter references an unknown field, uses a range
24    /// operator on a non-comparable field, or compares a field with an incompatible
25    /// value type.
26    pub fn validate_filter(&self, filter: &MetadataFilter) -> MetadataResult<()> {
27        filter.visit_conditions(|condition| self.validate_condition(condition))
28    }
29
30    /// Validates one filter condition against this schema.
31    fn validate_condition(&self, condition: &Condition) -> MetadataResult<()> {
32        match condition {
33            Condition::Equal { key, value } => self.validate_value_condition(key, "eq", value),
34            Condition::NotEqual { key, value } => self.validate_value_condition(key, "ne", value),
35            Condition::Less { key, value } => self.validate_range_condition(key, "lt", value),
36            Condition::LessEqual { key, value } => self.validate_range_condition(key, "le", value),
37            Condition::Greater { key, value } => self.validate_range_condition(key, "gt", value),
38            Condition::GreaterEqual { key, value } => {
39                self.validate_range_condition(key, "ge", value)
40            }
41            Condition::In { key, values } => {
42                for value in values {
43                    self.validate_value_condition(key, "in_set", value)?;
44                }
45                Ok(())
46            }
47            Condition::NotIn { key, values } => {
48                for value in values {
49                    self.validate_value_condition(key, "not_in_set", value)?;
50                }
51                Ok(())
52            }
53            Condition::Exists { key } | Condition::NotExists { key } => {
54                self.require_field(key)?;
55                Ok(())
56            }
57        }
58    }
59
60    /// Validates a non-range value condition.
61    fn validate_value_condition(
62        &self,
63        key: &str,
64        operator: &'static str,
65        value: &Value,
66    ) -> MetadataResult<()> {
67        let field = self.require_field(key)?;
68        if value_matches_field_type(value, field.data_type()) {
69            return Ok(());
70        }
71        Err(MetadataError::InvalidFilterOperator {
72            key: key.to_string(),
73            operator,
74            data_type: field.data_type(),
75            message: format!(
76                "filter value type {} is not compatible with field type {}",
77                value.data_type(),
78                field.data_type()
79            ),
80        })
81    }
82
83    /// Validates a range value condition.
84    fn validate_range_condition(
85        &self,
86        key: &str,
87        operator: &'static str,
88        value: &Value,
89    ) -> MetadataResult<()> {
90        let field = self.require_field(key)?;
91        if !is_range_comparable_type(field.data_type()) {
92            return Err(MetadataError::InvalidFilterOperator {
93                key: key.to_string(),
94                operator,
95                data_type: field.data_type(),
96                message: "range operators require a numeric or string field".to_string(),
97            });
98        }
99        if value_matches_field_type(value, field.data_type()) {
100            return Ok(());
101        }
102        Err(MetadataError::InvalidFilterOperator {
103            key: key.to_string(),
104            operator,
105            data_type: field.data_type(),
106            message: format!(
107                "filter value type {} is not compatible with field type {}",
108                value.data_type(),
109                field.data_type()
110            ),
111        })
112    }
113
114    /// Returns the field for `key` or a schema error if it is unknown.
115    fn require_field(&self, key: &str) -> MetadataResult<&MetadataField> {
116        self.field(key)
117            .ok_or_else(|| MetadataError::UnknownFilterField {
118                key: key.to_string(),
119            })
120    }
121}
122
123/// Returns `true` when `data_type` is numeric.
124#[inline]
125fn is_numeric_data_type(data_type: DataType) -> bool {
126    matches!(
127        data_type,
128        DataType::Int8
129            | DataType::Int16
130            | DataType::Int32
131            | DataType::Int64
132            | DataType::Int128
133            | DataType::UInt8
134            | DataType::UInt16
135            | DataType::UInt32
136            | DataType::UInt64
137            | DataType::UInt128
138            | DataType::Float32
139            | DataType::Float64
140            | DataType::BigInteger
141            | DataType::BigDecimal
142            | DataType::IntSize
143            | DataType::UIntSize
144    )
145}
146
147/// Returns `true` when `data_type` supports range comparisons.
148#[inline]
149fn is_range_comparable_type(data_type: DataType) -> bool {
150    is_numeric_data_type(data_type) || matches!(data_type, DataType::String)
151}
152
153/// Returns `true` when a filter value is compatible with a schema field type.
154#[inline]
155fn value_matches_field_type(value: &Value, field_type: DataType) -> bool {
156    let value_type = value.data_type();
157    value_type == field_type || is_numeric_data_type(value_type) && is_numeric_data_type(field_type)
158}