surrealdb-core 3.2.3

A scalable, distributed, collaborative, document-graph database, for the realtime web
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
//! Project operator for field selection and transformation.
//!
//! This module provides two operators:
//!
//! - [`Project`]: The original operator that evaluates expressions for field values
//! - [`SelectProject`]: A simplified operator that only does field selection/renaming
//!
//! The `SelectProject` operator is designed for the consolidated expression evaluation
//! approach, where complex expressions are pre-computed by a `Compute` operator and
//! Project only needs to select/rename fields for the final output.

use std::sync::Arc;

use futures::StreamExt;
use tracing::instrument;

use crate::exec::field_path::FieldPath;
use crate::exec::{
	AccessMode, CardinalityHint, CombineAccessModes, ContextLevel, EvalContext, ExecOperator,
	ExecutionContext, FlowResult, OperatorMetrics, PhysicalExpr, ValueBatch, ValueBatchStream,
	buffer_stream, monitor_stream,
};
use crate::expr::idiom::Idiom;
use crate::expr::part::{DestructurePart, Part};
use crate::val::{Object, Strand, Value};

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

/// Wrap an output name string into a single-part [`FieldPath`].
///
/// The name is treated as an opaque flat key. Callers that need a nested
/// output path must supply a structurally multi-part [`FieldPath`] directly
/// via [`FieldSelection::from_field_path`] or
/// [`FieldSelection::with_alias_path`] — typically by calling
/// `idiom_to_field_path` on the parsed idiom, which preserves the
/// distinction between `AS foo.bar` (nested) and `` AS `foo.bar` ``
/// (flat, single Part::Field whose identifier contains a dot).
fn parse_output_path(name: &str) -> FieldPath {
	FieldPath::field(name.to_string())
}

/// Set a value at the given [`FieldPath`] on an [`Object`], consuming `value`.
///
/// Internally wraps the object in a `Value::Object` so that
/// `Value::set_at_field_path_owned` can be used, then unwraps the result back.
/// Taking `value` by value avoids the per-leaf clone that the borrowed form
/// pays inside the recursive path walk.
#[inline]
fn set_field_on_object(obj: &mut Object, path: &FieldPath, value: Value) {
	let mut target = Value::Object(std::mem::take(obj));
	target.set_at_field_path_owned(path, value);
	if let Value::Object(new_obj) = target {
		*obj = new_obj;
	}
}

/// Drain `writes` into `obj` and return it as a `Value::Object`.
///
/// The drain leaves `writes` empty (capacity retained) so the caller can reuse
/// it across rows without re-allocating.
#[inline]
fn apply_field_writes(mut obj: Object, writes: &mut Vec<(FieldPath, Value)>) -> Value {
	for (path, v) in writes.drain(..) {
		set_field_on_object(&mut obj, &path, v);
	}
	Value::Object(obj)
}

// ---------------------------------------------------------------------------
// FieldSelection
// ---------------------------------------------------------------------------

/// Field selection specification.
#[derive(Debug, Clone)]
pub struct FieldSelection {
	/// The output path for this field - determines where the value goes in the result.
	/// Uses FieldPath for proper nested object construction and array iteration.
	pub output_path: FieldPath,
	/// The expression to evaluate for this field's value
	pub expr: Arc<dyn PhysicalExpr>,
	/// Whether the output_path came from an explicit alias.
	/// When true, projection functions should use output_path instead of dynamic field names.
	pub has_explicit_alias: bool,
}

impl FieldSelection {
	/// Create a new field selection from an output name string.
	///
	/// The name is treated as an opaque flat key (a single-part
	/// [`FieldPath`]). Callers that need a nested output path must supply
	/// a structurally multi-part [`FieldPath`] directly via
	/// [`FieldSelection::from_field_path`] or
	/// [`FieldSelection::with_alias_path`] — typically by calling
	/// `idiom_to_field_path` on the parsed idiom, which preserves the
	/// distinction between `AS foo.bar` (nested) and `` AS `foo.bar` ``
	/// (flat).
	pub fn new(output_name: &str, expr: Arc<dyn PhysicalExpr>) -> Self {
		Self {
			output_path: parse_output_path(output_name),
			expr,
			has_explicit_alias: false,
		}
	}

