1use std::sync::Arc;
2
3use crate::executor::expression::evaluator::compiled::{compiled_eval_enabled, CompiledExpr};
4use crate::executor::expression::evaluator::ExpressionEvaluator;
5use crate::executor::streaming::chunk::DataChunk;
6use crate::executor::streaming::executor::StreamingExecutor;
7use crate::executor::streaming::executor::ValueRowContext;
8use crate::executor::streaming::operators::source_operator::OperatorConfig;
9use crate::executor::streaming::runtime::ExecutionRuntime;
10use crate::executor::streaming::slot::SlotLayout;
11use crate::executor::streaming::subquery::{EvalEnv, SubqueryExecutor};
12use linkrs_core::error::QueryError;
13use linkrs_core::types::expr::Expression;
14use linkrs_core::Value;
15
16mod append_vertices;
17mod assign;
18mod dedup;
19mod filter;
20mod flatten;
21mod limit;
22mod project;
23mod sample;
24mod unwind;
25
26#[derive(Debug, Default)]
27pub struct UnaryOperatorState {
28 pub env: EvalEnv,
29 pub compiled_predicate: Option<CompiledExpr>,
32 pub compiled_project: Option<Vec<CompiledExpr>>,
34}
35
36#[derive(Debug)]
37pub enum UnaryOperatorKind {
38 Filter {
39 predicate: Expression,
40 state: UnaryOperatorState,
41 },
42 Project {
43 output_expressions: Vec<Expression>,
44 output_col_names: Vec<String>,
45 state: UnaryOperatorState,
46 },
47 Limit {
48 offset: u32,
49 limit: u32,
50 skipped: u32,
51 consumed: u32,
52 },
53 Dedup {
54 seen_rows: std::collections::HashSet<Vec<Value>>,
55 },
56 Assign {
57 assignments: Vec<(String, Expression)>,
58 state: UnaryOperatorState,
59 },
60 Unwind {
61 unwind_column: String,
62 list_expression: Option<Expression>,
63 col_index: Option<usize>,
64 layout: Option<Arc<SlotLayout>>,
65 all_rows: Vec<Vec<Value>>,
66 input_multiplicity: u64,
69 current_row_index: usize,
70 current_unwind_index: usize,
71 input_done: bool,
72 },
73 AppendVertices {
74 entity_var: String,
75 entity_expr: Expression,
76 prop_names: Vec<Arc<str>>,
77 tag: String,
78 storage: Option<Arc<parking_lot::RwLock<dyn crate::storage::QueryStorage>>>,
79 space_name: String,
80 state: UnaryOperatorState,
81 },
82 Sample {
83 count: u64,
84 consumed: u64,
85 },
86 Flatten {
87 group_pos: u32,
88 group_columns: Vec<String>,
89 expected_groups: Option<u32>,
90 current_idx: usize,
91 size_to_flatten: usize,
92 saved_sel_vector: Option<Vec<usize>>,
93 buffered_chunk: Option<DataChunk>,
94 batch_size: usize,
98 },
99}
100
101#[derive(Debug)]
107pub struct UnaryOperator {
108 pub kind: UnaryOperatorKind,
109 pub runtime: Option<Arc<ExecutionRuntime>>,
110 pub output_layout: Arc<SlotLayout>,
111 pub config: OperatorConfig,
112}
113
114impl UnaryOperatorKind {
115 pub fn flatten_group_pos(&self) -> Option<u32> {
117 match self {
118 UnaryOperatorKind::Flatten { group_pos, .. } => Some(*group_pos),
119 _ => None,
120 }
121 }
122}
123
124impl UnaryOperator {
125 pub fn from_spec(spec: &super::spec::UnarySpec, output_layout: Arc<SlotLayout>) -> Self {
127 let state = UnaryOperatorState::default();
128 let kind = match spec {
129 super::spec::UnarySpec::Filter {
130 predicate,
131 subquery_runners: _,
132 } => UnaryOperatorKind::Filter {
133 predicate: predicate.clone(),
134 state,
135 },
136 super::spec::UnarySpec::Project {
137 output_expressions,
138 output_col_names,
139 ..
140 } => UnaryOperatorKind::Project {
141 output_expressions: output_expressions.clone(),
142 output_col_names: output_col_names.clone(),
143 state,
144 },
145 super::spec::UnarySpec::Limit { offset, limit } => UnaryOperatorKind::Limit {
146 offset: *offset,
147 limit: *limit,
148 skipped: 0,
149 consumed: 0,
150 },
151 super::spec::UnarySpec::Assign {
152 assignments,
153 subquery_runners: _,
154 } => UnaryOperatorKind::Assign {
155 assignments: assignments.clone(),
156 state,
157 },
158 super::spec::UnarySpec::Unwind {
159 unwind_column,
160 list_expression,
161 } => UnaryOperatorKind::Unwind {
162 unwind_column: unwind_column.clone(),
163 list_expression: list_expression.clone(),
164 col_index: None,
165 layout: None,
166 all_rows: Vec::new(),
167 input_multiplicity: 1,
168 current_row_index: 0,
169 current_unwind_index: 0,
170 input_done: false,
171 },
172 super::spec::UnarySpec::AppendVertices {
173 space_name,
174 tag,
175 entity_var,
176 entity_expr,
177 prop_names,
178 } => UnaryOperatorKind::AppendVertices {
179 entity_var: entity_var.clone(),
180 entity_expr: entity_expr.clone(),
181 prop_names: prop_names
182 .iter()
183 .map(|name| Arc::from(name.as_str()))
184 .collect(),
185 tag: tag.clone(),
186 storage: None,
187 space_name: space_name.clone(),
188 state,
189 },
190 super::spec::UnarySpec::Sample { count } => UnaryOperatorKind::Sample {
191 count: *count,
192 consumed: 0,
193 },
194 super::spec::UnarySpec::Flatten {
195 group_pos,
196 group_columns,
197 expected_groups,
198 } => UnaryOperatorKind::Flatten {
199 group_pos: *group_pos,
200 group_columns: group_columns.clone(),
201 expected_groups: *expected_groups,
202 current_idx: 0,
203 size_to_flatten: 0,
204 saved_sel_vector: None,
205 buffered_chunk: None,
206 batch_size:
207 crate::executor::streaming::operators::flatten::DEFAULT_FLATTEN_BATCH_SIZE,
208 },
209 };
210 Self::new(kind, output_layout)
211 }
212
213 pub fn new(kind: UnaryOperatorKind, output_layout: Arc<SlotLayout>) -> Self {
214 Self {
215 kind,
216 runtime: None,
217 output_layout,
218 config: OperatorConfig::default(),
219 }
220 }
221
222 pub fn inject_context(
225 &mut self,
226 runtime: Option<&Arc<ExecutionRuntime>>,
227 config: OperatorConfig,
228 ) {
229 if let Some(rt) = runtime {
230 self.runtime = Some(rt.clone());
231 }
232 self.config = config;
233 }
234
235 pub fn set_subquery_executor(&mut self, executor: Arc<SubqueryExecutor>) {
239 match &mut self.kind {
240 UnaryOperatorKind::Filter { state, .. }
241 | UnaryOperatorKind::Project { state, .. }
242 | UnaryOperatorKind::Assign { state, .. } => {
243 state.env.subquery_executor = Some(executor);
244 }
245 _ => {}
246 }
247 }
248
249 pub fn open(&mut self, input: &mut StreamingExecutor) -> Result<(), QueryError> {
250 let params = self.runtime.as_ref().and_then(|rt| rt.parameter_values());
251 let session_variables = self
252 .runtime
253 .as_ref()
254 .and_then(|rt| rt.session_variable_values());
255 let sequence_manager = self.runtime.as_ref().and_then(|rt| rt.sequence_manager());
256 let storage = self.runtime.as_ref().and_then(|rt| rt.storage.clone());
257 match &mut self.kind {
258 UnaryOperatorKind::Filter { state, .. }
259 | UnaryOperatorKind::Project { state, .. }
260 | UnaryOperatorKind::Assign { state, .. }
261 | UnaryOperatorKind::AppendVertices { state, .. } => {
262 state.env.params = params;
263 state.env.session_variables = session_variables;
264 state.env.sequence_manager = sequence_manager;
265 }
266 _ => {}
267 }
268 if let UnaryOperatorKind::AppendVertices {
269 storage: target, ..
270 } = &mut self.kind
271 {
272 *target = storage;
273 }
274 if let UnaryOperatorKind::Flatten {
285 group_pos,
286 group_columns,
287 expected_groups,
288 ..
289 } = &self.kind
290 {
291 match expected_groups {
292 Some(count) if *group_pos >= *count => {
293 return Err(QueryError::execution(format!(
294 "Flatten(group={group_pos}): position out of range for {count} group(s)"
295 )));
296 }
297 Some(count) => {
298 log::debug!(
299 "flatten(group={group_pos}): position within {count} group(s), mapped columns: [{}]",
300 group_columns.join(", ")
301 );
302 }
303 None => {
304 log::debug!(
305 "flatten(group={group_pos}): unknown group count; skipping position validation"
306 );
307 }
308 }
309 }
310 input.open()?;
311 Ok(())
312 }
313
314 pub fn next(&mut self, input: &mut StreamingExecutor) -> Result<Option<DataChunk>, QueryError> {
315 if matches!(&self.kind, UnaryOperatorKind::Filter { .. }) {
316 return filter::handle(self, input);
317 }
318 if matches!(&self.kind, UnaryOperatorKind::Project { .. }) {
319 return project::handle(self, input);
320 }
321 if matches!(&self.kind, UnaryOperatorKind::Limit { .. }) {
322 return limit::handle(self, input);
323 }
324 if matches!(&self.kind, UnaryOperatorKind::Dedup { .. }) {
325 return dedup::handle(self, input);
326 }
327 if matches!(&self.kind, UnaryOperatorKind::Assign { .. }) {
328 return assign::handle(self, input);
329 }
330 if matches!(&self.kind, UnaryOperatorKind::Unwind { .. }) {
331 return unwind::handle(self, input);
332 }
333 if matches!(&self.kind, UnaryOperatorKind::AppendVertices { .. }) {
334 return append_vertices::handle(self, input);
335 }
336 if matches!(&self.kind, UnaryOperatorKind::Sample { .. }) {
337 return sample::handle(self, input);
338 }
339 if matches!(&self.kind, UnaryOperatorKind::Flatten { .. }) {
340 return flatten::handle(self, input);
341 }
342 unreachable!("unary_operator::next called for an unknown kind")
343 }
344
345 pub fn stop(&mut self) -> Result<(), QueryError> {
346 Ok(())
347 }
348
349 pub fn reset(&mut self, input: &mut StreamingExecutor) -> Result<bool, QueryError> {
355 match &mut self.kind {
356 UnaryOperatorKind::Limit {
357 skipped, consumed, ..
358 } => {
359 *skipped = 0;
360 *consumed = 0;
361 }
362 UnaryOperatorKind::Dedup { seen_rows } => seen_rows.clear(),
363 UnaryOperatorKind::Unwind {
364 col_index,
365 layout,
366 all_rows,
367 input_multiplicity,
368 current_row_index,
369 current_unwind_index,
370 input_done,
371 ..
372 } => {
373 *col_index = None;
374 *layout = None;
375 all_rows.clear();
376 *input_multiplicity = 1;
377 *current_row_index = 0;
378 *current_unwind_index = 0;
379 *input_done = false;
380 }
381 UnaryOperatorKind::Sample { consumed, .. } => *consumed = 0,
382 UnaryOperatorKind::Flatten {
383 current_idx,
384 size_to_flatten,
385 saved_sel_vector,
386 buffered_chunk,
387 batch_size,
388 ..
389 } => {
390 *current_idx = 0;
391 *size_to_flatten = 0;
392 *saved_sel_vector = None;
393 *buffered_chunk = None;
394 *batch_size =
395 crate::executor::streaming::operators::flatten::DEFAULT_FLATTEN_BATCH_SIZE;
396 }
397 UnaryOperatorKind::Filter { .. }
398 | UnaryOperatorKind::Project { .. }
399 | UnaryOperatorKind::Assign { .. }
400 | UnaryOperatorKind::AppendVertices { .. } => {}
401 }
402 input.reset()?;
403 Ok(false)
404 }
405
406 pub fn close(&mut self) -> Result<(), QueryError> {
407 if let UnaryOperatorKind::Flatten { buffered_chunk, .. } = &mut self.kind {
408 if let Some(chunk) = buffered_chunk.take() {
409 if let Some(stats) = chunk.columnar_stats {
410 stats.record_selection_materialized();
411 }
412 }
413 }
414 Ok(())
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::executor::base::MemoryBudget;
422 use crate::executor::streaming::operators::base::OperatorBase;
423 use crate::executor::streaming::operators::source_operator::SourceOperator;
424 use crate::executor::streaming::operators::source_operator::SourceOperatorKind;
425 use crate::executor::streaming::runtime::{ExecutionRuntime, QueryIdentity};
426 use crate::storage::StorageWriter;
427 use linkrs_core::types::storage_ids::VertexId;
428 use linkrs_core::{Tag, Vertex};
429 use parking_lot::RwLock;
430
431 fn runtime_with_storage(
432 storage: Arc<RwLock<dyn crate::storage::QueryStorage>>,
433 ) -> Arc<ExecutionRuntime> {
434 Arc::new(ExecutionRuntime::new(
435 QueryIdentity::default(),
436 MemoryBudget::new(1024 * 1024),
437 Some(storage),
438 crate::executor::base::SearchContext::default(),
439 ))
440 }
441
442 fn scan_source(rows: Vec<Vec<Value>>, col_names: Vec<String>) -> Box<StreamingExecutor> {
443 Box::new(StreamingExecutor::Source(
444 OperatorBase::new(0),
445 SourceOperator::new(
446 SourceOperatorKind::ScanVertices {
447 buffer: rows,
448 current_index: 0,
449 col_names,
450 },
451 Arc::new(SlotLayout::new(Vec::new())),
452 ),
453 ))
454 }
455
456 #[test]
457 fn append_vertices_fetches_vertex_and_appends_flat_columns() {
458 let mut mock = crate::storage::MockStorage::new().expect("MockStorage should be created");
459 mock.insert_vertex(
460 "test",
461 Vertex::new(
462 VertexId::try_from_int64(1).expect("valid vertex id"),
463 Tag::new(
464 "person".to_string(),
465 vec![
466 ("name".into(), Value::string("Alice")),
467 ("age".into(), Value::Int(30)),
468 ]
469 .into_iter()
470 .collect(),
471 ),
472 ),
473 )
474 .expect("insert vertex");
475 let storage: Arc<RwLock<dyn crate::storage::QueryStorage>> = Arc::new(RwLock::new(mock));
476
477 let input = scan_source(vec![vec![Value::Int(1)]], vec!["vid".to_string()]);
482 let mut append = StreamingExecutor::Unary(
483 OperatorBase::new(0),
484 input,
485 UnaryOperator::new(
486 UnaryOperatorKind::AppendVertices {
487 entity_var: "v".to_string(),
488 entity_expr: Expression::Variable("vid".to_string()),
489 prop_names: vec![Arc::from("name"), Arc::from("age")],
490 tag: "person".to_string(),
491 storage: None,
492 space_name: "test".to_string(),
493 state: UnaryOperatorState::default(),
494 },
495 Arc::new(SlotLayout::new(Vec::new())),
496 ),
497 );
498 append.base_mut().runtime = Some(runtime_with_storage(storage.clone()));
499 append.open().expect("open should succeed");
500 let chunk = append.advance().expect("advance should succeed");
501 let chunk = chunk.expect("one output chunk");
502 assert_eq!(chunk.len(), 1);
503 let row = &chunk.rows[0];
504 assert_eq!(row.len(), 3, "vid + name + age");
505 assert_eq!(row[0], Value::Int(1));
506 assert_eq!(row[1], Value::string("Alice"));
507 assert_eq!(row[2], Value::Int(30));
508 assert!(append.advance().expect("advance").is_none());
509 }
510
511 #[test]
512 fn append_vertices_full_value_appends_vertex_object() {
513 let mut mock = crate::storage::MockStorage::new().expect("MockStorage should be created");
514 mock.insert_vertex(
515 "test",
516 Vertex::new(
517 VertexId::try_from_int64(7).expect("valid vertex id"),
518 Tag::new(
519 "person".to_string(),
520 vec![("name".into(), Value::string("Bob"))]
521 .into_iter()
522 .collect(),
523 ),
524 ),
525 )
526 .expect("insert vertex");
527 let storage: Arc<RwLock<dyn crate::storage::QueryStorage>> = Arc::new(RwLock::new(mock));
528
529 let input = scan_source(vec![vec![Value::Int(7)]], vec!["vid".to_string()]);
530 let mut append = StreamingExecutor::Unary(
531 OperatorBase::new(0),
532 input,
533 UnaryOperator::new(
534 UnaryOperatorKind::AppendVertices {
535 entity_var: "v".to_string(),
536 entity_expr: Expression::Variable("vid".to_string()),
537 prop_names: vec![],
538 tag: "person".to_string(),
539 storage: None,
540 space_name: "test".to_string(),
541 state: UnaryOperatorState::default(),
542 },
543 Arc::new(SlotLayout::new(Vec::new())),
544 ),
545 );
546 append.base_mut().runtime = Some(runtime_with_storage(storage.clone()));
547 append.open().expect("open should succeed");
548 let chunk = append.advance().expect("advance").expect("one chunk");
549 let row = &chunk.rows[0];
550 assert_eq!(row.len(), 2, "vid + full vertex");
551 match &row[1] {
552 Value::Vertex(vertex) => {
553 assert_eq!(
554 vertex.vid,
555 VertexId::try_from_int64(7).expect("valid vertex id")
556 );
557 assert_eq!(vertex.property_value("name"), Some(Value::string("Bob")));
558 }
559 other => panic!("expected full vertex, got {:?}", other),
560 }
561 }
562
563 #[test]
564 fn append_vertices_missing_vertex_yields_null_columns() {
565 let storage: Arc<RwLock<dyn crate::storage::QueryStorage>> = Arc::new(RwLock::new(
566 crate::storage::MockStorage::new().expect("MockStorage should be created"),
567 ));
568 let input = scan_source(
569 vec![vec![Value::string("missing")]],
570 vec!["vid".to_string()],
571 );
572 let mut append = StreamingExecutor::Unary(
573 OperatorBase::new(0),
574 input,
575 UnaryOperator::new(
576 UnaryOperatorKind::AppendVertices {
577 entity_var: "v".to_string(),
578 entity_expr: Expression::Variable("vid".to_string()),
579 prop_names: vec![Arc::from("name")],
580 tag: "person".to_string(),
581 storage: None,
582 space_name: "test".to_string(),
583 state: UnaryOperatorState::default(),
584 },
585 Arc::new(SlotLayout::new(Vec::new())),
586 ),
587 );
588 append.base_mut().runtime = Some(runtime_with_storage(storage.clone()));
589 append.open().expect("open should succeed");
590 let chunk = append.advance().expect("advance").expect("one chunk");
591 let row = &chunk.rows[0];
592 assert_eq!(row.len(), 2);
593 assert!(matches!(row[1], Value::Null(_)));
594 }
595
596 #[test]
597 fn flatten_group_pos_is_observable() {
598 let kind = UnaryOperatorKind::Flatten {
599 group_pos: 7,
600 group_columns: vec!["a".to_string()],
601 expected_groups: Some(8),
602 current_idx: 0,
603 size_to_flatten: 0,
604 saved_sel_vector: None,
605 buffered_chunk: None,
606 batch_size: crate::executor::streaming::operators::flatten::DEFAULT_FLATTEN_BATCH_SIZE,
607 };
608 assert_eq!(kind.flatten_group_pos(), Some(7));
609 let filter = UnaryOperatorKind::Limit {
610 offset: 0,
611 limit: 1,
612 skipped: 0,
613 consumed: 0,
614 };
615 assert_eq!(filter.flatten_group_pos(), None);
616 }
617
618 fn flatten_op_with_mapping(
619 group_columns: Vec<String>,
620 expected_groups: Option<u32>,
621 layout_names: &[&str],
622 ) -> (UnaryOperator, Box<StreamingExecutor>) {
623 let layout = Arc::new(SlotLayout::from_names(
624 &layout_names
625 .iter()
626 .map(|s| s.to_string())
627 .collect::<Vec<_>>(),
628 ));
629 let op = UnaryOperator::from_spec(
630 &crate::executor::streaming::operators::spec::UnarySpec::Flatten {
631 group_pos: 1,
632 group_columns,
633 expected_groups,
634 },
635 layout,
636 );
637 let input = scan_source(
638 vec![vec![Value::Int(1)]],
639 layout_names.iter().map(|s| s.to_string()).collect(),
640 );
641 (op, input)
642 }
643
644 #[test]
645 fn flatten_open_accepts_position_within_expected_groups() {
646 let (mut op, mut input) =
647 flatten_op_with_mapping(vec!["b".to_string()], Some(2), &["a", "b"]);
648 assert!(op.open(&mut input).is_ok());
649 }
650
651 #[test]
652 fn flatten_open_without_expected_groups_skips_validation() {
653 let (mut op, mut input) = flatten_op_with_mapping(vec![], None, &["a", "b"]);
654 assert!(op.open(&mut input).is_ok());
655 }
656
657 #[test]
658 fn flatten_open_rejects_stale_group_position() {
659 let (mut op, mut input) =
663 flatten_op_with_mapping(vec!["b".to_string()], Some(1), &["a", "b"]);
664 let err = op
665 .open(&mut input)
666 .expect_err("stale group position must fail");
667 assert!(
668 err.to_string().contains("out of range"),
669 "error must report the range violation, got: {err}"
670 );
671 }
672
673 #[test]
674 fn append_vertices_without_storage_fails() {
675 let input = scan_source(vec![vec![Value::Int(1)]], vec!["vid".to_string()]);
676 let mut append = StreamingExecutor::Unary(
677 OperatorBase::new(0),
678 input,
679 UnaryOperator::new(
680 UnaryOperatorKind::AppendVertices {
681 entity_var: "v".to_string(),
682 entity_expr: Expression::Variable("vid".to_string()),
683 prop_names: vec![Arc::from("name")],
684 tag: "person".to_string(),
685 storage: None,
686 space_name: "test".to_string(),
687 state: UnaryOperatorState::default(),
688 },
689 Arc::new(SlotLayout::new(Vec::new())),
690 ),
691 );
692 append.open().expect("open should succeed");
693 let error = append.advance().expect_err("storage required");
694 assert!(error.to_string().contains("requires storage"));
695 }
696}
697
698#[cfg(test)]
699mod project_fast_path_tests {
700 use super::project::is_passthrough_or_const;
701 use super::*;
702 use linkrs_core::Value;
703
704 fn layout() -> Arc<SlotLayout> {
705 Arc::new(SlotLayout::from_names(&[
706 "n".to_string(),
707 "n.age".to_string(),
708 ]))
709 }
710
711 fn chunk_with_selection() -> DataChunk {
712 let rows = vec![
713 vec![Value::Int(1), Value::Int(30)],
714 vec![
715 Value::Int(2),
716 Value::Null(linkrs_core::value::NullType::Null),
717 ],
718 vec![Value::Int(3), Value::Int(40)],
719 ];
720 DataChunk::new_with_layout(rows, layout())
721 .with_selection(vec![0, 2])
722 .with_multiplicity(3)
723 }
724
725 #[test]
726 fn test_is_passthrough_or_const() {
727 assert!(is_passthrough_or_const(&Expression::Variable(
728 "n".to_string()
729 )));
730 assert!(is_passthrough_or_const(&Expression::Literal(Value::Int(1))));
731 assert!(is_passthrough_or_const(&Expression::Property {
732 object: Box::new(Expression::Variable("n".to_string())),
733 property: "age".to_string(),
734 }));
735 assert!(!is_passthrough_or_const(&Expression::Binary {
736 left: Box::new(Expression::Variable("n".to_string())),
737 op: linkrs_core::types::operators::BinaryOperator::Add,
738 right: Box::new(Expression::Literal(Value::Int(1))),
739 }));
740 }
741
742 #[test]
743 fn test_project_fast_path_gathers_all_rows() {
744 let exprs = vec![
747 Expression::Variable("n".to_string()),
748 Expression::Property {
749 object: Box::new(Expression::Variable("n".to_string())),
750 property: "age".to_string(),
751 },
752 Expression::Literal(Value::Int(7)),
753 ];
754 let mut state = UnaryOperatorState::default();
755 let rows = vec![
756 vec![Value::Int(1), Value::Int(30)],
757 vec![
758 Value::Int(2),
759 Value::Null(linkrs_core::value::NullType::Null),
760 ],
761 vec![Value::Int(3), Value::Int(40)],
762 ];
763 let mut chunk = DataChunk::new_with_layout(rows, layout());
764 let fast = UnaryOperator::evaluate_project_expressions(&mut chunk, &exprs, &mut state)
765 .expect("fast path evaluates");
766 assert_eq!(
767 fast,
768 vec![
769 vec![Value::Int(1), Value::Int(2), Value::Int(3)],
770 vec![
771 Value::Int(30),
772 Value::Null(linkrs_core::value::NullType::Null),
773 Value::Int(40)
774 ],
775 vec![Value::Int(7), Value::Int(7), Value::Int(7)],
776 ]
777 );
778
779 let mut selected = chunk_with_selection();
782 let mut visible = Vec::new();
783 for expr in &exprs {
784 visible.push(
785 selected
786 .evaluate_expression_visible(expr, Some(&state.env))
787 .expect("visible evaluates"),
788 );
789 }
790 let out_layout = Arc::new(SlotLayout::from_names(&[
791 "n".to_string(),
792 "n.age".to_string(),
793 "c".to_string(),
794 ]));
795 let out = DataChunk::project_columns(visible, Arc::clone(&out_layout)).with_multiplicity(3);
796 assert_eq!(out.rows.len(), 2);
797 assert_eq!(
798 out.rows[0],
799 vec![Value::Int(1), Value::Int(30), Value::Int(7)]
800 );
801 assert_eq!(
802 out.rows[1],
803 vec![Value::Int(3), Value::Int(40), Value::Int(7)]
804 );
805 assert_eq!(out.multiplicity(), 3);
806 }
807}