rxgraph 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {rxgraph-0.3.0 → rxgraph-0.4.0}/Cargo.lock +2 -2
  2. {rxgraph-0.3.0 → rxgraph-0.4.0}/PKG-INFO +1 -1
  3. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/Cargo.toml +1 -1
  4. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/arrow_value.rs +167 -2
  5. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/bind.rs +40 -9
  6. rxgraph-0.4.0/crates/rxgraph/src/dsl/eval.rs +170 -0
  7. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/mod.rs +177 -1
  8. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/ops/list.rs +161 -28
  9. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/polars_json.rs +2 -0
  10. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/value.rs +30 -10
  11. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/graph/graph.rs +237 -11
  12. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/graph/mod.rs +2 -1
  13. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/graph/repo.rs +104 -35
  14. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/traversal/algo.rs +272 -57
  15. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph-python/Cargo.toml +1 -1
  16. {rxgraph-0.3.0 → rxgraph-0.4.0}/python/rxgraph/__init__.py +7 -3
  17. {rxgraph-0.3.0 → rxgraph-0.4.0}/python/rxgraph/_graph_tables.py +4 -10
  18. rxgraph-0.3.0/crates/rxgraph/src/dsl/eval.rs +0 -95
  19. {rxgraph-0.3.0 → rxgraph-0.4.0}/Cargo.toml +0 -0
  20. {rxgraph-0.3.0 → rxgraph-0.4.0}/README.md +0 -0
  21. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/README.md +0 -0
  22. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/benches/flight_routes.rs +0 -0
  23. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/benches/memory.rs +0 -0
  24. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/benches/payment_risk.rs +0 -0
  25. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/examples/flight_routes.rs +0 -0
  26. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/arrow.rs +0 -0
  27. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/expr.rs +0 -0
  28. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/ops/mod.rs +0 -0
  29. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/ops/scalar.rs +0 -0
  30. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/ops/string.rs +0 -0
  31. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/dsl/ops/struct_.rs +0 -0
  32. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/graph/csr.rs +0 -0
  33. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/lib.rs +0 -0
  34. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/traversal/config.rs +0 -0
  35. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/traversal/mod.rs +0 -0
  36. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph/src/traversal/progress.rs +0 -0
  37. {rxgraph-0.3.0 → rxgraph-0.4.0}/crates/rxgraph-python/src/lib.rs +0 -0
  38. {rxgraph-0.3.0 → rxgraph-0.4.0}/pyproject.toml +0 -0
  39. {rxgraph-0.3.0 → rxgraph-0.4.0}/python/rxgraph/__init__.pyi +0 -0
  40. {rxgraph-0.3.0 → rxgraph-0.4.0}/python/rxgraph/py.typed +0 -0
@@ -1293,7 +1293,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1293
1293
 
1294
1294
  [[package]]
1295
1295
  name = "rxgraph"