	/// Create a new field selection with an explicit alias whose output path
	/// was derived directly from the parsed alias idiom.
	///
	/// Used when the user specified an alias in the query
	/// (e.g., `SELECT expr AS alias`). Preserves the single-part vs
	/// multi-part distinction (`` AS `foo.bar` `` vs `AS foo.bar`) without
	/// round-tripping through a string. For projection functions, the
	/// alias takes precedence over dynamic field names.
	pub fn with_alias_path(output_path: FieldPath, expr: Arc<dyn PhysicalExpr>) -> Self {
		Self {
			output_path,
			expr,
			has_explicit_alias: true,
		}
	}

	/// Create a new field selection from a FieldPath directly.
	/// Used for graph traversals without aliases where the path represents nested structure.
	pub fn from_field_path(output_path: FieldPath, expr: Arc<dyn PhysicalExpr>) -> Self {
		Self {
			output_path,
			expr,
			has_explicit_alias: false,
		}
	}
}

// ---------------------------------------------------------------------------
// Project operator
// ---------------------------------------------------------------------------

/// Project operator - selects and transforms fields from input records.
///
/// This is a pure transformation operator that evaluates expressions and builds
/// output objects. All permission checking occurs in the Scan operator.
#[derive(Debug, Clone)]
pub struct Project {
	/// The input plan to project from
	pub input: Arc<dyn ExecOperator>,
	/// The fields to select/project (shared across batches via Arc)
	pub fields: Arc<[FieldSelection]>,
	/// Fields to omit from output (for SELECT * OMIT) (shared across batches via Arc)
	pub omit: Arc<[Idiom]>,
	/// Whether to include all fields from input (for SELECT *, field1, field2)
	pub include_all: bool,
	/// Per-operator metrics for EXPLAIN ANALYZE
	pub(crate) metrics: Arc<OperatorMetrics>,
}

