surrealdb-core 3.2.2

A scalable, distributed, collaborative, document-graph database, for the realtime web
Documentation
//! Destructure part -- `{ field1, field2: path, ... }`.

use std::sync::Arc;

use surrealdb_strand::Strand;
use surrealdb_types::{SqlFormat, ToSql};

use super::evaluate_physical_path;
use crate::exec::physical_expr::{EvalContext, PhysicalExpr};
use crate::exec::{AccessMode, BoxFut, CombineAccessModes, ContextLevel};
use crate::expr::FlowResult;
use crate::val::Value;

/// Destructure - extract fields into a new object `{ field1, field2: path }`.
#[derive(Debug, Clone)]
pub struct DestructurePart {
	pub fields: Vec<DestructureField>,
}

/// A field in a destructure pattern.
#[derive(Debug, Clone)]
pub enum DestructureField {
	/// Include all fields from a nested object.
	All(Strand),
	/// Include a single field by name.
	Field(Strand),
	/// Include a field with an aliased path.
	Aliased {
		field: Strand,
		path: Vec<Arc<dyn PhysicalExpr>>,
	},
	/// Nested destructure on a field.
	Nested {
		field: Strand,
		parts: Vec<DestructureField>,
	},
}
impl PhysicalExpr for DestructurePart {
	fn name(&self) -> &'static str {
		"Destructure"
	}

	fn as_any(&self) -> &dyn std::any::Any {
		self
	}

	fn required_context(&self) -> ContextLevel {
		fn field_context(fields: &[DestructureField]) -> ContextLevel {
			fields
				.iter()
				.map(|f| match f {
					DestructureField::All(_) | DestructureField::Field(_) => ContextLevel::Root,
					DestructureField::Aliased {
						path,
						..
					} => path
						.iter()
						.map(|p| p.required_context())
						.max()
						.unwrap_or(ContextLevel::Root),
					DestructureField::Nested {
						parts,
						..
					} => field_context(parts),
				})
				.max()
				.unwrap_or(ContextLevel::Root)
		}
		// Destructure may need to fetch records (when applied to RecordId)
		ContextLevel::Database.max(field_context(&self.fields))
	}

	fn evaluate<'a>(&'a self, ctx: EvalContext<'a>) -> BoxFut<'a, FlowResult<Value>> {
		Box::pin(async move {
			let value = ctx.current_value.unwrap_or(&Value::NONE);
			evaluate_destructure(value, &self.fields, ctx).await
		})
	}

	fn access_mode(&self) -> AccessMode {
		fn field_access(fields: &[DestructureField]) -> AccessMode {
			fields
				.iter()
				.map(|f| match f {
					DestructureField::All(_) | DestructureField::Field(_) => AccessMode::ReadOnly,
					DestructureField::Aliased {
						path,
						..
					} => path.iter().map(|p| p.access_mode()).combine_all(),
					DestructureField::Nested {
						parts,
						..
					} => field_access(parts),
				})
				.combine_all()
		}
		field_access(&self.fields)
	}
}

impl ToSql for DestructurePart {
	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
		f.push('{');
		for (i, field) in self.fields.iter().enumerate() {
			if i > 0 {
				f.push_str(", ");
			}
			field.fmt_sql(f, fmt);
		}
		f.push('}');
	}
}

impl ToSql for DestructureField {
	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
		match self {
			DestructureField::All(name) => {
				f.push_str(name);
				f.push_str(".*");
			}
			DestructureField::Field(name) => {
				f.push_str(name);
			}
			DestructureField::Aliased {
				field,
				path,
			} => {
				f.push_str(field);
				f.push_str(": ");
				for part in path {
					part.fmt_sql(f, fmt);
				}
			}
			DestructureField::Nested {
				field,
				parts,
			} => {
				f.push_str(field);
				f.push_str(": {");
				for (i, part) in parts.iter().enumerate() {
					if i > 0 {
						f.push_str(", ");
					}
					part.fmt_sql(f, fmt);
				}
				f.push('}');
			}
		}
	}
}

/// Destructure evaluation - extract fields into a new object.
async fn evaluate_destructure(
	value: &Value,
	fields: &[DestructureField],
	ctx: EvalContext<'_>,
) -> FlowResult<Value> {
	match value {
		Value::Object(obj) => {
			let mut result = std::collections::BTreeMap::new();

			for field in fields {
				match field {
					DestructureField::All(name) => {
						let field_val = obj.get(name.as_str()).cloned().unwrap_or(Value::None);
						let resolved = match field_val {
							Value::RecordId(rid) => {
								if ctx.skip_fetch_perms {
									crate::exec::operators::fetch::fetch_record_no_perms(
										ctx.exec_ctx,
										&rid,
									)
									.await?
								} else {
									crate::exec::operators::fetch::fetch_record(ctx.exec_ctx, &rid)
										.await?
								}
							}
							other => other,
						};
						if let Value::Object(nested) = &resolved {
							result.insert(name.clone(), Value::Object(nested.clone()));
						}
					}
					DestructureField::Field(name) => {
						let v = obj.get(name.as_str()).cloned().unwrap_or(Value::None);
						result.insert(name.clone(), v);
					}
					DestructureField::Aliased {
						field: name,
						path,
					} => {
						// Evaluate the aliased path starting from the current value
						// (not from obj.get(field)). The field name is just the output label.
						let v = evaluate_physical_path(value, path, ctx.clone()).await?;
						result.insert(name.clone(), v);
					}
					DestructureField::Nested {
						field: name,
						parts,
					} => {
						let nested_value = obj.get(name.as_str()).cloned().unwrap_or(Value::None);
						let v = Box::pin(evaluate_destructure(&nested_value, parts, ctx.clone()))
							.await?;
						result.insert(name.clone(), v);
					}
				}
			}

			Ok(Value::Object(crate::val::Object::from(result)))
		}
		Value::RecordId(rid) => {
			let fetched = if ctx.skip_fetch_perms {
				crate::exec::operators::fetch::fetch_record_no_perms(ctx.exec_ctx, rid).await?
			} else {
				crate::exec::operators::fetch::fetch_record(ctx.exec_ctx, rid).await?
			};
			if fetched.is_none() {
				return Ok(Value::None);
			}

			// Continue destructure on the fetched object
			Box::pin(evaluate_destructure(&fetched, fields, ctx)).await
		}
		Value::Array(arr) => {
			// Apply destructure to each element
			let mut results = Vec::with_capacity(arr.len());
			for item in arr.iter() {
				let v = Box::pin(evaluate_destructure(item, fields, ctx.clone())).await?;
				results.push(v);
			}
			Ok(Value::Array(results.into()))
		}
		_ => Ok(Value::None),
	}
}