use rusqlite::Connection;
use crate::cypher::ast::{Expr, ExprKind, ReturnItem};
use crate::cypher::eval::{eval_expr, eval_predicate, expr_to_column_name};
use crate::cypher::executor::{literal_to_value, ExecContext};
use crate::cypher::ir::LogicalOp;
use crate::cypher::record::NamedRecord;
use crate::cypher::record_v2::{Record as SlotRecord, RecordSchema, SlotId};
use crate::cypher::record_view::SlotView;
use crate::cypher::schema_infer::{collect_property_refs, infer_with_props, PropertyRefs};
use crate::node;
use crate::types::{NodeId, Result, Value};
pub trait SlotRecordIter {
fn next_slot(&mut self) -> Result<Option<SlotRecord>>;
fn schema(&self) -> &RecordSchema;
}
pub fn collect_to_named(iter: &mut dyn SlotRecordIter) -> Result<Vec<NamedRecord>> {
let schema = iter.schema().clone();
let mut out = Vec::new();
while let Some(rec) = iter.next_slot()? {
out.push(slot_to_named(&schema, &rec));
}
Ok(out)
}
fn slot_to_named(schema: &RecordSchema, rec: &SlotRecord) -> NamedRecord {
let mut nr = NamedRecord::new();
for (slot, name) in schema.iter() {
let v = rec.get(slot).clone();
nr.set(name.to_string(), v);
}
nr
}
pub fn is_slot_supported(plan: &LogicalOp) -> bool {
match plan {
LogicalOp::EmptyRow | LogicalOp::SingleRow => true,
LogicalOp::Scan { .. } => true,
LogicalOp::FullTextLookup { .. } => false,
LogicalOp::IndexLookup {
remaining_filters, ..
} => match remaining_filters {
Some(f) => !expr_has_bare_variable(f),
None => true,
},
LogicalOp::Filter { input, predicate } => {
is_slot_supported(input) && !expr_has_bare_variable(predicate)
}
LogicalOp::Project { input, items, .. } => {
is_slot_supported(input) && items.iter().all(is_project_item_supported)
}
LogicalOp::Aggregate {
input,
group_keys,
aggregates,
} => {
is_slot_supported(input)
&& group_keys.iter().all(|e| !expr_has_pattern_subquery(e))
&& aggregates
.iter()
.all(|a| !expr_has_pattern_subquery(&a.input))
}
LogicalOp::Skip { input, .. } | LogicalOp::Limit { input, .. } => is_slot_supported(input),
LogicalOp::Distinct { input } => is_slot_supported(input),
LogicalOp::Sort { input, items } => {
is_slot_supported(input) && items.iter().all(|s| !expr_has_bare_variable(&s.expr))
}
LogicalOp::Unwind { input, expr, .. } => {
is_slot_supported(input) && !expr_has_pattern_subquery(expr)
}
LogicalOp::Expand {
input,
var_length_prop_filters,
..
} => {
is_slot_supported(input)
&& var_length_prop_filters
.values()
.all(|e| !expr_has_bare_variable(e))
}
LogicalOp::CrossProduct { .. }
| LogicalOp::CorrelatedJoin { .. }
| LogicalOp::LeftOuterJoin { .. } => true,
LogicalOp::CreateNode { .. }
| LogicalOp::CreateEdge { .. }
| LogicalOp::CreateSequence { .. }
| LogicalOp::MatchCreate { .. }
| LogicalOp::Delete { .. }
| LogicalOp::SetProperty { .. }
| LogicalOp::SetLabel { .. }
| LogicalOp::SetProperties { .. }
| LogicalOp::Remove { .. }
| LogicalOp::Merge { .. }
| LogicalOp::MatchMerge { .. }
| LogicalOp::CreateIndex { .. }
| LogicalOp::DropIndex { .. } => true,
_ => false,
}
}
fn is_project_item_supported(item: &ReturnItem) -> bool {
!expr_has_bare_variable(&item.expr)
}
fn expr_has_bare_variable(e: &Expr) -> bool {
use ExprKind::*;
match &e.kind {
Variable(_) | Star => true,
Literal(_) | Parameter(_) | HasLabel(_, _) => false,
Property(_, _) => false,
BinaryOp { left, right, .. } => {
expr_has_bare_variable(left) || expr_has_bare_variable(right)
}
Not(x) | IsNull(x) | IsNotNull(x) => expr_has_bare_variable(x),
FunctionCall { args, .. } => args.iter().any(expr_has_bare_variable),
Case {
operand,
alternatives,
default,
} => {
operand.as_deref().is_some_and(expr_has_bare_variable)
|| alternatives
.iter()
.any(|(c, r)| expr_has_bare_variable(c) || expr_has_bare_variable(r))
|| default.as_deref().is_some_and(expr_has_bare_variable)
}
List(xs) => xs.iter().any(expr_has_bare_variable),
ListComprehension {
list_expr,
filter,
map_expr,
..
} => {
expr_has_bare_variable(list_expr)
|| filter.as_deref().is_some_and(expr_has_bare_variable)
|| map_expr.as_deref().is_some_and(expr_has_bare_variable)
}
PatternComprehension { .. } | Exists { .. } | ExistsSubquery(_) | PatternPredicate(_) => {
true
}
MapLiteral(entries) => entries.iter().any(|(_, v)| expr_has_bare_variable(v)),
Index { expr, index } => expr_has_bare_variable(expr) || expr_has_bare_variable(index),
DotAccess { expr, .. } => expr_has_bare_variable(expr),
Slice { expr, start, end } => {
expr_has_bare_variable(expr)
|| start.as_deref().is_some_and(expr_has_bare_variable)
|| end.as_deref().is_some_and(expr_has_bare_variable)
}
Quantifier {
list_expr,
predicate,
..
} => expr_has_bare_variable(list_expr) || expr_has_bare_variable(predicate),
}
}
fn expr_has_pattern_subquery(e: &Expr) -> bool {
use ExprKind::*;
match &e.kind {
Variable(_) | Star | Literal(_) | Parameter(_) | HasLabel(_, _) | Property(_, _) => false,
PatternComprehension { .. } | Exists { .. } | ExistsSubquery(_) | PatternPredicate(_) => {
true
}
BinaryOp { left, right, .. } => {
expr_has_pattern_subquery(left) || expr_has_pattern_subquery(right)
}
Not(x) | IsNull(x) | IsNotNull(x) => expr_has_pattern_subquery(x),
FunctionCall { args, .. } => args.iter().any(expr_has_pattern_subquery),
Case {
operand,
alternatives,
default,
} => {
operand.as_deref().is_some_and(expr_has_pattern_subquery)
|| alternatives
.iter()
.any(|(c, r)| expr_has_pattern_subquery(c) || expr_has_pattern_subquery(r))
|| default.as_deref().is_some_and(expr_has_pattern_subquery)
}
List(xs) => xs.iter().any(expr_has_pattern_subquery),
ListComprehension {
list_expr,
filter,
map_expr,
..
} => {
expr_has_pattern_subquery(list_expr)
|| filter.as_deref().is_some_and(expr_has_pattern_subquery)
|| map_expr.as_deref().is_some_and(expr_has_pattern_subquery)
}
MapLiteral(entries) => entries.iter().any(|(_, v)| expr_has_pattern_subquery(v)),
Index { expr, index } => {
expr_has_pattern_subquery(expr) || expr_has_pattern_subquery(index)
}
DotAccess { expr, .. } => expr_has_pattern_subquery(expr),
Slice { expr, start, end } => {
expr_has_pattern_subquery(expr)
|| start.as_deref().is_some_and(expr_has_pattern_subquery)
|| end.as_deref().is_some_and(expr_has_pattern_subquery)
}
Quantifier {
list_expr,
predicate,
..
} => expr_has_pattern_subquery(list_expr) || expr_has_pattern_subquery(predicate),
}
}
pub fn build_slot_iter<'a>(
conn: &'a Connection,
plan: &'a LogicalOp,
ctx: &'a ExecContext,
) -> Result<Box<dyn SlotRecordIter + 'a>> {
let mut refs = PropertyRefs::new();
collect_property_refs(plan, &mut refs);
build_slot_iter_inner(conn, plan, ctx, &refs)
}
fn build_slot_iter_inner<'a>(
conn: &'a Connection,
plan: &'a LogicalOp,
ctx: &'a ExecContext,
refs: &PropertyRefs,
) -> Result<Box<dyn SlotRecordIter + 'a>> {
match plan {
LogicalOp::EmptyRow | LogicalOp::SingleRow => Ok(Box::new(EmptyRowSlotIter::new())),
LogicalOp::Scan { label, alias } => {
let schema = infer_with_props(plan, refs);
let nodes = node::find_nodes_by_label(conn, label)?;
let records: Vec<SlotRecord> = nodes
.iter()
.map(|n| node_to_slot_record(n, alias, &schema))
.collect();
Ok(Box::new(VecSlotIter::new(records, schema)))
}
LogicalOp::IndexLookup {
label,
alias,
index_properties,
lookups,
remaining_filters,
} => {
let schema = infer_with_props(plan, refs);
let node_ids =
crate::cypher::executor::index_lookup_ids(conn, label, index_properties, lookups)?;
let mut records = Vec::with_capacity(node_ids.len());
for id in node_ids {
let n = node::get_node(conn, id)?;
records.push(node_to_slot_record(&n, alias, &schema));
}
let base: Box<dyn SlotRecordIter + 'a> = Box::new(VecSlotIter::new(records, schema));
match remaining_filters {
Some(f) => Ok(Box::new(FilterSlotIter {
input: base,
predicate: f.clone(),
conn,
})),
None => Ok(base),
}
}
LogicalOp::Filter { input, predicate } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
Ok(Box::new(FilterSlotIter {
input: input_iter,
predicate: predicate.clone(),
conn,
}))
}
LogicalOp::Expand {
input,
src_alias,
dst_alias,
rel_alias,
edge_types,
direction,
min_hops,
max_hops,
var_length,
var_length_prop_filters,
result_cap: _,
} => {
let input_slot_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
let output_schema = infer_with_props(plan, refs);
let named_input: Box<dyn crate::cypher::iter::RecordIter + 'a> =
Box::new(SlotToNamedAdapter::new(input_slot_iter));
let prop_filter_values: std::collections::HashMap<String, Value> =
var_length_prop_filters
.iter()
.filter_map(|(k, e)| match &e.kind {
ExprKind::Literal(lit) => Some((k.clone(), literal_to_value(lit))),
_ => None,
})
.collect();
let named_expand = crate::cypher::iter::ExpandIter::new(
named_input,
conn,
src_alias.clone(),
dst_alias.clone(),
rel_alias.clone(),
edge_types.clone(),
*direction,
*min_hops,
*max_hops,
*var_length,
prop_filter_values,
ctx.max_traversal_work,
);
Ok(Box::new(NamedToSlotAdapter::new(
Box::new(named_expand),
output_schema,
)))
}
LogicalOp::Aggregate {
input,
group_keys,
aggregates,
} => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
let output_schema = infer_with_props(plan, refs);
Ok(Box::new(AggregateSlotIter::new(
input_iter,
group_keys.clone(),
aggregates.clone(),
output_schema,
conn,
)))
}
LogicalOp::Skip { input, count } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
Ok(Box::new(SkipSlotIter {
input: input_iter,
remaining_to_skip: *count,
}))
}
LogicalOp::Limit { input, count } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
Ok(Box::new(LimitSlotIter {
input: input_iter,
remaining: *count,
}))
}
LogicalOp::Distinct { input } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
Ok(Box::new(DistinctSlotIter::new(input_iter)))
}
LogicalOp::Sort { input, items } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
Ok(Box::new(SortSlotIter::new(input_iter, items.clone(), conn)))
}
LogicalOp::CrossProduct { .. }
| LogicalOp::CorrelatedJoin { .. }
| LogicalOp::LeftOuterJoin { .. }
| LogicalOp::CreateNode { .. }
| LogicalOp::CreateEdge { .. }
| LogicalOp::CreateSequence { .. }
| LogicalOp::MatchCreate { .. }
| LogicalOp::Delete { .. }
| LogicalOp::SetProperty { .. }
| LogicalOp::SetLabel { .. }
| LogicalOp::SetProperties { .. }
| LogicalOp::Remove { .. }
| LogicalOp::Merge { .. }
| LogicalOp::MatchMerge { .. }
| LogicalOp::CreateIndex { .. }
| LogicalOp::DropIndex { .. } => {
let output_schema = infer_with_props(plan, refs);
let records = crate::cypher::executor::exec_pub(conn, plan, ctx)?;
Ok(Box::new(MaterializedSlotIter::new(records, output_schema)))
}
LogicalOp::Unwind { input, expr, alias } => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
let output_schema = infer_with_props(plan, refs);
Ok(Box::new(UnwindSlotIter::new(
input_iter,
expr.clone(),
alias.clone(),
output_schema,
conn,
)))
}
LogicalOp::Project {
input,
items,
emit_compound,
} => {
let input_iter = build_slot_iter_inner(conn, input, ctx, refs)?;
let output_schema = infer_with_props(plan, refs);
Ok(Box::new(ProjectSlotIter::new(
input_iter,
items.clone(),
*emit_compound,
output_schema,
conn,
)))
}
_ => unreachable!(
"build_slot_iter called on unsupported op `{}` — caller must \
gate with is_slot_supported()",
plan.op_name()
),
}
}
pub fn materialize_named(schema: &RecordSchema, rec: &SlotRecord) -> NamedRecord {
let mut nr = NamedRecord::new();
for (slot, name) in schema.iter() {
nr.set(name.to_string(), rec.get(slot).clone());
}
nr
}
pub fn node_to_slot_record(
n: &crate::types::Node,
alias: &str,
schema: &RecordSchema,
) -> SlotRecord {
debug_assert!(n.id.0 <= i64::MAX as u64, "NodeId exceeds i64::MAX");
let mut rec = SlotRecord::with_capacity(schema.len());
if let Some(s) = schema.slot(alias) {
rec.set(s, Value::I64(n.id.0 as i64));
}
let id_key = format!("{alias}.__id");
if let Some(s) = schema.slot(&id_key) {
rec.set(s, Value::I64(n.id.0 as i64));
}
let label_key = format!("{alias}.__label");
if let Some(s) = schema.slot(&label_key) {
rec.set(s, Value::String(n.labels.join(":")));
}
let labels_key = format!("{alias}.__labels");
if let Some(s) = schema.slot(&labels_key) {
rec.set(
s,
Value::List(n.labels.iter().map(|l| Value::String(l.clone())).collect()),
);
}
for (key, val) in &n.properties {
let prop_key = format!("{alias}.{key}");
if let Some(s) = schema.slot(&prop_key) {
rec.set(s, val.clone());
}
}
rec
}
pub struct EmptyRowSlotIter {
schema: RecordSchema,
done: bool,
}
impl EmptyRowSlotIter {
pub fn new() -> Self {
Self {
schema: RecordSchema::new(),
done: false,
}
}
}
impl Default for EmptyRowSlotIter {
fn default() -> Self {
Self::new()
}
}
impl SlotRecordIter for EmptyRowSlotIter {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
if self.done {
Ok(None)
} else {
self.done = true;
Ok(Some(SlotRecord::new()))
}
}
fn schema(&self) -> &RecordSchema {
&self.schema
}
}
pub struct MaterializedSlotIter {
records: std::vec::IntoIter<NamedRecord>,
schema: RecordSchema,
}
impl MaterializedSlotIter {
pub fn new(records: Vec<NamedRecord>, schema: RecordSchema) -> Self {
Self {
records: records.into_iter(),
schema,
}
}
}
impl SlotRecordIter for MaterializedSlotIter {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
Ok(self.records.next().map(|r| named_to_slot(&self.schema, &r)))
}
fn schema(&self) -> &RecordSchema {
&self.schema
}
}
pub struct VecSlotIter {
records: std::vec::IntoIter<SlotRecord>,
schema: RecordSchema,
}
impl VecSlotIter {
pub fn new(records: Vec<SlotRecord>, schema: RecordSchema) -> Self {
Self {
records: records.into_iter(),
schema,
}
}
}
impl SlotRecordIter for VecSlotIter {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
Ok(self.records.next())
}
fn schema(&self) -> &RecordSchema {
&self.schema
}
}
pub struct FilterSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
predicate: Expr,
conn: &'a Connection,
}
impl<'a> SlotRecordIter for FilterSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
while let Some(rec) = self.input.next_slot()? {
let view = SlotView::new(self.input.schema(), &rec);
if eval_predicate(
&self.predicate,
&view,
crate::cypher::eval::EvalCx::new(self.conn),
)? {
return Ok(Some(rec));
}
}
Ok(None)
}
fn schema(&self) -> &RecordSchema {
self.input.schema()
}
}
pub struct ProjectSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
items: Vec<ReturnItem>,
_emit_compound: bool,
output_schema: RecordSchema,
conn: &'a Connection,
}
impl<'a> ProjectSlotIter<'a> {
pub fn new(
input: Box<dyn SlotRecordIter + 'a>,
items: Vec<ReturnItem>,
emit_compound: bool,
output_schema: RecordSchema,
conn: &'a Connection,
) -> Self {
Self {
input,
items,
_emit_compound: emit_compound,
output_schema,
conn,
}
}
}
impl<'a> SlotRecordIter for ProjectSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
let input_rec = match self.input.next_slot()? {
Some(r) => r,
None => return Ok(None),
};
let input_schema = self.input.schema();
let mut out = SlotRecord::with_capacity(self.output_schema.len());
for item in &self.items {
let col = column_name_for_item(item);
let Some(out_slot) = self.output_schema.slot(&col) else {
continue;
};
let expr_col = expr_to_column_name(&item.expr);
let deleted_var = property_deleted_var(&item.expr, input_schema, &input_rec);
let value = if deleted_var {
eval_project_item(&item.expr, input_schema, &input_rec, self.conn)?
} else if let Some(in_slot) = input_schema.slot(&expr_col) {
input_rec.get(in_slot).clone()
} else if col != expr_col && is_aggregate_call(&item.expr) {
if let Some(in_slot) = input_schema.slot(&col) {
input_rec.get(in_slot).clone()
} else {
eval_project_item(&item.expr, input_schema, &input_rec, self.conn)?
}
} else {
eval_project_item(&item.expr, input_schema, &input_rec, self.conn)?
};
out.set(out_slot, value);
}
Ok(Some(out))
}
fn schema(&self) -> &RecordSchema {
&self.output_schema
}
}
fn property_deleted_var(e: &Expr, input_schema: &RecordSchema, input_rec: &SlotRecord) -> bool {
if let ExprKind::Property(var, _) = &e.kind {
let key = format!("{var}.__deleted");
if let Some(slot) = input_schema.slot(&key) {
return matches!(input_rec.get(slot), Value::Bool(true));
}
}
false
}
fn column_name_for_item(item: &ReturnItem) -> String {
item.alias
.clone()
.unwrap_or_else(|| expr_to_column_name(&item.expr))
}
fn eval_project_item(
e: &Expr,
input_schema: &RecordSchema,
input_rec: &SlotRecord,
conn: &Connection,
) -> Result<Value> {
match &e.kind {
ExprKind::Literal(lit) => Ok(literal_to_value_local(lit)),
ExprKind::Property(var, prop) => {
let deleted_key = format!("{var}.__deleted");
let is_deleted = input_schema
.slot(&deleted_key)
.map(|s| matches!(input_rec.get(s), Value::Bool(true)))
.unwrap_or(false);
if !is_deleted {
let key = format!("{var}.{prop}");
if let Some(slot) = input_schema.slot(&key) {
return Ok(input_rec.get(slot).clone());
}
}
let view = SlotView::new(input_schema, input_rec);
eval_expr(e, &view, crate::cypher::eval::EvalCx::new(conn))
}
_ => {
let view = SlotView::new(input_schema, input_rec);
eval_expr(e, &view, crate::cypher::eval::EvalCx::new(conn))
}
}
}
fn is_aggregate_call(e: &Expr) -> bool {
matches!(
&e.kind,
ExprKind::FunctionCall { name, .. }
if matches!(
name.to_ascii_lowercase().as_str(),
"count" | "sum" | "avg" | "min" | "max" | "collect"
| "percentiledisc" | "percentilecont" | "stdev" | "stdevp"
)
)
}
fn literal_to_value_local(lit: &crate::cypher::ast::LiteralValue) -> Value {
use crate::cypher::ast::LiteralValue as L;
match lit {
L::Null => Value::Null,
L::Bool(b) => Value::Bool(*b),
L::I64(n) => Value::I64(*n),
L::F64(n) => Value::F64(*n),
L::String(s) => Value::String(s.clone()),
}
}
pub struct AggregateSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
group_keys: Vec<Expr>,
aggregates: Vec<crate::cypher::ir::AggregateExpr>,
output_schema: RecordSchema,
conn: &'a Connection,
results: Option<std::vec::IntoIter<SlotRecord>>,
}
impl<'a> AggregateSlotIter<'a> {
pub fn new(
input: Box<dyn SlotRecordIter + 'a>,
group_keys: Vec<Expr>,
aggregates: Vec<crate::cypher::ir::AggregateExpr>,
output_schema: RecordSchema,
conn: &'a Connection,
) -> Self {
Self {
input,
group_keys,
aggregates,
output_schema,
conn,
results: None,
}
}
fn materialize(&mut self) -> Result<()> {
let input_schema = self.input.schema().clone();
let mut slot_inputs: Vec<SlotRecord> = Vec::new();
while let Some(rec) = self.input.next_slot()? {
slot_inputs.push(rec);
}
let agg_results = crate::cypher::executor::aggregate_slot_records(
self.conn,
&input_schema,
&slot_inputs,
&self.output_schema,
&self.group_keys,
&self.aggregates,
)?;
self.results = Some(agg_results.into_iter());
Ok(())
}
}
impl<'a> SlotRecordIter for AggregateSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
if self.results.is_none() {
self.materialize()?;
}
Ok(self.results.as_mut().and_then(|it| it.next()))
}
fn schema(&self) -> &RecordSchema {
&self.output_schema
}
}
pub struct SkipSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
remaining_to_skip: u64,
}
impl<'a> SlotRecordIter for SkipSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
while self.remaining_to_skip > 0 {
match self.input.next_slot()? {
Some(_) => self.remaining_to_skip -= 1,
None => return Ok(None),
}
}
self.input.next_slot()
}
fn schema(&self) -> &RecordSchema {
self.input.schema()
}
}
pub struct LimitSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
remaining: u64,
}
impl<'a> SlotRecordIter for LimitSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
if self.remaining == 0 {
return Ok(None);
}
match self.input.next_slot()? {
Some(rec) => {
self.remaining -= 1;
Ok(Some(rec))
}
None => Ok(None),
}
}
fn schema(&self) -> &RecordSchema {
self.input.schema()
}
}
pub struct DistinctSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
schema: RecordSchema,
results: Option<std::vec::IntoIter<SlotRecord>>,
}
impl<'a> DistinctSlotIter<'a> {
pub fn new(input: Box<dyn SlotRecordIter + 'a>) -> Self {
let schema = input.schema().clone();
Self {
input,
schema,
results: None,
}
}
fn materialize(&mut self) -> Result<()> {
let mut deduped: Vec<SlotRecord> = Vec::new();
while let Some(rec) = self.input.next_slot()? {
if !deduped.iter().any(|s| slot_rows_equal(s, &rec)) {
deduped.push(rec);
}
}
self.results = Some(deduped.into_iter());
Ok(())
}
}
impl<'a> SlotRecordIter for DistinctSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
if self.results.is_none() {
self.materialize()?;
}
Ok(self.results.as_mut().and_then(|it| it.next()))
}
fn schema(&self) -> &RecordSchema {
&self.schema
}
}
fn slot_rows_equal(a: &SlotRecord, b: &SlotRecord) -> bool {
if a.len() != b.len() {
return false;
}
(0..a.len()).all(|i| {
let s = SlotId(i as u32);
a.get(s) == b.get(s)
})
}
pub struct SortSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
items: Vec<crate::cypher::ast::SortItem>,
conn: &'a Connection,
results: Option<std::vec::IntoIter<SlotRecord>>,
}
impl<'a> SortSlotIter<'a> {
pub fn new(
input: Box<dyn SlotRecordIter + 'a>,
items: Vec<crate::cypher::ast::SortItem>,
conn: &'a Connection,
) -> Self {
Self {
input,
items,
conn,
results: None,
}
}
fn materialize(&mut self) -> Result<()> {
let schema = self.input.schema().clone();
let mut tagged: Vec<(Vec<Value>, SlotRecord)> = Vec::new();
while let Some(rec) = self.input.next_slot()? {
let view = SlotView::new(&schema, &rec);
let keys: Vec<Value> = self
.items
.iter()
.map(|si| {
eval_expr(&si.expr, &view, crate::cypher::eval::EvalCx::new(self.conn))
.unwrap_or(Value::Null)
})
.collect();
tagged.push((keys, rec));
}
let descending: Vec<bool> = self.items.iter().map(|si| si.descending).collect();
tagged.sort_by(|a, b| {
for (i, desc) in descending.iter().enumerate() {
let ord = compare_values_for_sort(&a.0[i], &b.0[i]);
let ord = if *desc { ord.reverse() } else { ord };
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
std::cmp::Ordering::Equal
});
let sorted: Vec<SlotRecord> = tagged.into_iter().map(|(_, r)| r).collect();
self.results = Some(sorted.into_iter());
Ok(())
}
}
impl<'a> SlotRecordIter for SortSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
if self.results.is_none() {
self.materialize()?;
}
Ok(self.results.as_mut().and_then(|it| it.next()))
}
fn schema(&self) -> &RecordSchema {
self.input.schema()
}
}
fn compare_values_for_sort(a: &Value, b: &Value) -> std::cmp::Ordering {
crate::cypher::iter::compare_values_for_sort_pub(a, b)
}
pub struct UnwindSlotIter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
expr: Expr,
alias: String,
output_schema: RecordSchema,
conn: &'a Connection,
pending: std::vec::IntoIter<SlotRecord>,
}
impl<'a> UnwindSlotIter<'a> {
pub fn new(
input: Box<dyn SlotRecordIter + 'a>,
expr: Expr,
alias: String,
output_schema: RecordSchema,
conn: &'a Connection,
) -> Self {
Self {
input,
expr,
alias,
output_schema,
conn,
pending: Vec::new().into_iter(),
}
}
fn project(
&self,
input_schema: &RecordSchema,
base: &SlotRecord,
named: &NamedRecord,
) -> SlotRecord {
let mut out = SlotRecord::with_capacity(self.output_schema.len());
for (in_slot, name) in input_schema.iter() {
if let Some(out_slot) = self.output_schema.slot(name) {
out.set(out_slot, base.get(in_slot).clone());
}
}
let prefix = format!("{}.", self.alias);
for (k, v) in &named.fields {
if k == &self.alias || k.starts_with(&prefix) {
if let Some(out_slot) = self.output_schema.slot(k) {
out.set(out_slot, v.clone());
}
}
}
out
}
}
impl<'a> SlotRecordIter for UnwindSlotIter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
loop {
if let Some(rec) = self.pending.next() {
return Ok(Some(rec));
}
let Some(input_rec) = self.input.next_slot()? else {
return Ok(None);
};
let input_schema = self.input.schema();
let val = {
let view = SlotView::new(input_schema, &input_rec);
eval_expr(
&self.expr,
&view,
crate::cypher::eval::EvalCx::new(self.conn),
)?
};
let items = match val {
Value::List(items) => items,
Value::Null => continue,
other => {
return Err(crate::types::GraphError::semantic(format!(
"UNWIND requires a list, got: {other}"
)));
}
};
if items.is_empty() {
continue;
}
let view = materialize_named(input_schema, &input_rec);
let mut produced = Vec::with_capacity(items.len());
for item in items {
let mut named = view.clone();
match &item {
Value::Node(n) => {
let node_rec = crate::cypher::executor::node_to_record_pub(n, &self.alias);
for (k, v) in &node_rec.fields {
named.set(k.clone(), v.clone());
}
}
Value::Edge(e) => {
named.set(self.alias.clone(), Value::Edge(e.clone()));
named.set(format!("{}.__src", self.alias), Value::I64(e.src.0 as i64));
named.set(format!("{}.__dst", self.alias), Value::I64(e.dst.0 as i64));
named.set(
format!("{}.__type", self.alias),
Value::String(e.label.clone()),
);
for (k, v) in &e.properties {
named.set(format!("{}.{}", self.alias, k), v.clone());
}
}
Value::Map(entries) => {
named.set(self.alias.clone(), Value::Map(entries.clone()));
for (k, v) in entries {
named.set(format!("{}.{}", self.alias, k), v.clone());
}
}
_ => {
named.set(self.alias.clone(), item.clone());
}
}
produced.push(self.project(input_schema, &input_rec, &named));
}
self.pending = produced.into_iter();
}
}
fn schema(&self) -> &RecordSchema {
&self.output_schema
}
}
pub struct SlotToNamedAdapter<'a> {
input: Box<dyn SlotRecordIter + 'a>,
}
impl<'a> SlotToNamedAdapter<'a> {
pub fn new(input: Box<dyn SlotRecordIter + 'a>) -> Self {
Self { input }
}
}
impl<'a> crate::cypher::iter::RecordIter for SlotToNamedAdapter<'a> {
fn next_record(&mut self) -> Result<Option<NamedRecord>> {
Ok(self
.input
.next_slot()?
.map(|rec| materialize_named(self.input.schema(), &rec)))
}
}
pub struct NamedToSlotAdapter<'a> {
input: Box<dyn crate::cypher::iter::RecordIter + 'a>,
schema: RecordSchema,
}
impl<'a> NamedToSlotAdapter<'a> {
pub fn new(input: Box<dyn crate::cypher::iter::RecordIter + 'a>, schema: RecordSchema) -> Self {
Self { input, schema }
}
}
impl<'a> SlotRecordIter for NamedToSlotAdapter<'a> {
fn next_slot(&mut self) -> Result<Option<SlotRecord>> {
match self.input.next_record()? {
Some(named) => Ok(Some(named_to_slot(&self.schema, &named))),
None => Ok(None),
}
}
fn schema(&self) -> &RecordSchema {
&self.schema
}
}
fn named_to_slot(schema: &RecordSchema, named: &NamedRecord) -> SlotRecord {
let mut rec = SlotRecord::with_capacity(schema.len());
for (slot, name) in schema.iter() {
if let Some(v) = named.get(name) {
rec.set(slot, v.clone());
}
}
rec
}
#[allow(dead_code)]
fn _kept_for_future_use(_: NodeId, _: SlotId) {}