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(&STORE_REGISTRY);
1600        session
1601            .db
1602            .ensure_recovered_state()
1603            .expect("typed adapter test database should initialize");
1604        publish(
1605            &session,
1606            AcceptedSchemaRevision::NONE,
1607            AcceptedSchemaRevision::INITIAL,
1608            BTreeMap::from([(
1609                entity_tag,
1610                snapshot(
1611                    ENTITY_SOURCE,
1612                    "Entity",
1613                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1614                ),
1615            )]),
1616            BTreeMap::from([
1617                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1618                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1619            ]),
1620        );
1621
1622        let initial_catalog = session
1623            .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1624            .expect("initial source catalog lookup should inspect")
1625            .expect("initial source catalog should exist");
1626        assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1627        let initial = session
1628            .issue_typed_entity_binding(
1629                entity_source(ENTITY_SOURCE).as_str(),
1630                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1631            )
1632            .expect("initial typed binding should issue");
1633        assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1634        assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1635        assert_eq!(initial.output_field_slot("value"), Some(1));
1636        let initial_patch = initial
1637            .bind_write_fields(vec![(
1638                VALUE_SOURCE.to_string(),
1639                DynamicWriteCell::Value(InputValue::Nat64(7)),
1640            )])
1641            .expect("source-bound patch should lower");
1642        assert_eq!(
1643            initial_patch.fields(),
1644            &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1645        );
1646
1647        publish(
1648            &session,
1649            AcceptedSchemaRevision::INITIAL,
1650            AcceptedSchemaRevision::new(2),
1651            BTreeMap::from([
1652                (
1653                    entity_tag,
1654                    snapshot(
1655                        ENTITY_SOURCE,
1656                        "RenamedEntity",
1657                        vec![
1658                            nat64_field(1, "id", 0),
1659                            nat64_field(2, "renamed_value", 1),
1660                            nat64_field(3, "value", 2),
1661                        ],
1662                    ),
1663                ),
1664                (
1665                    other_entity_tag,
1666                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1667                ),
1668            ]),
1669            BTreeMap::from([
1670                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1671                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1672                (
1673                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1674                    FieldId::new(3),
1675                ),
1676                (
1677                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1678                    FieldId::new(1),
1679                ),
1680            ]),
1681        );
1682
1683        let stale_authority = session
1684            .ensure_accepted_schema_authority_is_current_for_store_path(
1685                STORE_PATH,
1686                initial_catalog.value_catalog_handle().authority(),
1687            )
1688            .expect_err("the initial accepted authority must be stale after revision two");
1689        assert_eq!(
1690            stale_authority.diagnostic_facts(),
1691            vec![
1692                (
1693                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedRevision,
1694                    AcceptedSchemaRevision::INITIAL.get(),
1695                ),
1696                (
1697                    icydb_diagnostic_code::DiagnosticFactTag::CurrentRevision,
1698                    AcceptedSchemaRevision::new(2).get(),
1699                ),
1700            ],
1701        );
1702
1703        assert!(
1704            !session
1705                .typed_entity_binding_is_current(&initial)
1706                .expect("renamed binding currentness should inspect")
1707        );
1708        let renamed = session
1709            .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1710            .expect("renamed source-bound adapter should rebind");
1711        assert_eq!(renamed.entity(), "RenamedEntity");
1712        assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1713        assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1714        assert_eq!(renamed.output_field_slot("value"), None);
1715
1716        publish(
1717            &session,
1718            AcceptedSchemaRevision::new(2),
1719            AcceptedSchemaRevision::new(3),
1720            BTreeMap::from([
1721                (
1722                    entity_tag,
1723                    snapshot(
1724                        ENTITY_SOURCE,
1725                        "RenamedEntity",
1726                        vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1727                    ),
1728                ),
1729                (
1730                    other_entity_tag,
1731                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1732                ),
1733            ]),
1734            BTreeMap::from([
1735                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1736                (
1737                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1738                    FieldId::new(2),
1739                ),
1740                (
1741                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1742                    FieldId::new(1),
1743                ),
1744            ]),
1745        );
1746
1747        assert!(matches!(
1748            session.issue_typed_entity_binding(
1749                ENTITY_SOURCE,
1750                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1751            ),
1752            Err(DynamicTypedBindingError::FieldUnavailable),
1753        ));
1754        assert!(
1755            !session
1756                .typed_entity_binding_is_current(&renamed)
1757                .expect("removed source binding should become stale")
1758        );
1759
1760        let replacement = session
1761            .issue_typed_entity_binding(
1762                ENTITY_SOURCE,
1763                &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
1764            )
1765            .expect("explicit replacement source should bind");
1766        assert!(
1767            session
1768                .execute_trusted_typed_mutation(
1769                    &replacement,
1770                    &DynamicTypedMutation::Insert {
1771                        patch: initial_patch
1772                    },
1773                )
1774                .expect("cross-binding patch should fail closed")
1775                .is_none()
1776        );
1777        let patch = replacement
1778            .bind_write_fields(vec![
1779                (
1780                    ID_SOURCE.to_string(),
1781                    DynamicWriteCell::Value(InputValue::Nat64(1)),
1782                ),
1783                (
1784                    REPLACEMENT_SOURCE.to_string(),
1785                    DynamicWriteCell::Value(InputValue::Nat64(9)),
1786                ),
1787            ])
1788            .expect("replacement source write should bind by accepted IDs and slots");
1789        let result = session
1790            .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
1791            .expect("typed insert should use the accepted mutation pipeline")
1792            .expect("replacement binding should remain current");
1793        assert_eq!(result.entity, "RenamedEntity");
1794        assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1795        assert_eq!(
1796            result.rows,
1797            vec![vec![
1798                crate::value::OutputValue::Nat64(1),
1799                crate::value::OutputValue::Nat64(9)
1800            ]]
1801        );
1802        assert_eq!(result.affected_rows, 1);
1803
1804        let second_patch = replacement
1805            .bind_write_fields(vec![
1806                (
1807                    ID_SOURCE.to_string(),
1808                    DynamicWriteCell::Value(InputValue::Nat64(2)),
1809                ),
1810                (
1811                    REPLACEMENT_SOURCE.to_string(),
1812                    DynamicWriteCell::Value(InputValue::Nat64(10)),
1813                ),
1814            ])
1815            .expect("second source-bound patch should lower");
1816        session
1817            .execute_trusted_typed_mutation(
1818                &replacement,
1819                &DynamicTypedMutation::Insert {
1820                    patch: second_patch,
1821                },
1822            )
1823            .expect("second typed insert should use the accepted mutation pipeline")
1824            .expect("replacement binding should remain current");
1825
1826        {
1827            let query = crate::db::DynamicQuery::new("RenamedEntity")
1828                .select(["id", "value"])
1829                .order_by(crate::db::asc("id"))
1830                .limit(1);
1831            let result = session
1832                .execute_trusted_dynamic_query(&query)
1833                .expect("SQL-free dynamic execution should use accepted authority");
1834            assert_eq!(result.entity, "RenamedEntity");
1835            assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1836            assert_eq!(
1837                result.rows,
1838                vec![vec![
1839                    crate::value::OutputValue::Nat64(1),
1840                    crate::value::OutputValue::Nat64(9)
1841                ]]
1842            );
1843            assert_eq!(result.row_count, 1);
1844            assert_query_diagnostic(
1845                session
1846                    .execute_trusted_dynamic_query(&query.cursor("00"))
1847                    .expect_err("scalar execution must reject grouped cursor state"),
1848                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1849                icydb_diagnostic_code::ErrorOrigin::Query,
1850                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1851                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1852                },
1853            );
1854            assert_query_diagnostic(
1855                session
1856                    .execute_public_dynamic_grouped_query(
1857                        &crate::db::DynamicQuery::new("RenamedEntity").grouped_limits(1, 1024),
1858                    )
1859                    .expect_err("grouped execution must reject scalar query state"),
1860                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1861                icydb_diagnostic_code::ErrorOrigin::Query,
1862                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1863                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1864                },
1865            );
1866
1867            let grouped_query = crate::db::DynamicQuery::new("RenamedEntity")
1868                .filter(crate::db::FieldRef::new("id").eq(1_u64))
1869                .group_by("value")
1870                .aggregate(crate::db::count())
1871                .grouped_limits(1, 1024)
1872                .limit(1);
1873            let grouped = session
1874                .execute_public_dynamic_grouped_query(&grouped_query)
1875                .expect("SQL-free grouped execution should use accepted authority");
1876            let typed_grouped = session
1877                .execute_public_dynamic_grouped_query_for_typed_binding(
1878                    &replacement,
1879                    &grouped_query,
1880                )
1881                .expect("typed grouped execution should inspect accepted authority")
1882                .expect("replacement binding should remain current");
1883            assert_eq!(typed_grouped, grouped);
1884            assert!(
1885                session
1886                    .execute_public_dynamic_grouped_query_for_typed_binding(
1887                        &renamed,
1888                        &grouped_query,
1889                    )
1890                    .expect("stale grouped binding should inspect accepted authority")
1891                    .is_none(),
1892                "stale typed grouped bindings must fail closed before execution"
1893            );
1894            assert_eq!(grouped.entity, "RenamedEntity");
1895            assert_eq!(grouped.row_count, 1);
1896            assert_eq!(grouped.rows.len(), 1);
1897            assert_eq!(
1898                grouped.rows[0].group_key(),
1899                &[crate::value::OutputValue::Nat64(9)]
1900            );
1901            assert_eq!(
1902                grouped.rows[0].aggregate_values(),
1903                &[crate::value::OutputValue::Nat64(1)]
1904            );
1905            assert_eq!(grouped.next_cursor, None);
1906
1907            assert_query_diagnostic(
1908                session
1909                    .execute_public_dynamic_grouped_query(&grouped_query.clone().select(["value"]))
1910                    .expect_err("grouped output must reject scalar selection"),
1911                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1912                icydb_diagnostic_code::ErrorOrigin::Query,
1913                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1914                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1915                },
1916            );
1917            assert_query_diagnostic(
1918                session
1919                    .execute_public_dynamic_grouped_query(
1920                        &crate::db::DynamicQuery::new("RenamedEntity")
1921                            .group_by("value")
1922                            .aggregate(crate::db::count()),
1923                    )
1924                    .expect_err("public grouped execution must require explicit limits"),
1925                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1926                icydb_diagnostic_code::ErrorOrigin::Query,
1927                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1928                    reason:
1929                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1930                },
1931            );
1932            assert_query_diagnostic(
1933                session
1934                    .execute_trusted_dynamic_grouped_query(
1935                        &crate::db::DynamicQuery::new("RenamedEntity")
1936                            .group_by("value")
1937                            .aggregate(crate::db::count())
1938                            .grouped_limits(0, 1024),
1939                    )
1940                    .expect_err("trusted grouped execution must reject zero limits"),
1941                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1942                icydb_diagnostic_code::ErrorOrigin::Query,
1943                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1944                    reason:
1945                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1946                },
1947            );
1948            assert_query_diagnostic(
1949                session
1950                    .execute_public_dynamic_grouped_query(&grouped_query.grouped_limits(101, 1024))
1951                    .expect_err("public grouped execution must enforce its group budget"),
1952                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1953                icydb_diagnostic_code::ErrorOrigin::Query,
1954                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1955                    reason:
1956                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryExceedsBudget,
1957                },
1958            );
1959
1960            let paged_query = crate::db::DynamicQuery::new("RenamedEntity")
1961                .group_by("value")
1962                .aggregate(crate::db::count())
1963                .grouped_limits(2, 1024)
1964                .limit(1);
1965            assert_query_diagnostic(
1966                session
1967                    .execute_public_dynamic_grouped_query(&paged_query)
1968                    .expect_err("public grouped execution must reject an unbounded full scan"),
1969                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1970                icydb_diagnostic_code::ErrorOrigin::Query,
1971                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1972                    reason:
1973                        icydb_diagnostic_code::QueryReadAdmissionCode::UnboundedFullScanRejected,
1974                },
1975            );
1976            let first_page = session
1977                .execute_trusted_dynamic_grouped_query(&paged_query)
1978                .expect("SQL-free grouped first page should execute");
1979            assert_eq!(first_page.row_count, 1);
1980            assert_eq!(
1981                first_page.rows[0].group_key(),
1982                &[crate::value::OutputValue::Nat64(9)]
1983            );
1984            let cursor = first_page
1985                .next_cursor
1986                .expect("first grouped page should return a continuation cursor");
1987            assert_query_diagnostic(
1988                session
1989                    .execute_trusted_dynamic_grouped_query(
1990                        &paged_query.clone().cursor(format!("{cursor}0")),
1991                    )
1992                    .expect_err("tampered grouped cursor must fail closed"),
1993                icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
1994                icydb_diagnostic_code::ErrorOrigin::Cursor,
1995                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1996                    kind: icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor,
1997                },
1998            );
1999            let second_page = session
2000                .execute_trusted_dynamic_grouped_query(&paged_query.cursor(cursor))
2001                .expect("SQL-free grouped continuation should execute");
2002            assert_eq!(second_page.row_count, 1);
2003            assert_eq!(
2004                second_page.rows[0].group_key(),
2005                &[crate::value::OutputValue::Nat64(10)]
2006            );
2007            assert_eq!(second_page.next_cursor, None);
2008        }
2009    }
2010}
2011
2012#[cfg(test)]
2013mod mixed_relation_batch_tests {
2014    use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
2015    use crate::{
2016        db::{
2017            data::DataStore,
2018            index::IndexStore,
2019            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
2020            schema::{
2021                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
2022                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2023                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2024                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
2025                SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
2026                SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
2027            },
2028        },
2029        error::ErrorClass,
2030        traits::{CanisterKind, Path},
2031        types::EntityTag,
2032        value::{InputValue, OutputValue},
2033    };
2034    use icydb_schema::FieldSourceKey;
2035    use std::{cell::RefCell, collections::BTreeMap};
2036
2037    const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
2038    const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
2039    const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
2040    const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
2041    const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
2042    const ENTITY_NAME: &str = "MixedRelationNode";
2043    const ENTITY_TAG: EntityTag = EntityTag::new(94);
2044    const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
2045    const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
2046    const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
2047    const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
2048    const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
2049
2050    struct TestCanister;
2051
2052    impl Path for TestCanister {
2053        const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
2054    }
2055
2056    impl CanisterKind for TestCanister {
2057        const COMMIT_MEMORY_ID: u8 = 47;
2058        const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
2059        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
2060        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2061            "icydb.mixed_relation_batch_tests.integrity.progress.v1";
2062    }
2063
2064    thread_local! {
2065        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2066        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2067        static SCHEMA_STORE: RefCell<SchemaStore> =
2068            const { RefCell::new(SchemaStore::init_heap()) };
2069        static STORE_REGISTRY: StoreRegistry = {
2070            let mut registry = StoreRegistry::new();
2071            registry.register_store(
2072                STORE_PATH,
2073                &DATA_STORE,
2074                &INDEX_STORE,
2075                &SCHEMA_STORE,
2076                StoreAllocationIdentities::absent(),
2077                StoreRuntimeStorageCapabilities::heap(),
2078            ).expect("mixed relation test store should register");
2079            registry
2080        };
2081    }
2082
2083    fn source_key(source: &str) -> FieldSourceKey {
2084        FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
2085    }
2086
2087    fn relation_snapshot() -> PersistedSchemaSnapshot {
2088        let fields = vec![
2089            PersistedFieldSnapshot::new_initial(
2090                FieldId::new(1),
2091                "id".to_string(),
2092                SchemaFieldSlot::new(0),
2093                AcceptedFieldKind::Nat64,
2094                Vec::new(),
2095                false,
2096                SchemaInsertDefault::None,
2097                FieldStorageDecode::ByKind,
2098                LeafCodec::Scalar(ScalarCodec::Nat64),
2099            ),
2100            PersistedFieldSnapshot::new_initial(
2101                FieldId::new(2),
2102                "parent_id".to_string(),
2103                SchemaFieldSlot::new(1),
2104                AcceptedFieldKind::Relation {
2105                    target_path: ENTITY_SOURCE.to_string(),
2106                    target_entity_name: ENTITY_NAME.to_string(),
2107                    target_entity_tag: ENTITY_TAG,
2108                    target_store_path: STORE_PATH.to_string(),
2109                    key_kind: Box::new(AcceptedFieldKind::Nat64),
2110                },
2111                Vec::new(),
2112                true,
2113                SchemaInsertDefault::None,
2114                FieldStorageDecode::ByKind,
2115                LeafCodec::Scalar(ScalarCodec::Nat64),
2116            ),
2117            PersistedFieldSnapshot::new_initial(
2118                FieldId::new(3),
2119                "code".to_string(),
2120                SchemaFieldSlot::new(2),
2121                AcceptedFieldKind::Nat64,
2122                Vec::new(),
2123                false,
2124                SchemaInsertDefault::None,
2125                FieldStorageDecode::ByKind,
2126                LeafCodec::Scalar(ScalarCodec::Nat64),
2127            ),
2128        ];
2129        let relation = PersistedRelationEdgeSnapshot::new(
2130            RelationId::new(1).expect("mixed relation identity should be non-zero"),
2131            "parent".to_string(),
2132            ENTITY_SOURCE.to_string(),
2133            vec![FieldId::new(2)],
2134        );
2135        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2136            SchemaVersion::initial(),
2137            ENTITY_SOURCE.to_string(),
2138            ENTITY_NAME.to_string(),
2139            FieldId::new(1),
2140            SchemaRowLayout::initial(
2141                fields
2142                    .iter()
2143                    .map(|field| (field.id(), field.slot()))
2144                    .collect(),
2145            ),
2146            fields,
2147            vec![PersistedIndexSnapshot::new(
2148                SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
2149                1,
2150                "by_code".to_string(),
2151                STORE_PATH.to_string(),
2152                true,
2153                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2154                    FieldId::new(3),
2155                    SchemaFieldSlot::new(2),
2156                    vec!["code".to_string()],
2157                    AcceptedFieldKind::Nat64,
2158                    false,
2159                )]),
2160                None,
2161            )],
2162        )
2163        .with_relations(vec![relation]);
2164        let constraints = AcceptedConstraintCatalog::initial(
2165            snapshot.fields(),
2166            snapshot.indexes(),
2167            snapshot.relations(),
2168        )
2169        .expect("mixed relation constraints should close");
2170        snapshot.with_constraint_catalog(constraints)
2171    }
2172
2173    fn other_snapshot() -> PersistedSchemaSnapshot {
2174        let fields = vec![
2175            PersistedFieldSnapshot::new_initial(
2176                FieldId::new(1),
2177                "id".to_string(),
2178                SchemaFieldSlot::new(0),
2179                AcceptedFieldKind::Nat64,
2180                Vec::new(),
2181                false,
2182                SchemaInsertDefault::None,
2183                FieldStorageDecode::ByKind,
2184                LeafCodec::Scalar(ScalarCodec::Nat64),
2185            ),
2186            PersistedFieldSnapshot::new_initial(
2187                FieldId::new(2),
2188                "value".to_string(),
2189                SchemaFieldSlot::new(1),
2190                AcceptedFieldKind::Nat64,
2191                Vec::new(),
2192                false,
2193                SchemaInsertDefault::None,
2194                FieldStorageDecode::ByKind,
2195                LeafCodec::Scalar(ScalarCodec::Nat64),
2196            ),
2197        ];
2198        PersistedSchemaSnapshot::new(
2199            SchemaVersion::initial(),
2200            OTHER_ENTITY_SOURCE.to_string(),
2201            OTHER_ENTITY_NAME.to_string(),
2202            FieldId::new(1),
2203            SchemaRowLayout::initial(
2204                fields
2205                    .iter()
2206                    .map(|field| (field.id(), field.slot()))
2207                    .collect(),
2208            ),
2209            fields,
2210        )
2211    }
2212
2213    fn initialize() -> DbSession<TestCanister> {
2214        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2215        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2216        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2217        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
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 mixed_relation_validation_uses_the_complete_final_row_overlay() {
2353        let session = initialize();
2354        session
2355            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
2356            .expect("the initial relation should commit");
2357
2358        let blocked = session
2359            .execute_trusted_dynamic_mutation(&delete(1))
2360            .expect_err("an unaffected committed source must block target deletion");
2361        assert_relation_violation(&blocked);
2362
2363        let deleted = session
2364            .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
2365            .expect("a source and its target should delete atomically");
2366        assert_eq!(
2367            deleted.rows,
2368            vec![expected_row(2, Some(1)), expected_row(1, None)],
2369        );
2370
2371        session
2372            .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
2373            .expect("the update-away fixture should commit");
2374        let updated_away = session
2375            .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
2376            .expect("an updated final source may release a deleted target");
2377        assert_eq!(
2378            updated_away.rows,
2379            vec![expected_row(4, None), expected_row(3, None)],
2380        );
2381
2382        session
2383            .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
2384            .expect("the retained-reference fixture should commit");
2385        let retained = session
2386            .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
2387            .expect_err("a final updated source must still block target deletion");
2388        assert_relation_violation(&retained);
2389
2390        session
2391            .execute_trusted_dynamic_mutation(&insert(7, None))
2392            .expect("the inserted-reference fixture target should commit");
2393        let inserted_reference = session
2394            .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
2395            .expect_err("a final inserted source must not reference a deleted target");
2396        assert_relation_violation(&inserted_reference);
2397
2398        let inserted_target = session
2399            .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
2400            .expect("an inserted relation should see its batch-final target");
2401        assert_eq!(
2402            inserted_target.rows,
2403            vec![expected_row(10, Some(9)), expected_row(9, None)],
2404        );
2405
2406        session
2407            .execute_trusted_dynamic_mutation(&insert(11, None))
2408            .expect("the updated-reference fixture source should commit");
2409        let updated_target = session
2410            .execute_trusted_dynamic_mutation_batch(vec![
2411                update_parent(11, Some(12)),
2412                insert(12, None),
2413            ])
2414            .expect("an updated relation should see its batch-final target");
2415        assert_eq!(
2416            updated_target.rows,
2417            vec![expected_row(11, Some(12)), expected_row(12, None)],
2418        );
2419    }
2420
2421    #[test]
2422    fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
2423        let session = initialize();
2424        session
2425            .execute_trusted_dynamic_mutation(&insert(1, None))
2426            .expect("the primary mixed fixture row should commit");
2427        session
2428            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2429                entity: OTHER_ENTITY_NAME.to_string(),
2430                patch: other_patch(Some(1), 10),
2431            })
2432            .expect("the secondary mixed fixture row should commit");
2433
2434        let mixed_entity = session
2435            .execute_trusted_dynamic_mutation_batch(vec![
2436                update_code(1, 11),
2437                DynamicMutation::Update {
2438                    entity: OTHER_ENTITY_NAME.to_string(),
2439                    key: InputValue::Nat64(1),
2440                    patch: other_patch(None, 11),
2441                },
2442            ])
2443            .expect_err("one atomic batch must not cross accepted entities");
2444        assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
2445        assert_eq!(
2446            mixed_entity.diagnostic_facts(),
2447            vec![
2448                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
2449                (
2450                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedEntityTag,
2451                    ENTITY_TAG.value(),
2452                ),
2453                (
2454                    icydb_diagnostic_code::DiagnosticFactTag::ActualEntityTag,
2455                    OTHER_ENTITY_TAG.value(),
2456                ),
2457            ],
2458        );
2459
2460        let missing = session
2461            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
2462            .expect_err("a late missing delete must reject the earlier staged update");
2463        assert_eq!(missing.class(), ErrorClass::NotFound);
2464
2465        session
2466            .execute_trusted_dynamic_mutation(&insert(2, None))
2467            .expect("the collision fixture should commit");
2468        let collision = session
2469            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
2470            .expect_err("an insert collision must reject the earlier staged update");
2471        assert_eq!(collision.class(), ErrorClass::Conflict);
2472        let failures_unchanged = session
2473            .execute_trusted_dynamic_mutation(&update_code(1, 1))
2474            .expect("failed batches must preserve the original unique value");
2475        assert_eq!(failures_unchanged.affected_rows, 0);
2476
2477        let replaced = session
2478            .execute_trusted_dynamic_mutation_batch(vec![
2479                update_code(1, 14),
2480                DynamicMutation::Replace {
2481                    entity: ENTITY_NAME.to_string(),
2482                    key: InputValue::Nat64(99),
2483                    patch: patch(None, None, Some(99)),
2484                },
2485            ])
2486            .expect("ordinary caller-key replace should insert its absent final row");
2487        assert_eq!(
2488            replaced.rows,
2489            vec![
2490                expected_row_with_code(1, None, 14),
2491                expected_row_with_code(99, None, 99),
2492            ],
2493        );
2494
2495        let unchanged = session
2496            .execute_trusted_dynamic_mutation(&update_code(1, 14))
2497            .expect("the successful mixed replace must publish its preceding update");
2498        assert_eq!(unchanged.affected_rows, 0);
2499        let other_unchanged = session
2500            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2501                entity: OTHER_ENTITY_NAME.to_string(),
2502                key: InputValue::Nat64(1),
2503                patch: other_patch(None, 10),
2504            })
2505            .expect("cross-entity rejection must preserve the secondary row");
2506        assert_eq!(other_unchanged.affected_rows, 0);
2507    }
2508
2509    #[test]
2510    fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
2511        let session = initialize();
2512        session
2513            .execute_trusted_dynamic_mutation_batch(vec![
2514                insert_with_code(1, None, 10),
2515                insert_with_code(2, None, 20),
2516            ])
2517            .expect("the unique-overlay fixture should commit");
2518
2519        let swapped = session
2520            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
2521            .expect("two final rows should atomically swap unique memberships");
2522        assert_eq!(
2523            swapped.rows,
2524            vec![
2525                expected_row_with_code(1, None, 20),
2526                expected_row_with_code(2, None, 10),
2527            ],
2528        );
2529
2530        let released = session
2531            .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
2532            .expect("a delete should release unique membership to a final inserted row");
2533        assert_eq!(
2534            released.rows,
2535            vec![
2536                expected_row_with_code(1, None, 20),
2537                expected_row_with_code(3, None, 20),
2538            ],
2539        );
2540    }
2541}
2542
2543#[cfg(test)]
2544mod identity_pre_key_tests {
2545    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2546    use super::DynamicTypedEntityBinding;
2547    use super::{
2548        AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
2549        AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
2550        DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
2551        DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
2552        MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
2553        add_structural_mutation_staged_bytes, checked_pre_key_candidate_count,
2554        insert_key_exists_after_generation, validate_structural_mutation_result_bytes,
2555    };
2556    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2557    use crate::db::executor::budget::{
2558        HardExecutionBudget, HardExecutionContext, HardExecutionFailureHeadroom,
2559        with_query_execution_budget_for_tests,
2560    };
2561    use crate::{
2562        db::{
2563            commit::{database_incarnation_id, forget_recovered_domain_for_tests},
2564            data::DataStore,
2565            executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
2566            index::IndexStore,
2567            integrity::{
2568                PhysicalUnitCheckpoint, QuickIntegrityStatus, RowInspectionLimits,
2569                execute_quick_integrity, execute_row_integrity_page,
2570            },
2571            journal::JournalTailStore,
2572            registry::{
2573                StoreAllocationIdentities, StoreAllocationIdentity, StoreRegistry,
2574                StoreRuntimeStorageCapabilities,
2575            },
2576            schema::{
2577                AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
2578                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2579                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2580                PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
2581                SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2582                accepted_schema_candidate_with_field_bindings_for_tests,
2583            },
2584            write_context::MutationMode,
2585        },
2586        error::{ErrorClass, ErrorOrigin, InternalError},
2587        testing::test_memory,
2588        traits::{CanisterKind, Path},
2589        types::{EntityTag, Timestamp},
2590        value::{InputValue, OutputValue, Value},
2591    };
2592    use icydb_schema::{FieldSourceKey, ScalarType};
2593    use std::{cell::RefCell, collections::BTreeMap, time::Instant};
2594
2595    const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
2596    const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
2597    const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
2598    const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
2599    const ENTITY_NAME: &str = "IdentityRow";
2600    const ENTITY_TAG: EntityTag = EntityTag::new(93);
2601    const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
2602
2603    struct TestCanister;
2604
2605    impl Path for TestCanister {
2606        const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
2607    }
2608
2609    impl CanisterKind for TestCanister {
2610        const COMMIT_MEMORY_ID: u8 = 45;
2611        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
2612        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
2613        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2614            "icydb.identity_pre_key_tests.integrity.progress.v1";
2615    }
2616
2617    thread_local! {
2618        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2619        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2620        static SCHEMA_STORE: RefCell<SchemaStore> =
2621            const { RefCell::new(SchemaStore::init_heap()) };
2622        static STORE_REGISTRY: StoreRegistry = {
2623            let mut registry = StoreRegistry::new();
2624            registry.register_store(
2625                STORE_PATH,
2626                &DATA_STORE,
2627                &INDEX_STORE,
2628                &SCHEMA_STORE,
2629                StoreAllocationIdentities::absent(),
2630                StoreRuntimeStorageCapabilities::heap(),
2631            ).expect("identity pre-key test store should register");
2632            registry
2633        };
2634        static JOURNALED_DATA_STORE: RefCell<DataStore> =
2635            RefCell::new(DataStore::init_journaled(test_memory(186)));
2636        static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
2637            RefCell::new(IndexStore::init_journaled(test_memory(187)));
2638        static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
2639            RefCell::new(SchemaStore::init_journaled(test_memory(188)));
2640        static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
2641            RefCell::new(JournalTailStore::init(test_memory(189)));
2642        static JOURNALED_STORE_REGISTRY: StoreRegistry = {
2643            let mut registry = StoreRegistry::new();
2644            registry.register_journaled_store(
2645                JOURNALED_STORE_PATH,
2646                &JOURNALED_DATA_STORE,
2647                &JOURNALED_INDEX_STORE,
2648                &JOURNALED_SCHEMA_STORE,
2649                &JOURNALED_TAIL_STORE,
2650                StoreAllocationIdentities::new_journaled(
2651                    StoreAllocationIdentity::new(186, "icydb.test.identity-range.data.v1"),
2652                    StoreAllocationIdentity::new(187, "icydb.test.identity-range.index.v1"),
2653                    StoreAllocationIdentity::new(188, "icydb.test.identity-range.schema.v1"),
2654                    StoreAllocationIdentity::new(189, "icydb.test.identity-range.journal.v1"),
2655                ),
2656                StoreRuntimeStorageCapabilities::journaled(),
2657            ).expect("identity range journaled store should register");
2658            registry
2659        };
2660    }
2661
2662    struct JournaledTestCanister;
2663
2664    impl Path for JournaledTestCanister {
2665        const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
2666    }
2667
2668    impl CanisterKind for JournaledTestCanister {
2669        const COMMIT_MEMORY_ID: u8 = 190;
2670        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
2671        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
2672        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2673            "icydb.identity_range_tests.integrity.progress.v1";
2674    }
2675
2676    fn source_key(source: &str) -> FieldSourceKey {
2677        FieldSourceKey::try_new(source).expect("identity test field source should admit")
2678    }
2679
2680    fn identity_snapshot(store_path: &str) -> PersistedSchemaSnapshot {
2681        let fields = vec![
2682            PersistedFieldSnapshot::new_initial_with_write_policy(
2683                FieldId::new(1),
2684                "id".to_string(),
2685                SchemaFieldSlot::new(0),
2686                AcceptedFieldKind::Nat64,
2687                Vec::new(),
2688                false,
2689                SchemaInsertDefault::None,
2690                SchemaFieldWritePolicy::from_model_policies(
2691                    Some(FieldInsertGeneration::Identity),
2692                    None,
2693                ),
2694                FieldStorageDecode::ByKind,
2695                LeafCodec::Scalar(ScalarCodec::Nat64),
2696            ),
2697            PersistedFieldSnapshot::new_initial(
2698                FieldId::new(2),
2699                "payload".to_string(),
2700                SchemaFieldSlot::new(1),
2701                AcceptedFieldKind::Nat64,
2702                Vec::new(),
2703                false,
2704                SchemaInsertDefault::None,
2705                FieldStorageDecode::ByKind,
2706                LeafCodec::Scalar(ScalarCodec::Nat64),
2707            ),
2708        ];
2709        PersistedSchemaSnapshot::new_with_indexes(
2710            SchemaVersion::initial(),
2711            ENTITY_SOURCE.to_string(),
2712            ENTITY_NAME.to_string(),
2713            FieldId::new(1),
2714            SchemaRowLayout::initial(
2715                fields
2716                    .iter()
2717                    .map(|field| (field.id(), field.slot()))
2718                    .collect(),
2719            ),
2720            fields,
2721            vec![PersistedIndexSnapshot::new(
2722                SchemaIndexId::new(1).expect("identity test index ID should admit"),
2723                1,
2724                "by_payload".to_string(),
2725                store_path.to_string(),
2726                false,
2727                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2728                    FieldId::new(2),
2729                    SchemaFieldSlot::new(1),
2730                    vec!["payload".to_string()],
2731                    AcceptedFieldKind::Nat64,
2732                    false,
2733                )]),
2734                None,
2735            )],
2736        )
2737    }
2738
2739    fn initialize() -> DbSession<TestCanister> {
2740        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2741        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2742        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2743        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
2744        session
2745            .db
2746            .ensure_recovered_state()
2747            .expect("identity pre-key test database should initialize");
2748        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2749            STORE_PATH,
2750            AcceptedSchemaRevision::INITIAL,
2751            BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH))]),
2752            BTreeMap::from([
2753                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2754                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2755            ]),
2756        );
2757        let store = session
2758            .db
2759            .store_handle(STORE_PATH)
2760            .expect("identity pre-key test store should resolve");
2761        crate::db::commit::publish_accepted_schema_candidate(
2762            STORE_PATH,
2763            store,
2764            AcceptedSchemaRevision::NONE,
2765            &candidate,
2766        )
2767        .expect("identity candidate should publish with explicit zero state");
2768        session
2769    }
2770
2771    fn initialize_journaled() -> DbSession<JournaledTestCanister> {
2772        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY);
2773        session
2774            .db
2775            .ensure_recovered_state()
2776            .expect("journaled identity database should initialize");
2777        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2778            JOURNALED_STORE_PATH,
2779            AcceptedSchemaRevision::INITIAL,
2780            BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH))]),
2781            BTreeMap::from([
2782                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2783                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2784            ]),
2785        );
2786        let store = session
2787            .db
2788            .store_handle(JOURNALED_STORE_PATH)
2789            .expect("journaled identity store should resolve");
2790        crate::db::commit::publish_accepted_schema_candidate(
2791            JOURNALED_STORE_PATH,
2792            store,
2793            AcceptedSchemaRevision::NONE,
2794            &candidate,
2795        )
2796        .expect("journaled identity candidate should publish");
2797        session
2798    }
2799
2800    fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
2801        AcceptedMutationIntentPatch::new()
2802            .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
2803    }
2804
2805    fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
2806        DynamicStructuralPatch::new(vec![(
2807            "payload".to_string(),
2808            DynamicWriteCell::Value(InputValue::Nat64(value)),
2809        )])
2810    }
2811
2812    fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
2813        vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
2814    }
2815
2816    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2817    fn exact_key_binding<C: CanisterKind>(session: &DbSession<C>) -> DynamicTypedEntityBinding {
2818        session
2819            .issue_typed_entity_binding(
2820                ENTITY_SOURCE,
2821                &[
2822                    DynamicTypedFieldBindingRequest::new(
2823                        ID_SOURCE.to_string(),
2824                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2825                        false,
2826                    ),
2827                    DynamicTypedFieldBindingRequest::new(
2828                        PAYLOAD_SOURCE.to_string(),
2829                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2830                        false,
2831                    ),
2832                ],
2833            )
2834            .expect("exact-key test binding should issue")
2835    }
2836
2837    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2838    fn insert_exact_key_fixture<C: CanisterKind>(session: &DbSession<C>, payload: u64) -> u64 {
2839        let output = session
2840            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2841                entity: ENTITY_NAME.to_string(),
2842                patch: dynamic_payload_patch(payload),
2843            })
2844            .expect("exact-key fixture insert should commit");
2845        match output.rows.as_slice() {
2846            [row] => match row.as_slice() {
2847                [OutputValue::Nat64(id), OutputValue::Nat64(actual_payload)]
2848                    if *actual_payload == payload =>
2849                {
2850                    *id
2851                }
2852                _ => panic!("exact-key fixture should return its identity and payload"),
2853            },
2854            _ => panic!("exact-key fixture insert should return one row"),
2855        }
2856    }
2857
2858    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2859    fn assert_exact_key_batch<C: CanisterKind>(session: &DbSession<C>) {
2860        let first = insert_exact_key_fixture(session, 41);
2861        let second = insert_exact_key_fixture(session, 42);
2862        let missing = u64::MAX;
2863        let binding = exact_key_binding(session);
2864        let gets_before = DataStore::current_get_call_count();
2865        let result = session
2866            .execute_public_exact_key_batch_for_typed_binding(
2867                &binding,
2868                &[second, missing, first, second],
2869            )
2870            .expect("exact-key batch should execute")
2871            .expect("exact-key binding should remain current");
2872
2873        assert_eq!(result.positions, vec![0, 1, 2, 0]);
2874        assert_eq!(
2875            result.distinct_rows,
2876            vec![
2877                Some(expected_dynamic_row(second, 42)),
2878                None,
2879                Some(expected_dynamic_row(first, 41)),
2880            ],
2881        );
2882        assert_eq!(
2883            DataStore::current_get_call_count().saturating_sub(gets_before),
2884            3,
2885            "four input positions with one duplicate must perform three physical reads",
2886        );
2887    }
2888
2889    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2890    #[test]
2891    fn exact_key_batches_preserve_semantics_across_heap_and_journaled_stores() {
2892        assert_exact_key_batch(&initialize());
2893        assert_exact_key_batch(&initialize_journaled());
2894    }
2895
2896    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2897    #[test]
2898    fn exact_key_batch_uses_typed_hard_execution_budget() {
2899        let session = initialize();
2900        let binding = exact_key_binding(&session);
2901        let budget =
2902            HardExecutionBudget::uniform_for_tests(0, HardExecutionFailureHeadroom::new(500, 256));
2903        let error = session
2904            .execute_exact_key_batch_with_hard_budget_for_tests(&binding, &[u64::MAX], &budget)
2905            .expect_err("zero query budget should reject the exact-key route");
2906
2907        assert!(matches!(
2908            error.diagnostic().detail(),
2909            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2910                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
2911            })
2912        ));
2913        let facts = error.diagnostic_facts();
2914        assert_eq!(
2915            &facts[..5],
2916            &[
2917                (
2918                    icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
2919                    icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions.raw(),
2920                ),
2921                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 0),
2922                (icydb_diagnostic_code::DiagnosticFactTag::Actual, 1),
2923                (
2924                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionBudgetScope,
2925                    icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution.raw(),
2926                ),
2927                (
2928                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionLane,
2929                    icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead.raw(),
2930                ),
2931            ],
2932        );
2933        assert_eq!(
2934            facts[5].0,
2935            icydb_diagnostic_code::DiagnosticFactTag::QueryShapeFingerprintPrefix,
2936        );
2937        assert_ne!(facts[5].1, 0);
2938    }
2939
2940    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2941    fn assert_planned_query_exhausts(
2942        session: &DbSession<TestCanister>,
2943        query: &crate::db::DynamicQuery,
2944        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
2945    ) {
2946        let budget = HardExecutionBudget::uniform_for_tests(
2947            u64::MAX,
2948            HardExecutionFailureHeadroom::new(500, 256),
2949        )
2950        .with_limit_for_tests(resource, 0);
2951        let context = HardExecutionContext::new(
2952            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
2953            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
2954            0x7068_7973_6963_616c,
2955        );
2956        let error = with_query_execution_budget_for_tests(budget, context, || {
2957            session.execute_trusted_dynamic_query(query)
2958        })
2959        .expect_err("the injected zero resource allowance should reject planned execution");
2960
2961        assert!(matches!(
2962            error.diagnostic().detail(),
2963            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2964                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
2965            })
2966        ));
2967        assert_eq!(
2968            error.diagnostic_facts()[0],
2969            (
2970                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
2971                resource.raw(),
2972            ),
2973        );
2974    }
2975
2976    #[cfg(all(feature = "sql", feature = "diagnostics"))]
2977    fn assert_grouped_query_exhausts(
2978        session: &DbSession<TestCanister>,
2979        query: &crate::db::DynamicQuery,
2980        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
2981    ) {
2982        let budget = HardExecutionBudget::uniform_for_tests(
2983            u64::MAX,
2984            HardExecutionFailureHeadroom::new(500, 256),
2985        )
2986        .with_limit_for_tests(resource, 0);
2987        let context = HardExecutionContext::new(
2988            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
2989            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
2990            0x6772_6f75_7065_642d,
2991        );
2992        let error = with_query_execution_budget_for_tests(budget, context, || {
2993            session.execute_trusted_dynamic_grouped_query(query)
2994        })
2995        .expect_err("the injected zero resource allowance should reject grouped execution");
2996
2997        assert!(matches!(
2998            error.diagnostic().detail(),
2999            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3000                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
3001            })
3002        ));
3003        assert_eq!(
3004            error.diagnostic_facts()[0],
3005            (
3006                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3007                resource.raw(),
3008            ),
3009        );
3010    }
3011
3012    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3013    fn assert_sql_query_exhausts(
3014        session: &DbSession<TestCanister>,
3015        sql: &str,
3016        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
3017    ) {
3018        let budget = HardExecutionBudget::uniform_for_tests(
3019            u64::MAX,
3020            HardExecutionFailureHeadroom::new(500, 256),
3021        )
3022        .with_limit_for_tests(resource, 0);
3023        let context = HardExecutionContext::new(
3024            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
3025            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
3026            0x7371_6c2d_736f_7274,
3027        );
3028        let error = with_query_execution_budget_for_tests(budget, context, || {
3029            session.execute_trusted_sql_query(sql)
3030        })
3031        .expect_err("the injected zero resource allowance should reject SQL execution");
3032
3033        assert!(matches!(
3034            error.diagnostic().detail(),
3035            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3036                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
3037            })
3038        ));
3039        assert_eq!(
3040            error.diagnostic_facts()[0],
3041            (
3042                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3043                resource.raw(),
3044            ),
3045        );
3046    }
3047
3048    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3049    #[test]
3050    fn planned_read_routes_share_physical_resource_accounting() {
3051        let session = initialize();
3052        let first = insert_exact_key_fixture(&session, 41);
3053        insert_exact_key_fixture(&session, 42);
3054
3055        let fallback = crate::db::DynamicQuery::new(ENTITY_NAME)
3056            .filter(crate::db::FieldRef::new("id").eq(first))
3057            .select(["id", "payload"])
3058            .order_by(crate::db::asc("id"))
3059            .limit(1);
3060        assert_eq!(
3061            session
3062                .execute_trusted_dynamic_query(&fallback)
3063                .expect("bounded fallback execution should preserve its result")
3064                .row_count,
3065            1,
3066        );
3067        assert_planned_query_exhausts(
3068            &session,
3069            &fallback,
3070            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::RowsVisited,
3071        );
3072
3073        let covering = crate::db::DynamicQuery::new(ENTITY_NAME)
3074            .filter(crate::db::FieldRef::new("payload").eq(41_u64))
3075            .select(["payload"])
3076            .order_by(crate::db::asc("payload"))
3077            .limit(1);
3078        assert_eq!(
3079            session
3080                .execute_trusted_dynamic_query(&covering)
3081                .expect("bounded covering execution should preserve its result")
3082                .row_count,
3083            1,
3084        );
3085        assert_planned_query_exhausts(
3086            &session,
3087            &covering,
3088            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
3089        );
3090
3091        let residual = crate::db::DynamicQuery::new(ENTITY_NAME)
3092            .filter(crate::db::FieldRef::new("payload").eq_field("id"))
3093            .select(["id"])
3094            .order_by(crate::db::asc("id"))
3095            .limit(1);
3096        assert_eq!(
3097            session
3098                .execute_trusted_dynamic_query(&residual)
3099                .expect("bounded residual execution should preserve its result")
3100                .row_count,
3101            0,
3102        );
3103        assert_planned_query_exhausts(
3104            &session,
3105            &residual,
3106            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
3107        );
3108
3109        assert_planned_query_exhausts(
3110            &session,
3111            &fallback,
3112            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::ResultBytes,
3113        );
3114
3115        let grouped = crate::db::DynamicQuery::new(ENTITY_NAME)
3116            .group_by("payload")
3117            .aggregate(crate::db::count())
3118            .order_by(crate::db::asc("payload"))
3119            .grouped_limits(10, 16 * 1_024)
3120            .limit(1);
3121        let grouped_result = session
3122            .execute_trusted_dynamic_grouped_query(&grouped)
3123            .expect("bounded grouped execution should preserve its result");
3124        assert_eq!(grouped_result.row_count, 1);
3125        assert!(grouped_result.next_cursor.is_some());
3126        assert_grouped_query_exhausts(
3127            &session,
3128            &grouped,
3129            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctEntries,
3130        );
3131        assert_grouped_query_exhausts(
3132            &session,
3133            &grouped,
3134            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::CursorSteps,
3135        );
3136
3137        assert_sql_query_exhausts(
3138            &session,
3139            "SELECT payload, COUNT(*) AS row_count FROM IdentityRow \
3140             GROUP BY payload ORDER BY row_count DESC, payload ASC LIMIT 1",
3141            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::SortEntries,
3142        );
3143    }
3144
3145    fn assert_dynamic_payload(session: &DbSession<TestCanister>, key: u64, expected_payload: u64) {
3146        let unchanged = session
3147            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3148                entity: ENTITY_NAME.to_string(),
3149                key: InputValue::Nat64(key),
3150                patch: dynamic_payload_patch(expected_payload),
3151            })
3152            .expect("the expected row should remain readable through a no-op update");
3153        assert_eq!(unchanged.affected_rows, 0);
3154        assert_eq!(
3155            unchanged.rows,
3156            vec![expected_dynamic_row(key, expected_payload)],
3157        );
3158    }
3159
3160    fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
3161        values
3162            .iter()
3163            .map(|value| {
3164                AcceptedStructuralMutation::save(
3165                    MutationMode::Insert,
3166                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
3167                    payload_patch(*value),
3168                )
3169            })
3170            .collect()
3171    }
3172
3173    fn assert_identity_boundary(error: &InternalError) {
3174        assert_eq!(error.class(), ErrorClass::Unsupported);
3175        assert_eq!(error.origin(), ErrorOrigin::Identity);
3176    }
3177
3178    #[test]
3179    fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
3180        let generated = insert_key_exists_after_generation(true);
3181        assert_eq!(generated.class(), ErrorClass::Corruption);
3182        assert_eq!(generated.origin(), ErrorOrigin::Identity);
3183
3184        let ordinary = insert_key_exists_after_generation(false);
3185        assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
3186    }
3187
3188    #[cfg(target_pointer_width = "64")]
3189    #[test]
3190    fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
3191        let error = checked_pre_key_candidate_count(
3192            usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
3193        )
3194        .expect_err("candidate counts beyond u32 must reject");
3195        assert_identity_boundary(&error);
3196    }
3197
3198    #[test]
3199    #[expect(
3200        clippy::too_many_lines,
3201        reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
3202    )]
3203    fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
3204        let session = initialize();
3205        let seeded = session
3206            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
3207            .expect("seed rows should commit");
3208        assert_eq!(seeded.affected_rows, 1);
3209
3210        let split = session
3211            .execute_trusted_dynamic_mutation_batch(vec![
3212                DynamicMutation::Update {
3213                    entity: ENTITY_NAME.to_string(),
3214                    key: InputValue::Nat64(1),
3215                    patch: dynamic_payload_patch(60),
3216                },
3217                DynamicMutation::Insert {
3218                    entity: ENTITY_NAME.to_string(),
3219                    patch: dynamic_payload_patch(40),
3220                },
3221            ])
3222            .expect("one holding should split atomically");
3223        assert_eq!(split.affected_rows, 2);
3224        assert_eq!(
3225            split.rows,
3226            vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
3227            "split after-images must retain input order and exact quantity",
3228        );
3229
3230        let rejected_split = session
3231            .execute_trusted_dynamic_mutation_batch(vec![
3232                DynamicMutation::Update {
3233                    entity: ENTITY_NAME.to_string(),
3234                    key: InputValue::Nat64(1),
3235                    patch: dynamic_payload_patch(50),
3236                },
3237                DynamicMutation::Insert {
3238                    entity: ENTITY_NAME.to_string(),
3239                    patch: DynamicStructuralPatch::new(Vec::new()),
3240                },
3241            ])
3242            .expect_err("an invalid split output must reject the staged source update");
3243        assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
3244        assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
3245        assert_eq!(
3246            rejected_split.diagnostic_facts(),
3247            vec![
3248                (
3249                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3250                    ENTITY_TAG.value(),
3251                ),
3252                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 2),
3253                (
3254                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3255                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
3256                ),
3257                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
3258            ],
3259        );
3260        assert_dynamic_payload(&session, 1, 60);
3261        assert_dynamic_payload(&session, 2, 40);
3262
3263        let transfer = session
3264            .execute_trusted_dynamic_mutation_batch(vec![
3265                DynamicMutation::Update {
3266                    entity: ENTITY_NAME.to_string(),
3267                    key: InputValue::Nat64(1),
3268                    patch: dynamic_payload_patch(70),
3269                },
3270                DynamicMutation::Update {
3271                    entity: ENTITY_NAME.to_string(),
3272                    key: InputValue::Nat64(2),
3273                    patch: dynamic_payload_patch(30),
3274                },
3275            ])
3276            .expect("distinct transfer patches should share one atomic batch");
3277        assert_eq!(
3278            transfer.rows,
3279            vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
3280            "the transfer must preserve the exact total quantity",
3281        );
3282
3283        let merge = session
3284            .execute_trusted_dynamic_mutation_batch(vec![
3285                DynamicMutation::Delete {
3286                    entity: ENTITY_NAME.to_string(),
3287                    key: InputValue::Nat64(2),
3288                },
3289                DynamicMutation::Update {
3290                    entity: ENTITY_NAME.to_string(),
3291                    key: InputValue::Nat64(1),
3292                    patch: dynamic_payload_patch(100),
3293                },
3294            ])
3295            .expect("two holdings should merge atomically");
3296        assert_eq!(
3297            merge.rows,
3298            vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
3299            "delete before-images and update after-images must retain input order",
3300        );
3301
3302        let resplit = session
3303            .execute_trusted_dynamic_mutation_batch(vec![
3304                DynamicMutation::Update {
3305                    entity: ENTITY_NAME.to_string(),
3306                    key: InputValue::Nat64(1),
3307                    patch: dynamic_payload_patch(60),
3308                },
3309                DynamicMutation::Insert {
3310                    entity: ENTITY_NAME.to_string(),
3311                    patch: dynamic_payload_patch(40),
3312                },
3313            ])
3314            .expect("the merged holding should split again");
3315        assert_eq!(
3316            resplit.rows,
3317            vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
3318        );
3319
3320        let rejected_merge = session
3321            .execute_trusted_dynamic_mutation_batch(vec![
3322                DynamicMutation::Delete {
3323                    entity: ENTITY_NAME.to_string(),
3324                    key: InputValue::Nat64(3),
3325                },
3326                DynamicMutation::Update {
3327                    entity: ENTITY_NAME.to_string(),
3328                    key: InputValue::Nat64(99),
3329                    patch: dynamic_payload_patch(100),
3330                },
3331            ])
3332            .expect_err("a late missing merge target must preserve the earlier staged delete");
3333        assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
3334        assert_dynamic_payload(&session, 1, 60);
3335        assert_dynamic_payload(&session, 3, 40);
3336
3337        SCHEMA_STORE.with(|store| {
3338            let cursor = store
3339                .borrow()
3340                .identity_statement_cursor(
3341                    database_incarnation_id().expect("database incarnation should remain readable"),
3342                    ENTITY_TAG,
3343                    FieldId::new(1),
3344                    &AcceptedFieldKind::Nat64,
3345                )
3346                .expect("mixed Identity state should remain readable");
3347            assert_eq!(cursor.expected_high_water(), 3);
3348            assert!(!cursor.has_allocations());
3349        });
3350    }
3351
3352    #[test]
3353    fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
3354        let session = initialize();
3355        session
3356            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
3357            .expect("the holding fixture should initialize");
3358
3359        let duplicate = session
3360            .execute_trusted_dynamic_mutation_batch(vec![
3361                DynamicMutation::Update {
3362                    entity: ENTITY_NAME.to_string(),
3363                    key: InputValue::Nat64(1),
3364                    patch: dynamic_payload_patch(60),
3365                },
3366                DynamicMutation::Delete {
3367                    entity: ENTITY_NAME.to_string(),
3368                    key: InputValue::Nat64(1),
3369                },
3370            ])
3371            .expect_err("duplicate targets across operation kinds must reject");
3372        assert!(matches!(
3373            duplicate.diagnostic().detail(),
3374            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3375                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
3376            }),
3377        ));
3378        assert_eq!(
3379            duplicate.diagnostic_facts(),
3380            vec![
3381                (
3382                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3383                    ENTITY_TAG.value(),
3384                ),
3385                (
3386                    icydb_diagnostic_code::DiagnosticFactTag::FirstBatchPosition,
3387                    0,
3388                ),
3389                (
3390                    icydb_diagnostic_code::DiagnosticFactTag::DuplicateBatchPosition,
3391                    1,
3392                ),
3393            ],
3394        );
3395        assert_dynamic_payload(&session, 1, 100);
3396    }
3397
3398    #[test]
3399    fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
3400        let session = initialize();
3401        let empty = session
3402            .execute_trusted_dynamic_mutation_batch(Vec::new())
3403            .expect_err("an empty public batch must reject");
3404        assert!(matches!(
3405            empty.diagnostic().detail(),
3406            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3407                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
3408            }),
3409        ));
3410        assert_eq!(
3411            empty.diagnostic_facts(),
3412            vec![(icydb_diagnostic_code::DiagnosticFactTag::ActualCount, 0,)],
3413        );
3414
3415        let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
3416            .map(|_| DynamicMutation::Delete {
3417                entity: ENTITY_NAME.to_string(),
3418                key: InputValue::Nat64(1),
3419            })
3420            .collect();
3421        let over_bound = session
3422            .execute_trusted_dynamic_mutation_batch(requests)
3423            .expect_err("operation cap plus one must reject before row resolution");
3424        assert!(matches!(
3425            over_bound.diagnostic().detail(),
3426            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3427                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
3428            }),
3429        ));
3430        assert_eq!(
3431            over_bound.diagnostic_facts(),
3432            vec![
3433                (
3434                    icydb_diagnostic_code::DiagnosticFactTag::ActualCount,
3435                    (MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1) as u64,
3436                ),
3437                (
3438                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
3439                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS as u64,
3440                ),
3441            ],
3442        );
3443    }
3444
3445    #[test]
3446    fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
3447        let mut exact = 0;
3448        add_structural_mutation_staged_bytes(
3449            &mut exact,
3450            [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
3451        )
3452        .expect("the exact staged-byte boundary should admit");
3453        assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
3454
3455        let error = add_structural_mutation_staged_bytes(&mut exact, [1])
3456            .expect_err("one byte above the staged-byte boundary must reject");
3457        assert!(matches!(
3458            error.diagnostic().detail(),
3459            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3460                boundary:
3461                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
3462            }),
3463        ));
3464        assert_eq!(
3465            error.diagnostic_facts(),
3466            vec![
3467                (
3468                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
3469                    (MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1) as u64,
3470                ),
3471                (
3472                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
3473                    MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES as u64,
3474                ),
3475            ],
3476        );
3477
3478        validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
3479            .expect("the exact result-byte boundary should admit");
3480        let error = validate_structural_mutation_result_bytes(
3481            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
3482        )
3483        .expect_err("one byte above the result-byte boundary must reject");
3484        assert!(matches!(
3485            error.diagnostic().detail(),
3486            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3487                boundary:
3488                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
3489            }),
3490        ));
3491        assert_eq!(
3492            error.diagnostic_facts(),
3493            vec![
3494                (
3495                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
3496                    (MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1) as u64,
3497                ),
3498                (
3499                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
3500                    MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES as u64,
3501                ),
3502            ],
3503        );
3504    }
3505
3506    #[expect(
3507        clippy::too_many_lines,
3508        reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
3509    )]
3510    #[test]
3511    fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
3512        let session = initialize();
3513        let catalog = session
3514            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3515            .expect("identity catalog should resolve");
3516        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3517            .expect("identity row layout should build");
3518        let initial_description = session
3519            .try_describe_entity_by_name(ENTITY_NAME)
3520            .expect("accepted Identity description should resolve");
3521        assert_eq!(
3522            initial_description.entity_tag(),
3523            catalog.identity().entity_tag().value()
3524        );
3525        assert_eq!(
3526            initial_description.accepted_schema_fingerprint_method(),
3527            catalog.fingerprint_method_version()
3528        );
3529        assert_eq!(
3530            initial_description.accepted_schema_fingerprint(),
3531            catalog.fingerprint()
3532        );
3533        let initial_identity = initial_description
3534            .identity()
3535            .expect("accepted Identity policy should be described");
3536        assert_eq!(initial_identity.field(), "id");
3537        assert_eq!(initial_identity.generator(), "Identity::next");
3538        assert_eq!(initial_identity.accepted_kind(), "nat64");
3539        assert_eq!(initial_identity.minimum(), 1);
3540        assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
3541        assert_eq!(initial_identity.high_water(), 0);
3542        assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
3543        assert!(!initial_identity.exhausted());
3544
3545        let rejected = session
3546            .execute_accepted_structural_save_batch(
3547                &catalog,
3548                &descriptor,
3549                batch(&[1_000, 2_000]),
3550                Timestamp::from_millis(6),
3551                |_| Err::<(), _>(InternalError::executor_unsupported()),
3552            )
3553            .expect_err("a rejected precommit result must not publish its tentative range");
3554        assert_eq!(rejected.class(), ErrorClass::Unsupported);
3555        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
3556
3557        let rows = session
3558            .execute_accepted_structural_save_batch(
3559                &catalog,
3560                &descriptor,
3561                batch(&[10, 20, 30]),
3562                Timestamp::from_millis(7),
3563                Ok,
3564            )
3565            .expect("one accepted batch should commit rows and one identity range");
3566        assert_eq!(
3567            rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
3568            vec![
3569                vec![Value::Nat64(1), Value::Nat64(10)],
3570                vec![Value::Nat64(2), Value::Nat64(20)],
3571                vec![Value::Nat64(3), Value::Nat64(30)],
3572            ],
3573        );
3574
3575        let dynamic = session
3576            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3577                entity: ENTITY_NAME.to_string(),
3578                patch: DynamicStructuralPatch::new(vec![(
3579                    "payload".to_string(),
3580                    DynamicWriteCell::Value(InputValue::Nat64(40)),
3581                )]),
3582            })
3583            .expect("dynamic omission should commit through shared Identity generation");
3584        assert_eq!(dynamic.affected_rows, 1);
3585
3586        for (request, operation) in [
3587            (
3588                DynamicMutation::Insert {
3589                    entity: ENTITY_NAME.to_string(),
3590                    patch: DynamicStructuralPatch::new(vec![
3591                        (
3592                            "id".to_string(),
3593                            DynamicWriteCell::Value(InputValue::Nat64(41)),
3594                        ),
3595                        (
3596                            "payload".to_string(),
3597                            DynamicWriteCell::Value(InputValue::Nat64(42)),
3598                        ),
3599                    ]),
3600                },
3601                icydb_diagnostic_code::DiagnosticMutationOperation::Insert,
3602            ),
3603            (
3604                DynamicMutation::Update {
3605                    entity: ENTITY_NAME.to_string(),
3606                    key: InputValue::Nat64(1),
3607                    patch: DynamicStructuralPatch::new(vec![(
3608                        "id".to_string(),
3609                        DynamicWriteCell::Default,
3610                    )]),
3611                },
3612                icydb_diagnostic_code::DiagnosticMutationOperation::Update,
3613            ),
3614        ] {
3615            let error = session
3616                .execute_trusted_dynamic_mutation(&request)
3617                .expect_err("structural Identity authorship and regeneration must reject");
3618            assert_eq!(error.class(), ErrorClass::Unsupported);
3619            assert_eq!(error.origin(), ErrorOrigin::Executor);
3620            assert_eq!(
3621                error.diagnostic_facts(),
3622                vec![
3623                    (
3624                        icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3625                        ENTITY_TAG.value(),
3626                    ),
3627                    (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
3628                    (
3629                        icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3630                        operation.raw(),
3631                    ),
3632                    (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
3633                ],
3634            );
3635        }
3636
3637        let binding = session
3638            .issue_typed_entity_binding(
3639                ENTITY_SOURCE,
3640                &[
3641                    DynamicTypedFieldBindingRequest::new(
3642                        ID_SOURCE.to_string(),
3643                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3644                        false,
3645                    ),
3646                    DynamicTypedFieldBindingRequest::new(
3647                        PAYLOAD_SOURCE.to_string(),
3648                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3649                        false,
3650                    ),
3651                ],
3652            )
3653            .expect("typed output should bind the Identity field");
3654        let typed_patch = binding
3655            .bind_write_fields(vec![(
3656                PAYLOAD_SOURCE.to_string(),
3657                DynamicWriteCell::Value(InputValue::Nat64(50)),
3658            )])
3659            .expect("typed payload should lower");
3660        let typed = session
3661            .execute_trusted_typed_mutation(
3662                &binding,
3663                &DynamicTypedMutation::Insert { patch: typed_patch },
3664            )
3665            .expect("typed omission should commit through shared Identity generation");
3666        assert_eq!(
3667            typed
3668                .expect("typed insert should return one mutation result")
3669                .affected_rows,
3670            1,
3671        );
3672        let explicit_typed_patch = binding
3673            .bind_write_fields(vec![
3674                (
3675                    ID_SOURCE.to_string(),
3676                    DynamicWriteCell::Value(InputValue::Nat64(51)),
3677                ),
3678                (
3679                    PAYLOAD_SOURCE.to_string(),
3680                    DynamicWriteCell::Value(InputValue::Nat64(52)),
3681                ),
3682            ])
3683            .expect("the low-level binding should retain exact authored intent");
3684        let explicit_typed_error = session
3685            .execute_trusted_typed_mutation(
3686                &binding,
3687                &DynamicTypedMutation::Insert {
3688                    patch: explicit_typed_patch,
3689                },
3690            )
3691            .expect_err("typed Identity authorship must reject before allocation");
3692        assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
3693        assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
3694        assert_eq!(
3695            explicit_typed_error.diagnostic_facts(),
3696            vec![
3697                (
3698                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3699                    ENTITY_TAG.value(),
3700                ),
3701                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
3702                (
3703                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3704                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
3705                ),
3706                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
3707            ],
3708        );
3709
3710        let replace_error = session
3711            .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
3712                entity: ENTITY_NAME.to_string(),
3713                key: InputValue::Nat64(99),
3714                patch: DynamicStructuralPatch::new(vec![(
3715                    "payload".to_string(),
3716                    DynamicWriteCell::Value(InputValue::Nat64(60)),
3717                )]),
3718            })
3719            .expect_err("save-as-insert with a chosen Identity must reject");
3720        assert_eq!(replace_error.class(), ErrorClass::Unsupported);
3721        assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
3722
3723        #[cfg(feature = "sql")]
3724        {
3725            for sql in [
3726                "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
3727                "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
3728            ] {
3729                let _result = session
3730                    .execute_trusted_sql_mutation(sql)
3731                    .expect("SQL omission and DEFAULT should commit Identity generation");
3732            }
3733
3734            let error = session
3735                .execute_trusted_sql_mutation(
3736                    "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
3737                )
3738                .expect_err("an explicit SQL Identity value must reject before allocation");
3739            let diagnostic = error.diagnostic();
3740            assert_eq!(
3741                diagnostic.code(),
3742                icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
3743            );
3744            assert!(matches!(
3745                diagnostic.detail(),
3746                Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
3747                    boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
3748                }),
3749            ));
3750        }
3751
3752        let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
3753        assert_eq!(
3754            DATA_STORE.with(|store| store.borrow().len()),
3755            expected_committed
3756        );
3757        SCHEMA_STORE.with(|store| {
3758            let cursor = store
3759                .borrow()
3760                .identity_statement_cursor(
3761                    database_incarnation_id().expect("database incarnation should remain readable"),
3762                    ENTITY_TAG,
3763                    FieldId::new(1),
3764                    &AcceptedFieldKind::Nat64,
3765                )
3766                .expect("committed writes must leave active state readable");
3767            assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
3768            assert!(!cursor.has_allocations());
3769        });
3770        let committed_description = session
3771            .try_describe_entity_by_name(ENTITY_NAME)
3772            .expect("committed Identity description should resolve");
3773        let committed_identity = committed_description
3774            .identity()
3775            .expect("accepted Identity policy should remain described");
3776        assert_eq!(
3777            committed_identity.high_water(),
3778            u128::from(expected_committed),
3779        );
3780        assert_eq!(
3781            committed_identity.remaining(),
3782            u128::from(u64::MAX - expected_committed),
3783        );
3784        assert!(!committed_identity.exhausted());
3785    }
3786
3787    #[test]
3788    #[expect(
3789        clippy::too_many_lines,
3790        reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
3791    )]
3792    fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
3793        let session = initialize_journaled();
3794        let catalog = session
3795            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3796            .expect("journaled identity catalog should resolve");
3797        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3798            .expect("journaled identity row layout should build");
3799
3800        for (ordinal, interruption) in [
3801            MutationCommitInterruption::MarkerPersisted,
3802            MutationCommitInterruption::JournalPublished,
3803            MutationCommitInterruption::RowsPublished,
3804            MutationCommitInterruption::StateMaterialized,
3805        ]
3806        .into_iter()
3807        .enumerate()
3808        {
3809            interrupt_next_mutation_commit_for_tests(interruption);
3810            let interrupted = session.execute_accepted_structural_save_batch(
3811                &catalog,
3812                &descriptor,
3813                batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
3814                Timestamp::from_millis(8),
3815                Ok,
3816            );
3817            assert!(
3818                interrupted.is_err(),
3819                "the selected durable boundary should interrupt",
3820            );
3821
3822            let committed = session
3823                .execute_accepted_structural_save_batch(
3824                    &catalog,
3825                    &descriptor,
3826                    batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
3827                    Timestamp::from_millis(9),
3828                    Ok,
3829                )
3830                .expect("the next mutation must recover before allocating");
3831            let expected_high_water =
3832                u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
3833            assert_eq!(
3834                committed
3835                    .into_iter()
3836                    .map(|row| row.values)
3837                    .collect::<Vec<_>>(),
3838                vec![vec![
3839                    Value::Nat64(expected_high_water),
3840                    Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
3841                ]],
3842            );
3843            assert_eq!(
3844                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
3845                expected_high_water,
3846            );
3847            JOURNALED_SCHEMA_STORE.with(|store| {
3848                let cursor = store
3849                    .borrow()
3850                    .identity_statement_cursor(
3851                        database_incarnation_id()
3852                            .expect("database incarnation should remain readable"),
3853                        ENTITY_TAG,
3854                        FieldId::new(1),
3855                        &AcceptedFieldKind::Nat64,
3856                    )
3857                    .expect("guarded recovery must leave quiescent active state");
3858                assert_eq!(
3859                    cursor.expected_high_water(),
3860                    u128::from(expected_high_water),
3861                );
3862                assert!(!cursor.has_allocations());
3863            });
3864        }
3865
3866        for (ordinal, (interruption, deleted_key)) in [
3867            (MutationCommitInterruption::MarkerPersisted, 2),
3868            (MutationCommitInterruption::JournalPublished, 4),
3869            (MutationCommitInterruption::RowPrefixPublished, 6),
3870            (MutationCommitInterruption::RowsPublished, 8),
3871            (MutationCommitInterruption::StateMaterialized, 7),
3872        ]
3873        .into_iter()
3874        .enumerate()
3875        {
3876            let expected_payload =
3877                501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
3878            interrupt_next_mutation_commit_for_tests(interruption);
3879            let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
3880                DynamicMutation::Update {
3881                    entity: ENTITY_NAME.to_string(),
3882                    key: InputValue::Nat64(1),
3883                    patch: dynamic_payload_patch(expected_payload),
3884                },
3885                DynamicMutation::Delete {
3886                    entity: ENTITY_NAME.to_string(),
3887                    key: InputValue::Nat64(deleted_key),
3888                },
3889            ]);
3890            assert!(
3891                interrupted.is_err(),
3892                "the selected caller-key mixed publication boundary should interrupt",
3893            );
3894            let recovered_update = session
3895                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3896                    entity: ENTITY_NAME.to_string(),
3897                    key: InputValue::Nat64(1),
3898                    patch: dynamic_payload_patch(expected_payload),
3899                })
3900                .expect("guarded reentry should complete the marker-authorized mixed batch");
3901            assert_eq!(
3902                recovered_update.affected_rows, 0,
3903                "the recovered update must already expose its admitted final image",
3904            );
3905            let recovered_delete = session
3906                .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
3907                    entity: ENTITY_NAME.to_string(),
3908                    key: InputValue::Nat64(deleted_key),
3909                })
3910                .expect_err("the recovered delete must already be materialized");
3911            assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
3912            JOURNALED_SCHEMA_STORE.with(|store| {
3913                let cursor = store
3914                    .borrow()
3915                    .identity_statement_cursor(
3916                        database_incarnation_id()
3917                            .expect("database incarnation should remain readable"),
3918                        ENTITY_TAG,
3919                        FieldId::new(1),
3920                        &AcceptedFieldKind::Nat64,
3921                    )
3922                    .expect("caller-key recovery must preserve active Identity state");
3923                assert_eq!(cursor.expected_high_water(), 8);
3924                assert!(!cursor.has_allocations());
3925            });
3926        }
3927
3928        forget_recovered_domain_for_tests(&session.db)
3929            .expect("the final journal tail should remain recoverable");
3930        session
3931            .db
3932            .ensure_recovered_state()
3933            .expect("derived rebuild must not allocate another identity");
3934
3935        let quick = execute_quick_integrity(&session.db, catalog.inspection_plan())
3936            .expect("quiescent Identity control inventory should be inspectable");
3937        assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
3938        let row_page = execute_row_integrity_page(
3939            &session.db,
3940            catalog.inspection_plan(),
3941            PhysicalUnitCheckpoint::BeforeFirst,
3942            RowInspectionLimits::standard(),
3943        )
3944        .expect("Identity rows should remain within committed high-water");
3945        assert!(row_page.exhausted());
3946        assert!(row_page.findings().is_empty());
3947
3948        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
3949        assert!(
3950            JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
3951            "derived index rebuild should restore witnesses without allocating identities",
3952        );
3953        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
3954        JOURNALED_SCHEMA_STORE.with(|store| {
3955            let cursor = store
3956                .borrow()
3957                .identity_statement_cursor(
3958                    database_incarnation_id().expect("database incarnation should remain readable"),
3959                    ENTITY_TAG,
3960                    FieldId::new(1),
3961                    &AcceptedFieldKind::Nat64,
3962                )
3963                .expect("folded identity state should reopen without allocating");
3964            assert_eq!(cursor.expected_high_water(), 8);
3965            assert!(!cursor.has_allocations());
3966        });
3967    }
3968
3969    #[test]
3970    #[ignore = "release-closeout native timing probe for one marker-authorized Identity recovery"]
3971    fn identity_recovery_closeout_reports_guarded_reentry_time() {
3972        let session = initialize_journaled();
3973        let catalog = session
3974            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3975            .expect("journaled identity catalog should resolve");
3976        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3977            .expect("journaled identity row layout should build");
3978
3979        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
3980        let interrupted = session.execute_accepted_structural_save_batch(
3981            &catalog,
3982            &descriptor,
3983            batch(&[1]),
3984            Timestamp::from_millis(10),
3985            Ok,
3986        );
3987        assert!(
3988            interrupted.is_err(),
3989            "the selected publication boundary should interrupt",
3990        );
3991
3992        let start = Instant::now();
3993        let committed = session
3994            .execute_accepted_structural_save_batch(
3995                &catalog,
3996                &descriptor,
3997                batch(&[2]),
3998                Timestamp::from_millis(11),
3999                Ok,
4000            )
4001            .expect("guarded reentry should recover before allocation");
4002        let elapsed = start.elapsed();
4003        assert_eq!(
4004            committed
4005                .into_iter()
4006                .map(|row| row.values)
4007                .collect::<Vec<_>>(),
4008            vec![vec![Value::Nat64(2), Value::Nat64(2)]],
4009        );
4010
4011        println!(
4012            "identity recovery closeout: guarded_reentry_nanos={}",
4013            elapsed.as_nanos(),
4014        );
4015    }
4016}
4017
4018#[cfg(test)]
4019mod targeted_rule_mutation_tests {
4020    use super::{
4021        DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
4022        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
4023    };
4024    use crate::{
4025        db::{
4026            data::{DataStore, encode_input_value_for_candidate_field_contract},
4027            index::IndexStore,
4028            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
4029            schema::{
4030                AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
4031                AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
4032                AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
4033                ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
4034                PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
4035                ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
4036                SchemaRowLayout, SchemaStore, SchemaVersion,
4037                accepted_schema_candidate_with_catalogs_for_tests,
4038                build_record_newtype_composite_catalog_for_tests,
4039                empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
4040            },
4041        },
4042        error::InternalError,
4043        traits::{CanisterKind, Path},
4044        types::EntityTag,
4045        value::InputValue,
4046    };
4047    use icydb_schema::{
4048        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
4049    };
4050    use std::{cell::RefCell, collections::BTreeMap};
4051
4052    const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
4053    const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
4054    const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
4055    const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
4056    const UPDATED_AT_SOURCE: &str =
4057        "session::write::targeted_rule_mutation_tests::Entity::updated_at";
4058    const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
4059    const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
4060    const DEGREE_MEMBER_SOURCE: &str =
4061        "session::write::targeted_rule_mutation_tests::Profile::degree";
4062    const DEGREE_RULE_SOURCE: &str =
4063        "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
4064
4065    struct TestCanister;
4066
4067    impl Path for TestCanister {
4068        const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
4069    }
4070
4071    impl CanisterKind for TestCanister {
4072        const COMMIT_MEMORY_ID: u8 = 43;
4073        const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
4074        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
4075        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
4076            "icydb.targeted_mutation_tests.integrity.progress.v1";
4077    }
4078
4079    thread_local! {
4080        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
4081        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
4082        static SCHEMA_STORE: RefCell<SchemaStore> =
4083            const { RefCell::new(SchemaStore::init_heap()) };
4084        static STORE_REGISTRY: StoreRegistry = {
4085            let mut registry = StoreRegistry::new();
4086            registry.register_store(
4087                STORE_PATH,
4088                &DATA_STORE,
4089                &INDEX_STORE,
4090                &SCHEMA_STORE,
4091                StoreAllocationIdentities::absent(),
4092                StoreRuntimeStorageCapabilities::heap(),
4093            ).expect("targeted mutation test store should register");
4094            registry
4095        };
4096    }
4097
4098    fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
4099        parse(raw.to_string()).expect("test source identity should admit")
4100    }
4101
4102    fn profile_input(degree: u64) -> InputValue {
4103        InputValue::Map(vec![(
4104            InputValue::Text("degree".to_string()),
4105            InputValue::Nat64(degree),
4106        )])
4107    }
4108
4109    fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
4110        DynamicStructuralPatch::new(vec![
4111            (
4112                "id".to_string(),
4113                DynamicWriteCell::Value(InputValue::Nat64(id)),
4114            ),
4115            (
4116                "profile".to_string(),
4117                DynamicWriteCell::Value(profile_input(degree)),
4118            ),
4119        ])
4120    }
4121
4122    fn encoded_value(
4123        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
4124        composite_catalog: &AcceptedCompositeCatalog,
4125        name: &str,
4126        kind: &AcceptedFieldKind,
4127        storage_decode: FieldStorageDecode,
4128        leaf_codec: LeafCodec,
4129        value: InputValue,
4130    ) -> Vec<u8> {
4131        let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
4132        encode_input_value_for_candidate_field_contract(
4133            enum_catalog,
4134            composite_catalog,
4135            field,
4136            value,
4137            &mut ValueAdmissionBudget::standard(),
4138        )
4139        .expect("test accepted value should encode")
4140    }
4141
4142    fn nat64_literal(
4143        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
4144        composite_catalog: &AcceptedCompositeCatalog,
4145        value: u64,
4146    ) -> AcceptedCheckLiteralV1 {
4147        let kind = AcceptedFieldKind::Nat64;
4148        AcceptedCheckLiteralV1::from_accepted_parts(
4149            kind.clone(),
4150            FieldStorageDecode::ByKind,
4151            LeafCodec::Scalar(ScalarCodec::Nat64),
4152            encoded_value(
4153                enum_catalog,
4154                composite_catalog,
4155                "degree_bound",
4156                &kind,
4157                FieldStorageDecode::ByKind,
4158                LeafCodec::Scalar(ScalarCodec::Nat64),
4159                InputValue::Nat64(value),
4160            ),
4161        )
4162    }
4163
4164    fn targeted_constraint_id(error: &InternalError) -> u32 {
4165        let facts = error.diagnostic_facts();
4166        assert!(facts.contains(&(
4167            icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
4168            icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
4169        )));
4170        assert!(facts.contains(&(icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,)));
4171        assert!(facts.contains(&(
4172            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
4173            icydb_diagnostic_code::DiagnosticConstraintKind::TargetedRule.raw(),
4174        )));
4175        assert_eq!(
4176            facts
4177                .iter()
4178                .filter(|(tag, _)| matches!(
4179                    tag,
4180                    icydb_diagnostic_code::DiagnosticFactTag::RootField
4181                        | icydb_diagnostic_code::DiagnosticFactTag::RecordMember
4182                ))
4183                .copied()
4184                .collect::<Vec<_>>(),
4185            vec![
4186                (icydb_diagnostic_code::DiagnosticFactTag::RootField, 2),
4187                (
4188                    icydb_diagnostic_code::DiagnosticFactTag::RecordMember,
4189                    icydb_diagnostic_code::pack_u32_pair(1, 1),
4190                ),
4191            ]
4192        );
4193        let value = facts
4194            .iter()
4195            .find_map(|(tag, value)| {
4196                (*tag == icydb_diagnostic_code::DiagnosticFactTag::ConstraintId).then_some(*value)
4197            })
4198            .expect("targeted mutation should retain its accepted constraint ID");
4199        u32::try_from(value).expect("accepted constraint ID fits u32")
4200    }
4201
4202    #[expect(
4203        clippy::too_many_lines,
4204        reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
4205    )]
4206    #[test]
4207    fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
4208        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
4209        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
4210        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
4211
4212        let entity_tag = EntityTag::new(93);
4213        let enum_catalog = empty_accepted_enum_catalog_for_tests();
4214        let (composite_catalog, profile_type, degree_type, degree_member) =
4215            build_record_newtype_composite_catalog_for_tests(
4216                "tests::TargetedProfile".to_string(),
4217                "degree".to_string(),
4218                "tests::TargetedDegree".to_string(),
4219                AcceptedFieldKind::Nat64,
4220                &enum_catalog,
4221            )
4222            .expect("targeted mutation composites should close");
4223        let profile_kind = AcceptedFieldKind::Composite {
4224            type_id: profile_type,
4225        };
4226        let profile_default = encoded_value(
4227            &enum_catalog,
4228            &composite_catalog,
4229            "profile",
4230            &profile_kind,
4231            FieldStorageDecode::CatalogValue,
4232            LeafCodec::Structural,
4233            profile_input(12),
4234        );
4235        let fields = vec![
4236            PersistedFieldSnapshot::new_initial(
4237                FieldId::new(1),
4238                "id".to_string(),
4239                SchemaFieldSlot::new(0),
4240                AcceptedFieldKind::Nat64,
4241                Vec::new(),
4242                false,
4243                SchemaInsertDefault::None,
4244                FieldStorageDecode::ByKind,
4245                LeafCodec::Scalar(ScalarCodec::Nat64),
4246            ),
4247            PersistedFieldSnapshot::new_initial(
4248                FieldId::new(2),
4249                "profile".to_string(),
4250                SchemaFieldSlot::new(1),
4251                profile_kind,
4252                vec![PersistedNestedLeafSnapshot::new(
4253                    vec!["degree".to_string()],
4254                    AcceptedFieldKind::Composite {
4255                        type_id: degree_type,
4256                    },
4257                    false,
4258                )],
4259                false,
4260                SchemaInsertDefault::SlotPayload(profile_default),
4261                FieldStorageDecode::CatalogValue,
4262                LeafCodec::Structural,
4263            ),
4264            PersistedFieldSnapshot::new_initial_with_write_policy(
4265                FieldId::new(3),
4266                "updated_at".to_string(),
4267                SchemaFieldSlot::new(2),
4268                AcceptedFieldKind::Timestamp,
4269                Vec::new(),
4270                false,
4271                SchemaInsertDefault::None,
4272                SchemaFieldWritePolicy::from_model_policies(
4273                    None,
4274                    Some(FieldWriteManagement::UpdatedAt),
4275                ),
4276                FieldStorageDecode::ByKind,
4277                LeafCodec::Scalar(ScalarCodec::Timestamp),
4278            ),
4279        ];
4280        let mut snapshot = PersistedSchemaSnapshot::new(
4281            SchemaVersion::initial(),
4282            ENTITY_SOURCE.to_string(),
4283            "TargetedMutation".to_string(),
4284            FieldId::new(1),
4285            SchemaRowLayout::initial(
4286                fields
4287                    .iter()
4288                    .map(|field| (field.id(), field.slot()))
4289                    .collect(),
4290            ),
4291            fields,
4292        );
4293        let constraint_catalog = snapshot
4294            .constraint_catalog()
4295            .clone()
4296            .with_added_targeted_rule(
4297                "profile_degree_multiple".to_string(),
4298                ConstraintOrigin::Generated,
4299                AcceptedRuleTarget::new(
4300                    FieldId::new(2),
4301                    AcceptedNamedTypeIdentity::Composite(degree_type),
4302                ),
4303                AcceptedRuleOperation::MultipleOf {
4304                    divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
4305                },
4306            )
4307            .expect("targeted mutation rule should allocate");
4308        let targeted_rule_id = constraint_catalog
4309            .constraints()
4310            .last()
4311            .expect("targeted mutation rule should persist")
4312            .id();
4313        snapshot = snapshot.with_constraint_catalog(constraint_catalog);
4314
4315        let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
4316        let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
4317        let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
4318        let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
4319        let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
4320        let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
4321        let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
4322        let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
4323        let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
4324            BTreeMap::from([(entity_source, entity_tag)]),
4325            BTreeMap::from([
4326                ((entity_tag, id_source), FieldId::new(1)),
4327                ((entity_tag, profile_source), FieldId::new(2)),
4328                ((entity_tag, updated_at_source), FieldId::new(3)),
4329            ]),
4330            BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
4331            BTreeMap::new(),
4332            BTreeMap::new(),
4333        )
4334        .with_initial_named_types_for_tests(
4335            BTreeMap::from([
4336                (
4337                    profile_type_source,
4338                    AcceptedNamedTypeIdentity::Composite(profile_type),
4339                ),
4340                (
4341                    degree_type_source,
4342                    AcceptedNamedTypeIdentity::Composite(degree_type),
4343                ),
4344            ]),
4345            BTreeMap::new(),
4346            BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
4347        );
4348        let candidate = accepted_schema_candidate_with_catalogs_for_tests(
4349            STORE_PATH,
4350            AcceptedSchemaRevision::INITIAL,
4351            enum_catalog,
4352            composite_catalog,
4353            source_bindings,
4354            BTreeMap::from([(entity_tag, snapshot)]),
4355        );
4356
4357        let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
4358        session
4359            .db
4360            .ensure_recovered_state()
4361            .expect("targeted mutation test database should initialize");
4362        let store = session
4363            .db
4364            .store_handle(STORE_PATH)
4365            .expect("targeted mutation test store should resolve");
4366        crate::db::commit::publish_accepted_schema_candidate(
4367            STORE_PATH,
4368            store,
4369            AcceptedSchemaRevision::NONE,
4370            &candidate,
4371        )
4372        .expect("targeted mutation candidate should publish");
4373
4374        let dynamic_error = session
4375            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
4376                entity: "TargetedMutation".to_string(),
4377                patch: structural_patch(1, 12),
4378            })
4379            .expect_err("dynamic write must enforce the targeted rule");
4380        assert_eq!(
4381            targeted_constraint_id(&dynamic_error),
4382            targeted_rule_id.get()
4383        );
4384
4385        let binding = session
4386            .issue_typed_entity_binding(
4387                ENTITY_SOURCE,
4388                &[
4389                    DynamicTypedFieldBindingRequest::new(
4390                        ID_SOURCE.to_string(),
4391                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
4392                        false,
4393                    ),
4394                    DynamicTypedFieldBindingRequest::new(
4395                        PROFILE_SOURCE.to_string(),
4396                        DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
4397                        false,
4398                    ),
4399                    DynamicTypedFieldBindingRequest::new(
4400                        UPDATED_AT_SOURCE.to_string(),
4401                        DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
4402                        false,
4403                    ),
4404                ],
4405            )
4406            .expect("targeted typed binding should issue");
4407        let typed_patch = binding
4408            .bind_write_fields(vec![
4409                (
4410                    ID_SOURCE.to_string(),
4411                    DynamicWriteCell::Value(InputValue::Nat64(2)),
4412                ),
4413                (
4414                    PROFILE_SOURCE.to_string(),
4415                    DynamicWriteCell::Value(profile_input(12)),
4416                ),
4417            ])
4418            .expect("targeted typed patch should bind");
4419        let typed_error = session
4420            .execute_trusted_typed_mutation(
4421                &binding,
4422                &DynamicTypedMutation::Insert { patch: typed_patch },
4423            )
4424            .expect_err("typed write must enforce the targeted rule");
4425        assert_eq!(targeted_constraint_id(&typed_error), targeted_rule_id.get());
4426
4427        #[cfg(feature = "sql")]
4428        {
4429            let sql_error = session
4430                .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
4431                .expect_err("SQL default resolution must enforce the targeted rule");
4432            let crate::db::QueryError::Execute(execute) = sql_error else {
4433                panic!("targeted SQL write should fail at shared execution admission");
4434            };
4435            assert_eq!(
4436                targeted_constraint_id(execute.as_internal()),
4437                targeted_rule_id.get()
4438            );
4439        }
4440
4441        session
4442            .execute_trusted_dynamic_mutation_batch(vec![
4443                DynamicMutation::Insert {
4444                    entity: "TargetedMutation".to_string(),
4445                    patch: structural_patch(4, 5),
4446                },
4447                DynamicMutation::Insert {
4448                    entity: "TargetedMutation".to_string(),
4449                    patch: structural_patch(5, 12),
4450                },
4451            ])
4452            .expect_err("one invalid targeted value must reject the whole batch");
4453        assert_eq!(
4454            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
4455            Some(0),
4456            "no frontend or earlier valid batch row may escape targeted admission",
4457        );
4458
4459        let admitted = session
4460            .execute_trusted_dynamic_mutation_batch(vec![
4461                DynamicMutation::Insert {
4462                    entity: "TargetedMutation".to_string(),
4463                    patch: structural_patch(6, 5),
4464                },
4465                DynamicMutation::Insert {
4466                    entity: "TargetedMutation".to_string(),
4467                    patch: structural_patch(7, 10),
4468                },
4469            ])
4470            .expect("compliant targeted values should share one accepted batch");
4471        let [first, second] = admitted.rows.as_slice() else {
4472            panic!("the mixed targeted batch should return two rows");
4473        };
4474        let first_timestamp = first
4475            .get(2)
4476            .expect("the first mixed row should contain its managed timestamp");
4477        assert!(matches!(
4478            first_timestamp,
4479            crate::value::OutputValue::Timestamp(_)
4480        ));
4481        assert_eq!(
4482            second.get(2),
4483            Some(first_timestamp),
4484            "one accepted mixed batch must materialize one managed timestamp",
4485        );
4486        assert_eq!(
4487            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
4488            Some(2),
4489        );
4490    }
4491}