Skip to main content

qubit_metadata/schema/
filter_validation.rs

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