#[cfg(feature = "sql")]
use crate::db::DynamicQueryResult;
use crate::{
db::{DynamicMutationResult, session::DbSession},
error::Error,
traits::CanisterKind,
types::{
Account, Blob, Date, Decimal, Duration, Float32, Float64, IntBig, NatBig, Principal,
Subaccount, Timestamp, Ulid, Unit,
},
value::{InputValue, OutputValue},
};
use candid::CandidType;
use icydb_core as core;
use icydb_schema::ScalarType;
use serde::Deserialize;
use std::{
collections::{BTreeMap, BTreeSet},
error::Error as StdError,
fmt,
};
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum WriteCell<T> {
Omitted,
Default,
Null,
Value(T),
}
impl<T> WriteCell<T> {
#[must_use]
pub fn map<U>(self, map: impl FnOnce(T) -> U) -> WriteCell<U> {
match self {
Self::Omitted => WriteCell::Omitted,
Self::Default => WriteCell::Default,
Self::Null => WriteCell::Null,
Self::Value(value) => WriteCell::Value(map(value)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OutputRow {
binding: core::db::DynamicTypedEntityBinding,
entity: String,
columns: Vec<String>,
values: Vec<OutputValue>,
}
impl OutputRow {
fn new(
binding: &TypedEntityBinding,
entity: impl Into<String>,
columns: Vec<String>,
values: Vec<OutputValue>,
) -> Result<Self, TypedAdapterError> {
if columns.len() != values.len() {
return Err(TypedAdapterError::RowShapeMismatch);
}
Ok(Self {
binding: binding.inner.clone(),
entity: entity.into(),
columns,
values,
})
}
#[must_use]
pub const fn entity(&self) -> &str {
self.entity.as_str()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TypedAdapterError {
EntityMismatch,
FieldUnavailable,
IncompatibleField,
RowFieldUnavailable,
RowShapeMismatch,
StaleBinding,
ValueShapeMismatch,
}
impl fmt::Display for TypedAdapterError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::EntityMismatch => "typed binding entity mismatch",
Self::FieldUnavailable => "typed binding field unavailable",
Self::IncompatibleField => "typed binding field contract is incompatible",
Self::RowFieldUnavailable => "typed row field unavailable",
Self::RowShapeMismatch => "typed row shape mismatch",
Self::StaleBinding => "typed binding is stale",
Self::ValueShapeMismatch => "typed row value shape mismatch",
})
}
}
impl StdError for TypedAdapterError {}
#[derive(Debug)]
pub enum TypedBindingError {
Adapter(TypedAdapterError),
Database(Error),
}
impl fmt::Display for TypedBindingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Adapter(error) => error.fmt(formatter),
Self::Database(error) => error.fmt(formatter),
}
}
}
impl StdError for TypedBindingError {}
#[derive(Debug)]
pub enum TypedRowError {
Adapter(TypedAdapterError),
Database(Error),
}
impl fmt::Display for TypedRowError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Adapter(error) => error.fmt(formatter),
Self::Database(error) => error.fmt(formatter),
}
}
}
impl StdError for TypedRowError {}
#[derive(Debug)]
pub enum TypedWriteError {
Adapter(TypedAdapterError),
Database(Error),
}
impl fmt::Display for TypedWriteError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Adapter(error) => error.fmt(formatter),
Self::Database(error) => error.fmt(formatter),
}
}
}
impl StdError for TypedWriteError {}
impl From<Error> for TypedWriteError {
fn from(error: Error) -> Self {
Self::Database(error)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TypedEntityBinding {
inner: core::db::DynamicTypedEntityBinding,
}
impl TypedEntityBinding {
const fn new(inner: core::db::DynamicTypedEntityBinding) -> Self {
Self { inner }
}
#[cfg(feature = "sql")]
pub(crate) const fn entity(&self) -> &str {
self.inner.entity()
}
pub fn row_value<'a>(
&self,
field_source_key: &str,
row: &'a OutputRow,
) -> Result<&'a OutputValue, TypedAdapterError> {
if row.binding != self.inner {
return Err(TypedAdapterError::StaleBinding);
}
if row.entity != self.inner.entity() {
return Err(TypedAdapterError::EntityMismatch);
}
let field = self
.inner
.field_name(field_source_key)
.ok_or(TypedAdapterError::FieldUnavailable)?;
let index = row
.columns
.iter()
.position(|column| column == field)
.ok_or(TypedAdapterError::RowFieldUnavailable)?;
row.values
.get(index)
.ok_or(TypedAdapterError::RowShapeMismatch)
}
#[doc(hidden)]
#[must_use]
pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
self.inner.named_type_name(source_key)
}
#[doc(hidden)]
#[must_use]
pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
self.inner.enum_variant_name(type_source_key, source_key)
}
#[doc(hidden)]
#[must_use]
pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
self.inner.composite_field_name(type_source_key, source_key)
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TypedFieldType {
Scalar(ScalarType),
List(Box<Self>),
Named(&'static str),
}
impl TypedFieldType {
fn into_core(self) -> core::db::DynamicTypedFieldType {
match self {
Self::Scalar(scalar) => core::db::DynamicTypedFieldType::Scalar(scalar),
Self::List(item) => core::db::DynamicTypedFieldType::List(Box::new(item.into_core())),
Self::Named(source_key) => {
core::db::DynamicTypedFieldType::Named(source_key.to_string())
}
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TypedFieldBindingRequest {
field_type: TypedFieldType,
nullable: bool,
source_key: &'static str,
}
impl TypedFieldBindingRequest {
#[must_use]
pub const fn new(source_key: &'static str, field_type: TypedFieldType, nullable: bool) -> Self {
Self {
field_type,
nullable,
source_key,
}
}
fn into_core(self) -> core::db::DynamicTypedFieldBindingRequest {
core::db::DynamicTypedFieldBindingRequest::new(
self.source_key.to_string(),
self.field_type.into_core(),
self.nullable,
)
}
}
pub trait TypedRowAdapter {
type Row;
fn decode_row(
binding: &TypedEntityBinding,
row: OutputRow,
) -> Result<Self::Row, TypedAdapterError>;
}
pub trait TypedEntityAdapter: TypedRowAdapter {
fn typed_binding<C>(session: &DbSession<C>) -> Result<TypedEntityBinding, TypedBindingError>
where
C: CanisterKind;
}
pub trait TypedWriteAdapter {
fn encode_write(self, binding: &TypedEntityBinding) -> Result<TypedWrite, TypedAdapterError>;
}
#[doc(hidden)]
pub trait TypedInputValue: Sized {
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError>;
}
#[doc(hidden)]
pub trait TypedNamedType {
const SOURCE_KEY: &'static str;
}
macro_rules! impl_typed_input_value {
($($ty:ty),* $(,)?) => {
$(
impl TypedInputValue for $ty {
fn encode_typed_input(
self,
_binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
Ok(InputValue::from(self))
}
}
)*
};
}
impl_typed_input_value!(
Account, Blob, bool, Date, Decimal, Duration, Float32, Float64, i8, i16, i32, i64, i128,
IntBig, NatBig, Principal, String, Subaccount, Timestamp, u8, u16, u32, u64, u128, Ulid, Unit,
);
impl<T> TypedInputValue for Box<T>
where
T: TypedInputValue,
{
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
(*self).encode_typed_input(binding)
}
}
impl<T> TypedInputValue for Option<T>
where
T: TypedInputValue,
{
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
self.map_or(Ok(InputValue::Null), |value| {
value.encode_typed_input(binding)
})
}
}
impl<T> TypedInputValue for Vec<T>
where
T: TypedInputValue,
{
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
self.into_iter()
.map(|value| value.encode_typed_input(binding))
.collect::<Result<Vec<_>, _>>()
.map(InputValue::List)
}
}
impl<K, V> TypedInputValue for BTreeMap<K, V>
where
K: TypedInputValue,
V: TypedInputValue,
{
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
self.into_iter()
.map(|(key, value)| {
Ok((
key.encode_typed_input(binding)?,
value.encode_typed_input(binding)?,
))
})
.collect::<Result<Vec<_>, _>>()
.map(InputValue::Map)
}
}
impl<T> TypedInputValue for BTreeSet<T>
where
T: TypedInputValue,
{
fn encode_typed_input(
self,
binding: &TypedEntityBinding,
) -> Result<InputValue, TypedAdapterError> {
self.into_iter()
.map(|value| value.encode_typed_input(binding))
.collect::<Result<Vec<_>, _>>()
.map(InputValue::List)
}
}
#[doc(hidden)]
pub trait TypedOutputValue: Sized {
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError>;
}
macro_rules! impl_typed_output_value_clone {
($($ty:ty => $variant:ident),* $(,)?) => {
$(
impl TypedOutputValue for $ty {
fn decode_typed_output(
_binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
match value {
OutputValue::$variant(value) => Ok(value.clone()),
_ => Err(TypedAdapterError::ValueShapeMismatch),
}
}
}
)*
};
}
macro_rules! impl_typed_output_value_narrow {
($($ty:ty => $variant:ident),* $(,)?) => {
$(
impl TypedOutputValue for $ty {
fn decode_typed_output(
_binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
let OutputValue::$variant(value) = value else {
return Err(TypedAdapterError::ValueShapeMismatch);
};
Self::try_from(*value).map_err(|_| TypedAdapterError::ValueShapeMismatch)
}
}
)*
};
}
impl_typed_output_value_clone!(
Account => Account,
bool => Bool,
Date => Date,
Decimal => Decimal,
Duration => Duration,
Float32 => Float32,
Float64 => Float64,
i64 => Int64,
i128 => Int128,
IntBig => IntBig,
NatBig => NatBig,
Principal => Principal,
String => Text,
Subaccount => Subaccount,
Timestamp => Timestamp,
u64 => Nat64,
u128 => Nat128,
Ulid => Ulid,
);
impl_typed_output_value_narrow!(
i8 => Int64,
i16 => Int64,
i32 => Int64,
u8 => Nat64,
u16 => Nat64,
u32 => Nat64,
);
impl TypedOutputValue for Blob {
fn decode_typed_output(
_binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
match value {
OutputValue::Blob(value) => Ok(Self::from(value.as_slice())),
_ => Err(TypedAdapterError::ValueShapeMismatch),
}
}
}
impl TypedOutputValue for Unit {
fn decode_typed_output(
_binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
match value {
OutputValue::Unit => Ok(Self),
_ => Err(TypedAdapterError::ValueShapeMismatch),
}
}
}
impl<T> TypedOutputValue for Box<T>
where
T: TypedOutputValue,
{
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
T::decode_typed_output(binding, value).map(Self::new)
}
}
impl<T> TypedOutputValue for Option<T>
where
T: TypedOutputValue,
{
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
match value {
OutputValue::Null => Ok(None),
_ => T::decode_typed_output(binding, value).map(Some),
}
}
}
impl<T> TypedOutputValue for Vec<T>
where
T: TypedOutputValue,
{
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
let OutputValue::List(values) = value else {
return Err(TypedAdapterError::ValueShapeMismatch);
};
values
.iter()
.map(|value| T::decode_typed_output(binding, value))
.collect()
}
}
impl<K, V> TypedOutputValue for BTreeMap<K, V>
where
K: Ord + TypedOutputValue,
V: TypedOutputValue,
{
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
let OutputValue::Map(entries) = value else {
return Err(TypedAdapterError::ValueShapeMismatch);
};
let mut decoded = Self::new();
for (key, value) in entries {
let key = K::decode_typed_output(binding, key)?;
let value = V::decode_typed_output(binding, value)?;
if decoded.insert(key, value).is_some() {
return Err(TypedAdapterError::ValueShapeMismatch);
}
}
Ok(decoded)
}
}
impl<T> TypedOutputValue for BTreeSet<T>
where
T: Ord + TypedOutputValue,
{
fn decode_typed_output(
binding: &TypedEntityBinding,
value: &OutputValue,
) -> Result<Self, TypedAdapterError> {
let OutputValue::List(values) = value else {
return Err(TypedAdapterError::ValueShapeMismatch);
};
let mut decoded = Self::new();
for value in values {
if !decoded.insert(T::decode_typed_output(binding, value)?) {
return Err(TypedAdapterError::ValueShapeMismatch);
}
}
Ok(decoded)
}
}
#[derive(Clone, Debug)]
pub struct TypedWrite {
binding: TypedEntityBinding,
mutation: StructuralMutation,
}
impl TypedWrite {
pub fn insert<I, S>(binding: &TypedEntityBinding, fields: I) -> Result<Self, TypedAdapterError>
where
I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
S: AsRef<str>,
{
let patch = structural_patch_from_binding(binding, fields)?;
Ok(Self {
binding: binding.clone(),
mutation: StructuralMutation::Insert {
entity: binding.inner.entity().to_string(),
patch,
},
})
}
pub fn update<I, S>(
binding: &TypedEntityBinding,
key: InputValue,
fields: I,
) -> Result<Self, TypedAdapterError>
where
I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
S: AsRef<str>,
{
let patch = structural_patch_from_binding(binding, fields)?;
Ok(Self {
binding: binding.clone(),
mutation: StructuralMutation::Update {
entity: binding.inner.entity().to_string(),
key,
patch,
},
})
}
pub fn replace<I, S>(
binding: &TypedEntityBinding,
key: InputValue,
fields: I,
) -> Result<Self, TypedAdapterError>
where
I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
S: AsRef<str>,
{
let patch = structural_patch_from_binding(binding, fields)?;
Ok(Self {
binding: binding.clone(),
mutation: StructuralMutation::Replace {
entity: binding.inner.entity().to_string(),
key,
patch,
},
})
}
}
fn structural_patch_from_binding<I, S>(
binding: &TypedEntityBinding,
fields: I,
) -> Result<StructuralPatch, TypedAdapterError>
where
I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
S: AsRef<str>,
{
let mut patch = StructuralPatch::new();
for (source, cell) in fields {
let field = binding
.inner
.field_name(source.as_ref())
.ok_or(TypedAdapterError::FieldUnavailable)?;
patch = patch.field(field, cell);
}
Ok(patch)
}
impl WriteCell<InputValue> {
fn into_core(self) -> core::db::DynamicWriteCell {
match self {
Self::Omitted => core::db::DynamicWriteCell::Omitted,
Self::Default => core::db::DynamicWriteCell::Default,
Self::Null => core::db::DynamicWriteCell::Null,
Self::Value(value) => core::db::DynamicWriteCell::Value(value),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct StructuralPatch {
fields: Vec<(String, WriteCell<InputValue>)>,
}
impl StructuralPatch {
#[must_use]
pub const fn new() -> Self {
Self { fields: Vec::new() }
}
#[must_use]
pub fn field(mut self, name: impl Into<String>, value: WriteCell<InputValue>) -> Self {
self.fields.push((name.into(), value));
self
}
fn into_core(self) -> core::db::DynamicStructuralPatch {
core::db::DynamicStructuralPatch::new(
self.fields
.into_iter()
.map(|(name, value)| (name, value.into_core()))
.collect(),
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StructuralMutation {
Insert {
entity: String,
patch: StructuralPatch,
},
Update {
entity: String,
key: InputValue,
patch: StructuralPatch,
},
Replace {
entity: String,
key: InputValue,
patch: StructuralPatch,
},
Delete {
entity: String,
key: InputValue,
},
}
impl StructuralMutation {
fn into_core(self) -> core::db::DynamicMutation {
match self {
Self::Insert { entity, patch } => core::db::DynamicMutation::Insert {
entity,
patch: patch.into_core(),
},
Self::Update { entity, key, patch } => core::db::DynamicMutation::Update {
entity,
key,
patch: patch.into_core(),
},
Self::Replace { entity, key, patch } => core::db::DynamicMutation::Replace {
entity,
key,
patch: patch.into_core(),
},
Self::Delete { entity, key } => core::db::DynamicMutation::Delete { entity, key },
}
}
}
impl<C: CanisterKind> DbSession<C> {
pub fn execute_trusted_structural_mutation(
&self,
request: StructuralMutation,
) -> Result<DynamicMutationResult, Error> {
Ok(self
.inner
.execute_trusted_dynamic_mutation(&request.into_core())?)
}
pub fn execute_trusted_structural_insert_batch(
&self,
entity: &str,
patches: Vec<StructuralPatch>,
) -> Result<DynamicMutationResult, Error> {
let patches = patches
.into_iter()
.map(StructuralPatch::into_core)
.collect();
Ok(self
.inner
.execute_trusted_dynamic_insert_batch(entity, patches)?)
}
fn typed_output_row(
&self,
binding: &TypedEntityBinding,
entity: &str,
columns: &[String],
rows: &[Vec<OutputValue>],
row_index: usize,
) -> Result<OutputRow, TypedRowError> {
let current = self
.inner
.typed_entity_binding_is_current(&binding.inner)
.map_err(|error| TypedRowError::Database(Error::from(error)))?;
if !current {
return Err(TypedRowError::Adapter(TypedAdapterError::StaleBinding));
}
if entity != binding.inner.entity() {
return Err(TypedRowError::Adapter(TypedAdapterError::EntityMismatch));
}
let values = rows
.get(row_index)
.cloned()
.ok_or(TypedRowError::Adapter(TypedAdapterError::RowShapeMismatch))?;
OutputRow::new(binding, entity, columns.to_vec(), values).map_err(TypedRowError::Adapter)
}
#[cfg(feature = "sql")]
pub fn typed_query_row(
&self,
binding: &TypedEntityBinding,
result: &DynamicQueryResult,
row_index: usize,
) -> Result<OutputRow, TypedRowError> {
self.typed_output_row(
binding,
result.entity.as_str(),
result.columns.as_slice(),
result.rows.as_slice(),
row_index,
)
}
pub fn typed_mutation_row(
&self,
binding: &TypedEntityBinding,
result: &DynamicMutationResult,
row_index: usize,
) -> Result<OutputRow, TypedRowError> {
self.typed_output_row(
binding,
result.entity.as_str(),
result.columns.as_slice(),
result.rows.as_slice(),
row_index,
)
}
#[doc(hidden)]
pub fn bind_typed_entity<I>(
&self,
entity_source_key: &str,
field_requests: I,
) -> Result<TypedEntityBinding, TypedBindingError>
where
I: IntoIterator<Item = TypedFieldBindingRequest>,
{
let fields = field_requests
.into_iter()
.map(TypedFieldBindingRequest::into_core)
.collect::<Vec<_>>();
self.inner
.issue_typed_entity_binding(entity_source_key, fields.as_slice())
.map(TypedEntityBinding::new)
.map_err(|error| match error {
core::db::DynamicTypedBindingError::FieldUnavailable => {
TypedBindingError::Adapter(TypedAdapterError::FieldUnavailable)
}
core::db::DynamicTypedBindingError::IncompatibleField => {
TypedBindingError::Adapter(TypedAdapterError::IncompatibleField)
}
core::db::DynamicTypedBindingError::Internal(error) => {
TypedBindingError::Database(Error::from(error))
}
})
}
pub fn execute_trusted_typed_write(
&self,
write: TypedWrite,
) -> Result<DynamicMutationResult, TypedWriteError> {
if !self
.inner
.typed_entity_binding_is_current(&write.binding.inner)
.map_err(Error::from)?
{
return Err(TypedWriteError::Adapter(TypedAdapterError::StaleBinding));
}
self.execute_trusted_structural_mutation(write.mutation)
.map_err(TypedWriteError::Database)
}
#[must_use]
pub fn structural_patch<I, S>(&self, fields: I) -> StructuralPatch
where
I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
S: Into<String>,
{
StructuralPatch {
fields: fields
.into_iter()
.map(|(name, value)| (name.into(), value))
.collect(),
}
}
}