1296
- version = "0.3.0"
1296
+ version = "0.4.0"
1297
1297
  dependencies = [
1298
1298
  "anyhow",
1299
1299
  "arrow",
@@ -1311,7 +1311,7 @@ dependencies = [
1311
1311
 
1312
1312
  [[package]]
1313
1313
  name = "rxgraph-python"
1314
- version = "0.3.0"
1314
+ version = "0.4.0"
1315
1315
  dependencies = [
1316
1316
  "anyhow",
1317
1317
  "arrow",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxgraph
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rxgraph"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  edition.workspace = true
5
5
  rust-version.workspace = true
6
6
  description = "High-performance graph traversal engine"
@@ -1,4 +1,4 @@
1
- use std::{io::Cursor, sync::Arc};
1
+ use std::{cmp::Ordering, io::Cursor, sync::Arc};
2
2
 
3
3
  use anyhow::{Context, Result, bail};
4
4
  use arrow::{
@@ -12,7 +12,7 @@ use arrow::{
12
12
  record_batch::RecordBatch,
13
13
  };
14
14
 
15
- use crate::dsl::Value;
15
+ use crate::dsl::{Value, ops::scalar::ScalarOp};
16
16
 
17
17
  #[derive(Debug, Clone)]
18
18
  pub(crate) enum ColumnReader {
@@ -35,6 +35,13 @@ pub(crate) enum ColumnReader {
35
35
  Struct(StructArray),
36
36
  }
37
37
 
38
+ enum ScalarValueRef<'a> {
39
+ Null,
40
+ Bool(bool),
41
+ Number(f64),
42
+ Str(&'a str),
43
+ }
44
+
38
45
  impl ColumnReader {
39
46
  pub(crate) fn bind(batch: &RecordBatch, name: &str) -> Result<Self> {
40
47
  let column = batch
@@ -106,6 +113,164 @@ impl ColumnReader {
106
113
  Self::Struct(array) => nullable!(array, struct_row_to_value(array, row)?),
107
114
  })
108
115
  }
116
+
117
+ pub(crate) fn eval_scalar_literal(
118
+ &self,
119
+ row: usize,
120
+ op: ScalarOp,
121
+ literal: &Value,
122
+ reverse: bool,
123
+ ) -> Result<Option<Value>> {
124
+ let Some(value) = self.scalar_value(row) else {
125
+ return Ok(None);
126
+ };
127
+ Ok(Some(match value {
128
+ ScalarValueRef::Null => eval_null_literal(op, literal),
129
+ ScalarValueRef::Bool(value) => eval_bool_literal(value, op, literal, reverse)?,
130
+ ScalarValueRef::Number(value) => eval_number_literal(value, op, literal, reverse)?,
131
+ ScalarValueRef::Str(value) => eval_str_literal(value, op, literal, reverse)?,
132
+ }))
133
+ }
134
+
135
+ fn scalar_value(&self, row: usize) -> Option<ScalarValueRef<'_>> {
136
+ macro_rules! nullable {
137
+ ($array:expr, $value:expr) => {
138
+ if $array.is_null(row) {
139
+ ScalarValueRef::Null
140
+ } else {
141
+ $value
142
+ }
143
+ };
144
+ }
145
+
146
+ Some(match self {
147
+ Self::Bool(array) => nullable!(array, ScalarValueRef::Bool(array.value(row))),
148
+ Self::I8(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
149
+ Self::I16(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
150
+ Self::I32(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
151
+ Self::I64(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
152
+ Self::U8(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
153
+ Self::U16(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
154
+ Self::U32(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
155
+ Self::U64(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
156
+ Self::F32(array) => nullable!(array, ScalarValueRef::Number(array.value(row) as f64)),
157
+ Self::F64(array) => nullable!(array, ScalarValueRef::Number(array.value(row))),
158
+ Self::Utf8(array) => nullable!(array, ScalarValueRef::Str(array.value(row))),
159
+ Self::LargeUtf8(array) => nullable!(array, ScalarValueRef::Str(array.value(row))),
160
+ Self::Utf8View(array) => nullable!(array, ScalarValueRef::Str(array.value(row))),
161
+ Self::List(_) | Self::LargeList(_) | Self::Struct(_) => return None,
162
+ })
163
+ }
164
+ }
165
+
166
+ fn eval_null_literal(op: ScalarOp, literal: &Value) -> Value {
167
+ match op {
168
+ ScalarOp::Eq => Value::Bool(literal.is_null()),
169
+ ScalarOp::NotEq => Value::Bool(!literal.is_null()),
170
+ ScalarOp::Lt | ScalarOp::LtEq | ScalarOp::Gt | ScalarOp::GtEq => Value::Null,
171
+ _ => unreachable!("fast scalar literal only handles comparison ops"),
172
+ }
173
+ }
174
+
175
+ fn eval_non_null_null_literal(op: ScalarOp) -> Value {
176
+ match op {
177
+ ScalarOp::Eq => Value::Bool(false),
178
+ ScalarOp::NotEq => Value::Bool(true),
179
+ ScalarOp::Lt | ScalarOp::LtEq | ScalarOp::Gt | ScalarOp::GtEq => Value::Null,
180
+ _ => unreachable!("fast scalar literal only handles comparison ops"),
181
+ }
182
+ }
183
+
184
+ fn eval_bool_literal(value: bool, op: ScalarOp, literal: &Value, reverse: bool) -> Result<Value> {
185
+ if literal.is_null() {
186
+ return Ok(eval_non_null_null_literal(op));
187
+ }
188
+ let Some(rhs) = literal_bool(literal) else {
189
+ return eval_incomparable_literal(op);
190
+ };
191
+ Ok(eval_ordering_or_eq(
192
+ op,
193
+ value == rhs,
194
+ value.cmp(&rhs),
195
+ reverse,
196
+ ))
197
+ }
198
+
199
+ fn eval_number_literal(value: f64, op: ScalarOp, literal: &Value, reverse: bool) -> Result<Value> {
200
+ if literal.is_null() {
201
+ return Ok(eval_non_null_null_literal(op));
202
+ }
203
+ let Some(rhs) = literal.as_f64() else {
204
+ return eval_incomparable_literal(op);
205
+ };
206
+ match op {
207
+ ScalarOp::Eq => Ok(Value::Bool(value == rhs)),
208
+ ScalarOp::NotEq => Ok(Value::Bool(value != rhs)),
209
+ ScalarOp::Lt | ScalarOp::LtEq | ScalarOp::Gt | ScalarOp::GtEq => {
210
+ let ordering = value.partial_cmp(&rhs).context("cannot compare values")?;
211
+ Ok(Value::Bool(apply_ordering(op, ordering, reverse)))
212
+ }
213
+ _ => unreachable!("fast scalar literal only handles comparison ops"),
214
+ }
215
+ }
216
+
217
+ fn eval_str_literal(value: &str, op: ScalarOp, literal: &Value, reverse: bool) -> Result<Value> {
218
+ if literal.is_null() {
219
+ return Ok(eval_non_null_null_literal(op));
220
+ }
221
+ let Value::Str(rhs) = literal else {
222
+ return eval_incomparable_literal(op);
223
+ };
224
+ Ok(eval_ordering_or_eq(
225
+ op,
226
+ value == rhs.as_ref(),
227
+ value.cmp(rhs.as_ref()),
228
+ reverse,
229
+ ))
230
+ }
231
+
232
+ fn literal_bool(literal: &Value) -> Option<bool> {
233
+ match literal {
234
+ Value::Bool(value) => Some(*value),
235
+ _ => None,
236
+ }
237
+ }
238
+
239
+ fn eval_incomparable_literal(op: ScalarOp) -> Result<Value> {
240
+ match op {
241
+ ScalarOp::Eq => Ok(Value::Bool(false)),
242
+ ScalarOp::NotEq => Ok(Value::Bool(true)),
243
+ ScalarOp::Lt | ScalarOp::LtEq | ScalarOp::Gt | ScalarOp::GtEq => {
244
+ bail!("cannot compare values")
245
+ }
246
+ _ => unreachable!("fast scalar literal only handles comparison ops"),
247
+ }
248
+ }
249
+
250
+ fn eval_ordering_or_eq(op: ScalarOp, equal: bool, ordering: Ordering, reverse: bool) -> Value {
251
+ match op {
252
+ ScalarOp::Eq => Value::Bool(equal),
253
+ ScalarOp::NotEq => Value::Bool(!equal),
254
+ ScalarOp::Lt | ScalarOp::LtEq | ScalarOp::Gt | ScalarOp::GtEq => {
255
+ Value::Bool(apply_ordering(op, ordering, reverse))
256
+ }
257
+ _ => unreachable!("fast scalar literal only handles comparison ops"),
258
+ }
259
+ }
260
+
261
+ fn apply_ordering(op: ScalarOp, ordering: Ordering, reverse: bool) -> bool {
262
+ let ordering = if reverse {
263
+ ordering.reverse()
264
+ } else {
265
+ ordering
266
+ };
267
+ match op {
268
+ ScalarOp::Lt => ordering.is_lt(),
269
+ ScalarOp::LtEq => ordering.is_le(),
270
+ ScalarOp::Gt => ordering.is_gt(),
271
+ ScalarOp::GtEq => ordering.is_ge(),
272
+ _ => unreachable!("fast scalar literal only handles ordering ops"),
273
+ }
109
274
  }
110
275
 
111
276
  pub(crate) fn array_to_values(array: &dyn Array) -> Result<Vec<Value>> {
@@ -2,12 +2,13 @@ use anyhow::{Context, Result};
2
2
 
3
3
  use crate::{
4
4
  dsl::{
5
- DslKernel, StateRow, StateValues, Value,
5
+ DslKernel, StateRow, StateValue, StateValues, Value,
6
6
  arrow_value::ColumnReader,
7
7
  eval::EvalCtx,
8
8
  expr::{ColumnRef, Expr},
9
+ ops::scalar::ScalarOp,
9
10
  },
10
- graph::{Graph, GraphId, GraphRepo},
11
+ graph::{EDGE_DEST_COL, EDGE_SRC_COL, Graph, GraphId, GraphRepo, ID_COL},
11
12
  };
12
13
 
13
14
  #[derive(Debug)]
@@ -50,10 +51,14 @@ impl BoundKernel {
50
51
  self.visit.eval(ctx)?.truthy()
51
52
  }
52
53
 
53
- pub(crate) fn next_state(&self, current: &[Value], ctx: &EvalCtx<'_>) -> Result<StateValues> {
54
+ pub(crate) fn next_state(
55
+ &self,
56
+ current: &[StateValue],
57
+ ctx: &EvalCtx<'_>,
58
+ ) -> Result<StateValues> {
54
59
  let mut next = current.iter().cloned().collect::<StateValues>();
55
60
  for (index, expr) in &self.next_state {
56
- next[*index] = expr.eval(ctx)?;
61
+ next[*index] = StateValue::new(expr.eval(ctx)?);
57
62
  }
58
63
  Ok(next)
59
64
  }
@@ -62,11 +67,11 @@ impl BoundKernel {
62
67
  self.stop.eval(ctx)?.truthy()
63
68
  }
64
69
 
65
- pub(crate) fn state_row(&self, state: &[Value]) -> StateRow {
70
+ pub(crate) fn state_row(&self, state: &[StateValue]) -> StateRow {
66
71
  self.names
67
72
  .iter()
68
73
  .cloned()
69
- .zip(state.iter().cloned())
74
+ .zip(state.iter().map(StateValue::to_value))
70
75
  .collect()
71
76
  }
72
77
  }
@@ -89,8 +94,13 @@ impl BoundColumn {
89
94
  ColumnRef::SrcId => Self::SrcId,
90
95
  ColumnRef::DestId => Self::DestId,
91
96
  ColumnRef::EdgeId => Self::EdgeId,
97
+ ColumnRef::SrcField(name) if name == ID_COL => Self::SrcId,
92
98
  ColumnRef::SrcField(name) => Self::Src(ColumnReader::bind(&graph.repo.nodes, &name)?),
99
+ ColumnRef::DestField(name) if name == ID_COL => Self::DestId,
93
100
  ColumnRef::DestField(name) => Self::Dest(ColumnReader::bind(&graph.repo.nodes, &name)?),
101
+ ColumnRef::EdgeField(name) if name == ID_COL => Self::EdgeId,
102
+ ColumnRef::EdgeField(name) if name == EDGE_SRC_COL => Self::SrcId,
103
+ ColumnRef::EdgeField(name) if name == EDGE_DEST_COL => Self::DestId,
94
104
  ColumnRef::EdgeField(name) => Self::Edge(ColumnReader::bind(&graph.repo.edges, &name)?),
95
105
  ColumnRef::State(name) => state_index(names, &name)
96
106
  .map(Self::State)
@@ -121,10 +131,31 @@ impl BoundColumn {
121
131
  Self::Src(reader) => reader.value(ctx.src as usize),
122
132
  Self::Dest(reader) => reader.value(ctx.dest as usize),
123
133
  Self::Edge(reader) => reader.value(ctx.edge as usize),
124
- Self::State(index) => Ok(ctx.state[*index].clone()),
134
+ Self::State(index) => Ok(ctx.state[*index].to_value()),
125
135
  Self::MissingState => Ok(Value::Null),
126
136
  }
127
137
  }
138
+
139
+ pub(crate) fn eval_scalar_literal(
140
+ &self,
141
+ ctx: &EvalCtx<'_>,
142
+ op: ScalarOp,
143
+ literal: &Value,
144
+ reverse: bool,
145
+ ) -> Result<Option<Value>> {
146
+ match self {
147
+ Self::Src(reader) => reader.eval_scalar_literal(ctx.src as usize, op, literal, reverse),
148
+ Self::Dest(reader) => {
149
+ reader.eval_scalar_literal(ctx.dest as usize, op, literal, reverse)
150
+ }
151
+ Self::Edge(reader) => {
152
+ reader.eval_scalar_literal(ctx.edge as usize, op, literal, reverse)
153
+ }
154
+ Self::SrcId | Self::DestId | Self::EdgeId | Self::State(_) | Self::MissingState => {
155
+ Ok(None)
156
+ }
157
+ }
158
+ }
128
159
  }
129
160
 
130
161
  fn graph_id_value(id: GraphId<'_>) -> Result<Value> {
@@ -152,8 +183,8 @@ fn normalize_state(state: StateRow, names: &[String]) -> StateValues {
152
183
  state
153
184
  .binary_search_by(|(key, _)| key.as_str().cmp(name))
154
185
  .ok()
155
- .map(|i| state[i].1.clone())
156
- .unwrap_or(Value::Null)
186
+ .map(|i| StateValue::new(state[i].1.clone()))
187
+ .unwrap_or_else(|| StateValue::new(Value::Null))
157
188
  })
158
189
  .collect::<StateValues>()
159
190
  }
@@ -0,0 +1,170 @@
1
+ use anyhow::{Context, Result};
2
+
3
+ use crate::{
4
+ dsl::{StateValue, Value, bind::BoundColumn, expr::Expr, ops::scalar::ScalarOp},
5
+ graph::{EdgeId, Graph, NodeId},
6
+ };
7
+
8
+ pub(crate) struct EvalCtx<'a> {
9
+ pub(crate) graph: &'a Graph,
10
+ pub(crate) src: NodeId,
11
+ pub(crate) dest: NodeId,
12
+ pub(crate) edge: EdgeId,
13
+ pub(crate) state: &'a [StateValue],
14
+ element: Option<&'a Value>,
15
+ }
16
+
17
+ impl<'a> EvalCtx<'a> {
18
+ pub(crate) fn new(
19
+ graph: &'a Graph,
20
+ src: NodeId,
21
+ dest: NodeId,
22
+ edge: EdgeId,
23
+ state: &'a [StateValue],
24
+ ) -> Self {
25
+ Self {
26
+ graph,
27
+ src,
28
+ dest,
29
+ edge,
30
+ state,
31
+ element: None,
32
+ }
33
+ }
34
+
35
+ pub(crate) fn with_state<'b>(&'b self, state: &'b [StateValue]) -> EvalCtx<'b> {
36
+ EvalCtx {
37
+ graph: self.graph,
38
+ src: self.src,
39
+ dest: self.dest,
40
+ edge: self.edge,
41
+ state,
42
+ element: self.element,
43
+ }
44
+ }
45
+
46
+ pub(crate) fn with_element<'b>(&'b self, element: &'b Value) -> EvalCtx<'b> {
47
+ EvalCtx {
48
+ graph: self.graph,
49
+ src: self.src,
50
+ dest: self.dest,
51
+ edge: self.edge,
52
+ state: self.state,
53
+ element: Some(element),
54
+ }
55
+ }
56
+ }
57
+
58
+ impl Expr<BoundColumn> {
59
+ pub(crate) fn eval(&self, ctx: &EvalCtx<'_>) -> Result<Value> {
60
+ match self {
61
+ Self::Column(column) => column.value(ctx),
62
+ Self::Element => ctx
63
+ .element
64
+ .cloned()
65
+ .context("pl.element() is only valid inside list.eval/list.filter"),
66
+ Self::Literal(value) => Ok(value.clone()),
67
+ Self::Alias(expr, _) => expr.eval(ctx),
68
+ Self::Ternary {
69
+ predicate,
70
+ truthy,
71
+ falsy,
72
+ } => {
73
+ if predicate.eval(ctx)?.truthy()? {
74
+ truthy.eval(ctx)
75
+ } else {
76
+ falsy.eval(ctx)
77
+ }
78
+ }
79
+ Self::Scalar(ScalarOp::And, args) => eval_and(args, ctx),
80
+ Self::Scalar(ScalarOp::Or, args) => eval_or(args, ctx),
81
+ Self::Scalar(op, args) => {
82
+ if let Some(value) = try_eval_scalar_fast_path(*op, args, ctx)? {
83
+ return Ok(value);
84
+ }
85
+ let args = eval_args(args, ctx)?;
86
+ op.eval(&args)
87
+ }
88
+ Self::String(op, args) => {
89
+ let args = eval_args(args, ctx)?;
90
+ op.eval(&args)
91
+ }
92
+ Self::List(op, args) => op.eval_with_exprs(args, ctx),
93
+ Self::Struct(op, args) => op.eval_with_exprs(args, ctx),
94
+ }
95
+ }
96
+ }
97
+
98
+ fn eval_args(args: &[Expr<BoundColumn>], ctx: &EvalCtx<'_>) -> Result<Vec<Value>> {
99
+ args.iter().map(|expr| expr.eval(ctx)).collect()
100
+ }
101
+
102
+ fn eval_and(args: &[Expr<BoundColumn>], ctx: &EvalCtx<'_>) -> Result<Value> {
103
+ let left = expr_arg(args, 0)?;
104
+ let right = expr_arg(args, 1)?;
105
+ // Short circuit (optimization)
106
+ if !left.eval(ctx)?.truthy()? {
107
+ return Ok(Value::Bool(false));
108
+ }
109
+ Ok(Value::Bool(right.eval(ctx)?.truthy()?))
110
+ }
111
+
112
+ fn eval_or(args: &[Expr<BoundColumn>], ctx: &EvalCtx<'_>) -> Result<Value> {
113
+ let left = expr_arg(args, 0)?;
114
+ let right = expr_arg(args, 1)?;
115
+ // Short circuit (optimization)
116
+ if left.eval(ctx)?.truthy()? {
117
+ return Ok(Value::Bool(true));
118
+ }
119
+ Ok(Value::Bool(right.eval(ctx)?.truthy()?))
120
+ }
121
+
122
+ // Handles primitive column/literal comparisons without boxing column values (optimization).
123
+ fn try_eval_scalar_fast_path(
124
+ op: ScalarOp,
125
+ args: &[Expr<BoundColumn>],
126
+ ctx: &EvalCtx<'_>,
127
+ ) -> Result<Option<Value>> {
128
+ if !matches!(
129
+ op,
130
+ ScalarOp::Eq
131
+ | ScalarOp::NotEq
132
+ | ScalarOp::Lt
133
+ | ScalarOp::LtEq
134
+ | ScalarOp::Gt
135
+ | ScalarOp::GtEq
136
+ ) {
137
+ return Ok(None);
138
+ }
139
+
140
+ let left = expr_arg(args, 0)?;
141
+ let right = expr_arg(args, 1)?;
142
+ if let (Some(column), Some(literal)) = (column_expr(left), literal_expr(right)) {
143
+ return column.eval_scalar_literal(ctx, op, literal, false);
144
+ }
145
+ if let (Some(literal), Some(column)) = (literal_expr(left), column_expr(right)) {
146
+ return column.eval_scalar_literal(ctx, op, literal, true);
147
+ }
148
+ Ok(None)
149
+ }
150
+
151
+ fn expr_arg(args: &[Expr<BoundColumn>], index: usize) -> Result<&Expr<BoundColumn>> {
152
+ args.get(index)
153
+ .with_context(|| format!("missing scalar op argument {index}"))
154
+ }
155
+
156
+ fn column_expr(expr: &Expr<BoundColumn>) -> Option<&BoundColumn> {
157
+ match expr {
158
+ Expr::Column(column) => Some(column),
159
+ Expr::Alias(expr, _) => column_expr(expr),
160
+ _ => None,
161
+ }
162
+ }
163
+
164
+ fn literal_expr(expr: &Expr<BoundColumn>) -> Option<&Value> {
165
+ match expr {
166
+ Expr::Literal(value) => Some(value),
167
+ Expr::Alias(expr, _) => literal_expr(expr),
168
+ _ => None,
169
+ }
170
+ }