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