use std::sync::Arc;
use anyhow::{Result, bail};
use reblessive::tree::Stk;
use surrealdb_types::{SqlFormat, 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::paths::{IN, OUT};
use crate::expr::statements::relate::RelateThrough;
use crate::expr::{Data, Expr, FlowResultExt as _, Output, Value};
use crate::idx::planner::RecordStrategy;
use crate::val::{Duration, RecordIdKey, TableName};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct InsertStatement {
pub into: Option<Expr>,
pub data: Data,
pub ignore: bool,
pub update: Option<Data>,
pub output: Option<Output>,
pub timeout: Expr,
pub relation: bool,
}
impl InsertStatement {
#[instrument(level = "trace", name = "InsertStatement::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;
let ctx = match stk
.run(|stk| self.timeout.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to::<Option<Duration>>()?
{
Some(timeout) => {
let mut ctx = Context::new_child(ctx);
ctx.add_timeout(timeout.0)?;
ctx_store = ctx.freeze();
&ctx_store
}
None => ctx,
};
let tb = match &self.into {
Some(into) => {
match stk.run(|stk| into.compute(stk, ctx, opt, doc)).await.catch_return()? {
Value::Table(into) => Some(into),
Value::String(into) => Some(TableName::new(into)),
_ => {
return Err(Error::InsertStatement {
value: into.to_sql(),
}
.into());
}
}
}
None => None,
};
let txn = ctx.tx();
let ns = ctx.tx().expect_ns_by_name(opt.ns()?).await?;
let db = ctx.tx().expect_db_by_name(opt.ns()?, opt.db()?).await?;
let parent = NsDbCtx {
ns: Arc::clone(&ns),
db: Arc::clone(&db),
};
let mut doc_ctx = None;
if let Some(tb) = &tb {
let tb_def = ctx.tx().get_or_add_tb(Some(ctx), &ns.name, &db.name, tb, None).await?;
doc_ctx = Some(
DocumentContext::initialise(ctx, &parent, tb_def, tb, opt.version, true).await?,
);
}
match &self.data {
Data::ValuesExpression(v) => {
for v in v {
let mut o = Value::empty_object();
for (k, v) in v.iter() {
let v =
stk.run(|stk| v.compute(stk, ctx, opt, None)).await.catch_return()?;
o.set(stk, ctx, opt, k, v).await?;
}
let (tb, id) = extract_table_and_rid_key(&o, &tb)?;
doc_ctx = match doc_ctx {
Some(ref dc) if dc.tb().is_ok_and(|t| t.name == tb) => doc_ctx,
Some(_) | None => {
let tb_def =
txn.get_or_add_tb(Some(ctx), &ns.name, &db.name, &tb, None).await?;
Some(
DocumentContext::initialise(
ctx,
&parent,
tb_def,
&tb,
opt.version,
true,
)
.await?,
)
}
};
iterator.ingest(iterable(
doc_ctx.clone().expect("doc_ctx must be set at this point"),
tb.clone(),
id,
o,
self.relation,
)?)
}
}
Data::SingleExpression(v) => {
let v = stk.run(|stk| v.compute(stk, ctx, opt, doc)).await.catch_return()?;
match v {
Value::Array(v) => {
for v in v {
let (tb, id) = extract_table_and_rid_key(&v, &tb)?;
doc_ctx = match doc_ctx {
Some(ref dc) if dc.tb().is_ok_and(|t| t.name == tb) => doc_ctx,
Some(_) | None => {
let tb_def = txn
.get_or_add_tb(Some(ctx), &ns.name, &db.name, &tb, None)
.await?;
Some(
DocumentContext::initialise(
ctx,
&parent,
tb_def,
&tb,
opt.version,
true,
)
.await?,
)
}
};
iterator.ingest(iterable(
doc_ctx.clone().expect("doc_ctx must be set at this point"),
tb.clone(),
id,
v,
self.relation,
)?)
}
}
Value::Object(_) => {
let (tb, id) = extract_table_and_rid_key(&v, &tb)?;
doc_ctx = match doc_ctx {
Some(ref dc) if dc.tb().is_ok_and(|t| t.name == tb) => doc_ctx,
Some(_) | None => {
let tb_def = txn
.get_or_add_tb(Some(ctx), &ns.name, &db.name, &tb, None)
.await?;
Some(
DocumentContext::initialise(
ctx,
&parent,
tb_def,
&tb,
opt.version,
true,
)
.await?,
)
}
};
iterator.ingest(iterable(
doc_ctx.clone().expect("doc_ctx must be set at this point"),
tb.clone(),
id,
v,
self.relation,
)?)
}
v => {
bail!(Error::InsertStatement {
value: v.to_sql(),
})
}
}
}
v => fail!("Unknown data clause type in INSERT statement: {v:?}"),
}
let stm = Statement::from(self);
ctx.get_db(opt).await?;
CursorDoc::update_parent(ctx, doc, async |ctx| {
let res = iterator.output(stk, &ctx, opt, &stm, RecordStrategy::KeysAndValues).await?;
ctx.expect_not_timedout().await?;
Ok(res)
})
.await
}
}
impl ToSql for InsertStatement {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
let stmt: crate::sql::statements::insert::InsertStatement = self.clone().into();
stmt.fmt_sql(f, fmt);
}
}
fn iterable(
doc_ctx: DocumentContext,
tb: TableName,
id: Option<RecordIdKey>,
v: Value,
relation: bool,
) -> Result<Iterable> {
if relation {
let f = match v.pick(&IN) {
Value::RecordId(v) => v,
v => {
bail!(Error::InsertStatementIn {
value: v.to_sql(),
})
}
};
let w = match v.pick(&OUT) {
Value::RecordId(v) => v,
v => {
bail!(Error::InsertStatementOut {
value: v.to_sql(),
})
}
};
Ok(Iterable::Relatable(doc_ctx, f, RelateThrough::from((tb, id)), w, Some(v)))
} else {
Ok(Iterable::Mergeable(doc_ctx, tb, id, v))
}
}
fn extract_table_and_rid_key(
record: &Value,
into: &Option<TableName>,
) -> Result<(TableName, Option<RecordIdKey>)> {
let Some(tb) = into else {
let record = record.rid();
let Value::RecordId(rid) = record else {
bail!(Error::InsertStatementId {
value: record.to_sql(),
});
};
return Ok((rid.table, Some(rid.key)));
};
let rid = match record.rid() {
Value::Number(id) if id.is_float() => match id.as_int_lossless() {
Some(i) => Some(RecordIdKey::Number(i)),
None => bail!(Error::InsertStatementId {
value: Value::Number(id).to_sql(),
}),
},
Value::Number(id) if id.is_int() => Some(RecordIdKey::Number(id.as_int())),
Value::String(id) if !id.is_empty() => Some(id.into()),
Value::Object(id) => Some(id.into()),
Value::Array(id) => Some(id.into()),
Value::Uuid(id) => Some(id.into()),
Value::RecordId(id) => {
Some(id.key)
}
Value::None => None,
v => {
bail!(Error::InsertStatementId {
value: v.to_sql(),
});
}
};
Ok((tb.clone(), rid))
}