impl Project {
	/// Create a new Project operator with fresh metrics.
	pub(crate) fn new(
		input: Arc<dyn ExecOperator>,
		fields: Vec<FieldSelection>,
		omit: Vec<Idiom>,
		include_all: bool,
	) -> Self {
		Self {
			input,
			fields: fields.into(),
			omit: omit.into(),
			include_all,
			metrics: Arc::new(OperatorMetrics::new()),
		}
	}
}
impl ExecOperator for Project {
	fn name(&self) -> &'static str {
		"Project"
	}

	fn required_context(&self) -> ContextLevel {
		// Combine field expression contexts with child operator context.
		// When include_all is true, we additionally need database access
		// to dereference RecordIds.
		let fields_ctx = self
			.fields
			.iter()
			.map(|f| f.expr.required_context())
			.max()
			.unwrap_or(ContextLevel::Root);
		let base = self.input.required_context().max(fields_ctx);
		if self.include_all {
			base.max(ContextLevel::Database)
		} else {
			base
		}
	}

	fn access_mode(&self) -> AccessMode {
		// Combine input's mode with all field expressions
		// This is critical: a projection like `SELECT *, (UPSERT person) FROM person`
		// must return ReadWrite because the subquery mutates!
		let expr_mode = self.fields.iter().map(|f| f.expr.access_mode()).combine_all();
		self.input.access_mode().combine(expr_mode)
	}

	fn cardinality_hint(&self) -> CardinalityHint {
		self.input.cardinality_hint()
	}

	fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
		vec![&self.input]
	}

	fn metrics(&self) -> Option<&OperatorMetrics> {
		Some(&self.metrics)
	}

	fn expressions(&self) -> Vec<(&str, &Arc<dyn PhysicalExpr>)> {
		self.fields.iter().map(|f| ("field", &f.expr)).collect()
	}

	fn output_ordering(&self) -> crate::exec::OutputOrdering {
		self.input.output_ordering()
	}

	#[instrument(level = "trace", skip_all)]
	fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
		let input_stream = buffer_stream(
			self.input.execute(ctx)?,
			self.input.access_mode(),
			self.input.cardinality_hint(),
			ctx.root().ctx.config.operator_buffer_size,
		);
		let fields = Arc::clone(&self.fields);
		let omit = Arc::clone(&self.omit);
		let include_all = self.include_all;
		let ctx = ctx.clone();

		// Create a stream that projects fields
		let projected = input_stream.then(move |batch_result| {
			let fields = Arc::clone(&fields);
			let omit = Arc::clone(&omit);
			let ctx = ctx.clone();

			async move {
				let batch = batch_result?;
				let eval_ctx = EvalContext::from_exec_ctx(&ctx);

				let projected_values = if include_all {
					// --- include_all path: per-row processing ---
					// RecordId dereferencing and row-skipping requires per-row handling.
					let mut values = Vec::with_capacity(batch.values.len());
					if fields.is_empty() && omit.is_empty() {
						// Pure SELECT * — move values directly, no clone needed
						for value in batch.values {
							match value {
								Value::RecordId(rid) => {
									let fetched =
										super::fetch::fetch_record(eval_ctx.exec_ctx, &rid).await?;
									if !matches!(fetched, Value::None) {
										values.push(fetched);
									}
								}
								other => values.push(other),
							}
						}
					} else {
						// Reused across rows; `drain(..)` empties it for the next
						// iteration while keeping the backing buffer.
						let mut field_writes: Vec<(FieldPath, Value)> =
							Vec::with_capacity(fields.len());
						for value in batch.values {
							// For RecordId inputs, resolve the target record FIRST.
							// When the record is missing or resolves to a non-Object,
							// field expressions must not run — field expressions can
							// have side effects (mutating subqueries) that must not
							// fire for rows we'll then drop or pass through raw.
							let fetched_obj: Option<Object> = if let Value::RecordId(rid) = &value {
								match super::fetch::fetch_record(eval_ctx.exec_ctx, rid).await? {
									Value::Object(obj) => Some(obj),
									Value::None => continue,
									mut raw => {
										for field in omit.iter() {
											omit_field_sync(&mut raw, field);
										}
										values.push(raw);
										continue;
									}
								}
							} else {
								None
							};

							// Evaluate every field expression against the original
							// `value` (still borrowed by `row_ctx`); writes are
							// applied later, after `value` is freely movable.
							field_writes.clear();
							if !fields.is_empty() {
								let row_ctx = eval_ctx.with_value_and_doc(&value);
								for field in fields.iter() {
									compute_field_writes(field, row_ctx.clone(), &mut field_writes)
										.await?;
								}
							}

							// Materialize the output. `value` is no longer borrowed,
							// so its Object payload can be moved out instead of
							// deep-cloned.
							let mut output_value = if let Some(obj) = fetched_obj {
								apply_field_writes(obj, &mut field_writes)
							} else {
								match value {
									Value::Object(obj) => {
										apply_field_writes(obj, &mut field_writes)
									}
									other if fields.is_empty() => other,
									_ => apply_field_writes(Object::default(), &mut field_writes),
								}
							};

							for field in omit.iter() {
								omit_field_sync(&mut output_value, field);
							}
							values.push(output_value);
						}
					}
					values
				} else {
					// --- Batch per-field evaluation for non-include_all ---
					// Evaluate each field expression across all rows in one batch call,
					// then assemble per-row objects from the results.
					let batch_len = batch.values.len();
					let mut objects: Vec<Object> =
						(0..batch_len).map(|_| Object::default()).collect();

					for field in fields.iter() {
						if field.expr.is_projection_function() {
							// Projection functions return multiple field bindings;
							// handle per-row since they need special object assembly.
							for (i, value) in batch.values.iter().enumerate() {
								evaluate_and_set_field(
									&mut objects[i],
									field,
									eval_ctx.with_value_and_doc(value),
								)
								.await?;
							}
						} else {
							// Batch evaluation: use evaluate_batch which allows
							// I/O-bound expressions (subqueries, lookups) to
							// parallelize. For simple field accesses, the default
							// sequential implementation is used.
							let field_values =
								field.expr.evaluate_batch(eval_ctx.clone(), &batch.values).await?;
							for (i, field_value) in field_values.into_iter().enumerate() {
								set_field_on_object(
									&mut objects[i],
									&field.output_path,
									field_value,
								);
							}
						}
					}

					let mut values: Vec<Value> = objects.into_iter().map(Value::Object).collect();
					if !omit.is_empty() {
						for val in &mut values {
							for field in omit.iter() {
								omit_field_sync(val, field);
							}
						}
					}
					values
				};

				Ok(ValueBatch {
					values: projected_values,
				})
			}
		});

		Ok(monitor_stream(Box::pin(projected), "Project", &self.metrics))
	}
}

