use std::sync::Arc;
use anyhow::{Result, bail};
use reblessive::tree::Stk;
use surrealdb_types::ToSql;
use crate::catalog::providers::{DatabaseProvider, NamespaceProvider, TableProvider};
use crate::ctx::{Context, FrozenContext};
use crate::dbs::{Iterable, Iterator, Options, Statement};
use crate::doc::{CursorDoc, DocumentContext, NsDbCtx};
use crate::err::Error;
use crate::expr::{Data, Expr, FlowResultExt as _, Output, Value};
use crate::idx::planner::RecordStrategy;
use crate::val::{Duration, RecordId, RecordIdKey, TableName};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct RelateStatement {
pub only: bool,
pub or_update: bool,
pub through: Expr,
pub from: Expr,
pub to: Expr,
pub data: Option<Data>,
pub output: Option<Output>,
pub timeout: Expr,
}
impl RelateStatement {
#[instrument(level = "trace", name = "RelateStatement::compute", skip_all)]
pub(crate) async fn compute(
&self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
) -> Result<Value> {
opt.valid_for_db()?;
let mut iterator = Iterator::new();
let ctx_store: FrozenContext;
let ctx = match stk
.run(|stk| self.timeout.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to::<Option<Duration>>()?
{
Some(timeout) => {
let mut new_ctx = Context::new_child(ctx);
new_ctx.add_timeout(timeout.0)?;
ctx_store = new_ctx.freeze();
&ctx_store
}
None => ctx,
};
let from = {
let mut out = Vec::new();
match stk.run(|stk| self.from.compute(stk, ctx, opt, doc)).await.catch_return()? {
Value::RecordId(v) => out.push(v),
Value::Array(v) => {
for v in v {
match v {
Value::RecordId(v) => out.push(v),
Value::Object(v) => match v.rid() {
Some(v) => out.push(v),
_ => {
bail!(Error::RelateStatementIn {
value: v.to_sql(),
})
}
},
v => {
bail!(Error::RelateStatementIn {
value: v.to_sql(),
})
}
}
}
}
Value::Object(v) => match v.rid() {
Some(v) => out.push(v),
None => {
bail!(Error::RelateStatementIn {
value: v.to_sql(),
})
}
},
v => {
bail!(Error::RelateStatementIn {
value: v.to_sql(),
})
}
};
out
};
let to = {
let mut out = Vec::new();
match stk.run(|stk| self.to.compute(stk, ctx, opt, doc)).await.catch_return()? {
Value::RecordId(v) => out.push(v),
Value::Array(v) => {
for v in v {
match v {
Value::RecordId(v) => out.push(v),
Value::Object(v) => match v.rid() {
Some(v) => out.push(v),
None => {
bail!(Error::RelateStatementId {
value: v.to_sql(),
})
}
},
v => {
bail!(Error::RelateStatementId {
value: v.to_sql(),
})
}
}
}
}
Value::Object(v) => match v.rid() {
Some(v) => out.push(v),
None => {
bail!(Error::RelateStatementId {
value: v.to_sql(),
})
}
},
v => {
bail!(Error::RelateStatementId {
value: v.to_sql(),
})
}
};
out
};
let txn = ctx.tx();
let ns = txn.expect_ns_by_name(opt.ns()?).await?;
let db = txn.expect_db_by_name(opt.ns()?, opt.db()?).await?;
for f in from.iter() {
for t in to.iter() {
let through =
stk.run(|stk| self.through.compute(stk, ctx, opt, doc)).await.catch_return()?;
let through = RelateThrough::try_from(through)?;
let through_table = match &through {
RelateThrough::Table(tb) => tb,
RelateThrough::RecordId(rid) => &rid.table,
};
let tb =
txn.get_or_add_tb(Some(ctx), opt.ns()?, opt.db()?, through_table, None).await?;
let parent = NsDbCtx {
ns: Arc::clone(&ns),
db: Arc::clone(&db),
};
let doc_ctx =
DocumentContext::initialise(ctx, &parent, tb, through_table, opt.version, true)
.await?;
iterator.ingest(Iterable::Relatable(doc_ctx, f.clone(), through, t.clone(), None));
}
}
let stm = Statement::from(self);
CursorDoc::update_parent(ctx, doc, async |ctx| {
let res = iterator
.output(stk, ctx.as_ref(), opt, &stm, RecordStrategy::KeysAndValues)
.await?;
ctx.expect_not_timedout().await?;
match res {
Value::Array(mut a) if self.only => match a.len() {
1 => Ok(a.0.pop().expect("array has exactly one element")),
0 => Ok(Value::None),
_ => Err(anyhow::Error::new(Error::SingleOnlyOutput)),
},
v => Ok(v),
}
})
.await
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum RelateThrough {
RecordId(RecordId),
Table(TableName),
}
impl From<(TableName, Option<RecordIdKey>)> for RelateThrough {
fn from((table, id): (TableName, Option<RecordIdKey>)) -> Self {
if let Some(id) = id {
RelateThrough::RecordId(RecordId::new(table, id))
} else {
RelateThrough::Table(table)
}
}
}
impl TryFrom<Value> for RelateThrough {
type Error = anyhow::Error;
fn try_from(value: Value) -> Result<Self> {
match value {
Value::RecordId(id) => Ok(RelateThrough::RecordId(id)),
Value::Table(table) => Ok(RelateThrough::Table(table)),
_ => bail!(Error::RelateStatementOut {
value: value.to_sql()
}),
}
}
}
impl From<RelateThrough> for Value {
fn from(v: RelateThrough) -> Self {
match v {
RelateThrough::RecordId(id) => Value::RecordId(id),
RelateThrough::Table(table) => Value::Table(table),
}
}
}