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    fn assert_query_diagnostic(
1463        error: crate::db::QueryError,
1464        code: icydb_diagnostic_code::DiagnosticCode,
1465        origin: icydb_diagnostic_code::ErrorOrigin,
1466        detail: icydb_diagnostic_code::DiagnosticDetail,
1467    ) {
1468        let diagnostic = error.diagnostic();
1469        assert_eq!(diagnostic.code(), code);
1470        assert_eq!(diagnostic.origin(), origin);
1471        assert_eq!(diagnostic.detail(), Some(&detail));
1472    }
1473
1474    #[test]
1475    fn typed_adapter_kind_matching_is_exact_but_accepts_relation_key_wrappers() {
1476        let relation = AcceptedFieldKind::Relation {
1477            target_path: "test::Target".to_string(),
1478            target_entity_name: "Target".to_string(),
1479            target_entity_tag: EntityTag::new(7),
1480            target_store_path: "test::Store".to_string(),
1481            key_kind: Box::new(AcceptedFieldKind::Nat64),
1482        };
1483
1484        assert!(typed_adapter_field_kind_matches(
1485            &relation,
1486            &AcceptedFieldKind::Nat64,
1487        ));
1488        assert!(typed_adapter_field_kind_matches(
1489            &AcceptedFieldKind::List(Box::new(relation)),
1490            &AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Nat64)),
1491        ));
1492        assert!(!typed_adapter_field_kind_matches(
1493            &AcceptedFieldKind::Nat64,
1494            &AcceptedFieldKind::Nat32,
1495        ));
1496    }
1497
1498    #[test]
1499    fn typed_adapter_field_contract_rejects_invalid_named_source_identity() {
1500        assert!(matches!(
1501            dynamic_typed_field_type(DynamicTypedFieldType::Named(String::new())),
1502            Err(DynamicTypedBindingError::FieldUnavailable),
1503        ));
1504        assert!(matches!(
1505            dynamic_typed_field_type(DynamicTypedFieldType::Scalar(ScalarType::Nat16)),
1506            Ok(icydb_schema::FieldType::Scalar(ScalarType::Nat16)),
1507        ));
1508    }
1509
1510    // Keep the full rename, stale-binding, and old-name-reuse lifecycle in one
1511    // regression so each issued binding is checked against the next revision.
1512    #[expect(clippy::too_many_lines)]
1513    #[test]
1514    fn typed_binding_uses_accepted_ids_and_slots_across_renames_and_name_reuse() {
1515        let entity_tag = EntityTag::new(91);
1516        let other_entity_tag = EntityTag::new(92);
1517        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1518        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1519        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1520
1521        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
1522        session
1523            .db
1524            .ensure_recovered_state()
1525            .expect("typed adapter test database should initialize");
1526        publish(
1527            &session,
1528            AcceptedSchemaRevision::NONE,
1529            AcceptedSchemaRevision::INITIAL,
1530            BTreeMap::from([(
1531                entity_tag,
1532                snapshot(
1533                    ENTITY_SOURCE,
1534                    "Entity",
1535                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1536                ),
1537            )]),
1538            BTreeMap::from([
1539                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1540                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1541            ]),
1542        );
1543
1544        let initial_catalog = session
1545            .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1546            .expect("initial source catalog lookup should inspect")
1547            .expect("initial source catalog should exist");
1548        assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1549        let initial = session
1550            .issue_typed_entity_binding(
1551                entity_source(ENTITY_SOURCE).as_str(),
1552                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1553            )
1554            .expect("initial typed binding should issue");
1555        assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1556        assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1557        assert_eq!(initial.output_field_slot("value"), Some(1));
1558        let initial_patch = initial
1559            .bind_write_fields(vec![(
1560                VALUE_SOURCE.to_string(),
1561                DynamicWriteCell::Value(InputValue::Nat64(7)),
1562            )])
1563            .expect("source-bound patch should lower");
1564        assert_eq!(
1565            initial_patch.fields(),
1566            &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1567        );
1568
1569        publish(
1570            &session,
1571            AcceptedSchemaRevision::INITIAL,
1572            AcceptedSchemaRevision::new(2),
1573            BTreeMap::from([
1574                (
1575                    entity_tag,
1576                    snapshot(
1577                        ENTITY_SOURCE,
1578                        "RenamedEntity",
1579                        vec![
1580                            nat64_field(1, "id", 0),
1581                            nat64_field(2, "renamed_value", 1),
1582                            nat64_field(3, "value", 2),
1583                        ],
1584                    ),
1585                ),
1586                (
1587                    other_entity_tag,
1588                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1589                ),
1590            ]),
1591            BTreeMap::from([
1592                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1593                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1594                (
1595                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1596                    FieldId::new(3),
1597                ),
1598                (
1599                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1600                    FieldId::new(1),
1601                ),
1602            ]),
1603        );
1604
1605        assert!(
1606            !session
1607                .typed_entity_binding_is_current(&initial)
1608                .expect("renamed binding currentness should inspect")
1609        );
1610        let renamed = session
1611            .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1612            .expect("renamed source-bound adapter should rebind");
1613        assert_eq!(renamed.entity(), "RenamedEntity");
1614        assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1615        assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1616        assert_eq!(renamed.output_field_slot("value"), None);
1617
1618        publish(
1619            &session,
1620            AcceptedSchemaRevision::new(2),
1621            AcceptedSchemaRevision::new(3),
1622            BTreeMap::from([
1623                (
1624                    entity_tag,
1625                    snapshot(
1626                        ENTITY_SOURCE,
1627                        "RenamedEntity",
1628                        vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1629                    ),
1630                ),
1631                (
1632                    other_entity_tag,
1633                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1634                ),
1635            ]),
1636            BTreeMap::from([
1637                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1638                (
1639                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1640                    FieldId::new(2),
1641                ),
1642                (
1643                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1644                    FieldId::new(1),
1645                ),
1646            ]),
1647        );
1648
1649        assert!(matches!(
1650            session.issue_typed_entity_binding(
1651                ENTITY_SOURCE,
1652                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1653            ),
1654            Err(DynamicTypedBindingError::FieldUnavailable),
1655        ));
1656        assert!(
1657            !session
1658                .typed_entity_binding_is_current(&renamed)
1659                .expect("removed source binding should become stale")
1660        );
1661
1662        let replacement = session
1663            .issue_typed_entity_binding(
1664                ENTITY_SOURCE,
1665                &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
1666            )
1667            .expect("explicit replacement source should bind");
1668        assert!(
1669            session
1670                .execute_trusted_typed_mutation(
1671                    &replacement,
1672                    &DynamicTypedMutation::Insert {
1673                        patch: initial_patch
1674                    },
1675                )
1676                .expect("cross-binding patch should fail closed")
1677                .is_none()
1678        );
1679        let patch = replacement
1680            .bind_write_fields(vec![
1681                (
1682                    ID_SOURCE.to_string(),
1683                    DynamicWriteCell::Value(InputValue::Nat64(1)),
1684                ),
1685                (
1686                    REPLACEMENT_SOURCE.to_string(),
1687                    DynamicWriteCell::Value(InputValue::Nat64(9)),
1688                ),
1689            ])
1690            .expect("replacement source write should bind by accepted IDs and slots");
1691        let result = session
1692            .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
1693            .expect("typed insert should use the accepted mutation pipeline")
1694            .expect("replacement binding should remain current");
1695        assert_eq!(result.entity, "RenamedEntity");
1696        assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1697        assert_eq!(
1698            result.rows,
1699            vec![vec![
1700                crate::value::OutputValue::Nat64(1),
1701                crate::value::OutputValue::Nat64(9)
1702            ]]
1703        );
1704        assert_eq!(result.affected_rows, 1);
1705
1706        let second_patch = replacement
1707            .bind_write_fields(vec![
1708                (
1709                    ID_SOURCE.to_string(),
1710                    DynamicWriteCell::Value(InputValue::Nat64(2)),
1711                ),
1712                (
1713                    REPLACEMENT_SOURCE.to_string(),
1714                    DynamicWriteCell::Value(InputValue::Nat64(10)),
1715                ),
1716            ])
1717            .expect("second source-bound patch should lower");
1718        session
1719            .execute_trusted_typed_mutation(
1720                &replacement,
1721                &DynamicTypedMutation::Insert {
1722                    patch: second_patch,
1723                },
1724            )
1725            .expect("second typed insert should use the accepted mutation pipeline")
1726            .expect("replacement binding should remain current");
1727
1728        {
1729            let query = crate::db::DynamicQuery::new("RenamedEntity")
1730                .select(["id", "value"])
1731                .order_by(crate::db::asc("id"))
1732                .limit(1);
1733            let result = session
1734                .execute_trusted_dynamic_query(&query)
1735                .expect("SQL-free dynamic execution should use accepted authority");
1736            assert_eq!(result.entity, "RenamedEntity");
1737            assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1738            assert_eq!(
1739                result.rows,
1740                vec![vec![
1741                    crate::value::OutputValue::Nat64(1),
1742                    crate::value::OutputValue::Nat64(9)
1743                ]]
1744            );
1745            assert_eq!(result.row_count, 1);
1746            assert_query_diagnostic(
1747                session
1748                    .execute_trusted_dynamic_query(&query.cursor("00"))
1749                    .expect_err("scalar execution must reject grouped cursor state"),
1750                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1751                icydb_diagnostic_code::ErrorOrigin::Query,
1752                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1753                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1754                },
1755            );
1756            assert_query_diagnostic(
1757                session
1758                    .execute_public_dynamic_grouped_query(
1759                        &crate::db::DynamicQuery::new("RenamedEntity").grouped_limits(1, 1024),
1760                    )
1761                    .expect_err("grouped execution must reject scalar query state"),
1762                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1763                icydb_diagnostic_code::ErrorOrigin::Query,
1764                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1765                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1766                },
1767            );
1768
1769            let grouped_query = crate::db::DynamicQuery::new("RenamedEntity")
1770                .filter(crate::db::FieldRef::new("id").eq(1_u64))
1771                .group_by("value")
1772                .aggregate(crate::db::count())
1773                .grouped_limits(1, 1024)
1774                .limit(1);
1775            let grouped = session
1776                .execute_public_dynamic_grouped_query(&grouped_query)
1777                .expect("SQL-free grouped execution should use accepted authority");
1778            let typed_grouped = session
1779                .execute_public_dynamic_grouped_query_for_typed_binding(
1780                    &replacement,
1781                    &grouped_query,
1782                )
1783                .expect("typed grouped execution should inspect accepted authority")
1784                .expect("replacement binding should remain current");
1785            assert_eq!(typed_grouped, grouped);
1786            assert!(
1787                session
1788                    .execute_public_dynamic_grouped_query_for_typed_binding(
1789                        &renamed,
1790                        &grouped_query,
1791                    )
1792                    .expect("stale grouped binding should inspect accepted authority")
1793                    .is_none(),
1794                "stale typed grouped bindings must fail closed before execution"
1795            );
1796            assert_eq!(grouped.entity, "RenamedEntity");
1797            assert_eq!(grouped.row_count, 1);
1798            assert_eq!(grouped.rows.len(), 1);
1799            assert_eq!(
1800                grouped.rows[0].group_key(),
1801                &[crate::value::OutputValue::Nat64(9)]
1802            );
1803            assert_eq!(
1804                grouped.rows[0].aggregate_values(),
1805                &[crate::value::OutputValue::Nat64(1)]
1806            );
1807            assert_eq!(grouped.next_cursor, None);
1808
1809            assert_query_diagnostic(
1810                session
1811                    .execute_public_dynamic_grouped_query(&grouped_query.clone().select(["value"]))
1812                    .expect_err("grouped output must reject scalar selection"),
1813                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1814                icydb_diagnostic_code::ErrorOrigin::Query,
1815                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1816                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1817                },
1818            );
1819            assert_query_diagnostic(
1820                session
1821                    .execute_public_dynamic_grouped_query(
1822                        &crate::db::DynamicQuery::new("RenamedEntity")
1823                            .group_by("value")
1824                            .aggregate(crate::db::count()),
1825                    )
1826                    .expect_err("public grouped execution must require explicit limits"),
1827                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1828                icydb_diagnostic_code::ErrorOrigin::Query,
1829                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1830                    reason:
1831                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1832                },
1833            );
1834            assert_query_diagnostic(
1835                session
1836                    .execute_trusted_dynamic_grouped_query(
1837                        &crate::db::DynamicQuery::new("RenamedEntity")
1838                            .group_by("value")
1839                            .aggregate(crate::db::count())
1840                            .grouped_limits(0, 1024),
1841                    )
1842                    .expect_err("trusted grouped execution must reject zero limits"),
1843                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1844                icydb_diagnostic_code::ErrorOrigin::Query,
1845                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1846                    reason:
1847                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1848                },
1849            );
1850            assert_query_diagnostic(
1851                session
1852                    .execute_public_dynamic_grouped_query(&grouped_query.grouped_limits(101, 1024))
1853                    .expect_err("public grouped execution must enforce its group budget"),
1854                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1855                icydb_diagnostic_code::ErrorOrigin::Query,
1856                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1857                    reason:
1858                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryExceedsBudget,
1859                },
1860            );
1861
1862            let paged_query = crate::db::DynamicQuery::new("RenamedEntity")
1863                .group_by("value")
1864                .aggregate(crate::db::count())
1865                .grouped_limits(2, 1024)
1866                .limit(1);
1867            assert_query_diagnostic(
1868                session
1869                    .execute_public_dynamic_grouped_query(&paged_query)
1870                    .expect_err("public grouped execution must reject an unbounded full scan"),
1871                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1872                icydb_diagnostic_code::ErrorOrigin::Query,
1873                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1874                    reason:
1875                        icydb_diagnostic_code::QueryReadAdmissionCode::UnboundedFullScanRejected,
1876                },
1877            );
1878            let first_page = session
1879                .execute_trusted_dynamic_grouped_query(&paged_query)
1880                .expect("SQL-free grouped first page should execute");
1881            assert_eq!(first_page.row_count, 1);
1882            assert_eq!(
1883                first_page.rows[0].group_key(),
1884                &[crate::value::OutputValue::Nat64(9)]
1885            );
1886            let cursor = first_page
1887                .next_cursor
1888                .expect("first grouped page should return a continuation cursor");
1889            assert_query_diagnostic(
1890                session
1891                    .execute_trusted_dynamic_grouped_query(
1892                        &paged_query.clone().cursor(format!("{cursor}0")),
1893                    )
1894                    .expect_err("tampered grouped cursor must fail closed"),
1895                icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
1896                icydb_diagnostic_code::ErrorOrigin::Cursor,
1897                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1898                    kind: icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor,
1899                },
1900            );
1901            let second_page = session
1902                .execute_trusted_dynamic_grouped_query(&paged_query.cursor(cursor))
1903                .expect("SQL-free grouped continuation should execute");
1904            assert_eq!(second_page.row_count, 1);
1905            assert_eq!(
1906                second_page.rows[0].group_key(),
1907                &[crate::value::OutputValue::Nat64(10)]
1908            );
1909            assert_eq!(second_page.next_cursor, None);
1910        }
1911    }
1912}
1913
1914#[cfg(test)]
1915mod mixed_relation_batch_tests {
1916    use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
1917    use crate::{
1918        db::{
1919            data::DataStore,
1920            index::IndexStore,
1921            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1922            schema::{
1923                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
1924                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
1925                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
1926                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
1927                SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
1928                SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
1929            },
1930        },
1931        error::{ConstraintDiagnosticKind, ErrorClass},
1932        traits::{CanisterKind, Path},
1933        types::EntityTag,
1934        value::{InputValue, OutputValue},
1935    };
1936    use icydb_schema::FieldSourceKey;
1937    use std::{cell::RefCell, collections::BTreeMap};
1938
1939    const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
1940    const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
1941    const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
1942    const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
1943    const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
1944    const ENTITY_NAME: &str = "MixedRelationNode";
1945    const ENTITY_TAG: EntityTag = EntityTag::new(94);
1946    const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
1947    const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
1948    const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
1949    const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
1950    const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
1951
1952    struct TestCanister;
1953
1954    impl Path for TestCanister {
1955        const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
1956    }
1957
1958    impl CanisterKind for TestCanister {
1959        const COMMIT_MEMORY_ID: u8 = 47;
1960        const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
1961        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
1962        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1963            "icydb.mixed_relation_batch_tests.integrity.progress.v1";
1964    }
1965
1966    thread_local! {
1967        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1968        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1969        static SCHEMA_STORE: RefCell<SchemaStore> =
1970            const { RefCell::new(SchemaStore::init_heap()) };
1971        static STORE_REGISTRY: StoreRegistry = {
1972            let mut registry = StoreRegistry::new();
1973            registry.register_store(
1974                STORE_PATH,
1975                &DATA_STORE,
1976                &INDEX_STORE,
1977                &SCHEMA_STORE,
1978                StoreAllocationIdentities::absent(),
1979                StoreRuntimeStorageCapabilities::heap(),
1980            ).expect("mixed relation test store should register");
1981            registry
1982        };
1983    }
1984
1985    fn source_key(source: &str) -> FieldSourceKey {
1986        FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
1987    }
1988
1989    fn relation_snapshot() -> PersistedSchemaSnapshot {
1990        let fields = vec![
1991            PersistedFieldSnapshot::new_initial(
1992                FieldId::new(1),
1993                "id".to_string(),
1994                SchemaFieldSlot::new(0),
1995                AcceptedFieldKind::Nat64,
1996                Vec::new(),
1997                false,
1998                SchemaInsertDefault::None,
1999                FieldStorageDecode::ByKind,
2000                LeafCodec::Scalar(ScalarCodec::Nat64),
2001            ),
2002            PersistedFieldSnapshot::new_initial(
2003                FieldId::new(2),
2004                "parent_id".to_string(),
2005                SchemaFieldSlot::new(1),
2006                AcceptedFieldKind::Relation {
2007                    target_path: ENTITY_SOURCE.to_string(),
2008                    target_entity_name: ENTITY_NAME.to_string(),
2009                    target_entity_tag: ENTITY_TAG,
2010                    target_store_path: STORE_PATH.to_string(),
2011                    key_kind: Box::new(AcceptedFieldKind::Nat64),
2012                },
2013                Vec::new(),
2014                true,
2015                SchemaInsertDefault::None,
2016                FieldStorageDecode::ByKind,
2017                LeafCodec::Scalar(ScalarCodec::Nat64),
2018            ),
2019            PersistedFieldSnapshot::new_initial(
2020                FieldId::new(3),
2021                "code".to_string(),
2022                SchemaFieldSlot::new(2),
2023                AcceptedFieldKind::Nat64,
2024                Vec::new(),
2025                false,
2026                SchemaInsertDefault::None,
2027                FieldStorageDecode::ByKind,
2028                LeafCodec::Scalar(ScalarCodec::Nat64),
2029            ),
2030        ];
2031        let relation = PersistedRelationEdgeSnapshot::new(
2032            RelationId::new(1).expect("mixed relation identity should be non-zero"),
2033            "parent".to_string(),
2034            ENTITY_SOURCE.to_string(),
2035            vec![FieldId::new(2)],
2036        );
2037        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2038            SchemaVersion::initial(),
2039            ENTITY_SOURCE.to_string(),
2040            ENTITY_NAME.to_string(),
2041            FieldId::new(1),
2042            SchemaRowLayout::initial(
2043                fields
2044                    .iter()
2045                    .map(|field| (field.id(), field.slot()))
2046                    .collect(),
2047            ),
2048            fields,
2049            vec![PersistedIndexSnapshot::new(
2050                SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
2051                1,
2052                "by_code".to_string(),
2053                STORE_PATH.to_string(),
2054                true,
2055                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2056                    FieldId::new(3),
2057                    SchemaFieldSlot::new(2),
2058                    vec!["code".to_string()],
2059                    AcceptedFieldKind::Nat64,
2060                    false,
2061                )]),
2062                None,
2063            )],
2064        )
2065        .with_relations(vec![relation]);
2066        let constraints = AcceptedConstraintCatalog::initial(
2067            snapshot.fields(),
2068            snapshot.indexes(),
2069            snapshot.relations(),
2070        )
2071        .expect("mixed relation constraints should close");
2072        snapshot.with_constraint_catalog(constraints)
2073    }
2074
2075    fn other_snapshot() -> PersistedSchemaSnapshot {
2076        let fields = vec![
2077            PersistedFieldSnapshot::new_initial(
2078                FieldId::new(1),
2079                "id".to_string(),
2080                SchemaFieldSlot::new(0),
2081                AcceptedFieldKind::Nat64,
2082                Vec::new(),
2083                false,
2084                SchemaInsertDefault::None,
2085                FieldStorageDecode::ByKind,
2086                LeafCodec::Scalar(ScalarCodec::Nat64),
2087            ),
2088            PersistedFieldSnapshot::new_initial(
2089                FieldId::new(2),
2090                "value".to_string(),
2091                SchemaFieldSlot::new(1),
2092                AcceptedFieldKind::Nat64,
2093                Vec::new(),
2094                false,
2095                SchemaInsertDefault::None,
2096                FieldStorageDecode::ByKind,
2097                LeafCodec::Scalar(ScalarCodec::Nat64),
2098            ),
2099        ];
2100        PersistedSchemaSnapshot::new(
2101            SchemaVersion::initial(),
2102            OTHER_ENTITY_SOURCE.to_string(),
2103            OTHER_ENTITY_NAME.to_string(),
2104            FieldId::new(1),
2105            SchemaRowLayout::initial(
2106                fields
2107                    .iter()
2108                    .map(|field| (field.id(), field.slot()))
2109                    .collect(),
2110            ),
2111            fields,
2112        )
2113    }
2114
2115    fn initialize() -> DbSession<TestCanister> {
2116        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2117        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2118        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2119        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
2120        session
2121            .db
2122            .ensure_recovered_state()
2123            .expect("mixed relation database should initialize");
2124        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2125            STORE_PATH,
2126            AcceptedSchemaRevision::INITIAL,
2127            BTreeMap::from([
2128                (ENTITY_TAG, relation_snapshot()),
2129                (OTHER_ENTITY_TAG, other_snapshot()),
2130            ]),
2131            BTreeMap::from([
2132                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2133                ((ENTITY_TAG, source_key(PARENT_SOURCE)), FieldId::new(2)),
2134                ((ENTITY_TAG, source_key(CODE_SOURCE)), FieldId::new(3)),
2135                (
2136                    (OTHER_ENTITY_TAG, source_key(OTHER_ID_SOURCE)),
2137                    FieldId::new(1),
2138                ),
2139                (
2140                    (OTHER_ENTITY_TAG, source_key(OTHER_VALUE_SOURCE)),
2141                    FieldId::new(2),
2142                ),
2143            ]),
2144        );
2145        let store = session
2146            .db
2147            .store_handle(STORE_PATH)
2148            .expect("mixed relation store should resolve");
2149        crate::db::commit::publish_accepted_schema_candidate(
2150            STORE_PATH,
2151            store,
2152            AcceptedSchemaRevision::NONE,
2153            &candidate,
2154        )
2155        .expect("mixed relation candidate should publish");
2156        session
2157    }
2158
2159    fn patch(id: Option<u64>, parent: Option<u64>, code: Option<u64>) -> DynamicStructuralPatch {
2160        let mut fields = Vec::new();
2161        if let Some(id) = id {
2162            fields.push((
2163                "id".to_string(),
2164                DynamicWriteCell::Value(InputValue::Nat64(id)),
2165            ));
2166        }
2167        fields.push((
2168            "parent_id".to_string(),
2169            parent.map_or(DynamicWriteCell::Null, |parent| {
2170                DynamicWriteCell::Value(InputValue::Nat64(parent))
2171            }),
2172        ));
2173        if let Some(code) = code {
2174            fields.push((
2175                "code".to_string(),
2176                DynamicWriteCell::Value(InputValue::Nat64(code)),
2177            ));
2178        }
2179        DynamicStructuralPatch::new(fields)
2180    }
2181
2182    fn insert(id: u64, parent: Option<u64>) -> DynamicMutation {
2183        insert_with_code(id, parent, id)
2184    }
2185
2186    fn insert_with_code(id: u64, parent: Option<u64>, code: u64) -> DynamicMutation {
2187        DynamicMutation::Insert {
2188            entity: ENTITY_NAME.to_string(),
2189            patch: patch(Some(id), parent, Some(code)),
2190        }
2191    }
2192
2193    fn update_parent(id: u64, parent: Option<u64>) -> DynamicMutation {
2194        DynamicMutation::Update {
2195            entity: ENTITY_NAME.to_string(),
2196            key: InputValue::Nat64(id),
2197            patch: patch(None, parent, None),
2198        }
2199    }
2200
2201    fn update_code(id: u64, code: u64) -> DynamicMutation {
2202        DynamicMutation::Update {
2203            entity: ENTITY_NAME.to_string(),
2204            key: InputValue::Nat64(id),
2205            patch: DynamicStructuralPatch::new(vec![(
2206                "code".to_string(),
2207                DynamicWriteCell::Value(InputValue::Nat64(code)),
2208            )]),
2209        }
2210    }
2211
2212    fn delete(id: u64) -> DynamicMutation {
2213        DynamicMutation::Delete {
2214            entity: ENTITY_NAME.to_string(),
2215            key: InputValue::Nat64(id),
2216        }
2217    }
2218
2219    fn expected_row(id: u64, parent: Option<u64>) -> Vec<OutputValue> {
2220        expected_row_with_code(id, parent, id)
2221    }
2222
2223    fn expected_row_with_code(id: u64, parent: Option<u64>, code: u64) -> Vec<OutputValue> {
2224        vec![
2225            OutputValue::Nat64(id),
2226            parent.map_or(OutputValue::Null, OutputValue::Nat64),
2227            OutputValue::Nat64(code),
2228        ]
2229    }
2230
2231    fn other_patch(id: Option<u64>, value: u64) -> DynamicStructuralPatch {
2232        let mut fields = Vec::new();
2233        if let Some(id) = id {
2234            fields.push((
2235                "id".to_string(),
2236                DynamicWriteCell::Value(InputValue::Nat64(id)),
2237            ));
2238        }
2239        fields.push((
2240            "value".to_string(),
2241            DynamicWriteCell::Value(InputValue::Nat64(value)),
2242        ));
2243        DynamicStructuralPatch::new(fields)
2244    }
2245
2246    fn assert_relation_violation(error: &crate::error::InternalError) {
2247        let diagnostic = error
2248            .constraint_diagnostic()
2249            .expect("relation violations should retain their accepted constraint");
2250        assert_eq!(
2251            diagnostic.constraint_kind(),
2252            ConstraintDiagnosticKind::Relation,
2253        );
2254    }
2255
2256    #[test]
2257    fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
2258        let session = initialize();
2259        session
2260            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
2261            .expect("the initial relation should commit");
2262
2263        let blocked = session
2264            .execute_trusted_dynamic_mutation(&delete(1))
2265            .expect_err("an unaffected committed source must block target deletion");
2266        assert_relation_violation(&blocked);
2267
2268        let deleted = session
2269            .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
2270            .expect("a source and its target should delete atomically");
2271        assert_eq!(
2272            deleted.rows,
2273            vec![expected_row(2, Some(1)), expected_row(1, None)],
2274        );
2275
2276        session
2277            .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
2278            .expect("the update-away fixture should commit");
2279        let updated_away = session
2280            .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
2281            .expect("an updated final source may release a deleted target");
2282        assert_eq!(
2283            updated_away.rows,
2284            vec![expected_row(4, None), expected_row(3, None)],
2285        );
2286
2287        session
2288            .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
2289            .expect("the retained-reference fixture should commit");
2290        let retained = session
2291            .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
2292            .expect_err("a final updated source must still block target deletion");
2293        assert_relation_violation(&retained);
2294
2295        session
2296            .execute_trusted_dynamic_mutation(&insert(7, None))
2297            .expect("the inserted-reference fixture target should commit");
2298        let inserted_reference = session
2299            .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
2300            .expect_err("a final inserted source must not reference a deleted target");
2301        assert_relation_violation(&inserted_reference);
2302
2303        let inserted_target = session
2304            .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
2305            .expect("an inserted relation should see its batch-final target");
2306        assert_eq!(
2307            inserted_target.rows,
2308            vec![expected_row(10, Some(9)), expected_row(9, None)],
2309        );
2310
2311        session
2312            .execute_trusted_dynamic_mutation(&insert(11, None))
2313            .expect("the updated-reference fixture source should commit");
2314        let updated_target = session
2315            .execute_trusted_dynamic_mutation_batch(vec![
2316                update_parent(11, Some(12)),
2317                insert(12, None),
2318            ])
2319            .expect("an updated relation should see its batch-final target");
2320        assert_eq!(
2321            updated_target.rows,
2322            vec![expected_row(11, Some(12)), expected_row(12, None)],
2323        );
2324    }
2325
2326    #[test]
2327    fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
2328        let session = initialize();
2329        session
2330            .execute_trusted_dynamic_mutation(&insert(1, None))
2331            .expect("the primary mixed fixture row should commit");
2332        session
2333            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2334                entity: OTHER_ENTITY_NAME.to_string(),
2335                patch: other_patch(Some(1), 10),
2336            })
2337            .expect("the secondary mixed fixture row should commit");
2338
2339        let mixed_entity = session
2340            .execute_trusted_dynamic_mutation_batch(vec![
2341                update_code(1, 11),
2342                DynamicMutation::Update {
2343                    entity: OTHER_ENTITY_NAME.to_string(),
2344                    key: InputValue::Nat64(1),
2345                    patch: other_patch(None, 11),
2346                },
2347            ])
2348            .expect_err("one atomic batch must not cross accepted entities");
2349        assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
2350
2351        let missing = session
2352            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
2353            .expect_err("a late missing delete must reject the earlier staged update");
2354        assert_eq!(missing.class(), ErrorClass::NotFound);
2355
2356        session
2357            .execute_trusted_dynamic_mutation(&insert(2, None))
2358            .expect("the collision fixture should commit");
2359        let collision = session
2360            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
2361            .expect_err("an insert collision must reject the earlier staged update");
2362        assert_eq!(collision.class(), ErrorClass::Conflict);
2363        let failures_unchanged = session
2364            .execute_trusted_dynamic_mutation(&update_code(1, 1))
2365            .expect("failed batches must preserve the original unique value");
2366        assert_eq!(failures_unchanged.affected_rows, 0);
2367
2368        let replaced = session
2369            .execute_trusted_dynamic_mutation_batch(vec![
2370                update_code(1, 14),
2371                DynamicMutation::Replace {
2372                    entity: ENTITY_NAME.to_string(),
2373                    key: InputValue::Nat64(99),
2374                    patch: patch(None, None, Some(99)),
2375                },
2376            ])
2377            .expect("ordinary caller-key replace should insert its absent final row");
2378        assert_eq!(
2379            replaced.rows,
2380            vec![
2381                expected_row_with_code(1, None, 14),
2382                expected_row_with_code(99, None, 99),
2383            ],
2384        );
2385
2386        let unchanged = session
2387            .execute_trusted_dynamic_mutation(&update_code(1, 14))
2388            .expect("the successful mixed replace must publish its preceding update");
2389        assert_eq!(unchanged.affected_rows, 0);
2390        let other_unchanged = session
2391            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2392                entity: OTHER_ENTITY_NAME.to_string(),
2393                key: InputValue::Nat64(1),
2394                patch: other_patch(None, 10),
2395            })
2396            .expect("cross-entity rejection must preserve the secondary row");
2397        assert_eq!(other_unchanged.affected_rows, 0);
2398    }
2399
2400    #[test]
2401    fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
2402        let session = initialize();
2403        session
2404            .execute_trusted_dynamic_mutation_batch(vec![
2405                insert_with_code(1, None, 10),
2406                insert_with_code(2, None, 20),
2407            ])
2408            .expect("the unique-overlay fixture should commit");
2409
2410        let swapped = session
2411            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
2412            .expect("two final rows should atomically swap unique memberships");
2413        assert_eq!(
2414            swapped.rows,
2415            vec![
2416                expected_row_with_code(1, None, 20),
2417                expected_row_with_code(2, None, 10),
2418            ],
2419        );
2420
2421        let released = session
2422            .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
2423            .expect("a delete should release unique membership to a final inserted row");
2424        assert_eq!(
2425            released.rows,
2426            vec![
2427                expected_row_with_code(1, None, 20),
2428                expected_row_with_code(3, None, 20),
2429            ],
2430        );
2431    }
2432}
2433
2434#[cfg(test)]
2435mod identity_pre_key_tests {
2436    use super::{
2437        AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
2438        AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
2439        DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
2440        DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
2441        MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
2442        add_structural_mutation_staged_bytes, checked_pre_key_candidate_count,
2443        insert_key_exists_after_generation, validate_structural_mutation_result_bytes,
2444    };
2445    use crate::{
2446        db::{
2447            commit::{database_incarnation_id, forget_recovered_domain_for_tests},
2448            data::DataStore,
2449            executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
2450            index::IndexStore,
2451            integrity::{
2452                PhysicalUnitCheckpoint, QuickIntegrityStatus, RowInspectionLimits,
2453                execute_quick_integrity, execute_row_integrity_page,
2454            },
2455            journal::JournalTailStore,
2456            registry::{
2457                StoreAllocationIdentities, StoreAllocationIdentity, StoreRegistry,
2458                StoreRuntimeStorageCapabilities,
2459            },
2460            schema::{
2461                AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
2462                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2463                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2464                PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
2465                SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2466                accepted_schema_candidate_with_field_bindings_for_tests,
2467            },
2468            write_context::MutationMode,
2469        },
2470        error::{ErrorClass, ErrorOrigin, InternalError},
2471        testing::test_memory,
2472        traits::{CanisterKind, Path},
2473        types::{EntityTag, Timestamp},
2474        value::{InputValue, OutputValue, Value},
2475    };
2476    use icydb_schema::{FieldSourceKey, ScalarType};
2477    use std::{cell::RefCell, collections::BTreeMap, time::Instant};
2478
2479    const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
2480    const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
2481    const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
2482    const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
2483    const ENTITY_NAME: &str = "IdentityRow";
2484    const ENTITY_TAG: EntityTag = EntityTag::new(93);
2485    const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
2486
2487    struct TestCanister;
2488
2489    impl Path for TestCanister {
2490        const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
2491    }
2492
2493    impl CanisterKind for TestCanister {
2494        const COMMIT_MEMORY_ID: u8 = 45;
2495        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
2496        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
2497        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2498            "icydb.identity_pre_key_tests.integrity.progress.v1";
2499    }
2500
2501    thread_local! {
2502        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2503        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2504        static SCHEMA_STORE: RefCell<SchemaStore> =
2505            const { RefCell::new(SchemaStore::init_heap()) };
2506        static STORE_REGISTRY: StoreRegistry = {
2507            let mut registry = StoreRegistry::new();
2508            registry.register_store(
2509                STORE_PATH,
2510                &DATA_STORE,
2511                &INDEX_STORE,
2512                &SCHEMA_STORE,
2513                StoreAllocationIdentities::absent(),
2514                StoreRuntimeStorageCapabilities::heap(),
2515            ).expect("identity pre-key test store should register");
2516            registry
2517        };
2518        static JOURNALED_DATA_STORE: RefCell<DataStore> =
2519            RefCell::new(DataStore::init_journaled(test_memory(186)));
2520        static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
2521            RefCell::new(IndexStore::init_journaled(test_memory(187)));
2522        static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
2523            RefCell::new(SchemaStore::init_journaled(test_memory(188)));
2524        static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
2525            RefCell::new(JournalTailStore::init(test_memory(189)));
2526        static JOURNALED_STORE_REGISTRY: StoreRegistry = {
2527            let mut registry = StoreRegistry::new();
2528            registry.register_journaled_store(
2529                JOURNALED_STORE_PATH,
2530                &JOURNALED_DATA_STORE,
2531                &JOURNALED_INDEX_STORE,
2532                &JOURNALED_SCHEMA_STORE,
2533                &JOURNALED_TAIL_STORE,
2534                StoreAllocationIdentities::new_journaled(
2535                    StoreAllocationIdentity::new(186, "icydb.test.identity-range.data.v1"),
2536                    StoreAllocationIdentity::new(187, "icydb.test.identity-range.index.v1"),
2537                    StoreAllocationIdentity::new(188, "icydb.test.identity-range.schema.v1"),
2538                    StoreAllocationIdentity::new(189, "icydb.test.identity-range.journal.v1"),
2539                ),
2540                StoreRuntimeStorageCapabilities::journaled(),
2541            ).expect("identity range journaled store should register");
2542            registry
2543        };
2544    }
2545
2546    struct JournaledTestCanister;
2547
2548    impl Path for JournaledTestCanister {
2549        const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
2550    }
2551
2552    impl CanisterKind for JournaledTestCanister {
2553        const COMMIT_MEMORY_ID: u8 = 190;
2554        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
2555        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
2556        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2557            "icydb.identity_range_tests.integrity.progress.v1";
2558    }
2559
2560    fn source_key(source: &str) -> FieldSourceKey {
2561        FieldSourceKey::try_new(source).expect("identity test field source should admit")
2562    }
2563
2564    fn identity_snapshot(store_path: &str) -> PersistedSchemaSnapshot {
2565        let fields = vec![
2566            PersistedFieldSnapshot::new_initial_with_write_policy(
2567                FieldId::new(1),
2568                "id".to_string(),
2569                SchemaFieldSlot::new(0),
2570                AcceptedFieldKind::Nat64,
2571                Vec::new(),
2572                false,
2573                SchemaInsertDefault::None,
2574                SchemaFieldWritePolicy::from_model_policies(
2575                    Some(FieldInsertGeneration::Identity),
2576                    None,
2577                ),
2578                FieldStorageDecode::ByKind,
2579                LeafCodec::Scalar(ScalarCodec::Nat64),
2580            ),
2581            PersistedFieldSnapshot::new_initial(
2582                FieldId::new(2),
2583                "payload".to_string(),
2584                SchemaFieldSlot::new(1),
2585                AcceptedFieldKind::Nat64,
2586                Vec::new(),
2587                false,
2588                SchemaInsertDefault::None,
2589                FieldStorageDecode::ByKind,
2590                LeafCodec::Scalar(ScalarCodec::Nat64),
2591            ),
2592        ];
2593        PersistedSchemaSnapshot::new_with_indexes(
2594            SchemaVersion::initial(),
2595            ENTITY_SOURCE.to_string(),
2596            ENTITY_NAME.to_string(),
2597            FieldId::new(1),
2598            SchemaRowLayout::initial(
2599                fields
2600                    .iter()
2601                    .map(|field| (field.id(), field.slot()))
2602                    .collect(),
2603            ),
2604            fields,
2605            vec![PersistedIndexSnapshot::new(
2606                SchemaIndexId::new(1).expect("identity test index ID should admit"),
2607                1,
2608                "by_payload".to_string(),
2609                store_path.to_string(),
2610                false,
2611                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2612                    FieldId::new(2),
2613                    SchemaFieldSlot::new(1),
2614                    vec!["payload".to_string()],
2615                    AcceptedFieldKind::Nat64,
2616                    false,
2617                )]),
2618                None,
2619            )],
2620        )
2621    }
2622
2623    fn initialize() -> DbSession<TestCanister> {
2624        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2625        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2626        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2627        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
2628        session
2629            .db
2630            .ensure_recovered_state()
2631            .expect("identity pre-key test database should initialize");
2632        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2633            STORE_PATH,
2634            AcceptedSchemaRevision::INITIAL,
2635            BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH))]),
2636            BTreeMap::from([
2637                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2638                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2639            ]),
2640        );
2641        let store = session
2642            .db
2643            .store_handle(STORE_PATH)
2644            .expect("identity pre-key test store should resolve");
2645        crate::db::commit::publish_accepted_schema_candidate(
2646            STORE_PATH,
2647            store,
2648            AcceptedSchemaRevision::NONE,
2649            &candidate,
2650        )
2651        .expect("identity candidate should publish with explicit zero state");
2652        session
2653    }
2654
2655    fn initialize_journaled() -> DbSession<JournaledTestCanister> {
2656        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY);
2657        session
2658            .db
2659            .ensure_recovered_state()
2660            .expect("journaled identity database should initialize");
2661        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2662            JOURNALED_STORE_PATH,
2663            AcceptedSchemaRevision::INITIAL,
2664            BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH))]),
2665            BTreeMap::from([
2666                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2667                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2668            ]),
2669        );
2670        let store = session
2671            .db
2672            .store_handle(JOURNALED_STORE_PATH)
2673            .expect("journaled identity store should resolve");
2674        crate::db::commit::publish_accepted_schema_candidate(
2675            JOURNALED_STORE_PATH,
2676            store,
2677            AcceptedSchemaRevision::NONE,
2678            &candidate,
2679        )
2680        .expect("journaled identity candidate should publish");
2681        session
2682    }
2683
2684    fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
2685        AcceptedMutationIntentPatch::new()
2686            .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
2687    }
2688
2689    fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
2690        DynamicStructuralPatch::new(vec![(
2691            "payload".to_string(),
2692            DynamicWriteCell::Value(InputValue::Nat64(value)),
2693        )])
2694    }
2695
2696    fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
2697        vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
2698    }
2699
2700    fn assert_dynamic_payload(session: &DbSession<TestCanister>, key: u64, expected_payload: u64) {
2701        let unchanged = session
2702            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2703                entity: ENTITY_NAME.to_string(),
2704                key: InputValue::Nat64(key),
2705                patch: dynamic_payload_patch(expected_payload),
2706            })
2707            .expect("the expected row should remain readable through a no-op update");
2708        assert_eq!(unchanged.affected_rows, 0);
2709        assert_eq!(
2710            unchanged.rows,
2711            vec![expected_dynamic_row(key, expected_payload)],
2712        );
2713    }
2714
2715    fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
2716        values
2717            .iter()
2718            .map(|value| {
2719                AcceptedStructuralMutation::save(
2720                    MutationMode::Insert,
2721                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
2722                    payload_patch(*value),
2723                )
2724            })
2725            .collect()
2726    }
2727
2728    fn assert_identity_boundary(error: &InternalError) {
2729        assert_eq!(error.class(), ErrorClass::Unsupported);
2730        assert_eq!(error.origin(), ErrorOrigin::Identity);
2731    }
2732
2733    #[test]
2734    fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
2735        let generated = insert_key_exists_after_generation(true);
2736        assert_eq!(generated.class(), ErrorClass::Corruption);
2737        assert_eq!(generated.origin(), ErrorOrigin::Identity);
2738
2739        let ordinary = insert_key_exists_after_generation(false);
2740        assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
2741    }
2742
2743    #[cfg(target_pointer_width = "64")]
2744    #[test]
2745    fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
2746        let error = checked_pre_key_candidate_count(
2747            usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
2748        )
2749        .expect_err("candidate counts beyond u32 must reject");
2750        assert_identity_boundary(&error);
2751    }
2752
2753    #[test]
2754    #[expect(
2755        clippy::too_many_lines,
2756        reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
2757    )]
2758    fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
2759        let session = initialize();
2760        let seeded = session
2761            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2762            .expect("seed rows should commit");
2763        assert_eq!(seeded.affected_rows, 1);
2764
2765        let split = session
2766            .execute_trusted_dynamic_mutation_batch(vec![
2767                DynamicMutation::Update {
2768                    entity: ENTITY_NAME.to_string(),
2769                    key: InputValue::Nat64(1),
2770                    patch: dynamic_payload_patch(60),
2771                },
2772                DynamicMutation::Insert {
2773                    entity: ENTITY_NAME.to_string(),
2774                    patch: dynamic_payload_patch(40),
2775                },
2776            ])
2777            .expect("one holding should split atomically");
2778        assert_eq!(split.affected_rows, 2);
2779        assert_eq!(
2780            split.rows,
2781            vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
2782            "split after-images must retain input order and exact quantity",
2783        );
2784
2785        let rejected_split = session
2786            .execute_trusted_dynamic_mutation_batch(vec![
2787                DynamicMutation::Update {
2788                    entity: ENTITY_NAME.to_string(),
2789                    key: InputValue::Nat64(1),
2790                    patch: dynamic_payload_patch(50),
2791                },
2792                DynamicMutation::Insert {
2793                    entity: ENTITY_NAME.to_string(),
2794                    patch: DynamicStructuralPatch::new(Vec::new()),
2795                },
2796            ])
2797            .expect_err("an invalid split output must reject the staged source update");
2798        assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
2799        assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
2800        assert_dynamic_payload(&session, 1, 60);
2801        assert_dynamic_payload(&session, 2, 40);
2802
2803        let transfer = session
2804            .execute_trusted_dynamic_mutation_batch(vec![
2805                DynamicMutation::Update {
2806                    entity: ENTITY_NAME.to_string(),
2807                    key: InputValue::Nat64(1),
2808                    patch: dynamic_payload_patch(70),
2809                },
2810                DynamicMutation::Update {
2811                    entity: ENTITY_NAME.to_string(),
2812                    key: InputValue::Nat64(2),
2813                    patch: dynamic_payload_patch(30),
2814                },
2815            ])
2816            .expect("distinct transfer patches should share one atomic batch");
2817        assert_eq!(
2818            transfer.rows,
2819            vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
2820            "the transfer must preserve the exact total quantity",
2821        );
2822
2823        let merge = session
2824            .execute_trusted_dynamic_mutation_batch(vec![
2825                DynamicMutation::Delete {
2826                    entity: ENTITY_NAME.to_string(),
2827                    key: InputValue::Nat64(2),
2828                },
2829                DynamicMutation::Update {
2830                    entity: ENTITY_NAME.to_string(),
2831                    key: InputValue::Nat64(1),
2832                    patch: dynamic_payload_patch(100),
2833                },
2834            ])
2835            .expect("two holdings should merge atomically");
2836        assert_eq!(
2837            merge.rows,
2838            vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
2839            "delete before-images and update after-images must retain input order",
2840        );
2841
2842        let resplit = session
2843            .execute_trusted_dynamic_mutation_batch(vec![
2844                DynamicMutation::Update {
2845                    entity: ENTITY_NAME.to_string(),
2846                    key: InputValue::Nat64(1),
2847                    patch: dynamic_payload_patch(60),
2848                },
2849                DynamicMutation::Insert {
2850                    entity: ENTITY_NAME.to_string(),
2851                    patch: dynamic_payload_patch(40),
2852                },
2853            ])
2854            .expect("the merged holding should split again");
2855        assert_eq!(
2856            resplit.rows,
2857            vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
2858        );
2859
2860        let rejected_merge = session
2861            .execute_trusted_dynamic_mutation_batch(vec![
2862                DynamicMutation::Delete {
2863                    entity: ENTITY_NAME.to_string(),
2864                    key: InputValue::Nat64(3),
2865                },
2866                DynamicMutation::Update {
2867                    entity: ENTITY_NAME.to_string(),
2868                    key: InputValue::Nat64(99),
2869                    patch: dynamic_payload_patch(100),
2870                },
2871            ])
2872            .expect_err("a late missing merge target must preserve the earlier staged delete");
2873        assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
2874        assert_dynamic_payload(&session, 1, 60);
2875        assert_dynamic_payload(&session, 3, 40);
2876
2877        SCHEMA_STORE.with(|store| {
2878            let cursor = store
2879                .borrow()
2880                .identity_statement_cursor(
2881                    database_incarnation_id().expect("database incarnation should remain readable"),
2882                    ENTITY_TAG,
2883                    FieldId::new(1),
2884                    &AcceptedFieldKind::Nat64,
2885                )
2886                .expect("mixed Identity state should remain readable");
2887            assert_eq!(cursor.expected_high_water(), 3);
2888            assert!(!cursor.has_allocations());
2889        });
2890    }
2891
2892    #[test]
2893    fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
2894        let session = initialize();
2895        session
2896            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2897            .expect("the holding fixture should initialize");
2898
2899        let duplicate = session
2900            .execute_trusted_dynamic_mutation_batch(vec![
2901                DynamicMutation::Update {
2902                    entity: ENTITY_NAME.to_string(),
2903                    key: InputValue::Nat64(1),
2904                    patch: dynamic_payload_patch(60),
2905                },
2906                DynamicMutation::Delete {
2907                    entity: ENTITY_NAME.to_string(),
2908                    key: InputValue::Nat64(1),
2909                },
2910            ])
2911            .expect_err("duplicate targets across operation kinds must reject");
2912        assert!(matches!(
2913            duplicate.diagnostic().detail(),
2914            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2915                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
2916            }),
2917        ));
2918        assert_dynamic_payload(&session, 1, 100);
2919    }
2920
2921    #[test]
2922    fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
2923        let session = initialize();
2924        let empty = session
2925            .execute_trusted_dynamic_mutation_batch(Vec::new())
2926            .expect_err("an empty public batch must reject");
2927        assert!(matches!(
2928            empty.diagnostic().detail(),
2929            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2930                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
2931            }),
2932        ));
2933
2934        let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
2935            .map(|_| DynamicMutation::Delete {
2936                entity: ENTITY_NAME.to_string(),
2937                key: InputValue::Nat64(1),
2938            })
2939            .collect();
2940        let over_bound = session
2941            .execute_trusted_dynamic_mutation_batch(requests)
2942            .expect_err("operation cap plus one must reject before row resolution");
2943        assert!(matches!(
2944            over_bound.diagnostic().detail(),
2945            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2946                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
2947            }),
2948        ));
2949    }
2950
2951    #[test]
2952    fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
2953        let mut exact = 0;
2954        add_structural_mutation_staged_bytes(
2955            &mut exact,
2956            [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
2957        )
2958        .expect("the exact staged-byte boundary should admit");
2959        assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
2960
2961        let error = add_structural_mutation_staged_bytes(&mut exact, [1])
2962            .expect_err("one byte above the staged-byte boundary must reject");
2963        assert!(matches!(
2964            error.diagnostic().detail(),
2965            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2966                boundary:
2967                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
2968            }),
2969        ));
2970
2971        validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
2972            .expect("the exact result-byte boundary should admit");
2973        let error = validate_structural_mutation_result_bytes(
2974            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
2975        )
2976        .expect_err("one byte above the result-byte boundary must reject");
2977        assert!(matches!(
2978            error.diagnostic().detail(),
2979            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2980                boundary:
2981                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
2982            }),
2983        ));
2984    }
2985
2986    #[expect(
2987        clippy::too_many_lines,
2988        reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
2989    )]
2990    #[test]
2991    fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
2992        let session = initialize();
2993        let catalog = session
2994            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
2995            .expect("identity catalog should resolve");
2996        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
2997            .expect("identity row layout should build");
2998        let initial_description = session
2999            .try_describe_entity_by_name(ENTITY_NAME)
3000            .expect("accepted Identity description should resolve");
3001        let initial_identity = initial_description
3002            .identity()
3003            .expect("accepted Identity policy should be described");
3004        assert_eq!(initial_identity.field(), "id");
3005        assert_eq!(initial_identity.generator(), "Identity::next");
3006        assert_eq!(initial_identity.accepted_kind(), "nat64");
3007        assert_eq!(initial_identity.minimum(), 1);
3008        assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
3009        assert_eq!(initial_identity.high_water(), 0);
3010        assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
3011        assert!(!initial_identity.exhausted());
3012
3013        let rejected = session
3014            .execute_accepted_structural_save_batch(
3015                &catalog,
3016                &descriptor,
3017                batch(&[1_000, 2_000]),
3018                Timestamp::from_millis(6),
3019                |_| Err::<(), _>(InternalError::executor_unsupported()),
3020            )
3021            .expect_err("a rejected precommit result must not publish its tentative range");
3022        assert_eq!(rejected.class(), ErrorClass::Unsupported);
3023        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
3024
3025        let rows = session
3026            .execute_accepted_structural_save_batch(
3027                &catalog,
3028                &descriptor,
3029                batch(&[10, 20, 30]),
3030                Timestamp::from_millis(7),
3031                Ok,
3032            )
3033            .expect("one accepted batch should commit rows and one identity range");
3034        assert_eq!(
3035            rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
3036            vec![
3037                vec![Value::Nat64(1), Value::Nat64(10)],
3038                vec![Value::Nat64(2), Value::Nat64(20)],
3039                vec![Value::Nat64(3), Value::Nat64(30)],
3040            ],
3041        );
3042
3043        let dynamic = session
3044            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3045                entity: ENTITY_NAME.to_string(),
3046                patch: DynamicStructuralPatch::new(vec![(
3047                    "payload".to_string(),
3048                    DynamicWriteCell::Value(InputValue::Nat64(40)),
3049                )]),
3050            })
3051            .expect("dynamic omission should commit through shared Identity generation");
3052        assert_eq!(dynamic.affected_rows, 1);
3053
3054        for request in [
3055            DynamicMutation::Insert {
3056                entity: ENTITY_NAME.to_string(),
3057                patch: DynamicStructuralPatch::new(vec![
3058                    (
3059                        "id".to_string(),
3060                        DynamicWriteCell::Value(InputValue::Nat64(41)),
3061                    ),
3062                    (
3063                        "payload".to_string(),
3064                        DynamicWriteCell::Value(InputValue::Nat64(42)),
3065                    ),
3066                ]),
3067            },
3068            DynamicMutation::Update {
3069                entity: ENTITY_NAME.to_string(),
3070                key: InputValue::Nat64(1),
3071                patch: DynamicStructuralPatch::new(vec![(
3072                    "id".to_string(),
3073                    DynamicWriteCell::Default,
3074                )]),
3075            },
3076        ] {
3077            let error = session
3078                .execute_trusted_dynamic_mutation(&request)
3079                .expect_err("structural Identity authorship and regeneration must reject");
3080            assert_eq!(error.class(), ErrorClass::Unsupported);
3081            assert_eq!(error.origin(), ErrorOrigin::Executor);
3082        }
3083
3084        let binding = session
3085            .issue_typed_entity_binding(
3086                ENTITY_SOURCE,
3087                &[
3088                    DynamicTypedFieldBindingRequest::new(
3089                        ID_SOURCE.to_string(),
3090                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3091                        false,
3092                    ),
3093                    DynamicTypedFieldBindingRequest::new(
3094                        PAYLOAD_SOURCE.to_string(),
3095                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3096                        false,
3097                    ),
3098                ],
3099            )
3100            .expect("typed output should bind the Identity field");
3101        let typed_patch = binding
3102            .bind_write_fields(vec![(
3103                PAYLOAD_SOURCE.to_string(),
3104                DynamicWriteCell::Value(InputValue::Nat64(50)),
3105            )])
3106            .expect("typed payload should lower");
3107        let typed = session
3108            .execute_trusted_typed_mutation(
3109                &binding,
3110                &DynamicTypedMutation::Insert { patch: typed_patch },
3111            )
3112            .expect("typed omission should commit through shared Identity generation");
3113        assert_eq!(
3114            typed
3115                .expect("typed insert should return one mutation result")
3116                .affected_rows,
3117            1,
3118        );
3119        let explicit_typed_patch = binding
3120            .bind_write_fields(vec![
3121                (
3122                    ID_SOURCE.to_string(),
3123                    DynamicWriteCell::Value(InputValue::Nat64(51)),
3124                ),
3125                (
3126                    PAYLOAD_SOURCE.to_string(),
3127                    DynamicWriteCell::Value(InputValue::Nat64(52)),
3128                ),
3129            ])
3130            .expect("the low-level binding should retain exact authored intent");
3131        let explicit_typed_error = session
3132            .execute_trusted_typed_mutation(
3133                &binding,
3134                &DynamicTypedMutation::Insert {
3135                    patch: explicit_typed_patch,
3136                },
3137            )
3138            .expect_err("typed Identity authorship must reject before allocation");
3139        assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
3140        assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
3141
3142        let replace_error = session
3143            .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
3144                entity: ENTITY_NAME.to_string(),
3145                key: InputValue::Nat64(99),
3146                patch: DynamicStructuralPatch::new(vec![(
3147                    "payload".to_string(),
3148                    DynamicWriteCell::Value(InputValue::Nat64(60)),
3149                )]),
3150            })
3151            .expect_err("save-as-insert with a chosen Identity must reject");
3152        assert_eq!(replace_error.class(), ErrorClass::Unsupported);
3153        assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
3154
3155        #[cfg(feature = "sql")]
3156        {
3157            for sql in [
3158                "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
3159                "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
3160            ] {
3161                let _result = session
3162                    .execute_trusted_sql_mutation(sql)
3163                    .expect("SQL omission and DEFAULT should commit Identity generation");
3164            }
3165
3166            let error = session
3167                .execute_trusted_sql_mutation(
3168                    "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
3169                )
3170                .expect_err("an explicit SQL Identity value must reject before allocation");
3171            let diagnostic = error.diagnostic();
3172            assert_eq!(
3173                diagnostic.code(),
3174                icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
3175            );
3176            assert!(matches!(
3177                diagnostic.detail(),
3178                Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
3179                    boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
3180                }),
3181            ));
3182        }
3183
3184        let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
3185        assert_eq!(
3186            DATA_STORE.with(|store| store.borrow().len()),
3187            expected_committed
3188        );
3189        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("committed writes must leave active state readable");
3199            assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
3200            assert!(!cursor.has_allocations());
3201        });
3202        let committed_description = session
3203            .try_describe_entity_by_name(ENTITY_NAME)
3204            .expect("committed Identity description should resolve");
3205        let committed_identity = committed_description
3206            .identity()
3207            .expect("accepted Identity policy should remain described");
3208        assert_eq!(
3209            committed_identity.high_water(),
3210            u128::from(expected_committed),
3211        );
3212        assert_eq!(
3213            committed_identity.remaining(),
3214            u128::from(u64::MAX - expected_committed),
3215        );
3216        assert!(!committed_identity.exhausted());
3217    }
3218
3219    #[test]
3220    #[expect(
3221        clippy::too_many_lines,
3222        reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
3223    )]
3224    fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
3225        let session = initialize_journaled();
3226        let catalog = session
3227            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3228            .expect("journaled identity catalog should resolve");
3229        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3230            .expect("journaled identity row layout should build");
3231
3232        for (ordinal, interruption) in [
3233            MutationCommitInterruption::MarkerPersisted,
3234            MutationCommitInterruption::JournalPublished,
3235            MutationCommitInterruption::RowsPublished,
3236            MutationCommitInterruption::StateMaterialized,
3237        ]
3238        .into_iter()
3239        .enumerate()
3240        {
3241            interrupt_next_mutation_commit_for_tests(interruption);
3242            let interrupted = session.execute_accepted_structural_save_batch(
3243                &catalog,
3244                &descriptor,
3245                batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
3246                Timestamp::from_millis(8),
3247                Ok,
3248            );
3249            assert!(
3250                interrupted.is_err(),
3251                "the selected durable boundary should interrupt",
3252            );
3253
3254            let committed = session
3255                .execute_accepted_structural_save_batch(
3256                    &catalog,
3257                    &descriptor,
3258                    batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
3259                    Timestamp::from_millis(9),
3260                    Ok,
3261                )
3262                .expect("the next mutation must recover before allocating");
3263            let expected_high_water =
3264                u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
3265            assert_eq!(
3266                committed
3267                    .into_iter()
3268                    .map(|row| row.values)
3269                    .collect::<Vec<_>>(),
3270                vec![vec![
3271                    Value::Nat64(expected_high_water),
3272                    Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
3273                ]],
3274            );
3275            assert_eq!(
3276                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
3277                expected_high_water,
3278            );
3279            JOURNALED_SCHEMA_STORE.with(|store| {
3280                let cursor = store
3281                    .borrow()
3282                    .identity_statement_cursor(
3283                        database_incarnation_id()
3284                            .expect("database incarnation should remain readable"),
3285                        ENTITY_TAG,
3286                        FieldId::new(1),
3287                        &AcceptedFieldKind::Nat64,
3288                    )
3289                    .expect("guarded recovery must leave quiescent active state");
3290                assert_eq!(
3291                    cursor.expected_high_water(),
3292                    u128::from(expected_high_water),
3293                );
3294                assert!(!cursor.has_allocations());
3295            });
3296        }
3297
3298        for (ordinal, (interruption, deleted_key)) in [
3299            (MutationCommitInterruption::MarkerPersisted, 2),
3300            (MutationCommitInterruption::JournalPublished, 4),
3301            (MutationCommitInterruption::RowPrefixPublished, 6),
3302            (MutationCommitInterruption::RowsPublished, 8),
3303            (MutationCommitInterruption::StateMaterialized, 7),
3304        ]
3305        .into_iter()
3306        .enumerate()
3307        {
3308            let expected_payload =
3309                501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
3310            interrupt_next_mutation_commit_for_tests(interruption);
3311            let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
3312                DynamicMutation::Update {
3313                    entity: ENTITY_NAME.to_string(),
3314                    key: InputValue::Nat64(1),
3315                    patch: dynamic_payload_patch(expected_payload),
3316                },
3317                DynamicMutation::Delete {
3318                    entity: ENTITY_NAME.to_string(),
3319                    key: InputValue::Nat64(deleted_key),
3320                },
3321            ]);
3322            assert!(
3323                interrupted.is_err(),
3324                "the selected caller-key mixed publication boundary should interrupt",
3325            );
3326            let recovered_update = session
3327                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3328                    entity: ENTITY_NAME.to_string(),
3329                    key: InputValue::Nat64(1),
3330                    patch: dynamic_payload_patch(expected_payload),
3331                })
3332                .expect("guarded reentry should complete the marker-authorized mixed batch");
3333            assert_eq!(
3334                recovered_update.affected_rows, 0,
3335                "the recovered update must already expose its admitted final image",
3336            );
3337            let recovered_delete = session
3338                .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
3339                    entity: ENTITY_NAME.to_string(),
3340                    key: InputValue::Nat64(deleted_key),
3341                })
3342                .expect_err("the recovered delete must already be materialized");
3343            assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
3344            JOURNALED_SCHEMA_STORE.with(|store| {
3345                let cursor = store
3346                    .borrow()
3347                    .identity_statement_cursor(
3348                        database_incarnation_id()
3349                            .expect("database incarnation should remain readable"),
3350                        ENTITY_TAG,
3351                        FieldId::new(1),
3352                        &AcceptedFieldKind::Nat64,
3353                    )
3354                    .expect("caller-key recovery must preserve active Identity state");
3355                assert_eq!(cursor.expected_high_water(), 8);
3356                assert!(!cursor.has_allocations());
3357            });
3358        }
3359
3360        forget_recovered_domain_for_tests(&session.db)
3361            .expect("the final journal tail should remain recoverable");
3362        session
3363            .db
3364            .ensure_recovered_state()
3365            .expect("derived rebuild must not allocate another identity");
3366
3367        let quick = execute_quick_integrity(&session.db, catalog.inspection_plan())
3368            .expect("quiescent Identity control inventory should be inspectable");
3369        assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
3370        let row_page = execute_row_integrity_page(
3371            &session.db,
3372            catalog.inspection_plan(),
3373            PhysicalUnitCheckpoint::BeforeFirst,
3374            RowInspectionLimits::standard(),
3375        )
3376        .expect("Identity rows should remain within committed high-water");
3377        assert!(row_page.exhausted());
3378        assert!(row_page.findings().is_empty());
3379
3380        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
3381        assert!(
3382            JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
3383            "derived index rebuild should restore witnesses without allocating identities",
3384        );
3385        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
3386        JOURNALED_SCHEMA_STORE.with(|store| {
3387            let cursor = store
3388                .borrow()
3389                .identity_statement_cursor(
3390                    database_incarnation_id().expect("database incarnation should remain readable"),
3391                    ENTITY_TAG,
3392                    FieldId::new(1),
3393                    &AcceptedFieldKind::Nat64,
3394                )
3395                .expect("folded identity state should reopen without allocating");
3396            assert_eq!(cursor.expected_high_water(), 8);
3397            assert!(!cursor.has_allocations());
3398        });
3399    }
3400
3401    #[test]
3402    #[ignore = "release-closeout native timing probe for one marker-authorized Identity recovery"]
3403    fn identity_recovery_closeout_reports_guarded_reentry_time() {
3404        let session = initialize_journaled();
3405        let catalog = session
3406            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3407            .expect("journaled identity catalog should resolve");
3408        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3409            .expect("journaled identity row layout should build");
3410
3411        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
3412        let interrupted = session.execute_accepted_structural_save_batch(
3413            &catalog,
3414            &descriptor,
3415            batch(&[1]),
3416            Timestamp::from_millis(10),
3417            Ok,
3418        );
3419        assert!(
3420            interrupted.is_err(),
3421            "the selected publication boundary should interrupt",
3422        );
3423
3424        let start = Instant::now();
3425        let committed = session
3426            .execute_accepted_structural_save_batch(
3427                &catalog,
3428                &descriptor,
3429                batch(&[2]),
3430                Timestamp::from_millis(11),
3431                Ok,
3432            )
3433            .expect("guarded reentry should recover before allocation");
3434        let elapsed = start.elapsed();
3435        assert_eq!(
3436            committed
3437                .into_iter()
3438                .map(|row| row.values)
3439                .collect::<Vec<_>>(),
3440            vec![vec![Value::Nat64(2), Value::Nat64(2)]],
3441        );
3442
3443        println!(
3444            "identity recovery closeout: guarded_reentry_nanos={}",
3445            elapsed.as_nanos(),
3446        );
3447    }
3448}
3449
3450#[cfg(test)]
3451mod targeted_rule_mutation_tests {
3452    use super::{
3453        DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
3454        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
3455    };
3456    use crate::{
3457        db::{
3458            data::{DataStore, encode_input_value_for_candidate_field_contract},
3459            index::IndexStore,
3460            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
3461            schema::{
3462                AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
3463                AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
3464                AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
3465                ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
3466                PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
3467                ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
3468                SchemaRowLayout, SchemaStore, SchemaVersion,
3469                accepted_schema_candidate_with_catalogs_for_tests,
3470                build_record_newtype_composite_catalog_for_tests,
3471                empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
3472            },
3473        },
3474        error::{
3475            ConstraintDiagnostic, ConstraintDiagnosticKind, ConstraintValuePathComponent,
3476            InternalError,
3477        },
3478        traits::{CanisterKind, Path},
3479        types::EntityTag,
3480        value::InputValue,
3481    };
3482    use icydb_schema::{
3483        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
3484    };
3485    use std::{cell::RefCell, collections::BTreeMap};
3486
3487    const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
3488    const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
3489    const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
3490    const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
3491    const UPDATED_AT_SOURCE: &str =
3492        "session::write::targeted_rule_mutation_tests::Entity::updated_at";
3493    const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
3494    const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
3495    const DEGREE_MEMBER_SOURCE: &str =
3496        "session::write::targeted_rule_mutation_tests::Profile::degree";
3497    const DEGREE_RULE_SOURCE: &str =
3498        "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
3499
3500    struct TestCanister;
3501
3502    impl Path for TestCanister {
3503        const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
3504    }
3505
3506    impl CanisterKind for TestCanister {
3507        const COMMIT_MEMORY_ID: u8 = 43;
3508        const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
3509        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
3510        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3511            "icydb.targeted_mutation_tests.integrity.progress.v1";
3512    }
3513
3514    thread_local! {
3515        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3516        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3517        static SCHEMA_STORE: RefCell<SchemaStore> =
3518            const { RefCell::new(SchemaStore::init_heap()) };
3519        static STORE_REGISTRY: StoreRegistry = {
3520            let mut registry = StoreRegistry::new();
3521            registry.register_store(
3522                STORE_PATH,
3523                &DATA_STORE,
3524                &INDEX_STORE,
3525                &SCHEMA_STORE,
3526                StoreAllocationIdentities::absent(),
3527                StoreRuntimeStorageCapabilities::heap(),
3528            ).expect("targeted mutation test store should register");
3529            registry
3530        };
3531    }
3532
3533    fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
3534        parse(raw.to_string()).expect("test source identity should admit")
3535    }
3536
3537    fn profile_input(degree: u64) -> InputValue {
3538        InputValue::Map(vec![(
3539            InputValue::Text("degree".to_string()),
3540            InputValue::Nat64(degree),
3541        )])
3542    }
3543
3544    fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
3545        DynamicStructuralPatch::new(vec![
3546            (
3547                "id".to_string(),
3548                DynamicWriteCell::Value(InputValue::Nat64(id)),
3549            ),
3550            (
3551                "profile".to_string(),
3552                DynamicWriteCell::Value(profile_input(degree)),
3553            ),
3554        ])
3555    }
3556
3557    fn encoded_value(
3558        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3559        composite_catalog: &AcceptedCompositeCatalog,
3560        name: &str,
3561        kind: &AcceptedFieldKind,
3562        storage_decode: FieldStorageDecode,
3563        leaf_codec: LeafCodec,
3564        value: InputValue,
3565    ) -> Vec<u8> {
3566        let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
3567        encode_input_value_for_candidate_field_contract(
3568            enum_catalog,
3569            composite_catalog,
3570            field,
3571            value,
3572            &mut ValueAdmissionBudget::standard(),
3573        )
3574        .expect("test accepted value should encode")
3575    }
3576
3577    fn nat64_literal(
3578        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3579        composite_catalog: &AcceptedCompositeCatalog,
3580        value: u64,
3581    ) -> AcceptedCheckLiteralV1 {
3582        let kind = AcceptedFieldKind::Nat64;
3583        AcceptedCheckLiteralV1::from_accepted_parts(
3584            kind.clone(),
3585            FieldStorageDecode::ByKind,
3586            LeafCodec::Scalar(ScalarCodec::Nat64),
3587            encoded_value(
3588                enum_catalog,
3589                composite_catalog,
3590                "degree_bound",
3591                &kind,
3592                FieldStorageDecode::ByKind,
3593                LeafCodec::Scalar(ScalarCodec::Nat64),
3594                InputValue::Nat64(value),
3595            ),
3596        )
3597    }
3598
3599    fn targeted_diagnostic(error: &InternalError) -> &ConstraintDiagnostic {
3600        let diagnostic = error
3601            .constraint_diagnostic()
3602            .expect("targeted mutation should retain a public diagnostic");
3603        assert_eq!(
3604            diagnostic.constraint_kind(),
3605            ConstraintDiagnosticKind::TargetedRule
3606        );
3607        assert_eq!(diagnostic.field_paths(), &["profile".to_string()]);
3608        assert_eq!(
3609            diagnostic
3610                .value_path()
3611                .expect("targeted mutation should retain its typed value path")
3612                .components(),
3613            &[
3614                ConstraintValuePathComponent::RootField { field_id: 2 },
3615                ConstraintValuePathComponent::RecordMember {
3616                    composite_type_id: 1,
3617                    member_id: 1,
3618                },
3619            ],
3620        );
3621        diagnostic
3622    }
3623
3624    #[expect(
3625        clippy::too_many_lines,
3626        reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
3627    )]
3628    #[test]
3629    fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
3630        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3631        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3632        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3633
3634        let entity_tag = EntityTag::new(93);
3635        let enum_catalog = empty_accepted_enum_catalog_for_tests();
3636        let (composite_catalog, profile_type, degree_type, degree_member) =
3637            build_record_newtype_composite_catalog_for_tests(
3638                "tests::TargetedProfile".to_string(),
3639                "degree".to_string(),
3640                "tests::TargetedDegree".to_string(),
3641                AcceptedFieldKind::Nat64,
3642                &enum_catalog,
3643            )
3644            .expect("targeted mutation composites should close");
3645        let profile_kind = AcceptedFieldKind::Composite {
3646            type_id: profile_type,
3647        };
3648        let profile_default = encoded_value(
3649            &enum_catalog,
3650            &composite_catalog,
3651            "profile",
3652            &profile_kind,
3653            FieldStorageDecode::CatalogValue,
3654            LeafCodec::Structural,
3655            profile_input(12),
3656        );
3657        let fields = vec![
3658            PersistedFieldSnapshot::new_initial(
3659                FieldId::new(1),
3660                "id".to_string(),
3661                SchemaFieldSlot::new(0),
3662                AcceptedFieldKind::Nat64,
3663                Vec::new(),
3664                false,
3665                SchemaInsertDefault::None,
3666                FieldStorageDecode::ByKind,
3667                LeafCodec::Scalar(ScalarCodec::Nat64),
3668            ),
3669            PersistedFieldSnapshot::new_initial(
3670                FieldId::new(2),
3671                "profile".to_string(),
3672                SchemaFieldSlot::new(1),
3673                profile_kind,
3674                vec![PersistedNestedLeafSnapshot::new(
3675                    vec!["degree".to_string()],
3676                    AcceptedFieldKind::Composite {
3677                        type_id: degree_type,
3678                    },
3679                    false,
3680                )],
3681                false,
3682                SchemaInsertDefault::SlotPayload(profile_default),
3683                FieldStorageDecode::CatalogValue,
3684                LeafCodec::Structural,
3685            ),
3686            PersistedFieldSnapshot::new_initial_with_write_policy(
3687                FieldId::new(3),
3688                "updated_at".to_string(),
3689                SchemaFieldSlot::new(2),
3690                AcceptedFieldKind::Timestamp,
3691                Vec::new(),
3692                false,
3693                SchemaInsertDefault::None,
3694                SchemaFieldWritePolicy::from_model_policies(
3695                    None,
3696                    Some(FieldWriteManagement::UpdatedAt),
3697                ),
3698                FieldStorageDecode::ByKind,
3699                LeafCodec::Scalar(ScalarCodec::Timestamp),
3700            ),
3701        ];
3702        let mut snapshot = PersistedSchemaSnapshot::new(
3703            SchemaVersion::initial(),
3704            ENTITY_SOURCE.to_string(),
3705            "TargetedMutation".to_string(),
3706            FieldId::new(1),
3707            SchemaRowLayout::initial(
3708                fields
3709                    .iter()
3710                    .map(|field| (field.id(), field.slot()))
3711                    .collect(),
3712            ),
3713            fields,
3714        );
3715        let constraint_catalog = snapshot
3716            .constraint_catalog()
3717            .clone()
3718            .with_added_targeted_rule(
3719                "profile_degree_multiple".to_string(),
3720                ConstraintOrigin::Generated,
3721                AcceptedRuleTarget::new(
3722                    FieldId::new(2),
3723                    AcceptedNamedTypeIdentity::Composite(degree_type),
3724                ),
3725                AcceptedRuleOperation::MultipleOf {
3726                    divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
3727                },
3728            )
3729            .expect("targeted mutation rule should allocate");
3730        let targeted_rule_id = constraint_catalog
3731            .constraints()
3732            .last()
3733            .expect("targeted mutation rule should persist")
3734            .id();
3735        snapshot = snapshot.with_constraint_catalog(constraint_catalog);
3736
3737        let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
3738        let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
3739        let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
3740        let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
3741        let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
3742        let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
3743        let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
3744        let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
3745        let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
3746            BTreeMap::from([(entity_source, entity_tag)]),
3747            BTreeMap::from([
3748                ((entity_tag, id_source), FieldId::new(1)),
3749                ((entity_tag, profile_source), FieldId::new(2)),
3750                ((entity_tag, updated_at_source), FieldId::new(3)),
3751            ]),
3752            BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
3753            BTreeMap::new(),
3754            BTreeMap::new(),
3755        )
3756        .with_initial_named_types_for_tests(
3757            BTreeMap::from([
3758                (
3759                    profile_type_source,
3760                    AcceptedNamedTypeIdentity::Composite(profile_type),
3761                ),
3762                (
3763                    degree_type_source,
3764                    AcceptedNamedTypeIdentity::Composite(degree_type),
3765                ),
3766            ]),
3767            BTreeMap::new(),
3768            BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
3769        );
3770        let candidate = accepted_schema_candidate_with_catalogs_for_tests(
3771            STORE_PATH,
3772            AcceptedSchemaRevision::INITIAL,
3773            enum_catalog,
3774            composite_catalog,
3775            source_bindings,
3776            BTreeMap::from([(entity_tag, snapshot)]),
3777        );
3778
3779        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
3780        session
3781            .db
3782            .ensure_recovered_state()
3783            .expect("targeted mutation test database should initialize");
3784        let store = session
3785            .db
3786            .store_handle(STORE_PATH)
3787            .expect("targeted mutation test store should resolve");
3788        crate::db::commit::publish_accepted_schema_candidate(
3789            STORE_PATH,
3790            store,
3791            AcceptedSchemaRevision::NONE,
3792            &candidate,
3793        )
3794        .expect("targeted mutation candidate should publish");
3795
3796        let dynamic_error = session
3797            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3798                entity: "TargetedMutation".to_string(),
3799                patch: structural_patch(1, 12),
3800            })
3801            .expect_err("dynamic write must enforce the targeted rule");
3802        let dynamic_diagnostic = targeted_diagnostic(&dynamic_error);
3803        assert_eq!(dynamic_diagnostic.constraint_id(), targeted_rule_id.get());
3804
3805        let binding = session
3806            .issue_typed_entity_binding(
3807                ENTITY_SOURCE,
3808                &[
3809                    DynamicTypedFieldBindingRequest::new(
3810                        ID_SOURCE.to_string(),
3811                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3812                        false,
3813                    ),
3814                    DynamicTypedFieldBindingRequest::new(
3815                        PROFILE_SOURCE.to_string(),
3816                        DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
3817                        false,
3818                    ),
3819                    DynamicTypedFieldBindingRequest::new(
3820                        UPDATED_AT_SOURCE.to_string(),
3821                        DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
3822                        false,
3823                    ),
3824                ],
3825            )
3826            .expect("targeted typed binding should issue");
3827        let typed_patch = binding
3828            .bind_write_fields(vec![
3829                (
3830                    ID_SOURCE.to_string(),
3831                    DynamicWriteCell::Value(InputValue::Nat64(2)),
3832                ),
3833                (
3834                    PROFILE_SOURCE.to_string(),
3835                    DynamicWriteCell::Value(profile_input(12)),
3836                ),
3837            ])
3838            .expect("targeted typed patch should bind");
3839        let typed_error = session
3840            .execute_trusted_typed_mutation(
3841                &binding,
3842                &DynamicTypedMutation::Insert { patch: typed_patch },
3843            )
3844            .expect_err("typed write must enforce the targeted rule");
3845        assert_eq!(
3846            targeted_diagnostic(&typed_error).constraint_id(),
3847            targeted_rule_id.get()
3848        );
3849
3850        #[cfg(feature = "sql")]
3851        {
3852            let sql_error = session
3853                .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
3854                .expect_err("SQL default resolution must enforce the targeted rule");
3855            let crate::db::QueryError::Execute(execute) = sql_error else {
3856                panic!("targeted SQL write should fail at shared execution admission");
3857            };
3858            assert_eq!(
3859                targeted_diagnostic(execute.as_internal()).constraint_id(),
3860                targeted_rule_id.get()
3861            );
3862        }
3863
3864        session
3865            .execute_trusted_dynamic_mutation_batch(vec![
3866                DynamicMutation::Insert {
3867                    entity: "TargetedMutation".to_string(),
3868                    patch: structural_patch(4, 5),
3869                },
3870                DynamicMutation::Insert {
3871                    entity: "TargetedMutation".to_string(),
3872                    patch: structural_patch(5, 12),
3873                },
3874            ])
3875            .expect_err("one invalid targeted value must reject the whole batch");
3876        assert_eq!(
3877            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3878            Some(0),
3879            "no frontend or earlier valid batch row may escape targeted admission",
3880        );
3881
3882        let admitted = session
3883            .execute_trusted_dynamic_mutation_batch(vec![
3884                DynamicMutation::Insert {
3885                    entity: "TargetedMutation".to_string(),
3886                    patch: structural_patch(6, 5),
3887                },
3888                DynamicMutation::Insert {
3889                    entity: "TargetedMutation".to_string(),
3890                    patch: structural_patch(7, 10),
3891                },
3892            ])
3893            .expect("compliant targeted values should share one accepted batch");
3894        let [first, second] = admitted.rows.as_slice() else {
3895            panic!("the mixed targeted batch should return two rows");
3896        };
3897        let first_timestamp = first
3898            .get(2)
3899            .expect("the first mixed row should contain its managed timestamp");
3900        assert!(matches!(
3901            first_timestamp,
3902            crate::value::OutputValue::Timestamp(_)
3903        ));
3904        assert_eq!(
3905            second.get(2),
3906            Some(first_timestamp),
3907            "one accepted mixed batch must materialize one managed timestamp",
3908        );
3909        assert_eq!(
3910            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3911            Some(2),
3912        );
3913    }
3914}