// ---------------------------------------------------------------------------
// evaluate_and_set_field
// ---------------------------------------------------------------------------

/// Evaluate a field expression and collect the resulting `(FieldPath, Value)`
/// writes into `writes`, without touching any output object.
///
/// Used by the `include_all` path so that field expressions can be evaluated
/// while the input row is still borrowed (for `row_ctx`), then applied later
/// to an output Object that has been moved out of the input value — avoiding
/// the deep clone of the input Object that mutating-in-place would force.
///
/// Semantics mirror [`evaluate_and_set_field`]:
/// - Projection functions with an explicit alias collapse to one write at the alias path (single
///   binding) or one write of an Array (multiple bindings).
/// - Projection functions without an alias produce one write per binding, keyed by the binding's
///   idiom.
/// - Regular expressions produce one write at `field.output_path`.
async fn compute_field_writes(
	field: &FieldSelection,
	eval_ctx: EvalContext<'_>,
	writes: &mut Vec<(FieldPath, Value)>,
) -> Result<(), crate::expr::ControlFlow> {
	if field.expr.is_projection_function()
		&& let Some(bindings) = field.expr.evaluate_projection(eval_ctx.clone()).await?
	{
		if field.has_explicit_alias {
			let value = if bindings.len() == 1 {
				bindings.into_iter().next().expect("bindings verified non-empty").1
			} else {
				Value::Array(bindings.into_iter().map(|(_, v)| v).collect::<Vec<_>>().into())
			};
			writes.push((field.output_path.clone(), value));
		} else {
			for (idiom, value) in bindings {
				if let Ok(path) = FieldPath::try_from(&idiom)
					&& !path.is_empty()
				{
					writes.push((path, value));
				}
			}
		}
		return Ok(());
	}

	let field_value = field.expr.evaluate(eval_ctx).await?;
	writes.push((field.output_path.clone(), field_value));
	Ok(())
}

/// Evaluate a field expression and set the resulting value(s) on the output object.
///
/// For regular expressions, evaluates the expression and sets the result at the output_path.
/// For projection functions:
/// - If has_explicit_alias is true, use the alias (output_path) as the field name
/// - Otherwise, use the dynamic field names from the function result
async fn evaluate_and_set_field(
	obj: &mut Object,
	field: &FieldSelection,
	eval_ctx: EvalContext<'_>,
) -> Result<(), crate::expr::ControlFlow> {
	if field.expr.is_projection_function()
		&& let Some(bindings) = field.expr.evaluate_projection(eval_ctx.clone()).await?
	{
		if field.has_explicit_alias {
			// User provided an alias - use it as the field name.
			// For multiple bindings, collect values into an array.
			let value = if bindings.len() == 1 {
				bindings.into_iter().next().expect("bindings verified non-empty").1
			} else {
				Value::Array(bindings.into_iter().map(|(_, v)| v).collect::<Vec<_>>().into())
			};
			set_field_on_object(obj, &field.output_path, value);
		} else {
			// No alias - use the dynamic field names from the function
			for (idiom, value) in bindings {
				if let Ok(path) = FieldPath::try_from(&idiom)
					&& !path.is_empty()
				{
					set_field_on_object(obj, &path, value);
				}
			}
		}
		return Ok(());
		// Fall through to regular evaluation if not actually a projection function
	}

	let field_value = field.expr.evaluate(eval_ctx).await?;
	set_field_on_object(obj, &field.output_path, field_value);
	Ok(())
}

