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