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