Skip to main content

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