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
136                .get_raw(key)
137                .is_some_and(|stored| compare_values(stored, value, number_comparison_policy) == Some(Ordering::Less)),
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(a: &Value, b: &Value, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
182    if is_numeric_value(a) && is_numeric_value(b) {
183        return compare_numbers(a, b, number_comparison_policy);
184    }
185    match (a, b) {
186        (Value::String(x), Value::String(y)) => x.partial_cmp(y),
187        _ => None,
188    }
189}
190
191/// Internal normalized representation for scalar numeric comparisons.
192#[derive(Debug, Clone, Copy)]
193enum NumberValue {
194    /// Signed integer value.
195    Signed(i128),
196    /// Unsigned integer value.
197    Unsigned(u128),
198    /// Floating-point value.
199    Float(f64),
200}
201
202/// Returns `true` when `value` is one of the numeric `Value` variants.
203#[inline]
204fn is_numeric_value(value: &Value) -> bool {
205    matches!(
206        value,
207        Value::Int8(_)
208            | Value::Int16(_)
209            | Value::Int32(_)
210            | Value::Int64(_)
211            | Value::Int128(_)
212            | Value::UInt8(_)
213            | Value::UInt16(_)
214            | Value::UInt32(_)
215            | Value::UInt64(_)
216            | Value::UInt128(_)
217            | Value::IntSize(_)
218            | Value::UIntSize(_)
219            | Value::Float32(_)
220            | Value::Float64(_)
221            | Value::BigInteger(_)
222            | Value::BigDecimal(_)
223    )
224}
225
226/// Converts a `Value` into the normalized numeric representation when supported.
227#[inline]
228fn number_value(value: &Value) -> Option<NumberValue> {
229    match value {
230        Value::Int8(v) => Some(NumberValue::Signed(i128::from(*v))),
231        Value::Int16(v) => Some(NumberValue::Signed(i128::from(*v))),
232        Value::Int32(v) => Some(NumberValue::Signed(i128::from(*v))),
233        Value::Int64(v) => Some(NumberValue::Signed(i128::from(*v))),
234        Value::Int128(v) => Some(NumberValue::Signed(*v)),
235        Value::UInt8(v) => Some(NumberValue::Unsigned(u128::from(*v))),
236        Value::UInt16(v) => Some(NumberValue::Unsigned(u128::from(*v))),
237        Value::UInt32(v) => Some(NumberValue::Unsigned(u128::from(*v))),
238        Value::UInt64(v) => Some(NumberValue::Unsigned(u128::from(*v))),
239        Value::UInt128(v) => Some(NumberValue::Unsigned(*v)),
240        Value::IntSize(v) => Some(NumberValue::Signed(*v as i128)),
241        Value::UIntSize(v) => Some(NumberValue::Unsigned(*v as u128)),
242        Value::Float32(v) => Some(NumberValue::Float(f64::from(*v))),
243        Value::Float64(v) => Some(NumberValue::Float(*v)),
244        _ => None,
245    }
246}
247
248/// Compares two numeric `Value` variants with the configured precision policy.
249#[inline]
250fn compare_numbers(a: &Value, b: &Value, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
251    if contains_big_number(a, b) {
252        return compare_big_numbers(a, b, number_comparison_policy);
253    }
254    match (number_value(a)?, number_value(b)?) {
255        (NumberValue::Signed(x), NumberValue::Signed(y)) => Some(x.cmp(&y)),
256        (NumberValue::Unsigned(x), NumberValue::Unsigned(y)) => Some(x.cmp(&y)),
257        (NumberValue::Signed(x), NumberValue::Unsigned(y)) => Some(compare_i128_u128(x, y)),
258        (NumberValue::Unsigned(x), NumberValue::Signed(y)) => Some(compare_i128_u128(y, x).reverse()),
259        (NumberValue::Signed(x), NumberValue::Float(y)) => compare_i128_f64(x, y, number_comparison_policy),
260        (NumberValue::Float(x), NumberValue::Signed(y)) => {
261            compare_i128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
262        }
263        (NumberValue::Unsigned(x), NumberValue::Float(y)) => compare_u128_f64(x, y, number_comparison_policy),
264        (NumberValue::Float(x), NumberValue::Unsigned(y)) => {
265            compare_u128_f64(y, x, number_comparison_policy).map(Ordering::reverse)
266        }
267        (NumberValue::Float(x), NumberValue::Float(y)) => x.partial_cmp(&y),
268    }
269}
270
271/// Returns `true` if either value is a big-number variant.
272#[inline]
273fn contains_big_number(a: &Value, b: &Value) -> bool {
274    matches!(a, Value::BigInteger(_) | Value::BigDecimal(_)) || matches!(b, Value::BigInteger(_) | Value::BigDecimal(_))
275}
276
277/// Compares values when at least one side is `BigInteger` or `BigDecimal`.
278fn compare_big_numbers(a: &Value, b: &Value, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
279    if let (Some(x), Some(y)) = (big_integer_value(a), big_integer_value(b)) {
280        return Some(x.cmp(&y));
281    }
282    if let (Some(x), Some(y)) = (big_decimal_value(a), big_decimal_value(b)) {
283        return Some(x.cmp(&y));
284    }
285    if matches!(number_comparison_policy, NumberComparisonPolicy::Approximate) {
286        return compare_as_f64(a, b);
287    }
288    None
289}
290
291/// Converts integral numeric values to `BigInt` for exact comparison.
292fn big_integer_value(value: &Value) -> Option<BigInt> {
293    match value {
294        Value::Int8(v) => Some(BigInt::from(*v)),
295        Value::Int16(v) => Some(BigInt::from(*v)),
296        Value::Int32(v) => Some(BigInt::from(*v)),
297        Value::Int64(v) => Some(BigInt::from(*v)),
298        Value::Int128(v) => Some(BigInt::from(*v)),
299        Value::UInt8(v) => Some(BigInt::from(*v)),
300        Value::UInt16(v) => Some(BigInt::from(*v)),
301        Value::UInt32(v) => Some(BigInt::from(*v)),
302        Value::UInt64(v) => Some(BigInt::from(*v)),
303        Value::UInt128(v) => Some(BigInt::from(*v)),
304        Value::IntSize(v) => Some(BigInt::from(*v)),
305        Value::UIntSize(v) => Some(BigInt::from(*v)),
306        Value::BigInteger(v) => Some(v.clone()),
307        _ => None,
308    }
309}
310
311/// Converts integral and decimal numeric values to `BigDecimal`.
312fn big_decimal_value(value: &Value) -> Option<BigDecimal> {
313    match value {
314        Value::BigDecimal(v) => Some(v.clone()),
315        _ => big_integer_value(value).map(BigDecimal::from),
316    }
317}
318
319/// Compares two numeric values through the approximate `f64` fallback.
320#[inline]
321fn compare_as_f64(a: &Value, b: &Value) -> Option<Ordering> {
322    a.to::<f64>().ok()?.partial_cmp(&b.to::<f64>().ok()?)
323}
324
325const MAX_SAFE_INTEGER_F64_U64: u64 = 9_007_199_254_740_992;
326const I64_MIN_F64: f64 = -9_223_372_036_854_775_808.0;
327const I64_EXCLUSIVE_MAX_F64: f64 = 9_223_372_036_854_775_808.0;
328const U64_EXCLUSIVE_MAX_F64: f64 = 18_446_744_073_709_551_616.0;
329
330/// Compares a signed integer and an unsigned integer without lossy casts.
331#[inline]
332fn compare_i128_u128(x: i128, y: u128) -> Ordering {
333    if x < 0 { Ordering::Less } else { (x as u128).cmp(&y) }
334}
335
336/// Compares a signed integer and a float, returning `None` for risky cases.
337fn compare_i128_f64(x: i128, y: f64, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
338    if let Ok(x64) = i64::try_from(x) {
339        return compare_i64_f64(x64, y, number_comparison_policy);
340    }
341    if matches!(number_comparison_policy, NumberComparisonPolicy::Approximate) {
342        return (x as f64).partial_cmp(&y);
343    }
344    None
345}
346
347/// Compares an unsigned integer and a float, returning `None` for risky cases.
348fn compare_u128_f64(x: u128, y: f64, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
349    if let Ok(x64) = u64::try_from(x) {
350        return compare_u64_f64(x64, y, number_comparison_policy);
351    }
352    if matches!(number_comparison_policy, NumberComparisonPolicy::Approximate) {
353        return (x as f64).partial_cmp(&y);
354    }
355    None
356}
357
358/// Compares an `i64` and a float using the conservative JSON-era rules.
359fn compare_i64_f64(x: i64, y: f64, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
360    if y.fract() == 0.0 && (I64_MIN_F64..I64_EXCLUSIVE_MAX_F64).contains(&y) {
361        return Some(x.cmp(&(y as i64)));
362    }
363
364    if x.unsigned_abs() <= MAX_SAFE_INTEGER_F64_U64 {
365        return (x as f64).partial_cmp(&y);
366    }
367
368    if matches!(number_comparison_policy, NumberComparisonPolicy::Approximate) {
369        return (x as f64).partial_cmp(&y);
370    }
371
372    None
373}
374
375/// Compares a `u64` and a float using the conservative JSON-era rules.
376fn compare_u64_f64(x: u64, y: f64, number_comparison_policy: NumberComparisonPolicy) -> Option<Ordering> {
377    if y < 0.0 {
378        return Some(Ordering::Greater);
379    }
380
381    if y.fract() == 0.0 && (0.0..U64_EXCLUSIVE_MAX_F64).contains(&y) {
382        return Some(x.cmp(&(y as u64)));
383    }
384
385    if x <= MAX_SAFE_INTEGER_F64_U64 {
386        return (x as f64).partial_cmp(&y);
387    }
388
389    if matches!(number_comparison_policy, NumberComparisonPolicy::Approximate) {
390        return (x as f64).partial_cmp(&y);
391    }
392
393    None
394}