// ---------------------------------------------------------------------------
// OMIT helpers
// ---------------------------------------------------------------------------

/// Synchronously remove a field from a value by idiom path.
#[inline]
pub(crate) fn omit_field_sync(value: &mut Value, idiom: &Idiom) {
	// For simple single-part idioms, directly remove from object
	if idiom.len() == 1 {
		if let Some(Part::Field(field_name)) = idiom.first()
			&& let Value::Object(obj) = value
		{
			obj.remove(field_name);
		}
	} else {
		// For nested paths, traverse and remove
		omit_nested_field(value, idiom, 0);
	}
}

/// Recursively traverse and remove a nested field.
fn omit_nested_field(value: &mut Value, idiom: &Idiom, depth: usize) {
	let Some(part) = idiom.get(depth) else {
		return;
	};
	let is_last = depth == idiom.len() - 1;

	match part {
		Part::Field(field_name) => {
			if let Value::Object(obj) = value {
				if is_last {
					obj.remove(field_name);
				} else if let Some(nested) = obj.get_mut(field_name) {
					omit_nested_field(nested, idiom, depth + 1);
				}
			}
		}
		Part::All => {
			if is_last {
				match value {
					Value::Object(obj) => obj.clear(),
					Value::Array(arr) => arr.clear(),
					_ => {}
				}
			} else {
				match value {
					Value::Object(obj) => {
						for (_, v) in obj.iter_mut() {
							omit_nested_field(v, idiom, depth + 1);
						}
					}
					Value::Array(arr) => {
						for v in arr.iter_mut() {
							omit_nested_field(v, idiom, depth + 1);
						}
					}
					_ => {}
				}
			}
		}
		Part::Value(expr) => {
			// Handle array index access: [0], [1], etc.
			if let crate::expr::Expr::Literal(crate::expr::Literal::Integer(idx)) = expr
				&& let Value::Array(arr) = value
				&& let Some(nested) = arr.get_mut(*idx as usize)
			{
				if is_last {
					// Can't "remove" an array element by index, set to None
					*nested = Value::None;
				} else {
					omit_nested_field(nested, idiom, depth + 1);
				}
			}
		}
		Part::Destructure(destructure_parts) => {
			// Destructure in OMIT: remove the listed fields from the current object.
			// E.g., OMIT obj.c.{ d, f } removes d and f from obj.c.
			if let Value::Object(obj) = value {
				omit_destructure_fields(obj, destructure_parts);
			}
		}
		_ => {
			// Other part types are not supported for omit
		}
	}
}

/// Recursively remove fields described by destructure parts from an object.
fn omit_destructure_fields(obj: &mut Object, parts: &[DestructurePart]) {
	for dp in parts {
		match dp {
			DestructurePart::Field(name) | DestructurePart::All(name) => {
				obj.remove(name.as_str());
			}
			DestructurePart::Destructure(name, nested) => {
				if let Some(Value::Object(inner)) = obj.get_mut(name.as_str()) {
					omit_destructure_fields(inner, nested);
				}
			}
			DestructurePart::Aliased(name, _) => {
				obj.remove(name.as_str());
			}
		}
	}
}

// ============================================================================
// SelectProject - Simplified projection without expression evaluation
// ============================================================================

/// Specifies how to handle a field in SelectProject.
///
/// Field names are stored as [`Strand`] rather than `String` so that per-row
/// inserts into the output [`Object`] only pay a 24-byte bitwise copy for
/// short names (the common case), avoiding the heap allocation that
/// `String::clone` would do on every row × projection.
#[derive(Debug, Clone)]
pub enum Projection {
	/// Include a field with its original name
	Include(Strand),
	/// Rename a field (from internal name to output name)
	Rename {
		from: Strand,
		to: Strand,
	},
	/// Include all fields from input (SELECT *)
	All,
	/// Exclude a field (for OMIT)
	Omit(Strand),
}

