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
7use super::AcceptedSchemaCatalogContext;
8use crate::{
9    db::{
10        DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
11        DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedFieldBindingRequest,
12        DynamicTypedFieldType, DynamicTypedMutation, DynamicTypedStructuralPatch, DynamicWriteCell,
13        commit::{CommitRowOp, database_incarnation_id},
14        data::{
15            AcceptedMutationIntentPatch, AcceptedPreKeyInsert, DecodedDataStoreKey, FieldSlot,
16            RawRow, StructuralRowContract, StructuralSlotReader,
17            canonical_row_from_raw_row_with_accepted_decode_contract,
18            resolve_existing_replace_structural_patch_with_accepted_contract,
19            resolve_insert_structural_patch_with_accepted_contract,
20            resolve_update_structural_patch_with_accepted_contract,
21        },
22        executor::{
23            AcceptedMutationConstraintScheduler, commit_structural_row_ops_with_window_for_path,
24            mutation_key_exists_error,
25        },
26        schema::{
27            AcceptedFieldKind, AcceptedIdentityAllocation, AcceptedRowLayoutRuntimeContract,
28            FieldId, FieldInsertGeneration, IdentityStatementCursor, lower_field_type,
29            output_value_from_runtime,
30        },
31        write_context::{AcceptedWriteContext, MutationMode},
32    },
33    error::InternalError,
34    metrics::sink::{MetricsEvent, SaveMutationKind, record},
35    traits::CanisterKind,
36    types::{CurrentTimestamp, Timestamp},
37    value::{InputValue, Value},
38};
39use icydb_schema::{EntitySourceKey, FieldSourceKey, FieldType, TypeSourceKey};
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42struct AcceptedIdentityInsertField {
43    field_id: FieldId,
44    field_slot: usize,
45    accepted_kind: AcceptedFieldKind,
46}
47
48/// Accepted row identity carried by a structural mutation after frontend
49/// lowering but before the canonical after-image exists.
50pub(in crate::db::session) enum AcceptedStructuralMutationTarget {
51    ResolveFromAfterImage,
52    Expected(Box<DecodedDataStoreKey>),
53}
54
55impl AcceptedStructuralMutationTarget {
56    pub(in crate::db::session) fn expected(key: DecodedDataStoreKey) -> Self {
57        Self::Expected(Box::new(key))
58    }
59}
60
61/// One accepted structural mutation intent ready for shared batch
62/// materialization.
63pub(in crate::db::session) enum AcceptedStructuralMutation {
64    Save {
65        mode: MutationMode,
66        target: AcceptedStructuralMutationTarget,
67        patch: AcceptedMutationIntentPatch,
68    },
69    Delete {
70        key: Box<DecodedDataStoreKey>,
71    },
72}
73
74impl AcceptedStructuralMutation {
75    pub(in crate::db::session) const fn save(
76        mode: MutationMode,
77        target: AcceptedStructuralMutationTarget,
78        patch: AcceptedMutationIntentPatch,
79    ) -> Self {
80        Self::Save {
81            mode,
82            target,
83            patch,
84        }
85    }
86
87    pub(in crate::db::session) fn delete(key: DecodedDataStoreKey) -> Self {
88        Self::Delete { key: Box::new(key) }
89    }
90}
91
92const MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS: usize = 4_096;
93const MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES: usize = 16 * 1024 * 1024;
94const MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES: usize = 1024 * 1024;
95
96fn add_structural_mutation_staged_bytes(
97    total: &mut usize,
98    lengths: impl IntoIterator<Item = usize>,
99) -> Result<(), InternalError> {
100    for length in lengths {
101        *total = total
102            .checked_add(length)
103            .ok_or_else(InternalError::mutation_batch_staged_bytes_exceeded)?;
104        if *total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
105            return Err(InternalError::mutation_batch_staged_bytes_exceeded());
106        }
107    }
108    Ok(())
109}
110
111fn validate_structural_mutation_result_bytes(encoded_bytes: usize) -> Result<(), InternalError> {
112    if encoded_bytes > MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES {
113        return Err(InternalError::mutation_batch_result_bytes_exceeded());
114    }
115    Ok(())
116}
117
118/// One canonical row produced by structural mutation materialization.
119pub(in crate::db::session) struct AcceptedStructuralMutationRow {
120    values: Vec<Value>,
121    logical_changed: bool,
122}
123
124impl AcceptedStructuralMutationRow {
125    #[cfg(feature = "sql")]
126    pub(in crate::db::session) fn into_values(self) -> Vec<Value> {
127        self.values
128    }
129
130    pub(in crate::db::session) const fn logical_changed(&self) -> bool {
131        self.logical_changed
132    }
133}
134
135const fn dynamic_mutation_mode(request: &DynamicMutation) -> Option<MutationMode> {
136    match request {
137        DynamicMutation::Insert { .. } => Some(MutationMode::Insert),
138        DynamicMutation::Update { .. } => Some(MutationMode::Update),
139        DynamicMutation::Replace { .. } => Some(MutationMode::Replace),
140        DynamicMutation::Delete { .. } => None,
141    }
142}
143
144const fn dynamic_typed_mutation_mode(request: &DynamicTypedMutation) -> MutationMode {
145    match request {
146        DynamicTypedMutation::Insert { .. } => MutationMode::Insert,
147        DynamicTypedMutation::Update { .. } => MutationMode::Update,
148        DynamicTypedMutation::Replace { .. } => MutationMode::Replace,
149    }
150}
151
152const fn dynamic_write_context(operation_timestamp: Timestamp) -> AcceptedWriteContext {
153    AcceptedWriteContext::new(operation_timestamp)
154}
155
156fn insert_key_exists_after_generation(identity_generated: bool) -> InternalError {
157    if identity_generated {
158        InternalError::identity_state_corruption()
159    } else {
160        mutation_key_exists_error()
161    }
162}
163
164fn dynamic_key(
165    entity_tag: crate::types::EntityTag,
166    key: &InputValue,
167) -> Result<DecodedDataStoreKey, InternalError> {
168    let value = key
169        .clone()
170        .try_into_runtime_non_enum()
171        .ok_or_else(InternalError::executor_unsupported)?;
172    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
173}
174
175fn lower_dynamic_patch(
176    entity_path: &str,
177    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
178    patch: &DynamicStructuralPatch,
179    mode: MutationMode,
180) -> Result<AcceptedMutationIntentPatch, InternalError> {
181    let mut lowered = AcceptedMutationIntentPatch::new();
182    for (field_name, cell) in patch.fields() {
183        let slot = descriptor
184            .field_slot_index_by_name(field_name)
185            .ok_or_else(|| {
186                InternalError::mutation_structural_field_unknown(entity_path, field_name)
187            })?;
188        let field = descriptor
189            .field_for_slot_index(slot)
190            .ok_or_else(InternalError::executor_invariant)?;
191        if !matches!(cell, DynamicWriteCell::Omitted)
192            && (field.write_policy().insert_generation().is_some()
193                || field.write_policy().write_management().is_some())
194        {
195            return Err(InternalError::mutation_database_owned_field_explicit(
196                entity_path,
197                field.name(),
198            ));
199        }
200        let slot = FieldSlot::from_validated_index(slot);
201        lowered = match cell {
202            DynamicWriteCell::Omitted => lowered,
203            DynamicWriteCell::Default => match mode {
204                MutationMode::Insert | MutationMode::Replace => {
205                    lowered.set_explicit_insert_default(slot)
206                }
207                MutationMode::Update => lowered.set_explicit_update_default(slot),
208            },
209            DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
210            DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
211        };
212    }
213    Ok(lowered)
214}
215
216fn lower_dynamic_mutation_intent(
217    entity_tag: crate::types::EntityTag,
218    entity_path: &str,
219    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
220    request: &DynamicMutation,
221) -> Result<(AcceptedStructuralMutation, Option<SaveMutationKind>), InternalError> {
222    match request {
223        DynamicMutation::Insert { patch, .. } => Ok((
224            AcceptedStructuralMutation::save(
225                MutationMode::Insert,
226                AcceptedStructuralMutationTarget::ResolveFromAfterImage,
227                lower_dynamic_patch(entity_path, descriptor, patch, MutationMode::Insert)?,
228            ),
229            Some(SaveMutationKind::Insert),
230        )),
231        DynamicMutation::Update { key, patch, .. }
232        | DynamicMutation::Replace { key, patch, .. } => {
233            let mode =
234                dynamic_mutation_mode(request).ok_or_else(InternalError::executor_invariant)?;
235            let kind = match mode {
236                MutationMode::Insert => SaveMutationKind::Insert,
237                MutationMode::Replace => SaveMutationKind::Replace,
238                MutationMode::Update => SaveMutationKind::Update,
239            };
240            Ok((
241                AcceptedStructuralMutation::save(
242                    mode,
243                    AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
244                    lower_dynamic_patch(entity_path, descriptor, patch, mode)?,
245                ),
246                Some(kind),
247            ))
248        }
249        DynamicMutation::Delete { key, .. } => Ok((
250            AcceptedStructuralMutation::delete(dynamic_key(entity_tag, key)?),
251            None,
252        )),
253    }
254}
255
256fn lower_typed_patch(
257    entity_path: &str,
258    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
259    patch: &DynamicTypedStructuralPatch,
260    mode: MutationMode,
261) -> Result<AcceptedMutationIntentPatch, InternalError> {
262    let mut lowered = AcceptedMutationIntentPatch::new();
263    for (field_id, slot, cell) in patch.fields() {
264        let slot_index = usize::from(*slot);
265        let field = descriptor
266            .field_for_slot_index(slot_index)
267            .ok_or_else(InternalError::store_invariant)?;
268        if field.field_id().get() != *field_id {
269            return Err(InternalError::store_invariant());
270        }
271        if !matches!(cell, DynamicWriteCell::Omitted)
272            && (field.write_policy().insert_generation().is_some()
273                || field.write_policy().write_management().is_some())
274        {
275            return Err(InternalError::mutation_database_owned_field_explicit(
276                entity_path,
277                field.name(),
278            ));
279        }
280        let slot = FieldSlot::from_validated_index(slot_index);
281        lowered = match cell {
282            DynamicWriteCell::Omitted => lowered,
283            DynamicWriteCell::Default => match mode {
284                MutationMode::Insert | MutationMode::Replace => {
285                    lowered.set_explicit_insert_default(slot)
286                }
287                MutationMode::Update => lowered.set_explicit_update_default(slot),
288            },
289            DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
290            DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
291        };
292    }
293    Ok(lowered)
294}
295
296fn preserve_dynamic_replacement_identity(
297    key: &DecodedDataStoreKey,
298    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
299    mut patch: AcceptedMutationIntentPatch,
300) -> Result<AcceptedMutationIntentPatch, InternalError> {
301    let primary_key_slots = descriptor.primary_key_slot_indices();
302    let runtime_key = key.primary_key_runtime_value();
303    let components = match runtime_key {
304        Value::List(values) if primary_key_slots.len() > 1 => values,
305        value if primary_key_slots.len() == 1 => vec![value],
306        _ => return Err(InternalError::executor_invariant()),
307    };
308    if components.len() != primary_key_slots.len() {
309        return Err(InternalError::executor_invariant());
310    }
311
312    for (slot, value) in primary_key_slots.iter().copied().zip(components) {
313        let _ = descriptor
314            .field_for_slot_index(slot)
315            .ok_or_else(InternalError::executor_invariant)?;
316        let has_explicit_intent = patch
317            .entries()
318            .iter()
319            .any(|entry| entry.slot().index() == slot);
320        if has_explicit_intent {
321            continue;
322        }
323        let value = InputValue::try_from_runtime_non_enum(&value)
324            .ok_or_else(InternalError::executor_invariant)?;
325        patch =
326            patch.set_preserved_replacement_identity(FieldSlot::from_validated_index(slot), value);
327    }
328
329    Ok(patch)
330}
331
332// Locate the sole accepted Identity owner that is eligible to resolve a
333// keyless insert. Accepted-schema integrity already freezes the exact shape;
334// this runtime check fails closed if a malformed contract reaches execution.
335fn accepted_identity_insert_field(
336    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
337) -> Result<Option<AcceptedIdentityInsertField>, InternalError> {
338    let mut identity = None;
339    for field in descriptor.fields() {
340        if field.write_policy().insert_generation() != Some(FieldInsertGeneration::Identity) {
341            continue;
342        }
343        let field_slot = usize::from(field.slot().get());
344        if identity
345            .replace(AcceptedIdentityInsertField {
346                field_id: field.field_id(),
347                field_slot,
348                accepted_kind: field.kind().clone(),
349            })
350            .is_some()
351            || descriptor.primary_key_slot_indices() != [field_slot]
352        {
353            return Err(InternalError::identity_corruption());
354        }
355    }
356    Ok(identity)
357}
358
359fn checked_pre_key_candidate_count(count: usize) -> Result<u32, InternalError> {
360    u32::try_from(count).map_err(|_| InternalError::identity_candidate_count_exhausted())
361}
362
363fn validate_identity_materialization(
364    entity_tag: crate::types::EntityTag,
365    identity_field: &AcceptedIdentityInsertField,
366    candidate: &AcceptedPreKeyInsert,
367    allocation: &AcceptedIdentityAllocation,
368    data_key: &DecodedDataStoreKey,
369    reader: &StructuralSlotReader<'_>,
370) -> Result<(), InternalError> {
371    let owner = allocation.owner();
372    let slot_value = reader.required_cached_value(identity_field.field_slot)?;
373    if candidate.entity_tag() != entity_tag
374        || candidate.input_ordinal() != allocation.input_ordinal()
375        || owner.entity_tag() != entity_tag
376        || owner.field_id() != identity_field.field_id
377        || allocation.field_slot() != identity_field.field_slot
378        || slot_value != allocation.value()
379        || data_key.primary_key_runtime_value() != *allocation.value()
380    {
381        return Err(InternalError::identity_corruption());
382    }
383    Ok(())
384}
385
386fn data_key_from_row(
387    entity_tag: crate::types::EntityTag,
388    contract: &StructuralRowContract,
389    row: &RawRow,
390) -> Result<DecodedDataStoreKey, InternalError> {
391    let reader =
392        StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
393    let values = contract
394        .primary_key_slot_indices()
395        .iter()
396        .map(|slot| reader.required_cached_value(*slot).cloned())
397        .collect::<Result<Vec<_>, _>>()?;
398    let value = match values.as_slice() {
399        [value] => value.clone(),
400        _ => Value::List(values),
401    };
402    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
403}
404
405#[cfg(feature = "sql")]
406pub(in crate::db::session) fn structural_data_key_from_runtime_values(
407    entity_tag: crate::types::EntityTag,
408    values: Vec<Value>,
409) -> Result<DecodedDataStoreKey, InternalError> {
410    let value = match values.as_slice() {
411        [value] => value.clone(),
412        _ => Value::List(values),
413    };
414    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
415}
416
417fn validated_existing_row(
418    store: crate::db::registry::StoreHandle,
419    data_key: &DecodedDataStoreKey,
420    contract: &StructuralRowContract,
421) -> Result<Option<RawRow>, InternalError> {
422    let raw_key = data_key.to_raw()?;
423    let row = store.with_data(|data| data.get(&raw_key));
424    if let Some(row) = row.as_ref() {
425        let reader =
426            StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
427        reader.validate_primary_key(data_key)?;
428    }
429    Ok(row)
430}
431
432fn prepare_dynamic_mutation_result(
433    catalog: &AcceptedSchemaCatalogContext,
434    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
435    rows: Vec<AcceptedStructuralMutationRow>,
436    enforce_mixed_batch_result_bound: bool,
437) -> Result<DynamicMutationResult, InternalError> {
438    let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
439        total
440            .checked_add(u32::from(row.logical_changed()))
441            .ok_or_else(InternalError::executor_invariant)
442    })?;
443    let columns = descriptor
444        .fields()
445        .iter()
446        .map(|field| field.name().to_string())
447        .collect();
448    let rows = rows
449        .into_iter()
450        .map(|row| {
451            row.values
452                .iter()
453                .map(|value| {
454                    output_value_from_runtime(catalog.enum_catalog(), value)
455                        .map_err(|_| InternalError::store_invariant())
456                })
457                .collect::<Result<Vec<_>, _>>()
458        })
459        .collect::<Result<Vec<_>, _>>()?;
460    let result = DynamicMutationResult {
461        entity: catalog.snapshot().entity_name().to_string(),
462        columns,
463        rows,
464        affected_rows,
465    };
466    if enforce_mixed_batch_result_bound {
467        let encoded =
468            candid::encode_one(&result).map_err(|_| InternalError::executor_invariant())?;
469        validate_structural_mutation_result_bytes(encoded.len())?;
470    }
471    Ok(result)
472}
473
474fn dynamic_typed_field_type(
475    field_type: DynamicTypedFieldType,
476) -> Result<FieldType, DynamicTypedBindingError> {
477    match field_type {
478        DynamicTypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
479        DynamicTypedFieldType::List(item) => {
480            Ok(FieldType::List(Box::new(dynamic_typed_field_type(*item)?)))
481        }
482        DynamicTypedFieldType::Named(source_key) => TypeSourceKey::try_new(source_key)
483            .map(FieldType::Named)
484            .map_err(|_| DynamicTypedBindingError::FieldUnavailable),
485    }
486}
487
488fn typed_adapter_field_kind_matches(
489    accepted: &AcceptedFieldKind,
490    expected: &AcceptedFieldKind,
491) -> bool {
492    if accepted == expected {
493        return true;
494    }
495    match (accepted, expected) {
496        (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
497            typed_adapter_field_kind_matches(key_kind, expected)
498        }
499        (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
500            typed_adapter_field_kind_matches(accepted, expected)
501        }
502        _ => false,
503    }
504}
505
506impl<C: CanisterKind> DbSession<C> {
507    /// Issue one opaque accepted binding for immutable generated source keys.
508    pub fn issue_typed_entity_binding(
509        &self,
510        entity_source_key: &str,
511        field_requests: &[DynamicTypedFieldBindingRequest],
512    ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
513        let entity_source = EntitySourceKey::try_new(entity_source_key)
514            .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?;
515        let field_requests = field_requests
516            .iter()
517            .map(|request| {
518                Ok((
519                    FieldSourceKey::try_new(request.source_key.clone())
520                        .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?,
521                    dynamic_typed_field_type(request.field_type.clone())?,
522                    request.nullable,
523                ))
524            })
525            .collect::<Result<Vec<_>, DynamicTypedBindingError>>()?;
526        let catalog = self
527            .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
528            .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
529        let identity = catalog.identity();
530        if identity.entity_path() != entity_source.as_str() {
531            return Err(InternalError::store_invariant().into());
532        }
533        let store = self.db.recovered_store(identity.store_path())?;
534        let bundle = store
535            .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
536            .ok_or_else(InternalError::store_invariant)?;
537        let entity_tag = identity.entity_tag();
538        if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
539            || bundle.revision() != catalog.revision()
540        {
541            return Err(InternalError::store_invariant().into());
542        }
543        let snapshot = bundle
544            .entity_snapshots()
545            .get(&entity_tag)
546            .ok_or_else(InternalError::store_invariant)?;
547        let descriptor =
548            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
549        let mut fields = Vec::with_capacity(field_requests.len());
550        for (source, field_type, nullable) in &field_requests {
551            let field_id = bundle
552                .source_bindings()
553                .field(entity_tag, source)
554                .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
555            let field = snapshot
556                .fields()
557                .iter()
558                .find(|field| field.id() == field_id)
559                .ok_or_else(InternalError::store_invariant)?;
560            let runtime_field = descriptor
561                .field_for_slot_index(usize::from(field.slot().get()))
562                .ok_or_else(InternalError::store_invariant)?;
563            if runtime_field.field_id() != field_id {
564                return Err(InternalError::store_invariant().into());
565            }
566            let expected_kind = lower_field_type(field_type, |source| {
567                bundle.source_bindings().named_type(source)
568            })
569            .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
570            if field.nullable() != *nullable
571                || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
572            {
573                return Err(DynamicTypedBindingError::IncompatibleField);
574            }
575            fields.push((
576                source.as_str().to_string(),
577                field_id.get(),
578                field.slot().get(),
579                field.name().to_string(),
580            ));
581        }
582        let adapter_names = bundle.typed_adapter_names()?;
583
584        DynamicTypedEntityBinding::new(
585            database_incarnation_id()?.to_bytes(),
586            entity_source.as_str().to_string(),
587            snapshot.entity_name().to_string(),
588            entity_tag.value(),
589            catalog.revision().get(),
590            catalog.fingerprint(),
591            descriptor.current_layout_version().get(),
592            fields,
593            adapter_names.named_types,
594            adapter_names.enum_variants,
595            adapter_names.composite_fields,
596        )
597        .map_err(Into::into)
598    }
599
600    pub(in crate::db::session) fn current_typed_entity_binding_catalog(
601        &self,
602        binding: &DynamicTypedEntityBinding,
603    ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
604        if database_incarnation_id()?.to_bytes() != binding.database_incarnation {
605            return Ok(None);
606        }
607        let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
608            binding.entity_source.as_str(),
609        )?
610        else {
611            return Ok(None);
612        };
613        let descriptor =
614            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
615        let identity = catalog.identity();
616        if identity.entity_path() != binding.entity_source.as_str()
617            || identity.entity_tag().value() != binding.entity_tag
618            || catalog.revision().get() != binding.accepted_revision
619            || catalog.fingerprint() != binding.accepted_fingerprint
620            || descriptor.current_layout_version().get() != binding.entity_generation
621        {
622            return Ok(None);
623        }
624        let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
625            .map_err(|_| InternalError::store_invariant())?;
626        let store = self.db.recovered_store(identity.store_path())?;
627        let bundle = store
628            .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
629            .ok_or_else(InternalError::store_invariant)?;
630        if bundle.revision() != catalog.revision()
631            || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
632        {
633            return Ok(None);
634        }
635        let snapshot = bundle
636            .entity_snapshots()
637            .get(&identity.entity_tag())
638            .ok_or_else(InternalError::store_invariant)?;
639        for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings() {
640            let source = FieldSourceKey::try_new(source_key)
641                .map_err(|_| InternalError::store_invariant())?;
642            let Some(field_id) = bundle
643                .source_bindings()
644                .field(identity.entity_tag(), &source)
645            else {
646                return Ok(None);
647            };
648            let Some(field) = snapshot
649                .fields()
650                .iter()
651                .find(|field| field.id() == field_id)
652            else {
653                return Err(InternalError::store_invariant());
654            };
655            if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
656                return Ok(None);
657            }
658        }
659        Ok(Some(catalog))
660    }
661
662    /// Verify that an opaque typed binding still names the exact accepted authority.
663    pub fn typed_entity_binding_is_current(
664        &self,
665        binding: &DynamicTypedEntityBinding,
666    ) -> Result<bool, InternalError> {
667        self.current_typed_entity_binding_catalog(binding)
668            .map(|catalog| catalog.is_some())
669    }
670
671    /// Materialize one accepted delete batch, run bounded frontend validation,
672    /// then commit it atomically.
673    #[cfg(feature = "sql")]
674    pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
675        &self,
676        catalog: &AcceptedSchemaCatalogContext,
677        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
678        keys: Vec<DecodedDataStoreKey>,
679        precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
680    ) -> Result<Vec<Vec<Value>>, InternalError> {
681        let mutations = keys
682            .into_iter()
683            .map(AcceptedStructuralMutation::delete)
684            .collect();
685        self.execute_accepted_structural_mutation_batch_inner(
686            catalog,
687            descriptor,
688            mutations,
689            Timestamp::now(),
690            false,
691            |rows| {
692                let rows = rows
693                    .into_iter()
694                    .map(AcceptedStructuralMutationRow::into_values)
695                    .collect::<Vec<_>>();
696                precommit_validation(rows.as_slice())?;
697                Ok(rows)
698            },
699        )
700    }
701
702    /// Materialize one accepted structural batch, let its caller prepare and
703    /// validate the final after-images, then commit atomically.
704    ///
705    /// The caller freezes one operation timestamp and supplies frontend-lowered
706    /// intent only. Accepted defaults, generated values, managed timestamps,
707    /// constraints, relations, row encoding, and commit preparation remain
708    /// owned by this database boundary.
709    pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
710        &self,
711        catalog: &AcceptedSchemaCatalogContext,
712        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
713        mutations: Vec<AcceptedStructuralMutation>,
714        operation_timestamp: Timestamp,
715        precommit_preparation: impl FnOnce(
716            Vec<AcceptedStructuralMutationRow>,
717        ) -> Result<T, InternalError>,
718    ) -> Result<T, InternalError> {
719        self.execute_accepted_structural_mutation_batch_inner(
720            catalog,
721            descriptor,
722            mutations,
723            operation_timestamp,
724            false,
725            precommit_preparation,
726        )
727    }
728
729    /// Commit the largest durable prefix of one accepted resumable update page.
730    #[cfg(feature = "sql")]
731    pub(in crate::db::session) fn execute_accepted_structural_update_prefix(
732        &self,
733        catalog: &AcceptedSchemaCatalogContext,
734        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
735        mutations: Vec<AcceptedStructuralMutation>,
736        operation_timestamp: Timestamp,
737    ) -> Result<usize, InternalError> {
738        self.execute_accepted_structural_mutation_batch_inner(
739            catalog,
740            descriptor,
741            mutations,
742            operation_timestamp,
743            true,
744            |rows| Ok(rows.len()),
745        )
746    }
747
748    #[expect(
749        clippy::too_many_lines,
750        reason = "one phased owner keeps accepted authority, mutation context, precommit preparation, output capture, and commit staging inseparable"
751    )]
752    fn execute_accepted_structural_mutation_batch_inner<T>(
753        &self,
754        catalog: &AcceptedSchemaCatalogContext,
755        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
756        mutations: Vec<AcceptedStructuralMutation>,
757        operation_timestamp: Timestamp,
758        largest_journaled_prefix: bool,
759        precommit_preparation: impl FnOnce(
760            Vec<AcceptedStructuralMutationRow>,
761        ) -> Result<T, InternalError>,
762    ) -> Result<T, InternalError> {
763        let identity = catalog.identity();
764        let entity_path = identity.entity_path();
765        let store_path = identity.store_path();
766        let row_decode_contract =
767            descriptor.row_decode_contract(catalog.value_catalog_handle().clone());
768        let row_contract = StructuralRowContract::from_accepted_decode_contract(
769            entity_path,
770            row_decode_contract.clone(),
771        );
772        let store = self.db.recovered_store(store_path)?;
773        let write_context = dynamic_write_context(operation_timestamp);
774        let identity_field = accepted_identity_insert_field(descriptor)?;
775        let identity_incarnation = identity_field
776            .as_ref()
777            .map(|_| database_incarnation_id())
778            .transpose()?;
779        if mutations.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
780            return Err(InternalError::mutation_batch_too_many_items());
781        }
782        let identity_candidate_count = mutations
783            .iter()
784            .filter(|mutation| {
785                matches!(
786                    mutation,
787                    AcceptedStructuralMutation::Save {
788                        mode: MutationMode::Insert,
789                        target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
790                        ..
791                    }
792                )
793            })
794            .count();
795        let _ = checked_pre_key_candidate_count(identity_candidate_count)?;
796        let mut identity_cursor: Option<IdentityStatementCursor> = None;
797        let mut identity_insert_ordinal = 0_u32;
798        let mut scheduler = AcceptedMutationConstraintScheduler::new(
799            entity_path,
800            row_decode_contract.clone(),
801            catalog.fingerprint(),
802            catalog.accepted_row_constraints(),
803            mutations.len(),
804        );
805        let mut output = Vec::with_capacity(mutations.len());
806        let mut staged_bytes = 0_usize;
807
808        for (input_index, mutation) in mutations.into_iter().enumerate() {
809            let batch_input_ordinal = u32::try_from(input_index)
810                .map_err(|_| InternalError::mutation_batch_too_many_items())?;
811            let AcceptedStructuralMutation::Save {
812                mode,
813                target,
814                patch: authored_patch,
815            } = mutation
816            else {
817                let AcceptedStructuralMutation::Delete { key } = mutation else {
818                    return Err(InternalError::executor_invariant());
819                };
820                let before = validated_existing_row(store, &key, &row_contract)?
821                    .ok_or_else(|| InternalError::store_not_found(&key))?;
822                let raw_key = key.to_raw()?;
823                let canonical_before = canonical_row_from_raw_row_with_accepted_decode_contract(
824                    entity_path,
825                    row_decode_contract.clone(),
826                    &before,
827                )?;
828                add_structural_mutation_staged_bytes(
829                    &mut staged_bytes,
830                    [
831                        raw_key.as_bytes().len(),
832                        canonical_before.as_raw_row().as_bytes().len(),
833                    ],
834                )?;
835                scheduler.schedule_delete(CommitRowOp::new(
836                    entity_path,
837                    raw_key,
838                    Some(canonical_before.as_raw_row().as_bytes().to_vec()),
839                    None,
840                    catalog.fingerprint(),
841                ))?;
842                let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
843                    canonical_before.as_raw_row(),
844                    &row_contract,
845                )?;
846                let mut values = Vec::with_capacity(descriptor.fields().len());
847                for field in descriptor.fields() {
848                    values.push(
849                        reader
850                            .required_cached_value(usize::from(field.slot().get()))?
851                            .clone(),
852                    );
853                }
854                output.push(AcceptedStructuralMutationRow {
855                    values,
856                    logical_changed: true,
857                });
858                continue;
859            };
860            let (expected_key, pre_key_insert, mut keyed_patch) = match target {
861                AcceptedStructuralMutationTarget::ResolveFromAfterImage => {
862                    let candidate_ordinal =
863                        if identity_field.is_some() && matches!(mode, MutationMode::Insert) {
864                            identity_insert_ordinal
865                        } else {
866                            batch_input_ordinal
867                        };
868                    (
869                        None,
870                        Some(AcceptedPreKeyInsert::new(
871                            identity.entity_tag(),
872                            authored_patch,
873                            candidate_ordinal,
874                        )),
875                        None,
876                    )
877                }
878                AcceptedStructuralMutationTarget::Expected(key) => {
879                    (Some(*key), None, Some(authored_patch))
880                }
881            };
882            if matches!(mode, MutationMode::Replace)
883                && let Some(key) = expected_key.as_ref()
884            {
885                let patch = keyed_patch
886                    .take()
887                    .ok_or_else(InternalError::executor_invariant)?;
888                keyed_patch = Some(preserve_dynamic_replacement_identity(
889                    key, descriptor, patch,
890                )?);
891            }
892            let patch = pre_key_insert
893                .as_ref()
894                .map(AcceptedPreKeyInsert::fields)
895                .or(keyed_patch.as_ref())
896                .ok_or_else(InternalError::executor_invariant)?;
897            let before = expected_key
898                .as_ref()
899                .map(|key| validated_existing_row(store, key, &row_contract))
900                .transpose()?
901                .flatten();
902            match mode {
903                MutationMode::Insert if before.is_some() => {
904                    return Err(mutation_key_exists_error());
905                }
906                MutationMode::Update if before.is_none() => {
907                    let key = expected_key
908                        .as_ref()
909                        .ok_or_else(InternalError::executor_invariant)?;
910                    return Err(InternalError::store_not_found(key));
911                }
912                MutationMode::Insert | MutationMode::Replace | MutationMode::Update => {}
913            }
914
915            let identity_allocation = if let Some(identity_field) = identity_field.as_ref()
916                && matches!(mode, MutationMode::Insert)
917                && before.is_none()
918            {
919                let candidate = pre_key_insert.as_ref().ok_or_else(|| {
920                    let field_name = descriptor
921                        .field_for_slot_index(identity_field.field_slot)
922                        .map_or("", |field| field.name());
923                    InternalError::mutation_database_owned_field_explicit(entity_path, field_name)
924                })?;
925                if identity_cursor.is_none() {
926                    let incarnation = identity_incarnation
927                        .ok_or_else(InternalError::identity_state_corruption)?;
928                    identity_cursor = Some(store.with_schema(|schema_store| {
929                        schema_store.identity_statement_cursor(
930                            incarnation,
931                            identity.entity_tag(),
932                            identity_field.field_id,
933                            &identity_field.accepted_kind,
934                        )
935                    })?);
936                }
937                let allocation = identity_cursor
938                    .as_mut()
939                    .ok_or_else(InternalError::identity_state_corruption)?
940                    .allocate(identity_field.field_slot, candidate.input_ordinal())?;
941                identity_insert_ordinal = identity_insert_ordinal
942                    .checked_add(1)
943                    .ok_or_else(InternalError::identity_candidate_count_exhausted)?;
944                Some(allocation)
945            } else if let Some(identity_field) = identity_field.as_ref()
946                && matches!(mode, MutationMode::Replace)
947                && before.is_none()
948            {
949                let field_name = descriptor
950                    .field_for_slot_index(identity_field.field_slot)
951                    .map_or("", |field| field.name());
952                return Err(InternalError::mutation_database_owned_field_explicit(
953                    entity_path,
954                    field_name,
955                ));
956            } else {
957                None
958            };
959
960            let resolved = match (mode, before.as_ref()) {
961                (MutationMode::Insert | MutationMode::Replace, None) => {
962                    resolve_insert_structural_patch_with_accepted_contract(
963                        entity_path,
964                        row_decode_contract.clone(),
965                        catalog.fingerprint(),
966                        catalog.accepted_row_constraints(),
967                        patch,
968                        write_context,
969                        identity_allocation.as_ref(),
970                    )?
971                }
972                (MutationMode::Update, Some(before)) => {
973                    resolve_update_structural_patch_with_accepted_contract(
974                        entity_path,
975                        row_decode_contract.clone(),
976                        catalog.fingerprint(),
977                        catalog.accepted_row_constraints(),
978                        before,
979                        patch,
980                        write_context,
981                    )?
982                }
983                (MutationMode::Replace, Some(before)) => {
984                    resolve_existing_replace_structural_patch_with_accepted_contract(
985                        entity_path,
986                        row_decode_contract.clone(),
987                        catalog.fingerprint(),
988                        catalog.accepted_row_constraints(),
989                        before,
990                        patch,
991                        write_context,
992                    )?
993                }
994                (MutationMode::Insert, Some(_)) | (MutationMode::Update, None) => {
995                    return Err(InternalError::executor_invariant());
996                }
997            };
998            let (after, provenance) = resolved.into_parts();
999            let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1000                after.as_raw_row(),
1001                &row_contract,
1002            )?;
1003            let data_key = match expected_key {
1004                Some(key) => {
1005                    reader.validate_primary_key(&key)?;
1006                    key
1007                }
1008                None => {
1009                    data_key_from_row(identity.entity_tag(), &row_contract, after.as_raw_row())?
1010                }
1011            };
1012            if let Some(allocation) = identity_allocation.as_ref() {
1013                validate_identity_materialization(
1014                    identity.entity_tag(),
1015                    identity_field
1016                        .as_ref()
1017                        .ok_or_else(InternalError::identity_corruption)?,
1018                    pre_key_insert
1019                        .as_ref()
1020                        .ok_or_else(InternalError::identity_corruption)?,
1021                    allocation,
1022                    &data_key,
1023                    &reader,
1024                )?;
1025            }
1026            if matches!(mode, MutationMode::Insert)
1027                && validated_existing_row(store, &data_key, &row_contract)?.is_some()
1028            {
1029                return Err(insert_key_exists_after_generation(
1030                    identity_allocation.is_some(),
1031                ));
1032            }
1033            let raw_key = data_key.to_raw()?;
1034            let canonical_before = before
1035                .as_ref()
1036                .map(|before| {
1037                    canonical_row_from_raw_row_with_accepted_decode_contract(
1038                        entity_path,
1039                        row_decode_contract.clone(),
1040                        before,
1041                    )
1042                })
1043                .transpose()?;
1044            let logical_changed = canonical_before.as_ref().is_none_or(|before| {
1045                before.as_raw_row().as_bytes() != after.as_raw_row().as_bytes()
1046            });
1047            let physical_changed = before
1048                .as_ref()
1049                .is_none_or(|before| before.as_bytes() != after.as_raw_row().as_bytes());
1050            add_structural_mutation_staged_bytes(
1051                &mut staged_bytes,
1052                [
1053                    raw_key.as_bytes().len(),
1054                    canonical_before
1055                        .as_ref()
1056                        .map_or(0, |before| before.as_raw_row().as_bytes().len()),
1057                    after.as_raw_row().as_bytes().len(),
1058                ],
1059            )?;
1060            let row_op = physical_changed.then(|| {
1061                CommitRowOp::new(
1062                    entity_path,
1063                    raw_key.clone(),
1064                    canonical_before
1065                        .as_ref()
1066                        .map(|before| before.as_raw_row().as_bytes().to_vec()),
1067                    Some(after.as_raw_row().as_bytes().to_vec()),
1068                    catalog.fingerprint(),
1069                )
1070            });
1071            scheduler.schedule_save_after_image(
1072                mode,
1073                &data_key,
1074                after.as_raw_row(),
1075                provenance.as_slice(),
1076                row_op,
1077            )?;
1078            if physical_changed {
1079                #[cfg(feature = "sql")]
1080                if largest_journaled_prefix
1081                    && !crate::db::commit::journaled_row_ops_fit_commit_window(scheduler.rows())
1082                {
1083                    scheduler.pop_last_save_row()?;
1084                    if output.is_empty() {
1085                        return Err(InternalError::query_sql_write_boundary(
1086                            icydb_diagnostic_code::SqlWriteBoundaryCode::ResumableUpdateSingleRowResourceExceeded,
1087                        ));
1088                    }
1089                    break;
1090                }
1091            }
1092
1093            let mut values = Vec::with_capacity(descriptor.fields().len());
1094            for field in descriptor.fields() {
1095                values.push(
1096                    reader
1097                        .required_cached_value(usize::from(field.slot().get()))?
1098                        .clone(),
1099                );
1100            }
1101            output.push(AcceptedStructuralMutationRow {
1102                values,
1103                logical_changed,
1104            });
1105        }
1106
1107        #[cfg(not(feature = "sql"))]
1108        let _ = largest_journaled_prefix;
1109
1110        let batch = scheduler.finish();
1111        let prepared = precommit_preparation(output)?;
1112        let identity_ranges = identity_cursor
1113            .map(IdentityStatementCursor::into_range_advance)
1114            .transpose()?
1115            .into_iter()
1116            .flatten()
1117            .collect::<Vec<_>>();
1118        if batch.is_empty() && !identity_ranges.is_empty() {
1119            return Err(InternalError::identity_corruption());
1120        }
1121        if !batch.is_empty() {
1122            commit_structural_row_ops_with_window_for_path(
1123                &self.db,
1124                entity_path,
1125                batch,
1126                identity_ranges,
1127                "accepted_structural_batch_apply",
1128            )?;
1129        }
1130        Ok(prepared)
1131    }
1132
1133    fn execute_one_accepted_save_mutation(
1134        &self,
1135        catalog: &AcceptedSchemaCatalogContext,
1136        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1137        mode: MutationMode,
1138        target: AcceptedStructuralMutationTarget,
1139        patch: AcceptedMutationIntentPatch,
1140    ) -> Result<DynamicMutationResult, InternalError> {
1141        let identity = catalog.identity();
1142        let entity_path = identity.entity_path();
1143        let result = self.execute_accepted_structural_save_batch(
1144            catalog,
1145            descriptor,
1146            vec![AcceptedStructuralMutation::save(mode, target, patch)],
1147            Timestamp::now(),
1148            |rows| prepare_dynamic_mutation_result(catalog, descriptor, rows, false),
1149        )?;
1150        record(MetricsEvent::SaveMutation {
1151            entity_path: entity_path.into(),
1152            kind: match mode {
1153                MutationMode::Insert => SaveMutationKind::Insert,
1154                MutationMode::Replace => SaveMutationKind::Replace,
1155                MutationMode::Update => SaveMutationKind::Update,
1156            },
1157            rows_touched: u64::from(result.affected_rows),
1158        });
1159        Ok(result)
1160    }
1161
1162    /// Execute one trusted entity-name-driven structural mutation.
1163    ///
1164    /// This lane resolves public values, defaults, generation, management,
1165    /// constraints, relations, and commit preparation from accepted schema.
1166    /// It never materializes a generated entity or invokes application
1167    /// validators/normalizers.
1168    pub fn execute_trusted_dynamic_mutation(
1169        &self,
1170        request: &DynamicMutation,
1171    ) -> Result<DynamicMutationResult, InternalError> {
1172        self.execute_trusted_dynamic_mutation_batch_with_result_policy(vec![request.clone()], false)
1173    }
1174
1175    /// Execute one bounded same-entity structural mutation batch atomically.
1176    ///
1177    /// Every item binds to the same accepted catalog identity, shares one
1178    /// operation timestamp, and is projected to its public result before the
1179    /// commit marker can be published.
1180    pub fn execute_trusted_dynamic_mutation_batch(
1181        &self,
1182        requests: Vec<DynamicMutation>,
1183    ) -> Result<DynamicMutationResult, InternalError> {
1184        self.execute_trusted_dynamic_mutation_batch_with_result_policy(requests, true)
1185    }
1186
1187    fn execute_trusted_dynamic_mutation_batch_with_result_policy(
1188        &self,
1189        requests: Vec<DynamicMutation>,
1190        enforce_mixed_batch_result_bound: bool,
1191    ) -> Result<DynamicMutationResult, InternalError> {
1192        if requests.is_empty() {
1193            return Err(InternalError::mutation_batch_empty());
1194        }
1195        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1196            return Err(InternalError::mutation_batch_too_many_items());
1197        }
1198        let first = requests
1199            .first()
1200            .ok_or_else(InternalError::mutation_batch_empty)?;
1201        if first.entity().is_empty() {
1202            return Err(InternalError::executor_unsupported());
1203        }
1204        let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
1205        let accepted_identity = catalog.identity();
1206        let descriptor =
1207            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1208        let mut mutations = Vec::with_capacity(requests.len());
1209        let mut save_kinds = Vec::with_capacity(requests.len());
1210
1211        for request in &requests {
1212            if request.entity().is_empty() {
1213                return Err(InternalError::executor_unsupported());
1214            }
1215            let item_catalog =
1216                self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
1217            if item_catalog.identity() != accepted_identity {
1218                return Err(InternalError::mutation_batch_entity_mismatch());
1219            }
1220            let (mutation, save_kind) = lower_dynamic_mutation_intent(
1221                accepted_identity.entity_tag(),
1222                accepted_identity.entity_path(),
1223                &descriptor,
1224                request,
1225            )?;
1226            mutations.push(mutation);
1227            save_kinds.push(save_kind);
1228        }
1229
1230        let entity_path = accepted_identity.entity_path_handle();
1231        let (result, metrics) = self.execute_accepted_structural_mutation_batch_inner(
1232            &catalog,
1233            &descriptor,
1234            mutations,
1235            Timestamp::now(),
1236            false,
1237            |rows| {
1238                if rows.len() != save_kinds.len() {
1239                    return Err(InternalError::executor_invariant());
1240                }
1241                let metrics = rows
1242                    .iter()
1243                    .zip(save_kinds)
1244                    .filter_map(|(row, kind)| kind.map(|kind| (kind, row.logical_changed())))
1245                    .collect::<Vec<_>>();
1246                let result = prepare_dynamic_mutation_result(
1247                    &catalog,
1248                    &descriptor,
1249                    rows,
1250                    enforce_mixed_batch_result_bound,
1251                )?;
1252                Ok((result, metrics))
1253            },
1254        )?;
1255        for (kind, logical_changed) in metrics {
1256            record(MetricsEvent::SaveMutation {
1257                entity_path: entity_path.clone(),
1258                kind,
1259                rows_touched: u64::from(logical_changed),
1260            });
1261        }
1262        Ok(result)
1263    }
1264
1265    /// Execute one generated typed write through immutable accepted entity and
1266    /// field identities. `None` means the opaque binding is stale.
1267    #[doc(hidden)]
1268    pub fn execute_trusted_typed_mutation(
1269        &self,
1270        binding: &DynamicTypedEntityBinding,
1271        request: &DynamicTypedMutation,
1272    ) -> Result<Option<DynamicMutationResult>, InternalError> {
1273        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
1274            return Ok(None);
1275        };
1276        let identity = catalog.identity();
1277        let descriptor =
1278            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1279        let mode = dynamic_typed_mutation_mode(request);
1280        let (target, patch) = match request {
1281            DynamicTypedMutation::Insert { patch } => (
1282                AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1283                patch,
1284            ),
1285            DynamicTypedMutation::Update { key, patch }
1286            | DynamicTypedMutation::Replace { key, patch } => (
1287                AcceptedStructuralMutationTarget::expected(dynamic_key(
1288                    identity.entity_tag(),
1289                    key,
1290                )?),
1291                patch,
1292            ),
1293        };
1294        if !patch.is_bound_to(binding) {
1295            return Ok(None);
1296        }
1297        let patch = lower_typed_patch(identity.entity_path(), &descriptor, patch, mode)?;
1298        self.execute_one_accepted_save_mutation(&catalog, &descriptor, mode, target, patch)
1299            .map(Some)
1300    }
1301
1302    /// Execute one trusted atomic insert batch from entity-name-driven patches.
1303    ///
1304    /// Every patch is lowered against the same accepted snapshot and shares
1305    /// one operation timestamp before the canonical structural batch owner
1306    /// stages any durable effect.
1307    pub fn execute_trusted_dynamic_insert_batch(
1308        &self,
1309        entity: &str,
1310        patches: Vec<DynamicStructuralPatch>,
1311    ) -> Result<DynamicMutationResult, InternalError> {
1312        let mutations = patches
1313            .into_iter()
1314            .map(|patch| DynamicMutation::Insert {
1315                entity: entity.to_string(),
1316                patch,
1317            })
1318            .collect();
1319        self.execute_trusted_dynamic_mutation_batch_with_result_policy(mutations, false)
1320    }
1321}
1322
1323#[cfg(test)]
1324mod typed_adapter_tests {
1325    use super::{
1326        AcceptedFieldKind, DbSession, DynamicTypedBindingError, DynamicTypedFieldBindingRequest,
1327        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell, dynamic_typed_field_type,
1328        typed_adapter_field_kind_matches,
1329    };
1330    use crate::{
1331        db::{
1332            data::DataStore,
1333            index::IndexStore,
1334            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1335            schema::{
1336                AcceptedSchemaRevision, FieldId, FieldStorageDecode, LeafCodec,
1337                PersistedFieldSnapshot, PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot,
1338                SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
1339                accepted_schema_candidate_with_field_bindings_for_tests,
1340            },
1341        },
1342        traits::{CanisterKind, Path},
1343        types::EntityTag,
1344        value::InputValue,
1345    };
1346    use icydb_schema::{EntitySourceKey, FieldSourceKey, ScalarType};
1347    use std::{cell::RefCell, collections::BTreeMap};
1348
1349    const STORE_PATH: &str = "session::write::typed_adapter_tests::Store";
1350    const ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::Entity";
1351    const OTHER_ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity";
1352    const ID_SOURCE: &str = "session::write::typed_adapter_tests::Entity::id";
1353    const VALUE_SOURCE: &str = "session::write::typed_adapter_tests::Entity::value";
1354    const REPLACEMENT_SOURCE: &str =
1355        "session::write::typed_adapter_tests::Entity::replacement_value";
1356    const OTHER_ID_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity::id";
1357
1358    struct TestCanister;
1359
1360    impl Path for TestCanister {
1361        const PATH: &'static str = "session::write::typed_adapter_tests::Canister";
1362    }
1363
1364    impl CanisterKind for TestCanister {
1365        const COMMIT_MEMORY_ID: u8 = 41;
1366        const COMMIT_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.commit.v1";
1367        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 42;
1368        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1369            "icydb.typed_adapter_tests.integrity.progress.v1";
1370    }
1371
1372    thread_local! {
1373        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1374        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1375        static SCHEMA_STORE: RefCell<SchemaStore> =
1376            const { RefCell::new(SchemaStore::init_heap()) };
1377        static STORE_REGISTRY: StoreRegistry = {
1378            let mut registry = StoreRegistry::new();
1379            registry.register_store(
1380                STORE_PATH,
1381                &DATA_STORE,
1382                &INDEX_STORE,
1383                &SCHEMA_STORE,
1384                StoreAllocationIdentities::absent(),
1385                StoreRuntimeStorageCapabilities::heap(),
1386            ).expect("typed adapter test store should register");
1387            registry
1388        };
1389    }
1390
1391    fn nat64_field(id: u32, name: &str, slot: u16) -> PersistedFieldSnapshot {
1392        PersistedFieldSnapshot::new_initial(
1393            FieldId::new(id),
1394            name.to_string(),
1395            SchemaFieldSlot::new(slot),
1396            AcceptedFieldKind::Nat64,
1397            Vec::new(),
1398            false,
1399            SchemaInsertDefault::None,
1400            FieldStorageDecode::ByKind,
1401            LeafCodec::Scalar(ScalarCodec::Nat64),
1402        )
1403    }
1404
1405    fn snapshot(
1406        entity_source: &str,
1407        entity_name: &str,
1408        fields: Vec<PersistedFieldSnapshot>,
1409    ) -> PersistedSchemaSnapshot {
1410        let layout = SchemaRowLayout::initial(
1411            fields
1412                .iter()
1413                .map(|field| (field.id(), field.slot()))
1414                .collect(),
1415        );
1416        PersistedSchemaSnapshot::new(
1417            SchemaVersion::initial(),
1418            entity_source.to_string(),
1419            entity_name.to_string(),
1420            FieldId::new(1),
1421            layout,
1422            fields,
1423        )
1424    }
1425
1426    fn field_source(source: &str) -> FieldSourceKey {
1427        FieldSourceKey::try_new(source).expect("typed field source should admit")
1428    }
1429
1430    fn entity_source(source: &str) -> EntitySourceKey {
1431        EntitySourceKey::try_new(source).expect("typed entity source should admit")
1432    }
1433
1434    fn publish(
1435        session: &DbSession<TestCanister>,
1436        expected: AcceptedSchemaRevision,
1437        revision: AcceptedSchemaRevision,
1438        snapshots: BTreeMap<EntityTag, PersistedSchemaSnapshot>,
1439        fields: BTreeMap<(EntityTag, FieldSourceKey), FieldId>,
1440    ) {
1441        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
1442            STORE_PATH, revision, snapshots, fields,
1443        );
1444        let store = session
1445            .db
1446            .store_handle(STORE_PATH)
1447            .expect("typed adapter test store should resolve");
1448        crate::db::commit::publish_accepted_schema_candidate(
1449            STORE_PATH, store, expected, &candidate,
1450        )
1451        .expect("typed binding candidate should publish");
1452    }
1453
1454    fn request(source: &str) -> DynamicTypedFieldBindingRequest {
1455        DynamicTypedFieldBindingRequest::new(
1456            source.to_string(),
1457            DynamicTypedFieldType::Scalar(ScalarType::Nat64),
1458            false,
1459        )
1460    }
1461
1462    #[test]
1463    fn typed_adapter_kind_matching_is_exact_but_accepts_relation_key_wrappers() {
1464        let relation = AcceptedFieldKind::Relation {
1465            target_path: "test::Target".to_string(),
1466            target_entity_name: "Target".to_string(),
1467            target_entity_tag: EntityTag::new(7),
1468            target_store_path: "test::Store".to_string(),
1469            key_kind: Box::new(AcceptedFieldKind::Nat64),
1470        };
1471
1472        assert!(typed_adapter_field_kind_matches(
1473            &relation,
1474            &AcceptedFieldKind::Nat64,
1475        ));
1476        assert!(typed_adapter_field_kind_matches(
1477            &AcceptedFieldKind::List(Box::new(relation)),
1478            &AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Nat64)),
1479        ));
1480        assert!(!typed_adapter_field_kind_matches(
1481            &AcceptedFieldKind::Nat64,
1482            &AcceptedFieldKind::Nat32,
1483        ));
1484    }
1485
1486    #[test]
1487    fn typed_adapter_field_contract_rejects_invalid_named_source_identity() {
1488        assert!(matches!(
1489            dynamic_typed_field_type(DynamicTypedFieldType::Named(String::new())),
1490            Err(DynamicTypedBindingError::FieldUnavailable),
1491        ));
1492        assert!(matches!(
1493            dynamic_typed_field_type(DynamicTypedFieldType::Scalar(ScalarType::Nat16)),
1494            Ok(icydb_schema::FieldType::Scalar(ScalarType::Nat16)),
1495        ));
1496    }
1497
1498    // Keep the full rename, stale-binding, and old-name-reuse lifecycle in one
1499    // regression so each issued binding is checked against the next revision.
1500    #[expect(clippy::too_many_lines)]
1501    #[test]
1502    fn typed_binding_uses_accepted_ids_and_slots_across_renames_and_name_reuse() {
1503        let entity_tag = EntityTag::new(91);
1504        let other_entity_tag = EntityTag::new(92);
1505        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1506        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1507        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1508
1509        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
1510        session
1511            .db
1512            .ensure_recovered_state()
1513            .expect("typed adapter test database should initialize");
1514        publish(
1515            &session,
1516            AcceptedSchemaRevision::NONE,
1517            AcceptedSchemaRevision::INITIAL,
1518            BTreeMap::from([(
1519                entity_tag,
1520                snapshot(
1521                    ENTITY_SOURCE,
1522                    "Entity",
1523                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1524                ),
1525            )]),
1526            BTreeMap::from([
1527                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1528                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1529            ]),
1530        );
1531
1532        let initial_catalog = session
1533            .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1534            .expect("initial source catalog lookup should inspect")
1535            .expect("initial source catalog should exist");
1536        assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1537        let initial = session
1538            .issue_typed_entity_binding(
1539                entity_source(ENTITY_SOURCE).as_str(),
1540                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1541            )
1542            .expect("initial typed binding should issue");
1543        assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1544        assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1545        assert_eq!(initial.output_field_slot("value"), Some(1));
1546        let initial_patch = initial
1547            .bind_write_fields(vec![(
1548                VALUE_SOURCE.to_string(),
1549                DynamicWriteCell::Value(InputValue::Nat64(7)),
1550            )])
1551            .expect("source-bound patch should lower");
1552        assert_eq!(
1553            initial_patch.fields(),
1554            &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1555        );
1556
1557        publish(
1558            &session,
1559            AcceptedSchemaRevision::INITIAL,
1560            AcceptedSchemaRevision::new(2),
1561            BTreeMap::from([
1562                (
1563                    entity_tag,
1564                    snapshot(
1565                        ENTITY_SOURCE,
1566                        "RenamedEntity",
1567                        vec![
1568                            nat64_field(1, "id", 0),
1569                            nat64_field(2, "renamed_value", 1),
1570                            nat64_field(3, "value", 2),
1571                        ],
1572                    ),
1573                ),
1574                (
1575                    other_entity_tag,
1576                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1577                ),
1578            ]),
1579            BTreeMap::from([
1580                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1581                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1582                (
1583                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1584                    FieldId::new(3),
1585                ),
1586                (
1587                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1588                    FieldId::new(1),
1589                ),
1590            ]),
1591        );
1592
1593        assert!(
1594            !session
1595                .typed_entity_binding_is_current(&initial)
1596                .expect("renamed binding currentness should inspect")
1597        );
1598        let renamed = session
1599            .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1600            .expect("renamed source-bound adapter should rebind");
1601        assert_eq!(renamed.entity(), "RenamedEntity");
1602        assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1603        assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1604        assert_eq!(renamed.output_field_slot("value"), None);
1605
1606        publish(
1607            &session,
1608            AcceptedSchemaRevision::new(2),
1609            AcceptedSchemaRevision::new(3),
1610            BTreeMap::from([
1611                (
1612                    entity_tag,
1613                    snapshot(
1614                        ENTITY_SOURCE,
1615                        "RenamedEntity",
1616                        vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1617                    ),
1618                ),
1619                (
1620                    other_entity_tag,
1621                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1622                ),
1623            ]),
1624            BTreeMap::from([
1625                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1626                (
1627                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1628                    FieldId::new(2),
1629                ),
1630                (
1631                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1632                    FieldId::new(1),
1633                ),
1634            ]),
1635        );
1636
1637        assert!(matches!(
1638            session.issue_typed_entity_binding(
1639                ENTITY_SOURCE,
1640                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1641            ),
1642            Err(DynamicTypedBindingError::FieldUnavailable),
1643        ));
1644        assert!(
1645            !session
1646                .typed_entity_binding_is_current(&renamed)
1647                .expect("removed source binding should become stale")
1648        );
1649
1650        let replacement = session
1651            .issue_typed_entity_binding(
1652                ENTITY_SOURCE,
1653                &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
1654            )
1655            .expect("explicit replacement source should bind");
1656        assert!(
1657            session
1658                .execute_trusted_typed_mutation(
1659                    &replacement,
1660                    &DynamicTypedMutation::Insert {
1661                        patch: initial_patch
1662                    },
1663                )
1664                .expect("cross-binding patch should fail closed")
1665                .is_none()
1666        );
1667        let patch = replacement
1668            .bind_write_fields(vec![
1669                (
1670                    ID_SOURCE.to_string(),
1671                    DynamicWriteCell::Value(InputValue::Nat64(1)),
1672                ),
1673                (
1674                    REPLACEMENT_SOURCE.to_string(),
1675                    DynamicWriteCell::Value(InputValue::Nat64(9)),
1676                ),
1677            ])
1678            .expect("replacement source write should bind by accepted IDs and slots");
1679        let result = session
1680            .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
1681            .expect("typed insert should use the accepted mutation pipeline")
1682            .expect("replacement binding should remain current");
1683        assert_eq!(result.entity, "RenamedEntity");
1684        assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1685        assert_eq!(
1686            result.rows,
1687            vec![vec![
1688                crate::value::OutputValue::Nat64(1),
1689                crate::value::OutputValue::Nat64(9)
1690            ]]
1691        );
1692        assert_eq!(result.affected_rows, 1);
1693
1694        #[cfg(feature = "query")]
1695        {
1696            let query = crate::db::DynamicQuery::new("RenamedEntity")
1697                .select(["id", "value"])
1698                .order_by(crate::db::asc("id"))
1699                .limit(1);
1700            let result = session
1701                .execute_trusted_dynamic_query(&query)
1702                .expect("query-only dynamic execution should use accepted authority");
1703            assert_eq!(result.entity, "RenamedEntity");
1704            assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1705            assert_eq!(
1706                result.rows,
1707                vec![vec![
1708                    crate::value::OutputValue::Nat64(1),
1709                    crate::value::OutputValue::Nat64(9)
1710                ]]
1711            );
1712            assert_eq!(result.row_count, 1);
1713        }
1714    }
1715}
1716
1717#[cfg(test)]
1718mod mixed_relation_batch_tests {
1719    use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
1720    use crate::{
1721        db::{
1722            data::DataStore,
1723            index::IndexStore,
1724            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1725            schema::{
1726                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
1727                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
1728                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
1729                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
1730                SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
1731                SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
1732            },
1733        },
1734        error::{ConstraintDiagnosticKind, ErrorClass},
1735        traits::{CanisterKind, Path},
1736        types::EntityTag,
1737        value::{InputValue, OutputValue},
1738    };
1739    use icydb_schema::FieldSourceKey;
1740    use std::{cell::RefCell, collections::BTreeMap};
1741
1742    const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
1743    const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
1744    const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
1745    const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
1746    const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
1747    const ENTITY_NAME: &str = "MixedRelationNode";
1748    const ENTITY_TAG: EntityTag = EntityTag::new(94);
1749    const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
1750    const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
1751    const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
1752    const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
1753    const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
1754
1755    struct TestCanister;
1756
1757    impl Path for TestCanister {
1758        const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
1759    }
1760
1761    impl CanisterKind for TestCanister {
1762        const COMMIT_MEMORY_ID: u8 = 47;
1763        const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
1764        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
1765        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1766            "icydb.mixed_relation_batch_tests.integrity.progress.v1";
1767    }
1768
1769    thread_local! {
1770        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1771        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1772        static SCHEMA_STORE: RefCell<SchemaStore> =
1773            const { RefCell::new(SchemaStore::init_heap()) };
1774        static STORE_REGISTRY: StoreRegistry = {
1775            let mut registry = StoreRegistry::new();
1776            registry.register_store(
1777                STORE_PATH,
1778                &DATA_STORE,
1779                &INDEX_STORE,
1780                &SCHEMA_STORE,
1781                StoreAllocationIdentities::absent(),
1782                StoreRuntimeStorageCapabilities::heap(),
1783            ).expect("mixed relation test store should register");
1784            registry
1785        };
1786    }
1787
1788    fn source_key(source: &str) -> FieldSourceKey {
1789        FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
1790    }
1791
1792    fn relation_snapshot() -> PersistedSchemaSnapshot {
1793        let fields = vec![
1794            PersistedFieldSnapshot::new_initial(
1795                FieldId::new(1),
1796                "id".to_string(),
1797                SchemaFieldSlot::new(0),
1798                AcceptedFieldKind::Nat64,
1799                Vec::new(),
1800                false,
1801                SchemaInsertDefault::None,
1802                FieldStorageDecode::ByKind,
1803                LeafCodec::Scalar(ScalarCodec::Nat64),
1804            ),
1805            PersistedFieldSnapshot::new_initial(
1806                FieldId::new(2),
1807                "parent_id".to_string(),
1808                SchemaFieldSlot::new(1),
1809                AcceptedFieldKind::Relation {
1810                    target_path: ENTITY_SOURCE.to_string(),
1811                    target_entity_name: ENTITY_NAME.to_string(),
1812                    target_entity_tag: ENTITY_TAG,
1813                    target_store_path: STORE_PATH.to_string(),
1814                    key_kind: Box::new(AcceptedFieldKind::Nat64),
1815                },
1816                Vec::new(),
1817                true,
1818                SchemaInsertDefault::None,
1819                FieldStorageDecode::ByKind,
1820                LeafCodec::Scalar(ScalarCodec::Nat64),
1821            ),
1822            PersistedFieldSnapshot::new_initial(
1823                FieldId::new(3),
1824                "code".to_string(),
1825                SchemaFieldSlot::new(2),
1826                AcceptedFieldKind::Nat64,
1827                Vec::new(),
1828                false,
1829                SchemaInsertDefault::None,
1830                FieldStorageDecode::ByKind,
1831                LeafCodec::Scalar(ScalarCodec::Nat64),
1832            ),
1833        ];
1834        let relation = PersistedRelationEdgeSnapshot::new(
1835            RelationId::new(1).expect("mixed relation identity should be non-zero"),
1836            "parent".to_string(),
1837            ENTITY_SOURCE.to_string(),
1838            vec![FieldId::new(2)],
1839        );
1840        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
1841            SchemaVersion::initial(),
1842            ENTITY_SOURCE.to_string(),
1843            ENTITY_NAME.to_string(),
1844            FieldId::new(1),
1845            SchemaRowLayout::initial(
1846                fields
1847                    .iter()
1848                    .map(|field| (field.id(), field.slot()))
1849                    .collect(),
1850            ),
1851            fields,
1852            vec![PersistedIndexSnapshot::new(
1853                SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
1854                1,
1855                "by_code".to_string(),
1856                STORE_PATH.to_string(),
1857                true,
1858                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
1859                    FieldId::new(3),
1860                    SchemaFieldSlot::new(2),
1861                    vec!["code".to_string()],
1862                    AcceptedFieldKind::Nat64,
1863                    false,
1864                )]),
1865                None,
1866            )],
1867        )
1868        .with_relations(vec![relation]);
1869        let constraints = AcceptedConstraintCatalog::initial(
1870            snapshot.fields(),
1871            snapshot.indexes(),
1872            snapshot.relations(),
1873        )
1874        .expect("mixed relation constraints should close");
1875        snapshot.with_constraint_catalog(constraints)
1876    }
1877
1878    fn other_snapshot() -> PersistedSchemaSnapshot {
1879        let fields = vec![
1880            PersistedFieldSnapshot::new_initial(
1881                FieldId::new(1),
1882                "id".to_string(),
1883                SchemaFieldSlot::new(0),
1884                AcceptedFieldKind::Nat64,
1885                Vec::new(),
1886                false,
1887                SchemaInsertDefault::None,
1888                FieldStorageDecode::ByKind,
1889                LeafCodec::Scalar(ScalarCodec::Nat64),
1890            ),
1891            PersistedFieldSnapshot::new_initial(
1892                FieldId::new(2),
1893                "value".to_string(),
1894                SchemaFieldSlot::new(1),
1895                AcceptedFieldKind::Nat64,
1896                Vec::new(),
1897                false,
1898                SchemaInsertDefault::None,
1899                FieldStorageDecode::ByKind,
1900                LeafCodec::Scalar(ScalarCodec::Nat64),
1901            ),
1902        ];
1903        PersistedSchemaSnapshot::new(
1904            SchemaVersion::initial(),
1905            OTHER_ENTITY_SOURCE.to_string(),
1906            OTHER_ENTITY_NAME.to_string(),
1907            FieldId::new(1),
1908            SchemaRowLayout::initial(
1909                fields
1910                    .iter()
1911                    .map(|field| (field.id(), field.slot()))
1912                    .collect(),
1913            ),
1914            fields,
1915        )
1916    }
1917
1918    fn initialize() -> DbSession<TestCanister> {
1919        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1920        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1921        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1922        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
1923        session
1924            .db
1925            .ensure_recovered_state()
1926            .expect("mixed relation database should initialize");
1927        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
1928            STORE_PATH,
1929            AcceptedSchemaRevision::INITIAL,
1930            BTreeMap::from([
1931                (ENTITY_TAG, relation_snapshot()),
1932                (OTHER_ENTITY_TAG, other_snapshot()),
1933            ]),
1934            BTreeMap::from([
1935                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
1936                ((ENTITY_TAG, source_key(PARENT_SOURCE)), FieldId::new(2)),
1937                ((ENTITY_TAG, source_key(CODE_SOURCE)), FieldId::new(3)),
1938                (
1939                    (OTHER_ENTITY_TAG, source_key(OTHER_ID_SOURCE)),
1940                    FieldId::new(1),
1941                ),
1942                (
1943                    (OTHER_ENTITY_TAG, source_key(OTHER_VALUE_SOURCE)),
1944                    FieldId::new(2),
1945                ),
1946            ]),
1947        );
1948        let store = session
1949            .db
1950            .store_handle(STORE_PATH)
1951            .expect("mixed relation store should resolve");
1952        crate::db::commit::publish_accepted_schema_candidate(
1953            STORE_PATH,
1954            store,
1955            AcceptedSchemaRevision::NONE,
1956            &candidate,
1957        )
1958        .expect("mixed relation candidate should publish");
1959        session
1960    }
1961
1962    fn patch(id: Option<u64>, parent: Option<u64>, code: Option<u64>) -> DynamicStructuralPatch {
1963        let mut fields = Vec::new();
1964        if let Some(id) = id {
1965            fields.push((
1966                "id".to_string(),
1967                DynamicWriteCell::Value(InputValue::Nat64(id)),
1968            ));
1969        }
1970        fields.push((
1971            "parent_id".to_string(),
1972            parent.map_or(DynamicWriteCell::Null, |parent| {
1973                DynamicWriteCell::Value(InputValue::Nat64(parent))
1974            }),
1975        ));
1976        if let Some(code) = code {
1977            fields.push((
1978                "code".to_string(),
1979                DynamicWriteCell::Value(InputValue::Nat64(code)),
1980            ));
1981        }
1982        DynamicStructuralPatch::new(fields)
1983    }
1984
1985    fn insert(id: u64, parent: Option<u64>) -> DynamicMutation {
1986        insert_with_code(id, parent, id)
1987    }
1988
1989    fn insert_with_code(id: u64, parent: Option<u64>, code: u64) -> DynamicMutation {
1990        DynamicMutation::Insert {
1991            entity: ENTITY_NAME.to_string(),
1992            patch: patch(Some(id), parent, Some(code)),
1993        }
1994    }
1995
1996    fn update_parent(id: u64, parent: Option<u64>) -> DynamicMutation {
1997        DynamicMutation::Update {
1998            entity: ENTITY_NAME.to_string(),
1999            key: InputValue::Nat64(id),
2000            patch: patch(None, parent, None),
2001        }
2002    }
2003
2004    fn update_code(id: u64, code: u64) -> DynamicMutation {
2005        DynamicMutation::Update {
2006            entity: ENTITY_NAME.to_string(),
2007            key: InputValue::Nat64(id),
2008            patch: DynamicStructuralPatch::new(vec![(
2009                "code".to_string(),
2010                DynamicWriteCell::Value(InputValue::Nat64(code)),
2011            )]),
2012        }
2013    }
2014
2015    fn delete(id: u64) -> DynamicMutation {
2016        DynamicMutation::Delete {
2017            entity: ENTITY_NAME.to_string(),
2018            key: InputValue::Nat64(id),
2019        }
2020    }
2021
2022    fn expected_row(id: u64, parent: Option<u64>) -> Vec<OutputValue> {
2023        expected_row_with_code(id, parent, id)
2024    }
2025
2026    fn expected_row_with_code(id: u64, parent: Option<u64>, code: u64) -> Vec<OutputValue> {
2027        vec![
2028            OutputValue::Nat64(id),
2029            parent.map_or(OutputValue::Null, OutputValue::Nat64),
2030            OutputValue::Nat64(code),
2031        ]
2032    }
2033
2034    fn other_patch(id: Option<u64>, value: u64) -> DynamicStructuralPatch {
2035        let mut fields = Vec::new();
2036        if let Some(id) = id {
2037            fields.push((
2038                "id".to_string(),
2039                DynamicWriteCell::Value(InputValue::Nat64(id)),
2040            ));
2041        }
2042        fields.push((
2043            "value".to_string(),
2044            DynamicWriteCell::Value(InputValue::Nat64(value)),
2045        ));
2046        DynamicStructuralPatch::new(fields)
2047    }
2048
2049    fn assert_relation_violation(error: &crate::error::InternalError) {
2050        let diagnostic = error
2051            .constraint_diagnostic()
2052            .expect("relation violations should retain their accepted constraint");
2053        assert_eq!(
2054            diagnostic.constraint_kind(),
2055            ConstraintDiagnosticKind::Relation,
2056        );
2057    }
2058
2059    #[test]
2060    fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
2061        let session = initialize();
2062        session
2063            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
2064            .expect("the initial relation should commit");
2065
2066        let blocked = session
2067            .execute_trusted_dynamic_mutation(&delete(1))
2068            .expect_err("an unaffected committed source must block target deletion");
2069        assert_relation_violation(&blocked);
2070
2071        let deleted = session
2072            .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
2073            .expect("a source and its target should delete atomically");
2074        assert_eq!(
2075            deleted.rows,
2076            vec![expected_row(2, Some(1)), expected_row(1, None)],
2077        );
2078
2079        session
2080            .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
2081            .expect("the update-away fixture should commit");
2082        let updated_away = session
2083            .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
2084            .expect("an updated final source may release a deleted target");
2085        assert_eq!(
2086            updated_away.rows,
2087            vec![expected_row(4, None), expected_row(3, None)],
2088        );
2089
2090        session
2091            .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
2092            .expect("the retained-reference fixture should commit");
2093        let retained = session
2094            .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
2095            .expect_err("a final updated source must still block target deletion");
2096        assert_relation_violation(&retained);
2097
2098        session
2099            .execute_trusted_dynamic_mutation(&insert(7, None))
2100            .expect("the inserted-reference fixture target should commit");
2101        let inserted_reference = session
2102            .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
2103            .expect_err("a final inserted source must not reference a deleted target");
2104        assert_relation_violation(&inserted_reference);
2105
2106        let inserted_target = session
2107            .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
2108            .expect("an inserted relation should see its batch-final target");
2109        assert_eq!(
2110            inserted_target.rows,
2111            vec![expected_row(10, Some(9)), expected_row(9, None)],
2112        );
2113
2114        session
2115            .execute_trusted_dynamic_mutation(&insert(11, None))
2116            .expect("the updated-reference fixture source should commit");
2117        let updated_target = session
2118            .execute_trusted_dynamic_mutation_batch(vec![
2119                update_parent(11, Some(12)),
2120                insert(12, None),
2121            ])
2122            .expect("an updated relation should see its batch-final target");
2123        assert_eq!(
2124            updated_target.rows,
2125            vec![expected_row(11, Some(12)), expected_row(12, None)],
2126        );
2127    }
2128
2129    #[test]
2130    fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
2131        let session = initialize();
2132        session
2133            .execute_trusted_dynamic_mutation(&insert(1, None))
2134            .expect("the primary mixed fixture row should commit");
2135        session
2136            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2137                entity: OTHER_ENTITY_NAME.to_string(),
2138                patch: other_patch(Some(1), 10),
2139            })
2140            .expect("the secondary mixed fixture row should commit");
2141
2142        let mixed_entity = session
2143            .execute_trusted_dynamic_mutation_batch(vec![
2144                update_code(1, 11),
2145                DynamicMutation::Update {
2146                    entity: OTHER_ENTITY_NAME.to_string(),
2147                    key: InputValue::Nat64(1),
2148                    patch: other_patch(None, 11),
2149                },
2150            ])
2151            .expect_err("one atomic batch must not cross accepted entities");
2152        assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
2153
2154        let missing = session
2155            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
2156            .expect_err("a late missing delete must reject the earlier staged update");
2157        assert_eq!(missing.class(), ErrorClass::NotFound);
2158
2159        session
2160            .execute_trusted_dynamic_mutation(&insert(2, None))
2161            .expect("the collision fixture should commit");
2162        let collision = session
2163            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
2164            .expect_err("an insert collision must reject the earlier staged update");
2165        assert_eq!(collision.class(), ErrorClass::Conflict);
2166        let failures_unchanged = session
2167            .execute_trusted_dynamic_mutation(&update_code(1, 1))
2168            .expect("failed batches must preserve the original unique value");
2169        assert_eq!(failures_unchanged.affected_rows, 0);
2170
2171        let replaced = session
2172            .execute_trusted_dynamic_mutation_batch(vec![
2173                update_code(1, 14),
2174                DynamicMutation::Replace {
2175                    entity: ENTITY_NAME.to_string(),
2176                    key: InputValue::Nat64(99),
2177                    patch: patch(None, None, Some(99)),
2178                },
2179            ])
2180            .expect("ordinary caller-key replace should insert its absent final row");
2181        assert_eq!(
2182            replaced.rows,
2183            vec![
2184                expected_row_with_code(1, None, 14),
2185                expected_row_with_code(99, None, 99),
2186            ],
2187        );
2188
2189        let unchanged = session
2190            .execute_trusted_dynamic_mutation(&update_code(1, 14))
2191            .expect("the successful mixed replace must publish its preceding update");
2192        assert_eq!(unchanged.affected_rows, 0);
2193        let other_unchanged = session
2194            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2195                entity: OTHER_ENTITY_NAME.to_string(),
2196                key: InputValue::Nat64(1),
2197                patch: other_patch(None, 10),
2198            })
2199            .expect("cross-entity rejection must preserve the secondary row");
2200        assert_eq!(other_unchanged.affected_rows, 0);
2201    }
2202
2203    #[test]
2204    fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
2205        let session = initialize();
2206        session
2207            .execute_trusted_dynamic_mutation_batch(vec![
2208                insert_with_code(1, None, 10),
2209                insert_with_code(2, None, 20),
2210            ])
2211            .expect("the unique-overlay fixture should commit");
2212
2213        let swapped = session
2214            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
2215            .expect("two final rows should atomically swap unique memberships");
2216        assert_eq!(
2217            swapped.rows,
2218            vec![
2219                expected_row_with_code(1, None, 20),
2220                expected_row_with_code(2, None, 10),
2221            ],
2222        );
2223
2224        let released = session
2225            .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
2226            .expect("a delete should release unique membership to a final inserted row");
2227        assert_eq!(
2228            released.rows,
2229            vec![
2230                expected_row_with_code(1, None, 20),
2231                expected_row_with_code(3, None, 20),
2232            ],
2233        );
2234    }
2235}
2236
2237#[cfg(test)]
2238mod identity_pre_key_tests {
2239    use super::{
2240        AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
2241        AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
2242        DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
2243        DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
2244        MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
2245        add_structural_mutation_staged_bytes, checked_pre_key_candidate_count,
2246        insert_key_exists_after_generation, validate_structural_mutation_result_bytes,
2247    };
2248    use crate::{
2249        db::{
2250            commit::{database_incarnation_id, forget_recovered_domain_for_tests},
2251            data::DataStore,
2252            executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
2253            index::IndexStore,
2254            integrity::{
2255                PhysicalUnitCheckpoint, QuickIntegrityStatus, RowInspectionLimits,
2256                execute_quick_integrity, execute_row_integrity_page,
2257            },
2258            journal::JournalTailStore,
2259            registry::{
2260                StoreAllocationIdentities, StoreAllocationIdentity, StoreRegistry,
2261                StoreRuntimeStorageCapabilities,
2262            },
2263            schema::{
2264                AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
2265                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2266                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2267                PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
2268                SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2269                accepted_schema_candidate_with_field_bindings_for_tests,
2270            },
2271            write_context::MutationMode,
2272        },
2273        error::{ErrorClass, ErrorOrigin, InternalError},
2274        testing::test_memory,
2275        traits::{CanisterKind, Path},
2276        types::{EntityTag, Timestamp},
2277        value::{InputValue, OutputValue, Value},
2278    };
2279    use icydb_schema::{FieldSourceKey, ScalarType};
2280    use std::{cell::RefCell, collections::BTreeMap, time::Instant};
2281
2282    const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
2283    const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
2284    const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
2285    const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
2286    const ENTITY_NAME: &str = "IdentityRow";
2287    const ENTITY_TAG: EntityTag = EntityTag::new(93);
2288    const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
2289
2290    struct TestCanister;
2291
2292    impl Path for TestCanister {
2293        const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
2294    }
2295
2296    impl CanisterKind for TestCanister {
2297        const COMMIT_MEMORY_ID: u8 = 45;
2298        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
2299        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
2300        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2301            "icydb.identity_pre_key_tests.integrity.progress.v1";
2302    }
2303
2304    thread_local! {
2305        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2306        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2307        static SCHEMA_STORE: RefCell<SchemaStore> =
2308            const { RefCell::new(SchemaStore::init_heap()) };
2309        static STORE_REGISTRY: StoreRegistry = {
2310            let mut registry = StoreRegistry::new();
2311            registry.register_store(
2312                STORE_PATH,
2313                &DATA_STORE,
2314                &INDEX_STORE,
2315                &SCHEMA_STORE,
2316                StoreAllocationIdentities::absent(),
2317                StoreRuntimeStorageCapabilities::heap(),
2318            ).expect("identity pre-key test store should register");
2319            registry
2320        };
2321        static JOURNALED_DATA_STORE: RefCell<DataStore> =
2322            RefCell::new(DataStore::init_journaled(test_memory(186)));
2323        static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
2324            RefCell::new(IndexStore::init_journaled(test_memory(187)));
2325        static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
2326            RefCell::new(SchemaStore::init_journaled(test_memory(188)));
2327        static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
2328            RefCell::new(JournalTailStore::init(test_memory(189)));
2329        static JOURNALED_STORE_REGISTRY: StoreRegistry = {
2330            let mut registry = StoreRegistry::new();
2331            registry.register_journaled_store(
2332                JOURNALED_STORE_PATH,
2333                &JOURNALED_DATA_STORE,
2334                &JOURNALED_INDEX_STORE,
2335                &JOURNALED_SCHEMA_STORE,
2336                &JOURNALED_TAIL_STORE,
2337                StoreAllocationIdentities::new_journaled(
2338                    StoreAllocationIdentity::new(186, "icydb.test.identity-range.data.v1"),
2339                    StoreAllocationIdentity::new(187, "icydb.test.identity-range.index.v1"),
2340                    StoreAllocationIdentity::new(188, "icydb.test.identity-range.schema.v1"),
2341                    StoreAllocationIdentity::new(189, "icydb.test.identity-range.journal.v1"),
2342                ),
2343                StoreRuntimeStorageCapabilities::journaled(),
2344            ).expect("identity range journaled store should register");
2345            registry
2346        };
2347    }
2348
2349    struct JournaledTestCanister;
2350
2351    impl Path for JournaledTestCanister {
2352        const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
2353    }
2354
2355    impl CanisterKind for JournaledTestCanister {
2356        const COMMIT_MEMORY_ID: u8 = 190;
2357        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
2358        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
2359        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2360            "icydb.identity_range_tests.integrity.progress.v1";
2361    }
2362
2363    fn source_key(source: &str) -> FieldSourceKey {
2364        FieldSourceKey::try_new(source).expect("identity test field source should admit")
2365    }
2366
2367    fn identity_snapshot(store_path: &str) -> PersistedSchemaSnapshot {
2368        let fields = vec![
2369            PersistedFieldSnapshot::new_initial_with_write_policy(
2370                FieldId::new(1),
2371                "id".to_string(),
2372                SchemaFieldSlot::new(0),
2373                AcceptedFieldKind::Nat64,
2374                Vec::new(),
2375                false,
2376                SchemaInsertDefault::None,
2377                SchemaFieldWritePolicy::from_model_policies(
2378                    Some(FieldInsertGeneration::Identity),
2379                    None,
2380                ),
2381                FieldStorageDecode::ByKind,
2382                LeafCodec::Scalar(ScalarCodec::Nat64),
2383            ),
2384            PersistedFieldSnapshot::new_initial(
2385                FieldId::new(2),
2386                "payload".to_string(),
2387                SchemaFieldSlot::new(1),
2388                AcceptedFieldKind::Nat64,
2389                Vec::new(),
2390                false,
2391                SchemaInsertDefault::None,
2392                FieldStorageDecode::ByKind,
2393                LeafCodec::Scalar(ScalarCodec::Nat64),
2394            ),
2395        ];
2396        PersistedSchemaSnapshot::new_with_indexes(
2397            SchemaVersion::initial(),
2398            ENTITY_SOURCE.to_string(),
2399            ENTITY_NAME.to_string(),
2400            FieldId::new(1),
2401            SchemaRowLayout::initial(
2402                fields
2403                    .iter()
2404                    .map(|field| (field.id(), field.slot()))
2405                    .collect(),
2406            ),
2407            fields,
2408            vec![PersistedIndexSnapshot::new(
2409                SchemaIndexId::new(1).expect("identity test index ID should admit"),
2410                1,
2411                "by_payload".to_string(),
2412                store_path.to_string(),
2413                false,
2414                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2415                    FieldId::new(2),
2416                    SchemaFieldSlot::new(1),
2417                    vec!["payload".to_string()],
2418                    AcceptedFieldKind::Nat64,
2419                    false,
2420                )]),
2421                None,
2422            )],
2423        )
2424    }
2425
2426    fn initialize() -> DbSession<TestCanister> {
2427        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2428        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2429        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2430        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
2431        session
2432            .db
2433            .ensure_recovered_state()
2434            .expect("identity pre-key test database should initialize");
2435        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2436            STORE_PATH,
2437            AcceptedSchemaRevision::INITIAL,
2438            BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH))]),
2439            BTreeMap::from([
2440                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2441                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2442            ]),
2443        );
2444        let store = session
2445            .db
2446            .store_handle(STORE_PATH)
2447            .expect("identity pre-key test store should resolve");
2448        crate::db::commit::publish_accepted_schema_candidate(
2449            STORE_PATH,
2450            store,
2451            AcceptedSchemaRevision::NONE,
2452            &candidate,
2453        )
2454        .expect("identity candidate should publish with explicit zero state");
2455        session
2456    }
2457
2458    fn initialize_journaled() -> DbSession<JournaledTestCanister> {
2459        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY);
2460        session
2461            .db
2462            .ensure_recovered_state()
2463            .expect("journaled identity database should initialize");
2464        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2465            JOURNALED_STORE_PATH,
2466            AcceptedSchemaRevision::INITIAL,
2467            BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH))]),
2468            BTreeMap::from([
2469                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2470                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2471            ]),
2472        );
2473        let store = session
2474            .db
2475            .store_handle(JOURNALED_STORE_PATH)
2476            .expect("journaled identity store should resolve");
2477        crate::db::commit::publish_accepted_schema_candidate(
2478            JOURNALED_STORE_PATH,
2479            store,
2480            AcceptedSchemaRevision::NONE,
2481            &candidate,
2482        )
2483        .expect("journaled identity candidate should publish");
2484        session
2485    }
2486
2487    fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
2488        AcceptedMutationIntentPatch::new()
2489            .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
2490    }
2491
2492    fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
2493        DynamicStructuralPatch::new(vec![(
2494            "payload".to_string(),
2495            DynamicWriteCell::Value(InputValue::Nat64(value)),
2496        )])
2497    }
2498
2499    fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
2500        vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
2501    }
2502
2503    fn assert_dynamic_payload(session: &DbSession<TestCanister>, key: u64, expected_payload: u64) {
2504        let unchanged = session
2505            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2506                entity: ENTITY_NAME.to_string(),
2507                key: InputValue::Nat64(key),
2508                patch: dynamic_payload_patch(expected_payload),
2509            })
2510            .expect("the expected row should remain readable through a no-op update");
2511        assert_eq!(unchanged.affected_rows, 0);
2512        assert_eq!(
2513            unchanged.rows,
2514            vec![expected_dynamic_row(key, expected_payload)],
2515        );
2516    }
2517
2518    fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
2519        values
2520            .iter()
2521            .map(|value| {
2522                AcceptedStructuralMutation::save(
2523                    MutationMode::Insert,
2524                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
2525                    payload_patch(*value),
2526                )
2527            })
2528            .collect()
2529    }
2530
2531    fn assert_identity_boundary(error: &InternalError) {
2532        assert_eq!(error.class(), ErrorClass::Unsupported);
2533        assert_eq!(error.origin(), ErrorOrigin::Identity);
2534    }
2535
2536    #[test]
2537    fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
2538        let generated = insert_key_exists_after_generation(true);
2539        assert_eq!(generated.class(), ErrorClass::Corruption);
2540        assert_eq!(generated.origin(), ErrorOrigin::Identity);
2541
2542        let ordinary = insert_key_exists_after_generation(false);
2543        assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
2544    }
2545
2546    #[cfg(target_pointer_width = "64")]
2547    #[test]
2548    fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
2549        let error = checked_pre_key_candidate_count(
2550            usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
2551        )
2552        .expect_err("candidate counts beyond u32 must reject");
2553        assert_identity_boundary(&error);
2554    }
2555
2556    #[test]
2557    #[expect(
2558        clippy::too_many_lines,
2559        reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
2560    )]
2561    fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
2562        let session = initialize();
2563        let seeded = session
2564            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2565            .expect("seed rows should commit");
2566        assert_eq!(seeded.affected_rows, 1);
2567
2568        let split = session
2569            .execute_trusted_dynamic_mutation_batch(vec![
2570                DynamicMutation::Update {
2571                    entity: ENTITY_NAME.to_string(),
2572                    key: InputValue::Nat64(1),
2573                    patch: dynamic_payload_patch(60),
2574                },
2575                DynamicMutation::Insert {
2576                    entity: ENTITY_NAME.to_string(),
2577                    patch: dynamic_payload_patch(40),
2578                },
2579            ])
2580            .expect("one holding should split atomically");
2581        assert_eq!(split.affected_rows, 2);
2582        assert_eq!(
2583            split.rows,
2584            vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
2585            "split after-images must retain input order and exact quantity",
2586        );
2587
2588        let rejected_split = session
2589            .execute_trusted_dynamic_mutation_batch(vec![
2590                DynamicMutation::Update {
2591                    entity: ENTITY_NAME.to_string(),
2592                    key: InputValue::Nat64(1),
2593                    patch: dynamic_payload_patch(50),
2594                },
2595                DynamicMutation::Insert {
2596                    entity: ENTITY_NAME.to_string(),
2597                    patch: DynamicStructuralPatch::new(Vec::new()),
2598                },
2599            ])
2600            .expect_err("an invalid split output must reject the staged source update");
2601        assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
2602        assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
2603        assert_dynamic_payload(&session, 1, 60);
2604        assert_dynamic_payload(&session, 2, 40);
2605
2606        let transfer = session
2607            .execute_trusted_dynamic_mutation_batch(vec![
2608                DynamicMutation::Update {
2609                    entity: ENTITY_NAME.to_string(),
2610                    key: InputValue::Nat64(1),
2611                    patch: dynamic_payload_patch(70),
2612                },
2613                DynamicMutation::Update {
2614                    entity: ENTITY_NAME.to_string(),
2615                    key: InputValue::Nat64(2),
2616                    patch: dynamic_payload_patch(30),
2617                },
2618            ])
2619            .expect("distinct transfer patches should share one atomic batch");
2620        assert_eq!(
2621            transfer.rows,
2622            vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
2623            "the transfer must preserve the exact total quantity",
2624        );
2625
2626        let merge = session
2627            .execute_trusted_dynamic_mutation_batch(vec![
2628                DynamicMutation::Delete {
2629                    entity: ENTITY_NAME.to_string(),
2630                    key: InputValue::Nat64(2),
2631                },
2632                DynamicMutation::Update {
2633                    entity: ENTITY_NAME.to_string(),
2634                    key: InputValue::Nat64(1),
2635                    patch: dynamic_payload_patch(100),
2636                },
2637            ])
2638            .expect("two holdings should merge atomically");
2639        assert_eq!(
2640            merge.rows,
2641            vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
2642            "delete before-images and update after-images must retain input order",
2643        );
2644
2645        let resplit = session
2646            .execute_trusted_dynamic_mutation_batch(vec![
2647                DynamicMutation::Update {
2648                    entity: ENTITY_NAME.to_string(),
2649                    key: InputValue::Nat64(1),
2650                    patch: dynamic_payload_patch(60),
2651                },
2652                DynamicMutation::Insert {
2653                    entity: ENTITY_NAME.to_string(),
2654                    patch: dynamic_payload_patch(40),
2655                },
2656            ])
2657            .expect("the merged holding should split again");
2658        assert_eq!(
2659            resplit.rows,
2660            vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
2661        );
2662
2663        let rejected_merge = session
2664            .execute_trusted_dynamic_mutation_batch(vec![
2665                DynamicMutation::Delete {
2666                    entity: ENTITY_NAME.to_string(),
2667                    key: InputValue::Nat64(3),
2668                },
2669                DynamicMutation::Update {
2670                    entity: ENTITY_NAME.to_string(),
2671                    key: InputValue::Nat64(99),
2672                    patch: dynamic_payload_patch(100),
2673                },
2674            ])
2675            .expect_err("a late missing merge target must preserve the earlier staged delete");
2676        assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
2677        assert_dynamic_payload(&session, 1, 60);
2678        assert_dynamic_payload(&session, 3, 40);
2679
2680        SCHEMA_STORE.with(|store| {
2681            let cursor = store
2682                .borrow()
2683                .identity_statement_cursor(
2684                    database_incarnation_id().expect("database incarnation should remain readable"),
2685                    ENTITY_TAG,
2686                    FieldId::new(1),
2687                    &AcceptedFieldKind::Nat64,
2688                )
2689                .expect("mixed Identity state should remain readable");
2690            assert_eq!(cursor.expected_high_water(), 3);
2691            assert!(!cursor.has_allocations());
2692        });
2693    }
2694
2695    #[test]
2696    fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
2697        let session = initialize();
2698        session
2699            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2700            .expect("the holding fixture should initialize");
2701
2702        let duplicate = session
2703            .execute_trusted_dynamic_mutation_batch(vec![
2704                DynamicMutation::Update {
2705                    entity: ENTITY_NAME.to_string(),
2706                    key: InputValue::Nat64(1),
2707                    patch: dynamic_payload_patch(60),
2708                },
2709                DynamicMutation::Delete {
2710                    entity: ENTITY_NAME.to_string(),
2711                    key: InputValue::Nat64(1),
2712                },
2713            ])
2714            .expect_err("duplicate targets across operation kinds must reject");
2715        assert!(matches!(
2716            duplicate.diagnostic().detail(),
2717            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2718                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
2719            }),
2720        ));
2721        assert_dynamic_payload(&session, 1, 100);
2722    }
2723
2724    #[test]
2725    fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
2726        let session = initialize();
2727        let empty = session
2728            .execute_trusted_dynamic_mutation_batch(Vec::new())
2729            .expect_err("an empty public batch must reject");
2730        assert!(matches!(
2731            empty.diagnostic().detail(),
2732            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2733                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
2734            }),
2735        ));
2736
2737        let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
2738            .map(|_| DynamicMutation::Delete {
2739                entity: ENTITY_NAME.to_string(),
2740                key: InputValue::Nat64(1),
2741            })
2742            .collect();
2743        let over_bound = session
2744            .execute_trusted_dynamic_mutation_batch(requests)
2745            .expect_err("operation cap plus one must reject before row resolution");
2746        assert!(matches!(
2747            over_bound.diagnostic().detail(),
2748            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2749                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
2750            }),
2751        ));
2752    }
2753
2754    #[test]
2755    fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
2756        let mut exact = 0;
2757        add_structural_mutation_staged_bytes(
2758            &mut exact,
2759            [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
2760        )
2761        .expect("the exact staged-byte boundary should admit");
2762        assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
2763
2764        let error = add_structural_mutation_staged_bytes(&mut exact, [1])
2765            .expect_err("one byte above the staged-byte boundary must reject");
2766        assert!(matches!(
2767            error.diagnostic().detail(),
2768            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2769                boundary:
2770                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
2771            }),
2772        ));
2773
2774        validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
2775            .expect("the exact result-byte boundary should admit");
2776        let error = validate_structural_mutation_result_bytes(
2777            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
2778        )
2779        .expect_err("one byte above the result-byte boundary must reject");
2780        assert!(matches!(
2781            error.diagnostic().detail(),
2782            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2783                boundary:
2784                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
2785            }),
2786        ));
2787    }
2788
2789    #[expect(
2790        clippy::too_many_lines,
2791        reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
2792    )]
2793    #[test]
2794    fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
2795        let session = initialize();
2796        let catalog = session
2797            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
2798            .expect("identity catalog should resolve");
2799        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
2800            .expect("identity row layout should build");
2801        let initial_description = session
2802            .try_describe_entity_by_name(ENTITY_NAME)
2803            .expect("accepted Identity description should resolve");
2804        let initial_identity = initial_description
2805            .identity()
2806            .expect("accepted Identity policy should be described");
2807        assert_eq!(initial_identity.field(), "id");
2808        assert_eq!(initial_identity.generator(), "Identity::next");
2809        assert_eq!(initial_identity.accepted_kind(), "nat64");
2810        assert_eq!(initial_identity.minimum(), 1);
2811        assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
2812        assert_eq!(initial_identity.high_water(), 0);
2813        assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
2814        assert!(!initial_identity.exhausted());
2815
2816        let rejected = session
2817            .execute_accepted_structural_save_batch(
2818                &catalog,
2819                &descriptor,
2820                batch(&[1_000, 2_000]),
2821                Timestamp::from_millis(6),
2822                |_| Err::<(), _>(InternalError::executor_unsupported()),
2823            )
2824            .expect_err("a rejected precommit result must not publish its tentative range");
2825        assert_eq!(rejected.class(), ErrorClass::Unsupported);
2826        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
2827
2828        let rows = session
2829            .execute_accepted_structural_save_batch(
2830                &catalog,
2831                &descriptor,
2832                batch(&[10, 20, 30]),
2833                Timestamp::from_millis(7),
2834                Ok,
2835            )
2836            .expect("one accepted batch should commit rows and one identity range");
2837        assert_eq!(
2838            rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
2839            vec![
2840                vec![Value::Nat64(1), Value::Nat64(10)],
2841                vec![Value::Nat64(2), Value::Nat64(20)],
2842                vec![Value::Nat64(3), Value::Nat64(30)],
2843            ],
2844        );
2845
2846        let dynamic = session
2847            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2848                entity: ENTITY_NAME.to_string(),
2849                patch: DynamicStructuralPatch::new(vec![(
2850                    "payload".to_string(),
2851                    DynamicWriteCell::Value(InputValue::Nat64(40)),
2852                )]),
2853            })
2854            .expect("dynamic omission should commit through shared Identity generation");
2855        assert_eq!(dynamic.affected_rows, 1);
2856
2857        for request in [
2858            DynamicMutation::Insert {
2859                entity: ENTITY_NAME.to_string(),
2860                patch: DynamicStructuralPatch::new(vec![
2861                    (
2862                        "id".to_string(),
2863                        DynamicWriteCell::Value(InputValue::Nat64(41)),
2864                    ),
2865                    (
2866                        "payload".to_string(),
2867                        DynamicWriteCell::Value(InputValue::Nat64(42)),
2868                    ),
2869                ]),
2870            },
2871            DynamicMutation::Update {
2872                entity: ENTITY_NAME.to_string(),
2873                key: InputValue::Nat64(1),
2874                patch: DynamicStructuralPatch::new(vec![(
2875                    "id".to_string(),
2876                    DynamicWriteCell::Default,
2877                )]),
2878            },
2879        ] {
2880            let error = session
2881                .execute_trusted_dynamic_mutation(&request)
2882                .expect_err("structural Identity authorship and regeneration must reject");
2883            assert_eq!(error.class(), ErrorClass::Unsupported);
2884            assert_eq!(error.origin(), ErrorOrigin::Executor);
2885        }
2886
2887        let binding = session
2888            .issue_typed_entity_binding(
2889                ENTITY_SOURCE,
2890                &[
2891                    DynamicTypedFieldBindingRequest::new(
2892                        ID_SOURCE.to_string(),
2893                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2894                        false,
2895                    ),
2896                    DynamicTypedFieldBindingRequest::new(
2897                        PAYLOAD_SOURCE.to_string(),
2898                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2899                        false,
2900                    ),
2901                ],
2902            )
2903            .expect("typed output should bind the Identity field");
2904        let typed_patch = binding
2905            .bind_write_fields(vec![(
2906                PAYLOAD_SOURCE.to_string(),
2907                DynamicWriteCell::Value(InputValue::Nat64(50)),
2908            )])
2909            .expect("typed payload should lower");
2910        let typed = session
2911            .execute_trusted_typed_mutation(
2912                &binding,
2913                &DynamicTypedMutation::Insert { patch: typed_patch },
2914            )
2915            .expect("typed omission should commit through shared Identity generation");
2916        assert_eq!(
2917            typed
2918                .expect("typed insert should return one mutation result")
2919                .affected_rows,
2920            1,
2921        );
2922        let explicit_typed_patch = binding
2923            .bind_write_fields(vec![
2924                (
2925                    ID_SOURCE.to_string(),
2926                    DynamicWriteCell::Value(InputValue::Nat64(51)),
2927                ),
2928                (
2929                    PAYLOAD_SOURCE.to_string(),
2930                    DynamicWriteCell::Value(InputValue::Nat64(52)),
2931                ),
2932            ])
2933            .expect("the low-level binding should retain exact authored intent");
2934        let explicit_typed_error = session
2935            .execute_trusted_typed_mutation(
2936                &binding,
2937                &DynamicTypedMutation::Insert {
2938                    patch: explicit_typed_patch,
2939                },
2940            )
2941            .expect_err("typed Identity authorship must reject before allocation");
2942        assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
2943        assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
2944
2945        let replace_error = session
2946            .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
2947                entity: ENTITY_NAME.to_string(),
2948                key: InputValue::Nat64(99),
2949                patch: DynamicStructuralPatch::new(vec![(
2950                    "payload".to_string(),
2951                    DynamicWriteCell::Value(InputValue::Nat64(60)),
2952                )]),
2953            })
2954            .expect_err("save-as-insert with a chosen Identity must reject");
2955        assert_eq!(replace_error.class(), ErrorClass::Unsupported);
2956        assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
2957
2958        #[cfg(feature = "sql")]
2959        {
2960            for sql in [
2961                "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
2962                "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
2963            ] {
2964                let _result = session
2965                    .execute_trusted_sql_mutation(sql)
2966                    .expect("SQL omission and DEFAULT should commit Identity generation");
2967            }
2968
2969            let error = session
2970                .execute_trusted_sql_mutation(
2971                    "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
2972                )
2973                .expect_err("an explicit SQL Identity value must reject before allocation");
2974            let diagnostic = error.diagnostic();
2975            assert_eq!(
2976                diagnostic.code(),
2977                icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
2978            );
2979            assert!(matches!(
2980                diagnostic.detail(),
2981                Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
2982                    boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
2983                }),
2984            ));
2985        }
2986
2987        let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
2988        assert_eq!(
2989            DATA_STORE.with(|store| store.borrow().len()),
2990            expected_committed
2991        );
2992        SCHEMA_STORE.with(|store| {
2993            let cursor = store
2994                .borrow()
2995                .identity_statement_cursor(
2996                    database_incarnation_id().expect("database incarnation should remain readable"),
2997                    ENTITY_TAG,
2998                    FieldId::new(1),
2999                    &AcceptedFieldKind::Nat64,
3000                )
3001                .expect("committed writes must leave active state readable");
3002            assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
3003            assert!(!cursor.has_allocations());
3004        });
3005        let committed_description = session
3006            .try_describe_entity_by_name(ENTITY_NAME)
3007            .expect("committed Identity description should resolve");
3008        let committed_identity = committed_description
3009            .identity()
3010            .expect("accepted Identity policy should remain described");
3011        assert_eq!(
3012            committed_identity.high_water(),
3013            u128::from(expected_committed),
3014        );
3015        assert_eq!(
3016            committed_identity.remaining(),
3017            u128::from(u64::MAX - expected_committed),
3018        );
3019        assert!(!committed_identity.exhausted());
3020    }
3021
3022    #[test]
3023    #[expect(
3024        clippy::too_many_lines,
3025        reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
3026    )]
3027    fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
3028        let session = initialize_journaled();
3029        let catalog = session
3030            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3031            .expect("journaled identity catalog should resolve");
3032        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3033            .expect("journaled identity row layout should build");
3034
3035        for (ordinal, interruption) in [
3036            MutationCommitInterruption::MarkerPersisted,
3037            MutationCommitInterruption::JournalPublished,
3038            MutationCommitInterruption::RowsPublished,
3039            MutationCommitInterruption::StateMaterialized,
3040        ]
3041        .into_iter()
3042        .enumerate()
3043        {
3044            interrupt_next_mutation_commit_for_tests(interruption);
3045            let interrupted = session.execute_accepted_structural_save_batch(
3046                &catalog,
3047                &descriptor,
3048                batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
3049                Timestamp::from_millis(8),
3050                Ok,
3051            );
3052            assert!(
3053                interrupted.is_err(),
3054                "the selected durable boundary should interrupt",
3055            );
3056
3057            let committed = session
3058                .execute_accepted_structural_save_batch(
3059                    &catalog,
3060                    &descriptor,
3061                    batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
3062                    Timestamp::from_millis(9),
3063                    Ok,
3064                )
3065                .expect("the next mutation must recover before allocating");
3066            let expected_high_water =
3067                u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
3068            assert_eq!(
3069                committed
3070                    .into_iter()
3071                    .map(|row| row.values)
3072                    .collect::<Vec<_>>(),
3073                vec![vec![
3074                    Value::Nat64(expected_high_water),
3075                    Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
3076                ]],
3077            );
3078            assert_eq!(
3079                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
3080                expected_high_water,
3081            );
3082            JOURNALED_SCHEMA_STORE.with(|store| {
3083                let cursor = store
3084                    .borrow()
3085                    .identity_statement_cursor(
3086                        database_incarnation_id()
3087                            .expect("database incarnation should remain readable"),
3088                        ENTITY_TAG,
3089                        FieldId::new(1),
3090                        &AcceptedFieldKind::Nat64,
3091                    )
3092                    .expect("guarded recovery must leave quiescent active state");
3093                assert_eq!(
3094                    cursor.expected_high_water(),
3095                    u128::from(expected_high_water),
3096                );
3097                assert!(!cursor.has_allocations());
3098            });
3099        }
3100
3101        for (ordinal, (interruption, deleted_key)) in [
3102            (MutationCommitInterruption::MarkerPersisted, 2),
3103            (MutationCommitInterruption::JournalPublished, 4),
3104            (MutationCommitInterruption::RowPrefixPublished, 6),
3105            (MutationCommitInterruption::RowsPublished, 8),
3106            (MutationCommitInterruption::StateMaterialized, 7),
3107        ]
3108        .into_iter()
3109        .enumerate()
3110        {
3111            let expected_payload =
3112                501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
3113            interrupt_next_mutation_commit_for_tests(interruption);
3114            let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
3115                DynamicMutation::Update {
3116                    entity: ENTITY_NAME.to_string(),
3117                    key: InputValue::Nat64(1),
3118                    patch: dynamic_payload_patch(expected_payload),
3119                },
3120                DynamicMutation::Delete {
3121                    entity: ENTITY_NAME.to_string(),
3122                    key: InputValue::Nat64(deleted_key),
3123                },
3124            ]);
3125            assert!(
3126                interrupted.is_err(),
3127                "the selected caller-key mixed publication boundary should interrupt",
3128            );
3129            let recovered_update = session
3130                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3131                    entity: ENTITY_NAME.to_string(),
3132                    key: InputValue::Nat64(1),
3133                    patch: dynamic_payload_patch(expected_payload),
3134                })
3135                .expect("guarded reentry should complete the marker-authorized mixed batch");
3136            assert_eq!(
3137                recovered_update.affected_rows, 0,
3138                "the recovered update must already expose its admitted final image",
3139            );
3140            let recovered_delete = session
3141                .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
3142                    entity: ENTITY_NAME.to_string(),
3143                    key: InputValue::Nat64(deleted_key),
3144                })
3145                .expect_err("the recovered delete must already be materialized");
3146            assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
3147            JOURNALED_SCHEMA_STORE.with(|store| {
3148                let cursor = store
3149                    .borrow()
3150                    .identity_statement_cursor(
3151                        database_incarnation_id()
3152                            .expect("database incarnation should remain readable"),
3153                        ENTITY_TAG,
3154                        FieldId::new(1),
3155                        &AcceptedFieldKind::Nat64,
3156                    )
3157                    .expect("caller-key recovery must preserve active Identity state");
3158                assert_eq!(cursor.expected_high_water(), 8);
3159                assert!(!cursor.has_allocations());
3160            });
3161        }
3162
3163        forget_recovered_domain_for_tests(&session.db)
3164            .expect("the final journal tail should remain recoverable");
3165        session
3166            .db
3167            .ensure_recovered_state()
3168            .expect("derived rebuild must not allocate another identity");
3169
3170        let quick = execute_quick_integrity(&session.db, catalog.inspection_plan())
3171            .expect("quiescent Identity control inventory should be inspectable");
3172        assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
3173        let row_page = execute_row_integrity_page(
3174            &session.db,
3175            catalog.inspection_plan(),
3176            PhysicalUnitCheckpoint::BeforeFirst,
3177            RowInspectionLimits::standard(),
3178        )
3179        .expect("Identity rows should remain within committed high-water");
3180        assert!(row_page.exhausted());
3181        assert!(row_page.findings().is_empty());
3182
3183        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
3184        assert!(
3185            JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
3186            "derived index rebuild should restore witnesses without allocating identities",
3187        );
3188        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
3189        JOURNALED_SCHEMA_STORE.with(|store| {
3190            let cursor = store
3191                .borrow()
3192                .identity_statement_cursor(
3193                    database_incarnation_id().expect("database incarnation should remain readable"),
3194                    ENTITY_TAG,
3195                    FieldId::new(1),
3196                    &AcceptedFieldKind::Nat64,
3197                )
3198                .expect("folded identity state should reopen without allocating");
3199            assert_eq!(cursor.expected_high_water(), 8);
3200            assert!(!cursor.has_allocations());
3201        });
3202    }
3203
3204    #[test]
3205    #[ignore = "release-closeout native timing probe for one marker-authorized Identity recovery"]
3206    fn identity_recovery_closeout_reports_guarded_reentry_time() {
3207        let session = initialize_journaled();
3208        let catalog = session
3209            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3210            .expect("journaled identity catalog should resolve");
3211        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3212            .expect("journaled identity row layout should build");
3213
3214        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
3215        let interrupted = session.execute_accepted_structural_save_batch(
3216            &catalog,
3217            &descriptor,
3218            batch(&[1]),
3219            Timestamp::from_millis(10),
3220            Ok,
3221        );
3222        assert!(
3223            interrupted.is_err(),
3224            "the selected publication boundary should interrupt",
3225        );
3226
3227        let start = Instant::now();
3228        let committed = session
3229            .execute_accepted_structural_save_batch(
3230                &catalog,
3231                &descriptor,
3232                batch(&[2]),
3233                Timestamp::from_millis(11),
3234                Ok,
3235            )
3236            .expect("guarded reentry should recover before allocation");
3237        let elapsed = start.elapsed();
3238        assert_eq!(
3239            committed
3240                .into_iter()
3241                .map(|row| row.values)
3242                .collect::<Vec<_>>(),
3243            vec![vec![Value::Nat64(2), Value::Nat64(2)]],
3244        );
3245
3246        println!(
3247            "identity recovery closeout: guarded_reentry_nanos={}",
3248            elapsed.as_nanos(),
3249        );
3250    }
3251}
3252
3253#[cfg(test)]
3254mod targeted_rule_mutation_tests {
3255    use super::{
3256        DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
3257        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
3258    };
3259    use crate::{
3260        db::{
3261            data::{DataStore, encode_input_value_for_candidate_field_contract},
3262            index::IndexStore,
3263            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
3264            schema::{
3265                AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
3266                AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
3267                AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
3268                ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
3269                PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
3270                ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
3271                SchemaRowLayout, SchemaStore, SchemaVersion,
3272                accepted_schema_candidate_with_catalogs_for_tests,
3273                build_record_newtype_composite_catalog_for_tests,
3274                empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
3275            },
3276        },
3277        error::{
3278            ConstraintDiagnostic, ConstraintDiagnosticKind, ConstraintValuePathComponent,
3279            InternalError,
3280        },
3281        traits::{CanisterKind, Path},
3282        types::EntityTag,
3283        value::InputValue,
3284    };
3285    use icydb_schema::{
3286        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
3287    };
3288    use std::{cell::RefCell, collections::BTreeMap};
3289
3290    const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
3291    const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
3292    const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
3293    const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
3294    const UPDATED_AT_SOURCE: &str =
3295        "session::write::targeted_rule_mutation_tests::Entity::updated_at";
3296    const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
3297    const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
3298    const DEGREE_MEMBER_SOURCE: &str =
3299        "session::write::targeted_rule_mutation_tests::Profile::degree";
3300    const DEGREE_RULE_SOURCE: &str =
3301        "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
3302
3303    struct TestCanister;
3304
3305    impl Path for TestCanister {
3306        const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
3307    }
3308
3309    impl CanisterKind for TestCanister {
3310        const COMMIT_MEMORY_ID: u8 = 43;
3311        const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
3312        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
3313        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3314            "icydb.targeted_mutation_tests.integrity.progress.v1";
3315    }
3316
3317    thread_local! {
3318        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3319        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3320        static SCHEMA_STORE: RefCell<SchemaStore> =
3321            const { RefCell::new(SchemaStore::init_heap()) };
3322        static STORE_REGISTRY: StoreRegistry = {
3323            let mut registry = StoreRegistry::new();
3324            registry.register_store(
3325                STORE_PATH,
3326                &DATA_STORE,
3327                &INDEX_STORE,
3328                &SCHEMA_STORE,
3329                StoreAllocationIdentities::absent(),
3330                StoreRuntimeStorageCapabilities::heap(),
3331            ).expect("targeted mutation test store should register");
3332            registry
3333        };
3334    }
3335
3336    fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
3337        parse(raw.to_string()).expect("test source identity should admit")
3338    }
3339
3340    fn profile_input(degree: u64) -> InputValue {
3341        InputValue::Map(vec![(
3342            InputValue::Text("degree".to_string()),
3343            InputValue::Nat64(degree),
3344        )])
3345    }
3346
3347    fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
3348        DynamicStructuralPatch::new(vec![
3349            (
3350                "id".to_string(),
3351                DynamicWriteCell::Value(InputValue::Nat64(id)),
3352            ),
3353            (
3354                "profile".to_string(),
3355                DynamicWriteCell::Value(profile_input(degree)),
3356            ),
3357        ])
3358    }
3359
3360    fn encoded_value(
3361        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3362        composite_catalog: &AcceptedCompositeCatalog,
3363        name: &str,
3364        kind: &AcceptedFieldKind,
3365        storage_decode: FieldStorageDecode,
3366        leaf_codec: LeafCodec,
3367        value: InputValue,
3368    ) -> Vec<u8> {
3369        let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
3370        encode_input_value_for_candidate_field_contract(
3371            enum_catalog,
3372            composite_catalog,
3373            field,
3374            value,
3375            &mut ValueAdmissionBudget::standard(),
3376        )
3377        .expect("test accepted value should encode")
3378    }
3379
3380    fn nat64_literal(
3381        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3382        composite_catalog: &AcceptedCompositeCatalog,
3383        value: u64,
3384    ) -> AcceptedCheckLiteralV1 {
3385        let kind = AcceptedFieldKind::Nat64;
3386        AcceptedCheckLiteralV1::from_accepted_parts(
3387            kind.clone(),
3388            FieldStorageDecode::ByKind,
3389            LeafCodec::Scalar(ScalarCodec::Nat64),
3390            encoded_value(
3391                enum_catalog,
3392                composite_catalog,
3393                "degree_bound",
3394                &kind,
3395                FieldStorageDecode::ByKind,
3396                LeafCodec::Scalar(ScalarCodec::Nat64),
3397                InputValue::Nat64(value),
3398            ),
3399        )
3400    }
3401
3402    fn targeted_diagnostic(error: &InternalError) -> &ConstraintDiagnostic {
3403        let diagnostic = error
3404            .constraint_diagnostic()
3405            .expect("targeted mutation should retain a public diagnostic");
3406        assert_eq!(
3407            diagnostic.constraint_kind(),
3408            ConstraintDiagnosticKind::TargetedRule
3409        );
3410        assert_eq!(diagnostic.field_paths(), &["profile".to_string()]);
3411        assert_eq!(
3412            diagnostic
3413                .value_path()
3414                .expect("targeted mutation should retain its typed value path")
3415                .components(),
3416            &[
3417                ConstraintValuePathComponent::RootField { field_id: 2 },
3418                ConstraintValuePathComponent::RecordMember {
3419                    composite_type_id: 1,
3420                    member_id: 1,
3421                },
3422            ],
3423        );
3424        diagnostic
3425    }
3426
3427    #[expect(
3428        clippy::too_many_lines,
3429        reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
3430    )]
3431    #[test]
3432    fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
3433        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3434        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3435        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3436
3437        let entity_tag = EntityTag::new(93);
3438        let enum_catalog = empty_accepted_enum_catalog_for_tests();
3439        let (composite_catalog, profile_type, degree_type, degree_member) =
3440            build_record_newtype_composite_catalog_for_tests(
3441                "tests::TargetedProfile".to_string(),
3442                "degree".to_string(),
3443                "tests::TargetedDegree".to_string(),
3444                AcceptedFieldKind::Nat64,
3445                &enum_catalog,
3446            )
3447            .expect("targeted mutation composites should close");
3448        let profile_kind = AcceptedFieldKind::Composite {
3449            type_id: profile_type,
3450        };
3451        let profile_default = encoded_value(
3452            &enum_catalog,
3453            &composite_catalog,
3454            "profile",
3455            &profile_kind,
3456            FieldStorageDecode::CatalogValue,
3457            LeafCodec::Structural,
3458            profile_input(12),
3459        );
3460        let fields = vec![
3461            PersistedFieldSnapshot::new_initial(
3462                FieldId::new(1),
3463                "id".to_string(),
3464                SchemaFieldSlot::new(0),
3465                AcceptedFieldKind::Nat64,
3466                Vec::new(),
3467                false,
3468                SchemaInsertDefault::None,
3469                FieldStorageDecode::ByKind,
3470                LeafCodec::Scalar(ScalarCodec::Nat64),
3471            ),
3472            PersistedFieldSnapshot::new_initial(
3473                FieldId::new(2),
3474                "profile".to_string(),
3475                SchemaFieldSlot::new(1),
3476                profile_kind,
3477                vec![PersistedNestedLeafSnapshot::new(
3478                    vec!["degree".to_string()],
3479                    AcceptedFieldKind::Composite {
3480                        type_id: degree_type,
3481                    },
3482                    false,
3483                )],
3484                false,
3485                SchemaInsertDefault::SlotPayload(profile_default),
3486                FieldStorageDecode::CatalogValue,
3487                LeafCodec::Structural,
3488            ),
3489            PersistedFieldSnapshot::new_initial_with_write_policy(
3490                FieldId::new(3),
3491                "updated_at".to_string(),
3492                SchemaFieldSlot::new(2),
3493                AcceptedFieldKind::Timestamp,
3494                Vec::new(),
3495                false,
3496                SchemaInsertDefault::None,
3497                SchemaFieldWritePolicy::from_model_policies(
3498                    None,
3499                    Some(FieldWriteManagement::UpdatedAt),
3500                ),
3501                FieldStorageDecode::ByKind,
3502                LeafCodec::Scalar(ScalarCodec::Timestamp),
3503            ),
3504        ];
3505        let mut snapshot = PersistedSchemaSnapshot::new(
3506            SchemaVersion::initial(),
3507            ENTITY_SOURCE.to_string(),
3508            "TargetedMutation".to_string(),
3509            FieldId::new(1),
3510            SchemaRowLayout::initial(
3511                fields
3512                    .iter()
3513                    .map(|field| (field.id(), field.slot()))
3514                    .collect(),
3515            ),
3516            fields,
3517        );
3518        let constraint_catalog = snapshot
3519            .constraint_catalog()
3520            .clone()
3521            .with_added_targeted_rule(
3522                "profile_degree_multiple".to_string(),
3523                ConstraintOrigin::Generated,
3524                AcceptedRuleTarget::new(
3525                    FieldId::new(2),
3526                    AcceptedNamedTypeIdentity::Composite(degree_type),
3527                ),
3528                AcceptedRuleOperation::MultipleOf {
3529                    divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
3530                },
3531            )
3532            .expect("targeted mutation rule should allocate");
3533        let targeted_rule_id = constraint_catalog
3534            .constraints()
3535            .last()
3536            .expect("targeted mutation rule should persist")
3537            .id();
3538        snapshot = snapshot.with_constraint_catalog(constraint_catalog);
3539
3540        let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
3541        let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
3542        let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
3543        let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
3544        let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
3545        let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
3546        let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
3547        let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
3548        let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
3549            BTreeMap::from([(entity_source, entity_tag)]),
3550            BTreeMap::from([
3551                ((entity_tag, id_source), FieldId::new(1)),
3552                ((entity_tag, profile_source), FieldId::new(2)),
3553                ((entity_tag, updated_at_source), FieldId::new(3)),
3554            ]),
3555            BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
3556            BTreeMap::new(),
3557            BTreeMap::new(),
3558        )
3559        .with_initial_named_types_for_tests(
3560            BTreeMap::from([
3561                (
3562                    profile_type_source,
3563                    AcceptedNamedTypeIdentity::Composite(profile_type),
3564                ),
3565                (
3566                    degree_type_source,
3567                    AcceptedNamedTypeIdentity::Composite(degree_type),
3568                ),
3569            ]),
3570            BTreeMap::new(),
3571            BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
3572        );
3573        let candidate = accepted_schema_candidate_with_catalogs_for_tests(
3574            STORE_PATH,
3575            AcceptedSchemaRevision::INITIAL,
3576            enum_catalog,
3577            composite_catalog,
3578            source_bindings,
3579            BTreeMap::from([(entity_tag, snapshot)]),
3580        );
3581
3582        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
3583        session
3584            .db
3585            .ensure_recovered_state()
3586            .expect("targeted mutation test database should initialize");
3587        let store = session
3588            .db
3589            .store_handle(STORE_PATH)
3590            .expect("targeted mutation test store should resolve");
3591        crate::db::commit::publish_accepted_schema_candidate(
3592            STORE_PATH,
3593            store,
3594            AcceptedSchemaRevision::NONE,
3595            &candidate,
3596        )
3597        .expect("targeted mutation candidate should publish");
3598
3599        let dynamic_error = session
3600            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3601                entity: "TargetedMutation".to_string(),
3602                patch: structural_patch(1, 12),
3603            })
3604            .expect_err("dynamic write must enforce the targeted rule");
3605        let dynamic_diagnostic = targeted_diagnostic(&dynamic_error);
3606        assert_eq!(dynamic_diagnostic.constraint_id(), targeted_rule_id.get());
3607
3608        let binding = session
3609            .issue_typed_entity_binding(
3610                ENTITY_SOURCE,
3611                &[
3612                    DynamicTypedFieldBindingRequest::new(
3613                        ID_SOURCE.to_string(),
3614                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3615                        false,
3616                    ),
3617                    DynamicTypedFieldBindingRequest::new(
3618                        PROFILE_SOURCE.to_string(),
3619                        DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
3620                        false,
3621                    ),
3622                    DynamicTypedFieldBindingRequest::new(
3623                        UPDATED_AT_SOURCE.to_string(),
3624                        DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
3625                        false,
3626                    ),
3627                ],
3628            )
3629            .expect("targeted typed binding should issue");
3630        let typed_patch = binding
3631            .bind_write_fields(vec![
3632                (
3633                    ID_SOURCE.to_string(),
3634                    DynamicWriteCell::Value(InputValue::Nat64(2)),
3635                ),
3636                (
3637                    PROFILE_SOURCE.to_string(),
3638                    DynamicWriteCell::Value(profile_input(12)),
3639                ),
3640            ])
3641            .expect("targeted typed patch should bind");
3642        let typed_error = session
3643            .execute_trusted_typed_mutation(
3644                &binding,
3645                &DynamicTypedMutation::Insert { patch: typed_patch },
3646            )
3647            .expect_err("typed write must enforce the targeted rule");
3648        assert_eq!(
3649            targeted_diagnostic(&typed_error).constraint_id(),
3650            targeted_rule_id.get()
3651        );
3652
3653        #[cfg(feature = "sql")]
3654        {
3655            let sql_error = session
3656                .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
3657                .expect_err("SQL default resolution must enforce the targeted rule");
3658            let crate::db::QueryError::Execute(execute) = sql_error else {
3659                panic!("targeted SQL write should fail at shared execution admission");
3660            };
3661            assert_eq!(
3662                targeted_diagnostic(execute.as_internal()).constraint_id(),
3663                targeted_rule_id.get()
3664            );
3665        }
3666
3667        session
3668            .execute_trusted_dynamic_mutation_batch(vec![
3669                DynamicMutation::Insert {
3670                    entity: "TargetedMutation".to_string(),
3671                    patch: structural_patch(4, 5),
3672                },
3673                DynamicMutation::Insert {
3674                    entity: "TargetedMutation".to_string(),
3675                    patch: structural_patch(5, 12),
3676                },
3677            ])
3678            .expect_err("one invalid targeted value must reject the whole batch");
3679        assert_eq!(
3680            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3681            Some(0),
3682            "no frontend or earlier valid batch row may escape targeted admission",
3683        );
3684
3685        let admitted = session
3686            .execute_trusted_dynamic_mutation_batch(vec![
3687                DynamicMutation::Insert {
3688                    entity: "TargetedMutation".to_string(),
3689                    patch: structural_patch(6, 5),
3690                },
3691                DynamicMutation::Insert {
3692                    entity: "TargetedMutation".to_string(),
3693                    patch: structural_patch(7, 10),
3694                },
3695            ])
3696            .expect("compliant targeted values should share one accepted batch");
3697        let [first, second] = admitted.rows.as_slice() else {
3698            panic!("the mixed targeted batch should return two rows");
3699        };
3700        let first_timestamp = first
3701            .get(2)
3702            .expect("the first mixed row should contain its managed timestamp");
3703        assert!(matches!(
3704            first_timestamp,
3705            crate::value::OutputValue::Timestamp(_)
3706        ));
3707        assert_eq!(
3708            second.get(2),
3709            Some(first_timestamp),
3710            "one accepted mixed batch must materialize one managed timestamp",
3711        );
3712        assert_eq!(
3713            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3714            Some(2),
3715        );
3716    }
3717}