Skip to main content

icydb_core/db/session/
write.rs

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