partition/
collider.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Provides a Collider tool to convert [`PartitionExpr`] into a form that is easier to operate by program.
16//!
17//! This mod provides the following major structs:
18//!
19//! - [`Collider`]: The main struct that converts [`PartitionExpr`].
20//! - [`AtomicExpr`]: An "atomic" Expression, which isn't composed (OR-ed) of other expressions.
21//! - [`NucleonExpr`]: A simplified expression representation.
22//! - [`GluonOp`]: Further restricted operation set.
23//!
24//! On the naming aspect, "collider" is a high-energy machine that cracks particles, "atomic" is a typical
25//! non-divisible particle before ~100 years ago, "nucleon" is what composes an atom and "gluon" is the
26//! force inside nucleons.
27
28use std::collections::HashMap;
29use std::fmt::Debug;
30use std::sync::Arc;
31
32use datafusion_expr::Operator;
33use datafusion_physical_expr::expressions::{col, lit, BinaryExpr};
34use datafusion_physical_expr::PhysicalExpr;
35use datatypes::arrow::datatypes::Schema;
36use datatypes::value::{OrderedF64, OrderedFloat, Value};
37
38use crate::error;
39use crate::error::Result;
40use crate::expr::{Operand, PartitionExpr, RestrictedOp};
41
42const ZERO: OrderedF64 = OrderedFloat(0.0f64);
43pub(crate) const NORMALIZE_STEP: OrderedF64 = OrderedFloat(1.0f64);
44pub(crate) const CHECK_STEP: OrderedF64 = OrderedFloat(0.5f64);
45
46/// Represents an "atomic" Expression, which isn't composed (OR-ed) of other expressions.
47#[allow(unused)]
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct AtomicExpr {
50    /// A (ordered) list of simplified expressions. They are [`RestrictedOp::And`]'ed together.
51    pub nucleons: Vec<NucleonExpr>,
52    /// Index to reference the [`PartitionExpr`] that this [`AtomicExpr`] is derived from.
53    /// This index is used with `exprs` field in [`MultiDimPartitionRule`](crate::multi_dim::MultiDimPartitionRule).
54    pub source_expr_index: usize,
55}
56
57impl AtomicExpr {
58    pub fn to_physical_expr(&self, schema: &Schema) -> Arc<dyn PhysicalExpr> {
59        let mut exprs = Vec::with_capacity(self.nucleons.len());
60        for nucleon in &self.nucleons {
61            exprs.push(nucleon.to_physical_expr(schema));
62        }
63        let result: Arc<dyn PhysicalExpr> = exprs
64            .into_iter()
65            .reduce(|l, r| Arc::new(BinaryExpr::new(l, Operator::And, r)))
66            .unwrap();
67        result
68    }
69}
70
71impl PartialOrd for AtomicExpr {
72    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
73        Some(self.nucleons.cmp(&other.nucleons))
74    }
75}
76
77/// A simplified expression representation.
78///
79/// This struct is used to compose [`AtomicExpr`], hence "nucleon".
80#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
81pub struct NucleonExpr {
82    column: String,
83    op: GluonOp,
84    /// Normalized [`Value`].
85    value: OrderedF64,
86}
87
88impl NucleonExpr {
89    pub fn to_physical_expr(&self, schema: &Schema) -> Arc<dyn PhysicalExpr> {
90        Arc::new(BinaryExpr::new(
91            col(&self.column, schema).unwrap(),
92            self.op.to_operator(),
93            lit(*self.value.as_ref()),
94        ))
95    }
96
97    /// Get the column name
98    pub fn column(&self) -> &str {
99        &self.column
100    }
101
102    /// Get the normalized value
103    pub fn value(&self) -> OrderedF64 {
104        self.value
105    }
106
107    /// Get the operation
108    pub fn op(&self) -> &GluonOp {
109        &self.op
110    }
111
112    pub fn new(column: impl Into<String>, op: GluonOp, value: OrderedF64) -> Self {
113        Self {
114            column: column.into(),
115            op,
116            value,
117        }
118    }
119}
120
121/// Further restricted operation set.
122///
123/// Conjunction operations are removed from [`RestrictedOp`].
124/// This enumeration is used to bind elements in [`NucleonExpr`], hence "gluon".
125#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
126pub enum GluonOp {
127    Eq,
128    NotEq,
129    Lt,
130    LtEq,
131    Gt,
132    GtEq,
133}
134
135impl GluonOp {
136    pub fn to_operator(&self) -> Operator {
137        match self {
138            GluonOp::Eq => Operator::Eq,
139            GluonOp::NotEq => Operator::NotEq,
140            GluonOp::Lt => Operator::Lt,
141            GluonOp::LtEq => Operator::LtEq,
142            GluonOp::Gt => Operator::Gt,
143            GluonOp::GtEq => Operator::GtEq,
144        }
145    }
146}
147
148/// Collider is used to collide a list of [`PartitionExpr`] into a list of [`AtomicExpr`]
149///
150/// It also normalizes the values of the columns in the expressions.
151#[allow(unused)]
152pub struct Collider<'a> {
153    source_exprs: &'a [PartitionExpr],
154
155    pub atomic_exprs: Vec<AtomicExpr>,
156    /// A map of column name to a list of `(value, normalized value)` pairs.
157    ///
158    /// The normalized value is used for comparison. The normalization process keeps the order of the values.
159    pub normalized_values: HashMap<String, Vec<(Value, OrderedF64)>>,
160}
161
162impl<'a> Collider<'a> {
163    pub fn new(source_exprs: &'a [PartitionExpr]) -> Result<Self> {
164        // first walk to collect all values
165        let mut values: HashMap<String, Vec<Value>> = HashMap::new();
166        for expr in source_exprs {
167            Self::collect_column_values_from_expr(expr, &mut values)?;
168        }
169
170        // normalize values, assumes all values on a column are the same type
171        let mut normalized_values: HashMap<String, HashMap<Value, OrderedF64>> =
172            HashMap::with_capacity(values.len());
173        for (column, mut column_values) in values {
174            column_values.sort_unstable();
175            column_values.dedup(); // Remove duplicates
176            let mut value_map = HashMap::with_capacity(column_values.len());
177            let mut start_value = ZERO;
178            for value in column_values {
179                value_map.insert(value, start_value);
180                start_value += NORMALIZE_STEP;
181            }
182            normalized_values.insert(column, value_map);
183        }
184
185        // second walk to get atomic exprs
186        let mut atomic_exprs = Vec::with_capacity(source_exprs.len());
187        for (index, expr) in source_exprs.iter().enumerate() {
188            Self::collide_expr(expr, index, &normalized_values, &mut atomic_exprs)?;
189        }
190        // sort nucleon exprs
191        for expr in &mut atomic_exprs {
192            expr.nucleons.sort_unstable();
193        }
194
195        // convert normalized values to a map
196        let normalized_values = normalized_values
197            .into_iter()
198            .map(|(col, values)| {
199                let mut values = values.into_iter().collect::<Vec<_>>();
200                values.sort_unstable_by_key(|(_, v)| *v);
201                (col, values)
202            })
203            .collect();
204
205        Ok(Self {
206            source_exprs,
207            atomic_exprs,
208            normalized_values,
209        })
210    }
211
212    /// Helper to collect values with their associated columns from an expression
213    fn collect_column_values_from_expr(
214        expr: &PartitionExpr,
215        values: &mut HashMap<String, Vec<Value>>,
216    ) -> Result<()> {
217        // Handle binary operations between column and value
218        match (&*expr.lhs, &*expr.rhs) {
219            (Operand::Column(col), Operand::Value(val))
220            | (Operand::Value(val), Operand::Column(col)) => {
221                values.entry(col.clone()).or_default().push(val.clone());
222                Ok(())
223            }
224            (Operand::Expr(left_expr), Operand::Expr(right_expr)) => {
225                Self::collect_column_values_from_expr(left_expr, values)?;
226                Self::collect_column_values_from_expr(right_expr, values)
227            }
228            // Other combinations don't directly contribute column-value pairs
229            _ => error::InvalidExprSnafu { expr: expr.clone() }.fail(),
230        }
231    }
232
233    /// Collide a [`PartitionExpr`] into multiple [`AtomicExpr`]s.
234    ///
235    /// Split the [`PartitionExpr`] on every [`RestrictedOp::Or`] (disjunction), each branch is an [`AtomicExpr`].
236    /// Since [`PartitionExpr`] doesn't allow parentheses, Expression like `(a = 1 OR b = 2) AND c = 3` won't occur.
237    /// We can safely split on every [`RestrictedOp::Or`].
238    fn collide_expr(
239        expr: &PartitionExpr,
240        index: usize,
241        normalized_values: &HashMap<String, HashMap<Value, OrderedF64>>,
242        result: &mut Vec<AtomicExpr>,
243    ) -> Result<()> {
244        match expr.op {
245            RestrictedOp::Or => {
246                // Split on OR operation - each side becomes a separate atomic expression
247
248                // Process left side
249                match &*expr.lhs {
250                    Operand::Expr(left_expr) => {
251                        Self::collide_expr(left_expr, index, normalized_values, result)?;
252                    }
253                    _ => {
254                        // Single operand - this shouldn't happen with OR
255                        // OR should always connect two sub-expressions
256                        return error::InvalidExprSnafu { expr: expr.clone() }.fail();
257                    }
258                }
259
260                // Process right side
261                match &*expr.rhs {
262                    Operand::Expr(right_expr) => {
263                        Self::collide_expr(right_expr, index, normalized_values, result)?;
264                    }
265                    _ => {
266                        // Single operand - this shouldn't happen with OR
267                        // OR should always connect two sub-expressions
268                        return error::InvalidExprSnafu { expr: expr.clone() }.fail();
269                    }
270                }
271            }
272            RestrictedOp::And => {
273                // For AND operations, we need to combine nucleons
274                let mut nucleons = Vec::new();
275                Self::collect_nucleons_from_expr(expr, &mut nucleons, normalized_values)?;
276
277                result.push(AtomicExpr {
278                    nucleons,
279                    source_expr_index: index,
280                });
281            }
282            _ => {
283                // For other operations, create a single atomic expression
284                let mut nucleons = Vec::new();
285                Self::collect_nucleons_from_expr(expr, &mut nucleons, normalized_values)?;
286
287                result.push(AtomicExpr {
288                    nucleons,
289                    source_expr_index: index,
290                });
291            }
292        }
293        Ok(())
294    }
295
296    /// Collect nucleons from an expression (handles AND operations recursively)
297    fn collect_nucleons_from_expr(
298        expr: &PartitionExpr,
299        nucleons: &mut Vec<NucleonExpr>,
300        normalized_values: &HashMap<String, HashMap<Value, OrderedF64>>,
301    ) -> Result<()> {
302        match expr.op {
303            RestrictedOp::And => {
304                // For AND operations, collect nucleons from both sides
305                Self::collect_nucleons_from_operand(&expr.lhs, nucleons, normalized_values)?;
306                Self::collect_nucleons_from_operand(&expr.rhs, nucleons, normalized_values)?;
307            }
308            _ => {
309                // For non-AND operations, try to create a nucleon directly
310                nucleons.push(Self::try_create_nucleon(
311                    &expr.lhs,
312                    &expr.op,
313                    &expr.rhs,
314                    normalized_values,
315                )?);
316            }
317        }
318        Ok(())
319    }
320
321    /// Collect nucleons from an operand
322    fn collect_nucleons_from_operand(
323        operand: &Operand,
324        nucleons: &mut Vec<NucleonExpr>,
325        normalized_values: &HashMap<String, HashMap<Value, OrderedF64>>,
326    ) -> Result<()> {
327        match operand {
328            Operand::Expr(expr) => {
329                Self::collect_nucleons_from_expr(expr, nucleons, normalized_values)
330            }
331            _ => {
332                // Only `Operand::Expr` can be conjuncted by AND.
333                error::NoExprOperandSnafu {
334                    operand: operand.clone(),
335                }
336                .fail()
337            }
338        }
339    }
340
341    /// Try to create a nucleon from operands
342    fn try_create_nucleon(
343        lhs: &Operand,
344        op: &RestrictedOp,
345        rhs: &Operand,
346        normalized_values: &HashMap<String, HashMap<Value, OrderedF64>>,
347    ) -> Result<NucleonExpr> {
348        let gluon_op = match op {
349            RestrictedOp::Eq => GluonOp::Eq,
350            RestrictedOp::NotEq => GluonOp::NotEq,
351            RestrictedOp::Lt => GluonOp::Lt,
352            RestrictedOp::LtEq => GluonOp::LtEq,
353            RestrictedOp::Gt => GluonOp::Gt,
354            RestrictedOp::GtEq => GluonOp::GtEq,
355            RestrictedOp::And | RestrictedOp::Or => {
356                // These should be handled elsewhere
357                return error::UnexpectedSnafu {
358                    err_msg: format!("Conjunction operation {:?} should be handled elsewhere", op),
359                }
360                .fail();
361            }
362        };
363
364        match (lhs, rhs) {
365            (Operand::Column(col), Operand::Value(val)) => {
366                if let Some(column_values) = normalized_values.get(col) {
367                    if let Some(&normalized_val) = column_values.get(val) {
368                        return Ok(NucleonExpr {
369                            column: col.clone(),
370                            op: gluon_op,
371                            value: normalized_val,
372                        });
373                    }
374                }
375            }
376            (Operand::Value(val), Operand::Column(col)) => {
377                if let Some(column_values) = normalized_values.get(col) {
378                    if let Some(&normalized_val) = column_values.get(val) {
379                        // Flip the operation for value op column
380                        let flipped_op = match gluon_op {
381                            GluonOp::Lt => GluonOp::Gt,
382                            GluonOp::LtEq => GluonOp::GtEq,
383                            GluonOp::Gt => GluonOp::Lt,
384                            GluonOp::GtEq => GluonOp::LtEq,
385                            op => op, // Eq and NotEq remain the same
386                        };
387                        return Ok(NucleonExpr {
388                            column: col.clone(),
389                            op: flipped_op,
390                            value: normalized_val,
391                        });
392                    }
393                }
394            }
395            _ => {}
396        }
397
398        // Other combinations not supported for nucleons
399        error::InvalidExprSnafu {
400            expr: PartitionExpr::new(lhs.clone(), op.clone(), rhs.clone()),
401        }
402        .fail()
403    }
404}
405
406#[cfg(test)]
407mod test {
408    use super::*;
409    use crate::expr::col;
410
411    #[test]
412    fn test_collider_basic_value_normalization() {
413        // Test with different value types in different columns
414        let exprs = vec![
415            // Integer values
416            col("age").eq(Value::UInt32(25)),
417            col("age").eq(Value::UInt32(30)),
418            col("age").eq(Value::UInt32(25)), // Duplicate should be handled
419            // String values
420            col("name").eq(Value::String("alice".into())),
421            col("name").eq(Value::String("bob".into())),
422            // Boolean values
423            col("active").eq(Value::Boolean(true)),
424            col("active").eq(Value::Boolean(false)),
425            // Float values
426            col("score").eq(Value::Float64(OrderedFloat(95.5))),
427            col("score").eq(Value::Float64(OrderedFloat(87.2))),
428        ];
429
430        let collider = Collider::new(&exprs).expect("Failed to create collider");
431
432        // Check that we have the right number of columns
433        assert_eq!(collider.normalized_values.len(), 4);
434
435        // Check age column - should have 2 unique values (25, 30)
436        let age_values = &collider.normalized_values["age"];
437        assert_eq!(age_values.len(), 2);
438        assert_eq!(
439            age_values,
440            &[
441                (Value::UInt32(25), OrderedFloat(0.0f64)),
442                (Value::UInt32(30), OrderedFloat(1.0f64))
443            ]
444        );
445
446        // Check name column - should have 2 values
447        let name_values = &collider.normalized_values["name"];
448        assert_eq!(name_values.len(), 2);
449        assert_eq!(
450            name_values,
451            &[
452                (Value::String("alice".into()), OrderedFloat(0.0f64)),
453                (Value::String("bob".into()), OrderedFloat(1.0f64))
454            ]
455        );
456
457        // Check active column - should have 2 values
458        let active_values = &collider.normalized_values["active"];
459        assert_eq!(active_values.len(), 2);
460        assert_eq!(
461            active_values,
462            &[
463                (Value::Boolean(false), OrderedFloat(0.0f64)),
464                (Value::Boolean(true), OrderedFloat(1.0f64))
465            ]
466        );
467
468        // Check score column - should have 2 values
469        let score_values = &collider.normalized_values["score"];
470        assert_eq!(score_values.len(), 2);
471        assert_eq!(
472            score_values,
473            &[
474                (Value::Float64(OrderedFloat(87.2)), OrderedFloat(0.0f64)),
475                (Value::Float64(OrderedFloat(95.5)), OrderedFloat(1.0f64))
476            ]
477        );
478    }
479
480    #[test]
481    fn test_collider_simple_expressions() {
482        // Test simple equality
483        let exprs = vec![col("id").eq(Value::UInt32(1))];
484
485        let collider = Collider::new(&exprs).unwrap();
486        assert_eq!(collider.atomic_exprs.len(), 1);
487        assert_eq!(collider.atomic_exprs[0].nucleons.len(), 1);
488        assert_eq!(collider.atomic_exprs[0].source_expr_index, 0);
489
490        // Test simple AND
491        let exprs = vec![col("id")
492            .eq(Value::UInt32(1))
493            .and(col("status").eq(Value::String("active".into())))];
494
495        let collider = Collider::new(&exprs).unwrap();
496        assert_eq!(collider.atomic_exprs.len(), 1);
497        assert_eq!(collider.atomic_exprs[0].nucleons.len(), 2);
498
499        // Test simple OR - should create 2 atomic expressions
500        let expr = PartitionExpr::new(
501            Operand::Expr(col("id").eq(Value::UInt32(1))),
502            RestrictedOp::Or,
503            Operand::Expr(col("id").eq(Value::UInt32(2))),
504        );
505        let exprs = vec![expr];
506
507        let collider = Collider::new(&exprs).unwrap();
508        assert_eq!(collider.atomic_exprs.len(), 2);
509        assert_eq!(collider.atomic_exprs[0].nucleons.len(), 1);
510        assert_eq!(collider.atomic_exprs[1].nucleons.len(), 1);
511    }
512
513    #[test]
514    fn test_collider_complex_nested_expressions() {
515        // Test: (id = 1 AND status = 'active') OR (id = 2 AND status = 'inactive') OR (id = 3)
516        let branch1 = col("id")
517            .eq(Value::UInt32(1))
518            .and(col("status").eq(Value::String("active".into())));
519        let branch2 = col("id")
520            .eq(Value::UInt32(2))
521            .and(col("status").eq(Value::String("inactive".into())));
522        let branch3 = col("id").eq(Value::UInt32(3));
523
524        let expr = PartitionExpr::new(
525            Operand::Expr(PartitionExpr::new(
526                Operand::Expr(branch1),
527                RestrictedOp::Or,
528                Operand::Expr(branch2),
529            )),
530            RestrictedOp::Or,
531            Operand::Expr(branch3),
532        );
533
534        let exprs = vec![expr];
535        let collider = Collider::new(&exprs).unwrap();
536
537        assert_eq!(collider.atomic_exprs.len(), 3);
538
539        let total_nucleons: usize = collider
540            .atomic_exprs
541            .iter()
542            .map(|ae| ae.nucleons.len())
543            .sum();
544        assert_eq!(total_nucleons, 5);
545    }
546
547    #[test]
548    fn test_collider_deep_nesting() {
549        // Test deeply nested AND operations: a = 1 AND b = 2 AND c = 3 AND d = 4
550        let expr = col("a")
551            .eq(Value::UInt32(1))
552            .and(col("b").eq(Value::UInt32(2)))
553            .and(col("c").eq(Value::UInt32(3)))
554            .and(col("d").eq(Value::UInt32(4)));
555
556        let exprs = vec![expr];
557        let collider = Collider::new(&exprs).unwrap();
558
559        assert_eq!(collider.atomic_exprs.len(), 1);
560        assert_eq!(collider.atomic_exprs[0].nucleons.len(), 4);
561
562        // All nucleons should have Eq operation
563        for nucleon in &collider.atomic_exprs[0].nucleons {
564            assert_eq!(nucleon.op, GluonOp::Eq);
565        }
566    }
567
568    #[test]
569    fn test_collider_multiple_expressions() {
570        // Test multiple separate expressions
571        let exprs = vec![
572            col("id").eq(Value::UInt32(1)),
573            col("name").eq(Value::String("alice".into())),
574            col("score").gt_eq(Value::Float64(OrderedFloat(90.0))),
575        ];
576
577        let collider = Collider::new(&exprs).unwrap();
578
579        // Should create 3 atomic expressions (one for each input expression)
580        assert_eq!(collider.atomic_exprs.len(), 3);
581
582        // Each should have exactly 1 nucleon
583        for atomic_expr in &collider.atomic_exprs {
584            assert_eq!(atomic_expr.nucleons.len(), 1);
585        }
586
587        // Check that source indices are correct
588        let indices: Vec<usize> = collider
589            .atomic_exprs
590            .iter()
591            .map(|ae| ae.source_expr_index)
592            .collect();
593        assert!(indices.contains(&0));
594        assert!(indices.contains(&1));
595        assert!(indices.contains(&2));
596    }
597
598    #[test]
599    fn test_collider_value_column_order() {
600        // Test expressions where value comes before column (should flip operation)
601        let expr1 = PartitionExpr::new(
602            Operand::Value(Value::UInt32(10)),
603            RestrictedOp::Lt,
604            Operand::Column("age".to_string()),
605        ); // 10 < age should become age > 10
606
607        let expr2 = PartitionExpr::new(
608            Operand::Value(Value::UInt32(20)),
609            RestrictedOp::GtEq,
610            Operand::Column("score".to_string()),
611        ); // 20 >= score should become score <= 20
612
613        let exprs = vec![expr1, expr2];
614        let collider = Collider::new(&exprs).unwrap();
615
616        assert_eq!(collider.atomic_exprs.len(), 2);
617
618        // Check that operations were flipped correctly
619        let operations: Vec<GluonOp> = collider
620            .atomic_exprs
621            .iter()
622            .map(|ae| ae.nucleons[0].op.clone())
623            .collect();
624
625        assert!(operations.contains(&GluonOp::Gt)); // 10 < age -> age > 10
626        assert!(operations.contains(&GluonOp::LtEq)); // 20 >= score -> score <= 20
627    }
628
629    #[test]
630    fn test_collider_complex_or_with_different_columns() {
631        // Test: (name = 'alice' AND age = 25) OR (status = 'active' AND score > 90)
632        let branch1 = col("name")
633            .eq(Value::String("alice".into()))
634            .and(col("age").eq(Value::UInt32(25)));
635
636        let branch2 = col("status")
637            .eq(Value::String("active".into()))
638            .and(PartitionExpr::new(
639                Operand::Column("score".to_string()),
640                RestrictedOp::Gt,
641                Operand::Value(Value::Float64(OrderedFloat(90.0))),
642            ));
643
644        let expr = PartitionExpr::new(
645            Operand::Expr(branch1),
646            RestrictedOp::Or,
647            Operand::Expr(branch2),
648        );
649
650        let exprs = vec![expr];
651        let collider = Collider::new(&exprs).expect("Failed to create collider");
652
653        // Should create 2 atomic expressions
654        assert_eq!(collider.atomic_exprs.len(), 2);
655
656        // Each atomic expression should have 2 nucleons
657        for atomic_expr in &collider.atomic_exprs {
658            assert_eq!(atomic_expr.nucleons.len(), 2);
659        }
660
661        // Should have normalized values for all 4 columns
662        assert_eq!(collider.normalized_values.len(), 4);
663        assert!(collider.normalized_values.contains_key("name"));
664        assert!(collider.normalized_values.contains_key("age"));
665        assert!(collider.normalized_values.contains_key("status"));
666        assert!(collider.normalized_values.contains_key("score"));
667    }
668
669    #[test]
670    fn test_try_create_nucleon_edge_cases() {
671        let normalized_values = HashMap::new();
672
673        // Test with AND operation
674        let result = Collider::try_create_nucleon(
675            &col("a"),
676            &RestrictedOp::And,
677            &Operand::Value(Value::UInt32(1)),
678            &normalized_values,
679        );
680        assert!(result.is_err());
681
682        // Test with OR operation
683        let result = Collider::try_create_nucleon(
684            &col("a"),
685            &RestrictedOp::Or,
686            &Operand::Value(Value::UInt32(1)),
687            &normalized_values,
688        );
689        assert!(result.is_err());
690
691        // Test with Column-Column
692        let result = Collider::try_create_nucleon(
693            &col("a"),
694            &RestrictedOp::Eq,
695            &col("b"),
696            &normalized_values,
697        );
698        assert!(result.is_err());
699
700        // Test with Value-Value
701        let result = Collider::try_create_nucleon(
702            &Operand::Value(Value::UInt32(1)),
703            &RestrictedOp::Eq,
704            &Operand::Value(Value::UInt32(2)),
705            &normalized_values,
706        );
707        assert!(result.is_err());
708
709        // Test empty expression list
710        let exprs = vec![];
711        let collider = Collider::new(&exprs).unwrap();
712        assert_eq!(collider.atomic_exprs.len(), 0);
713        assert_eq!(collider.normalized_values.len(), 0);
714    }
715}