/// Simplified project operator that only does field selection and renaming.
///
/// This is designed for the consolidated expression evaluation approach:
/// 1. Complex expressions are pre-computed by a `Compute` operator
/// 2. `SelectProject` only needs to select/rename fields for output
/// 3. No expression evaluation occurs in this operator
///
/// Benefits:
/// - Clearer separation of concerns (Compute evaluates, SelectProject shapes)
/// - Simpler and faster execution (just field manipulation)
/// - Easier to reason about when expressions are evaluated
#[derive(Debug, Clone)]
pub struct SelectProject {
	/// The input plan to project from
	pub input: Arc<dyn ExecOperator>,
	/// The projections to apply (shared across batches via Arc)
	pub projections: Arc<[Projection]>,
	/// Per-operator metrics for EXPLAIN ANALYZE
	pub(crate) metrics: Arc<OperatorMetrics>,
}

impl SelectProject {
	/// Create a new SelectProject operator.
	pub fn new(
		input: Arc<dyn ExecOperator>,
		projections: Vec<Projection>,
		metrics: Arc<OperatorMetrics>,
	) -> Self {
		Self {
			input,
			projections: projections.into(),
			metrics,
		}
	}
}
impl ExecOperator for SelectProject {
	fn name(&self) -> &'static str {
		"SelectProject"
	}

	fn attrs(&self) -> Vec<(String, String)> {
		let proj_str = self
			.projections
			.iter()
			.map(|p| match p {
				Projection::Include(name) => name.to_string(),
				Projection::Rename {
					from,
					to,
				} => format!("{} AS {}", from, to),
				Projection::All => "*".to_string(),
				Projection::Omit(name) => format!("OMIT {}", name),
			})
			.collect::<Vec<_>>()
			.join(", ");
		vec![("projections".to_string(), proj_str)]
	}

	fn required_context(&self) -> ContextLevel {
		// When projections include All, we may need to dereference RecordIds,
		// which requires database access
		let has_all = self.projections.iter().any(|p| matches!(p, Projection::All));
		if has_all {
			ContextLevel::Database.max(self.input.required_context())
		} else {
			self.input.required_context()
		}
	}

	fn access_mode(&self) -> AccessMode {
		// SelectProject is pure field manipulation - inherits input's access mode
		self.input.access_mode()
	}

	fn cardinality_hint(&self) -> CardinalityHint {
		self.input.cardinality_hint()
	}

	fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
		vec![&self.input]
	}

	fn metrics(&self) -> Option<&OperatorMetrics> {
		Some(&self.metrics)
	}

	fn output_ordering(&self) -> crate::exec::OutputOrdering {
		self.input.output_ordering()
	}

	#[instrument(level = "trace", skip_all)]
	fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
		let input_stream = buffer_stream(
			self.input.execute(ctx)?,
			self.input.access_mode(),
			self.input.cardinality_hint(),
			ctx.root().ctx.config.operator_buffer_size,
		);
		let projections = Arc::clone(&self.projections);
		let ctx = ctx.clone();

		// Create a stream that applies projections
		let projected = input_stream.then(move |batch_result| {
			let projections = Arc::clone(&projections);
			let ctx = ctx.clone();

			async move {
				let batch = batch_result?;
				let mut projected_values = Vec::with_capacity(batch.values.len());

				for value in batch.values {
					let is_record_id = matches!(&value, Value::RecordId(_));
					let projected = apply_projections(value, &projections, &ctx).await?;
					if is_record_id && matches!(&projected, Value::None) {
						continue;
					}
					projected_values.push(projected);
				}

				Ok(ValueBatch {
					values: projected_values,
				})
			}
		});

		Ok(monitor_stream(Box::pin(projected), "SelectProject", &self.metrics))
	}
}

// ---------------------------------------------------------------------------
// SelectProject helpers
// ---------------------------------------------------------------------------

