Skip to main content

icydb/db/session/
write.rs

1//! Module: db::session::write
2//!
3//! Responsibility: public `DbSession` write helpers, write-returning projection
4//! conversion, and structural mutation facade types.
5//! Does not own: core mutation execution, commit staging, or persisted encoding.
6//! Boundary: keeps public write semantics and row-returning projection payloads
7//! above the core save pipeline.
8
9use crate::{
10    db::{DynamicMutationResult, TypedBindingContext, session::DbSession},
11    error::Error,
12    traits::CanisterKind,
13    value::{InputValue, OutputValue, PublicEnumValue, PublicValue},
14};
15
16use icydb_core as core;
17use serde::Deserialize;
18use std::{
19    collections::BTreeSet, error::Error as StdError, fmt, marker::PhantomData, sync::Arc,
20    vec::IntoIter,
21};
22
23#[doc(hidden)]
24pub use icydb_core::db::{TypedEntityDescriptor, TypedFieldDescriptor, TypedFieldType};
25
26///
27/// WriteCell
28///
29/// Explicit authored intent for one structural or generated typed write field.
30/// The database retains the distinction through accepted-policy resolution.
31///
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
34pub enum WriteCell<T> {
35    /// Supply no authored value for this operation.
36    Omitted,
37
38    /// Explicitly request the accepted database default.
39    Default,
40
41    /// Explicitly author `NULL`.
42    Null,
43
44    /// Explicitly author one concrete value.
45    Value(T),
46}
47
48impl<T> WriteCell<T> {
49    /// Map only the authored value while retaining its exact write intent.
50    #[must_use]
51    pub fn map<U>(self, map: impl FnOnce(T) -> U) -> WriteCell<U> {
52        match self {
53            Self::Omitted => WriteCell::Omitted,
54            Self::Default => WriteCell::Default,
55            Self::Null => WriteCell::Null,
56            Self::Value(value) => WriteCell::Value(map(value)),
57        }
58    }
59}
60
61/// One complete accepted row supplied to an automatic generated adapter.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct OutputRow {
64    projection: OutputRowProjection,
65    values: Vec<OutputValue>,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub(crate) struct OutputRowProjection(Arc<OutputRowProjectionInner>);
70
71#[derive(Debug, Eq, PartialEq)]
72struct OutputRowProjectionInner {
73    binding: TypedEntityBinding,
74    entity: String,
75    accepted_slots: Vec<(u16, usize)>,
76    value_count: usize,
77}
78
79impl OutputRowProjection {
80    pub(crate) fn new(
81        binding: &TypedEntityBinding,
82        entity: impl Into<String>,
83        columns: &[String],
84    ) -> Result<Self, TypedAdapterError> {
85        let entity = entity.into();
86        if entity != binding.entity() {
87            return Err(TypedAdapterError::EntityMismatch);
88        }
89        let mut seen_slots = BTreeSet::new();
90        let mut accepted_slots = Vec::with_capacity(columns.len());
91        for (value_index, column) in columns.iter().enumerate() {
92            let Some(slot) = binding.inner.output_field_slot(column) else {
93                continue;
94            };
95            if !seen_slots.insert(slot) {
96                return Err(TypedAdapterError::RowShapeMismatch);
97            }
98            accepted_slots.push((slot, value_index));
99        }
100        Ok(Self(Arc::new(OutputRowProjectionInner {
101            binding: binding.clone(),
102            entity,
103            accepted_slots,
104            value_count: columns.len(),
105        })))
106    }
107
108    pub(crate) fn project(&self, values: Vec<OutputValue>) -> Result<OutputRow, TypedAdapterError> {
109        if values.len() != self.0.value_count {
110            return Err(TypedAdapterError::RowShapeMismatch);
111        }
112        Ok(self.project_validated(values))
113    }
114
115    fn project_validated(&self, values: Vec<OutputValue>) -> OutputRow {
116        OutputRow {
117            projection: self.clone(),
118            values,
119        }
120    }
121
122    fn entity(&self) -> &str {
123        self.0.entity.as_str()
124    }
125
126    fn binding(&self) -> &TypedEntityBinding {
127        &self.0.binding
128    }
129}
130
131/// Owned output rows sharing one accepted typed projection.
132#[doc(hidden)]
133pub struct PreparedOutputRows {
134    projection: OutputRowProjection,
135    rows: IntoIter<Vec<OutputValue>>,
136}
137
138impl PreparedOutputRows {
139    fn new(
140        binding: &TypedEntityBinding,
141        entity: String,
142        columns: Vec<String>,
143        rows: Vec<Vec<OutputValue>>,
144    ) -> Result<Self, TypedAdapterError> {
145        let projection = OutputRowProjection::new(binding, entity, columns.as_slice())?;
146        for values in &rows {
147            if values.len() != projection.0.value_count {
148                return Err(TypedAdapterError::RowShapeMismatch);
149            }
150        }
151        Ok(Self {
152            projection,
153            rows: rows.into_iter(),
154        })
155    }
156}
157
158impl Iterator for PreparedOutputRows {
159    type Item = OutputRow;
160
161    fn next(&mut self) -> Option<Self::Item> {
162        self.rows
163            .next()
164            .map(|values| self.projection.project_validated(values))
165    }
166
167    fn size_hint(&self) -> (usize, Option<usize>) {
168        self.rows.size_hint()
169    }
170}
171
172impl ExactSizeIterator for PreparedOutputRows {}
173
174impl OutputRow {
175    fn new(
176        binding: &TypedEntityBinding,
177        entity: impl Into<String>,
178        columns: &[String],
179        values: Vec<OutputValue>,
180    ) -> Result<Self, TypedAdapterError> {
181        OutputRowProjection::new(binding, entity, columns)?.project(values)
182    }
183
184    /// Borrow the accepted entity display name.
185    #[must_use]
186    pub fn entity(&self) -> &str {
187        self.projection.entity()
188    }
189}
190
191struct MutationResultRowParts {
192    affected_rows: u32,
193    columns: Vec<String>,
194    entity: String,
195    values: Vec<OutputValue>,
196}
197
198fn mutation_result_row_parts(
199    result: DynamicMutationResult,
200) -> Result<MutationResultRowParts, TypedAdapterError> {
201    let DynamicMutationResult {
202        entity,
203        columns,
204        mut rows,
205        affected_rows,
206    } = result;
207    if rows.len() != 1 {
208        return Err(TypedAdapterError::RowShapeMismatch);
209    }
210    let values = rows.pop().ok_or(TypedAdapterError::RowShapeMismatch)?;
211    Ok(MutationResultRowParts {
212        affected_rows,
213        columns,
214        entity,
215        values,
216    })
217}
218
219#[inline(never)]
220fn project_single_mutation_result(
221    binding: &TypedEntityBinding,
222    result: DynamicMutationResult,
223) -> Result<OutputRow, TypedAdapterError> {
224    let parts = mutation_result_row_parts(result)?;
225    OutputRow::new(
226        binding,
227        parts.entity,
228        parts.columns.as_slice(),
229        parts.values,
230    )
231}
232
233#[inline(never)]
234fn project_mutation_result_batch(
235    binding: &TypedEntityBinding,
236    results: Vec<DynamicMutationResult>,
237    expected_results: usize,
238) -> Result<Vec<OutputRow>, TypedAdapterError> {
239    if results.len() != expected_results {
240        return Err(TypedAdapterError::RowShapeMismatch);
241    }
242    let mut results = results.into_iter();
243    let Some(first) = results.next() else {
244        return Ok(Vec::new());
245    };
246    let first = mutation_result_row_parts(first)?;
247    let projection = OutputRowProjection::new(binding, first.entity, first.columns.as_slice())?;
248    let mut projected = Vec::with_capacity(expected_results);
249    projected.push(projection.project(first.values)?);
250
251    for result in results {
252        let result = mutation_result_row_parts(result)?;
253        if result.entity != projection.entity() {
254            return Err(TypedAdapterError::EntityMismatch);
255        }
256        if result.columns != first.columns {
257            return Err(TypedAdapterError::RowShapeMismatch);
258        }
259        projected.push(projection.project(result.values)?);
260    }
261
262    Ok(projected)
263}
264
265/// Stable typed-adapter boundary failures.
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub enum TypedAdapterError {
268    /// A typed item handle does not belong to this exact batch result owner.
269    BatchHandleMismatch,
270
271    /// The typed batch row selected by this handle has already been decoded.
272    BatchRowConsumed,
273
274    /// A generated entity or field source cannot bind to accepted schema authority.
275    BindingUnavailable(TypedBindingContext),
276
277    /// The row belongs to another accepted entity.
278    EntityMismatch,
279
280    /// An immutable source key is absent from the binding projection.
281    FieldUnavailable,
282
283    /// The generated field contract disagrees with accepted authority.
284    IncompatibleField,
285
286    /// The row does not contain the bound accepted field.
287    RowFieldUnavailable,
288
289    /// Column and value cardinalities disagree.
290    RowShapeMismatch,
291
292    /// The binding no longer matches current accepted authority.
293    StaleBinding,
294
295    /// A public output value does not match the generated Rust field shape.
296    ValueShapeMismatch,
297}
298
299impl fmt::Display for TypedAdapterError {
300    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301        formatter.write_str(match self {
302            Self::BatchHandleMismatch => "typed batch handle mismatch",
303            Self::BatchRowConsumed => "typed batch row already consumed",
304            Self::BindingUnavailable(context) => {
305                return write!(
306                    formatter,
307                    "typed binding source unavailable (entity {:?}, field {:?})",
308                    context.entity_source(),
309                    context.field_source(),
310                );
311            }
312            Self::EntityMismatch => "typed binding entity mismatch",
313            Self::FieldUnavailable => "typed binding field unavailable",
314            Self::IncompatibleField => "typed binding field contract is incompatible",
315            Self::RowFieldUnavailable => "typed row field unavailable",
316            Self::RowShapeMismatch => "typed row shape mismatch",
317            Self::StaleBinding => "typed binding is stale",
318            Self::ValueShapeMismatch => "typed row value shape mismatch",
319        })
320    }
321}
322
323impl StdError for TypedAdapterError {}
324
325impl From<icydb_model::TypedValueError> for TypedAdapterError {
326    fn from(error: icydb_model::TypedValueError) -> Self {
327        match error {
328            icydb_model::TypedValueError::SourceUnavailable => Self::FieldUnavailable,
329            icydb_model::TypedValueError::ShapeMismatch => Self::ValueShapeMismatch,
330        }
331    }
332}
333
334/// Failure while binding, decoding, or executing one typed operation.
335#[derive(Debug)]
336pub enum TypedOperationError {
337    /// Generated shape, binding, or returned-row validation failed.
338    Adapter(TypedAdapterError),
339    /// The accepted database operation rejected or failed.
340    Database(Error),
341}
342
343impl fmt::Display for TypedOperationError {
344    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345        match self {
346            Self::Adapter(error) => error.fmt(formatter),
347            Self::Database(error) => error.fmt(formatter),
348        }
349    }
350}
351
352impl StdError for TypedOperationError {}
353
354impl From<Error> for TypedOperationError {
355    fn from(error: Error) -> Self {
356        Self::Database(error)
357    }
358}
359
360impl From<TypedAdapterError> for TypedOperationError {
361    fn from(error: TypedAdapterError) -> Self {
362        Self::Adapter(error)
363    }
364}
365
366/// Opaque accepted-schema binding for one automatic generated adapter.
367#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct TypedEntityBinding {
369    // Projection rows and generated adapters share immutable accepted mappings.
370    // Arc preserves Send/Sync and value equality for independently issued
371    // bindings while making clone-derived equality checks constant-time.
372    inner: Arc<core::db::DynamicTypedEntityBinding>,
373}
374
375impl TypedEntityBinding {
376    fn new(inner: core::db::DynamicTypedEntityBinding) -> Self {
377        Self {
378            inner: Arc::new(inner),
379        }
380    }
381
382    pub(crate) fn inner(&self) -> &core::db::DynamicTypedEntityBinding {
383        &self.inner
384    }
385
386    pub(crate) fn entity(&self) -> &str {
387        self.inner.entity()
388    }
389
390    /// Take one bound field, leaving NULL in the consumed row's scratch slot.
391    ///
392    /// Generated and handwritten [`TypedRowAdapter`] decoders use the field's
393    /// source key, rather than its current accepted display name. Request each
394    /// field once and discard the row afterward: taking it again returns NULL.
395    /// Binding and field checks run before changing the slot, so a rejected
396    /// extraction leaves the row unchanged.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`TypedAdapterError::StaleBinding`] if the row has a different
401    /// binding, or [`TypedAdapterError::EntityMismatch`] if it names a different
402    /// entity. Returns [`TypedAdapterError::FieldUnavailable`] if the source key
403    /// is not bound, [`TypedAdapterError::RowFieldUnavailable`] if the field is
404    /// outside the row's projection, or [`TypedAdapterError::RowShapeMismatch`]
405    /// if the projected slot is missing from the row.
406    pub fn take_row_value(
407        &self,
408        field_source_key: &str,
409        row: &mut OutputRow,
410    ) -> Result<OutputValue, TypedAdapterError> {
411        if row.projection.0.binding != *self {
412            return Err(TypedAdapterError::StaleBinding);
413        }
414        if row.projection.0.entity != self.inner.entity() {
415            return Err(TypedAdapterError::EntityMismatch);
416        }
417        let slot = self
418            .inner
419            .field_slot(field_source_key)
420            .ok_or(TypedAdapterError::FieldUnavailable)?;
421        let index = row
422            .projection
423            .0
424            .accepted_slots
425            .iter()
426            .find_map(|(bound_slot, index)| (*bound_slot == slot).then_some(*index))
427            .ok_or(TypedAdapterError::RowFieldUnavailable)?;
428        let value = row
429            .values
430            .get_mut(index)
431            .ok_or(TypedAdapterError::RowShapeMismatch)?;
432        Ok(std::mem::replace(value, OutputValue::null()))
433    }
434
435    /// Resolve one generated named-type source key through accepted authority.
436    #[doc(hidden)]
437    #[must_use]
438    pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
439        self.inner.named_type_name(source_key)
440    }
441
442    /// Resolve one generated enum-variant source key through accepted authority.
443    #[doc(hidden)]
444    #[must_use]
445    pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
446        self.inner.enum_variant_name(type_source_key, source_key)
447    }
448
449    /// Resolve one generated record-member source key through accepted authority.
450    #[doc(hidden)]
451    #[must_use]
452    pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
453        self.inner.composite_field_name(type_source_key, source_key)
454    }
455
456    /// Resolve and validate one exact source-bound record output projection.
457    #[doc(hidden)]
458    pub fn record_output_values(
459        &self,
460        type_source_key: &str,
461        member_source_keys: &[&str],
462        value: PublicValue,
463    ) -> Result<Vec<PublicValue>, TypedAdapterError> {
464        let mut accepted_names = Vec::with_capacity(member_source_keys.len());
465        for source_key in member_source_keys {
466            let name = self
467                .composite_field_name(type_source_key, source_key)
468                .ok_or(TypedAdapterError::FieldUnavailable)?;
469            accepted_names.push(name);
470        }
471
472        exact_record_output_values(accepted_names.as_slice(), value)
473    }
474}
475
476impl icydb_model::TypedAdapterContext for TypedEntityBinding {
477    type PublicValue = PublicValue;
478
479    fn input_scalar(&self, value: icydb_model::TypedScalarValue) -> Self::PublicValue {
480        match value {
481            icydb_model::TypedScalarValue::Account(value) => PublicValue::Account(value),
482            icydb_model::TypedScalarValue::Blob(value) => PublicValue::Blob(value.into_bytes()),
483            icydb_model::TypedScalarValue::Bool(value) => PublicValue::Bool(value),
484            icydb_model::TypedScalarValue::Date(value) => PublicValue::Date(value),
485            icydb_model::TypedScalarValue::Decimal(value) => PublicValue::Decimal(value),
486            icydb_model::TypedScalarValue::Duration(value) => PublicValue::Duration(value),
487            icydb_model::TypedScalarValue::Float32(value) => PublicValue::Float32(value),
488            icydb_model::TypedScalarValue::Float64(value) => PublicValue::Float64(value),
489            icydb_model::TypedScalarValue::Int64(value) => PublicValue::Int64(value),
490            icydb_model::TypedScalarValue::Int128(value) => PublicValue::Int128(value),
491            icydb_model::TypedScalarValue::Nat64(value) => PublicValue::Nat64(value),
492            icydb_model::TypedScalarValue::Nat128(value) => PublicValue::Nat128(value),
493            icydb_model::TypedScalarValue::Principal(value) => PublicValue::Principal(value),
494            icydb_model::TypedScalarValue::Subaccount(value) => PublicValue::Subaccount(value),
495            icydb_model::TypedScalarValue::Text(value) => PublicValue::Text(value),
496            icydb_model::TypedScalarValue::Timestamp(value) => PublicValue::Timestamp(value),
497            icydb_model::TypedScalarValue::Ulid(value) => PublicValue::Ulid(value),
498            icydb_model::TypedScalarValue::Unit => PublicValue::Unit,
499            icydb_model::TypedScalarValue::U256(value) => PublicValue::U256(value),
500        }
501    }
502
503    fn input_list(&self, values: Vec<Self::PublicValue>) -> Self::PublicValue {
504        PublicValue::List(values)
505    }
506
507    fn input_map(&self, entries: Vec<(Self::PublicValue, Self::PublicValue)>) -> Self::PublicValue {
508        PublicValue::Map(entries)
509    }
510
511    fn input_null(&self) -> Self::PublicValue {
512        PublicValue::Null
513    }
514
515    fn input_enum(
516        &self,
517        type_source_key: &'static str,
518        variant_source_key: &'static str,
519        payload: Option<Self::PublicValue>,
520    ) -> Result<Self::PublicValue, icydb_model::TypedValueError> {
521        let type_name = self
522            .named_type_name(type_source_key)
523            .ok_or(icydb_model::TypedValueError::SourceUnavailable)?;
524        let variant_name = self
525            .enum_variant_name(type_source_key, variant_source_key)
526            .ok_or(icydb_model::TypedValueError::SourceUnavailable)?;
527        let value = PublicEnumValue::new(variant_name, Some(type_name));
528        Ok(PublicValue::Enum(match payload {
529            Some(payload) => value.with_payload(payload),
530            None => value,
531        }))
532    }
533
534    fn input_record(
535        &self,
536        type_source_key: &'static str,
537        fields: Vec<(&'static str, Self::PublicValue)>,
538    ) -> Result<Self::PublicValue, icydb_model::TypedValueError> {
539        fields
540            .into_iter()
541            .map(|(source_key, value)| {
542                let name = self
543                    .composite_field_name(type_source_key, source_key)
544                    .ok_or(icydb_model::TypedValueError::SourceUnavailable)?;
545                Ok((PublicValue::Text(name.to_string()), value))
546            })
547            .collect::<Result<Vec<_>, _>>()
548            .map(PublicValue::Map)
549    }
550
551    fn output_scalar(&self, value: Self::PublicValue) -> Option<icydb_model::TypedScalarValue> {
552        Some(match value {
553            PublicValue::Account(value) => icydb_model::TypedScalarValue::Account(value),
554            PublicValue::Blob(value) => {
555                icydb_model::TypedScalarValue::Blob(icydb_schema::Blob::from(value))
556            }
557            PublicValue::Bool(value) => icydb_model::TypedScalarValue::Bool(value),
558            PublicValue::Date(value) => icydb_model::TypedScalarValue::Date(value),
559            PublicValue::Decimal(value) => icydb_model::TypedScalarValue::Decimal(value),
560            PublicValue::Duration(value) => icydb_model::TypedScalarValue::Duration(value),
561            PublicValue::Float32(value) => icydb_model::TypedScalarValue::Float32(value),
562            PublicValue::Float64(value) => icydb_model::TypedScalarValue::Float64(value),
563            PublicValue::Int64(value) => icydb_model::TypedScalarValue::Int64(value),
564            PublicValue::Int128(value) => icydb_model::TypedScalarValue::Int128(value),
565            PublicValue::Nat64(value) => icydb_model::TypedScalarValue::Nat64(value),
566            PublicValue::Nat128(value) => icydb_model::TypedScalarValue::Nat128(value),
567            PublicValue::Principal(value) => icydb_model::TypedScalarValue::Principal(value),
568            PublicValue::Subaccount(value) => icydb_model::TypedScalarValue::Subaccount(value),
569            PublicValue::Text(value) => icydb_model::TypedScalarValue::Text(value),
570            PublicValue::Timestamp(value) => icydb_model::TypedScalarValue::Timestamp(value),
571            PublicValue::Ulid(value) => icydb_model::TypedScalarValue::Ulid(value),
572            PublicValue::Unit => icydb_model::TypedScalarValue::Unit,
573            PublicValue::U256(value) => icydb_model::TypedScalarValue::U256(value),
574            PublicValue::Enum(_)
575            | PublicValue::List(_)
576            | PublicValue::Map(_)
577            | PublicValue::Null => {
578                return None;
579            }
580        })
581    }
582
583    fn output_list(&self, value: Self::PublicValue) -> Option<Vec<Self::PublicValue>> {
584        match value {
585            PublicValue::List(values) => Some(values),
586            _ => None,
587        }
588    }
589
590    fn output_map(
591        &self,
592        value: Self::PublicValue,
593    ) -> Option<Vec<(Self::PublicValue, Self::PublicValue)>> {
594        match value {
595            PublicValue::Map(entries) => Some(entries),
596            _ => None,
597        }
598    }
599
600    fn output_is_null(&self, value: &Self::PublicValue) -> bool {
601        matches!(value, PublicValue::Null)
602    }
603
604    fn output_enum(
605        &self,
606        descriptor: &'static icydb_model::TypedEnumDescriptor,
607        value: Self::PublicValue,
608    ) -> Result<icydb_model::TypedEnumSelection<Self::PublicValue>, icydb_model::TypedValueError>
609    {
610        let PublicValue::Enum(value) = value else {
611            return Err(icydb_model::TypedValueError::ShapeMismatch);
612        };
613        let type_name = self
614            .named_type_name(descriptor.type_source_key)
615            .ok_or(icydb_model::TypedValueError::SourceUnavailable)?;
616        if value.path() != Some(type_name) {
617            return Err(icydb_model::TypedValueError::ShapeMismatch);
618        }
619        let variant_source_key = self
620            .inner
621            .enum_variant_source_key(descriptor.type_source_key, value.variant())
622            .ok_or(icydb_model::TypedValueError::ShapeMismatch)?;
623        let ordinal = descriptor
624            .variants
625            .iter()
626            .position(|source_key| *source_key == variant_source_key)
627            .ok_or(icydb_model::TypedValueError::ShapeMismatch)?;
628        Ok(icydb_model::TypedEnumSelection {
629            ordinal,
630            payload: value.into_parts().2,
631        })
632    }
633
634    fn output_record(
635        &self,
636        type_source_key: &'static str,
637        member_source_keys: &[&'static str],
638        value: Self::PublicValue,
639    ) -> Result<Vec<Self::PublicValue>, icydb_model::TypedValueError> {
640        self.record_output_values(type_source_key, member_source_keys, value)
641            .map_err(|error| match error {
642                TypedAdapterError::FieldUnavailable => {
643                    icydb_model::TypedValueError::SourceUnavailable
644                }
645                _ => icydb_model::TypedValueError::ShapeMismatch,
646            })
647    }
648}
649
650fn exact_record_output_values(
651    accepted_names: &[&str],
652    value: PublicValue,
653) -> Result<Vec<PublicValue>, TypedAdapterError> {
654    let PublicValue::Map(mut entries) = value else {
655        return Err(TypedAdapterError::ValueShapeMismatch);
656    };
657    if entries.len() != accepted_names.len() {
658        return Err(TypedAdapterError::ValueShapeMismatch);
659    }
660
661    // Equal cardinality and unique accepted names make this an exact permutation:
662    // a duplicate, unknown or non-text key necessarily leaves a required name absent.
663    // Finish validation before exposing any member to the generated decoder.
664    for (index, accepted) in accepted_names.iter().enumerate() {
665        if accepted_names[..index].contains(accepted) {
666            return Err(TypedAdapterError::ValueShapeMismatch);
667        }
668        let offset = entries[index..]
669            .iter()
670            .position(|(key, _)| matches!(key, PublicValue::Text(name) if name == accepted))
671            .ok_or(TypedAdapterError::ValueShapeMismatch)?;
672        entries.swap(index, index + offset);
673    }
674
675    Ok(entries.into_iter().map(|(_, value)| value).collect())
676}
677
678#[cfg(test)]
679mod typed_record_output_tests {
680    use super::{TypedAdapterError, exact_record_output_values};
681    use crate::value::PublicValue;
682
683    #[test]
684    fn typed_bindings_and_output_rows_remain_send_and_sync() {
685        fn assert_send_sync<T: Send + Sync>() {}
686        assert_send_sync::<super::TypedEntityBinding>();
687        assert_send_sync::<super::OutputRow>();
688    }
689
690    fn text(value: &str) -> PublicValue {
691        PublicValue::Text(value.to_string())
692    }
693
694    #[test]
695    fn exact_record_output_reorders_accepted_members() {
696        let value = PublicValue::Map(vec![
697            (text("count"), PublicValue::Nat64(7)),
698            (text("label"), text("Ada")),
699        ]);
700
701        let values = exact_record_output_values(&["label", "count"], value)
702            .expect("exact accepted record output should decode");
703
704        assert!(matches!(&values[0], PublicValue::Text(value) if value == "Ada"));
705        assert_eq!(values[1], PublicValue::Nat64(7));
706    }
707
708    #[test]
709    fn exact_record_output_rejects_duplicate_missing_and_unknown_members() {
710        let malformed = [
711            PublicValue::Map(vec![
712                (text("label"), text("Ada")),
713                (text("label"), text("Grace")),
714            ]),
715            PublicValue::Map(vec![(text("label"), text("Ada"))]),
716            PublicValue::Map(vec![
717                (text("label"), text("Ada")),
718                (text("other"), PublicValue::Nat64(7)),
719            ]),
720            PublicValue::Map(vec![
721                (PublicValue::Nat64(1), text("Ada")),
722                (text("count"), PublicValue::Nat64(7)),
723            ]),
724            PublicValue::List(Vec::new()),
725        ];
726
727        for value in malformed.clone() {
728            assert_eq!(
729                exact_record_output_values(&["label", "count"], value),
730                Err(TypedAdapterError::ValueShapeMismatch),
731            );
732        }
733        assert_eq!(
734            exact_record_output_values(&["label", "label"], malformed[0].clone()),
735            Err(TypedAdapterError::ValueShapeMismatch),
736        );
737    }
738
739    #[test]
740    fn exact_record_output_preserves_every_member_permutation_and_null() {
741        let members = [
742            (text("label"), text("Ada")),
743            (text("count"), PublicValue::Nat64(7)),
744            (text("note"), PublicValue::Null),
745        ];
746        for order in [
747            [0, 1, 2],
748            [0, 2, 1],
749            [1, 0, 2],
750            [1, 2, 0],
751            [2, 0, 1],
752            [2, 1, 0],
753        ] {
754            let record = PublicValue::Map(
755                order
756                    .into_iter()
757                    .map(|index| members[index].clone())
758                    .collect(),
759            );
760            assert_eq!(
761                exact_record_output_values(&["label", "count", "note"], record),
762                Ok(vec![text("Ada"), PublicValue::Nat64(7), PublicValue::Null]),
763            );
764        }
765        assert_eq!(
766            exact_record_output_values(&[], PublicValue::Map(Vec::new())),
767            Ok(Vec::new())
768        );
769    }
770
771    #[test]
772    fn typed_input_failures_retain_source_and_shape_classes() {
773        assert_eq!(
774            TypedAdapterError::from(icydb_model::TypedValueError::SourceUnavailable),
775            TypedAdapterError::FieldUnavailable,
776        );
777        assert_eq!(
778            TypedAdapterError::from(icydb_model::TypedValueError::ShapeMismatch),
779            TypedAdapterError::ValueShapeMismatch,
780        );
781    }
782}
783
784/// IcyDB-owned decode adapter implemented by runtime-enabled generated code.
785pub trait TypedRowAdapter {
786    /// Complete application row produced by decoding.
787    type Row;
788
789    /// Decode one accepted output row through an opaque current binding.
790    fn decode_row(
791        binding: &TypedEntityBinding,
792        row: OutputRow,
793    ) -> Result<Self::Row, TypedAdapterError>;
794}
795
796/// IcyDB-owned binding adapter implemented by runtime-enabled generated entities.
797pub trait TypedEntityAdapter: TypedRowAdapter {
798    /// Static generated source contract validated before an opaque binding is issued.
799    #[doc(hidden)]
800    const DESCRIPTOR: &'static TypedEntityDescriptor;
801
802    /// Bind generated source identities to current accepted schema authority.
803    fn typed_binding<C>(session: &DbSession<C>) -> Result<TypedEntityBinding, TypedOperationError>
804    where
805        C: CanisterKind,
806    {
807        session.bind_typed_entity(Self::DESCRIPTOR)
808    }
809}
810
811/// IcyDB-owned write adapter implemented by runtime-enabled generated inputs.
812pub trait TypedWriteAdapter {
813    /// Generated entity that owns this operation-specific write input.
814    type Entity: TypedEntityAdapter;
815
816    /// Lower explicit application write intent without resolving database policy.
817    fn encode_write(self, binding: &TypedEntityBinding) -> Result<TypedWrite, TypedAdapterError>;
818}
819
820/// Concrete generated-field encoder over one opaque accepted binding.
821#[doc(hidden)]
822pub struct BoundWriteEncoder {
823    binding: TypedEntityBinding,
824    fields: Vec<(usize, core::db::DynamicWriteCell)>,
825}
826
827impl BoundWriteEncoder {
828    /// Prepare one generated write with its exact authored-field capacity.
829    #[must_use]
830    pub fn new(binding: &TypedEntityBinding, field_count: usize) -> Self {
831        Self {
832            binding: binding.clone(),
833            fields: Vec::with_capacity(field_count),
834        }
835    }
836
837    /// Append one generated write cell by binding-local descriptor ordinal.
838    pub fn push(&mut self, descriptor_ordinal: usize, cell: WriteCell<InputValue>) {
839        self.fields.push((descriptor_ordinal, cell.into_core()));
840    }
841
842    /// Finish one insert intent through the opaque accepted binding.
843    pub fn insert(self) -> Result<TypedWrite, TypedAdapterError> {
844        let (binding, patch) = self.into_bound_patch()?;
845        Ok(TypedWrite {
846            binding,
847            mutation: core::db::DynamicTypedMutation::Insert { patch },
848        })
849    }
850
851    /// Finish one patch/update intent through the opaque accepted binding.
852    pub fn update(self, key: InputValue) -> Result<TypedWrite, TypedAdapterError> {
853        let (binding, patch) = self.into_bound_patch()?;
854        Ok(TypedWrite {
855            binding,
856            mutation: core::db::DynamicTypedMutation::Update { key, patch },
857        })
858    }
859
860    /// Finish one replacement intent through the opaque accepted binding.
861    pub fn replace(self, key: InputValue) -> Result<TypedWrite, TypedAdapterError> {
862        let (binding, patch) = self.into_bound_patch()?;
863        Ok(TypedWrite {
864            binding,
865            mutation: core::db::DynamicTypedMutation::Replace { key, patch },
866        })
867    }
868
869    fn into_bound_patch(
870        self,
871    ) -> Result<(TypedEntityBinding, core::db::DynamicTypedStructuralPatch), TypedAdapterError>
872    {
873        let Self { binding, fields } = self;
874        let patch = binding
875            .inner
876            .bind_write_ordinals(fields)
877            .ok_or(TypedAdapterError::FieldUnavailable)?;
878        Ok((binding, patch))
879    }
880}
881
882/// One generated write lowered through binding-owned accepted field identities.
883#[derive(Clone, Debug)]
884pub struct TypedWrite {
885    binding: TypedEntityBinding,
886    mutation: core::db::DynamicTypedMutation,
887}
888
889impl TypedWrite {
890    /// Build one delete intent from an accepted primary-key value.
891    #[must_use]
892    pub fn delete(binding: &TypedEntityBinding, key: InputValue) -> Self {
893        Self {
894            binding: binding.clone(),
895            mutation: core::db::DynamicTypedMutation::Delete { key },
896        }
897    }
898}
899
900/// Sealed reference to one item accepted by a typed write-batch builder.
901///
902/// Handles are process-local facade values. They are not serializable,
903/// durable, authoritative, or reusable with another builder result.
904pub struct TypedWriteHandle<E> {
905    owner: Arc<()>,
906    position: usize,
907    entity: PhantomData<fn() -> E>,
908}
909
910impl<E> Clone for TypedWriteHandle<E> {
911    fn clone(&self) -> Self {
912        Self {
913            owner: self.owner.clone(),
914            position: self.position,
915            entity: PhantomData,
916        }
917    }
918}
919
920/// One ephemeral mixed-entity typed batch over the canonical write executor.
921pub struct TrustedTypedWriteBatch<'session, C: CanisterKind> {
922    session: &'session DbSession<C>,
923    owner: Arc<()>,
924    writes: Vec<TypedWrite>,
925    bindings: Vec<TypedEntityBinding>,
926}
927
928impl<'session, C: CanisterKind> TrustedTypedWriteBatch<'session, C> {
929    fn new(session: &'session DbSession<C>) -> Self {
930        Self {
931            session,
932            owner: Arc::new(()),
933            writes: Vec::new(),
934            bindings: Vec::new(),
935        }
936    }
937
938    /// Resolve and encode one generated write under its generated entity.
939    pub fn push<W>(&mut self, input: W) -> Result<TypedWriteHandle<W::Entity>, TypedOperationError>
940    where
941        W: TypedWriteAdapter,
942    {
943        let binding = W::Entity::typed_binding(self.session)?;
944        let write = input.encode_write(&binding)?;
945        if write.binding != binding {
946            return Err(TypedAdapterError::EntityMismatch.into());
947        }
948        let position = self.writes.len();
949        self.writes.push(write);
950        self.bindings.push(binding);
951        Ok(TypedWriteHandle {
952            owner: self.owner.clone(),
953            position,
954            entity: PhantomData,
955        })
956    }
957
958    /// Consume this builder and execute its writes in one canonical batch.
959    pub fn execute(self) -> Result<TypedWriteBatchResults, TypedOperationError> {
960        let results = self
961            .session
962            .execute_trusted_typed_write_batch(self.writes)?;
963        let results = project_typed_write_batch_results(self.bindings, results)?;
964        Ok(TypedWriteBatchResults {
965            owner: self.owner,
966            results,
967        })
968    }
969}
970
971/// One projected result retained by a mixed-entity typed batch.
972pub struct TypedWriteBatchResult {
973    affected_rows: u32,
974    projection: OutputRowProjection,
975    row: Option<OutputRow>,
976}
977
978impl TypedWriteBatchResult {
979    /// Return the accepted entity display name for this result.
980    #[must_use]
981    pub fn entity(&self) -> &str {
982        self.projection.entity()
983    }
984
985    /// Return the number of rows affected by this write.
986    #[must_use]
987    pub const fn affected_rows(&self) -> u32 {
988        self.affected_rows
989    }
990
991    fn take_row(&mut self) -> Result<OutputRow, TypedAdapterError> {
992        self.row.take().ok_or(TypedAdapterError::BatchRowConsumed)
993    }
994}
995
996/// Ordered projected results from one mixed-entity typed batch.
997pub struct TypedWriteBatchResults {
998    owner: Arc<()>,
999    results: Vec<TypedWriteBatchResult>,
1000}
1001
1002impl TypedWriteBatchResults {
1003    fn handle_position<E>(&self, handle: &TypedWriteHandle<E>) -> Result<usize, TypedAdapterError> {
1004        if !Arc::ptr_eq(&self.owner, &handle.owner) || handle.position >= self.results.len() {
1005            return Err(TypedAdapterError::BatchHandleMismatch);
1006        }
1007        Ok(handle.position)
1008    }
1009
1010    /// Borrow the projected result selected by a builder-issued handle.
1011    pub fn result<E>(
1012        &self,
1013        handle: &TypedWriteHandle<E>,
1014    ) -> Result<&TypedWriteBatchResult, TypedAdapterError> {
1015        let position = self.handle_position(handle)?;
1016        self.results
1017            .get(position)
1018            .ok_or(TypedAdapterError::BatchHandleMismatch)
1019    }
1020
1021    /// Consume and decode the selected result's row through its retained binding.
1022    ///
1023    /// Each builder handle owns exactly one row decode. A second decode attempt
1024    /// returns [`TypedAdapterError::BatchRowConsumed`].
1025    pub fn row<E>(&mut self, handle: &TypedWriteHandle<E>) -> Result<E::Row, TypedOperationError>
1026    where
1027        E: TypedEntityAdapter,
1028    {
1029        let position = self.handle_position(handle)?;
1030        let result = self
1031            .results
1032            .get_mut(position)
1033            .ok_or(TypedAdapterError::BatchHandleMismatch)?;
1034        let row = result.take_row()?;
1035        Ok(E::decode_row(result.projection.binding(), row)?)
1036    }
1037}
1038
1039fn project_typed_write_batch_results(
1040    bindings: Vec<TypedEntityBinding>,
1041    results: Vec<DynamicMutationResult>,
1042) -> Result<Vec<TypedWriteBatchResult>, TypedAdapterError> {
1043    if bindings.len() != results.len() {
1044        return Err(TypedAdapterError::RowShapeMismatch);
1045    }
1046    let mut projected = Vec::with_capacity(results.len());
1047    for (binding, result) in bindings.into_iter().zip(results) {
1048        let result = mutation_result_row_parts(result)?;
1049        let projection =
1050            OutputRowProjection::new(&binding, result.entity, result.columns.as_slice())?;
1051        let row = projection.project(result.values)?;
1052        projected.push(TypedWriteBatchResult {
1053            affected_rows: result.affected_rows,
1054            projection,
1055            row: Some(row),
1056        });
1057    }
1058    Ok(projected)
1059}
1060
1061impl WriteCell<InputValue> {
1062    fn into_core(self) -> core::db::DynamicWriteCell {
1063        match self {
1064            Self::Omitted => core::db::DynamicWriteCell::Omitted,
1065            Self::Default => core::db::DynamicWriteCell::Default,
1066            Self::Null => core::db::DynamicWriteCell::Null,
1067            Self::Value(value) => core::db::DynamicWriteCell::Value(value),
1068        }
1069    }
1070}
1071
1072///
1073/// StructuralPatch
1074///
1075/// Public field-name-driven structural mutation patch.
1076/// Names are resolved only when the request is admitted against accepted
1077/// schema; the patch cannot carry physical row slots or generated field order.
1078///
1079
1080#[derive(Clone, Debug, Default, Eq, PartialEq)]
1081pub struct StructuralPatch {
1082    fields: Vec<(String, WriteCell<InputValue>)>,
1083}
1084
1085impl StructuralPatch {
1086    /// Build one empty structural patch.
1087    #[must_use]
1088    pub const fn new() -> Self {
1089        Self { fields: Vec::new() }
1090    }
1091
1092    /// Append one named field intent.
1093    #[must_use]
1094    pub fn field(mut self, name: impl Into<String>, value: WriteCell<InputValue>) -> Self {
1095        self.fields.push((name.into(), value));
1096        self
1097    }
1098
1099    fn into_core(self) -> core::db::DynamicStructuralPatch {
1100        core::db::DynamicStructuralPatch::new(
1101            self.fields
1102                .into_iter()
1103                .map(|(name, value)| (name, value.into_core()))
1104                .collect(),
1105        )
1106    }
1107}
1108
1109///
1110/// StructuralMutation
1111///
1112/// Entity-name-driven dynamic mutation request.
1113/// Each variant owns its key requirement and row-existence semantics.
1114///
1115
1116#[derive(Clone, Debug, Eq, PartialEq)]
1117pub enum StructuralMutation {
1118    /// Insert one row and derive its identity from the accepted after-image.
1119    Insert {
1120        /// Accepted entity display name.
1121        entity: String,
1122        /// Authored insert fields.
1123        patch: StructuralPatch,
1124    },
1125    /// Patch one existing row.
1126    Update {
1127        /// Accepted entity display name.
1128        entity: String,
1129        /// Public scalar or composite primary key.
1130        key: InputValue,
1131        /// Authored update fields.
1132        patch: StructuralPatch,
1133    },
1134    /// Replace one row, inserting it when absent.
1135    /// A missing row requires caller-owned primary-key fields; generated or
1136    /// managed keys can only be preserved from an existing row.
1137    Replace {
1138        /// Accepted entity display name.
1139        entity: String,
1140        /// Public scalar or composite primary key.
1141        key: InputValue,
1142        /// Authored replacement fields.
1143        patch: StructuralPatch,
1144    },
1145    /// Delete one existing row.
1146    Delete {
1147        /// Accepted entity display name.
1148        entity: String,
1149        /// Public scalar or composite primary key.
1150        key: InputValue,
1151    },
1152}
1153
1154impl StructuralMutation {
1155    fn entity(&self) -> &str {
1156        match self {
1157            Self::Insert { entity, .. }
1158            | Self::Update { entity, .. }
1159            | Self::Replace { entity, .. }
1160            | Self::Delete { entity, .. } => entity,
1161        }
1162    }
1163
1164    fn into_core(self) -> core::db::DynamicMutation {
1165        match self {
1166            Self::Insert { entity, patch } => core::db::DynamicMutation::Insert {
1167                entity,
1168                patch: patch.into_core(),
1169            },
1170            Self::Update { entity, key, patch } => core::db::DynamicMutation::Update {
1171                entity,
1172                key,
1173                patch: patch.into_core(),
1174            },
1175            Self::Replace { entity, key, patch } => core::db::DynamicMutation::Replace {
1176                entity,
1177                key,
1178                patch: patch.into_core(),
1179            },
1180            Self::Delete { entity, key } => core::db::DynamicMutation::Delete { entity, key },
1181        }
1182    }
1183}
1184
1185impl<C: CanisterKind> DbSession<C> {
1186    #[inline(never)]
1187    fn ensure_typed_binding_is_current(
1188        &self,
1189        binding: &TypedEntityBinding,
1190    ) -> Result<(), TypedOperationError> {
1191        let current = self
1192            .inner
1193            .typed_entity_binding_is_current(&binding.inner)
1194            .map_err(|error| TypedOperationError::Database(Error::from(error)))?;
1195        if !current {
1196            return Err(TypedAdapterError::StaleBinding.into());
1197        }
1198        Ok(())
1199    }
1200
1201    /// Execute one trusted structural mutation through accepted schema only.
1202    ///
1203    /// This dynamic lane never materializes a generated entity and never runs
1204    /// application validators or normalizers. The caller must enforce any
1205    /// required authorization before dispatch.
1206    pub fn execute_trusted_structural_mutation(
1207        &self,
1208        request: StructuralMutation,
1209    ) -> Result<DynamicMutationResult, Error> {
1210        Ok(self
1211            .inner
1212            .execute_trusted_dynamic_mutation(&request.into_core())?)
1213    }
1214
1215    /// Execute one bounded same-store structural mutation batch atomically.
1216    ///
1217    /// Inserts, updates, replacements, and deletes share one accepted snapshot,
1218    /// operation timestamp, final-row overlay, commit marker, and recovery
1219    /// outcome. Results retain input order, with one single-row result per
1220    /// request. The batch admits at most 4,096 operations across 64 accepted
1221    /// entities, 16 MiB of staged canonical keys/rows, and a 1 MiB encoded
1222    /// result. Every entity must belong to the same store and each operation
1223    /// must target a distinct entity-qualified primary key.
1224    pub fn execute_trusted_structural_mutation_batch(
1225        &self,
1226        mutations: Vec<StructuralMutation>,
1227    ) -> Result<Vec<DynamicMutationResult>, Error> {
1228        let mutations = mutations
1229            .into_iter()
1230            .map(StructuralMutation::into_core)
1231            .collect();
1232        Ok(self
1233            .inner
1234            .execute_trusted_dynamic_mutation_batch(mutations)?)
1235    }
1236
1237    /// Execute and project one same-entity structural mutation batch.
1238    ///
1239    /// The binding is checked before execution, every mutation must target its
1240    /// accepted entity, and every result must contain exactly one row. This
1241    /// concrete terminal keeps mutation execution, cardinality validation and
1242    /// accepted-field projection outside generated entity monomorphs; callers
1243    /// retain only their final [`TypedRowAdapter::decode_row`] step.
1244    #[inline(never)]
1245    pub fn execute_trusted_structural_mutation_batch_rows(
1246        &self,
1247        binding: &TypedEntityBinding,
1248        mutations: Vec<StructuralMutation>,
1249    ) -> Result<Vec<OutputRow>, TypedOperationError> {
1250        self.ensure_typed_binding_is_current(binding)?;
1251        if mutations
1252            .iter()
1253            .any(|mutation| mutation.entity() != binding.entity())
1254        {
1255            return Err(TypedAdapterError::EntityMismatch.into());
1256        }
1257
1258        let expected_results = mutations.len();
1259        let mutations = mutations
1260            .into_iter()
1261            .map(StructuralMutation::into_core)
1262            .collect();
1263        let results = self
1264            .inner
1265            .execute_trusted_dynamic_mutation_batch(mutations)
1266            .map_err(|error| TypedOperationError::Database(Error::from(error)))?;
1267        Ok(project_mutation_result_batch(
1268            binding,
1269            results,
1270            expected_results,
1271        )?)
1272    }
1273
1274    /// Execute one same-entity structural insert batch atomically.
1275    ///
1276    /// All patches bind to one accepted snapshot and share one database-owned
1277    /// operation timestamp. The batch either commits completely or returns a
1278    /// typed error without publishing a partial prefix.
1279    pub fn execute_trusted_structural_insert_batch(
1280        &self,
1281        entity: &str,
1282        patches: Vec<StructuralPatch>,
1283    ) -> Result<DynamicMutationResult, Error> {
1284        let patches = patches
1285            .into_iter()
1286            .map(StructuralPatch::into_core)
1287            .collect();
1288        Ok(self
1289            .inner
1290            .execute_trusted_dynamic_insert_batch(entity, patches)?)
1291    }
1292
1293    /// Prepare one owned dynamic row batch for non-entity-generic decoding.
1294    #[doc(hidden)]
1295    pub fn prepare_typed_output_rows(
1296        &self,
1297        binding: &TypedEntityBinding,
1298        entity: String,
1299        columns: Vec<String>,
1300        rows: Vec<Vec<OutputValue>>,
1301    ) -> Result<PreparedOutputRows, TypedOperationError> {
1302        self.ensure_typed_binding_is_current(binding)?;
1303        PreparedOutputRows::new(binding, entity, columns, rows)
1304            .map_err(TypedOperationError::Adapter)
1305    }
1306
1307    /// Issue one opaque current accepted binding for generated field contracts.
1308    #[doc(hidden)]
1309    pub fn bind_typed_entity(
1310        &self,
1311        descriptor: &TypedEntityDescriptor,
1312    ) -> Result<TypedEntityBinding, TypedOperationError> {
1313        self.inner
1314            .issue_typed_entity_binding(descriptor)
1315            .map(TypedEntityBinding::new)
1316            .map_err(|error| match error {
1317                core::db::DynamicTypedBindingError::SourceUnavailable(context) => {
1318                    TypedOperationError::Adapter(TypedAdapterError::BindingUnavailable(context))
1319                }
1320                core::db::DynamicTypedBindingError::IncompatibleField => {
1321                    TypedOperationError::Adapter(TypedAdapterError::IncompatibleField)
1322                }
1323                core::db::DynamicTypedBindingError::Internal(error) => {
1324                    TypedOperationError::Database(Error::from(error))
1325                }
1326            })
1327    }
1328
1329    /// Encode one generated value as a structural input through an exact
1330    /// current accepted binding.
1331    ///
1332    /// Generated records, enums, and collections reuse their existing
1333    /// [`icydb_model::TypedInputValue`] implementation. Accepted source
1334    /// bindings resolve every named type, record member, and enum variant;
1335    /// the returned [`InputValue`] enters the ordinary structural write
1336    /// admission path.
1337    pub fn bind_typed_input<T>(
1338        &self,
1339        binding: &TypedEntityBinding,
1340        value: T,
1341    ) -> Result<InputValue, TypedOperationError>
1342    where
1343        T: icydb_model::TypedInputValue,
1344    {
1345        self.ensure_typed_binding_is_current(binding)?;
1346        let value = value
1347            .encode_typed_input(binding)
1348            .map_err(TypedAdapterError::from)?;
1349        Ok(InputValue::from_public(value))
1350    }
1351
1352    /// Execute one generated write only while its opaque accepted binding is current.
1353    pub fn execute_trusted_typed_write(
1354        &self,
1355        write: TypedWrite,
1356    ) -> Result<DynamicMutationResult, TypedOperationError> {
1357        self.inner
1358            .execute_trusted_typed_mutation(&write.binding.inner, write.mutation)
1359            .map_err(|error| TypedOperationError::Database(Error::from(error)))?
1360            .ok_or(TypedOperationError::Adapter(
1361                TypedAdapterError::StaleBinding,
1362            ))
1363    }
1364
1365    /// Execute one generated write and project its required single row.
1366    ///
1367    /// `TypedWrite` already owns its accepted binding, so execution, exact
1368    /// single-row validation and accepted-field projection remain in one
1369    /// concrete non-entity terminal. Generated adapters need only decode the
1370    /// returned row into their final Rust entity.
1371    #[inline(never)]
1372    pub fn execute_trusted_typed_write_row(
1373        &self,
1374        write: TypedWrite,
1375    ) -> Result<OutputRow, TypedOperationError> {
1376        let TypedWrite { binding, mutation } = write;
1377        let result = self
1378            .inner
1379            .execute_trusted_typed_mutation(&binding.inner, mutation)
1380            .map_err(|error| TypedOperationError::Database(Error::from(error)))?
1381            .ok_or(TypedOperationError::Adapter(
1382                TypedAdapterError::StaleBinding,
1383            ))?;
1384        Ok(project_single_mutation_result(&binding, result)?)
1385    }
1386
1387    /// Execute one non-empty same-store generated write batch atomically.
1388    ///
1389    /// Each exact current binding is revalidated from one captured accepted
1390    /// root. The canonical structural batch owner enforces the entity,
1391    /// operation, staged-byte, and result-byte limits before publishing any
1392    /// durable effect. Results retain input order with one item per write.
1393    pub fn execute_trusted_typed_write_batch(
1394        &self,
1395        writes: Vec<TypedWrite>,
1396    ) -> Result<Vec<DynamicMutationResult>, TypedOperationError> {
1397        let requests = writes
1398            .into_iter()
1399            .map(|write| (write.binding.inner, write.mutation))
1400            .collect();
1401        self.inner
1402            .execute_trusted_typed_mutation_batch_shared(requests)
1403            .map_err(|error| TypedOperationError::Database(Error::from(error)))?
1404            .ok_or(TypedOperationError::Adapter(
1405                TypedAdapterError::StaleBinding,
1406            ))
1407    }
1408
1409    /// Execute and prepare one non-empty same-entity generated write batch.
1410    ///
1411    /// Every write must carry the supplied exact binding. The existing typed
1412    /// mutation batch remains the atomic commit and recovery owner; this
1413    /// terminal adds only exact result cardinality and one shared accepted-row
1414    /// projection before generated code performs its final typed decode.
1415    #[inline(never)]
1416    pub fn execute_trusted_typed_write_batch_rows(
1417        &self,
1418        binding: &TypedEntityBinding,
1419        writes: Vec<TypedWrite>,
1420    ) -> Result<PreparedOutputRows, TypedOperationError> {
1421        if writes.iter().any(|write| write.binding != *binding) {
1422            return Err(TypedAdapterError::EntityMismatch.into());
1423        }
1424        let expected_results = writes.len();
1425        let requests = writes.into_iter().map(|write| write.mutation).collect();
1426        let result = self
1427            .inner
1428            .execute_trusted_same_entity_typed_mutation_batch(binding.inner(), requests)
1429            .map_err(|error| TypedOperationError::Database(Error::from(error)))?
1430            .ok_or(TypedOperationError::Adapter(
1431                TypedAdapterError::StaleBinding,
1432            ))?;
1433        let DynamicMutationResult {
1434            entity,
1435            columns,
1436            rows,
1437            affected_rows: _,
1438        } = result;
1439        if rows.len() != expected_results {
1440            return Err(TypedAdapterError::RowShapeMismatch.into());
1441        }
1442        Ok(PreparedOutputRows::new(binding, entity, columns, rows)?)
1443    }
1444
1445    /// Start one mixed-entity generated typed-write batch.
1446    ///
1447    /// `push` resolves each generated input's entity binding automatically;
1448    /// `execute` dispatches exactly once through the canonical typed batch.
1449    #[must_use]
1450    pub fn trusted_typed_write_batch(&self) -> TrustedTypedWriteBatch<'_, C> {
1451        TrustedTypedWriteBatch::new(self)
1452    }
1453
1454    /// Build one field-name-driven structural patch.
1455    #[must_use]
1456    pub fn structural_patch<I, S>(&self, fields: I) -> StructuralPatch
1457    where
1458        I: IntoIterator<Item = (S, WriteCell<InputValue>)>,
1459        S: Into<String>,
1460    {
1461        StructuralPatch {
1462            fields: fields
1463                .into_iter()
1464                .map(|(name, value)| (name.into(), value))
1465                .collect(),
1466        }
1467    }
1468}