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