/// Apply projections to a single value, taking ownership to avoid cloning.
async fn apply_projections(
	value: Value,
	projections: &[Projection],
	ctx: &ExecutionContext,
) -> Result<Value, crate::expr::ControlFlow> {
	let has_all = projections.iter().any(|p| matches!(p, Projection::All));
	let has_includes =
		projections.iter().any(|p| matches!(p, Projection::Include(_) | Projection::Rename { .. }));

	let input_obj = match value {
		Value::Object(obj) => obj,
		Value::RecordId(rid) if has_all || has_includes => {
			match super::fetch::fetch_record(ctx, &rid).await? {
				Value::Object(obj) => obj,
				Value::None => return Ok(Value::None),
				other => return Ok(other),
			}
		}
		Value::Geometry(geo) if has_all || has_includes => geo.as_object(),
		other => return Ok(other),
	};

	Ok(apply_projections_to_object(input_obj, projections, has_all))
}

/// Apply projections to an already-resolved object (sync version).
/// Takes ownership of the input object to avoid cloning in the `has_all` path.
fn apply_projections_to_object(
	input_obj: Object,
	projections: &[Projection],
	has_all: bool,
) -> Value {
	if has_all {
		let mut output = input_obj;
		for projection in projections {
			match projection {
				Projection::Include(name) => {
					if !output.contains_key(name) {
						output.insert(name.clone(), Value::None);
					}
				}
				Projection::Rename {
					from,
					to,
				} => {
					let v = output.remove(from).unwrap_or(Value::None);
					output.insert(to.clone(), v);
				}
				Projection::Omit(name) => {
					output.remove(name.as_str());
				}
				Projection::All => {}
			}
		}
		Value::Object(output)
	} else {
		let mut output = Object::default();
		for projection in projections {
			match projection {
				Projection::Include(name) => {
					let v = input_obj.get(name).cloned().unwrap_or(Value::None);
					output.insert(name.clone(), v);
				}
				Projection::Rename {
					from,
					to,
				} => {
					let v = input_obj.get(from).cloned().unwrap_or(Value::None);
					output.insert(to.clone(), v);
				}
				Projection::Omit(name) => {
					output.remove(name.as_str());
				}
				Projection::All => {}
			}
		}
		Value::Object(output)
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_apply_projections_include() {
		let obj = Object::from(vec![
			("a".to_string(), Value::from(1)),
			("b".to_string(), Value::from(2)),
			("c".to_string(), Value::from(3)),
		]);

		let projections =
			vec![Projection::Include(Strand::new("a")), Projection::Include(Strand::new("c"))];

		let result = apply_projections_to_object(obj, &projections, false);
		if let Value::Object(result_obj) = result {
			assert!(result_obj.contains_key("a"));
			assert!(!result_obj.contains_key("b"));
			assert!(result_obj.contains_key("c"));
		} else {
			panic!("Expected Object");
		}
	}

	#[test]
	fn test_apply_projections_rename() {
		let obj = Object::from(vec![("old_name".to_string(), Value::from(42))]);

		let projections = vec![Projection::Rename {
			from: Strand::new("old_name"),
			to: Strand::new("new_name"),
		}];

		let result = apply_projections_to_object(obj, &projections, false);
		if let Value::Object(result_obj) = result {
			assert!(!result_obj.contains_key("old_name"));
			assert!(result_obj.contains_key("new_name"));
			assert_eq!(result_obj.get("new_name"), Some(&Value::from(42)));
		} else {
			panic!("Expected Object");
		}
	}

	#[test]
	fn test_apply_projections_all_with_omit() {
		let obj = Object::from(vec![
			("a".to_string(), Value::from(1)),
			("b".to_string(), Value::from(2)),
			("c".to_string(), Value::from(3)),
		]);

		let projections = vec![Projection::All, Projection::Omit(Strand::new("b"))];

		let result = apply_projections_to_object(obj, &projections, true);
		if let Value::Object(result_obj) = result {
			assert!(result_obj.contains_key("a"));
			assert!(!result_obj.contains_key("b"));
			assert!(result_obj.contains_key("c"));
		} else {
			panic!("Expected Object");
		}
	}
}