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