#![cfg_attr(not(feature = "gql"), allow(dead_code))]
use std::sync::Arc;
use futures::StreamExt;
use tracing::debug;
use crate::exec::operators::scan::fetch::{FetchFieldStateCache, resolve_with_field_state};
use crate::exec::{
AccessMode, CardinalityHint, ContextLevel, ExecOperator, ExecutionContext, FlowResult,
OperatorMetrics, OutputOrdering, ValueBatch, ValueBatchStream, buffer_stream, monitor_stream,
};
use crate::val::{Object, RecordId, TableName, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointField {
In,
Out,
}
impl EndpointField {
fn field(self) -> &'static str {
match self {
EndpointField::In => "in",
EndpointField::Out => "out",
}
}
}
#[derive(Debug, Clone)]
pub struct EndpointBind {
pub(crate) input: Arc<dyn ExecOperator>,
pub(crate) edge: String,
pub(crate) field: EndpointField,
pub(crate) target_binding: String,
pub(crate) target_label: Option<TableName>,
pub(crate) metrics: Arc<OperatorMetrics>,
}
impl EndpointBind {
pub(crate) fn new(
input: Arc<dyn ExecOperator>,
edge: String,
field: EndpointField,
target_binding: String,
target_label: Option<TableName>,
) -> Self {
Self {
input,
edge,
field,
target_binding,
target_label,
metrics: Arc::new(OperatorMetrics::new()),
}
}
}
impl ExecOperator for EndpointBind {
fn name(&self) -> &'static str {
"EndpointBind"
}
fn attrs(&self) -> Vec<(String, String)> {
let mut attrs = vec![
("edge".to_string(), self.edge.clone()),
("field".to_string(), self.field.field().to_string()),
("node".to_string(), self.target_binding.clone()),
];
if let Some(label) = self.target_label.as_ref() {
attrs.push(("target_label".to_string(), label.as_str().to_string()));
}
attrs
}
fn required_context(&self) -> ContextLevel {
ContextLevel::Database.max(self.input.required_context())
}
fn access_mode(&self) -> AccessMode {
self.input.access_mode()
}
fn cardinality_hint(&self) -> CardinalityHint {
self.input.cardinality_hint()
}
fn output_ordering(&self) -> OutputOrdering {
self.input.output_ordering()
}
fn children(&self) -> Vec<&Arc<dyn ExecOperator>> {
vec![&self.input]
}
fn metrics(&self) -> Option<&OperatorMetrics> {
Some(&self.metrics)
}
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 edge = self.edge.clone();
let field = self.field;
let target_binding = self.target_binding.clone();
let target_label = self.target_label.clone();
let ctx = ctx.clone();
let stream = async_stream::try_stream! {
let mut target_cache = FetchFieldStateCache::new();
futures::pin_mut!(input_stream);
while let Some(batch_result) = input_stream.next().await {
crate::exec::operators::check_cancelled(&ctx)?;
let batch = batch_result?;
let mut targets_to_fetch: Vec<RecordId> = Vec::with_capacity(batch.values.len());
let mut row_positions: Vec<usize> = Vec::with_capacity(batch.values.len());
for (i, row) in batch.values.iter().enumerate() {
match extract_endpoint_id(row, &edge, field) {
Some(rid) => {
if let Some(label) = target_label.as_ref()
&& &rid.table != label
{
continue;
}
targets_to_fetch.push(rid);
row_positions.push(i);
}
None => {
debug!(
binding = %edge,
"EndpointBind: edge binding missing, not a full edge record, or has no endpoint; dropping row"
);
}
}
}
let target_values =
resolve_with_field_state(&ctx, &mut target_cache, &targets_to_fetch).await?;
let mut out: Vec<Value> = Vec::with_capacity(target_values.len());
for (slot, target_value) in target_values.into_iter().enumerate() {
let Some(target_value) = target_value else {
continue;
};
let input_row = &batch.values[row_positions[slot]];
out.push(bound_row(input_row, &target_binding, target_value));
}
if !out.is_empty() {
yield ValueBatch { values: out };
}
}
};
Ok(monitor_stream(Box::pin(stream), "EndpointBind", &self.metrics))
}
}
fn extract_endpoint_id(row: &Value, edge: &str, field: EndpointField) -> Option<RecordId> {
let Value::Object(obj) = row else {
return None;
};
let Some(Value::Object(edge_obj)) = obj.get(edge) else {
return None;
};
match edge_obj.get(field.field()) {
Some(Value::RecordId(rid)) => Some(rid.clone()),
_ => None,
}
}
fn bound_row(input: &Value, target_binding: &str, target: Value) -> Value {
let mut obj = match input {
Value::Object(o) => o.clone(),
_ => Object::default(),
};
obj.insert(target_binding, target);
Value::Object(obj)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::operators::test_util::ValuesOperator;
fn obj_row(pairs: &[(&str, Value)]) -> Value {
let mut o = Object::default();
for (k, v) in pairs {
o.insert(*k, v.clone());
}
Value::Object(o)
}
fn rid(table: &str, key: &str) -> RecordId {
RecordId {
table: TableName::new(table.to_string()),
key: crate::val::RecordIdKey::from(key.to_string()),
}
}
fn edge_obj(table: &str, key: &str, in_rid: RecordId, out_rid: RecordId) -> Value {
obj_row(&[
("id", Value::RecordId(rid(table, key))),
("in", Value::RecordId(in_rid)),
("out", Value::RecordId(out_rid)),
])
}
fn sample(field: EndpointField, target_label: Option<&str>) -> EndpointBind {
EndpointBind::new(
ValuesOperator::new(Vec::new()),
"__e0".to_string(),
field,
"a".to_string(),
target_label.map(|l| TableName::new(l.to_string())),
)
}
#[test]
fn name_is_endpoint_bind() {
assert_eq!(sample(EndpointField::In, None).name(), "EndpointBind");
}
#[test]
fn attrs_render_edge_field_node_and_label() {
let attrs = sample(EndpointField::Out, Some("person")).attrs();
assert!(attrs.iter().any(|(k, v)| k == "edge" && v == "__e0"));
assert!(attrs.iter().any(|(k, v)| k == "field" && v == "out"));
assert!(attrs.iter().any(|(k, v)| k == "node" && v == "a"));
assert!(attrs.iter().any(|(k, v)| k == "target_label" && v == "person"));
}
#[test]
fn attrs_omit_label_when_unset() {
let attrs = sample(EndpointField::In, None).attrs();
assert!(attrs.iter().any(|(k, v)| k == "field" && v == "in"));
assert!(!attrs.iter().any(|(k, _)| k == "target_label"));
}
#[test]
fn required_context_is_at_least_database() {
assert_eq!(sample(EndpointField::In, None).required_context(), ContextLevel::Database);
}
#[test]
fn field_maps_to_edge_field_name() {
assert_eq!(EndpointField::In.field(), "in");
assert_eq!(EndpointField::Out.field(), "out");
}
#[test]
fn extract_endpoint_id_reads_correct_field() {
let row = obj_row(&[(
"__e0",
edge_obj("knows", "e1", rid("person", "alice"), rid("person", "bob")),
)]);
assert_eq!(
extract_endpoint_id(&row, "__e0", EndpointField::In),
Some(rid("person", "alice"))
);
assert_eq!(
extract_endpoint_id(&row, "__e0", EndpointField::Out),
Some(rid("person", "bob"))
);
}
#[test]
fn extract_endpoint_id_drops_missing_binding() {
let row = obj_row(&[("__e1", edge_obj("knows", "e1", rid("p", "a"), rid("p", "b")))]);
assert_eq!(extract_endpoint_id(&row, "__e0", EndpointField::In), None);
}
#[test]
fn extract_endpoint_id_drops_id_only_edge() {
let row = obj_row(&[("__e0", Value::RecordId(rid("knows", "e1")))]);
assert_eq!(extract_endpoint_id(&row, "__e0", EndpointField::Out), None);
}
#[test]
fn extract_endpoint_id_drops_optional_miss_and_bad_values() {
let null_row = obj_row(&[("__e0", Value::Null)]);
assert_eq!(extract_endpoint_id(&null_row, "__e0", EndpointField::In), None);
let bad_row = obj_row(&[("__e0", Value::Bool(true))]);
assert_eq!(extract_endpoint_id(&bad_row, "__e0", EndpointField::In), None);
let no_field =
obj_row(&[("__e0", obj_row(&[("id", Value::RecordId(rid("knows", "e1")))]))]);
assert_eq!(extract_endpoint_id(&no_field, "__e0", EndpointField::Out), None);
let bad_field =
obj_row(&[("__e0", obj_row(&[("in", Value::Bool(true)), ("out", Value::Bool(true))]))]);
assert_eq!(extract_endpoint_id(&bad_field, "__e0", EndpointField::In), None);
assert_eq!(extract_endpoint_id(&Value::Bool(true), "__e0", EndpointField::In), None);
}
#[test]
fn bound_row_inserts_target_preserving_input() {
let input = obj_row(&[(
"__e0",
edge_obj("knows", "e1", rid("person", "alice"), rid("person", "bob")),
)]);
let node = obj_row(&[
("id", Value::RecordId(rid("person", "alice"))),
("name", Value::from("Alice")),
]);
let row = bound_row(&input, "a", node.clone());
let Value::Object(o) = row else {
panic!("expected object");
};
assert!(o.get("__e0").is_some());
assert_eq!(o.get("a"), Some(&node));
}
#[test]
fn bound_row_overwrites_only_target_binding() {
let input = obj_row(&[("x", Value::from(1)), ("a", Value::from(0))]);
let row = bound_row(&input, "a", Value::from(42));
let Value::Object(o) = row else {
panic!("expected object");
};
assert_eq!(o.get("x"), Some(&Value::from(1)));
assert_eq!(o.get("a"), Some(&Value::from(42)));
}
}