use std::borrow::Cow;
use std::ops::Deref;
use anyhow::{Result, ensure};
use reblessive::tree::Stk;
use tracing::instrument;
use uuid::Uuid;
use crate::catalog::providers::TableProvider;
use crate::catalog::{self, Error as CatalogError};
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::exec::Error as ExecError;
use crate::expr::Base;
use crate::expr::statements::alter::AlterKind;
use crate::expr::statements::alter::field::{AlterDefault, AlterFieldStatement};
use crate::expr::statements::define::kind_contains_object;
use crate::iam::{Action, AuthLimit, ResourceKind};
use crate::key::schema::FieldKey;
use crate::legacy::{expr_to_ident, expr_to_idiom};
use crate::val::{TableName, Value};
#[instrument(level = "trace", name = "AlterFieldStatement::compute", skip_all)]
pub(crate) async fn alter_field_statement_compute(
this: &AlterFieldStatement,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
) -> Result<Value> {
ctx.is_allowed(opt, Action::Edit, ResourceKind::Field, Base::Db)?;
let (ns_name, db_name) = opt.ns_db()?;
let (ns, db) = ctx.expect_ns_db_ids(opt).await?;
let txn = ctx.tx();
let idiom = expr_to_idiom(stk, ctx, opt, doc, &this.name, "field name").await?;
let name = idiom.to_raw_string();
let what = TableName::new(expr_to_ident(stk, ctx, opt, doc, &this.what, "table name").await?);
let mut df = match txn.get_tb_field(ns, db, &what, &name, None).await? {
Some(tb) => tb.deref().clone(),
None => {
if this.if_exists {
return Ok(Value::None);
}
return Err(CatalogError::FdNotFound {
name,
}
.into());
}
};
if let Some(tb) = txn.get_tb(ns, db, &what, None).await? {
ensure!(
crate::kvs::lightweight::lightweight_relation(&tb.table_type).is_none(),
ExecError::Thrown(
"a LIGHTWEIGHT relation's fields cannot be altered: its in/out contract is \
managed by the table definition"
.to_owned(),
)
);
}
let old_definition = df.clone();
match this.kind {
AlterKind::Set(ref k) => df.field_kind = Some(k.clone()),
AlterKind::Drop => df.field_kind = None,
AlterKind::None => {}
}
match this.flexible {
AlterKind::Set(_) => df.flexible = true,
AlterKind::Drop => df.flexible = false,
AlterKind::None => {}
}
match this.readonly {
AlterKind::Set(_) => df.readonly = true,
AlterKind::Drop => df.readonly = false,
AlterKind::None => {}
}
match this.inline {
AlterKind::Set(_) => df.inline = true,
AlterKind::Drop => df.inline = false,
AlterKind::None => {}
}
match this.value {
AlterKind::Set(ref k) => df.value = Some(k.clone()),
AlterKind::Drop => df.value = None,
AlterKind::None => {}
}
match this.assert {
AlterKind::Set(ref k) => df.assert = Some(k.clone()),
AlterKind::Drop => df.assert = None,
AlterKind::None => {}
}
match this.default {
AlterDefault::None => {}
AlterDefault::Drop => df.default = catalog::DefineDefault::None,
AlterDefault::Always(ref expr) => df.default = catalog::DefineDefault::Always(expr.clone()),
AlterDefault::Set(ref expr) => df.default = catalog::DefineDefault::Set(expr.clone()),
}
if let Some(permissions) = &this.permissions {
df.select_permission = permissions.select.clone();
df.create_permission = permissions.create.clone();
df.update_permission = permissions.update.clone();
}
match this.comment {
AlterKind::Set(ref k) => df.comment = Some(k.clone()),
AlterKind::Drop => df.comment = None,
AlterKind::None => {}
}
match this.reference {
AlterKind::Set(ref k) => df.reference = Some(k.clone()),
AlterKind::Drop => df.reference = None,
AlterKind::None => {}
}
df.auth_limit = AuthLimit::new_from_auth(opt.auth.as_ref()).into();
crate::legacy::expr::statements::define::field::validate_id_field_restrictions(&df)?;
if df.inline && !opt.import {
let is_relation = txn
.get_tb(ns, db, &what, None)
.await?
.is_some_and(|tb| matches!(tb.table_type, catalog::TableType::Relation(_)));
ensure!(
is_relation,
ExecError::Thrown(
"INLINE requires an existing TYPE RELATION table: the value is embedded into \
the edges' adjacency payloads"
.to_owned(),
)
);
ensure!(
!matches!(name.as_str(), "id" | "in" | "out"),
ExecError::Thrown(
"the `id`, `in` and `out` fields already live in the adjacency keys and cannot \
be INLINE"
.to_owned(),
)
);
ensure!(
df.name.len() == 1,
ExecError::Thrown("only a top-level field can be INLINE".to_owned())
);
ensure!(
df.computed.is_none(),
ExecError::Thrown(
"a COMPUTED field cannot be INLINE: its value is derived at read time, so \
there is nothing stored to embed"
.to_owned(),
)
);
let fields = txn.all_tb_fields(ns, db, &what, None).await?;
let others = fields.iter().filter(|f| f.inline && f.name != df.name).count();
ensure!(
others < u8::MAX as usize,
ExecError::Thrown(
"a table cannot carry more than 255 INLINE fields: the adjacency payload's \
value count is a single byte"
.to_owned(),
)
);
}
if df.flexible {
ensure!(
df.field_kind.as_ref().is_some_and(kind_contains_object),
ExecError::Thrown("FLEXIBLE can only be used with types containing object".into())
);
let Some(tb) = txn.get_tb(ns, db, &what, None).await? else {
return Err(CatalogError::TbNotFound {
name: what.clone(),
}
.into());
};
ensure!(
tb.schemafull,
ExecError::Thrown("FLEXIBLE can only be used in SCHEMAFULL tables".into())
);
}
let key = FieldKey {
ns,
db,
tb: Cow::Borrowed(&what),
fd: Cow::Borrowed(&name),
};
txn.set_key(&key, &df.to_stored()).await?;
if !opt.import {
crate::legacy::expr::statements::define::field::purge_dropped_reference_keys(
&txn,
ns,
db,
&what,
&old_definition,
Some(&df),
)
.await?;
}
let Some(tb) = txn.get_tb(ns, db, &what, None).await? else {
return Err(CatalogError::TbNotFound {
name: what.clone(),
}
.into());
};
let inline_toggled = df.inline != old_definition.inline;
let inline_kind_changed =
df.inline && old_definition.inline && df.field_kind != old_definition.field_kind;
txn.replace_tb(
ns_name,
db_name,
&catalog::TableDefinition {
cache_fields_ts: Uuid::now_v7(),
graph_inline_gen: if inline_toggled || inline_kind_changed {
tb.graph_inline_gen.wrapping_add(1)
} else {
tb.graph_inline_gen
},
..(*tb).clone()
},
)
.await?;
txn.clear_cache();
Ok(Value::None)
}