Skip to main content

icydb_core/db/session/
write.rs

1//! Module: db::session::write
2//! Responsibility: session-owned typed write APIs for insert, replace, update,
3//! and structural mutation entrypoints over the shared save pipeline.
4//! Does not own: commit staging, mutation execution, or persistence encoding.
5//! Boundary: keeps public session write semantics above the executor save surface.
6
7#[cfg(test)]
8mod identity_pre_key_tests;
9#[cfg(test)]
10mod key_handoff_tests;
11#[cfg(test)]
12mod mixed_relation_batch_tests;
13#[cfg(test)]
14mod output_handoff_tests;
15#[cfg(test)]
16mod targeted_rule_mutation_tests;
17#[cfg(test)]
18mod typed_adapter_tests;
19
20#[cfg(test)]
21use crate::db::executor::mutation::structural::{
22    AcceptedStructuralMutationPacking, AcceptedStructuralMutationStagedAdmission,
23    MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES, add_structural_mutation_staged_bytes,
24    admit_structural_mutation_staged_charge, data_key_from_validated_reader,
25    insert_key_exists_after_generation, into_mutation_output_values,
26    structural_mutation_staged_charge,
27};
28use crate::db::executor::{
29    AcceptedSchemaCatalogContext,
30    mutation::structural::{
31        AcceptedStructuralMutation, AcceptedStructuralMutationBatchItem,
32        AcceptedStructuralMutationCommitDirective, AcceptedStructuralMutationCommitOptions,
33        AcceptedStructuralMutationRow, AcceptedStructuralMutationTarget,
34        MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
35        execute_accepted_structural_mutation_batch, mutation_diagnostic_context,
36        validate_structural_mutation_result_bytes,
37    },
38};
39#[cfg(any(feature = "sql", test))]
40use crate::value::Value;
41
42#[cfg(test)]
43use crate::db::{
44    data::{RawRow, StructuralRowContract, StructuralSlotReader},
45    executor::mutation::structural::{
46        MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, checked_pre_key_candidate_count,
47    },
48    integrity::MutationProgressRecordOp,
49    schema::FieldId,
50};
51use crate::{
52    db::{
53        DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
54        DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedMutation,
55        DynamicTypedStructuralPatch, DynamicWriteCell, TypedEntityDescriptor, TypedFieldType,
56        commit::database_incarnation_id,
57        data::{AcceptedMutationIntentPatch, DecodedDataStoreKey, FieldSlot},
58        schema::{
59            AcceptedFieldKind, AcceptedRowLayoutRuntimeContract, AcceptedRowLayoutRuntimeField,
60            lower_field_type, output_value_from_runtime,
61        },
62        write_context::MutationMode,
63    },
64    error::{InternalError, MutationDiagnosticContext},
65    traits::CanisterKind,
66    types::{CurrentTimestamp, Timestamp},
67    value::InputValue,
68};
69use icydb_schema::{
70    EntitySourceKey, FieldSourceKey, FieldType, SchemaContractError, TypeSourceKey,
71};
72
73fn dynamic_key(
74    entity_tag: crate::types::EntityTag,
75    key: InputValue,
76) -> Result<DecodedDataStoreKey, InternalError> {
77    let value = key
78        .try_into_runtime_non_enum()
79        .ok_or_else(InternalError::executor_unsupported)?;
80    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
81}
82
83fn lower_resolved_write_cell(
84    lowered: AcceptedMutationIntentPatch,
85    field: &AcceptedRowLayoutRuntimeField<'_>,
86    cell: DynamicWriteCell,
87    mode: MutationMode,
88    mutation_context: MutationDiagnosticContext,
89) -> Result<AcceptedMutationIntentPatch, InternalError> {
90    // Insert DEFAULT requests select accepted database policy rather than
91    // authoring a value. Keyed writes retain their existing ownership rules.
92    if (matches!(cell, DynamicWriteCell::Null | DynamicWriteCell::Value(_))
93        || (matches!(cell, DynamicWriteCell::Default) && !matches!(mode, MutationMode::Insert)))
94        && (field.write_policy().insert_generation().is_some()
95            || field.write_policy().write_management().is_some())
96    {
97        return Err(InternalError::mutation_database_owned_field_explicit(
98            mutation_context,
99            field.field_id().get(),
100        ));
101    }
102
103    let slot = FieldSlot::from_validated_index(usize::from(field.slot().get()));
104    Ok(match cell {
105        DynamicWriteCell::Omitted => lowered,
106        DynamicWriteCell::Default => match mode {
107            MutationMode::Insert | MutationMode::Replace => {
108                lowered.set_explicit_insert_default(slot)
109            }
110            MutationMode::Update => lowered.set_explicit_update_default(slot),
111        },
112        DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::null()),
113        DynamicWriteCell::Value(value) => lowered.set_authored(slot, value),
114    })
115}
116
117fn lower_dynamic_patch(
118    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
119    patch: DynamicStructuralPatch,
120    mode: MutationMode,
121    mutation_context: MutationDiagnosticContext,
122) -> Result<AcceptedMutationIntentPatch, InternalError> {
123    let mut lowered = AcceptedMutationIntentPatch::new();
124    for (field_name, cell) in patch.into_fields() {
125        let slot = descriptor
126            .field_slot_index_by_name(&field_name)
127            .ok_or_else(InternalError::executor_unsupported)?;
128        let field = descriptor
129            .field_for_slot_index(slot)
130            .ok_or_else(InternalError::executor_invariant)?;
131        lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
132    }
133    Ok(lowered)
134}
135
136fn lower_dynamic_save_intent(
137    catalog: &AcceptedSchemaCatalogContext,
138    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
139    patch: DynamicStructuralPatch,
140    mode: MutationMode,
141    target: AcceptedStructuralMutationTarget,
142    batch_position: u32,
143) -> Result<AcceptedStructuralMutation, InternalError> {
144    Ok(AcceptedStructuralMutation::save(
145        mode,
146        target,
147        lower_dynamic_patch(
148            descriptor,
149            patch,
150            mode,
151            mutation_diagnostic_context(catalog, mode, batch_position),
152        )?,
153    ))
154}
155
156fn lower_dynamic_mutation_intent(
157    catalog: &AcceptedSchemaCatalogContext,
158    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
159    request: DynamicMutation,
160    batch_position: u32,
161) -> Result<AcceptedStructuralMutation, InternalError> {
162    let entity_tag = catalog.identity().entity_tag();
163    match request {
164        DynamicMutation::Insert { patch, .. } => lower_dynamic_save_intent(
165            catalog,
166            descriptor,
167            patch,
168            MutationMode::Insert,
169            AcceptedStructuralMutationTarget::ResolveFromAfterImage,
170            batch_position,
171        ),
172        DynamicMutation::Update { key, patch, .. } => lower_dynamic_save_intent(
173            catalog,
174            descriptor,
175            patch,
176            MutationMode::Update,
177            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
178            batch_position,
179        ),
180        DynamicMutation::Replace { key, patch, .. } => lower_dynamic_save_intent(
181            catalog,
182            descriptor,
183            patch,
184            MutationMode::Replace,
185            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
186            batch_position,
187        ),
188        DynamicMutation::Delete { key, .. } => Ok(AcceptedStructuralMutation::delete(dynamic_key(
189            entity_tag, key,
190        )?)),
191    }
192}
193
194fn lower_typed_patch(
195    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
196    binding: &DynamicTypedEntityBinding,
197    patch: DynamicTypedStructuralPatch,
198    mode: MutationMode,
199    mutation_context: MutationDiagnosticContext,
200) -> Result<AcceptedMutationIntentPatch, InternalError> {
201    let mut lowered = AcceptedMutationIntentPatch::new();
202    for (descriptor_ordinal, cell) in patch.into_fields() {
203        let (field_id, slot) = binding
204            .field_identity_binding(descriptor_ordinal)
205            .ok_or_else(InternalError::store_invariant)?;
206        let slot_index = usize::from(slot);
207        let field = descriptor
208            .field_for_slot_index(slot_index)
209            .ok_or_else(InternalError::store_invariant)?;
210        if field.field_id().get() != field_id {
211            return Err(InternalError::store_invariant());
212        }
213        lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
214    }
215    Ok(lowered)
216}
217
218fn lower_typed_mutation_intent(
219    catalog: &AcceptedSchemaCatalogContext,
220    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
221    binding: &DynamicTypedEntityBinding,
222    request: DynamicTypedMutation,
223    batch_position: u32,
224) -> Result<Option<AcceptedStructuralMutation>, InternalError> {
225    let entity_tag = catalog.identity().entity_tag();
226    let (mode, target, patch) = match request {
227        DynamicTypedMutation::Insert { patch } => (
228            MutationMode::Insert,
229            AcceptedStructuralMutationTarget::ResolveFromAfterImage,
230            patch,
231        ),
232        DynamicTypedMutation::Update { key, patch } => (
233            MutationMode::Update,
234            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
235            patch,
236        ),
237        DynamicTypedMutation::Replace { key, patch } => (
238            MutationMode::Replace,
239            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
240            patch,
241        ),
242        DynamicTypedMutation::Delete { key } => {
243            return Ok(Some(AcceptedStructuralMutation::delete(dynamic_key(
244                entity_tag, key,
245            )?)));
246        }
247    };
248    if !patch.is_bound_to(binding) {
249        return Ok(None);
250    }
251    let patch = lower_typed_patch(
252        descriptor,
253        binding,
254        patch,
255        mode,
256        mutation_diagnostic_context(catalog, mode, batch_position),
257    )?;
258    Ok(Some(AcceptedStructuralMutation::save(mode, target, patch)))
259}
260
261fn prepare_dynamic_mutation_result(
262    catalog: &AcceptedSchemaCatalogContext,
263    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
264    rows: Vec<AcceptedStructuralMutationRow>,
265    enforce_mixed_batch_result_bound: bool,
266) -> Result<DynamicMutationResult, InternalError> {
267    let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
268        total
269            .checked_add(u32::from(row.logical_changed()))
270            .ok_or_else(InternalError::executor_invariant)
271    })?;
272    let columns = descriptor
273        .fields()
274        .iter()
275        .map(|field| field.name().to_string())
276        .collect();
277    let rows = rows
278        .into_iter()
279        .map(|row| {
280            row.values
281                .into_iter()
282                .map(|value| {
283                    output_value_from_runtime(catalog.enum_catalog(), value)
284                        .map_err(|_| InternalError::store_invariant())
285                })
286                .collect::<Result<Vec<_>, _>>()
287        })
288        .collect::<Result<Vec<_>, _>>()?;
289    let result = DynamicMutationResult {
290        entity: catalog.snapshot().entity_name().to_string(),
291        columns,
292        rows,
293        affected_rows,
294    };
295    if enforce_mixed_batch_result_bound {
296        let encoded_bytes = icydb_schema::wire::encoded_reply_len(&result)
297            .map_err(|_| InternalError::executor_invariant())?;
298        validate_structural_mutation_result_bytes(encoded_bytes)?;
299    }
300    Ok(result)
301}
302
303fn typed_descriptor_field_type(
304    field_type: TypedFieldType,
305) -> Result<FieldType, SchemaContractError> {
306    match field_type {
307        TypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
308        TypedFieldType::List(item) => Ok(FieldType::List(Box::new(typed_descriptor_field_type(
309            *item,
310        )?))),
311        TypedFieldType::Named(source_key) => {
312            TypeSourceKey::try_new(source_key.to_string()).map(FieldType::Named)
313        }
314    }
315}
316
317fn typed_adapter_field_kind_matches(
318    accepted: &AcceptedFieldKind,
319    expected: &AcceptedFieldKind,
320) -> bool {
321    if accepted == expected {
322        return true;
323    }
324    match (accepted, expected) {
325        (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
326            typed_adapter_field_kind_matches(key_kind, expected)
327        }
328        (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
329            typed_adapter_field_kind_matches(accepted, expected)
330        }
331        _ => false,
332    }
333}
334
335impl<C: CanisterKind> DbSession<C> {
336    /// Issue one opaque accepted binding for immutable generated source keys.
337    pub fn issue_typed_entity_binding(
338        &self,
339        descriptor: &TypedEntityDescriptor,
340    ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
341        let unavailable = |field_source| {
342            DynamicTypedBindingError::source_unavailable(descriptor.entity_source_key, field_source)
343        };
344        let entity_source = EntitySourceKey::try_new(descriptor.entity_source_key)
345            .map_err(|_| unavailable(None))?;
346        let catalog = self
347            .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
348            .ok_or_else(|| unavailable(None))?;
349        let identity = catalog.identity();
350        if identity.entity_path() != entity_source.as_str() {
351            return Err(InternalError::store_invariant().into());
352        }
353        let store = self.db.recovered_store(identity.store_path())?;
354        // Binding issuance only projects owned adapter data. Borrow the verified
355        // authority in place instead of cloning every entity's schema bundle;
356        // release the borrow before the returned binding can execute or mutate.
357        store.with_schema(|schema| {
358            let bundle = schema
359                .borrow_accepted_schema_bundle_for_authority(
360                    catalog.value_catalog_handle().authority(),
361                )?
362                .ok_or_else(InternalError::store_invariant)?;
363            let entity_tag = identity.entity_tag();
364            if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
365                || bundle.revision() != catalog.revision()
366            {
367                return Err(InternalError::store_invariant().into());
368            }
369            let snapshot = bundle
370                .entity_snapshots()
371                .get(&entity_tag)
372                .ok_or_else(InternalError::store_invariant)?;
373            if descriptor.primary_key_source_keys.len() != snapshot.primary_key_field_ids().len() {
374                return Err(DynamicTypedBindingError::IncompatibleField);
375            }
376            for (source_key, accepted_field_id) in descriptor
377                .primary_key_source_keys
378                .iter()
379                .zip(snapshot.primary_key_field_ids())
380            {
381                let source = FieldSourceKey::try_new((*source_key).to_string())
382                    .map_err(|_| unavailable(Some(*source_key)))?;
383                let descriptor_field_id = bundle
384                    .source_bindings()
385                    .field(entity_tag, &source)
386                    .ok_or_else(|| unavailable(Some(*source_key)))?;
387                if descriptor_field_id != *accepted_field_id {
388                    return Err(DynamicTypedBindingError::IncompatibleField);
389                }
390            }
391            let row_contract = catalog.inspection_plan().row_contract();
392            let mut fields = Vec::with_capacity(descriptor.fields.len());
393            for field_descriptor in descriptor.fields {
394                let source = FieldSourceKey::try_new(field_descriptor.source_key.to_string())
395                    .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
396                let field_id = bundle
397                    .source_bindings()
398                    .field(entity_tag, &source)
399                    .ok_or_else(|| unavailable(Some(field_descriptor.source_key)))?;
400                let field = snapshot
401                    .fields()
402                    .iter()
403                    .find(|field| field.id() == field_id)
404                    .ok_or_else(InternalError::store_invariant)?;
405                let runtime_field = row_contract
406                    .required_accepted_field_contract(usize::from(field.slot().get()))?;
407                if runtime_field.field_id() != field_id {
408                    return Err(InternalError::store_invariant().into());
409                }
410                let field_type = typed_descriptor_field_type(field_descriptor.field_type)
411                    .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
412                let expected_kind = lower_field_type(&field_type, bundle.source_bindings())
413                    .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
414                if field.nullable() != field_descriptor.nullable
415                    || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
416                {
417                    return Err(DynamicTypedBindingError::IncompatibleField);
418                }
419                fields.push((
420                    source.as_str().to_string(),
421                    field_id.get(),
422                    field.slot().get(),
423                    field.name().to_string(),
424                ));
425            }
426            let adapter_names = bundle.typed_adapter_names()?;
427
428            DynamicTypedEntityBinding::new(
429                database_incarnation_id()?.to_bytes(),
430                entity_source.as_str().to_string(),
431                snapshot.entity_name().to_string(),
432                entity_tag.value(),
433                catalog.revision().get(),
434                catalog.fingerprint(),
435                row_contract.current_layout_version().get(),
436                fields,
437                adapter_names.named_types,
438                adapter_names.enum_variants,
439                adapter_names.composite_fields,
440            )
441            .map_err(Into::into)
442        })
443    }
444
445    pub(in crate::db::session) fn current_typed_entity_binding_catalog(
446        &self,
447        binding: &DynamicTypedEntityBinding,
448    ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
449        // Select this session's commit domain before inspecting its identity.
450        // The checked value is local to this synchronous validation, not the
451        // binding lifetime; catalog lookup and matching retain their live checks.
452        self.db.ensure_recovered_state()?;
453        let incarnation = database_incarnation_id()?.to_bytes();
454        if incarnation != binding.database_incarnation {
455            return Ok(None);
456        }
457        let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
458            binding.entity_source.as_str(),
459        )?
460        else {
461            return Ok(None);
462        };
463        self.typed_entity_binding_matches_catalog(binding, &catalog, incarnation)
464            .map(|current| current.then_some(catalog))
465    }
466
467    fn typed_entity_binding_matches_catalog(
468        &self,
469        binding: &DynamicTypedEntityBinding,
470        catalog: &AcceptedSchemaCatalogContext,
471        incarnation: [u8; 16],
472    ) -> Result<bool, InternalError> {
473        if incarnation != binding.database_incarnation {
474            return Ok(false);
475        }
476        let row_contract = catalog.inspection_plan().row_contract();
477        let identity = catalog.identity();
478        if identity.entity_path() != binding.entity_source.as_str()
479            || identity.entity_tag().value() != binding.entity_tag
480            || catalog.revision().get() != binding.accepted_revision
481            || catalog.fingerprint() != binding.accepted_fingerprint
482            || row_contract.current_layout_version().get() != binding.entity_generation
483        {
484            return Ok(false);
485        }
486        let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
487            .map_err(|_| InternalError::store_invariant())?;
488        let store = self.db.recovered_store(identity.store_path())?;
489        // Only inspect the bundle here; keep its schema-owned validation and
490        // release the borrow before the caller can prepare or commit writes.
491        store.with_schema(|schema| {
492            let bundle = schema
493                .borrow_accepted_schema_bundle_for_authority(
494                    catalog.value_catalog_handle().authority(),
495                )?
496                .ok_or_else(InternalError::store_invariant)?;
497            if bundle.revision() != catalog.revision()
498                || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
499            {
500                return Ok(false);
501            }
502            let snapshot = bundle
503                .entity_snapshots()
504                .get(&identity.entity_tag())
505                .ok_or_else(InternalError::store_invariant)?;
506            for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings()
507            {
508                let source = FieldSourceKey::try_new(source_key)
509                    .map_err(|_| InternalError::store_invariant())?;
510                let Some(field_id) = bundle
511                    .source_bindings()
512                    .field(identity.entity_tag(), &source)
513                else {
514                    return Ok(false);
515                };
516                let Some(field) = snapshot
517                    .fields()
518                    .iter()
519                    .find(|field| field.id() == field_id)
520                else {
521                    return Err(InternalError::store_invariant());
522                };
523                if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
524                    return Ok(false);
525                }
526            }
527
528            Ok(true)
529        })
530    }
531
532    /// Verify that an opaque typed binding still names the exact accepted authority.
533    pub fn typed_entity_binding_is_current(
534        &self,
535        binding: &DynamicTypedEntityBinding,
536    ) -> Result<bool, InternalError> {
537        self.current_typed_entity_binding_catalog(binding)
538            .map(|catalog| catalog.is_some())
539    }
540
541    /// Materialize one accepted delete batch, run bounded frontend validation,
542    /// then commit it atomically.
543    #[cfg(feature = "sql")]
544    pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
545        &self,
546        catalog: &AcceptedSchemaCatalogContext,
547        capture_output_values: bool,
548        keys: Vec<DecodedDataStoreKey>,
549        precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
550    ) -> Result<Vec<Vec<Value>>, InternalError> {
551        let mutations = keys
552            .into_iter()
553            .map(AcceptedStructuralMutation::delete)
554            .collect::<Vec<_>>();
555        let mutation_capacity = mutations.len();
556        let mut mutations = mutations.into_iter();
557        execute_accepted_structural_mutation_batch(
558            &self.db,
559            catalog,
560            mutation_capacity,
561            0,
562            || {
563                Ok(mutations
564                    .next()
565                    .map(|mutation| AcceptedStructuralMutationBatchItem {
566                        catalog: catalog.clone(),
567                        mutation,
568                    }))
569            },
570            Timestamp::now(),
571            AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
572            |rows, _report| {
573                let rows = rows
574                    .into_iter()
575                    .map(AcceptedStructuralMutationRow::into_values)
576                    .collect::<Vec<_>>();
577                precommit_validation(rows.as_slice())?;
578                Ok((rows, AcceptedStructuralMutationCommitDirective::Standard))
579            },
580        )
581    }
582
583    /// Materialize one accepted structural batch, let its caller prepare and
584    /// validate the final after-images, then commit atomically.
585    ///
586    /// The caller freezes one operation timestamp and supplies frontend-lowered
587    /// intent only. Accepted defaults, generated values, managed timestamps,
588    /// constraints, relations, row encoding, and commit preparation remain
589    /// owned by this database boundary.
590    /// Count-only callers omit result values, never row validation or constraints.
591    pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
592        &self,
593        catalog: &AcceptedSchemaCatalogContext,
594        capture_output_values: bool,
595        mutations: Vec<AcceptedStructuralMutation>,
596        operation_timestamp: Timestamp,
597        precommit_preparation: impl FnOnce(
598            Vec<AcceptedStructuralMutationRow>,
599        ) -> Result<T, InternalError>,
600    ) -> Result<T, InternalError> {
601        let mutation_capacity = mutations.len();
602        let identity_candidate_count = mutations
603            .iter()
604            .filter(|mutation| {
605                matches!(
606                    mutation,
607                    AcceptedStructuralMutation::Save {
608                        mode: MutationMode::Insert,
609                        target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
610                        ..
611                    }
612                )
613            })
614            .count();
615        let mut mutations = mutations.into_iter();
616        execute_accepted_structural_mutation_batch(
617            &self.db,
618            catalog,
619            mutation_capacity,
620            identity_candidate_count,
621            || {
622                Ok(mutations
623                    .next()
624                    .map(|mutation| AcceptedStructuralMutationBatchItem {
625                        catalog: catalog.clone(),
626                        mutation,
627                    }))
628            },
629            operation_timestamp,
630            AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
631            |rows, _report| {
632                precommit_preparation(rows).map(|prepared| {
633                    (
634                        prepared,
635                        AcceptedStructuralMutationCommitDirective::Standard,
636                    )
637                })
638            },
639        )
640    }
641
642    /// Commit one complete accepted update page and its exact durable progress successor.
643    #[cfg(test)]
644    pub(in crate::db::session) fn execute_accepted_structural_update_with_mutation_progress(
645        &self,
646        catalog: &AcceptedSchemaCatalogContext,
647        mutations: Vec<AcceptedStructuralMutation>,
648        operation_timestamp: Timestamp,
649        mutation_progress: MutationProgressRecordOp,
650    ) -> Result<usize, InternalError> {
651        let mutation_capacity = mutations.len();
652        let mut mutations = mutations.into_iter();
653        execute_accepted_structural_mutation_batch(
654            &self.db,
655            catalog,
656            mutation_capacity,
657            0,
658            || {
659                Ok(mutations
660                    .next()
661                    .map(|mutation| AcceptedStructuralMutationBatchItem {
662                        catalog: catalog.clone(),
663                        mutation,
664                    }))
665            },
666            operation_timestamp,
667            AcceptedStructuralMutationCommitOptions::with_mutation_progress(),
668            |rows, _report| {
669                Ok((
670                    rows.len(),
671                    AcceptedStructuralMutationCommitDirective::WithMutationProgress(
672                        mutation_progress,
673                    ),
674                ))
675            },
676        )
677    }
678
679    fn execute_lowered_dynamic_mutation_batch(
680        &self,
681        catalog: &AcceptedSchemaCatalogContext,
682        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
683        mutations: Vec<AcceptedStructuralMutation>,
684        enforce_mixed_batch_result_bound: bool,
685    ) -> Result<DynamicMutationResult, InternalError> {
686        self.execute_accepted_structural_save_batch(
687            catalog,
688            true,
689            mutations,
690            Timestamp::now(),
691            |rows| {
692                prepare_dynamic_mutation_result(
693                    catalog,
694                    descriptor,
695                    rows,
696                    enforce_mixed_batch_result_bound,
697                )
698            },
699        )
700    }
701
702    /// Execute one trusted entity-name-driven structural mutation.
703    ///
704    /// This lane resolves public values, defaults, generation, management,
705    /// constraints, relations, and commit preparation from accepted schema.
706    /// It never materializes a generated entity or invokes application
707    /// validators/normalizers.
708    pub fn execute_trusted_dynamic_mutation(
709        &self,
710        request: &DynamicMutation,
711    ) -> Result<DynamicMutationResult, InternalError> {
712        if request.entity().is_empty() {
713            return Err(InternalError::executor_unsupported());
714        }
715        let catalog =
716            self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
717        let descriptor =
718            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
719        let mutation = lower_dynamic_mutation_intent(&catalog, &descriptor, request.clone(), 0)?;
720
721        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
722    }
723
724    /// Execute one bounded same-store structural mutation batch atomically.
725    ///
726    /// Every item resolves from one captured accepted root and store, shares
727    /// one operation timestamp, and is projected to its public result before
728    /// the commit marker can be published.
729    pub fn execute_trusted_dynamic_mutation_batch(
730        &self,
731        requests: Vec<DynamicMutation>,
732    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
733        self.execute_trusted_dynamic_mutation_batch_mixed(requests)
734    }
735
736    fn execute_trusted_dynamic_mutation_batch_mixed(
737        &self,
738        requests: Vec<DynamicMutation>,
739    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
740        if requests.is_empty() {
741            return Err(InternalError::mutation_batch_empty());
742        }
743        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
744            return Err(InternalError::mutation_batch_too_many_items(
745                requests.len(),
746                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
747            ));
748        }
749        let first = requests
750            .first()
751            .ok_or_else(InternalError::mutation_batch_empty)?;
752        if first.entity().is_empty() {
753            return Err(InternalError::executor_unsupported());
754        }
755        let anchor_catalog =
756            self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
757        let anchor_identity = anchor_catalog.identity();
758        let mut entity_tags = std::collections::BTreeSet::new();
759        let mut items = Vec::with_capacity(requests.len());
760        let mut result_catalogs = Vec::with_capacity(requests.len());
761        let mut identity_candidate_count = 0_usize;
762
763        let request_count = requests.len();
764        for (batch_position, request) in requests.into_iter().enumerate() {
765            let batch_position = u32::try_from(batch_position).map_err(|_| {
766                InternalError::mutation_batch_too_many_items(
767                    request_count,
768                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
769                )
770            })?;
771            if request.entity().is_empty() {
772                return Err(InternalError::executor_unsupported());
773            }
774            let item_catalog = anchor_catalog
775                .for_entity_name(request.entity())
776                .ok_or_else(|| InternalError::unsupported_entity_path(request.entity()))?;
777            let item_identity = item_catalog.identity();
778            if item_identity.store_path() != anchor_identity.store_path() {
779                return Err(InternalError::mutation_batch_store_mismatch(
780                    batch_position,
781                    anchor_identity.entity_tag().value(),
782                    item_identity.entity_tag().value(),
783                ));
784            }
785            entity_tags.insert(item_identity.entity_tag());
786            if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
787                return Err(InternalError::mutation_batch_too_many_entities(
788                    entity_tags.len(),
789                    MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
790                ));
791            }
792            let descriptor =
793                AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
794            let mutation =
795                lower_dynamic_mutation_intent(&item_catalog, &descriptor, request, batch_position)?;
796            if matches!(
797                mutation,
798                AcceptedStructuralMutation::Save {
799                    mode: MutationMode::Insert,
800                    target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
801                    ..
802                }
803            ) {
804                identity_candidate_count = identity_candidate_count.saturating_add(1);
805            }
806            result_catalogs.push(item_catalog.clone());
807            items.push(AcceptedStructuralMutationBatchItem {
808                catalog: item_catalog,
809                mutation,
810            });
811        }
812
813        self.execute_lowered_mixed_mutation_batch(
814            &anchor_catalog,
815            items,
816            result_catalogs,
817            identity_candidate_count,
818        )
819    }
820
821    fn execute_lowered_mixed_mutation_batch(
822        &self,
823        anchor_catalog: &AcceptedSchemaCatalogContext,
824        items: Vec<AcceptedStructuralMutationBatchItem>,
825        result_catalogs: Vec<AcceptedSchemaCatalogContext>,
826        identity_candidate_count: usize,
827    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
828        if items.len() != result_catalogs.len() {
829            return Err(InternalError::executor_invariant());
830        }
831        let mutation_count = items.len();
832        let mut items = items.into_iter();
833        execute_accepted_structural_mutation_batch(
834            &self.db,
835            anchor_catalog,
836            mutation_count,
837            identity_candidate_count,
838            || Ok(items.next()),
839            Timestamp::now(),
840            AcceptedStructuralMutationCommitOptions::standard(true),
841            |rows, _report| {
842                if rows.len() != result_catalogs.len() {
843                    return Err(InternalError::executor_invariant());
844                }
845                let mut results = Vec::with_capacity(rows.len());
846                for (row, catalog) in rows.into_iter().zip(result_catalogs.iter()) {
847                    let descriptor =
848                        AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
849                    results.push(prepare_dynamic_mutation_result(
850                        catalog,
851                        &descriptor,
852                        vec![row],
853                        false,
854                    )?);
855                }
856                let encoded_bytes = icydb_schema::wire::encoded_reply_len(&results)
857                    .map_err(|_| InternalError::executor_invariant())?;
858                validate_structural_mutation_result_bytes(encoded_bytes)?;
859                Ok((results, AcceptedStructuralMutationCommitDirective::Standard))
860            },
861        )
862    }
863
864    /// Execute one generated typed write through immutable accepted entity and
865    /// field identities. `None` means the opaque binding is stale.
866    #[doc(hidden)]
867    pub fn execute_trusted_typed_mutation(
868        &self,
869        binding: &DynamicTypedEntityBinding,
870        request: DynamicTypedMutation,
871    ) -> Result<Option<DynamicMutationResult>, InternalError> {
872        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
873            return Ok(None);
874        };
875        let descriptor =
876            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
877        let Some(mutation) =
878            lower_typed_mutation_intent(&catalog, &descriptor, binding, request, 0)?
879        else {
880            return Ok(None);
881        };
882        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
883            .map(Some)
884    }
885
886    /// Execute one bounded same-entity generated typed-write batch through one
887    /// exact current binding. `None` means the binding or a patch is stale or
888    /// mismatched.
889    #[doc(hidden)]
890    pub fn execute_trusted_same_entity_typed_mutation_batch(
891        &self,
892        binding: &DynamicTypedEntityBinding,
893        requests: Vec<DynamicTypedMutation>,
894    ) -> Result<Option<DynamicMutationResult>, InternalError> {
895        if requests.is_empty() {
896            return Err(InternalError::mutation_batch_empty());
897        }
898        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
899            return Err(InternalError::mutation_batch_too_many_items(
900                requests.len(),
901                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
902            ));
903        }
904        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
905            return Ok(None);
906        };
907        let descriptor =
908            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
909        let mut mutations = Vec::with_capacity(requests.len());
910        let request_count = requests.len();
911        for (batch_position, request) in requests.into_iter().enumerate() {
912            let batch_position = u32::try_from(batch_position).map_err(|_| {
913                InternalError::mutation_batch_too_many_items(
914                    request_count,
915                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
916                )
917            })?;
918            let Some(mutation) = lower_typed_mutation_intent(
919                &catalog,
920                &descriptor,
921                binding,
922                request,
923                batch_position,
924            )?
925            else {
926                return Ok(None);
927            };
928            mutations.push(mutation);
929        }
930
931        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, true)
932            .map(Some)
933    }
934
935    /// Execute one bounded generated typed-write batch atomically through
936    /// exact current same-store bindings. `None` means a binding or patch is
937    /// stale or mismatched.
938    #[doc(hidden)]
939    pub fn execute_trusted_typed_mutation_batch(
940        &self,
941        requests: Vec<(DynamicTypedEntityBinding, DynamicTypedMutation)>,
942    ) -> Result<Option<Vec<DynamicMutationResult>>, InternalError> {
943        if requests.is_empty() {
944            return Err(InternalError::mutation_batch_empty());
945        }
946        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
947            return Err(InternalError::mutation_batch_too_many_items(
948                requests.len(),
949                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
950            ));
951        }
952        let first_binding = requests
953            .first()
954            .map(|(binding, _)| binding)
955            .ok_or_else(InternalError::mutation_batch_empty)?;
956        let Some(catalog) = self.current_typed_entity_binding_catalog(first_binding)? else {
957            return Ok(None);
958        };
959        let anchor_identity = catalog.identity();
960        let mut entity_tags = std::collections::BTreeSet::new();
961        let mut items = Vec::with_capacity(requests.len());
962        let mut result_catalogs = Vec::with_capacity(requests.len());
963        let mut identity_candidate_count = 0_usize;
964
965        let request_count = requests.len();
966        for (batch_position, (binding, request)) in requests.into_iter().enumerate() {
967            let batch_position = u32::try_from(batch_position).map_err(|_| {
968                InternalError::mutation_batch_too_many_items(
969                    request_count,
970                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
971                )
972            })?;
973            let Some(item_catalog) = catalog.for_entity_path(binding.entity_source.as_str()) else {
974                return Ok(None);
975            };
976            if !self.typed_entity_binding_matches_catalog(
977                &binding,
978                &item_catalog,
979                database_incarnation_id()?.to_bytes(),
980            )? {
981                return Ok(None);
982            }
983            let item_identity = item_catalog.identity();
984            if item_identity.store_path() != anchor_identity.store_path() {
985                return Err(InternalError::mutation_batch_store_mismatch(
986                    batch_position,
987                    anchor_identity.entity_tag().value(),
988                    item_identity.entity_tag().value(),
989                ));
990            }
991            entity_tags.insert(item_identity.entity_tag());
992            if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
993                return Err(InternalError::mutation_batch_too_many_entities(
994                    entity_tags.len(),
995                    MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
996                ));
997            }
998            let descriptor =
999                AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
1000            let Some(mutation) = lower_typed_mutation_intent(
1001                &item_catalog,
1002                &descriptor,
1003                &binding,
1004                request,
1005                batch_position,
1006            )?
1007            else {
1008                return Ok(None);
1009            };
1010            if matches!(
1011                mutation,
1012                AcceptedStructuralMutation::Save {
1013                    mode: MutationMode::Insert,
1014                    target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1015                    ..
1016                }
1017            ) {
1018                identity_candidate_count = identity_candidate_count.saturating_add(1);
1019            }
1020            result_catalogs.push(item_catalog.clone());
1021            items.push(AcceptedStructuralMutationBatchItem {
1022                catalog: item_catalog,
1023                mutation,
1024            });
1025        }
1026
1027        self.execute_lowered_mixed_mutation_batch(
1028            &catalog,
1029            items,
1030            result_catalogs,
1031            identity_candidate_count,
1032        )
1033        .map(Some)
1034    }
1035
1036    /// Execute one trusted atomic insert batch from entity-name-driven patches.
1037    ///
1038    /// Every patch is lowered against the same accepted snapshot and shares
1039    /// one operation timestamp before the canonical structural batch owner
1040    /// stages any durable effect.
1041    pub fn execute_trusted_dynamic_insert_batch(
1042        &self,
1043        entity: &str,
1044        patches: Vec<DynamicStructuralPatch>,
1045    ) -> Result<DynamicMutationResult, InternalError> {
1046        let patch_count = patches.len();
1047        if patch_count == 0 {
1048            return Err(InternalError::mutation_batch_empty());
1049        }
1050        if patch_count > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1051            return Err(InternalError::mutation_batch_too_many_items(
1052                patch_count,
1053                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1054            ));
1055        }
1056        if entity.is_empty() {
1057            return Err(InternalError::executor_unsupported());
1058        }
1059        let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(entity))?;
1060        let descriptor =
1061            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1062        let mut mutations = Vec::with_capacity(patch_count);
1063        // The batch already names one entity. Lower each patch against that
1064        // captured authority without constructing or resolving per-row names.
1065        for (batch_position, patch) in patches.into_iter().enumerate() {
1066            let batch_position = u32::try_from(batch_position).map_err(|_| {
1067                InternalError::mutation_batch_too_many_items(
1068                    patch_count,
1069                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1070                )
1071            })?;
1072            mutations.push(lower_dynamic_save_intent(
1073                &catalog,
1074                &descriptor,
1075                patch,
1076                MutationMode::Insert,
1077                AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1078                batch_position,
1079            )?);
1080        }
1081
1082        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, false)
1083    }
1084}