use crate::ctx::Context;
use crate::dbs::capabilities::ExperimentalTarget;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::fmt::{is_pretty, pretty_indent};
use crate::sql::reference::Reference;
use crate::sql::statements::info::InfoStructure;
use crate::sql::statements::DefineTableStatement;
use crate::sql::{Base, Ident, Idiom, Kind, Permissions, Strand, Value};
use crate::sql::{Literal, Part};
use crate::sql::{Relation, TableType};
use derive::Store;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Write};
use uuid::Uuid;
#[revisioned(revision = 6)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct DefineFieldStatement {
pub name: Idiom,
pub what: Ident,
pub flex: bool,
pub kind: Option<Kind>,
#[revision(start = 2)]
pub readonly: bool,
pub value: Option<Value>,
pub assert: Option<Value>,
pub default: Option<Value>,
pub permissions: Permissions,
pub comment: Option<Strand>,
#[revision(start = 3)]
pub if_not_exists: bool,
#[revision(start = 4)]
pub overwrite: bool,
#[revision(start = 5)]
pub reference: Option<Reference>,
#[revision(start = 6)]
pub default_always: bool,
}
impl DefineFieldStatement {
pub(crate) async fn compute(
&self,
ctx: &Context,
opt: &Options,
_doc: Option<&CursorDoc>,
) -> Result<Value, Error> {
opt.is_allowed(Action::Edit, ResourceKind::Field, &Base::Db)?;
self.validate_reference_options(ctx)?;
let kind = if let Some(kind) = self.correct_reference_type(ctx, opt).await? {
Some(kind)
} else {
self.kind.clone()
};
self.disallow_mismatched_types(ctx, opt).await?;
let ns = opt.ns()?;
let db = opt.db()?;
let txn = ctx.tx();
let fd = self.name.to_string();
if txn.get_tb_field(ns, db, &self.what, &fd).await.is_ok() {
if self.if_not_exists {
return Ok(Value::None);
} else if !self.overwrite {
return Err(Error::FdAlreadyExists {
value: fd,
});
}
}
let key = crate::key::table::fd::new(ns, db, &self.what, &fd);
txn.get_or_add_ns(ns, opt.strict).await?;
txn.get_or_add_db(ns, db, opt.strict).await?;
txn.get_or_add_tb(ns, db, &self.what, opt.strict).await?;
txn.set(
key,
DefineFieldStatement {
if_not_exists: false,
overwrite: false,
kind,
..self.clone()
},
None,
)
.await?;
let key = crate::key::database::tb::new(ns, db, &self.what);
let tb = txn.get_tb(ns, db, &self.what).await?;
txn.set(
key,
DefineTableStatement {
cache_fields_ts: Uuid::now_v7(),
..tb.as_ref().clone()
},
None,
)
.await?;
if let Some(cache) = ctx.get_cache() {
cache.clear_tb(ns, db, &self.what);
}
txn.clear();
let fields = txn.all_tb_fields(ns, db, &self.what, None).await.ok();
if let Some(mut cur_kind) = self.kind.as_ref().and_then(|x| x.inner_kind()) {
let mut name = self.name.clone();
loop {
if let Kind::Any = cur_kind {
break;
}
let new_kind = cur_kind.inner_kind();
name.0.push(Part::All);
let fd = name.to_string();
let key = crate::key::table::fd::new(ns, db, &self.what, &fd);
let val = if let Some(existing) =
fields.as_ref().and_then(|x| x.iter().find(|x| x.name == name))
{
DefineFieldStatement {
kind: Some(cur_kind),
reference: self.reference.clone(),
if_not_exists: false,
overwrite: false,
..existing.clone()
}
} else {
DefineFieldStatement {
name: name.clone(),
what: self.what.clone(),
flex: self.flex,
kind: Some(cur_kind),
reference: self.reference.clone(),
..Default::default()
}
};
txn.set(key, val, None).await?;
if let Some(new_kind) = new_kind {
cur_kind = new_kind;
} else {
break;
}
}
}
if fd.as_str() == "in" {
let tb = txn.get_tb(ns, db, &self.what).await?;
if let TableType::Relation(ref relation) = tb.kind {
if let Some(kind) = self.kind.as_ref() {
if !kind.is_record() {
return Err(Error::Thrown(
"in field on a relation must be a record".into(),
));
}
if relation.from.as_ref() != self.kind.as_ref() {
let key = crate::key::database::tb::new(ns, db, &self.what);
let val = DefineTableStatement {
cache_fields_ts: Uuid::now_v7(),
kind: TableType::Relation(Relation {
from: self.kind.to_owned(),
..relation.to_owned()
}),
..tb.as_ref().to_owned()
};
txn.set(key, val, None).await?;
if let Some(cache) = ctx.get_cache() {
cache.clear_tb(ns, db, &self.what);
}
txn.clear();
}
}
}
}
if fd.as_str() == "out" {
let tb = txn.get_tb(ns, db, &self.what).await?;
if let TableType::Relation(ref relation) = tb.kind {
if let Some(kind) = self.kind.as_ref() {
if !kind.is_record() {
return Err(Error::Thrown(
"out field on a relation must be a record".into(),
));
}
if relation.from.as_ref() != self.kind.as_ref() {
let key = crate::key::database::tb::new(ns, db, &self.what);
let val = DefineTableStatement {
cache_fields_ts: Uuid::now_v7(),
kind: TableType::Relation(Relation {
to: self.kind.to_owned(),
..relation.to_owned()
}),
..tb.as_ref().to_owned()
};
txn.set(key, val, None).await?;
if let Some(cache) = ctx.get_cache() {
cache.clear_tb(ns, db, &self.what);
}
txn.clear();
}
}
}
}
txn.clear();
Ok(Value::None)
}
fn validate_reference_options(&self, ctx: &Context) -> Result<(), Error> {
if !ctx.get_capabilities().allows_experimental(&ExperimentalTarget::RecordReferences) {
return Ok(());
}
if let Some(kind) = &self.kind {
let kinds = match kind {
Kind::Either(kinds) => kinds,
kind => &vec![kind.to_owned()],
};
if kinds.iter().any(|k| matches!(k, Kind::References(_, _))) {
if !kinds.iter().all(|k| matches!(k, Kind::References(_, _))) {
return Err(Error::RefsMismatchingVariants);
}
let typename = kind.to_string();
if self.reference.is_some() {
return Err(Error::RefsTypeConflict("REFERENCE".into(), typename));
}
if self.default.is_some() {
return Err(Error::RefsTypeConflict("DEFAULT".into(), typename));
}
if self.value.is_some() {
return Err(Error::RefsTypeConflict("VALUE".into(), typename));
}
if self.assert.is_some() {
return Err(Error::RefsTypeConflict("ASSERT".into(), typename));
}
if self.flex {
return Err(Error::RefsTypeConflict("FLEXIBLE".into(), typename));
}
if self.readonly {
return Err(Error::RefsTypeConflict("READONLY".into(), typename));
}
}
if self.reference.is_some() {
let kinds = match kind.non_optional() {
Kind::Either(kinds) => kinds,
Kind::Array(kind, _) | Kind::Set(kind, _) => match kind.as_ref() {
Kind::Either(kinds) => kinds,
kind => &vec![kind.to_owned()],
},
Kind::Literal(lit) => match lit {
Literal::Array(kinds) => kinds,
lit => &vec![Kind::Literal(lit.to_owned())],
},
kind => &vec![kind.to_owned()],
};
if !kinds.iter().all(|k| matches!(k, Kind::Record(_))) {
return Err(Error::ReferenceTypeConflict(kind.to_string()));
}
}
}
Ok(())
}
async fn correct_reference_type(
&self,
ctx: &Context,
opt: &Options,
) -> Result<Option<Kind>, Error> {
if !ctx.get_capabilities().allows_experimental(&ExperimentalTarget::RecordReferences) {
return Ok(None);
}
if let Some(Kind::References(Some(ft), Some(ff))) = &self.kind {
let fd = match ctx
.tx()
.get_tb_field(opt.ns()?, opt.db()?, &ft.to_string(), &ff.to_string())
.await
{
Ok(fd) => fd,
Err(Error::FdNotFound {
..
}) => return Ok(None),
Err(e) => return Err(e),
};
let is_contained = if let Some(kind) = &fd.kind {
matches!(
kind.non_optional(),
Kind::Array(_, _) | Kind::Set(_, _) | Kind::Literal(Literal::Array(_))
)
} else {
false
};
if is_contained {
let ff = ff.clone().push(Part::All);
return Ok(Some(Kind::References(Some(ft.clone()), Some(ff))));
}
}
Ok(None)
}
async fn disallow_mismatched_types(&self, ctx: &Context, opt: &Options) -> Result<(), Error> {
let fds = ctx.tx().all_tb_fields(opt.ns()?, opt.db()?, &self.what, None).await?;
if let Some(self_kind) = &self.kind {
for fd in fds.iter() {
if self.name.starts_with(&fd.name) && self.name != fd.name {
if let Some(fd_kind) = &fd.kind {
let path = self.name[fd.name.len()..].to_vec();
if !fd_kind.allows_nested_kind(&path, self_kind) {
return Err(Error::MismatchedFieldTypes {
name: self.name.to_string(),
kind: self_kind.to_string(),
existing_name: fd.name.to_string(),
existing_kind: fd_kind.to_string(),
});
}
}
}
}
}
Ok(())
}
}
impl Display for DefineFieldStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DEFINE FIELD")?;
if self.if_not_exists {
write!(f, " IF NOT EXISTS")?
}
if self.overwrite {
write!(f, " OVERWRITE")?
}
write!(f, " {} ON {}", self.name, self.what)?;
if self.flex {
write!(f, " FLEXIBLE")?
}
if let Some(ref v) = self.kind {
write!(f, " TYPE {v}")?
}
if let Some(ref v) = self.default {
write!(f, " DEFAULT")?;
if self.default_always {
write!(f, " ALWAYS")?
}
write!(f, " {v}")?
}
if self.readonly {
write!(f, " READONLY")?
}
if let Some(ref v) = self.value {
write!(f, " VALUE {v}")?
}
if let Some(ref v) = self.assert {
write!(f, " ASSERT {v}")?
}
if let Some(ref v) = self.reference {
write!(f, " REFERENCE {v}")?
}
if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")?
}
let _indent = if is_pretty() {
Some(pretty_indent())
} else {
f.write_char(' ')?;
None
};
write!(f, "{:#}", self.permissions)?;
Ok(())
}
}
impl InfoStructure for DefineFieldStatement {
fn structure(self) -> Value {
Value::from(map! {
"name".to_string() => self.name.structure(),
"what".to_string() => self.what.structure(),
"flex".to_string() => self.flex.into(),
"kind".to_string(), if let Some(v) = self.kind => v.structure(),
"value".to_string(), if let Some(v) = self.value => v.structure(),
"assert".to_string(), if let Some(v) = self.assert => v.structure(),
"default".to_string(), if let Some(v) = self.default => v.structure(),
"reference".to_string(), if let Some(v) = self.reference => v.structure(),
"readonly".to_string() => self.readonly.into(),
"permissions".to_string() => self.permissions.structure(),
"comment".to_string(), if let Some(v) = self.comment => v.into(),
})
}
}