Skip to main content

qubit_metadata/
condition.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! A single comparison predicate against one metadata key.
10
11use std::cmp::Ordering;
12
13use qubit_value::Value;
14use serde::{Deserialize, Serialize};
15
16use crate::{Metadata, MissingKeyPolicy, NumberComparisonPolicy};
17
18/// A single comparison operator applied to one metadata key.
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub enum Condition {
21    /// Key equals value.
22    Equal {
23        /// The metadata key.
24        key: String,
25        /// The expected value.
26        value: Value,
27    },
28    /// Key does not equal value.
29    NotEqual {
30        /// The metadata key.
31        key: String,
32        /// The value to compare against.
33        value: Value,
34    },
35    /// Key is less than value.
36    Less {
37        /// The metadata key.
38        key: String,
39        /// The upper bound (exclusive).
40        value: Value,
41    },
42    /// Key is less than or equal to value.
43    LessEqual {
44        /// The metadata key.
45        key: String,
46        /// The upper bound (inclusive).
47        value: Value,
48    },
49    /// Key is greater than value.
50    Greater {
51        /// The metadata key.
52        key: String,
53        /// The lower bound (exclusive).
54        value: Value,
55    },
56    /// Key is greater than or equal to value.
57    GreaterEqual {
58        /// The metadata key.
59        key: String,
60        /// The lower bound (inclusive).
61        value: Value,
62    },
63    /// The stored value is one of the listed candidates.
64    In {
65        /// The metadata key.
66        key: String,
67        /// The set of acceptable values.
68        values: Vec<Value>,
69    },
70    /// The stored value is not any of the listed candidates.
71    NotIn {
72        /// The metadata key.
73        key: String,
74        /// The set of excluded values.
75        values: Vec<Value>,
76    },
77    /// Key exists in the metadata regardless of its value.
78    Exists {
79        /// The metadata key.
80        key: String,
81    },
82    /// Key does not exist in the metadata.
83    NotExists {
84        /// The metadata key.
85        key: String,
86    },
87}
88
89impl Condition {
90    /// Evaluates this condition against `meta` using the supplied policies.
91    #[inline]
92    pub(crate) fn matches(
93        &self,
94        meta: &Metadata,
95        missing_key_policy: MissingKeyPolicy,
96        number_comparison_policy: NumberComparisonPolicy,
97    ) -> bool {
98        match self {
99            Condition::Equal { key, value } => meta
100                .get_raw(key)
101                .is_some_and(|stored| values_equal(stored, value, number_comparison_policy)),
102            Condition::NotEqual { key, value } => match meta.get_raw(key) {
103                Some(stored) => !values_equal(stored, value, number_comparison_policy),
104                None => missing_key_policy.matches_negative_predicates(),
105            },
106            Condition::Less { key, value } => meta.get_raw(key).is_some_and(|stored| {
107                compare_values(stored, value, number_comparison_policy) == Some(Ordering::Less)
108            }),
109            Condition::LessEqual { key, value } => meta.get_raw(key).is_some_and(|stored| {
110                matches!(
111                    compare_values(stored, value, number_comparison_policy),
112                    Some(Ordering::Less) | Some(Ordering::Equal)
113                )
114            }),
115            Condition::Greater { key, value } => meta.get_raw(key).is_some_and(|stored| {
116                compare_values(stored, value, number_comparison_policy) == Some(Ordering::Greater)
117            }),
118            Condition::GreaterEqual { key, value } => meta.get_raw(key).is_some_and(|stored| {
119                matches!(
120                    compare_values(stored, value, number_comparison_policy),
121                    Some(Ordering::Greater) | Some(Ordering::Equal)
122                )
123            }),
124            Condition::In { key, values } => meta.get_raw(key).is_some_and(|stored| {
125                values
126                    .iter()
127                    .any(|value| values_equal(stored, value, number_comparison_policy))
128            }),
129            Condition::NotIn { key, values } => match meta.get_raw(key) {
130                Some(stored) => values
131                    .iter()
132                    .all(|value| !values_equal(stored, value, number_comparison_policy)),
133                None => missing_key_policy.matches_negative_predicates(),
134            },
135            Condition::Exists { key } => meta.contains_key(key),
136            Condition::NotExists { key } => !meta.contains_key(key),
137        }
138    }
139}
140
141/// Compares two values for equality, treating numeric variants by numeric value.
142#[inline]
143fn values_equal(a: &Value, b: &Value, number_comparison_policy: NumberComparisonPolicy) -> bool {
144    if is_numeric_value(a) && is_numeric_value(b) {
145        return compare_numbers(a, b, number_comparison_policy) == Some(Ordering::Equal);
146    }
147    a == b
148}
149
150/// Compares two values where both are compatible numeric or string variants.
151#[inline]
152fn compare_values(
153    a: &Value,
154    b: &Value,
155    number_comparison_policy: NumberComparisonPolicy,
156) -> Option<Ordering> {
157    if is_numeric_value(a) && is_numeric_value(b) {
158        return compare_numbers(a, b, number_comparison_policy);
159    }
160    match (a, b) {
161        (Value::String(x), Value::String(y)) => x.partial_cmp(y),
162        _ => None,
163    }
164}
165
166/// Internal normalized representation for scalar numeric comparisons.
167#[derive(Debug, Clone, Copy)]
168enum NumberValue {
169    /// Signed integer value.
170    Signed(i128),
171    /// Unsigned integer value.
172    Unsigned(u128),
173    /// Floating-point value.
174    Float(f64),
175}
176
177/// Returns `true` when `value` is one of the numeric `Value` variants.
178#[inline]
179fn is_numeric_value(value: &Value) -> bool {
180    matches!(
181        value,
182        Value::Int8(_)
183            | Value::Int16(_)
184            | Value::Int32(_)
185            | Value::Int64(_)
186            | Value::Int128(_)
187            | Value::UInt8(_)
188            | Value::UInt16(_)
189            | Value::UInt32(_)
190            | Value::UInt64(_)
191            | Value::UInt128(_)
192            | Value::IntSize(_)
193            | Value::UIntSize(_)
194            | Value::Float32(_)
195            | Value::Float64(_)
196            | Value::BigInteger(_)
197            | Value::BigDecimal(_)
198    )
199}
200
201/// Converts a `Value` into the normalized numeric representation when supported.
202#[inline]
203fn number_value(value: &Value, policy: NumberComparisonPolicy) -> Option<NumberValue> {
204    match value {
205        Value::Int8(v) => Some(NumberValue::Signed(i128::from(*v))),
206        Value::Int16(v) => Some(NumberValue::Signed(i128::from(*v))),
207        Value::Int32(v) => Some(NumberValue::Signed(i128::from(*v))),
208        Value::Int64(v) => Some(NumberValue::Signed(i128::from(*v))),
209        Value::Int128(v) => Some(NumberValue::Signed(*v)),
210        Value::UInt8(v) => Some(NumberValue::Unsigned(u128::from(*v))),
211        Value::UInt16(v) => Some(NumberValue::Unsigned(u128::from(*v))),
212        Value::UInt32(v) => Some(NumberValue::Unsigned(u128::from(*v))),
213        Value::UInt64(v) => Some(NumberValue::Unsigned(u128::from(*v))),
214        Value::UInt128(v) => Some(NumberValue::Unsigned(*v)),
215        Value::IntSize(v) => Some(NumberValue::Signed(*v as i128)),
216        Value::UIntSize(v) => Some(NumberValue::Unsigned(*v as u128)),
217        Value::Float32(v) => Some(NumberValue::Float(f64::from(*v))),
218        Value::Float64(v) => Some(NumberValue::Float(*v)),
219        Value::BigInteger(_) | Value::BigDecimal(_)
220            if matches!(policy, NumberComparisonPolicy::Approximate) =>
221        {
222            value.to::<f64>().ok().map(NumberValue::Float)
223        }
224        _ => None,
225    }
226}
227
228/// Compares two numeric `Value` variants with the configured precision policy.
229#[inline]
230fn compare_numbers(
231    a: &Value,
232    b: &Value,
233    number_comparison_policy: NumberComparisonPolicy,
234) -> Option<Ordering> {
235    match (
236        number_value(a, number_comparison_policy)?,
237        number_value(b, number_comparison_policy)?,
238    ) {
239        (NumberValue::Signed(x), NumberValue::Signed(y)) => Some(x.cmp(&y)),
240        (NumberValue::Unsigned(x), NumberValue::Unsigned(y)) => Some(x.cmp(&y)),
241        (NumberValue::Signed(x), NumberValue::Unsigned(y)) => Some(compare_i128_u128(x, y)),
242        (NumberValue::Unsigned(x), NumberValue::Signed(y)) => {
243            Some(compare_i128_u128(y, x).reverse())
244        }
245        (NumberValue::Signed(x), NumberValue::Float(y)) => {
246            compare_i128_f64(x, y, number_comparison_policy)
247        }
248        (NumberValue::Float(x), NumberValue::Signed(y)) => {
249            compare_i128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
250        }
251        (NumberValue::Unsigned(x), NumberValue::Float(y)) => {
252            compare_u128_f64(x, y, number_comparison_policy)
253        }
254        (NumberValue::Float(x), NumberValue::Unsigned(y)) => {
255            compare_u128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
256        }
257        (NumberValue::Float(x), NumberValue::Float(y)) => x.partial_cmp(&y),
258    }
259}
260
261const MAX_SAFE_INTEGER_F64_U64: u64 = 9_007_199_254_740_992;
262const I64_MIN_F64: f64 = -9_223_372_036_854_775_808.0;
263const I64_EXCLUSIVE_MAX_F64: f64 = 9_223_372_036_854_775_808.0;
264const U64_EXCLUSIVE_MAX_F64: f64 = 18_446_744_073_709_551_616.0;
265
266/// Compares a signed integer and an unsigned integer without lossy casts.
267#[inline]
268fn compare_i128_u128(x: i128, y: u128) -> Ordering {
269    if x < 0 {
270        Ordering::Less
271    } else {
272        (x as u128).cmp(&y)
273    }
274}
275
276/// Compares a signed integer and a float, returning `None` for risky cases.
277fn compare_i128_f64(
278    x: i128,
279    y: f64,
280    number_comparison_policy: NumberComparisonPolicy,
281) -> Option<Ordering> {
282    if let Ok(x64) = i64::try_from(x) {
283        return compare_i64_f64(x64, y, number_comparison_policy);
284    }
285    if matches!(
286        number_comparison_policy,
287        NumberComparisonPolicy::Approximate
288    ) {
289        return (x as f64).partial_cmp(&y);
290    }
291    None
292}
293
294/// Compares an unsigned integer and a float, returning `None` for risky cases.
295fn compare_u128_f64(
296    x: u128,
297    y: f64,
298    number_comparison_policy: NumberComparisonPolicy,
299) -> Option<Ordering> {
300    if let Ok(x64) = u64::try_from(x) {
301        return compare_u64_f64(x64, y, number_comparison_policy);
302    }
303    if matches!(
304        number_comparison_policy,
305        NumberComparisonPolicy::Approximate
306    ) {
307        return (x as f64).partial_cmp(&y);
308    }
309    None
310}
311
312/// Compares an `i64` and a float using the conservative JSON-era rules.
313fn compare_i64_f64(
314    x: i64,
315    y: f64,
316    number_comparison_policy: NumberComparisonPolicy,
317) -> Option<Ordering> {
318    if y.fract() == 0.0 && (I64_MIN_F64..I64_EXCLUSIVE_MAX_F64).contains(&y) {
319        return Some(x.cmp(&(y as i64)));
320    }
321
322    if x.unsigned_abs() <= MAX_SAFE_INTEGER_F64_U64 {
323        return (x as f64).partial_cmp(&y);
324    }
325
326    if matches!(
327        number_comparison_policy,
328        NumberComparisonPolicy::Approximate
329    ) {
330        return (x as f64).partial_cmp(&y);
331    }
332
333    None
334}
335
336/// Compares a `u64` and a float using the conservative JSON-era rules.
337fn compare_u64_f64(
338    x: u64,
339    y: f64,
340    number_comparison_policy: NumberComparisonPolicy,
341) -> Option<Ordering> {
342    if y < 0.0 {
343        return Some(Ordering::Greater);
344    }
345
346    if y.fract() == 0.0 && (0.0..U64_EXCLUSIVE_MAX_F64).contains(&y) {
347        return Some(x.cmp(&(y as u64)));
348    }
349
350    if x <= MAX_SAFE_INTEGER_F64_U64 {
351        return (x as f64).partial_cmp(&y);
352    }
353
354    if matches!(
355        number_comparison_policy,
356        NumberComparisonPolicy::Approximate
357    ) {
358        return (x as f64).partial_cmp(&y);
359    }
360
361    None
362}