Skip to main content

qubit_metadata/filter/
condition.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//! A single comparison predicate against one metadata key.
11
12use std::cmp::Ordering;
13
14use bigdecimal::BigDecimal;
15use num_bigint::BigInt;
16use qubit_value::Value;
17use serde::{
18    Deserialize,
19    Deserializer,
20    Serialize,
21    Serializer,
22};
23
24use super::missing_key_policy::MissingKeyPolicy;
25use super::number_comparison_policy::NumberComparisonPolicy;
26use super::wire::ConditionWire;
27use crate::Metadata;
28
29/// A single comparison operator applied to one metadata key.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Condition {
32    /// Key equals value.
33    Equal {
34        /// The metadata key.
35        key: String,
36        /// The expected value.
37        value: Value,
38    },
39    /// Key does not equal value.
40    NotEqual {
41        /// The metadata key.
42        key: String,
43        /// The value to compare against.
44        value: Value,
45    },
46    /// Key is less than value.
47    Less {
48        /// The metadata key.
49        key: String,
50        /// The upper bound (exclusive).
51        value: Value,
52    },
53    /// Key is less than or equal to value.
54    LessEqual {
55        /// The metadata key.
56        key: String,
57        /// The upper bound (inclusive).
58        value: Value,
59    },
60    /// Key is greater than value.
61    Greater {
62        /// The metadata key.
63        key: String,
64        /// The lower bound (exclusive).
65        value: Value,
66    },
67    /// Key is greater than or equal to value.
68    GreaterEqual {
69        /// The metadata key.
70        key: String,
71        /// The lower bound (inclusive).
72        value: Value,
73    },
74    /// The stored value is one of the listed candidates.
75    In {
76        /// The metadata key.
77        key: String,
78        /// The set of acceptable values.
79        values: Vec<Value>,
80    },
81    /// The stored value is not any of the listed candidates.
82    NotIn {
83        /// The metadata key.
84        key: String,
85        /// The set of excluded values.
86        values: Vec<Value>,
87    },
88    /// Key exists in the metadata regardless of its value.
89    Exists {
90        /// The metadata key.
91        key: String,
92    },
93    /// Key does not exist in the metadata.
94    NotExists {
95        /// The metadata key.
96        key: String,
97    },
98}
99
100impl Serialize for Condition {
101    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        ConditionWire::from(self).serialize(serializer)
106    }
107}
108
109impl<'de> Deserialize<'de> for Condition {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: Deserializer<'de>,
113    {
114        Ok(ConditionWire::deserialize(deserializer)?.into_condition())
115    }
116}
117
118impl Condition {
119    /// Evaluates this condition against `meta` using the supplied policies.
120    #[inline]
121    pub(crate) fn matches(
122        &self,
123        meta: &Metadata,
124        missing_key_policy: MissingKeyPolicy,
125        number_comparison_policy: NumberComparisonPolicy,
126    ) -> bool {
127        match self {
128            Condition::Equal { key, value } => meta
129                .get_raw(key)
130                .is_some_and(|stored| values_equal(stored, value, number_comparison_policy)),
131            Condition::NotEqual { key, value } => match meta.get_raw(key) {
132                Some(stored) => !values_equal(stored, value, number_comparison_policy),
133                None => missing_key_policy.matches_negative_predicates(),
134            },
135            Condition::Less { key, value } => meta.get_raw(key).is_some_and(|stored| {
136                compare_values(stored, value, number_comparison_policy) == Some(Ordering::Less)
137            }),
138            Condition::LessEqual { key, value } => meta.get_raw(key).is_some_and(|stored| {
139                matches!(
140                    compare_values(stored, value, number_comparison_policy),
141                    Some(Ordering::Less) | Some(Ordering::Equal)
142                )
143            }),
144            Condition::Greater { key, value } => meta.get_raw(key).is_some_and(|stored| {
145                compare_values(stored, value, number_comparison_policy) == Some(Ordering::Greater)
146            }),
147            Condition::GreaterEqual { key, value } => meta.get_raw(key).is_some_and(|stored| {
148                matches!(
149                    compare_values(stored, value, number_comparison_policy),
150                    Some(Ordering::Greater) | Some(Ordering::Equal)
151                )
152            }),
153            Condition::In { key, values } => meta.get_raw(key).is_some_and(|stored| {
154                values
155                    .iter()
156                    .any(|value| values_equal(stored, value, number_comparison_policy))
157            }),
158            Condition::NotIn { key, values } => match meta.get_raw(key) {
159                Some(stored) => values
160                    .iter()
161                    .all(|value| !values_equal(stored, value, number_comparison_policy)),
162                None => missing_key_policy.matches_negative_predicates(),
163            },
164            Condition::Exists { key } => meta.contains_key(key),
165            Condition::NotExists { key } => !meta.contains_key(key),
166        }
167    }
168}
169
170/// Compares two values for equality, treating numeric variants by numeric value.
171#[inline]
172fn values_equal(a: &Value, b: &Value, number_comparison_policy: NumberComparisonPolicy) -> bool {
173    if is_numeric_value(a) && is_numeric_value(b) {
174        return compare_numbers(a, b, number_comparison_policy) == Some(Ordering::Equal);
175    }
176    a == b
177}
178
179/// Compares two values where both are compatible numeric or string variants.
180#[inline]
181fn compare_values(
182    a: &Value,
183    b: &Value,
184    number_comparison_policy: NumberComparisonPolicy,
185) -> Option<Ordering> {
186    if is_numeric_value(a) && is_numeric_value(b) {
187        return compare_numbers(a, b, number_comparison_policy);
188    }
189    match (a, b) {
190        (Value::String(x), Value::String(y)) => x.partial_cmp(y),
191        _ => None,
192    }
193}
194
195/// Internal normalized representation for scalar numeric comparisons.
196#[derive(Debug, Clone, Copy)]
197enum NumberValue {
198    /// Signed integer value.
199    Signed(i128),
200    /// Unsigned integer value.
201    Unsigned(u128),
202    /// Floating-point value.
203    Float(f64),
204}
205
206/// Returns `true` when `value` is one of the numeric `Value` variants.
207#[inline]
208fn is_numeric_value(value: &Value) -> bool {
209    matches!(
210        value,
211        Value::Int8(_)
212            | Value::Int16(_)
213            | Value::Int32(_)
214            | Value::Int64(_)
215            | Value::Int128(_)
216            | Value::UInt8(_)
217            | Value::UInt16(_)
218            | Value::UInt32(_)
219            | Value::UInt64(_)
220            | Value::UInt128(_)
221            | Value::IntSize(_)
222            | Value::UIntSize(_)
223            | Value::Float32(_)
224            | Value::Float64(_)
225            | Value::BigInteger(_)
226            | Value::BigDecimal(_)
227    )
228}
229
230/// Converts a `Value` into the normalized numeric representation when supported.
231#[inline]
232fn number_value(value: &Value) -> Option<NumberValue> {
233    match value {
234        Value::Int8(v) => Some(NumberValue::Signed(i128::from(*v))),
235        Value::Int16(v) => Some(NumberValue::Signed(i128::from(*v))),
236        Value::Int32(v) => Some(NumberValue::Signed(i128::from(*v))),
237        Value::Int64(v) => Some(NumberValue::Signed(i128::from(*v))),
238        Value::Int128(v) => Some(NumberValue::Signed(*v)),
239        Value::UInt8(v) => Some(NumberValue::Unsigned(u128::from(*v))),
240        Value::UInt16(v) => Some(NumberValue::Unsigned(u128::from(*v))),
241        Value::UInt32(v) => Some(NumberValue::Unsigned(u128::from(*v))),
242        Value::UInt64(v) => Some(NumberValue::Unsigned(u128::from(*v))),
243        Value::UInt128(v) => Some(NumberValue::Unsigned(*v)),
244        Value::IntSize(v) => Some(NumberValue::Signed(*v as i128)),
245        Value::UIntSize(v) => Some(NumberValue::Unsigned(*v as u128)),
246        Value::Float32(v) => Some(NumberValue::Float(f64::from(*v))),
247        Value::Float64(v) => Some(NumberValue::Float(*v)),
248        _ => None,
249    }
250}
251
252/// Compares two numeric `Value` variants with the configured precision policy.
253#[inline]
254fn compare_numbers(
255    a: &Value,
256    b: &Value,
257    number_comparison_policy: NumberComparisonPolicy,
258) -> Option<Ordering> {
259    if contains_big_number(a, b) {
260        return compare_big_numbers(a, b, number_comparison_policy);
261    }
262    match (number_value(a)?, number_value(b)?) {
263        (NumberValue::Signed(x), NumberValue::Signed(y)) => Some(x.cmp(&y)),
264        (NumberValue::Unsigned(x), NumberValue::Unsigned(y)) => Some(x.cmp(&y)),
265        (NumberValue::Signed(x), NumberValue::Unsigned(y)) => Some(compare_i128_u128(x, y)),
266        (NumberValue::Unsigned(x), NumberValue::Signed(y)) => {
267            Some(compare_i128_u128(y, x).reverse())
268        }
269        (NumberValue::Signed(x), NumberValue::Float(y)) => {
270            compare_i128_f64(x, y, number_comparison_policy)
271        }
272        (NumberValue::Float(x), NumberValue::Signed(y)) => {
273            compare_i128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
274        }
275        (NumberValue::Unsigned(x), NumberValue::Float(y)) => {
276            compare_u128_f64(x, y, number_comparison_policy)
277        }
278        (NumberValue::Float(x), NumberValue::Unsigned(y)) => {
279            compare_u128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
280        }
281        (NumberValue::Float(x), NumberValue::Float(y)) => x.partial_cmp(&y),
282    }
283}
284
285/// Returns `true` if either value is a big-number variant.
286#[inline]
287fn contains_big_number(a: &Value, b: &Value) -> bool {
288    matches!(a, Value::BigInteger(_) | Value::BigDecimal(_))
289        || matches!(b, Value::BigInteger(_) | Value::BigDecimal(_))
290}
291
292/// Compares values when at least one side is `BigInteger` or `BigDecimal`.
293fn compare_big_numbers(
294    a: &Value,
295    b: &Value,
296    number_comparison_policy: NumberComparisonPolicy,
297) -> Option<Ordering> {
298    if let (Some(x), Some(y)) = (big_integer_value(a), big_integer_value(b)) {
299        return Some(x.cmp(&y));
300    }
301    if let (Some(x), Some(y)) = (big_decimal_value(a), big_decimal_value(b)) {
302        return Some(x.cmp(&y));
303    }
304    if matches!(
305        number_comparison_policy,
306        NumberComparisonPolicy::Approximate
307    ) {
308        return compare_as_f64(a, b);
309    }
310    None
311}
312
313/// Converts integral numeric values to `BigInt` for exact comparison.
314fn big_integer_value(value: &Value) -> Option<BigInt> {
315    match value {
316        Value::Int8(v) => Some(BigInt::from(*v)),
317        Value::Int16(v) => Some(BigInt::from(*v)),
318        Value::Int32(v) => Some(BigInt::from(*v)),
319        Value::Int64(v) => Some(BigInt::from(*v)),
320        Value::Int128(v) => Some(BigInt::from(*v)),
321        Value::UInt8(v) => Some(BigInt::from(*v)),
322        Value::UInt16(v) => Some(BigInt::from(*v)),
323        Value::UInt32(v) => Some(BigInt::from(*v)),
324        Value::UInt64(v) => Some(BigInt::from(*v)),
325        Value::UInt128(v) => Some(BigInt::from(*v)),
326        Value::IntSize(v) => Some(BigInt::from(*v)),
327        Value::UIntSize(v) => Some(BigInt::from(*v)),
328        Value::BigInteger(v) => Some(v.clone()),
329        _ => None,
330    }
331}
332
333/// Converts integral and decimal numeric values to `BigDecimal`.
334fn big_decimal_value(value: &Value) -> Option<BigDecimal> {
335    match value {
336        Value::BigDecimal(v) => Some(v.clone()),
337        _ => big_integer_value(value).map(BigDecimal::from),
338    }
339}
340
341/// Compares two numeric values through the approximate `f64` fallback.
342#[inline]
343fn compare_as_f64(a: &Value, b: &Value) -> Option<Ordering> {
344    a.to::<f64>().ok()?.partial_cmp(&b.to::<f64>().ok()?)
345}
346
347const MAX_SAFE_INTEGER_F64_U64: u64 = 9_007_199_254_740_992;
348const I64_MIN_F64: f64 = -9_223_372_036_854_775_808.0;
349const I64_EXCLUSIVE_MAX_F64: f64 = 9_223_372_036_854_775_808.0;
350const U64_EXCLUSIVE_MAX_F64: f64 = 18_446_744_073_709_551_616.0;
351
352/// Compares a signed integer and an unsigned integer without lossy casts.
353#[inline]
354fn compare_i128_u128(x: i128, y: u128) -> Ordering {
355    if x < 0 {
356        Ordering::Less
357    } else {
358        (x as u128).cmp(&y)
359    }
360}
361
362/// Compares a signed integer and a float, returning `None` for risky cases.
363fn compare_i128_f64(
364    x: i128,
365    y: f64,
366    number_comparison_policy: NumberComparisonPolicy,
367) -> Option<Ordering> {
368    if let Ok(x64) = i64::try_from(x) {
369        return compare_i64_f64(x64, y, number_comparison_policy);
370    }
371    if matches!(
372        number_comparison_policy,
373        NumberComparisonPolicy::Approximate
374    ) {
375        return (x as f64).partial_cmp(&y);
376    }
377    None
378}
379
380/// Compares an unsigned integer and a float, returning `None` for risky cases.
381fn compare_u128_f64(
382    x: u128,
383    y: f64,
384    number_comparison_policy: NumberComparisonPolicy,
385) -> Option<Ordering> {
386    if let Ok(x64) = u64::try_from(x) {
387        return compare_u64_f64(x64, y, number_comparison_policy);
388    }
389    if matches!(
390        number_comparison_policy,
391        NumberComparisonPolicy::Approximate
392    ) {
393        return (x as f64).partial_cmp(&y);
394    }
395    None
396}
397
398/// Compares an `i64` and a float using the conservative JSON-era rules.
399fn compare_i64_f64(
400    x: i64,
401    y: f64,
402    number_comparison_policy: NumberComparisonPolicy,
403) -> Option<Ordering> {
404    if y.fract() == 0.0 && (I64_MIN_F64..I64_EXCLUSIVE_MAX_F64).contains(&y) {
405        return Some(x.cmp(&(y as i64)));
406    }
407
408    if x.unsigned_abs() <= MAX_SAFE_INTEGER_F64_U64 {
409        return (x as f64).partial_cmp(&y);
410    }
411
412    if matches!(
413        number_comparison_policy,
414        NumberComparisonPolicy::Approximate
415    ) {
416        return (x as f64).partial_cmp(&y);
417    }
418
419    None
420}
421
422/// Compares a `u64` and a float using the conservative JSON-era rules.
423fn compare_u64_f64(
424    x: u64,
425    y: f64,
426    number_comparison_policy: NumberComparisonPolicy,
427) -> Option<Ordering> {
428    if y < 0.0 {
429        return Some(Ordering::Greater);
430    }
431
432    if y.fract() == 0.0 && (0.0..U64_EXCLUSIVE_MAX_F64).contains(&y) {
433        return Some(x.cmp(&(y as u64)));
434    }
435
436    if x <= MAX_SAFE_INTEGER_F64_U64 {
437        return (x as f64).partial_cmp(&y);
438    }
439
440    if matches!(
441        number_comparison_policy,
442        NumberComparisonPolicy::Approximate
443    ) {
444        return (x as f64).partial_cmp(&y);
445    }
446
447    None
448}