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
7#[cfg(test)]
8mod key_handoff_tests;
9#[cfg(test)]
10mod output_handoff_tests;
11
12use super::AcceptedSchemaCatalogContext;
13use crate::{
14    db::{
15        DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
16        DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedMutation,
17        DynamicTypedStructuralPatch, DynamicWriteCell, TypedEntityDescriptor, TypedFieldType,
18        commit::{CommitRowOp, database_incarnation_id},
19        data::{
20            AcceptedMutationIntentPatch, AcceptedPreKeyInsert, DecodedDataStoreKey, FieldSlot,
21            RawRow, StructuralRowContract, StructuralSlotReader,
22            canonical_row_from_raw_row_with_accepted_decode_contract,
23            resolve_existing_replace_structural_patch_with_accepted_contract,
24            resolve_insert_structural_patch_with_accepted_contract,
25            resolve_update_structural_patch_with_accepted_contract,
26        },
27        executor::{
28            AcceptedMutationConstraintContext, AcceptedMutationConstraintScheduler,
29            budget::finish_current_execution_instruction_watermark,
30            commit_structural_row_ops_with_mutation_progress,
31            commit_structural_row_ops_with_window, mutation_key_exists_error,
32        },
33        integrity::MutationProgressRecordOp,
34        schema::{
35            AcceptedFieldKind, AcceptedIdentityAllocation, AcceptedRowLayoutRuntimeContract,
36            AcceptedRowLayoutRuntimeField, FieldId, FieldInsertGeneration, IdentityStatementCursor,
37            lower_field_type, output_value_from_runtime,
38        },
39        write_context::{AcceptedWriteContext, MutationMode},
40    },
41    error::{InternalError, MutationDiagnosticContext},
42    metrics::EntityMetricsSpan,
43    traits::CanisterKind,
44    types::{CurrentTimestamp, Timestamp},
45    value::{InputValue, Value},
46};
47use icydb_schema::{
48    EntitySourceKey, FieldSourceKey, FieldType, SchemaContractError, TypeSourceKey,
49};
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52struct AcceptedIdentityInsertField {
53    field_id: FieldId,
54    field_slot: usize,
55    accepted_kind: AcceptedFieldKind,
56}
57
58struct AcceptedStructuralMutationCommitOptions {
59    capture_output_values: bool,
60    packing: AcceptedStructuralMutationPacking,
61}
62
63impl AcceptedStructuralMutationCommitOptions {
64    const fn standard(capture_output_values: bool) -> Self {
65        Self {
66            capture_output_values,
67            packing: AcceptedStructuralMutationPacking::Complete,
68        }
69    }
70
71    #[cfg(test)]
72    const fn with_mutation_progress() -> Self {
73        Self {
74            capture_output_values: false,
75            packing: AcceptedStructuralMutationPacking::Complete,
76        }
77    }
78
79    const fn bounded_prefix() -> Self {
80        Self {
81            capture_output_values: false,
82            packing: AcceptedStructuralMutationPacking::BoundedPrefix,
83        }
84    }
85}
86
87#[derive(Clone, Copy)]
88enum AcceptedStructuralMutationPacking {
89    Complete,
90    BoundedPrefix,
91}
92
93pub(in crate::db::session) enum AcceptedStructuralMutationCommitDirective {
94    Standard,
95    WithMutationProgress(MutationProgressRecordOp),
96    Skip,
97}
98
99/// Accepted row identity carried by a structural mutation after frontend
100/// lowering but before the canonical after-image exists.
101pub(in crate::db::session) enum AcceptedStructuralMutationTarget {
102    ResolveFromAfterImage,
103    Expected(Box<DecodedDataStoreKey>),
104    ExpectedLoaded(AcceptedLoadedStructuralRow),
105}
106
107/// One retained row whose accepted key relationship was validated by the
108/// synchronous operation that loaded it.
109pub(in crate::db::session) struct AcceptedLoadedStructuralRow {
110    key: Box<DecodedDataStoreKey>,
111    row: RawRow,
112}
113
114impl AcceptedLoadedStructuralRow {
115    pub(in crate::db::session) fn from_validated_parts(
116        key: DecodedDataStoreKey,
117        row: RawRow,
118    ) -> Self {
119        Self {
120            key: Box::new(key),
121            row,
122        }
123    }
124
125    fn into_parts(self) -> (DecodedDataStoreKey, RawRow) {
126        (*self.key, self.row)
127    }
128}
129
130impl AcceptedStructuralMutationTarget {
131    pub(in crate::db::session) fn expected(key: DecodedDataStoreKey) -> Self {
132        Self::Expected(Box::new(key))
133    }
134
135    /// Retain a row loaded by the same synchronous operation so mutation
136    /// materialization does not perform a duplicate backend point read.
137    pub(in crate::db::session) const fn expected_loaded(row: AcceptedLoadedStructuralRow) -> Self {
138        Self::ExpectedLoaded(row)
139    }
140}
141
142/// One accepted structural mutation intent ready for shared batch
143/// materialization.
144pub(in crate::db::session) enum AcceptedStructuralMutation {
145    Save {
146        mode: MutationMode,
147        target: AcceptedStructuralMutationTarget,
148        patch: AcceptedMutationIntentPatch,
149    },
150    Delete {
151        key: Box<DecodedDataStoreKey>,
152    },
153}
154
155impl AcceptedStructuralMutation {
156    pub(in crate::db::session) const fn save(
157        mode: MutationMode,
158        target: AcceptedStructuralMutationTarget,
159        patch: AcceptedMutationIntentPatch,
160    ) -> Self {
161        Self::Save {
162            mode,
163            target,
164            patch,
165        }
166    }
167
168    pub(in crate::db::session) fn delete(key: DecodedDataStoreKey) -> Self {
169        Self::Delete { key: Box::new(key) }
170    }
171}
172
173const MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS: usize = 4_096;
174const MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES: usize = 64;
175pub(in crate::db::session) const STRUCTURAL_MUTATION_BATCH_STAGED_BYTES_POLICY: u32 =
176    16 * 1024 * 1024;
177pub(in crate::db::session) const MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES: usize =
178    STRUCTURAL_MUTATION_BATCH_STAGED_BYTES_POLICY as usize;
179const MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES: usize = 1024 * 1024;
180
181struct AcceptedStructuralMutationBatchItem {
182    catalog: AcceptedSchemaCatalogContext,
183    mutation: AcceptedStructuralMutation,
184}
185
186struct AcceptedStructuralMutationEntityState {
187    entity_tag: crate::types::EntityTag,
188    identity_field: Option<AcceptedIdentityInsertField>,
189    identity_incarnation: Option<crate::db::integrity::DatabaseIncarnationId>,
190    identity_cursor: Option<IdentityStatementCursor>,
191    identity_insert_ordinal: u32,
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub(in crate::db::session) struct AcceptedStructuralMutationPackingReport {
196    admitted_mutations: usize,
197    staged_bytes: usize,
198    stopped_before_candidate: bool,
199    candidate_exceeds_batch_policy: bool,
200}
201
202impl AcceptedStructuralMutationPackingReport {
203    #[must_use]
204    pub(in crate::db::session) const fn admitted_mutations(self) -> usize {
205        self.admitted_mutations
206    }
207
208    #[must_use]
209    pub(in crate::db::session) const fn stopped_before_candidate(self) -> bool {
210        self.stopped_before_candidate
211    }
212
213    #[must_use]
214    pub(in crate::db::session) const fn candidate_exceeds_batch_policy(self) -> bool {
215        self.candidate_exceeds_batch_policy
216    }
217}
218
219fn structural_mutation_staged_charge(
220    lengths: impl IntoIterator<Item = usize>,
221) -> Result<usize, InternalError> {
222    lengths.into_iter().try_fold(0_usize, |total, length| {
223        total.checked_add(length).ok_or_else(|| {
224            InternalError::mutation_batch_staged_bytes_exceeded(
225                None,
226                MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
227            )
228        })
229    })
230}
231
232fn add_structural_mutation_staged_bytes(
233    total: &mut usize,
234    lengths: impl IntoIterator<Item = usize>,
235) -> Result<(), InternalError> {
236    let charge = structural_mutation_staged_charge(lengths)?;
237    *total = total.checked_add(charge).ok_or_else(|| {
238        InternalError::mutation_batch_staged_bytes_exceeded(
239            None,
240            MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
241        )
242    })?;
243    if *total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
244        return Err(InternalError::mutation_batch_staged_bytes_exceeded(
245            Some(*total),
246            MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
247        ));
248    }
249    Ok(())
250}
251
252fn admit_structural_mutation_staged_charge(
253    total: &mut usize,
254    lengths: impl IntoIterator<Item = usize>,
255    packing: AcceptedStructuralMutationPacking,
256) -> Result<AcceptedStructuralMutationStagedAdmission, InternalError> {
257    if matches!(packing, AcceptedStructuralMutationPacking::Complete) {
258        add_structural_mutation_staged_bytes(total, lengths)?;
259        return Ok(AcceptedStructuralMutationStagedAdmission::Admitted);
260    }
261
262    let charge = structural_mutation_staged_charge(lengths)?;
263    if charge > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
264        return Ok(AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy);
265    }
266    let Some(next_total) = total.checked_add(charge) else {
267        return Ok(AcceptedStructuralMutationStagedAdmission::PageFull);
268    };
269    if next_total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
270        return Ok(AcceptedStructuralMutationStagedAdmission::PageFull);
271    }
272    *total = next_total;
273    Ok(AcceptedStructuralMutationStagedAdmission::Admitted)
274}
275
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
277enum AcceptedStructuralMutationStagedAdmission {
278    Admitted,
279    PageFull,
280    CandidateExceedsPolicy,
281}
282
283fn validate_structural_mutation_result_bytes(encoded_bytes: usize) -> Result<(), InternalError> {
284    if encoded_bytes > MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES {
285        return Err(InternalError::mutation_batch_result_bytes_exceeded(
286            encoded_bytes,
287            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES,
288        ));
289    }
290    Ok(())
291}
292
293/// One canonical row produced by structural mutation materialization.
294pub(in crate::db::session) struct AcceptedStructuralMutationRow {
295    values: Vec<Value>,
296    logical_changed: bool,
297}
298
299impl AcceptedStructuralMutationRow {
300    #[cfg(any(feature = "sql", test))]
301    pub(in crate::db::session) fn into_values(self) -> Vec<Value> {
302        self.values
303    }
304
305    pub(in crate::db::session) const fn logical_changed(&self) -> bool {
306        self.logical_changed
307    }
308}
309
310fn mutation_diagnostic_context(
311    catalog: &AcceptedSchemaCatalogContext,
312    mode: MutationMode,
313    batch_position: u32,
314) -> MutationDiagnosticContext {
315    MutationDiagnosticContext::new(
316        catalog.fingerprint_method_version(),
317        catalog.fingerprint(),
318        catalog.identity().entity_tag().value(),
319        mode.diagnostic_operation(),
320        batch_position,
321    )
322}
323
324const fn dynamic_write_context(operation_timestamp: Timestamp) -> AcceptedWriteContext {
325    AcceptedWriteContext::new(operation_timestamp)
326}
327
328fn insert_key_exists_after_generation(identity_generated: bool) -> InternalError {
329    if identity_generated {
330        InternalError::identity_state_corruption()
331    } else {
332        mutation_key_exists_error()
333    }
334}
335
336fn dynamic_key(
337    entity_tag: crate::types::EntityTag,
338    key: InputValue,
339) -> Result<DecodedDataStoreKey, InternalError> {
340    let value = key
341        .try_into_runtime_non_enum()
342        .ok_or_else(InternalError::executor_unsupported)?;
343    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
344}
345
346fn lower_resolved_write_cell(
347    lowered: AcceptedMutationIntentPatch,
348    field: &AcceptedRowLayoutRuntimeField<'_>,
349    cell: DynamicWriteCell,
350    mode: MutationMode,
351    mutation_context: MutationDiagnosticContext,
352) -> Result<AcceptedMutationIntentPatch, InternalError> {
353    if !matches!(cell, DynamicWriteCell::Omitted)
354        && (field.write_policy().insert_generation().is_some()
355            || field.write_policy().write_management().is_some())
356    {
357        return Err(InternalError::mutation_database_owned_field_explicit(
358            mutation_context,
359            field.field_id().get(),
360        ));
361    }
362
363    let slot = FieldSlot::from_validated_index(usize::from(field.slot().get()));
364    Ok(match cell {
365        DynamicWriteCell::Omitted => lowered,
366        DynamicWriteCell::Default => match mode {
367            MutationMode::Insert | MutationMode::Replace => {
368                lowered.set_explicit_insert_default(slot)
369            }
370            MutationMode::Update => lowered.set_explicit_update_default(slot),
371        },
372        DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::null()),
373        DynamicWriteCell::Value(value) => lowered.set_authored(slot, value),
374    })
375}
376
377fn lower_dynamic_patch(
378    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
379    patch: DynamicStructuralPatch,
380    mode: MutationMode,
381    mutation_context: MutationDiagnosticContext,
382) -> Result<AcceptedMutationIntentPatch, InternalError> {
383    let mut lowered = AcceptedMutationIntentPatch::new();
384    for (field_name, cell) in patch.into_fields() {
385        let slot = descriptor
386            .field_slot_index_by_name(&field_name)
387            .ok_or_else(InternalError::executor_unsupported)?;
388        let field = descriptor
389            .field_for_slot_index(slot)
390            .ok_or_else(InternalError::executor_invariant)?;
391        lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
392    }
393    Ok(lowered)
394}
395
396fn lower_dynamic_save_intent(
397    catalog: &AcceptedSchemaCatalogContext,
398    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
399    patch: DynamicStructuralPatch,
400    mode: MutationMode,
401    target: AcceptedStructuralMutationTarget,
402    batch_position: u32,
403) -> Result<AcceptedStructuralMutation, InternalError> {
404    Ok(AcceptedStructuralMutation::save(
405        mode,
406        target,
407        lower_dynamic_patch(
408            descriptor,
409            patch,
410            mode,
411            mutation_diagnostic_context(catalog, mode, batch_position),
412        )?,
413    ))
414}
415
416fn lower_dynamic_mutation_intent(
417    catalog: &AcceptedSchemaCatalogContext,
418    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
419    request: DynamicMutation,
420    batch_position: u32,
421) -> Result<AcceptedStructuralMutation, InternalError> {
422    let entity_tag = catalog.identity().entity_tag();
423    match request {
424        DynamicMutation::Insert { patch, .. } => lower_dynamic_save_intent(
425            catalog,
426            descriptor,
427            patch,
428            MutationMode::Insert,
429            AcceptedStructuralMutationTarget::ResolveFromAfterImage,
430            batch_position,
431        ),
432        DynamicMutation::Update { key, patch, .. } => lower_dynamic_save_intent(
433            catalog,
434            descriptor,
435            patch,
436            MutationMode::Update,
437            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
438            batch_position,
439        ),
440        DynamicMutation::Replace { key, patch, .. } => lower_dynamic_save_intent(
441            catalog,
442            descriptor,
443            patch,
444            MutationMode::Replace,
445            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
446            batch_position,
447        ),
448        DynamicMutation::Delete { key, .. } => Ok(AcceptedStructuralMutation::delete(dynamic_key(
449            entity_tag, key,
450        )?)),
451    }
452}
453
454fn lower_typed_patch(
455    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
456    binding: &DynamicTypedEntityBinding,
457    patch: DynamicTypedStructuralPatch,
458    mode: MutationMode,
459    mutation_context: MutationDiagnosticContext,
460) -> Result<AcceptedMutationIntentPatch, InternalError> {
461    let mut lowered = AcceptedMutationIntentPatch::new();
462    for (descriptor_ordinal, cell) in patch.into_fields() {
463        let (field_id, slot) = binding
464            .field_identity_binding(descriptor_ordinal)
465            .ok_or_else(InternalError::store_invariant)?;
466        let slot_index = usize::from(slot);
467        let field = descriptor
468            .field_for_slot_index(slot_index)
469            .ok_or_else(InternalError::store_invariant)?;
470        if field.field_id().get() != field_id {
471            return Err(InternalError::store_invariant());
472        }
473        lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
474    }
475    Ok(lowered)
476}
477
478fn lower_typed_mutation_intent(
479    catalog: &AcceptedSchemaCatalogContext,
480    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
481    binding: &DynamicTypedEntityBinding,
482    request: DynamicTypedMutation,
483    batch_position: u32,
484) -> Result<Option<AcceptedStructuralMutation>, InternalError> {
485    let entity_tag = catalog.identity().entity_tag();
486    let (mode, target, patch) = match request {
487        DynamicTypedMutation::Insert { patch } => (
488            MutationMode::Insert,
489            AcceptedStructuralMutationTarget::ResolveFromAfterImage,
490            patch,
491        ),
492        DynamicTypedMutation::Update { key, patch } => (
493            MutationMode::Update,
494            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
495            patch,
496        ),
497        DynamicTypedMutation::Replace { key, patch } => (
498            MutationMode::Replace,
499            AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
500            patch,
501        ),
502        DynamicTypedMutation::Delete { key } => {
503            return Ok(Some(AcceptedStructuralMutation::delete(dynamic_key(
504                entity_tag, key,
505            )?)));
506        }
507    };
508    if !patch.is_bound_to(binding) {
509        return Ok(None);
510    }
511    let patch = lower_typed_patch(
512        descriptor,
513        binding,
514        patch,
515        mode,
516        mutation_diagnostic_context(catalog, mode, batch_position),
517    )?;
518    Ok(Some(AcceptedStructuralMutation::save(mode, target, patch)))
519}
520
521fn preserve_dynamic_replacement_identity(
522    key: &DecodedDataStoreKey,
523    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
524    mut patch: AcceptedMutationIntentPatch,
525) -> Result<AcceptedMutationIntentPatch, InternalError> {
526    let primary_key_slots = descriptor.primary_key_slot_indices();
527    let runtime_key = key.primary_key_runtime_value();
528    let components = match runtime_key {
529        Value::List(values) if primary_key_slots.len() > 1 => values,
530        value if primary_key_slots.len() == 1 => vec![value],
531        _ => return Err(InternalError::executor_invariant()),
532    };
533    if components.len() != primary_key_slots.len() {
534        return Err(InternalError::executor_invariant());
535    }
536
537    for (slot, value) in primary_key_slots.iter().copied().zip(components) {
538        let _ = descriptor
539            .field_for_slot_index(slot)
540            .ok_or_else(InternalError::executor_invariant)?;
541        let has_explicit_intent = patch
542            .entries()
543            .iter()
544            .any(|entry| entry.slot().index() == slot);
545        if has_explicit_intent {
546            continue;
547        }
548        let value = InputValue::try_from_runtime_non_enum(&value)
549            .ok_or_else(InternalError::executor_invariant)?;
550        patch =
551            patch.set_preserved_replacement_identity(FieldSlot::from_validated_index(slot), value);
552    }
553
554    Ok(patch)
555}
556
557// Locate the sole accepted Identity owner that is eligible to resolve a
558// keyless insert. Accepted-schema integrity already freezes the exact shape;
559// this runtime check fails closed if a malformed contract reaches execution.
560fn accepted_identity_insert_field(
561    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
562) -> Result<Option<AcceptedIdentityInsertField>, InternalError> {
563    let mut identity = None;
564    for field in descriptor.fields() {
565        if field.write_policy().insert_generation() != Some(FieldInsertGeneration::Identity) {
566            continue;
567        }
568        let field_slot = usize::from(field.slot().get());
569        if identity
570            .replace(AcceptedIdentityInsertField {
571                field_id: field.field_id(),
572                field_slot,
573                accepted_kind: field.kind().clone(),
574            })
575            .is_some()
576            || descriptor.primary_key_slot_indices() != [field_slot]
577        {
578            return Err(InternalError::identity_corruption());
579        }
580    }
581    Ok(identity)
582}
583
584fn checked_pre_key_candidate_count(count: usize) -> Result<u32, InternalError> {
585    u32::try_from(count).map_err(|_| InternalError::identity_candidate_count_exhausted())
586}
587
588fn validate_identity_materialization(
589    entity_tag: crate::types::EntityTag,
590    identity_field: &AcceptedIdentityInsertField,
591    candidate: &AcceptedPreKeyInsert,
592    allocation: &AcceptedIdentityAllocation,
593    data_key: &DecodedDataStoreKey,
594    reader: &StructuralSlotReader<'_>,
595) -> Result<(), InternalError> {
596    let owner = allocation.owner();
597    let slot_value = reader.required_cached_value(identity_field.field_slot)?;
598    if candidate.entity_tag() != entity_tag
599        || candidate.input_ordinal() != allocation.input_ordinal()
600        || owner.entity_tag() != entity_tag
601        || owner.field_id() != identity_field.field_id
602        || allocation.field_slot() != identity_field.field_slot
603        || slot_value != allocation.value()
604        || data_key.primary_key_runtime_value() != *allocation.value()
605    {
606        return Err(InternalError::identity_corruption());
607    }
608    Ok(())
609}
610
611// The write owner validates the whole after-image before selecting its key.
612// Borrow that reader and retain cached components for subsequent Identity checks.
613fn data_key_from_validated_reader(
614    entity_tag: crate::types::EntityTag,
615    reader: &StructuralSlotReader<'_>,
616) -> Result<DecodedDataStoreKey, InternalError> {
617    let values = reader
618        .contract()
619        .primary_key_slot_indices()
620        .iter()
621        .map(|slot| reader.required_cached_value(*slot).cloned())
622        .collect::<Result<Vec<_>, _>>()?;
623
624    DecodedDataStoreKey::try_from_structural_key_values(entity_tag, &values)
625}
626
627fn validated_existing_row(
628    store: crate::db::registry::StoreHandle,
629    data_key: &DecodedDataStoreKey,
630    contract: &StructuralRowContract,
631) -> Result<Option<RawRow>, InternalError> {
632    let raw_key = data_key.to_raw()?;
633    let row = store.with_data(|data| data.get(&raw_key));
634    if let Some(row) = row.as_ref() {
635        let reader =
636            StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
637        reader.validate_primary_key(data_key)?;
638    }
639    Ok(row)
640}
641
642// This is the reader's last use, after whole-row and Identity validation.
643// Result columns follow accepted field order, not the physical slot layout.
644fn into_mutation_output_values(
645    mut reader: StructuralSlotReader<'_>,
646    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
647) -> Result<Vec<Value>, InternalError> {
648    let mut values = Vec::with_capacity(descriptor.fields().len());
649    for field in descriptor.fields() {
650        values.push(reader.take_required_value(usize::from(field.slot().get()))?);
651    }
652    Ok(values)
653}
654
655fn prepare_dynamic_mutation_result(
656    catalog: &AcceptedSchemaCatalogContext,
657    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
658    rows: Vec<AcceptedStructuralMutationRow>,
659    enforce_mixed_batch_result_bound: bool,
660) -> Result<DynamicMutationResult, InternalError> {
661    let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
662        total
663            .checked_add(u32::from(row.logical_changed()))
664            .ok_or_else(InternalError::executor_invariant)
665    })?;
666    let columns = descriptor
667        .fields()
668        .iter()
669        .map(|field| field.name().to_string())
670        .collect();
671    let rows = rows
672        .into_iter()
673        .map(|row| {
674            row.values
675                .into_iter()
676                .map(|value| {
677                    output_value_from_runtime(catalog.enum_catalog(), value)
678                        .map_err(|_| InternalError::store_invariant())
679                })
680                .collect::<Result<Vec<_>, _>>()
681        })
682        .collect::<Result<Vec<_>, _>>()?;
683    let result = DynamicMutationResult {
684        entity: catalog.snapshot().entity_name().to_string(),
685        columns,
686        rows,
687        affected_rows,
688    };
689    if enforce_mixed_batch_result_bound {
690        let encoded =
691            candid::encode_one(&result).map_err(|_| InternalError::executor_invariant())?;
692        validate_structural_mutation_result_bytes(encoded.len())?;
693    }
694    Ok(result)
695}
696
697fn typed_descriptor_field_type(
698    field_type: TypedFieldType,
699) -> Result<FieldType, SchemaContractError> {
700    match field_type {
701        TypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
702        TypedFieldType::List(item) => Ok(FieldType::List(Box::new(typed_descriptor_field_type(
703            *item,
704        )?))),
705        TypedFieldType::Named(source_key) => {
706            TypeSourceKey::try_new(source_key.to_string()).map(FieldType::Named)
707        }
708    }
709}
710
711fn typed_adapter_field_kind_matches(
712    accepted: &AcceptedFieldKind,
713    expected: &AcceptedFieldKind,
714) -> bool {
715    if accepted == expected {
716        return true;
717    }
718    match (accepted, expected) {
719        (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
720            typed_adapter_field_kind_matches(key_kind, expected)
721        }
722        (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
723            typed_adapter_field_kind_matches(accepted, expected)
724        }
725        _ => false,
726    }
727}
728
729impl<C: CanisterKind> DbSession<C> {
730    /// Issue one opaque accepted binding for immutable generated source keys.
731    pub fn issue_typed_entity_binding(
732        &self,
733        descriptor: &TypedEntityDescriptor,
734    ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
735        let unavailable = |field_source| {
736            DynamicTypedBindingError::source_unavailable(descriptor.entity_source_key, field_source)
737        };
738        let entity_source = EntitySourceKey::try_new(descriptor.entity_source_key)
739            .map_err(|_| unavailable(None))?;
740        let catalog = self
741            .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
742            .ok_or_else(|| unavailable(None))?;
743        let identity = catalog.identity();
744        if identity.entity_path() != entity_source.as_str() {
745            return Err(InternalError::store_invariant().into());
746        }
747        let store = self.db.recovered_store(identity.store_path())?;
748        // Binding issuance only projects owned adapter data. Borrow the verified
749        // authority in place instead of cloning every entity's schema bundle;
750        // release the borrow before the returned binding can execute or mutate.
751        store.with_schema(|schema| {
752            let bundle = schema
753                .borrow_accepted_schema_bundle_for_authority(
754                    catalog.value_catalog_handle().authority(),
755                )?
756                .ok_or_else(InternalError::store_invariant)?;
757            let entity_tag = identity.entity_tag();
758            if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
759                || bundle.revision() != catalog.revision()
760            {
761                return Err(InternalError::store_invariant().into());
762            }
763            let snapshot = bundle
764                .entity_snapshots()
765                .get(&entity_tag)
766                .ok_or_else(InternalError::store_invariant)?;
767            if descriptor.primary_key_source_keys.len() != snapshot.primary_key_field_ids().len() {
768                return Err(DynamicTypedBindingError::IncompatibleField);
769            }
770            for (source_key, accepted_field_id) in descriptor
771                .primary_key_source_keys
772                .iter()
773                .zip(snapshot.primary_key_field_ids())
774            {
775                let source = FieldSourceKey::try_new((*source_key).to_string())
776                    .map_err(|_| unavailable(Some(*source_key)))?;
777                let descriptor_field_id = bundle
778                    .source_bindings()
779                    .field(entity_tag, &source)
780                    .ok_or_else(|| unavailable(Some(*source_key)))?;
781                if descriptor_field_id != *accepted_field_id {
782                    return Err(DynamicTypedBindingError::IncompatibleField);
783                }
784            }
785            let row_contract = catalog.inspection_plan().row_contract();
786            let mut fields = Vec::with_capacity(descriptor.fields.len());
787            for field_descriptor in descriptor.fields {
788                let source = FieldSourceKey::try_new(field_descriptor.source_key.to_string())
789                    .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
790                let field_id = bundle
791                    .source_bindings()
792                    .field(entity_tag, &source)
793                    .ok_or_else(|| unavailable(Some(field_descriptor.source_key)))?;
794                let field = snapshot
795                    .fields()
796                    .iter()
797                    .find(|field| field.id() == field_id)
798                    .ok_or_else(InternalError::store_invariant)?;
799                let runtime_field = row_contract
800                    .required_accepted_field_contract(usize::from(field.slot().get()))?;
801                if runtime_field.field_id() != field_id {
802                    return Err(InternalError::store_invariant().into());
803                }
804                let field_type = typed_descriptor_field_type(field_descriptor.field_type)
805                    .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
806                let expected_kind = lower_field_type(&field_type, bundle.source_bindings())
807                    .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
808                if field.nullable() != field_descriptor.nullable
809                    || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
810                {
811                    return Err(DynamicTypedBindingError::IncompatibleField);
812                }
813                fields.push((
814                    source.as_str().to_string(),
815                    field_id.get(),
816                    field.slot().get(),
817                    field.name().to_string(),
818                ));
819            }
820            let adapter_names = bundle.typed_adapter_names()?;
821
822            DynamicTypedEntityBinding::new(
823                database_incarnation_id()?.to_bytes(),
824                entity_source.as_str().to_string(),
825                snapshot.entity_name().to_string(),
826                entity_tag.value(),
827                catalog.revision().get(),
828                catalog.fingerprint(),
829                row_contract.current_layout_version().get(),
830                fields,
831                adapter_names.named_types,
832                adapter_names.enum_variants,
833                adapter_names.composite_fields,
834            )
835            .map_err(Into::into)
836        })
837    }
838
839    pub(in crate::db::session) fn current_typed_entity_binding_catalog(
840        &self,
841        binding: &DynamicTypedEntityBinding,
842    ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
843        // Select this session's commit domain before inspecting its identity.
844        // The checked value is local to this synchronous validation, not the
845        // binding lifetime; catalog lookup and matching retain their live checks.
846        self.db.ensure_recovered_state()?;
847        let incarnation = database_incarnation_id()?.to_bytes();
848        if incarnation != binding.database_incarnation {
849            return Ok(None);
850        }
851        let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
852            binding.entity_source.as_str(),
853        )?
854        else {
855            return Ok(None);
856        };
857        self.typed_entity_binding_matches_catalog(binding, &catalog, incarnation)
858            .map(|current| current.then_some(catalog))
859    }
860
861    fn typed_entity_binding_matches_catalog(
862        &self,
863        binding: &DynamicTypedEntityBinding,
864        catalog: &AcceptedSchemaCatalogContext,
865        incarnation: [u8; 16],
866    ) -> Result<bool, InternalError> {
867        if incarnation != binding.database_incarnation {
868            return Ok(false);
869        }
870        let row_contract = catalog.inspection_plan().row_contract();
871        let identity = catalog.identity();
872        if identity.entity_path() != binding.entity_source.as_str()
873            || identity.entity_tag().value() != binding.entity_tag
874            || catalog.revision().get() != binding.accepted_revision
875            || catalog.fingerprint() != binding.accepted_fingerprint
876            || row_contract.current_layout_version().get() != binding.entity_generation
877        {
878            return Ok(false);
879        }
880        let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
881            .map_err(|_| InternalError::store_invariant())?;
882        let store = self.db.recovered_store(identity.store_path())?;
883        // Only inspect the bundle here; keep its schema-owned validation and
884        // release the borrow before the caller can prepare or commit writes.
885        store.with_schema(|schema| {
886            let bundle = schema
887                .borrow_accepted_schema_bundle_for_authority(
888                    catalog.value_catalog_handle().authority(),
889                )?
890                .ok_or_else(InternalError::store_invariant)?;
891            if bundle.revision() != catalog.revision()
892                || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
893            {
894                return Ok(false);
895            }
896            let snapshot = bundle
897                .entity_snapshots()
898                .get(&identity.entity_tag())
899                .ok_or_else(InternalError::store_invariant)?;
900            for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings()
901            {
902                let source = FieldSourceKey::try_new(source_key)
903                    .map_err(|_| InternalError::store_invariant())?;
904                let Some(field_id) = bundle
905                    .source_bindings()
906                    .field(identity.entity_tag(), &source)
907                else {
908                    return Ok(false);
909                };
910                let Some(field) = snapshot
911                    .fields()
912                    .iter()
913                    .find(|field| field.id() == field_id)
914                else {
915                    return Err(InternalError::store_invariant());
916                };
917                if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
918                    return Ok(false);
919                }
920            }
921
922            Ok(true)
923        })
924    }
925
926    /// Verify that an opaque typed binding still names the exact accepted authority.
927    pub fn typed_entity_binding_is_current(
928        &self,
929        binding: &DynamicTypedEntityBinding,
930    ) -> Result<bool, InternalError> {
931        self.current_typed_entity_binding_catalog(binding)
932            .map(|catalog| catalog.is_some())
933    }
934
935    /// Materialize one accepted delete batch, run bounded frontend validation,
936    /// then commit it atomically.
937    #[cfg(feature = "sql")]
938    pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
939        &self,
940        catalog: &AcceptedSchemaCatalogContext,
941        capture_output_values: bool,
942        keys: Vec<DecodedDataStoreKey>,
943        precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
944    ) -> Result<Vec<Vec<Value>>, InternalError> {
945        let mutations = keys
946            .into_iter()
947            .map(AcceptedStructuralMutation::delete)
948            .collect::<Vec<_>>();
949        let mutation_capacity = mutations.len();
950        let mut mutations = mutations.into_iter();
951        self.execute_accepted_structural_mutation_batch_inner(
952            catalog,
953            mutation_capacity,
954            0,
955            || {
956                Ok(mutations
957                    .next()
958                    .map(|mutation| AcceptedStructuralMutationBatchItem {
959                        catalog: catalog.clone(),
960                        mutation,
961                    }))
962            },
963            Timestamp::now(),
964            AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
965            |rows, _report| {
966                let rows = rows
967                    .into_iter()
968                    .map(AcceptedStructuralMutationRow::into_values)
969                    .collect::<Vec<_>>();
970                precommit_validation(rows.as_slice())?;
971                Ok((rows, AcceptedStructuralMutationCommitDirective::Standard))
972            },
973        )
974    }
975
976    /// Materialize one accepted structural batch, let its caller prepare and
977    /// validate the final after-images, then commit atomically.
978    ///
979    /// The caller freezes one operation timestamp and supplies frontend-lowered
980    /// intent only. Accepted defaults, generated values, managed timestamps,
981    /// constraints, relations, row encoding, and commit preparation remain
982    /// owned by this database boundary.
983    /// Count-only callers omit result values, never row validation or constraints.
984    pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
985        &self,
986        catalog: &AcceptedSchemaCatalogContext,
987        capture_output_values: bool,
988        mutations: Vec<AcceptedStructuralMutation>,
989        operation_timestamp: Timestamp,
990        precommit_preparation: impl FnOnce(
991            Vec<AcceptedStructuralMutationRow>,
992        ) -> Result<T, InternalError>,
993    ) -> Result<T, InternalError> {
994        let mutation_capacity = mutations.len();
995        let identity_candidate_count = mutations
996            .iter()
997            .filter(|mutation| {
998                matches!(
999                    mutation,
1000                    AcceptedStructuralMutation::Save {
1001                        mode: MutationMode::Insert,
1002                        target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1003                        ..
1004                    }
1005                )
1006            })
1007            .count();
1008        let mut mutations = mutations.into_iter();
1009        self.execute_accepted_structural_mutation_batch_inner(
1010            catalog,
1011            mutation_capacity,
1012            identity_candidate_count,
1013            || {
1014                Ok(mutations
1015                    .next()
1016                    .map(|mutation| AcceptedStructuralMutationBatchItem {
1017                        catalog: catalog.clone(),
1018                        mutation,
1019                    }))
1020            },
1021            operation_timestamp,
1022            AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
1023            |rows, _report| {
1024                precommit_preparation(rows).map(|prepared| {
1025                    (
1026                        prepared,
1027                        AcceptedStructuralMutationCommitDirective::Standard,
1028                    )
1029                })
1030            },
1031        )
1032    }
1033
1034    /// Commit one complete accepted update page and its exact durable progress successor.
1035    #[cfg(test)]
1036    pub(in crate::db::session) fn execute_accepted_structural_update_with_mutation_progress(
1037        &self,
1038        catalog: &AcceptedSchemaCatalogContext,
1039        _descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1040        mutations: Vec<AcceptedStructuralMutation>,
1041        operation_timestamp: Timestamp,
1042        mutation_progress: MutationProgressRecordOp,
1043    ) -> Result<usize, InternalError> {
1044        let mutation_capacity = mutations.len();
1045        let mut mutations = mutations.into_iter();
1046        self.execute_accepted_structural_mutation_batch_inner(
1047            catalog,
1048            mutation_capacity,
1049            0,
1050            || {
1051                Ok(mutations
1052                    .next()
1053                    .map(|mutation| AcceptedStructuralMutationBatchItem {
1054                        catalog: catalog.clone(),
1055                        mutation,
1056                    }))
1057            },
1058            operation_timestamp,
1059            AcceptedStructuralMutationCommitOptions::with_mutation_progress(),
1060            |rows, _report| {
1061                Ok((
1062                    rows.len(),
1063                    AcceptedStructuralMutationCommitDirective::WithMutationProgress(
1064                        mutation_progress,
1065                    ),
1066                ))
1067            },
1068        )
1069    }
1070
1071    /// Pack a checkpoint-aware update prefix using the writer's exact staging
1072    /// charge, then apply the caller's atomic commit decision.
1073    #[cfg(any(feature = "sql", test))]
1074    pub(in crate::db::session) fn execute_accepted_structural_update_bounded_prefix<T>(
1075        &self,
1076        catalog: &AcceptedSchemaCatalogContext,
1077        _descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1078        mutation_capacity: usize,
1079        mut next_mutation: impl FnMut() -> Result<Option<AcceptedStructuralMutation>, InternalError>,
1080        operation_timestamp: Timestamp,
1081        precommit_preparation: impl FnOnce(
1082            AcceptedStructuralMutationPackingReport,
1083        ) -> Result<
1084            (T, AcceptedStructuralMutationCommitDirective),
1085            InternalError,
1086        >,
1087    ) -> Result<T, InternalError> {
1088        self.execute_accepted_structural_mutation_batch_inner(
1089            catalog,
1090            mutation_capacity,
1091            0,
1092            || {
1093                next_mutation().map(|mutation| {
1094                    mutation.map(|mutation| AcceptedStructuralMutationBatchItem {
1095                        catalog: catalog.clone(),
1096                        mutation,
1097                    })
1098                })
1099            },
1100            operation_timestamp,
1101            AcceptedStructuralMutationCommitOptions::bounded_prefix(),
1102            |rows, report| {
1103                if rows.len() != report.admitted_mutations() {
1104                    return Err(InternalError::executor_invariant());
1105                }
1106                precommit_preparation(report)
1107            },
1108        )
1109    }
1110
1111    #[expect(
1112        clippy::too_many_arguments,
1113        clippy::too_many_lines,
1114        reason = "one phased owner keeps accepted authority, mutation context, precommit preparation, output capture, and commit staging inseparable"
1115    )]
1116    fn execute_accepted_structural_mutation_batch_inner<T>(
1117        &self,
1118        anchor_catalog: &AcceptedSchemaCatalogContext,
1119        mutation_capacity: usize,
1120        identity_candidate_count: usize,
1121        mut next_mutation: impl FnMut() -> Result<
1122            Option<AcceptedStructuralMutationBatchItem>,
1123            InternalError,
1124        >,
1125        operation_timestamp: Timestamp,
1126        options: AcceptedStructuralMutationCommitOptions,
1127        precommit_preparation: impl FnOnce(
1128            Vec<AcceptedStructuralMutationRow>,
1129            AcceptedStructuralMutationPackingReport,
1130        ) -> Result<
1131            (T, AcceptedStructuralMutationCommitDirective),
1132            InternalError,
1133        >,
1134    ) -> Result<T, InternalError> {
1135        let AcceptedStructuralMutationCommitOptions {
1136            capture_output_values,
1137            packing,
1138        } = options;
1139        let anchor_identity = anchor_catalog.identity();
1140        let accepted_root_identity = anchor_catalog.runtime_root_identity();
1141        let store_path = anchor_identity.store_path();
1142        let store = self.db.recovered_store(store_path)?;
1143        let write_context = dynamic_write_context(operation_timestamp);
1144        if mutation_capacity > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1145            return Err(InternalError::mutation_batch_too_many_items(
1146                mutation_capacity,
1147                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1148            ));
1149        }
1150        let _ = checked_pre_key_candidate_count(identity_candidate_count)?;
1151        let mut entity_states: Vec<AcceptedStructuralMutationEntityState> = Vec::new();
1152        let mut scheduler = AcceptedMutationConstraintScheduler::new(mutation_capacity);
1153        let mut output = Vec::with_capacity(mutation_capacity);
1154        let mut staged_bytes = 0_usize;
1155        let mut stopped_before_candidate = false;
1156        let mut candidate_exceeds_batch_policy = false;
1157        let mut input_index = 0_usize;
1158
1159        while let Some(item) = next_mutation()? {
1160            if input_index >= mutation_capacity {
1161                return Err(InternalError::mutation_batch_too_many_items(
1162                    input_index.saturating_add(1),
1163                    mutation_capacity,
1164                ));
1165            }
1166            let batch_input_ordinal = u32::try_from(input_index).map_err(|_| {
1167                InternalError::mutation_batch_too_many_items(
1168                    mutation_capacity,
1169                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1170                )
1171            })?;
1172            input_index = input_index.saturating_add(1);
1173            let catalog = &item.catalog;
1174            let identity = catalog.identity();
1175            if catalog.runtime_root_identity() != accepted_root_identity
1176                || identity.store_path() != store_path
1177            {
1178                return Err(InternalError::query_executor_invariant());
1179            }
1180            let descriptor =
1181                AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1182            let row_decode_contract =
1183                descriptor.row_decode_contract(catalog.value_catalog_handle().clone());
1184            let entity_path = identity.entity_path();
1185            let _metrics_span = EntityMetricsSpan::new(entity_path);
1186            let row_contract = StructuralRowContract::from_accepted_decode_contract(
1187                entity_path,
1188                row_decode_contract.clone(),
1189            );
1190            let entity_state_index = entity_states
1191                .iter()
1192                .position(|state| state.entity_tag == identity.entity_tag());
1193            let entity_state_index = if let Some(index) = entity_state_index {
1194                index
1195            } else {
1196                if entity_states.len() >= MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
1197                    return Err(InternalError::mutation_batch_too_many_entities(
1198                        entity_states.len().saturating_add(1),
1199                        MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
1200                    ));
1201                }
1202                let identity_field = accepted_identity_insert_field(&descriptor)?;
1203                let identity_incarnation = identity_field
1204                    .as_ref()
1205                    .map(|_| database_incarnation_id())
1206                    .transpose()?;
1207                entity_states.push(AcceptedStructuralMutationEntityState {
1208                    entity_tag: identity.entity_tag(),
1209                    identity_field,
1210                    identity_incarnation,
1211                    identity_cursor: None,
1212                    identity_insert_ordinal: 0,
1213                });
1214                entity_states.len().saturating_sub(1)
1215            };
1216            let identity_field = entity_states[entity_state_index].identity_field.clone();
1217            let identity_insert_ordinal = entity_states[entity_state_index].identity_insert_ordinal;
1218            let mutation = item.mutation;
1219            let AcceptedStructuralMutation::Save {
1220                mode,
1221                target,
1222                patch: authored_patch,
1223            } = mutation
1224            else {
1225                let AcceptedStructuralMutation::Delete { key } = mutation else {
1226                    return Err(InternalError::executor_invariant());
1227                };
1228                let before = validated_existing_row(store, &key, &row_contract)?
1229                    .ok_or_else(|| InternalError::store_not_found(&key))?;
1230                let raw_key = key.to_raw()?;
1231                let canonical_before = canonical_row_from_raw_row_with_accepted_decode_contract(
1232                    entity_path,
1233                    row_decode_contract.clone(),
1234                    &before,
1235                )?;
1236                let admission = admit_structural_mutation_staged_charge(
1237                    &mut staged_bytes,
1238                    [
1239                        raw_key.as_bytes().len(),
1240                        canonical_before.as_raw_row().as_bytes().len(),
1241                    ],
1242                    packing,
1243                )?;
1244                match admission {
1245                    AcceptedStructuralMutationStagedAdmission::Admitted => {}
1246                    AcceptedStructuralMutationStagedAdmission::PageFull => {
1247                        stopped_before_candidate = true;
1248                        break;
1249                    }
1250                    AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy => {
1251                        stopped_before_candidate = true;
1252                        candidate_exceeds_batch_policy = true;
1253                        break;
1254                    }
1255                }
1256                scheduler.schedule_delete(
1257                    entity_path,
1258                    identity.entity_tag(),
1259                    catalog.fingerprint(),
1260                    CommitRowOp::new(
1261                        entity_path,
1262                        raw_key,
1263                        Some(canonical_before.as_raw_row().as_bytes().to_vec()),
1264                        None,
1265                        catalog.fingerprint(),
1266                    ),
1267                    batch_input_ordinal,
1268                )?;
1269                let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1270                    canonical_before.as_raw_row(),
1271                    &row_contract,
1272                )?;
1273                let values = if capture_output_values {
1274                    into_mutation_output_values(reader, &descriptor)?
1275                } else {
1276                    Vec::new()
1277                };
1278                output.push(AcceptedStructuralMutationRow {
1279                    values,
1280                    logical_changed: true,
1281                });
1282                continue;
1283            };
1284            let mutation_context = mutation_diagnostic_context(catalog, mode, batch_input_ordinal);
1285            let (expected_key, preloaded_before, pre_key_insert, mut keyed_patch) = match target {
1286                AcceptedStructuralMutationTarget::ResolveFromAfterImage => {
1287                    let candidate_ordinal =
1288                        if identity_field.is_some() && matches!(mode, MutationMode::Insert) {
1289                            identity_insert_ordinal
1290                        } else {
1291                            batch_input_ordinal
1292                        };
1293                    (
1294                        None,
1295                        None,
1296                        Some(AcceptedPreKeyInsert::new(
1297                            identity.entity_tag(),
1298                            authored_patch,
1299                            candidate_ordinal,
1300                        )),
1301                        None,
1302                    )
1303                }
1304                AcceptedStructuralMutationTarget::Expected(key) => {
1305                    (Some(*key), None, None, Some(authored_patch))
1306                }
1307                AcceptedStructuralMutationTarget::ExpectedLoaded(loaded) => {
1308                    let (key, row) = loaded.into_parts();
1309                    (Some(key), Some(row), None, Some(authored_patch))
1310                }
1311            };
1312            if matches!(mode, MutationMode::Replace)
1313                && let Some(key) = expected_key.as_ref()
1314            {
1315                let patch = keyed_patch
1316                    .take()
1317                    .ok_or_else(InternalError::executor_invariant)?;
1318                keyed_patch = Some(preserve_dynamic_replacement_identity(
1319                    key,
1320                    &descriptor,
1321                    patch,
1322                )?);
1323            }
1324            let patch = pre_key_insert
1325                .as_ref()
1326                .map(AcceptedPreKeyInsert::fields)
1327                .or(keyed_patch.as_ref())
1328                .ok_or_else(InternalError::executor_invariant)?;
1329            let before = match (expected_key.as_ref(), preloaded_before) {
1330                (Some(_), Some(row)) => Some(row),
1331                (Some(key), None) => validated_existing_row(store, key, &row_contract)?,
1332                (None, None) => None,
1333                (None, Some(_)) => return Err(InternalError::executor_invariant()),
1334            };
1335            match mode {
1336                MutationMode::Insert if before.is_some() => {
1337                    return Err(mutation_key_exists_error());
1338                }
1339                MutationMode::Update if before.is_none() => {
1340                    let key = expected_key
1341                        .as_ref()
1342                        .ok_or_else(InternalError::executor_invariant)?;
1343                    return Err(InternalError::store_not_found(key));
1344                }
1345                MutationMode::Insert | MutationMode::Replace | MutationMode::Update => {}
1346            }
1347
1348            let identity_allocation = if let Some(identity_field) = identity_field.as_ref()
1349                && matches!(mode, MutationMode::Insert)
1350                && before.is_none()
1351            {
1352                let candidate = pre_key_insert.as_ref().ok_or_else(|| {
1353                    InternalError::mutation_database_owned_field_explicit(
1354                        mutation_context,
1355                        identity_field.field_id.get(),
1356                    )
1357                })?;
1358                if entity_states[entity_state_index].identity_cursor.is_none() {
1359                    let incarnation = entity_states[entity_state_index]
1360                        .identity_incarnation
1361                        .ok_or_else(InternalError::identity_state_corruption)?;
1362                    entity_states[entity_state_index].identity_cursor =
1363                        Some(store.with_schema(|schema_store| {
1364                            schema_store.identity_statement_cursor(
1365                                incarnation,
1366                                identity.entity_tag(),
1367                                identity_field.field_id,
1368                                &identity_field.accepted_kind,
1369                            )
1370                        })?);
1371                }
1372                let allocation = entity_states[entity_state_index]
1373                    .identity_cursor
1374                    .as_mut()
1375                    .ok_or_else(InternalError::identity_state_corruption)?
1376                    .allocate(identity_field.field_slot, candidate.input_ordinal())?;
1377                entity_states[entity_state_index].identity_insert_ordinal = identity_insert_ordinal
1378                    .checked_add(1)
1379                    .ok_or_else(InternalError::identity_candidate_count_exhausted)?;
1380                Some(allocation)
1381            } else if let Some(identity_field) = identity_field.as_ref()
1382                && matches!(mode, MutationMode::Replace)
1383                && before.is_none()
1384            {
1385                return Err(InternalError::mutation_database_owned_field_explicit(
1386                    mutation_context,
1387                    identity_field.field_id.get(),
1388                ));
1389            } else {
1390                None
1391            };
1392
1393            let resolved = match (mode, before.as_ref()) {
1394                (MutationMode::Insert | MutationMode::Replace, None) => {
1395                    resolve_insert_structural_patch_with_accepted_contract(
1396                        entity_path,
1397                        row_decode_contract.clone(),
1398                        catalog.fingerprint(),
1399                        catalog.accepted_row_constraints(),
1400                        patch,
1401                        write_context,
1402                        mutation_context,
1403                        identity_allocation.as_ref(),
1404                    )?
1405                }
1406                (MutationMode::Update, Some(before)) => {
1407                    resolve_update_structural_patch_with_accepted_contract(
1408                        entity_path,
1409                        row_decode_contract.clone(),
1410                        catalog.fingerprint(),
1411                        catalog.accepted_row_constraints(),
1412                        before,
1413                        patch,
1414                        write_context,
1415                        mutation_context,
1416                    )?
1417                }
1418                (MutationMode::Replace, Some(before)) => {
1419                    resolve_existing_replace_structural_patch_with_accepted_contract(
1420                        entity_path,
1421                        row_decode_contract.clone(),
1422                        catalog.fingerprint(),
1423                        catalog.accepted_row_constraints(),
1424                        before,
1425                        patch,
1426                        write_context,
1427                        mutation_context,
1428                    )?
1429                }
1430                (MutationMode::Insert, Some(_)) | (MutationMode::Update, None) => {
1431                    return Err(InternalError::executor_invariant());
1432                }
1433            };
1434            let (after, provenance) = resolved.into_parts();
1435            let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1436                after.as_raw_row(),
1437                &row_contract,
1438            )?;
1439            let data_key = match expected_key {
1440                Some(key) => {
1441                    reader.validate_primary_key(&key)?;
1442                    key
1443                }
1444                None => data_key_from_validated_reader(identity.entity_tag(), &reader)?,
1445            };
1446            if let Some(allocation) = identity_allocation.as_ref() {
1447                validate_identity_materialization(
1448                    identity.entity_tag(),
1449                    identity_field
1450                        .as_ref()
1451                        .ok_or_else(InternalError::identity_corruption)?,
1452                    pre_key_insert
1453                        .as_ref()
1454                        .ok_or_else(InternalError::identity_corruption)?,
1455                    allocation,
1456                    &data_key,
1457                    &reader,
1458                )?;
1459            }
1460            if matches!(mode, MutationMode::Insert)
1461                && validated_existing_row(store, &data_key, &row_contract)?.is_some()
1462            {
1463                return Err(insert_key_exists_after_generation(
1464                    identity_allocation.is_some(),
1465                ));
1466            }
1467            let raw_key = data_key.to_raw()?;
1468            let canonical_before = before
1469                .as_ref()
1470                .map(|before| {
1471                    canonical_row_from_raw_row_with_accepted_decode_contract(
1472                        entity_path,
1473                        row_decode_contract.clone(),
1474                        before,
1475                    )
1476                })
1477                .transpose()?;
1478            let logical_changed = canonical_before.as_ref().is_none_or(|before| {
1479                before.as_raw_row().as_bytes() != after.as_raw_row().as_bytes()
1480            });
1481            let physical_changed = before
1482                .as_ref()
1483                .is_none_or(|before| before.as_bytes() != after.as_raw_row().as_bytes());
1484            let admission = admit_structural_mutation_staged_charge(
1485                &mut staged_bytes,
1486                [
1487                    raw_key.as_bytes().len(),
1488                    canonical_before
1489                        .as_ref()
1490                        .map_or(0, |before| before.as_raw_row().as_bytes().len()),
1491                    after.as_raw_row().as_bytes().len(),
1492                ],
1493                packing,
1494            )?;
1495            match admission {
1496                AcceptedStructuralMutationStagedAdmission::Admitted => {}
1497                AcceptedStructuralMutationStagedAdmission::PageFull => {
1498                    stopped_before_candidate = true;
1499                    break;
1500                }
1501                AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy => {
1502                    stopped_before_candidate = true;
1503                    candidate_exceeds_batch_policy = true;
1504                    break;
1505                }
1506            }
1507            let row_op = physical_changed.then(|| {
1508                CommitRowOp::new(
1509                    entity_path,
1510                    raw_key.clone(),
1511                    canonical_before
1512                        .as_ref()
1513                        .map(|before| before.as_raw_row().as_bytes().to_vec()),
1514                    Some(after.as_raw_row().as_bytes().to_vec()),
1515                    catalog.fingerprint(),
1516                )
1517            });
1518            scheduler.schedule_save_after_image(
1519                AcceptedMutationConstraintContext {
1520                    entity_path,
1521                    entity_tag: identity.entity_tag(),
1522                    row_decode_contract: row_decode_contract.clone(),
1523                    schema_fingerprint: catalog.fingerprint(),
1524                    fingerprint_method: catalog.fingerprint_method_version(),
1525                    row_constraints: catalog.accepted_row_constraints(),
1526                },
1527                mode,
1528                &data_key,
1529                after.as_raw_row(),
1530                provenance.as_slice(),
1531                row_op,
1532                batch_input_ordinal,
1533            )?;
1534            let values = if capture_output_values {
1535                into_mutation_output_values(reader, &descriptor)?
1536            } else {
1537                Vec::new()
1538            };
1539            output.push(AcceptedStructuralMutationRow {
1540                values,
1541                logical_changed,
1542            });
1543        }
1544
1545        let report = AcceptedStructuralMutationPackingReport {
1546            admitted_mutations: output.len(),
1547            staged_bytes,
1548            stopped_before_candidate,
1549            candidate_exceeds_batch_policy,
1550        };
1551        let batch = scheduler.finish();
1552        let (prepared, commit_directive) = precommit_preparation(output, report)?;
1553        finish_current_execution_instruction_watermark()?;
1554        let mut identity_ranges = Vec::with_capacity(entity_states.len());
1555        for state in entity_states {
1556            if let Some(range) = state
1557                .identity_cursor
1558                .map(IdentityStatementCursor::into_range_advance)
1559                .transpose()?
1560                .flatten()
1561            {
1562                identity_ranges.push(range);
1563            }
1564        }
1565        if !matches!(
1566            commit_directive,
1567            AcceptedStructuralMutationCommitDirective::Skip
1568        ) && batch.is_empty()
1569            && !identity_ranges.is_empty()
1570        {
1571            return Err(InternalError::identity_corruption());
1572        }
1573        match commit_directive {
1574            AcceptedStructuralMutationCommitDirective::Skip => {}
1575            AcceptedStructuralMutationCommitDirective::Standard if batch.is_empty() => {}
1576            AcceptedStructuralMutationCommitDirective::Standard => {
1577                commit_structural_row_ops_with_window(&self.db, batch, identity_ranges)?;
1578            }
1579            AcceptedStructuralMutationCommitDirective::WithMutationProgress(operation)
1580                if batch.is_empty() =>
1581            {
1582                let _ = operation;
1583                return Err(InternalError::executor_invariant());
1584            }
1585            AcceptedStructuralMutationCommitDirective::WithMutationProgress(operation) => {
1586                commit_structural_row_ops_with_mutation_progress(
1587                    &self.db,
1588                    batch,
1589                    identity_ranges,
1590                    operation,
1591                )?;
1592            }
1593        }
1594        Ok(prepared)
1595    }
1596
1597    fn execute_lowered_dynamic_mutation_batch(
1598        &self,
1599        catalog: &AcceptedSchemaCatalogContext,
1600        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1601        mutations: Vec<AcceptedStructuralMutation>,
1602        enforce_mixed_batch_result_bound: bool,
1603    ) -> Result<DynamicMutationResult, InternalError> {
1604        self.execute_accepted_structural_save_batch(
1605            catalog,
1606            true,
1607            mutations,
1608            Timestamp::now(),
1609            |rows| {
1610                prepare_dynamic_mutation_result(
1611                    catalog,
1612                    descriptor,
1613                    rows,
1614                    enforce_mixed_batch_result_bound,
1615                )
1616            },
1617        )
1618    }
1619
1620    /// Execute one trusted entity-name-driven structural mutation.
1621    ///
1622    /// This lane resolves public values, defaults, generation, management,
1623    /// constraints, relations, and commit preparation from accepted schema.
1624    /// It never materializes a generated entity or invokes application
1625    /// validators/normalizers.
1626    pub fn execute_trusted_dynamic_mutation(
1627        &self,
1628        request: &DynamicMutation,
1629    ) -> Result<DynamicMutationResult, InternalError> {
1630        if request.entity().is_empty() {
1631            return Err(InternalError::executor_unsupported());
1632        }
1633        let catalog =
1634            self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
1635        let descriptor =
1636            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1637        let mutation = lower_dynamic_mutation_intent(&catalog, &descriptor, request.clone(), 0)?;
1638
1639        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
1640    }
1641
1642    /// Execute one bounded same-store structural mutation batch atomically.
1643    ///
1644    /// Every item resolves from one captured accepted root and store, shares
1645    /// one operation timestamp, and is projected to its public result before
1646    /// the commit marker can be published.
1647    pub fn execute_trusted_dynamic_mutation_batch(
1648        &self,
1649        requests: Vec<DynamicMutation>,
1650    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
1651        self.execute_trusted_dynamic_mutation_batch_mixed(requests)
1652    }
1653
1654    fn execute_trusted_dynamic_mutation_batch_mixed(
1655        &self,
1656        requests: Vec<DynamicMutation>,
1657    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
1658        if requests.is_empty() {
1659            return Err(InternalError::mutation_batch_empty());
1660        }
1661        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1662            return Err(InternalError::mutation_batch_too_many_items(
1663                requests.len(),
1664                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1665            ));
1666        }
1667        let first = requests
1668            .first()
1669            .ok_or_else(InternalError::mutation_batch_empty)?;
1670        if first.entity().is_empty() {
1671            return Err(InternalError::executor_unsupported());
1672        }
1673        let anchor_catalog =
1674            self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
1675        let anchor_identity = anchor_catalog.identity();
1676        let mut entity_tags = std::collections::BTreeSet::new();
1677        let mut items = Vec::with_capacity(requests.len());
1678        let mut result_catalogs = Vec::with_capacity(requests.len());
1679        let mut identity_candidate_count = 0_usize;
1680
1681        let request_count = requests.len();
1682        for (batch_position, request) in requests.into_iter().enumerate() {
1683            let batch_position = u32::try_from(batch_position).map_err(|_| {
1684                InternalError::mutation_batch_too_many_items(
1685                    request_count,
1686                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1687                )
1688            })?;
1689            if request.entity().is_empty() {
1690                return Err(InternalError::executor_unsupported());
1691            }
1692            let item_catalog = anchor_catalog
1693                .for_entity_name(request.entity())
1694                .ok_or_else(|| InternalError::unsupported_entity_path(request.entity()))?;
1695            let item_identity = item_catalog.identity();
1696            if item_identity.store_path() != anchor_identity.store_path() {
1697                return Err(InternalError::mutation_batch_store_mismatch(
1698                    batch_position,
1699                    anchor_identity.entity_tag().value(),
1700                    item_identity.entity_tag().value(),
1701                ));
1702            }
1703            entity_tags.insert(item_identity.entity_tag());
1704            if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
1705                return Err(InternalError::mutation_batch_too_many_entities(
1706                    entity_tags.len(),
1707                    MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
1708                ));
1709            }
1710            let descriptor =
1711                AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
1712            let mutation =
1713                lower_dynamic_mutation_intent(&item_catalog, &descriptor, request, batch_position)?;
1714            if matches!(
1715                mutation,
1716                AcceptedStructuralMutation::Save {
1717                    mode: MutationMode::Insert,
1718                    target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1719                    ..
1720                }
1721            ) {
1722                identity_candidate_count = identity_candidate_count.saturating_add(1);
1723            }
1724            result_catalogs.push(item_catalog.clone());
1725            items.push(AcceptedStructuralMutationBatchItem {
1726                catalog: item_catalog,
1727                mutation,
1728            });
1729        }
1730
1731        self.execute_lowered_mixed_mutation_batch(
1732            &anchor_catalog,
1733            items,
1734            result_catalogs,
1735            identity_candidate_count,
1736        )
1737    }
1738
1739    fn execute_lowered_mixed_mutation_batch(
1740        &self,
1741        anchor_catalog: &AcceptedSchemaCatalogContext,
1742        items: Vec<AcceptedStructuralMutationBatchItem>,
1743        result_catalogs: Vec<AcceptedSchemaCatalogContext>,
1744        identity_candidate_count: usize,
1745    ) -> Result<Vec<DynamicMutationResult>, InternalError> {
1746        if items.len() != result_catalogs.len() {
1747            return Err(InternalError::executor_invariant());
1748        }
1749        let mutation_count = items.len();
1750        let mut items = items.into_iter();
1751        self.execute_accepted_structural_mutation_batch_inner(
1752            anchor_catalog,
1753            mutation_count,
1754            identity_candidate_count,
1755            || Ok(items.next()),
1756            Timestamp::now(),
1757            AcceptedStructuralMutationCommitOptions::standard(true),
1758            |rows, _report| {
1759                if rows.len() != result_catalogs.len() {
1760                    return Err(InternalError::executor_invariant());
1761                }
1762                let mut results = Vec::with_capacity(rows.len());
1763                for (row, catalog) in rows.into_iter().zip(result_catalogs.iter()) {
1764                    let descriptor =
1765                        AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1766                    results.push(prepare_dynamic_mutation_result(
1767                        catalog,
1768                        &descriptor,
1769                        vec![row],
1770                        false,
1771                    )?);
1772                }
1773                let encoded = candid::encode_one(&results)
1774                    .map_err(|_| InternalError::executor_invariant())?;
1775                validate_structural_mutation_result_bytes(encoded.len())?;
1776                Ok((results, AcceptedStructuralMutationCommitDirective::Standard))
1777            },
1778        )
1779    }
1780
1781    /// Execute one generated typed write through immutable accepted entity and
1782    /// field identities. `None` means the opaque binding is stale.
1783    #[doc(hidden)]
1784    pub fn execute_trusted_typed_mutation(
1785        &self,
1786        binding: &DynamicTypedEntityBinding,
1787        request: DynamicTypedMutation,
1788    ) -> Result<Option<DynamicMutationResult>, InternalError> {
1789        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
1790            return Ok(None);
1791        };
1792        let descriptor =
1793            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1794        let Some(mutation) =
1795            lower_typed_mutation_intent(&catalog, &descriptor, binding, request, 0)?
1796        else {
1797            return Ok(None);
1798        };
1799        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
1800            .map(Some)
1801    }
1802
1803    /// Execute one bounded same-entity generated typed-write batch through one
1804    /// exact current binding. `None` means the binding or a patch is stale or
1805    /// mismatched.
1806    #[doc(hidden)]
1807    pub fn execute_trusted_same_entity_typed_mutation_batch(
1808        &self,
1809        binding: &DynamicTypedEntityBinding,
1810        requests: Vec<DynamicTypedMutation>,
1811    ) -> Result<Option<DynamicMutationResult>, InternalError> {
1812        if requests.is_empty() {
1813            return Err(InternalError::mutation_batch_empty());
1814        }
1815        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1816            return Err(InternalError::mutation_batch_too_many_items(
1817                requests.len(),
1818                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1819            ));
1820        }
1821        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
1822            return Ok(None);
1823        };
1824        let descriptor =
1825            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1826        let mut mutations = Vec::with_capacity(requests.len());
1827        let request_count = requests.len();
1828        for (batch_position, request) in requests.into_iter().enumerate() {
1829            let batch_position = u32::try_from(batch_position).map_err(|_| {
1830                InternalError::mutation_batch_too_many_items(
1831                    request_count,
1832                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1833                )
1834            })?;
1835            let Some(mutation) = lower_typed_mutation_intent(
1836                &catalog,
1837                &descriptor,
1838                binding,
1839                request,
1840                batch_position,
1841            )?
1842            else {
1843                return Ok(None);
1844            };
1845            mutations.push(mutation);
1846        }
1847
1848        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, true)
1849            .map(Some)
1850    }
1851
1852    /// Execute one bounded generated typed-write batch atomically through
1853    /// exact current same-store bindings. `None` means a binding or patch is
1854    /// stale or mismatched.
1855    #[doc(hidden)]
1856    pub fn execute_trusted_typed_mutation_batch(
1857        &self,
1858        requests: Vec<(DynamicTypedEntityBinding, DynamicTypedMutation)>,
1859    ) -> Result<Option<Vec<DynamicMutationResult>>, InternalError> {
1860        if requests.is_empty() {
1861            return Err(InternalError::mutation_batch_empty());
1862        }
1863        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1864            return Err(InternalError::mutation_batch_too_many_items(
1865                requests.len(),
1866                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1867            ));
1868        }
1869        let first_binding = requests
1870            .first()
1871            .map(|(binding, _)| binding)
1872            .ok_or_else(InternalError::mutation_batch_empty)?;
1873        let Some(catalog) = self.current_typed_entity_binding_catalog(first_binding)? else {
1874            return Ok(None);
1875        };
1876        let anchor_identity = catalog.identity();
1877        let mut entity_tags = std::collections::BTreeSet::new();
1878        let mut items = Vec::with_capacity(requests.len());
1879        let mut result_catalogs = Vec::with_capacity(requests.len());
1880        let mut identity_candidate_count = 0_usize;
1881
1882        let request_count = requests.len();
1883        for (batch_position, (binding, request)) in requests.into_iter().enumerate() {
1884            let batch_position = u32::try_from(batch_position).map_err(|_| {
1885                InternalError::mutation_batch_too_many_items(
1886                    request_count,
1887                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1888                )
1889            })?;
1890            let Some(item_catalog) = catalog.for_entity_path(binding.entity_source.as_str()) else {
1891                return Ok(None);
1892            };
1893            if !self.typed_entity_binding_matches_catalog(
1894                &binding,
1895                &item_catalog,
1896                database_incarnation_id()?.to_bytes(),
1897            )? {
1898                return Ok(None);
1899            }
1900            let item_identity = item_catalog.identity();
1901            if item_identity.store_path() != anchor_identity.store_path() {
1902                return Err(InternalError::mutation_batch_store_mismatch(
1903                    batch_position,
1904                    anchor_identity.entity_tag().value(),
1905                    item_identity.entity_tag().value(),
1906                ));
1907            }
1908            entity_tags.insert(item_identity.entity_tag());
1909            if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
1910                return Err(InternalError::mutation_batch_too_many_entities(
1911                    entity_tags.len(),
1912                    MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
1913                ));
1914            }
1915            let descriptor =
1916                AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
1917            let Some(mutation) = lower_typed_mutation_intent(
1918                &item_catalog,
1919                &descriptor,
1920                &binding,
1921                request,
1922                batch_position,
1923            )?
1924            else {
1925                return Ok(None);
1926            };
1927            if matches!(
1928                mutation,
1929                AcceptedStructuralMutation::Save {
1930                    mode: MutationMode::Insert,
1931                    target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1932                    ..
1933                }
1934            ) {
1935                identity_candidate_count = identity_candidate_count.saturating_add(1);
1936            }
1937            result_catalogs.push(item_catalog.clone());
1938            items.push(AcceptedStructuralMutationBatchItem {
1939                catalog: item_catalog,
1940                mutation,
1941            });
1942        }
1943
1944        self.execute_lowered_mixed_mutation_batch(
1945            &catalog,
1946            items,
1947            result_catalogs,
1948            identity_candidate_count,
1949        )
1950        .map(Some)
1951    }
1952
1953    /// Execute one trusted atomic insert batch from entity-name-driven patches.
1954    ///
1955    /// Every patch is lowered against the same accepted snapshot and shares
1956    /// one operation timestamp before the canonical structural batch owner
1957    /// stages any durable effect.
1958    pub fn execute_trusted_dynamic_insert_batch(
1959        &self,
1960        entity: &str,
1961        patches: Vec<DynamicStructuralPatch>,
1962    ) -> Result<DynamicMutationResult, InternalError> {
1963        let patch_count = patches.len();
1964        if patch_count == 0 {
1965            return Err(InternalError::mutation_batch_empty());
1966        }
1967        if patch_count > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1968            return Err(InternalError::mutation_batch_too_many_items(
1969                patch_count,
1970                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1971            ));
1972        }
1973        if entity.is_empty() {
1974            return Err(InternalError::executor_unsupported());
1975        }
1976        let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(entity))?;
1977        let descriptor =
1978            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1979        let mut mutations = Vec::with_capacity(patch_count);
1980        // The batch already names one entity. Lower each patch against that
1981        // captured authority without constructing or resolving per-row names.
1982        for (batch_position, patch) in patches.into_iter().enumerate() {
1983            let batch_position = u32::try_from(batch_position).map_err(|_| {
1984                InternalError::mutation_batch_too_many_items(
1985                    patch_count,
1986                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1987                )
1988            })?;
1989            mutations.push(lower_dynamic_save_intent(
1990                &catalog,
1991                &descriptor,
1992                patch,
1993                MutationMode::Insert,
1994                AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1995                batch_position,
1996            )?);
1997        }
1998
1999        self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, false)
2000    }
2001}
2002
2003#[cfg(test)]
2004mod typed_adapter_tests {
2005    mod binding_diagnostics_tests;
2006    mod incarnation_tests;
2007    mod input_handoff_tests;
2008
2009    use super::{
2010        AcceptedFieldKind, DbSession, DynamicTypedBindingError, DynamicTypedEntityBinding,
2011        DynamicTypedMutation, DynamicWriteCell, TypedEntityDescriptor, TypedFieldType,
2012        typed_adapter_field_kind_matches, typed_descriptor_field_type,
2013    };
2014    use crate::{
2015        db::{
2016            TypedFieldDescriptor,
2017            data::DataStore,
2018            index::IndexStore,
2019            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
2020            schema::{
2021                AcceptedSchemaRevision, FieldId, FieldStorageDecode, LeafCodec,
2022                PersistedFieldSnapshot, PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot,
2023                SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2024                accepted_schema_candidate_with_field_bindings_for_tests,
2025            },
2026        },
2027        traits::{CanisterKind, Path},
2028        types::EntityTag,
2029        value::InputValue,
2030    };
2031    use icydb_schema::{FieldSourceKey, ScalarType};
2032    use std::{cell::RefCell, collections::BTreeMap};
2033
2034    const STORE_PATH: &str = "session::write::typed_adapter_tests::Store";
2035    const OTHER_STORE_PATH: &str = "session::write::typed_adapter_tests::OtherStore";
2036    const ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::Entity";
2037    const OTHER_ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity";
2038    const ID_SOURCE: &str = "session::write::typed_adapter_tests::Entity::id";
2039    const VALUE_SOURCE: &str = "session::write::typed_adapter_tests::Entity::value";
2040    const REPLACEMENT_SOURCE: &str =
2041        "session::write::typed_adapter_tests::Entity::replacement_value";
2042    const OTHER_ID_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity::id";
2043    const ENTITY_DESCRIPTOR: TypedEntityDescriptor = TypedEntityDescriptor::new(
2044        ENTITY_SOURCE,
2045        &[ID_SOURCE],
2046        &[
2047            TypedFieldDescriptor::new(ID_SOURCE, TypedFieldType::Scalar(ScalarType::Nat64), false),
2048            TypedFieldDescriptor::new(
2049                VALUE_SOURCE,
2050                TypedFieldType::Scalar(ScalarType::Nat64),
2051                false,
2052            ),
2053        ],
2054    );
2055    const OTHER_ENTITY_DESCRIPTOR: TypedEntityDescriptor = TypedEntityDescriptor::new(
2056        OTHER_ENTITY_SOURCE,
2057        &[OTHER_ID_SOURCE],
2058        &[TypedFieldDescriptor::new(
2059            OTHER_ID_SOURCE,
2060            TypedFieldType::Scalar(ScalarType::Nat64),
2061            false,
2062        )],
2063    );
2064    const REPLACEMENT_DESCRIPTOR: TypedEntityDescriptor = TypedEntityDescriptor::new(
2065        ENTITY_SOURCE,
2066        &[ID_SOURCE],
2067        &[
2068            TypedFieldDescriptor::new(ID_SOURCE, TypedFieldType::Scalar(ScalarType::Nat64), false),
2069            TypedFieldDescriptor::new(
2070                REPLACEMENT_SOURCE,
2071                TypedFieldType::Scalar(ScalarType::Nat64),
2072                false,
2073            ),
2074        ],
2075    );
2076
2077    struct TestCanister;
2078
2079    impl Path for TestCanister {
2080        const PATH: &'static str = "session::write::typed_adapter_tests::Canister";
2081    }
2082
2083    impl CanisterKind for TestCanister {
2084        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
2085            Ok(41)
2086        }
2087        const COMMIT_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.commit.v1";
2088        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
2089            Ok(49)
2090        }
2091        const STARTUP_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.startup.control.v1";
2092        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
2093            Ok(42)
2094        }
2095        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2096            "icydb.typed_adapter_tests.integrity.progress.v1";
2097    }
2098
2099    thread_local! {
2100        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2101        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2102        static SCHEMA_STORE: RefCell<SchemaStore> =
2103            const { RefCell::new(SchemaStore::init_heap()) };
2104        static OTHER_DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2105        static OTHER_INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2106        static OTHER_SCHEMA_STORE: RefCell<SchemaStore> =
2107            const { RefCell::new(SchemaStore::init_heap()) };
2108        static STORE_REGISTRY: StoreRegistry = {
2109            let mut registry = StoreRegistry::new();
2110            registry.register_store(
2111                STORE_PATH,
2112                &DATA_STORE,
2113                &INDEX_STORE,
2114                &SCHEMA_STORE,
2115                StoreAllocationIdentities::absent(),
2116                StoreRuntimeStorageCapabilities::heap(),
2117            ).expect("typed adapter test store should register");
2118            registry.register_store(
2119                OTHER_STORE_PATH,
2120                &OTHER_DATA_STORE,
2121                &OTHER_INDEX_STORE,
2122                &OTHER_SCHEMA_STORE,
2123                StoreAllocationIdentities::absent(),
2124                StoreRuntimeStorageCapabilities::heap(),
2125            ).expect("second typed adapter test store should register");
2126            registry
2127        };
2128    }
2129
2130    fn nat64_field(id: u32, name: &str, slot: u16) -> PersistedFieldSnapshot {
2131        PersistedFieldSnapshot::new_initial(
2132            FieldId::new(id),
2133            name.to_string(),
2134            SchemaFieldSlot::new(slot),
2135            AcceptedFieldKind::Nat64,
2136            Vec::new(),
2137            false,
2138            SchemaInsertDefault::None,
2139            FieldStorageDecode::ByKind,
2140            LeafCodec::Scalar(ScalarCodec::Nat64),
2141        )
2142    }
2143
2144    fn snapshot(
2145        entity_source: &str,
2146        entity_name: &str,
2147        fields: Vec<PersistedFieldSnapshot>,
2148    ) -> PersistedSchemaSnapshot {
2149        let layout = SchemaRowLayout::initial(
2150            fields
2151                .iter()
2152                .map(|field| (field.id(), field.slot()))
2153                .collect(),
2154        );
2155        PersistedSchemaSnapshot::new(
2156            SchemaVersion::initial(),
2157            entity_source.to_string(),
2158            entity_name.to_string(),
2159            FieldId::new(1),
2160            layout,
2161            fields,
2162        )
2163    }
2164
2165    fn field_source(source: &str) -> FieldSourceKey {
2166        FieldSourceKey::try_new(source).expect("typed field source should admit")
2167    }
2168
2169    fn publish(
2170        session: &DbSession<TestCanister>,
2171        expected: AcceptedSchemaRevision,
2172        revision: AcceptedSchemaRevision,
2173        snapshots: BTreeMap<EntityTag, PersistedSchemaSnapshot>,
2174        fields: BTreeMap<(EntityTag, FieldSourceKey), FieldId>,
2175    ) {
2176        publish_to_store(session, STORE_PATH, expected, revision, snapshots, fields);
2177    }
2178
2179    fn publish_to_store(
2180        session: &DbSession<TestCanister>,
2181        store_path: &'static str,
2182        expected: AcceptedSchemaRevision,
2183        revision: AcceptedSchemaRevision,
2184        snapshots: BTreeMap<EntityTag, PersistedSchemaSnapshot>,
2185        fields: BTreeMap<(EntityTag, FieldSourceKey), FieldId>,
2186    ) {
2187        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2188            store_path, revision, snapshots, fields,
2189        );
2190        let store = session
2191            .db
2192            .store_handle(store_path)
2193            .expect("typed adapter test store should resolve");
2194        crate::db::commit::publish_accepted_schema_candidate(
2195            store_path, store, expected, &candidate,
2196        )
2197        .expect("typed binding candidate should publish");
2198    }
2199
2200    fn initialize_typed_session() -> DbSession<TestCanister> {
2201        let entity_tag = EntityTag::new(91);
2202        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2203        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2204        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2205        OTHER_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2206        OTHER_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2207        OTHER_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2208        let session = DbSession::<TestCanister>::new(
2209            &STORE_REGISTRY,
2210            &crate::db::RequestExecutionRoot::__new_runtime_root(),
2211        );
2212        session
2213            .db
2214            .drive_startup_recovery_page()
2215            .expect("typed adapter test database should initialize");
2216        publish(
2217            &session,
2218            AcceptedSchemaRevision::NONE,
2219            AcceptedSchemaRevision::INITIAL,
2220            BTreeMap::from([(
2221                entity_tag,
2222                snapshot(
2223                    ENTITY_SOURCE,
2224                    "Entity",
2225                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
2226                ),
2227            )]),
2228            BTreeMap::from([
2229                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2230                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
2231            ]),
2232        );
2233        session
2234    }
2235
2236    fn initialize_mixed_typed_session(other_store: bool) -> DbSession<TestCanister> {
2237        let entity_tag = EntityTag::new(91);
2238        let other_entity_tag = EntityTag::new(92);
2239        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2240        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2241        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2242        OTHER_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2243        OTHER_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2244        OTHER_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2245        let session = DbSession::<TestCanister>::new(
2246            &STORE_REGISTRY,
2247            &crate::db::RequestExecutionRoot::__new_runtime_root(),
2248        );
2249        session
2250            .db
2251            .drive_startup_recovery_page()
2252            .expect("mixed typed adapter database should initialize");
2253
2254        let entity_snapshot = snapshot(
2255            ENTITY_SOURCE,
2256            "Entity",
2257            vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
2258        );
2259        let other_snapshot = snapshot(
2260            OTHER_ENTITY_SOURCE,
2261            "OtherEntity",
2262            vec![nat64_field(1, "id", 0)],
2263        );
2264        let entity_fields = BTreeMap::from([
2265            ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2266            ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
2267        ]);
2268        if other_store {
2269            publish(
2270                &session,
2271                AcceptedSchemaRevision::NONE,
2272                AcceptedSchemaRevision::INITIAL,
2273                BTreeMap::from([(entity_tag, entity_snapshot)]),
2274                entity_fields,
2275            );
2276            publish_to_store(
2277                &session,
2278                OTHER_STORE_PATH,
2279                AcceptedSchemaRevision::NONE,
2280                AcceptedSchemaRevision::INITIAL,
2281                BTreeMap::from([(other_entity_tag, other_snapshot)]),
2282                BTreeMap::from([(
2283                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
2284                    FieldId::new(1),
2285                )]),
2286            );
2287        } else {
2288            let mut fields = entity_fields;
2289            fields.insert(
2290                (other_entity_tag, field_source(OTHER_ID_SOURCE)),
2291                FieldId::new(1),
2292            );
2293            publish(
2294                &session,
2295                AcceptedSchemaRevision::NONE,
2296                AcceptedSchemaRevision::INITIAL,
2297                BTreeMap::from([
2298                    (entity_tag, entity_snapshot),
2299                    (other_entity_tag, other_snapshot),
2300                ]),
2301                fields,
2302            );
2303        }
2304        session
2305    }
2306
2307    fn typed_insert(
2308        binding: &DynamicTypedEntityBinding,
2309        id: u64,
2310        value: u64,
2311    ) -> DynamicTypedMutation {
2312        let patch = binding
2313            .bind_write_ordinals(vec![
2314                (0, DynamicWriteCell::Value(InputValue::nat64(id))),
2315                (1, DynamicWriteCell::Value(InputValue::nat64(value))),
2316            ])
2317            .expect("typed insert patch should bind");
2318        DynamicTypedMutation::Insert { patch }
2319    }
2320
2321    fn typed_other_insert(binding: &DynamicTypedEntityBinding, id: u64) -> DynamicTypedMutation {
2322        let patch = binding
2323            .bind_write_ordinals(vec![(0, DynamicWriteCell::Value(InputValue::nat64(id)))])
2324            .expect("other typed insert patch should bind");
2325        DynamicTypedMutation::Insert { patch }
2326    }
2327
2328    fn typed_delete(id: u64) -> DynamicTypedMutation {
2329        DynamicTypedMutation::Delete {
2330            key: InputValue::nat64(id),
2331        }
2332    }
2333
2334    fn typed_value_patch(
2335        binding: &DynamicTypedEntityBinding,
2336        value: u64,
2337    ) -> super::DynamicTypedStructuralPatch {
2338        binding
2339            .bind_write_ordinals(vec![(1, DynamicWriteCell::Value(InputValue::nat64(value)))])
2340            .expect("typed value patch should bind")
2341    }
2342
2343    fn assert_query_diagnostic(
2344        error: crate::db::QueryError,
2345        code: icydb_diagnostic_code::DiagnosticCode,
2346        origin: icydb_diagnostic_code::ErrorOrigin,
2347        detail: icydb_diagnostic_code::DiagnosticDetail,
2348    ) {
2349        let diagnostic = error.diagnostic();
2350        assert_eq!(diagnostic.code(), code);
2351        assert_eq!(diagnostic.origin(), origin);
2352        assert_eq!(diagnostic.detail(), Some(&detail));
2353    }
2354
2355    #[test]
2356    fn typed_adapter_kind_matching_is_exact_but_accepts_relation_key_wrappers() {
2357        let relation = AcceptedFieldKind::Relation {
2358            target_path: "test::Target".to_string(),
2359            target_entity_name: "Target".to_string(),
2360            target_entity_tag: EntityTag::new(7),
2361            target_store_path: "test::Store".to_string(),
2362            key_kind: Box::new(AcceptedFieldKind::Nat64),
2363        };
2364
2365        assert!(typed_adapter_field_kind_matches(
2366            &relation,
2367            &AcceptedFieldKind::Nat64,
2368        ));
2369        assert!(typed_adapter_field_kind_matches(
2370            &AcceptedFieldKind::List(Box::new(relation)),
2371            &AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Nat64)),
2372        ));
2373        assert!(!typed_adapter_field_kind_matches(
2374            &AcceptedFieldKind::Nat64,
2375            &AcceptedFieldKind::Nat32,
2376        ));
2377    }
2378
2379    #[test]
2380    fn typed_adapter_field_contract_rejects_invalid_named_source_identity() {
2381        const NAT64: TypedFieldType = TypedFieldType::Scalar(ScalarType::Nat64);
2382
2383        assert!(matches!(
2384            typed_descriptor_field_type(TypedFieldType::Named("")),
2385            Err(icydb_schema::SchemaContractError::EmptyIdentity),
2386        ));
2387        assert!(matches!(
2388            typed_descriptor_field_type(TypedFieldType::Scalar(ScalarType::Nat16)),
2389            Ok(icydb_schema::FieldType::Scalar(ScalarType::Nat16)),
2390        ));
2391        assert!(matches!(
2392            typed_descriptor_field_type(TypedFieldType::List(&NAT64)),
2393            Ok(icydb_schema::FieldType::List(item))
2394                if *item == icydb_schema::FieldType::Scalar(ScalarType::Nat64),
2395        ));
2396    }
2397
2398    #[test]
2399    fn typed_descriptor_primary_key_must_match_accepted_source_order() {
2400        const PRIMARY_KEY_MISMATCH: TypedEntityDescriptor =
2401            TypedEntityDescriptor::new(ENTITY_SOURCE, &[VALUE_SOURCE], ENTITY_DESCRIPTOR.fields);
2402        const NULLABILITY_MISMATCH: TypedEntityDescriptor = TypedEntityDescriptor::new(
2403            ENTITY_SOURCE,
2404            &[ID_SOURCE],
2405            &[
2406                TypedFieldDescriptor::new(
2407                    ID_SOURCE,
2408                    TypedFieldType::Scalar(ScalarType::Nat64),
2409                    false,
2410                ),
2411                TypedFieldDescriptor::new(
2412                    VALUE_SOURCE,
2413                    TypedFieldType::Scalar(ScalarType::Nat64),
2414                    true,
2415                ),
2416            ],
2417        );
2418
2419        let session = initialize_typed_session();
2420        assert!(matches!(
2421            session.issue_typed_entity_binding(&PRIMARY_KEY_MISMATCH),
2422            Err(DynamicTypedBindingError::IncompatibleField),
2423        ));
2424        assert!(matches!(
2425            session.issue_typed_entity_binding(&NULLABILITY_MISMATCH),
2426            Err(DynamicTypedBindingError::IncompatibleField),
2427        ));
2428    }
2429
2430    #[test]
2431    fn typed_mutation_batch_is_bounded_and_atomic() {
2432        let session = initialize_typed_session();
2433        let binding = session
2434            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2435            .expect("typed batch binding should issue");
2436
2437        session
2438            .execute_trusted_typed_mutation_batch(Vec::new())
2439            .expect_err("empty typed batch should reject");
2440        let insert = typed_insert(&binding, 1, 10);
2441        let oversized = (0..=super::MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
2442            .map(|_| (binding.clone(), insert.clone()))
2443            .collect();
2444        session
2445            .execute_trusted_typed_mutation_batch(oversized)
2446            .expect_err("oversized typed batch should reject");
2447
2448        let duplicate = vec![
2449            (binding.clone(), insert.clone()),
2450            (binding.clone(), typed_insert(&binding, 1, 11)),
2451        ];
2452        session
2453            .execute_trusted_typed_mutation_batch(duplicate)
2454            .expect_err("late duplicate key should reject the whole typed batch");
2455        let empty = session
2456            .execute_trusted_live_page(&crate::db::DynamicQuery::new("Entity"), None)
2457            .expect("failed typed batch should leave the entity readable");
2458        assert!(empty.rows.is_empty());
2459
2460        let result = session
2461            .execute_trusted_typed_mutation_batch(vec![
2462                (binding.clone(), insert),
2463                (binding.clone(), typed_insert(&binding, 2, 20)),
2464            ])
2465            .expect("valid typed batch should execute")
2466            .expect("exact binding should remain current");
2467        assert_eq!(result.len(), 2);
2468        assert!(result.iter().all(|item| item.affected_rows == 1));
2469        assert_eq!(
2470            result
2471                .into_iter()
2472                .map(|item| item.rows.into_iter().next().expect("one row per request"))
2473                .collect::<Vec<_>>(),
2474            vec![
2475                vec![
2476                    crate::value::OutputValue::nat64(1),
2477                    crate::value::OutputValue::nat64(10),
2478                ],
2479                vec![
2480                    crate::value::OutputValue::nat64(2),
2481                    crate::value::OutputValue::nat64(20),
2482                ],
2483            ]
2484        );
2485
2486        let mut mismatched = binding.clone();
2487        mismatched.accepted_revision = mismatched.accepted_revision.saturating_add(1);
2488        let mismatch = session
2489            .execute_trusted_typed_mutation_batch(vec![
2490                (binding.clone(), typed_insert(&binding, 3, 30)),
2491                (mismatched.clone(), typed_insert(&binding, 4, 40)),
2492            ])
2493            .expect("mismatched typed batch should fail closed");
2494        assert!(mismatch.is_none());
2495        let stale = session
2496            .execute_trusted_typed_mutation_batch(vec![(mismatched, typed_insert(&binding, 5, 50))])
2497            .expect("stale typed batch should fail closed");
2498        assert!(stale.is_none());
2499    }
2500
2501    #[test]
2502    fn same_entity_typed_mutation_batch_rejects_empty_oversized_and_stale_input() {
2503        let session = initialize_typed_session();
2504        let binding = session
2505            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2506            .expect("typed batch binding should issue");
2507
2508        session
2509            .execute_trusted_same_entity_typed_mutation_batch(&binding, Vec::new())
2510            .expect_err("empty same-entity typed batch should reject");
2511        let insert = typed_insert(&binding, 1, 10);
2512        session
2513            .execute_trusted_same_entity_typed_mutation_batch(
2514                &binding,
2515                vec![insert.clone(); super::MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1],
2516            )
2517            .expect_err("oversized same-entity typed batch should reject");
2518
2519        let mut stale = binding;
2520        stale.accepted_revision = stale.accepted_revision.saturating_add(1);
2521        let result = session
2522            .execute_trusted_same_entity_typed_mutation_batch(&stale, vec![insert])
2523            .expect("stale same-entity typed admission should remain an adapter outcome");
2524        assert!(result.is_none());
2525    }
2526
2527    #[test]
2528    fn typed_mutation_batch_accepts_mixed_same_store_bindings_and_rejects_late_stale_input() {
2529        let session = initialize_mixed_typed_session(false);
2530        let binding = session
2531            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2532            .expect("first typed entity should bind");
2533        let other = session
2534            .issue_typed_entity_binding(&OTHER_ENTITY_DESCRIPTOR)
2535            .expect("second typed entity should bind");
2536
2537        let mut stale_other = other.clone();
2538        stale_other.accepted_revision = stale_other.accepted_revision.saturating_add(1);
2539        let stale = session
2540            .execute_trusted_typed_mutation_batch(vec![
2541                (binding.clone(), typed_insert(&binding, 1, 10)),
2542                (stale_other, typed_other_insert(&other, 1)),
2543            ])
2544            .expect("stale typed admission should remain an adapter outcome");
2545        assert!(stale.is_none());
2546        for entity in ["Entity", "OtherEntity"] {
2547            let rows = session
2548                .execute_trusted_live_page(&crate::db::DynamicQuery::new(entity), None)
2549                .expect("failed mixed admission should leave both entities readable");
2550            assert!(rows.rows.is_empty());
2551        }
2552
2553        let results = session
2554            .execute_trusted_typed_mutation_batch(vec![
2555                (other.clone(), typed_other_insert(&other, 2)),
2556                (binding.clone(), typed_insert(&binding, 3, 30)),
2557            ])
2558            .expect("same-store typed batch should execute")
2559            .expect("both typed bindings should remain current");
2560        assert_eq!(results.len(), 2);
2561        assert_eq!(results[0].entity, "OtherEntity");
2562        assert_eq!(
2563            results[0].rows,
2564            vec![vec![crate::value::OutputValue::nat64(2)]]
2565        );
2566        assert_eq!(results[1].entity, "Entity");
2567        assert_eq!(
2568            results[1].rows,
2569            vec![vec![
2570                crate::value::OutputValue::nat64(3),
2571                crate::value::OutputValue::nat64(30),
2572            ]],
2573        );
2574    }
2575
2576    #[test]
2577    fn typed_mutation_batch_rejects_cross_store_bindings_before_writes() {
2578        let session = initialize_mixed_typed_session(true);
2579        let binding = session
2580            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2581            .expect("first store typed entity should bind");
2582        let other = session
2583            .issue_typed_entity_binding(&OTHER_ENTITY_DESCRIPTOR)
2584            .expect("second store typed entity should bind");
2585
2586        let error = session
2587            .execute_trusted_typed_mutation_batch(vec![
2588                (binding.clone(), typed_insert(&binding, 1, 10)),
2589                (other.clone(), typed_other_insert(&other, 1)),
2590            ])
2591            .expect_err("typed cross-store rows must reject");
2592        assert!(matches!(
2593            error.diagnostic().detail(),
2594            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2595                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStoreMismatch,
2596            })
2597        ));
2598        for entity in ["Entity", "OtherEntity"] {
2599            let rows = session
2600                .execute_trusted_live_page(&crate::db::DynamicQuery::new(entity), None)
2601                .expect("cross-store rejection should leave both entities readable");
2602            assert!(rows.rows.is_empty());
2603        }
2604    }
2605
2606    #[test]
2607    fn typed_mutation_batch_rechecks_late_field_identity_under_current_authority() {
2608        let session = initialize_typed_session();
2609        let binding = session
2610            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2611            .expect("entity should bind");
2612
2613        // Matching entity/revision/fingerprint is insufficient: every supplied
2614        // field mapping must still agree with the accepted source binding.
2615        for (field_id, slot) in [(3, 1), (2, 2)] {
2616            let mismatched = DynamicTypedEntityBinding::new(
2617                binding.database_incarnation,
2618                binding.entity_source.clone(),
2619                binding.entity_label.clone(),
2620                binding.entity_tag,
2621                binding.accepted_revision,
2622                binding.accepted_fingerprint,
2623                binding.entity_generation,
2624                vec![
2625                    (ID_SOURCE.to_string(), 1, 0, "id".to_string()),
2626                    (
2627                        VALUE_SOURCE.to_string(),
2628                        field_id,
2629                        slot,
2630                        "value".to_string(),
2631                    ),
2632                ],
2633                binding.named_types.clone(),
2634                binding.enum_variants.clone(),
2635                binding.composite_fields.clone(),
2636            )
2637            .expect("distinct field mapping should form an opaque binding");
2638            let result = session
2639                .execute_trusted_typed_mutation_batch(vec![
2640                    (binding.clone(), typed_insert(&binding, 1, 10)),
2641                    (mismatched, typed_insert(&binding, 2, 20)),
2642                ])
2643                .expect("mismatched mapping should remain an adapter rejection");
2644            assert!(result.is_none());
2645            DATA_STORE.with(|store| assert_eq!(store.borrow().len(), 0));
2646        }
2647
2648        let result = session
2649            .execute_trusted_typed_mutation_batch(vec![
2650                (binding.clone(), typed_insert(&binding, 1, 10)),
2651                (binding.clone(), typed_insert(&binding, 2, 20)),
2652            ])
2653            .expect("corrected batch should execute after rejected borrows")
2654            .expect("current binding should remain valid");
2655        assert_eq!(result.len(), 2);
2656    }
2657
2658    #[test]
2659    fn same_entity_typed_mutation_batch_preserves_mixed_result_order() {
2660        let session = initialize_typed_session();
2661        let binding = session
2662            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2663            .expect("typed batch binding should issue");
2664        session
2665            .execute_trusted_same_entity_typed_mutation_batch(
2666                &binding,
2667                vec![
2668                    typed_insert(&binding, 1, 10),
2669                    typed_insert(&binding, 2, 20),
2670                    typed_insert(&binding, 4, 40),
2671                ],
2672            )
2673            .expect("typed fixture batch should execute")
2674            .expect("typed fixture binding should be current");
2675
2676        let result = session
2677            .execute_trusted_same_entity_typed_mutation_batch(
2678                &binding,
2679                vec![
2680                    DynamicTypedMutation::Update {
2681                        key: InputValue::nat64(1),
2682                        patch: typed_value_patch(&binding, 11),
2683                    },
2684                    DynamicTypedMutation::Replace {
2685                        key: InputValue::nat64(2),
2686                        patch: typed_value_patch(&binding, 22),
2687                    },
2688                    typed_insert(&binding, 3, 30),
2689                    typed_delete(4),
2690                ],
2691            )
2692            .expect("mixed typed batch should execute")
2693            .expect("mixed typed binding should remain current");
2694        assert_eq!(result.len(), 4);
2695        assert_eq!(result.affected_rows, 4);
2696        assert_eq!(
2697            result.rows,
2698            vec![
2699                vec![
2700                    crate::value::OutputValue::nat64(1),
2701                    crate::value::OutputValue::nat64(11),
2702                ],
2703                vec![
2704                    crate::value::OutputValue::nat64(2),
2705                    crate::value::OutputValue::nat64(22),
2706                ],
2707                vec![
2708                    crate::value::OutputValue::nat64(3),
2709                    crate::value::OutputValue::nat64(30),
2710                ],
2711                vec![
2712                    crate::value::OutputValue::nat64(4),
2713                    crate::value::OutputValue::nat64(40),
2714                ],
2715            ],
2716        );
2717    }
2718
2719    // Keep the full rename, stale-binding, and old-name-reuse lifecycle in one
2720    // regression so each issued binding is checked against the next revision.
2721    #[expect(clippy::too_many_lines)]
2722    #[test]
2723    fn typed_binding_uses_accepted_ids_and_slots_across_renames_and_name_reuse() {
2724        let entity_tag = EntityTag::new(91);
2725        let other_entity_tag = EntityTag::new(92);
2726        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2727        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2728        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2729        OTHER_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2730        OTHER_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2731        OTHER_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2732
2733        let session = DbSession::<TestCanister>::new(
2734            &STORE_REGISTRY,
2735            &crate::db::RequestExecutionRoot::__new_runtime_root(),
2736        );
2737        session
2738            .db
2739            .drive_startup_recovery_page()
2740            .expect("typed adapter test database should initialize");
2741        publish(
2742            &session,
2743            AcceptedSchemaRevision::NONE,
2744            AcceptedSchemaRevision::INITIAL,
2745            BTreeMap::from([(
2746                entity_tag,
2747                snapshot(
2748                    ENTITY_SOURCE,
2749                    "Entity",
2750                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
2751                ),
2752            )]),
2753            BTreeMap::from([
2754                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2755                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
2756            ]),
2757        );
2758
2759        let initial_catalog = session
2760            .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
2761            .expect("initial source catalog lookup should inspect")
2762            .expect("initial source catalog should exist");
2763        assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
2764        let initial = session
2765            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2766            .expect("initial typed binding should issue");
2767        assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
2768        assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
2769        assert_eq!(initial.output_field_slot("value"), Some(1));
2770        let initial_patch = initial
2771            .bind_write_ordinals(vec![(1, DynamicWriteCell::Value(InputValue::nat64(7)))])
2772            .expect("source-bound patch should lower");
2773        assert_eq!(
2774            initial_patch.fields(),
2775            &[(1, DynamicWriteCell::Value(InputValue::nat64(7)))]
2776        );
2777        assert!(
2778            initial
2779                .bind_write_ordinals(vec![(2, DynamicWriteCell::Value(InputValue::nat64(8)),)])
2780                .is_none(),
2781            "out-of-range descriptor ordinals must fail closed",
2782        );
2783        assert!(
2784            initial
2785                .bind_write_ordinals(vec![
2786                    (1, DynamicWriteCell::Omitted),
2787                    (1, DynamicWriteCell::Default),
2788                ])
2789                .is_none(),
2790            "duplicate descriptor ordinals must fail closed",
2791        );
2792        assert!(
2793            initial
2794                .bind_write_ordinals(vec![
2795                    (1, DynamicWriteCell::Omitted),
2796                    (0, DynamicWriteCell::Default),
2797                ])
2798                .is_none(),
2799            "out-of-order descriptor ordinals must fail closed",
2800        );
2801
2802        publish(
2803            &session,
2804            AcceptedSchemaRevision::INITIAL,
2805            AcceptedSchemaRevision::new(2),
2806            BTreeMap::from([
2807                (
2808                    entity_tag,
2809                    snapshot(
2810                        ENTITY_SOURCE,
2811                        "RenamedEntity",
2812                        vec![
2813                            nat64_field(1, "id", 0),
2814                            nat64_field(2, "renamed_value", 1),
2815                            nat64_field(3, "value", 2),
2816                        ],
2817                    ),
2818                ),
2819                (
2820                    other_entity_tag,
2821                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
2822                ),
2823            ]),
2824            BTreeMap::from([
2825                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2826                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
2827                (
2828                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
2829                    FieldId::new(3),
2830                ),
2831                (
2832                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
2833                    FieldId::new(1),
2834                ),
2835            ]),
2836        );
2837
2838        let stale_authority = session
2839            .ensure_accepted_schema_authority_is_current_for_store_path(
2840                STORE_PATH,
2841                initial_catalog.value_catalog_handle().authority(),
2842            )
2843            .expect_err("the initial accepted authority must be stale after revision two");
2844        assert_eq!(
2845            stale_authority.diagnostic_facts(),
2846            vec![
2847                (
2848                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedRevision,
2849                    AcceptedSchemaRevision::INITIAL.get(),
2850                ),
2851                (
2852                    icydb_diagnostic_code::DiagnosticFactTag::CurrentRevision,
2853                    AcceptedSchemaRevision::new(2).get(),
2854                ),
2855            ],
2856        );
2857
2858        assert!(
2859            !session
2860                .typed_entity_binding_is_current(&initial)
2861                .expect("renamed binding currentness should inspect")
2862        );
2863        let renamed = session
2864            .issue_typed_entity_binding(&ENTITY_DESCRIPTOR)
2865            .expect("renamed source-bound adapter should rebind");
2866        assert_eq!(renamed.entity(), "RenamedEntity");
2867        assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
2868        assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
2869        assert_eq!(renamed.output_field_slot("value"), None);
2870
2871        publish(
2872            &session,
2873            AcceptedSchemaRevision::new(2),
2874            AcceptedSchemaRevision::new(3),
2875            BTreeMap::from([
2876                (
2877                    entity_tag,
2878                    snapshot(
2879                        ENTITY_SOURCE,
2880                        "RenamedEntity",
2881                        vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
2882                    ),
2883                ),
2884                (
2885                    other_entity_tag,
2886                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
2887                ),
2888            ]),
2889            BTreeMap::from([
2890                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2891                (
2892                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
2893                    FieldId::new(2),
2894                ),
2895                (
2896                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
2897                    FieldId::new(1),
2898                ),
2899            ]),
2900        );
2901
2902        assert!(matches!(
2903            session.issue_typed_entity_binding(&ENTITY_DESCRIPTOR),
2904            Err(DynamicTypedBindingError::SourceUnavailable(context))
2905                if context.entity_source() == ENTITY_SOURCE
2906                    && context.field_source() == Some(VALUE_SOURCE),
2907        ));
2908        assert!(
2909            !session
2910                .typed_entity_binding_is_current(&renamed)
2911                .expect("removed source binding should become stale")
2912        );
2913
2914        let replacement = session
2915            .issue_typed_entity_binding(&REPLACEMENT_DESCRIPTOR)
2916            .expect("explicit replacement source should bind");
2917        assert!(
2918            session
2919                .execute_trusted_typed_mutation(
2920                    &replacement,
2921                    DynamicTypedMutation::Insert {
2922                        patch: initial_patch
2923                    },
2924                )
2925                .expect("cross-binding patch should fail closed")
2926                .is_none()
2927        );
2928        let patch = replacement
2929            .bind_write_ordinals(vec![
2930                (0, DynamicWriteCell::Value(InputValue::nat64(1))),
2931                (1, DynamicWriteCell::Value(InputValue::nat64(9))),
2932            ])
2933            .expect("replacement source write should bind by accepted IDs and slots");
2934        let result = session
2935            .execute_trusted_typed_mutation(&replacement, DynamicTypedMutation::Insert { patch })
2936            .expect("typed insert should use the accepted mutation pipeline")
2937            .expect("replacement binding should remain current");
2938        assert_eq!(result.entity, "RenamedEntity");
2939        assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
2940        assert_eq!(
2941            result.rows,
2942            vec![vec![
2943                crate::value::OutputValue::nat64(1),
2944                crate::value::OutputValue::nat64(9)
2945            ]]
2946        );
2947        assert_eq!(result.affected_rows, 1);
2948
2949        let second_patch = replacement
2950            .bind_write_ordinals(vec![
2951                (0, DynamicWriteCell::Value(InputValue::nat64(2))),
2952                (1, DynamicWriteCell::Value(InputValue::nat64(10))),
2953            ])
2954            .expect("second source-bound patch should lower");
2955        session
2956            .execute_trusted_typed_mutation(
2957                &replacement,
2958                DynamicTypedMutation::Insert {
2959                    patch: second_patch,
2960                },
2961            )
2962            .expect("second typed insert should use the accepted mutation pipeline")
2963            .expect("replacement binding should remain current");
2964
2965        {
2966            let query = crate::db::DynamicQuery::new("RenamedEntity")
2967                .select(["id", "value"])
2968                .order_by(crate::db::asc("id"))
2969                .limit(1);
2970            let result = session
2971                .execute_trusted_live_page(&query, None)
2972                .expect("SQL-free dynamic execution should use accepted authority");
2973            assert_eq!(result.entity, "RenamedEntity");
2974            assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
2975            assert_eq!(
2976                result.rows,
2977                vec![vec![
2978                    crate::value::OutputValue::nat64(1),
2979                    crate::value::OutputValue::nat64(9)
2980                ]]
2981            );
2982            assert_eq!(result.row_count, 1);
2983            assert_query_diagnostic(
2984                session
2985                    .execute_trusted_live_page(&query.cursor("00"), None)
2986                    .expect_err("scalar execution must reject grouped cursor state"),
2987                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
2988                icydb_diagnostic_code::ErrorOrigin::Query,
2989                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
2990                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
2991                },
2992            );
2993            assert_query_diagnostic(
2994                session
2995                    .execute_public_dynamic_grouped_query(
2996                        &crate::db::DynamicQuery::new("RenamedEntity").grouped_limits(1, 1024),
2997                    )
2998                    .expect_err("grouped execution must reject scalar query state"),
2999                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
3000                icydb_diagnostic_code::ErrorOrigin::Query,
3001                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
3002                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
3003                },
3004            );
3005
3006            let grouped_query = crate::db::DynamicQuery::new("RenamedEntity")
3007                .filter(crate::db::FieldRef::new("id").eq(1_u64))
3008                .group_by("value")
3009                .aggregate(crate::db::count())
3010                .grouped_limits(1, 16 * 1024)
3011                .limit(1);
3012            let grouped = session
3013                .execute_public_dynamic_grouped_query(&grouped_query)
3014                .expect("SQL-free grouped execution should use accepted authority");
3015            let typed_grouped = session
3016                .execute_public_dynamic_grouped_query_for_typed_binding(
3017                    &replacement,
3018                    &grouped_query,
3019                )
3020                .expect("typed grouped execution should inspect accepted authority")
3021                .expect("replacement binding should remain current");
3022            assert_eq!(typed_grouped, grouped);
3023            assert!(
3024                session
3025                    .execute_public_dynamic_grouped_query_for_typed_binding(
3026                        &renamed,
3027                        &grouped_query,
3028                    )
3029                    .expect("stale grouped binding should inspect accepted authority")
3030                    .is_none(),
3031                "stale typed grouped bindings must fail closed before execution"
3032            );
3033            assert_eq!(grouped.entity, "RenamedEntity");
3034            assert_eq!(grouped.row_count, 1);
3035            assert_eq!(grouped.rows.len(), 1);
3036            assert_eq!(
3037                grouped.rows[0].group_key(),
3038                &[crate::value::OutputValue::nat64(9)]
3039            );
3040            assert_eq!(
3041                grouped.rows[0].aggregate_values(),
3042                &[crate::value::OutputValue::nat64(1)]
3043            );
3044            assert_eq!(grouped.next_cursor, None);
3045
3046            let grouped_state_error = session
3047                .execute_trusted_dynamic_grouped_query(&grouped_query.clone().grouped_limits(1, 1))
3048                .expect_err("grouped retained state must respect its explicit byte ceiling");
3049            assert!(matches!(
3050                grouped_state_error.diagnostic().detail(),
3051                Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3052                    boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
3053                })
3054            ));
3055            assert_eq!(
3056                grouped_state_error.diagnostic_facts()[0],
3057                (
3058                    icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3059                    icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctStateBytes.raw(),
3060                ),
3061            );
3062
3063            assert_query_diagnostic(
3064                session
3065                    .execute_public_dynamic_grouped_query(&grouped_query.clone().select(["value"]))
3066                    .expect_err("grouped output must reject scalar selection"),
3067                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
3068                icydb_diagnostic_code::ErrorOrigin::Query,
3069                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
3070                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
3071                },
3072            );
3073            assert_query_diagnostic(
3074                session
3075                    .execute_public_dynamic_grouped_query(
3076                        &crate::db::DynamicQuery::new("RenamedEntity")
3077                            .group_by("value")
3078                            .aggregate(crate::db::count()),
3079                    )
3080                    .expect_err("public grouped execution must require explicit limits"),
3081                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
3082                icydb_diagnostic_code::ErrorOrigin::Query,
3083                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
3084                    reason:
3085                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
3086                },
3087            );
3088            assert_query_diagnostic(
3089                session
3090                    .execute_trusted_dynamic_grouped_query(
3091                        &crate::db::DynamicQuery::new("RenamedEntity")
3092                            .group_by("value")
3093                            .aggregate(crate::db::count())
3094                            .grouped_limits(0, 1024),
3095                    )
3096                    .expect_err("trusted grouped execution must reject zero limits"),
3097                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
3098                icydb_diagnostic_code::ErrorOrigin::Query,
3099                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
3100                    reason:
3101                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
3102                },
3103            );
3104            assert_query_diagnostic(
3105                session
3106                    .execute_public_dynamic_grouped_query(&grouped_query.grouped_limits(101, 1024))
3107                    .expect_err("public grouped execution must enforce its group budget"),
3108                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
3109                icydb_diagnostic_code::ErrorOrigin::Query,
3110                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
3111                    reason:
3112                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryExceedsBudget,
3113                },
3114            );
3115
3116            let paged_query = crate::db::DynamicQuery::new("RenamedEntity")
3117                .group_by("value")
3118                .aggregate(crate::db::count())
3119                .grouped_limits(2, 16 * 1024)
3120                .limit(1);
3121            assert_query_diagnostic(
3122                session
3123                    .execute_public_dynamic_grouped_query(&paged_query)
3124                    .expect_err("public grouped execution must reject an unbounded full scan"),
3125                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
3126                icydb_diagnostic_code::ErrorOrigin::Query,
3127                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
3128                    reason:
3129                        icydb_diagnostic_code::QueryReadAdmissionCode::UnboundedFullScanRejected,
3130                },
3131            );
3132            let first_page = session
3133                .execute_trusted_dynamic_grouped_query(&paged_query)
3134                .expect("SQL-free grouped first page should execute");
3135            assert_eq!(first_page.row_count, 1);
3136            assert_eq!(
3137                first_page.rows[0].group_key(),
3138                &[crate::value::OutputValue::nat64(9)]
3139            );
3140            let cursor = first_page
3141                .next_cursor
3142                .expect("first grouped page should return a continuation cursor");
3143            assert_query_diagnostic(
3144                session
3145                    .execute_trusted_dynamic_grouped_query(
3146                        &paged_query.clone().cursor(format!("{cursor}0")),
3147                    )
3148                    .expect_err("tampered grouped cursor must fail closed"),
3149                icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
3150                icydb_diagnostic_code::ErrorOrigin::Cursor,
3151                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
3152                    kind: icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor,
3153                },
3154            );
3155            let second_page = session
3156                .execute_trusted_dynamic_grouped_query(&paged_query.cursor(cursor))
3157                .expect("SQL-free grouped continuation should execute");
3158            assert_eq!(second_page.row_count, 1);
3159            assert_eq!(
3160                second_page.rows[0].group_key(),
3161                &[crate::value::OutputValue::nat64(10)]
3162            );
3163            assert_eq!(second_page.next_cursor, None);
3164        }
3165    }
3166}
3167
3168#[cfg(test)]
3169mod mixed_relation_batch_tests {
3170    use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
3171    use crate::{
3172        db::{
3173            DynamicQuery, asc,
3174            data::DataStore,
3175            desc,
3176            index::IndexStore,
3177            query::expr::FilterExpr,
3178            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
3179            schema::{
3180                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
3181                FieldStorageDecode, FieldWriteManagement, LeafCodec, PersistedFieldSnapshot,
3182                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
3183                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
3184                SchemaFieldSlot, SchemaFieldWritePolicy, SchemaIndexId, SchemaInsertDefault,
3185                SchemaRowLayout, SchemaStore, SchemaVersion,
3186                accepted_schema_candidate_with_field_bindings_for_tests,
3187            },
3188        },
3189        error::{ErrorClass, ErrorOrigin},
3190        traits::{CanisterKind, Path},
3191        types::EntityTag,
3192        value::{InputValue, OutputValue},
3193    };
3194    use icydb_schema::FieldSourceKey;
3195    use std::{cell::RefCell, collections::BTreeMap};
3196
3197    const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
3198    const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
3199    const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
3200    const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
3201    const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
3202    const ENTITY_NAME: &str = "MixedRelationNode";
3203    const ENTITY_TAG: EntityTag = EntityTag::new(94);
3204    const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
3205    const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
3206    const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
3207    const OTHER_NODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::node_id";
3208    const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
3209    const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
3210    const CROSS_STORE_PATH: &str = "session::write::mixed_relation_batch_tests::OtherStore";
3211    const CROSS_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::CrossStore";
3212    const CROSS_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::CrossStore::id";
3213    const CROSS_ENTITY_NAME: &str = "MixedCrossStore";
3214    const CROSS_ENTITY_TAG: EntityTag = EntityTag::new(2_000);
3215    fn batch_rows(results: &[crate::db::DynamicMutationResult]) -> Vec<Vec<OutputValue>> {
3216        results
3217            .iter()
3218            .flat_map(|result| result.rows.iter().cloned())
3219            .collect()
3220    }
3221
3222    struct TestCanister;
3223
3224    impl Path for TestCanister {
3225        const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
3226    }
3227
3228    impl CanisterKind for TestCanister {
3229        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
3230            Ok(47)
3231        }
3232        const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
3233        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
3234            Ok(50)
3235        }
3236        const STARTUP_STABLE_KEY: &'static str =
3237            "icydb.mixed_relation_batch_tests.startup.control.v1";
3238        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
3239            Ok(48)
3240        }
3241        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3242            "icydb.mixed_relation_batch_tests.integrity.progress.v1";
3243    }
3244
3245    thread_local! {
3246        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3247        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3248        static SCHEMA_STORE: RefCell<SchemaStore> =
3249            const { RefCell::new(SchemaStore::init_heap()) };
3250        static CROSS_DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3251        static CROSS_INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3252        static CROSS_SCHEMA_STORE: RefCell<SchemaStore> =
3253            const { RefCell::new(SchemaStore::init_heap()) };
3254        static STORE_REGISTRY: StoreRegistry = {
3255            let mut registry = StoreRegistry::new();
3256            registry.register_store(
3257                STORE_PATH,
3258                &DATA_STORE,
3259                &INDEX_STORE,
3260                &SCHEMA_STORE,
3261                StoreAllocationIdentities::absent(),
3262                StoreRuntimeStorageCapabilities::heap(),
3263            ).expect("mixed relation test store should register");
3264            registry.register_store(
3265                CROSS_STORE_PATH,
3266                &CROSS_DATA_STORE,
3267                &CROSS_INDEX_STORE,
3268                &CROSS_SCHEMA_STORE,
3269                StoreAllocationIdentities::absent(),
3270                StoreRuntimeStorageCapabilities::heap(),
3271            ).expect("cross-store test store should register");
3272            registry
3273        };
3274    }
3275
3276    fn source_key(source: &str) -> FieldSourceKey {
3277        FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
3278    }
3279
3280    fn relation_snapshot() -> PersistedSchemaSnapshot {
3281        let fields = vec![
3282            PersistedFieldSnapshot::new_initial(
3283                FieldId::new(1),
3284                "id".to_string(),
3285                SchemaFieldSlot::new(0),
3286                AcceptedFieldKind::Nat64,
3287                Vec::new(),
3288                false,
3289                SchemaInsertDefault::None,
3290                FieldStorageDecode::ByKind,
3291                LeafCodec::Scalar(ScalarCodec::Nat64),
3292            ),
3293            PersistedFieldSnapshot::new_initial(
3294                FieldId::new(2),
3295                "parent_id".to_string(),
3296                SchemaFieldSlot::new(1),
3297                AcceptedFieldKind::Nat64,
3298                Vec::new(),
3299                true,
3300                SchemaInsertDefault::None,
3301                FieldStorageDecode::ByKind,
3302                LeafCodec::Scalar(ScalarCodec::Nat64),
3303            ),
3304            PersistedFieldSnapshot::new_initial(
3305                FieldId::new(3),
3306                "code".to_string(),
3307                SchemaFieldSlot::new(2),
3308                AcceptedFieldKind::Nat64,
3309                Vec::new(),
3310                false,
3311                SchemaInsertDefault::None,
3312                FieldStorageDecode::ByKind,
3313                LeafCodec::Scalar(ScalarCodec::Nat64),
3314            ),
3315        ];
3316        let relation = PersistedRelationEdgeSnapshot::new_direct(
3317            RelationId::new(1).expect("mixed relation identity should be non-zero"),
3318            "parent".to_string(),
3319            ENTITY_SOURCE.to_string(),
3320            vec![FieldId::new(2)],
3321        );
3322        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
3323            SchemaVersion::initial(),
3324            ENTITY_SOURCE.to_string(),
3325            ENTITY_NAME.to_string(),
3326            FieldId::new(1),
3327            SchemaRowLayout::initial(
3328                fields
3329                    .iter()
3330                    .map(|field| (field.id(), field.slot()))
3331                    .collect(),
3332            ),
3333            fields,
3334            vec![PersistedIndexSnapshot::new(
3335                SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
3336                1,
3337                "by_code".to_string(),
3338                STORE_PATH.to_string(),
3339                true,
3340                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
3341                    FieldId::new(3),
3342                    SchemaFieldSlot::new(2),
3343                    vec!["code".to_string()],
3344                    AcceptedFieldKind::Nat64,
3345                    false,
3346                )]),
3347                None,
3348            )],
3349        )
3350        .with_relations(vec![relation]);
3351        let constraints = AcceptedConstraintCatalog::initial(
3352            snapshot.fields(),
3353            snapshot.indexes(),
3354            snapshot.relations(),
3355        )
3356        .expect("mixed relation constraints should close");
3357        snapshot.with_constraint_catalog(constraints)
3358    }
3359
3360    fn other_snapshot() -> PersistedSchemaSnapshot {
3361        let fields = vec![
3362            PersistedFieldSnapshot::new_initial(
3363                FieldId::new(1),
3364                "id".to_string(),
3365                SchemaFieldSlot::new(0),
3366                AcceptedFieldKind::Nat64,
3367                Vec::new(),
3368                false,
3369                SchemaInsertDefault::None,
3370                FieldStorageDecode::ByKind,
3371                LeafCodec::Scalar(ScalarCodec::Nat64),
3372            ),
3373            PersistedFieldSnapshot::new_initial(
3374                FieldId::new(2),
3375                "value".to_string(),
3376                SchemaFieldSlot::new(1),
3377                AcceptedFieldKind::Nat64,
3378                Vec::new(),
3379                false,
3380                SchemaInsertDefault::None,
3381                FieldStorageDecode::ByKind,
3382                LeafCodec::Scalar(ScalarCodec::Nat64),
3383            ),
3384            PersistedFieldSnapshot::new_initial(
3385                FieldId::new(3),
3386                "node_id".to_string(),
3387                SchemaFieldSlot::new(2),
3388                AcceptedFieldKind::Nat64,
3389                Vec::new(),
3390                true,
3391                SchemaInsertDefault::None,
3392                FieldStorageDecode::ByKind,
3393                LeafCodec::Scalar(ScalarCodec::Nat64),
3394            ),
3395        ];
3396        let relation = PersistedRelationEdgeSnapshot::new_direct(
3397            RelationId::new(1).expect("cross-entity relation identity should be non-zero"),
3398            "node".to_string(),
3399            ENTITY_SOURCE.to_string(),
3400            vec![FieldId::new(3)],
3401        );
3402        let snapshot = PersistedSchemaSnapshot::new(
3403            SchemaVersion::initial(),
3404            OTHER_ENTITY_SOURCE.to_string(),
3405            OTHER_ENTITY_NAME.to_string(),
3406            FieldId::new(1),
3407            SchemaRowLayout::initial(
3408                fields
3409                    .iter()
3410                    .map(|field| (field.id(), field.slot()))
3411                    .collect(),
3412            ),
3413            fields,
3414        )
3415        .with_relations(vec![relation]);
3416        let constraints = AcceptedConstraintCatalog::initial(
3417            snapshot.fields(),
3418            snapshot.indexes(),
3419            snapshot.relations(),
3420        )
3421        .expect("cross-entity relation constraints should close");
3422        snapshot.with_constraint_catalog(constraints)
3423    }
3424
3425    fn bounded_entity_snapshot(index: usize) -> PersistedSchemaSnapshot {
3426        let fields = vec![
3427            PersistedFieldSnapshot::new_initial(
3428                FieldId::new(1),
3429                "id".to_string(),
3430                SchemaFieldSlot::new(0),
3431                AcceptedFieldKind::Nat64,
3432                Vec::new(),
3433                false,
3434                SchemaInsertDefault::None,
3435                FieldStorageDecode::ByKind,
3436                LeafCodec::Scalar(ScalarCodec::Nat64),
3437            ),
3438            PersistedFieldSnapshot::new_initial_with_write_policy(
3439                FieldId::new(2),
3440                "updated_at".to_string(),
3441                SchemaFieldSlot::new(1),
3442                AcceptedFieldKind::Timestamp,
3443                Vec::new(),
3444                false,
3445                SchemaInsertDefault::None,
3446                SchemaFieldWritePolicy::from_model_policies(
3447                    None,
3448                    Some(FieldWriteManagement::UpdatedAt),
3449                ),
3450                FieldStorageDecode::ByKind,
3451                LeafCodec::Scalar(ScalarCodec::Timestamp),
3452            ),
3453        ];
3454        PersistedSchemaSnapshot::new(
3455            SchemaVersion::initial(),
3456            format!("session::write::mixed_relation_batch_tests::Bounded{index}"),
3457            format!("MixedBounded{index}"),
3458            FieldId::new(1),
3459            SchemaRowLayout::initial(
3460                fields
3461                    .iter()
3462                    .map(|field| (field.id(), field.slot()))
3463                    .collect(),
3464            ),
3465            fields,
3466        )
3467    }
3468
3469    fn cross_store_snapshot() -> PersistedSchemaSnapshot {
3470        let field = PersistedFieldSnapshot::new_initial(
3471            FieldId::new(1),
3472            "id".to_string(),
3473            SchemaFieldSlot::new(0),
3474            AcceptedFieldKind::Nat64,
3475            Vec::new(),
3476            false,
3477            SchemaInsertDefault::None,
3478            FieldStorageDecode::ByKind,
3479            LeafCodec::Scalar(ScalarCodec::Nat64),
3480        );
3481        PersistedSchemaSnapshot::new(
3482            SchemaVersion::initial(),
3483            CROSS_ENTITY_SOURCE.to_string(),
3484            CROSS_ENTITY_NAME.to_string(),
3485            FieldId::new(1),
3486            SchemaRowLayout::initial(vec![(field.id(), field.slot())]),
3487            vec![field],
3488        )
3489    }
3490
3491    fn initialize() -> DbSession<TestCanister> {
3492        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3493        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3494        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3495        CROSS_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3496        CROSS_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3497        CROSS_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3498        let session = DbSession::<TestCanister>::new(
3499            &STORE_REGISTRY,
3500            &crate::db::RequestExecutionRoot::__new_runtime_root(),
3501        );
3502        session
3503            .db
3504            .drive_startup_recovery_page()
3505            .expect("mixed relation database should initialize");
3506        let mut snapshots = BTreeMap::from([
3507            (ENTITY_TAG, relation_snapshot()),
3508            (OTHER_ENTITY_TAG, other_snapshot()),
3509        ]);
3510        let mut field_bindings = BTreeMap::from([
3511            ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
3512            ((ENTITY_TAG, source_key(PARENT_SOURCE)), FieldId::new(2)),
3513            ((ENTITY_TAG, source_key(CODE_SOURCE)), FieldId::new(3)),
3514            (
3515                (OTHER_ENTITY_TAG, source_key(OTHER_ID_SOURCE)),
3516                FieldId::new(1),
3517            ),
3518            (
3519                (OTHER_ENTITY_TAG, source_key(OTHER_VALUE_SOURCE)),
3520                FieldId::new(2),
3521            ),
3522            (
3523                (OTHER_ENTITY_TAG, source_key(OTHER_NODE_SOURCE)),
3524                FieldId::new(3),
3525            ),
3526        ]);
3527        for index in 0..65 {
3528            let tag = EntityTag::new(1_000 + index as u64);
3529            snapshots.insert(tag, bounded_entity_snapshot(index));
3530            field_bindings.insert(
3531                (
3532                    tag,
3533                    source_key(
3534                        format!("session::write::mixed_relation_batch_tests::Bounded{index}::id")
3535                            .as_str(),
3536                    ),
3537                ),
3538                FieldId::new(1),
3539            );
3540            field_bindings.insert(
3541                (
3542                    tag,
3543                    source_key(
3544                        format!(
3545                            "session::write::mixed_relation_batch_tests::Bounded{index}::updated_at"
3546                        )
3547                        .as_str(),
3548                    ),
3549                ),
3550                FieldId::new(2),
3551            );
3552        }
3553        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
3554            STORE_PATH,
3555            AcceptedSchemaRevision::INITIAL,
3556            snapshots,
3557            field_bindings,
3558        );
3559        let store = session
3560            .db
3561            .store_handle(STORE_PATH)
3562            .expect("mixed relation store should resolve");
3563        crate::db::commit::publish_accepted_schema_candidate(
3564            STORE_PATH,
3565            store,
3566            AcceptedSchemaRevision::NONE,
3567            &candidate,
3568        )
3569        .expect("mixed relation candidate should publish");
3570        let cross_candidate = accepted_schema_candidate_with_field_bindings_for_tests(
3571            CROSS_STORE_PATH,
3572            AcceptedSchemaRevision::INITIAL,
3573            BTreeMap::from([(CROSS_ENTITY_TAG, cross_store_snapshot())]),
3574            BTreeMap::from([(
3575                (CROSS_ENTITY_TAG, source_key(CROSS_ID_SOURCE)),
3576                FieldId::new(1),
3577            )]),
3578        );
3579        let cross_store = session
3580            .db
3581            .store_handle(CROSS_STORE_PATH)
3582            .expect("cross-store fixture should resolve");
3583        crate::db::commit::publish_accepted_schema_candidate(
3584            CROSS_STORE_PATH,
3585            cross_store,
3586            AcceptedSchemaRevision::NONE,
3587            &cross_candidate,
3588        )
3589        .expect("cross-store candidate should publish");
3590        session
3591    }
3592
3593    fn patch(id: Option<u64>, parent: Option<u64>, code: Option<u64>) -> DynamicStructuralPatch {
3594        let mut fields = Vec::new();
3595        if let Some(id) = id {
3596            fields.push((
3597                "id".to_string(),
3598                DynamicWriteCell::Value(InputValue::nat64(id)),
3599            ));
3600        }
3601        fields.push((
3602            "parent_id".to_string(),
3603            parent.map_or(DynamicWriteCell::Null, |parent| {
3604                DynamicWriteCell::Value(InputValue::nat64(parent))
3605            }),
3606        ));
3607        if let Some(code) = code {
3608            fields.push((
3609                "code".to_string(),
3610                DynamicWriteCell::Value(InputValue::nat64(code)),
3611            ));
3612        }
3613        DynamicStructuralPatch::new(fields)
3614    }
3615
3616    fn insert(id: u64, parent: Option<u64>) -> DynamicMutation {
3617        insert_with_code(id, parent, id)
3618    }
3619
3620    fn insert_with_code(id: u64, parent: Option<u64>, code: u64) -> DynamicMutation {
3621        DynamicMutation::Insert {
3622            entity: ENTITY_NAME.to_string(),
3623            patch: patch(Some(id), parent, Some(code)),
3624        }
3625    }
3626
3627    fn update_parent(id: u64, parent: Option<u64>) -> DynamicMutation {
3628        DynamicMutation::Update {
3629            entity: ENTITY_NAME.to_string(),
3630            key: InputValue::nat64(id),
3631            patch: patch(None, parent, None),
3632        }
3633    }
3634
3635    fn update_code(id: u64, code: u64) -> DynamicMutation {
3636        DynamicMutation::Update {
3637            entity: ENTITY_NAME.to_string(),
3638            key: InputValue::nat64(id),
3639            patch: DynamicStructuralPatch::new(vec![(
3640                "code".to_string(),
3641                DynamicWriteCell::Value(InputValue::nat64(code)),
3642            )]),
3643        }
3644    }
3645
3646    fn delete(id: u64) -> DynamicMutation {
3647        DynamicMutation::Delete {
3648            entity: ENTITY_NAME.to_string(),
3649            key: InputValue::nat64(id),
3650        }
3651    }
3652
3653    fn expected_row(id: u64, parent: Option<u64>) -> Vec<OutputValue> {
3654        expected_row_with_code(id, parent, id)
3655    }
3656
3657    fn expected_row_with_code(id: u64, parent: Option<u64>, code: u64) -> Vec<OutputValue> {
3658        vec![
3659            OutputValue::nat64(id),
3660            parent.map_or_else(OutputValue::null, OutputValue::nat64),
3661            OutputValue::nat64(code),
3662        ]
3663    }
3664
3665    fn other_patch(id: Option<u64>, value: u64) -> DynamicStructuralPatch {
3666        other_patch_with_node(id, value, None)
3667    }
3668
3669    fn other_patch_with_node(
3670        id: Option<u64>,
3671        value: u64,
3672        node_id: Option<u64>,
3673    ) -> DynamicStructuralPatch {
3674        let mut fields = Vec::new();
3675        if let Some(id) = id {
3676            fields.push((
3677                "id".to_string(),
3678                DynamicWriteCell::Value(InputValue::nat64(id)),
3679            ));
3680        }
3681        fields.push((
3682            "value".to_string(),
3683            DynamicWriteCell::Value(InputValue::nat64(value)),
3684        ));
3685        fields.push((
3686            "node_id".to_string(),
3687            node_id.map_or(DynamicWriteCell::Null, |node_id| {
3688                DynamicWriteCell::Value(InputValue::nat64(node_id))
3689            }),
3690        ));
3691        DynamicStructuralPatch::new(fields)
3692    }
3693
3694    fn assert_relation_violation(error: &crate::error::InternalError) {
3695        assert!(error.diagnostic_facts().contains(&(
3696            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
3697            icydb_diagnostic_code::DiagnosticConstraintKind::Relation.raw(),
3698        )));
3699    }
3700
3701    #[test]
3702    fn live_pages_resume_mixed_projection_from_authenticated_hidden_order_values() {
3703        let session = initialize();
3704        session
3705            .execute_trusted_dynamic_mutation_batch(vec![
3706                insert_with_code(1, None, 10),
3707                insert_with_code(2, Some(1), 20),
3708                insert_with_code(3, None, 30),
3709            ])
3710            .expect("live-page rows should insert");
3711        let query = DynamicQuery::new(ENTITY_NAME)
3712            .select(["id"])
3713            .order_by(desc("code"));
3714
3715        let first = session
3716            .execute_public_live_page(&query, None)
3717            .expect("initial live page should execute");
3718        assert_eq!(
3719            first.rows,
3720            vec![vec![OutputValue::nat64(3)], vec![OutputValue::nat64(2)]]
3721        );
3722        let cursor = first
3723            .continuation
3724            .as_deref()
3725            .expect("unreturned matching row should produce continuation");
3726        let second = session
3727            .execute_public_live_page(&query, Some(cursor))
3728            .expect("authenticated live continuation should resume");
3729        assert_eq!(second.rows, vec![vec![OutputValue::nat64(1)]]);
3730        assert_eq!(second.continuation, None);
3731
3732        let total_limit = session
3733            .execute_public_live_page(&query.clone().limit(2), None)
3734            .expect("total live-page limit should execute");
3735        assert_eq!(
3736            total_limit.rows,
3737            vec![vec![OutputValue::nat64(3)], vec![OutputValue::nat64(2)]],
3738        );
3739        assert_eq!(
3740            total_limit.continuation, None,
3741            "query LIMIT is a total traversal window rather than a page size",
3742        );
3743
3744        let three_row_window = query.clone().limit(3);
3745        let limited_first = session
3746            .execute_public_live_page(&three_row_window, None)
3747            .expect("first total-window page should execute");
3748        let limited_cursor = limited_first
3749            .continuation
3750            .as_deref()
3751            .expect("a partially consumed total window should continue");
3752        let limited_second = session
3753            .execute_public_live_page(&three_row_window, Some(limited_cursor))
3754            .expect("remaining total window should preserve the plan signature");
3755        assert_eq!(limited_second.rows, vec![vec![OutputValue::nat64(1)]]);
3756        assert_eq!(limited_second.continuation, None);
3757
3758        let mixed_order = DynamicQuery::new(ENTITY_NAME)
3759            .select(["id"])
3760            .order_by(desc("parent_id"))
3761            .order_by(asc("id"));
3762        let mixed_first = session
3763            .execute_trusted_live_page(&mixed_order, None)
3764            .expect("mixed-direction nullable order should execute");
3765        assert_eq!(
3766            mixed_first.rows,
3767            vec![vec![OutputValue::nat64(2)], vec![OutputValue::nat64(1)]],
3768        );
3769        let mixed_cursor = mixed_first
3770            .continuation
3771            .as_deref()
3772            .expect("duplicate null order values should retain continuation");
3773        let mixed_second = session
3774            .execute_trusted_live_page(&mixed_order, Some(mixed_cursor))
3775            .expect("mixed-direction nullable order should resume");
3776        assert_eq!(mixed_second.rows, vec![vec![OutputValue::nat64(3)]]);
3777        assert_eq!(mixed_second.continuation, None);
3778
3779        let mismatched_window = session
3780            .execute_public_live_page(&query.clone().limit(3), Some(cursor))
3781            .expect_err("a changed total limit must invalidate the continuation");
3782        assert_eq!(
3783            mismatched_window.diagnostic_code(),
3784            icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
3785        );
3786
3787        let mut tampered = cursor.as_bytes().to_vec();
3788        let last = tampered.len().saturating_sub(1);
3789        tampered[last] = if tampered[last] == b'0' { b'1' } else { b'0' };
3790        let tampered = String::from_utf8(tampered).expect("Base64 cursor should remain UTF-8");
3791        let error = session
3792            .execute_public_live_page(&query, Some(tampered.as_str()))
3793            .expect_err("tampered cursor must fail closed");
3794        assert_eq!(
3795            error.diagnostic_code(),
3796            icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
3797        );
3798    }
3799
3800    #[test]
3801    fn live_pages_resume_across_changed_output_work_envelopes() {
3802        let session = initialize();
3803        session
3804            .execute_trusted_dynamic_mutation_batch(vec![
3805                insert(1, None),
3806                insert(2, None),
3807                insert(3, None),
3808            ])
3809            .expect("output-envelope rows should insert");
3810        let query = DynamicQuery::new(ENTITY_NAME)
3811            .select(["id"])
3812            .order_by(desc("code"));
3813        let first = session
3814            .execute_trusted_live_page_with_result_bytes_limit_for_tests(&query, None, 32)
3815            .expect("small output envelope should publish the first bounded page");
3816        assert_eq!(first.rows, vec![vec![OutputValue::nat64(3)]]);
3817        let continuation = first
3818            .continuation
3819            .expect("small output envelope should leave authenticated progress");
3820
3821        let second = session
3822            .execute_trusted_live_page_with_result_bytes_limit_for_tests(
3823                &query,
3824                Some(continuation.as_str()),
3825                64,
3826            )
3827            .unwrap_or_else(|error| {
3828                panic!(
3829                    "larger output envelope should resume the same query: {error:?}, facts={:?}",
3830                    error.diagnostic_facts(),
3831                )
3832            });
3833        assert_eq!(
3834            second.rows,
3835            vec![vec![OutputValue::nat64(2)], vec![OutputValue::nat64(1)]]
3836        );
3837        let second_continuation = second
3838            .continuation
3839            .as_deref()
3840            .expect("an exact-full page still needs to prove physical exhaustion");
3841        assert_ne!(first.work.envelope_identity, second.work.envelope_identity);
3842
3843        let terminal = session
3844            .execute_trusted_live_page_with_result_bytes_limit_for_tests(
3845                &query,
3846                Some(second_continuation),
3847                48,
3848            )
3849            .expect("a third finite envelope should prove exhaustion without replaying rows");
3850        assert!(terminal.rows.is_empty());
3851        assert_eq!(terminal.continuation, None);
3852        assert_ne!(
3853            second.work.envelope_identity,
3854            terminal.work.envelope_identity
3855        );
3856
3857        assert_eq!(
3858            [first.rows, second.rows, terminal.rows].concat(),
3859            vec![
3860                vec![OutputValue::nat64(3)],
3861                vec![OutputValue::nat64(2)],
3862                vec![OutputValue::nat64(1)],
3863            ]
3864        );
3865    }
3866
3867    #[test]
3868    fn distinct_live_pages_resume_adjacent_groups_and_global_replay_end_to_end() {
3869        let session = initialize();
3870        session
3871            .execute_trusted_dynamic_mutation_batch(vec![
3872                insert(1, None),
3873                insert(2, None),
3874                insert(3, Some(1)),
3875                insert(4, Some(2)),
3876                insert(5, Some(1)),
3877                insert(6, Some(3)),
3878                insert(7, Some(2)),
3879            ])
3880            .expect("DISTINCT continuation rows should insert atomically");
3881
3882        let adjacent = DynamicQuery::new(ENTITY_NAME)
3883            .select(["parent_id"])
3884            .order_by(asc("parent_id"))
3885            .order_by(asc("id"))
3886            .distinct_for_internal_execution();
3887        let global = DynamicQuery::new(ENTITY_NAME)
3888            .select(["parent_id"])
3889            .order_by(asc("id"))
3890            .distinct_for_internal_execution();
3891
3892        let traverse = |query: &DynamicQuery, strategy: &str| {
3893            let mut continuation = None;
3894            let mut rows = Vec::new();
3895            let mut cursors = std::collections::BTreeSet::new();
3896            let mut pages = 0_u32;
3897            let mut entries_visited = 0_u64;
3898            loop {
3899                let page = session
3900                    .execute_trusted_live_page(query, continuation.as_deref())
3901                    .unwrap_or_else(|error| {
3902                        panic!("{strategy} DISTINCT page should execute: {error:?}")
3903                    });
3904                pages = pages.saturating_add(1);
3905                entries_visited = entries_visited.saturating_add(page.work.entries_visited);
3906                assert_eq!(page.row_count as usize, page.rows.len());
3907                assert_eq!(page.work.result_rows, page.row_count);
3908                rows.extend(page.rows);
3909                let Some(cursor) = page.continuation else {
3910                    break;
3911                };
3912                assert!(
3913                    cursors.insert(cursor.clone()),
3914                    "{strategy} DISTINCT continuation must advance monotonically",
3915                );
3916                continuation = Some(cursor);
3917                assert!(pages < 8, "{strategy} DISTINCT traversal must terminate");
3918            }
3919
3920            (rows, pages, entries_visited)
3921        };
3922
3923        let expected = vec![
3924            vec![OutputValue::null()],
3925            vec![OutputValue::nat64(1)],
3926            vec![OutputValue::nat64(2)],
3927            vec![OutputValue::nat64(3)],
3928        ];
3929        let (adjacent_rows, adjacent_pages, adjacent_entries) = traverse(&adjacent, "adjacent");
3930        let (global_rows, global_pages, global_entries) = traverse(&global, "global");
3931
3932        assert_eq!(adjacent_rows, expected);
3933        assert_eq!(global_rows, expected);
3934        assert_eq!(adjacent_pages, 2);
3935        assert_eq!(global_pages, 2);
3936        assert!(adjacent_entries > 0);
3937        assert!(global_entries > 0);
3938    }
3939
3940    #[test]
3941    fn selective_live_pages_publish_monotonic_empty_physical_progress() {
3942        let session = initialize();
3943        session
3944            .execute_trusted_dynamic_mutation_batch(
3945                (1..=9)
3946                    .map(|id| {
3947                        let parent = match id {
3948                            1 => Some(2),
3949                            9 => Some(1),
3950                            _ => None,
3951                        };
3952                        insert(id, parent)
3953                    })
3954                    .collect(),
3955            )
3956            .expect("selective live-page rows should insert");
3957        let query = DynamicQuery::new(ENTITY_NAME)
3958            .select(["id"])
3959            .filter(FilterExpr::eq("parent_id", 1_u64))
3960            .order_by(asc("id"))
3961            .limit(1);
3962
3963        let first = session
3964            .execute_trusted_live_page(&query, None)
3965            .expect("first selective page should stop with physical progress");
3966        assert!(first.rows.is_empty());
3967        assert_eq!(first.work.entries_visited, 4);
3968        let first_cursor = first
3969            .continuation
3970            .expect("filtered physical progress must return a continuation");
3971
3972        let second = session
3973            .execute_trusted_live_page(&query, Some(first_cursor.as_str()))
3974            .expect("second selective page should resume after the first physical frontier");
3975        assert!(second.rows.is_empty());
3976        assert_eq!(second.work.entries_visited, 4);
3977        let second_cursor = second
3978            .continuation
3979            .expect("second filtered frontier must remain resumable");
3980        assert_ne!(second_cursor, first_cursor);
3981
3982        let third = session
3983            .execute_trusted_live_page(&query, Some(second_cursor.as_str()))
3984            .expect("final selective page should return the late match");
3985        assert_eq!(third.rows, vec![vec![OutputValue::nat64(9)]]);
3986        assert_eq!(third.work.entries_visited, 1);
3987        assert_eq!(third.continuation, None);
3988
3989        let descending = DynamicQuery::new(ENTITY_NAME)
3990            .select(["id"])
3991            .filter(FilterExpr::eq("parent_id", 2_u64))
3992            .order_by(desc("id"))
3993            .limit(1);
3994        let descending_first = session
3995            .execute_trusted_live_page(&descending, None)
3996            .expect("descending selective page should stop with physical progress");
3997        assert!(descending_first.rows.is_empty());
3998        let descending_first_cursor = descending_first
3999            .continuation
4000            .expect("descending filtered progress must return a continuation");
4001        let descending_second = session
4002            .execute_trusted_live_page(&descending, Some(descending_first_cursor.as_str()))
4003            .expect("descending progress should resume after its physical frontier");
4004        assert!(descending_second.rows.is_empty());
4005        let descending_second_cursor = descending_second
4006            .continuation
4007            .expect("descending second frontier must remain resumable");
4008        assert_ne!(descending_second_cursor, descending_first_cursor);
4009        let descending_third = session
4010            .execute_trusted_live_page(&descending, Some(descending_second_cursor.as_str()))
4011            .expect("descending final page should return the late match");
4012        assert_eq!(descending_third.rows, vec![vec![OutputValue::nat64(1)]]);
4013        assert_eq!(descending_third.continuation, None);
4014    }
4015
4016    #[test]
4017    fn accepted_relation_edges_drive_catalog_and_describe_introspection() {
4018        let session = initialize();
4019        let entities = session
4020            .show_entities()
4021            .expect("accepted entity catalog should resolve");
4022        let source = entities
4023            .iter()
4024            .find(|entity| entity.entity_name() == ENTITY_NAME)
4025            .expect("relation source should be listed");
4026        assert_eq!(source.relations(), 1);
4027
4028        let description = session
4029            .try_describe_entity_by_name(ENTITY_NAME)
4030            .expect("accepted relation source should describe");
4031        let [relation] = description.relations() else {
4032            panic!("accepted relation edge should produce one relation row");
4033        };
4034        assert_eq!(relation.field(), "parent_id");
4035        assert_eq!(relation.target_path(), ENTITY_SOURCE);
4036        assert_eq!(relation.target_entity_name(), ENTITY_NAME);
4037        assert_eq!(relation.target_store_path(), STORE_PATH);
4038        assert_eq!(
4039            relation.cardinality(),
4040            crate::db::EntityRelationCardinality::Single,
4041        );
4042    }
4043
4044    #[test]
4045    fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
4046        let session = initialize();
4047        session
4048            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
4049            .expect("the initial relation should commit");
4050
4051        let blocked = session
4052            .execute_trusted_dynamic_mutation(&delete(1))
4053            .expect_err("an unaffected committed source must block target deletion");
4054        assert_relation_violation(&blocked);
4055
4056        let deleted = session
4057            .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
4058            .expect("a source and its target should delete atomically");
4059        assert_eq!(
4060            batch_rows(&deleted),
4061            vec![expected_row(2, Some(1)), expected_row(1, None)],
4062        );
4063
4064        session
4065            .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
4066            .expect("the update-away fixture should commit");
4067        let updated_away = session
4068            .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
4069            .expect("an updated final source may release a deleted target");
4070        assert_eq!(
4071            batch_rows(&updated_away),
4072            vec![expected_row(4, None), expected_row(3, None)],
4073        );
4074
4075        session
4076            .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
4077            .expect("the retained-reference fixture should commit");
4078        let retained = session
4079            .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
4080            .expect_err("a final updated source must still block target deletion");
4081        assert_relation_violation(&retained);
4082
4083        session
4084            .execute_trusted_dynamic_mutation(&insert(7, None))
4085            .expect("the inserted-reference fixture target should commit");
4086        let inserted_reference = session
4087            .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
4088            .expect_err("a final inserted source must not reference a deleted target");
4089        assert_relation_violation(&inserted_reference);
4090
4091        let inserted_target = session
4092            .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
4093            .expect("an inserted relation should see its batch-final target");
4094        assert_eq!(
4095            batch_rows(&inserted_target),
4096            vec![expected_row(10, Some(9)), expected_row(9, None)],
4097        );
4098
4099        session
4100            .execute_trusted_dynamic_mutation(&insert(11, None))
4101            .expect("the updated-reference fixture source should commit");
4102        let updated_target = session
4103            .execute_trusted_dynamic_mutation_batch(vec![
4104                update_parent(11, Some(12)),
4105                insert(12, None),
4106            ])
4107            .expect("an updated relation should see its batch-final target");
4108        assert_eq!(
4109            batch_rows(&updated_target),
4110            vec![expected_row(11, Some(12)), expected_row(12, None)],
4111        );
4112    }
4113
4114    #[test]
4115    fn mixed_batch_commits_cross_entity_then_rejects_late_failures_atomically() {
4116        let session = initialize();
4117        session
4118            .execute_trusted_dynamic_mutation(&insert(1, None))
4119            .expect("the primary mixed fixture row should commit");
4120        session
4121            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
4122                entity: OTHER_ENTITY_NAME.to_string(),
4123                patch: other_patch(Some(1), 10),
4124            })
4125            .expect("the secondary mixed fixture row should commit");
4126
4127        let mixed_entity = session
4128            .execute_trusted_dynamic_mutation_batch(vec![
4129                update_code(1, 11),
4130                DynamicMutation::Update {
4131                    entity: OTHER_ENTITY_NAME.to_string(),
4132                    key: InputValue::nat64(1),
4133                    patch: other_patch(None, 11),
4134                },
4135            ])
4136            .expect("one atomic batch may span accepted entities in the same store");
4137        assert_eq!(
4138            batch_rows(&mixed_entity),
4139            vec![
4140                expected_row_with_code(1, None, 11),
4141                vec![
4142                    OutputValue::nat64(1),
4143                    OutputValue::nat64(11),
4144                    OutputValue::null(),
4145                ],
4146            ],
4147        );
4148
4149        let missing = session
4150            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
4151            .expect_err("a late missing delete must reject the earlier staged update");
4152        assert_eq!(missing.class(), ErrorClass::NotFound);
4153
4154        session
4155            .execute_trusted_dynamic_mutation(&insert(2, None))
4156            .expect("the collision fixture should commit");
4157        let collision = session
4158            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
4159            .expect_err("an insert collision must reject the earlier staged update");
4160        assert_eq!(collision.class(), ErrorClass::Conflict);
4161        let failures_unchanged = session
4162            .execute_trusted_dynamic_mutation(&update_code(1, 11))
4163            .expect("failed batches must preserve the original unique value");
4164        assert_eq!(failures_unchanged.affected_rows, 0);
4165
4166        let replaced = session
4167            .execute_trusted_dynamic_mutation_batch(vec![
4168                update_code(1, 14),
4169                DynamicMutation::Replace {
4170                    entity: ENTITY_NAME.to_string(),
4171                    key: InputValue::nat64(99),
4172                    patch: patch(None, None, Some(99)),
4173                },
4174            ])
4175            .expect("ordinary caller-key replace should insert its absent final row");
4176        assert_eq!(
4177            batch_rows(&replaced),
4178            vec![
4179                expected_row_with_code(1, None, 14),
4180                expected_row_with_code(99, None, 99),
4181            ],
4182        );
4183
4184        let unchanged = session
4185            .execute_trusted_dynamic_mutation(&update_code(1, 14))
4186            .expect("the successful mixed replace must publish its preceding update");
4187        assert_eq!(unchanged.affected_rows, 0);
4188        let other_unchanged = session
4189            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
4190                entity: OTHER_ENTITY_NAME.to_string(),
4191                key: InputValue::nat64(1),
4192                patch: other_patch(None, 11),
4193            })
4194            .expect("the cross-entity commit must publish the secondary row");
4195        assert_eq!(other_unchanged.affected_rows, 0);
4196    }
4197
4198    #[test]
4199    fn structural_unknown_root_and_dotted_subpath_reject_before_commit() {
4200        let session = initialize();
4201        session
4202            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, None)])
4203            .expect("structural rejection fixtures should commit");
4204
4205        let unknown_root = session
4206            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
4207                entity: ENTITY_NAME.to_string(),
4208                key: InputValue::nat64(1),
4209                patch: DynamicStructuralPatch::new(vec![(
4210                    "missing".to_string(),
4211                    DynamicWriteCell::Value(InputValue::nat64(10)),
4212                )]),
4213            })
4214            .expect_err("an unknown structural root field must reject");
4215        assert_eq!(unknown_root.class(), ErrorClass::Unsupported);
4216        assert_eq!(unknown_root.origin(), ErrorOrigin::Executor);
4217        assert_eq!(
4218            unknown_root.diagnostic_code(),
4219            icydb_diagnostic_code::DiagnosticCode::RuntimeUnsupported,
4220        );
4221        assert!(unknown_root.diagnostic_facts().is_empty());
4222
4223        let dotted_subpath = session
4224            .execute_trusted_dynamic_mutation_batch(vec![
4225                update_code(1, 11),
4226                DynamicMutation::Update {
4227                    entity: ENTITY_NAME.to_string(),
4228                    key: InputValue::nat64(2),
4229                    patch: DynamicStructuralPatch::new(vec![(
4230                        "code.value".to_string(),
4231                        DynamicWriteCell::Value(InputValue::nat64(12)),
4232                    )]),
4233                },
4234            ])
4235            .expect_err("a dotted structural subpath must reject the complete batch");
4236        assert_eq!(dotted_subpath.class(), ErrorClass::Unsupported);
4237        assert_eq!(dotted_subpath.origin(), ErrorOrigin::Executor);
4238        assert_eq!(
4239            dotted_subpath.diagnostic_code(),
4240            icydb_diagnostic_code::DiagnosticCode::RuntimeUnsupported,
4241        );
4242        assert!(dotted_subpath.diagnostic_facts().is_empty());
4243
4244        let unchanged = session
4245            .execute_trusted_dynamic_mutation(&update_code(1, 1))
4246            .expect("the rejected batch must preserve the earlier row");
4247        assert_eq!(unchanged.affected_rows, 0);
4248
4249        let whole_field = session
4250            .execute_trusted_dynamic_mutation(&update_code(2, 12))
4251            .expect("a complete root-field update must remain supported");
4252        assert_eq!(whole_field.affected_rows, 1);
4253        assert_eq!(whole_field.rows, vec![expected_row_with_code(2, None, 12)]);
4254    }
4255
4256    #[test]
4257    fn cross_entity_relations_observe_one_complete_final_overlay() {
4258        let session = initialize();
4259        let inserted = session
4260            .execute_trusted_dynamic_mutation_batch(vec![
4261                DynamicMutation::Insert {
4262                    entity: OTHER_ENTITY_NAME.to_string(),
4263                    patch: other_patch_with_node(Some(20), 200, Some(42)),
4264                },
4265                insert(42, None),
4266            ])
4267            .expect("a source may precede its same-batch target in another entity");
4268        assert_eq!(inserted.len(), 2);
4269
4270        session
4271            .execute_trusted_dynamic_mutation_batch(vec![
4272                delete(42),
4273                DynamicMutation::Delete {
4274                    entity: OTHER_ENTITY_NAME.to_string(),
4275                    key: InputValue::nat64(20),
4276                },
4277            ])
4278            .expect("a target and cross-entity source may delete in either request order");
4279
4280        session
4281            .execute_trusted_dynamic_mutation_batch(vec![
4282                insert(43, None),
4283                DynamicMutation::Insert {
4284                    entity: OTHER_ENTITY_NAME.to_string(),
4285                    patch: other_patch_with_node(Some(21), 210, Some(43)),
4286                },
4287            ])
4288            .expect("the retained cross-entity relation fixture should commit");
4289        let blocked = session
4290            .execute_trusted_dynamic_mutation_batch(vec![delete(43)])
4291            .expect_err("a retained source in another entity must protect its target");
4292        assert_relation_violation(&blocked);
4293    }
4294
4295    #[test]
4296    fn mixed_batch_admits_64_entities_with_one_timestamp_and_rejects_the_65th() {
4297        let session = initialize();
4298        let requests = (0..64)
4299            .map(|index| DynamicMutation::Insert {
4300                entity: format!("MixedBounded{index}"),
4301                patch: DynamicStructuralPatch::new(vec![(
4302                    "id".to_string(),
4303                    DynamicWriteCell::Value(InputValue::nat64(1)),
4304                )]),
4305            })
4306            .collect();
4307        let admitted = session
4308            .execute_trusted_dynamic_mutation_batch(requests)
4309            .expect("exactly 64 same-store entities should admit");
4310        assert_eq!(admitted.len(), 64);
4311        let timestamps = admitted
4312            .iter()
4313            .map(|result| {
4314                result
4315                    .rows
4316                    .first()
4317                    .and_then(|row| row.get(1))
4318                    .expect("every bounded entity should return its managed timestamp")
4319            })
4320            .collect::<Vec<_>>();
4321        assert!(timestamps.windows(2).all(|pair| pair[0] == pair[1]));
4322
4323        let over_limit = (0..65)
4324            .map(|index| DynamicMutation::Insert {
4325                entity: format!("MixedBounded{index}"),
4326                patch: DynamicStructuralPatch::new(vec![(
4327                    "id".to_string(),
4328                    DynamicWriteCell::Value(InputValue::nat64(2)),
4329                )]),
4330            })
4331            .collect();
4332        let error = session
4333            .execute_trusted_dynamic_mutation_batch(over_limit)
4334            .expect_err("the 65th distinct entity must reject before staging");
4335        assert_eq!(error.class(), ErrorClass::Unsupported);
4336        assert_eq!(
4337            error.diagnostic_facts(),
4338            vec![
4339                (icydb_diagnostic_code::DiagnosticFactTag::ActualCount, 65),
4340                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 64),
4341            ],
4342        );
4343    }
4344
4345    #[test]
4346    fn mixed_batch_rejects_a_cross_store_item_with_bounded_tags() {
4347        let session = initialize();
4348        let error = session
4349            .execute_trusted_dynamic_mutation_batch(vec![
4350                insert(70, None),
4351                DynamicMutation::Insert {
4352                    entity: CROSS_ENTITY_NAME.to_string(),
4353                    patch: DynamicStructuralPatch::new(vec![(
4354                        "id".to_string(),
4355                        DynamicWriteCell::Value(InputValue::nat64(70)),
4356                    )]),
4357                },
4358            ])
4359            .expect_err("a structural batch must remain inside one accepted store");
4360        assert_eq!(error.class(), ErrorClass::Conflict);
4361        assert_eq!(
4362            error.diagnostic_facts(),
4363            vec![
4364                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1),
4365                (
4366                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedEntityTag,
4367                    ENTITY_TAG.value(),
4368                ),
4369                (
4370                    icydb_diagnostic_code::DiagnosticFactTag::ActualEntityTag,
4371                    CROSS_ENTITY_TAG.value(),
4372                ),
4373            ],
4374        );
4375        session
4376            .execute_trusted_dynamic_mutation(&insert(70, None))
4377            .expect("cross-store rejection must publish no first-item effect");
4378    }
4379
4380    #[test]
4381    fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
4382        let session = initialize();
4383        session
4384            .execute_trusted_dynamic_mutation_batch(vec![
4385                insert_with_code(1, None, 10),
4386                insert_with_code(2, None, 20),
4387            ])
4388            .expect("the unique-overlay fixture should commit");
4389
4390        let conflict = session
4391            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 30), update_code(2, 30)])
4392            .expect_err("the final row must still reject a duplicate unique membership");
4393        assert_eq!(
4394            conflict.diagnostic().error_code(),
4395            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_CONSTRAINT_VIOLATION,
4396        );
4397        for (id, code) in [(1, 10), (2, 20)] {
4398            let unchanged = session
4399                .execute_trusted_dynamic_mutation(&update_code(id, code))
4400                .expect("rejected preflight must preserve both original unique values");
4401            assert_eq!(unchanged.affected_rows, 0);
4402        }
4403
4404        let swapped = session
4405            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
4406            .expect("two final rows should atomically swap unique memberships");
4407        assert_eq!(
4408            batch_rows(&swapped),
4409            vec![
4410                expected_row_with_code(1, None, 20),
4411                expected_row_with_code(2, None, 10),
4412            ],
4413        );
4414
4415        let released = session
4416            .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
4417            .expect("a delete should release unique membership to a final inserted row");
4418        assert_eq!(
4419            batch_rows(&released),
4420            vec![
4421                expected_row_with_code(1, None, 20),
4422                expected_row_with_code(3, None, 20),
4423            ],
4424        );
4425    }
4426}
4427
4428#[cfg(test)]
4429mod identity_pre_key_tests {
4430    #[cfg(feature = "sql")]
4431    mod grouped_count_tests;
4432    mod nested_relation_tests;
4433    mod replay_construction_tests;
4434    mod result_boundary_tests;
4435    #[cfg(feature = "sql")]
4436    mod schema_publication_tests;
4437
4438    use super::DynamicTypedEntityBinding;
4439    use super::{
4440        AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
4441        AcceptedStructuralMutationPacking, AcceptedStructuralMutationStagedAdmission,
4442        AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
4443        DynamicTypedMutation, DynamicWriteCell, FieldSlot,
4444        MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS, MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES,
4445        MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES, MutationProgressRecordOp,
4446        TypedEntityDescriptor, TypedFieldType, add_structural_mutation_staged_bytes,
4447        admit_structural_mutation_staged_charge, checked_pre_key_candidate_count,
4448        insert_key_exists_after_generation, structural_mutation_staged_charge,
4449        validate_structural_mutation_result_bytes,
4450    };
4451    #[cfg(feature = "sql")]
4452    use crate::db::data::DecodedDataStoreKey;
4453    #[cfg(feature = "sql")]
4454    use crate::db::executor::budget::{
4455        HardExecutionBudget, HardExecutionContext, HardExecutionFailureHeadroom,
4456        with_execution_budget_for_tests, with_query_execution_budget_for_tests,
4457    };
4458    use crate::db::mutation_job::{MutationJobRecord, MutationJobTransition};
4459    #[cfg(feature = "sql")]
4460    use crate::db::{
4461        CompareProofAndAdvanceError, ExhaustiveReadError, MutationJobError,
4462        MutationJobRestartReason, PrimaryKeyComponent, PrimaryKeyValue, RawDataStoreKey,
4463        ReadSetRevisionError, ResumableJobAdvance, ResumableJobAdvanceRequest,
4464        ResumableJobAdvanceStatus, ResumableJobError, ResumableJobId, ResumableJobIdempotencyKey,
4465        ResumableJobStatus, asc,
4466    };
4467    use crate::db::{DynamicQuery, QueryExecutionError};
4468    use crate::{
4469        db::{
4470            GeneratedStartupDriverStep, MutationJobAdvanceRequest, MutationJobId,
4471            MutationJobIdempotencyKey, MutationJobPhase, MutationJobStatus, TypedFieldDescriptor,
4472            commit::{
4473                database_incarnation_id, forget_recovered_domain_for_tests,
4474                install_startup_recovery_wakeup,
4475            },
4476            data::DataStore,
4477            drive_generated_startup_recovery_page,
4478            executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
4479            index::{IndexId, IndexKey, IndexKeyKind, IndexStore, IndexStoreVisit},
4480            integrity::{
4481                InsertMutationJobResult, PhysicalUnitCheckpoint, QuickIntegrityStatus,
4482                RowInspectionLimits, execute_quick_integrity, execute_row_integrity_page,
4483                with_mutation_progress_store,
4484            },
4485            journal::{
4486                JournalBatch, JournalRecord, JournalSequence, JournalTailControl, JournalTailStore,
4487                encode_journal_batch,
4488            },
4489            registry::{
4490                StoreAllocationIdentities, StoreAllocationIdentity, StoreHandle, StoreRegistry,
4491                StoreRuntimeStorageCapabilities,
4492            },
4493            schema::{
4494                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
4495                FieldInsertGeneration, FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
4496                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
4497                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
4498                SchemaFieldSlot, SchemaFieldWritePolicy, SchemaIndexId, SchemaInsertDefault,
4499                SchemaRowLayout, SchemaStore, SchemaVersion,
4500                accepted_schema_candidate_with_field_bindings_for_tests,
4501                cardinality_build::{
4502                    CardinalityBuildAuthority, CardinalityGenerationPageOutcome,
4503                    drive_cardinality_generation_page,
4504                },
4505                cardinality_generation::{CardinalityGenerationHeader, CardinalityGenerationState},
4506            },
4507            write_context::MutationMode,
4508        },
4509        error::{ErrorClass, ErrorOrigin, InternalError},
4510        testing::test_memory,
4511        traits::{CanisterKind, Path},
4512        types::{EntityTag, Timestamp},
4513        value::{InputValue, OutputValue, Value},
4514    };
4515    use icydb_schema::{FieldSourceKey, ScalarType};
4516    use std::{
4517        cell::{Cell, RefCell},
4518        collections::BTreeMap,
4519    };
4520
4521    const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
4522    const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
4523    const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
4524    const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
4525    const ENTITY_NAME: &str = "IdentityRow";
4526    const ENTITY_TAG: EntityTag = EntityTag::new(93);
4527    const TYPED_DESCRIPTOR: TypedEntityDescriptor = TypedEntityDescriptor::new(
4528        ENTITY_SOURCE,
4529        &[ID_SOURCE],
4530        &[
4531            TypedFieldDescriptor::new(ID_SOURCE, TypedFieldType::Scalar(ScalarType::Nat64), false),
4532            TypedFieldDescriptor::new(
4533                PAYLOAD_SOURCE,
4534                TypedFieldType::Scalar(ScalarType::Nat64),
4535                false,
4536            ),
4537        ],
4538    );
4539    const SECOND_ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::SecondEntity";
4540    const SECOND_ID_SOURCE: &str = "session::write::identity_pre_key_tests::SecondEntity::id";
4541    const SECOND_PAYLOAD_SOURCE: &str =
4542        "session::write::identity_pre_key_tests::SecondEntity::payload";
4543    const SECOND_TARGET_SOURCE: &str =
4544        "session::write::identity_pre_key_tests::SecondEntity::target_id";
4545    const SECOND_ENTITY_NAME: &str = "SecondIdentityRow";
4546    const SECOND_ENTITY_TAG: EntityTag = EntityTag::new(96);
4547    const THIRD_ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::ThirdEntity";
4548    const THIRD_ID_SOURCE: &str = "session::write::identity_pre_key_tests::ThirdEntity::id";
4549    const THIRD_PAYLOAD_SOURCE: &str =
4550        "session::write::identity_pre_key_tests::ThirdEntity::payload";
4551    const THIRD_ENTITY_NAME: &str = "ThirdIdentityRow";
4552    const THIRD_ENTITY_TAG: EntityTag = EntityTag::new(97);
4553    const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
4554    const UNRELATED_STORE_PATH: &str = "session::write::identity_pre_key_tests::UnrelatedStore";
4555
4556    fn batch_rows(results: &[crate::db::DynamicMutationResult]) -> Vec<Vec<OutputValue>> {
4557        results
4558            .iter()
4559            .flat_map(|result| result.rows.iter().cloned())
4560            .collect()
4561    }
4562
4563    struct TestCanister;
4564
4565    impl Path for TestCanister {
4566        const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
4567    }
4568
4569    impl CanisterKind for TestCanister {
4570        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4571            Ok(45)
4572        }
4573        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
4574        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4575            Ok(49)
4576        }
4577        const STARTUP_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.startup.control.v1";
4578        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4579            Ok(46)
4580        }
4581        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
4582            "icydb.identity_pre_key_tests.integrity.progress.v1";
4583    }
4584
4585    thread_local! {
4586        static STARTUP_WAKEUPS: Cell<u32> = const { Cell::new(0) };
4587        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
4588        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
4589        static SCHEMA_STORE: RefCell<SchemaStore> =
4590            const { RefCell::new(SchemaStore::init_heap()) };
4591        static UNRELATED_DATA_STORE: RefCell<DataStore> =
4592            const { RefCell::new(DataStore::init_heap()) };
4593        static UNRELATED_INDEX_STORE: RefCell<IndexStore> =
4594            const { RefCell::new(IndexStore::init_heap()) };
4595        static UNRELATED_SCHEMA_STORE: RefCell<SchemaStore> =
4596            const { RefCell::new(SchemaStore::init_heap()) };
4597        static STORE_REGISTRY: StoreRegistry = {
4598            let mut registry = StoreRegistry::new();
4599            registry.register_store(
4600                STORE_PATH,
4601                &DATA_STORE,
4602                &INDEX_STORE,
4603                &SCHEMA_STORE,
4604                StoreAllocationIdentities::absent(),
4605                StoreRuntimeStorageCapabilities::heap(),
4606            ).expect("identity pre-key test store should register");
4607            registry.register_store(
4608                UNRELATED_STORE_PATH,
4609                &UNRELATED_DATA_STORE,
4610                &UNRELATED_INDEX_STORE,
4611                &UNRELATED_SCHEMA_STORE,
4612                StoreAllocationIdentities::absent(),
4613                StoreRuntimeStorageCapabilities::heap(),
4614            ).expect("unrelated identity test store should register");
4615            registry
4616        };
4617        static JOURNALED_DATA_STORE: RefCell<DataStore> =
4618            RefCell::new(DataStore::init_journaled(test_memory(186)));
4619        static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
4620            RefCell::new(IndexStore::init_journaled(test_memory(187)));
4621        static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
4622            RefCell::new(SchemaStore::init_journaled(test_memory(188)));
4623        static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
4624            RefCell::new(JournalTailStore::init(test_memory(189)));
4625        static JOURNALED_STORE_REGISTRY: StoreRegistry = {
4626            let mut registry = StoreRegistry::new();
4627            registry.register_journaled_store(
4628                JOURNALED_STORE_PATH,
4629                &JOURNALED_DATA_STORE,
4630                &JOURNALED_INDEX_STORE,
4631                &JOURNALED_SCHEMA_STORE,
4632                &JOURNALED_TAIL_STORE,
4633                StoreAllocationIdentities::new_journaled(
4634                    StoreAllocationIdentity::new(186, "icydb.test.identity_range.data.v1"),
4635                    StoreAllocationIdentity::new(187, "icydb.test.identity_range.index.v1"),
4636                    StoreAllocationIdentity::new(188, "icydb.test.identity_range.schema.v1"),
4637                    StoreAllocationIdentity::new(189, "icydb.test.identity_range.journal.v1"),
4638                ),
4639                StoreRuntimeStorageCapabilities::journaled(),
4640            ).expect("identity range journaled store should register");
4641            registry
4642        };
4643    }
4644
4645    fn record_startup_wakeup() {
4646        STARTUP_WAKEUPS.with(|wakeups| wakeups.set(wakeups.get().saturating_add(1)));
4647    }
4648
4649    struct JournaledTestCanister;
4650
4651    impl Path for JournaledTestCanister {
4652        const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
4653    }
4654
4655    impl CanisterKind for JournaledTestCanister {
4656        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4657            Ok(190)
4658        }
4659        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
4660        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4661            Ok(192)
4662        }
4663        const STARTUP_STABLE_KEY: &'static str = "icydb.identity_range_tests.startup.control.v1";
4664        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
4665            Ok(191)
4666        }
4667        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
4668            "icydb.identity_range_tests.integrity.progress.v1";
4669    }
4670
4671    fn source_key(source: &str) -> FieldSourceKey {
4672        FieldSourceKey::try_new(source).expect("identity test field source should admit")
4673    }
4674
4675    fn identity_snapshot(store_path: &str, payload_unique: bool) -> PersistedSchemaSnapshot {
4676        identity_snapshot_for_entity(
4677            store_path,
4678            payload_unique,
4679            false,
4680            false,
4681            ENTITY_SOURCE,
4682            ENTITY_NAME,
4683            None,
4684        )
4685    }
4686
4687    fn identity_snapshot_with_nullable_payload(store_path: &str) -> PersistedSchemaSnapshot {
4688        identity_snapshot_for_entity(
4689            store_path,
4690            false,
4691            false,
4692            true,
4693            ENTITY_SOURCE,
4694            ENTITY_NAME,
4695            None,
4696        )
4697    }
4698
4699    fn identity_snapshot_with_payload_index(
4700        store_path: &str,
4701        payload_unique: bool,
4702        composite: bool,
4703    ) -> PersistedSchemaSnapshot {
4704        identity_snapshot_for_entity(
4705            store_path,
4706            payload_unique,
4707            composite,
4708            false,
4709            ENTITY_SOURCE,
4710            ENTITY_NAME,
4711            None,
4712        )
4713    }
4714
4715    fn identity_snapshot_for_entity(
4716        store_path: &str,
4717        payload_unique: bool,
4718        composite: bool,
4719        payload_nullable: bool,
4720        entity_source: &str,
4721        entity_name: &str,
4722        relation_target: Option<&str>,
4723    ) -> PersistedSchemaSnapshot {
4724        let mut fields = vec![
4725            PersistedFieldSnapshot::new_initial_with_write_policy(
4726                FieldId::new(1),
4727                "id".to_string(),
4728                SchemaFieldSlot::new(0),
4729                AcceptedFieldKind::Nat64,
4730                Vec::new(),
4731                false,
4732                SchemaInsertDefault::None,
4733                SchemaFieldWritePolicy::from_model_policies(
4734                    Some(FieldInsertGeneration::Identity),
4735                    None,
4736                ),
4737                FieldStorageDecode::ByKind,
4738                LeafCodec::Scalar(ScalarCodec::Nat64),
4739            ),
4740            PersistedFieldSnapshot::new_initial(
4741                FieldId::new(2),
4742                "payload".to_string(),
4743                SchemaFieldSlot::new(1),
4744                AcceptedFieldKind::Nat64,
4745                Vec::new(),
4746                payload_nullable,
4747                SchemaInsertDefault::None,
4748                FieldStorageDecode::ByKind,
4749                LeafCodec::Scalar(ScalarCodec::Nat64),
4750            ),
4751        ];
4752        if relation_target.is_some() {
4753            fields.push(PersistedFieldSnapshot::new_initial(
4754                FieldId::new(3),
4755                "target_id".to_string(),
4756                SchemaFieldSlot::new(2),
4757                AcceptedFieldKind::Nat64,
4758                Vec::new(),
4759                true,
4760                SchemaInsertDefault::None,
4761                FieldStorageDecode::ByKind,
4762                LeafCodec::Scalar(ScalarCodec::Nat64),
4763            ));
4764        }
4765        let mut index_fields = vec![PersistedIndexFieldPathSnapshot::new(
4766            FieldId::new(2),
4767            SchemaFieldSlot::new(1),
4768            vec!["payload".to_string()],
4769            AcceptedFieldKind::Nat64,
4770            payload_nullable,
4771        )];
4772        if composite {
4773            index_fields.push(PersistedIndexFieldPathSnapshot::new(
4774                FieldId::new(1),
4775                SchemaFieldSlot::new(0),
4776                vec!["id".to_string()],
4777                AcceptedFieldKind::Nat64,
4778                false,
4779            ));
4780        }
4781        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
4782            SchemaVersion::initial(),
4783            entity_source.to_string(),
4784            entity_name.to_string(),
4785            FieldId::new(1),
4786            SchemaRowLayout::initial(
4787                fields
4788                    .iter()
4789                    .map(|field| (field.id(), field.slot()))
4790                    .collect(),
4791            ),
4792            fields,
4793            vec![PersistedIndexSnapshot::new(
4794                SchemaIndexId::new(1).expect("identity test index ID should admit"),
4795                1,
4796                if composite {
4797                    "by_payload_id".to_string()
4798                } else {
4799                    "by_payload".to_string()
4800                },
4801                store_path.to_string(),
4802                payload_unique,
4803                PersistedIndexKeySnapshot::FieldPath(index_fields),
4804                None,
4805            )],
4806        );
4807        let Some(relation_target) = relation_target else {
4808            return snapshot;
4809        };
4810        let snapshot = snapshot.with_relations(vec![PersistedRelationEdgeSnapshot::new_direct(
4811            RelationId::new(1).expect("mixed recovery relation identity should be non-zero"),
4812            "target".to_string(),
4813            relation_target.to_string(),
4814            vec![FieldId::new(3)],
4815        )]);
4816        let constraints = AcceptedConstraintCatalog::initial(
4817            snapshot.fields(),
4818            snapshot.indexes(),
4819            snapshot.relations(),
4820        )
4821        .expect("mixed recovery relation constraints should close");
4822        snapshot.with_constraint_catalog(constraints)
4823    }
4824
4825    fn initialize() -> DbSession<TestCanister> {
4826        initialize_with_snapshot(identity_snapshot(STORE_PATH, false))
4827    }
4828
4829    fn initialize_with_composite_payload_index() -> DbSession<TestCanister> {
4830        initialize_with_snapshot(identity_snapshot_with_payload_index(
4831            STORE_PATH, false, true,
4832        ))
4833    }
4834
4835    fn initialize_with_snapshot(snapshot: PersistedSchemaSnapshot) -> DbSession<TestCanister> {
4836        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
4837        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
4838        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
4839        UNRELATED_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
4840        UNRELATED_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
4841        UNRELATED_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
4842        let session = DbSession::<TestCanister>::new(
4843            &STORE_REGISTRY,
4844            &crate::db::RequestExecutionRoot::__new_runtime_root(),
4845        );
4846        session
4847            .db
4848            .drive_startup_recovery_page()
4849            .expect("identity pre-key test database should initialize");
4850        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
4851            STORE_PATH,
4852            AcceptedSchemaRevision::INITIAL,
4853            BTreeMap::from([(ENTITY_TAG, snapshot)]),
4854            BTreeMap::from([
4855                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
4856                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
4857            ]),
4858        );
4859        let store = session
4860            .db
4861            .store_handle(STORE_PATH)
4862            .expect("identity pre-key test store should resolve");
4863        crate::db::commit::publish_accepted_schema_candidate(
4864            STORE_PATH,
4865            store,
4866            AcceptedSchemaRevision::NONE,
4867            &candidate,
4868        )
4869        .expect("identity candidate should publish with explicit zero state");
4870        session
4871    }
4872
4873    fn initialize_journaled_with_root_and_payload_uniqueness(
4874        payload_unique: bool,
4875    ) -> (
4876        DbSession<JournaledTestCanister>,
4877        crate::db::RequestExecutionRoot,
4878    ) {
4879        let root = crate::db::RequestExecutionRoot::__new_runtime_root();
4880        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY, &root);
4881        session
4882            .db
4883            .drive_startup_recovery_page()
4884            .expect("journaled identity database should initialize");
4885        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
4886            JOURNALED_STORE_PATH,
4887            AcceptedSchemaRevision::INITIAL,
4888            BTreeMap::from([(
4889                ENTITY_TAG,
4890                identity_snapshot(JOURNALED_STORE_PATH, payload_unique),
4891            )]),
4892            BTreeMap::from([
4893                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
4894                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
4895            ]),
4896        );
4897        let store = session
4898            .db
4899            .store_handle(JOURNALED_STORE_PATH)
4900            .expect("journaled identity store should resolve");
4901        crate::db::commit::publish_accepted_schema_candidate(
4902            JOURNALED_STORE_PATH,
4903            store,
4904            AcceptedSchemaRevision::NONE,
4905            &candidate,
4906        )
4907        .expect("journaled identity candidate should publish");
4908        (session, root)
4909    }
4910
4911    fn initialize_journaled_with_root() -> (
4912        DbSession<JournaledTestCanister>,
4913        crate::db::RequestExecutionRoot,
4914    ) {
4915        initialize_journaled_with_root_and_payload_uniqueness(false)
4916    }
4917
4918    fn initialize_journaled_multi_entity() -> DbSession<JournaledTestCanister> {
4919        let root = crate::db::RequestExecutionRoot::__new_runtime_root();
4920        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY, &root);
4921        session
4922            .db
4923            .drive_startup_recovery_page()
4924            .expect("multi-entity journaled database should initialize");
4925        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
4926            JOURNALED_STORE_PATH,
4927            AcceptedSchemaRevision::INITIAL,
4928            BTreeMap::from([
4929                (ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH, false)),
4930                (
4931                    SECOND_ENTITY_TAG,
4932                    identity_snapshot_for_entity(
4933                        JOURNALED_STORE_PATH,
4934                        false,
4935                        false,
4936                        false,
4937                        SECOND_ENTITY_SOURCE,
4938                        SECOND_ENTITY_NAME,
4939                        Some(ENTITY_SOURCE),
4940                    ),
4941                ),
4942                (
4943                    THIRD_ENTITY_TAG,
4944                    identity_snapshot_for_entity(
4945                        JOURNALED_STORE_PATH,
4946                        false,
4947                        false,
4948                        false,
4949                        THIRD_ENTITY_SOURCE,
4950                        THIRD_ENTITY_NAME,
4951                        None,
4952                    ),
4953                ),
4954            ]),
4955            BTreeMap::from([
4956                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
4957                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
4958                (
4959                    (SECOND_ENTITY_TAG, source_key(SECOND_ID_SOURCE)),
4960                    FieldId::new(1),
4961                ),
4962                (
4963                    (SECOND_ENTITY_TAG, source_key(SECOND_PAYLOAD_SOURCE)),
4964                    FieldId::new(2),
4965                ),
4966                (
4967                    (SECOND_ENTITY_TAG, source_key(SECOND_TARGET_SOURCE)),
4968                    FieldId::new(3),
4969                ),
4970                (
4971                    (THIRD_ENTITY_TAG, source_key(THIRD_ID_SOURCE)),
4972                    FieldId::new(1),
4973                ),
4974                (
4975                    (THIRD_ENTITY_TAG, source_key(THIRD_PAYLOAD_SOURCE)),
4976                    FieldId::new(2),
4977                ),
4978            ]),
4979        );
4980        let store = session
4981            .db
4982            .store_handle(JOURNALED_STORE_PATH)
4983            .expect("multi-entity journaled store should resolve");
4984        crate::db::commit::publish_accepted_schema_candidate(
4985            JOURNALED_STORE_PATH,
4986            store,
4987            AcceptedSchemaRevision::NONE,
4988            &candidate,
4989        )
4990        .expect("multi-entity journaled candidate should publish");
4991        session
4992    }
4993
4994    fn initialize_journaled() -> DbSession<JournaledTestCanister> {
4995        initialize_journaled_with_root().0
4996    }
4997
4998    fn initialize_journaled_with_unique_payload() -> DbSession<JournaledTestCanister> {
4999        initialize_journaled_with_root_and_payload_uniqueness(true).0
5000    }
5001
5002    fn drive_journaled_recovery_to_completion(session: &DbSession<JournaledTestCanister>) {
5003        for _ in 0..8 {
5004            if session
5005                .db
5006                .drive_startup_recovery_page()
5007                .expect("dedicated driver recovery should remain valid")
5008            {
5009                return;
5010            }
5011        }
5012        panic!("dedicated driver recovery should quiesce within eight complete batches");
5013    }
5014
5015    fn drive_journaled_cardinality_to_ready(session: &DbSession<JournaledTestCanister>) {
5016        let handle = session
5017            .db
5018            .store_handle(JOURNALED_STORE_PATH)
5019            .expect("journaled cardinality store should resolve");
5020        for _ in 0..8 {
5021            let outcome = handle
5022                .with_data(|data| {
5023                    handle.with_index(|index| {
5024                        handle.with_schema_mut(|schema| {
5025                            drive_cardinality_generation_page(data, index, schema, |schema| {
5026                                let watermark = JOURNALED_TAIL_STORE
5027                                    .with(|tail| tail.borrow().fold_watermark())?;
5028                                CardinalityBuildAuthority::derive(
5029                                    schema,
5030                                    database_incarnation_id()?,
5031                                    handle.allocation_identities(),
5032                                    watermark,
5033                                )
5034                            })
5035                        })
5036                    })
5037                })
5038                .expect("bounded cardinality generation should advance");
5039            if outcome == CardinalityGenerationPageOutcome::Quiescent {
5040                return;
5041            }
5042        }
5043        panic!("cardinality generation should become Ready within eight bounded pages");
5044    }
5045
5046    fn journaled_user_index_prefix() -> (IndexId, Vec<Vec<u8>>) {
5047        JOURNALED_INDEX_STORE.with(|store| {
5048            let mut selected = None;
5049            store
5050                .borrow()
5051                .visit_entries(|raw_key, _value| {
5052                    let key = IndexKey::try_from_raw(raw_key)
5053                        .expect("accepted user index key should decode");
5054                    if key.key_kind() != IndexKeyKind::User {
5055                        return Ok::<_, InternalError>(IndexStoreVisit::Continue);
5056                    }
5057                    let components = (0..key.component_count())
5058                        .map(|index| {
5059                            key.component(index)
5060                                .expect("accepted index component should exist")
5061                                .to_vec()
5062                        })
5063                        .collect::<Vec<_>>();
5064                    selected = Some((*key.index_id(), components));
5065                    Ok(IndexStoreVisit::Stop)
5066                })
5067                .expect("accepted user index should be inspectable");
5068            selected.expect("the cardinality fixture should contain one user index entry")
5069        })
5070    }
5071
5072    fn reset_journaled_cardinality_projections() -> u64 {
5073        JOURNALED_DATA_STORE.with(|store| {
5074            store
5075                .borrow_mut()
5076                .reset_journaled_live_projection()
5077                .expect("row projection should reset without a count scan");
5078        });
5079        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
5080        let fold_watermark = JOURNALED_TAIL_STORE
5081            .with(|store| store.borrow().fold_watermark())
5082            .expect("journal watermark should remain current-form");
5083        JOURNALED_INDEX_STORE.with(|store| {
5084            store
5085                .borrow_mut()
5086                .reset_journaled_live_projection(data_generation, fold_watermark)
5087                .expect("index projection should reset without a count scan");
5088        });
5089        data_generation
5090    }
5091
5092    fn assert_journaled_cardinality(
5093        handle: StoreHandle,
5094        index_id: IndexId,
5095        prefix_components: &[Vec<u8>],
5096        expected: u64,
5097    ) {
5098        assert_eq!(handle.exact_entity_count(ENTITY_TAG), Some(expected));
5099        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
5100        assert_eq!(
5101            handle.exact_user_index_prefix_count(
5102                data_generation,
5103                IndexKeyKind::User,
5104                index_id,
5105                prefix_components,
5106            ),
5107            Some(expected),
5108        );
5109    }
5110
5111    fn mark_journaled_cardinality_building() {
5112        let current = JOURNALED_SCHEMA_STORE.with(|store| {
5113            store
5114                .borrow()
5115                .cardinality_generation_header()
5116                .expect("Ready header should decode")
5117                .expect("Ready header should exist")
5118        });
5119        JOURNALED_SCHEMA_STORE.with(|store| {
5120            store
5121                .borrow_mut()
5122                .write_cardinality_generation_header(CardinalityGenerationHeader::new(
5123                    current.generation(),
5124                    CardinalityGenerationState::Building,
5125                    current.slot(),
5126                    current.source(),
5127                ))
5128                .expect("Building fallback fixture should persist");
5129        });
5130    }
5131
5132    fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
5133        AcceptedMutationIntentPatch::new()
5134            .set_authored(FieldSlot::from_validated_index(1), InputValue::nat64(value))
5135    }
5136
5137    fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
5138        DynamicStructuralPatch::new(vec![(
5139            "payload".to_string(),
5140            DynamicWriteCell::Value(InputValue::nat64(value)),
5141        )])
5142    }
5143
5144    fn related_dynamic_payload_patch(value: u64, target_id: u64) -> DynamicStructuralPatch {
5145        DynamicStructuralPatch::new(vec![
5146            (
5147                "payload".to_string(),
5148                DynamicWriteCell::Value(InputValue::nat64(value)),
5149            ),
5150            (
5151                "target_id".to_string(),
5152                DynamicWriteCell::Value(InputValue::nat64(target_id)),
5153            ),
5154        ])
5155    }
5156
5157    fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
5158        vec![OutputValue::nat64(id), OutputValue::nat64(payload)]
5159    }
5160
5161    fn exact_key_binding<C: CanisterKind>(session: &DbSession<C>) -> DynamicTypedEntityBinding {
5162        session
5163            .issue_typed_entity_binding(&TYPED_DESCRIPTOR)
5164            .expect("exact-key test binding should issue")
5165    }
5166
5167    fn typed_payload_insert(
5168        binding: &DynamicTypedEntityBinding,
5169        payload: u64,
5170    ) -> DynamicTypedMutation {
5171        let patch = binding
5172            .bind_write_ordinals(vec![(
5173                1,
5174                DynamicWriteCell::Value(InputValue::nat64(payload)),
5175            )])
5176            .expect("typed payload patch should bind");
5177        DynamicTypedMutation::Insert { patch }
5178    }
5179
5180    fn typed_payload_delete(id: u64) -> DynamicTypedMutation {
5181        DynamicTypedMutation::Delete {
5182            key: InputValue::nat64(id),
5183        }
5184    }
5185
5186    fn insert_exact_key_fixture<C: CanisterKind>(session: &DbSession<C>, payload: u64) -> u64 {
5187        let output = session
5188            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
5189                entity: ENTITY_NAME.to_string(),
5190                patch: dynamic_payload_patch(payload),
5191            })
5192            .expect("exact-key fixture insert should commit");
5193        match output.rows.as_slice() {
5194            [row] => match row.as_slice() {
5195                [id, actual_payload] if matches!(actual_payload.as_public(), crate::value::PublicValue::Nat64(value) if *value == payload) =>
5196                {
5197                    let crate::value::PublicValue::Nat64(id) = id.as_public() else {
5198                        panic!("exact-key fixture should return a natural identity");
5199                    };
5200                    *id
5201                }
5202                _ => panic!("exact-key fixture should return its identity and payload"),
5203            },
5204            _ => panic!("exact-key fixture insert should return one row"),
5205        }
5206    }
5207
5208    #[cfg(feature = "sql")]
5209    fn sql_projection_rows(session: &DbSession<TestCanister>, sql: &str) -> Vec<Vec<OutputValue>> {
5210        let crate::db::SqlStatementResult::Projection { rows, .. } = session
5211            .execute_trusted_sql_query(sql)
5212            .expect("focused SQL projection should execute")
5213        else {
5214            panic!("focused SQL projection should return rows")
5215        };
5216
5217        rows
5218    }
5219
5220    #[cfg(feature = "sql")]
5221    #[test]
5222    fn secondary_ordered_covering_limit_stops_at_the_present_row_window() {
5223        let session = initialize_with_composite_payload_index();
5224        for payload in [30, 10, 20, 20, 40] {
5225            insert_exact_key_fixture(&session, payload);
5226        }
5227
5228        assert_eq!(
5229            sql_projection_rows(
5230                &session,
5231                "SELECT payload FROM IdentityRow ORDER BY payload ASC, id ASC LIMIT 1",
5232            ),
5233            vec![vec![OutputValue::nat64(10)]],
5234        );
5235        #[cfg(feature = "sql")]
5236        assert_sql_query_fits_resource_limit(
5237            &session,
5238            "SELECT payload FROM IdentityRow ORDER BY payload ASC, id ASC LIMIT 1",
5239            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
5240            1,
5241        );
5242
5243        assert_eq!(
5244            sql_projection_rows(
5245                &session,
5246                "SELECT payload FROM IdentityRow ORDER BY payload DESC, id DESC LIMIT 1",
5247            ),
5248            vec![vec![OutputValue::nat64(40)]],
5249        );
5250        #[cfg(feature = "sql")]
5251        assert_sql_query_fits_resource_limit(
5252            &session,
5253            "SELECT payload FROM IdentityRow ORDER BY payload DESC, id DESC LIMIT 1",
5254            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
5255            1,
5256        );
5257
5258        assert_eq!(
5259            sql_projection_rows(
5260                &session,
5261                "SELECT id, payload FROM IdentityRow \
5262                 ORDER BY payload ASC, id ASC LIMIT 2 OFFSET 1",
5263            ),
5264            vec![
5265                vec![OutputValue::nat64(3), OutputValue::nat64(20)],
5266                vec![OutputValue::nat64(4), OutputValue::nat64(20)],
5267            ],
5268        );
5269        #[cfg(feature = "sql")]
5270        assert_sql_query_fits_resource_limit(
5271            &session,
5272            "SELECT id, payload FROM IdentityRow \
5273             ORDER BY payload ASC, id ASC LIMIT 2 OFFSET 1",
5274            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
5275            3,
5276        );
5277
5278        assert_eq!(
5279            sql_projection_rows(
5280                &session,
5281                "SELECT payload FROM IdentityRow ORDER BY payload ASC, id ASC",
5282            ),
5283            [10, 20, 20, 30, 40]
5284                .into_iter()
5285                .map(|payload| vec![OutputValue::nat64(payload)])
5286                .collect::<Vec<_>>(),
5287        );
5288    }
5289
5290    #[cfg(feature = "sql")]
5291    #[test]
5292    fn secondary_ordered_covering_limit_fails_on_an_accessed_missing_row() {
5293        let session = initialize_with_composite_payload_index();
5294        let first = insert_exact_key_fixture(&session, 10);
5295        insert_exact_key_fixture(&session, 20);
5296        let raw_key =
5297            DecodedDataStoreKey::try_from_structural_key(ENTITY_TAG, &Value::Nat64(first))
5298                .expect("missing-row fixture key should decode")
5299                .to_raw()
5300                .expect("missing-row fixture key should encode");
5301        let store = session
5302            .db
5303            .store_handle(STORE_PATH)
5304            .expect("missing-row fixture store should resolve");
5305        assert!(
5306            store.with_data_mut(|data| data.remove(&raw_key)).is_some(),
5307            "fixture must remove only the authoritative row",
5308        );
5309
5310        let error = session
5311            .execute_trusted_sql_query(
5312                "SELECT payload FROM IdentityRow ORDER BY payload ASC, id ASC LIMIT 1",
5313            )
5314            .expect_err("an accessed accepted-index row must remain fail-closed");
5315        assert!(matches!(
5316            error,
5317            crate::db::QueryError::Execute(QueryExecutionError::Corruption(_))
5318        ));
5319    }
5320
5321    #[cfg(feature = "sql")]
5322    #[test]
5323    fn secondary_indexed_max_uses_one_descending_edge_across_ties() {
5324        let session = initialize_with_composite_payload_index();
5325        let mut inserted = Vec::new();
5326        for payload in [30, 10, 20, 20, 40, 40] {
5327            inserted.push(insert_exact_key_fixture(&session, payload));
5328        }
5329
5330        let sql = "SELECT MAX(payload) FROM IdentityRow";
5331        let data_reads_before = DataStore::current_get_call_count();
5332        let index_reads_before = IndexStore::current_entry_read_count();
5333        assert_eq!(
5334            sql_projection_rows(&session, sql),
5335            vec![vec![OutputValue::nat64(40)]],
5336        );
5337        assert_eq!(DataStore::current_get_call_count() - data_reads_before, 1);
5338        assert!(IndexStore::current_entry_read_count() - index_reads_before <= 1);
5339
5340        let range_sql = "SELECT MAX(payload) FROM IdentityRow WHERE payload < 40";
5341        let data_reads_before = DataStore::current_get_call_count();
5342        let index_reads_before = IndexStore::current_entry_read_count();
5343        assert_eq!(
5344            sql_projection_rows(&session, range_sql),
5345            vec![vec![OutputValue::nat64(30)]],
5346        );
5347        assert_eq!(DataStore::current_get_call_count() - data_reads_before, 1);
5348        assert!(IndexStore::current_entry_read_count() - index_reads_before <= 1);
5349
5350        let last = inserted
5351            .last()
5352            .copied()
5353            .expect("secondary MAX fixture should retain its last identity");
5354        let raw_key = DecodedDataStoreKey::try_from_structural_key(ENTITY_TAG, &Value::Nat64(last))
5355            .expect("missing-row fixture key should decode")
5356            .to_raw()
5357            .expect("missing-row fixture key should encode");
5358        let store = session
5359            .db
5360            .store_handle(STORE_PATH)
5361            .expect("missing-row fixture store should resolve");
5362        assert!(
5363            store.with_data_mut(|data| data.remove(&raw_key)).is_some(),
5364            "fixture must remove only the descending edge row",
5365        );
5366
5367        let error = session
5368            .execute_trusted_sql_query("SELECT MAX(payload) FROM IdentityRow")
5369            .expect_err("an accessed accepted-index row must remain fail-closed");
5370        assert!(matches!(
5371            error,
5372            crate::db::QueryError::Execute(QueryExecutionError::Corruption(_))
5373        ));
5374    }
5375
5376    #[cfg(feature = "sql")]
5377    #[test]
5378    fn secondary_indexed_max_upper_range_fails_on_an_accessed_missing_row() {
5379        let session = initialize_with_composite_payload_index();
5380        let upper_range_edge = insert_exact_key_fixture(&session, 30);
5381        for payload in [10, 20, 40] {
5382            insert_exact_key_fixture(&session, payload);
5383        }
5384        let raw_key = DecodedDataStoreKey::try_from_structural_key(
5385            ENTITY_TAG,
5386            &Value::Nat64(upper_range_edge),
5387        )
5388        .expect("missing-row fixture key should decode")
5389        .to_raw()
5390        .expect("missing-row fixture key should encode");
5391        let store = session
5392            .db
5393            .store_handle(STORE_PATH)
5394            .expect("missing-row fixture store should resolve");
5395        assert!(
5396            store.with_data_mut(|data| data.remove(&raw_key)).is_some(),
5397            "fixture must remove only the upper-range edge row",
5398        );
5399
5400        let error = session
5401            .execute_trusted_sql_query("SELECT MAX(payload) FROM IdentityRow WHERE payload < 40")
5402            .expect_err("an accessed upper-range edge row must remain fail-closed");
5403        assert!(matches!(
5404            error,
5405            crate::db::QueryError::Execute(QueryExecutionError::Corruption(_))
5406        ));
5407    }
5408
5409    #[test]
5410    fn exact_counts_use_entity_and_bounded_index_metadata_without_physical_reads() {
5411        let session = initialize();
5412        for payload in [10, 10, 20] {
5413            insert_exact_key_fixture(&session, payload);
5414        }
5415        let binding = exact_key_binding(&session);
5416        let entity = DynamicQuery::new(ENTITY_NAME);
5417        let tens =
5418            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FieldRef::new("payload").eq(10_u64));
5419        let selected = DynamicQuery::new(ENTITY_NAME)
5420            .filter(crate::db::FieldRef::new("payload").in_list([10_u64, 10, 20, 99]));
5421        let missing =
5422            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FieldRef::new("payload").eq(99_u64));
5423        let data_reads_before = DataStore::current_get_call_count();
5424        let index_reads_before = IndexStore::current_entry_read_count();
5425
5426        assert_eq!(session.execute_public_exact_count(&entity).unwrap(), 3);
5427        assert_eq!(session.execute_public_exact_count(&tens).unwrap(), 2);
5428        assert_eq!(session.execute_public_exact_count(&selected).unwrap(), 3);
5429        assert_eq!(session.execute_public_exact_count(&missing).unwrap(), 0);
5430        assert_eq!(
5431            session
5432                .execute_public_exact_count_for_typed_binding(&binding, &tens)
5433                .unwrap(),
5434            Some(2),
5435        );
5436        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5437        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5438
5439        session
5440            .execute_trusted_dynamic_insert_batch(
5441                ENTITY_NAME,
5442                (0..64).map(|_| dynamic_payload_patch(10)).collect(),
5443            )
5444            .expect("a larger matching population should commit");
5445        let data_reads_before = DataStore::current_get_call_count();
5446        let index_reads_before = IndexStore::current_entry_read_count();
5447        assert_eq!(session.execute_public_exact_count(&tens).unwrap(), 66);
5448        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5449        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5450    }
5451
5452    #[test]
5453    fn exact_count_accepts_the_leading_field_of_a_composite_user_index() {
5454        let session = initialize_with_composite_payload_index();
5455        for payload in [10, 10, 20] {
5456            insert_exact_key_fixture(&session, payload);
5457        }
5458        let tens =
5459            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FieldRef::new("payload").eq(10_u64));
5460        let selected = DynamicQuery::new(ENTITY_NAME)
5461            .filter(crate::db::FieldRef::new("payload").in_list([10_u64, 20, 99]));
5462        let data_reads_before = DataStore::current_get_call_count();
5463        let index_reads_before = IndexStore::current_entry_read_count();
5464
5465        assert_eq!(session.execute_public_exact_count(&tens).unwrap(), 2);
5466        assert_eq!(session.execute_public_exact_count(&selected).unwrap(), 3);
5467        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5468        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5469    }
5470
5471    #[cfg(feature = "sql")]
5472    #[test]
5473    fn exact_count_shared_executor_preserves_sql_direct_count_results() {
5474        let session = initialize();
5475        let data_reads_before = DataStore::current_get_call_count();
5476        let crate::db::SqlStatementResult::Projection { rows, .. } = session
5477            .execute_trusted_sql_query("SELECT COUNT(*) FROM IdentityRow")
5478            .expect("empty SQL direct count should succeed")
5479        else {
5480            panic!("empty SQL direct count should return one projection row")
5481        };
5482        assert_eq!(rows, vec![vec![OutputValue::nat64(0)]]);
5483        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5484
5485        for payload in [10, 10, 20] {
5486            insert_exact_key_fixture(&session, payload);
5487        }
5488
5489        let data_reads_before = DataStore::current_get_call_count();
5490        let index_reads_before = IndexStore::current_entry_read_count();
5491        for sql in [
5492            "SELECT COUNT(*) FROM IdentityRow",
5493            "SELECT COUNT(payload) FROM IdentityRow",
5494            "SELECT COUNT(1) FROM IdentityRow",
5495            "SELECT COUNT(*) FROM IdentityRow WHERE true",
5496            "SELECT COUNT(*) FROM IdentityRow WHERE payload IN (10, 10, 20, 99)",
5497        ] {
5498            let crate::db::SqlStatementResult::Projection { rows, .. } = session
5499                .execute_trusted_sql_query(sql)
5500                .expect("SQL direct count should use the shared exact executor")
5501            else {
5502                panic!("SQL direct count should return one projection row")
5503            };
5504            assert_eq!(rows, vec![vec![OutputValue::nat64(3)]], "{sql}");
5505        }
5506        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5507        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5508
5509        let crate::db::SqlStatementResult::Projection { rows, .. } = session
5510            .execute_trusted_sql_query("SELECT COUNT(*) FROM IdentityRow WHERE payload = 10")
5511            .expect("nontrivial exact-prefix count should preserve its predicate")
5512        else {
5513            panic!("nontrivial exact-prefix count should return one projection row")
5514        };
5515        assert_eq!(rows, vec![vec![OutputValue::nat64(2)]]);
5516        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5517        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5518
5519        let data_reads_before = DataStore::current_get_call_count();
5520        for (sql, expected) in [
5521            ("SELECT COUNT(*) FROM IdentityRow WHERE false", 0_u64),
5522            ("SELECT COUNT(*) FROM IdentityRow WHERE id = 1", 1),
5523        ] {
5524            let crate::db::SqlStatementResult::Projection { rows, .. } = session
5525                .execute_trusted_sql_query(sql)
5526                .expect("non-entity count control should succeed")
5527            else {
5528                panic!("non-entity count control should return one projection row")
5529            };
5530            assert_eq!(rows, vec![vec![OutputValue::nat64(expected)]], "{sql}");
5531        }
5532        assert!(DataStore::current_get_call_count() > data_reads_before);
5533
5534        session
5535            .execute_trusted_sql_query("SELECT COUNT(*) FROM IdentityRow LIMIT 1")
5536            .expect_err("unordered aggregate input pagination must remain rejected");
5537
5538        let data_reads_before = DataStore::current_get_call_count();
5539        let crate::db::SqlStatementResult::Projection { rows, .. } = session
5540            .execute_trusted_sql_query("SELECT COUNT(DISTINCT payload) FROM IdentityRow")
5541            .expect("distinct count should retain prepared execution")
5542        else {
5543            panic!("distinct count should return one projection row")
5544        };
5545        assert_eq!(rows, vec![vec![OutputValue::nat64(2)]]);
5546        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5547    }
5548
5549    #[cfg(feature = "sql")]
5550    #[test]
5551    fn exact_count_composite_prefix_admits_seventeen_canonical_keys_only() {
5552        let session = initialize_with_composite_payload_index();
5553        for payload in [10, 10, 20] {
5554            insert_exact_key_fixture(&session, payload);
5555        }
5556        let ids_at_count_cap = (1_u64..=17)
5557            .map(|id| id.to_string())
5558            .collect::<Vec<_>>()
5559            .join(", ");
5560        let at_count_cap_sql = format!(
5561            "SELECT COUNT(*) FROM IdentityRow WHERE payload = 10 AND id IN ({ids_at_count_cap})",
5562        );
5563        let data_reads_before = DataStore::current_get_call_count();
5564        let index_reads_before = IndexStore::current_entry_read_count();
5565        assert_eq!(
5566            sql_projection_rows(&session, at_count_cap_sql.as_str()),
5567            vec![vec![OutputValue::nat64(2)]],
5568        );
5569        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5570        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5571
5572        let authored_duplicate_sql = format!(
5573            "SELECT COUNT(*) FROM IdentityRow WHERE payload = 10 AND id IN (1, {ids_at_count_cap})",
5574        );
5575        assert_eq!(
5576            sql_projection_rows(&session, authored_duplicate_sql.as_str()),
5577            vec![vec![OutputValue::nat64(2)]],
5578        );
5579        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5580        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5581
5582        let ids_over_count_cap = format!("{ids_at_count_cap}, 18");
5583        let over_count_cap_sql = format!(
5584            "SELECT COUNT(*) FROM IdentityRow WHERE payload = 10 AND id IN ({ids_over_count_cap})",
5585        );
5586        assert_eq!(
5587            sql_projection_rows(&session, over_count_cap_sql.as_str()),
5588            vec![vec![OutputValue::nat64(2)]],
5589        );
5590        assert!(DataStore::current_get_call_count() > data_reads_before);
5591    }
5592
5593    #[cfg(feature = "sql")]
5594    #[test]
5595    fn exact_count_nullable_field_uses_prepared_borrowed_primary_scan() {
5596        let session = initialize_with_snapshot(identity_snapshot_with_nullable_payload(STORE_PATH));
5597        session
5598            .execute_trusted_dynamic_insert_batch(
5599                ENTITY_NAME,
5600                vec![
5601                    dynamic_payload_patch(10),
5602                    DynamicStructuralPatch::new(Vec::new()),
5603                ],
5604            )
5605            .expect("nullable count fixture should insert");
5606
5607        let data_reads_before = DataStore::current_get_call_count();
5608        let crate::db::SqlStatementResult::Projection { rows, .. } = session
5609            .execute_trusted_sql_query("SELECT COUNT(payload) FROM IdentityRow")
5610            .expect("nullable count should retain prepared execution")
5611        else {
5612            panic!("nullable count should return one projection row")
5613        };
5614        assert_eq!(rows, vec![vec![OutputValue::nat64(1)]]);
5615        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5616    }
5617
5618    #[cfg(feature = "sql")]
5619    #[test]
5620    fn indexed_extrema_nullable_field_uses_prepared_borrowed_primary_scan() {
5621        let session = initialize_with_snapshot(identity_snapshot_with_nullable_payload(STORE_PATH));
5622        session
5623            .execute_trusted_dynamic_insert_batch(
5624                ENTITY_NAME,
5625                vec![DynamicStructuralPatch::new(Vec::new())],
5626            )
5627            .expect("all-null extrema fixture should insert");
5628
5629        for sql in [
5630            "SELECT MIN(payload) FROM IdentityRow",
5631            "SELECT MAX(payload) FROM IdentityRow",
5632        ] {
5633            let data_reads_before = DataStore::current_get_call_count();
5634            let crate::db::SqlStatementResult::Projection { rows, .. } = session
5635                .execute_trusted_sql_query(sql)
5636                .expect("all-null extrema should retain complete reduction")
5637            else {
5638                panic!("all-null extrema should return one projection row")
5639            };
5640            assert_eq!(rows, vec![vec![OutputValue::null()]], "{sql}");
5641            assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5642        }
5643
5644        session
5645            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(10)])
5646            .expect("mixed nullable extrema fixture should insert");
5647
5648        for sql in [
5649            "SELECT MIN(payload) FROM IdentityRow",
5650            "SELECT MAX(payload) FROM IdentityRow",
5651        ] {
5652            let data_reads_before = DataStore::current_get_call_count();
5653            let crate::db::SqlStatementResult::Projection { rows, .. } = session
5654                .execute_trusted_sql_query(sql)
5655                .expect("mixed nullable extrema should retain complete reduction")
5656            else {
5657                panic!("mixed nullable extrema should return one projection row")
5658            };
5659            assert_eq!(rows, vec![vec![OutputValue::nat64(10)]], "{sql}");
5660            assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5661        }
5662    }
5663
5664    #[test]
5665    fn exact_count_rejects_non_metadata_shapes_without_physical_reads() {
5666        let session = initialize();
5667        insert_exact_key_fixture(&session, 10);
5668        let rejected = [
5669            DynamicQuery::new(ENTITY_NAME).limit(1),
5670            DynamicQuery::new(ENTITY_NAME).select(["payload"]),
5671            DynamicQuery::new(ENTITY_NAME).order_by(crate::db::asc("payload")),
5672            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FieldRef::new("id").eq(1_u64)),
5673            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FilterExpr::and(vec![
5674                crate::db::FieldRef::new("payload").eq(10_u64),
5675                crate::db::FieldRef::new("id").eq(1_u64),
5676            ])),
5677            DynamicQuery::new(ENTITY_NAME)
5678                .filter(crate::db::FieldRef::new("payload").in_list(0_u64..=16)),
5679        ];
5680        let data_reads_before = DataStore::current_get_call_count();
5681        let index_reads_before = IndexStore::current_entry_read_count();
5682        for request in rejected {
5683            let error = session
5684                .execute_public_exact_count(&request)
5685                .expect_err("unsupported count shape must reject");
5686            assert_eq!(
5687                error.diagnostic().error_code(),
5688                icydb_diagnostic_code::ErrorCode::RUNTIME_UNSUPPORTED,
5689            );
5690            assert!(matches!(
5691                error,
5692                crate::db::QueryError::Execute(QueryExecutionError::Unsupported(_)),
5693            ));
5694        }
5695        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5696        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5697    }
5698
5699    #[test]
5700    fn exact_count_unavailable_metadata_has_a_typed_diagnostic_without_physical_reads() {
5701        let journaled = initialize_journaled();
5702        insert_exact_key_fixture(&journaled, 10);
5703        let binding = exact_key_binding(&journaled);
5704        let requests = [
5705            DynamicQuery::new(ENTITY_NAME),
5706            DynamicQuery::new(ENTITY_NAME).filter(crate::db::FieldRef::new("payload").eq(10_u64)),
5707        ];
5708        let data_reads_before = DataStore::current_get_call_count();
5709        let index_reads_before = IndexStore::current_entry_read_count();
5710        for request in requests {
5711            let dynamic = journaled
5712                .execute_public_exact_count(&request)
5713                .expect_err("journal overlay has no exact metadata");
5714            let typed = journaled
5715                .execute_public_exact_count_for_typed_binding(&binding, &request)
5716                .expect_err("typed binding must retain unavailable-metadata diagnostic");
5717            for error in [dynamic, typed] {
5718                let diagnostic = error.diagnostic();
5719                assert_eq!(
5720                    diagnostic.error_code(),
5721                    icydb_diagnostic_code::ErrorCode::QUERY_EXACT_COUNT_METADATA_UNAVAILABLE,
5722                );
5723                assert_eq!(
5724                    diagnostic.class(),
5725                    icydb_diagnostic_code::ErrorClass::Unsupported
5726                );
5727                assert_eq!(
5728                    diagnostic.origin(),
5729                    icydb_diagnostic_code::ErrorOrigin::Query
5730                );
5731                assert!(matches!(
5732                    error,
5733                    crate::db::QueryError::Execute(QueryExecutionError::Unsupported(_)),
5734                ));
5735            }
5736        }
5737        assert_eq!(DataStore::current_get_call_count(), data_reads_before);
5738        assert_eq!(IndexStore::current_entry_read_count(), index_reads_before);
5739    }
5740
5741    #[test]
5742    fn exact_count_typed_binding_fails_closed_after_accepted_revision_changes() {
5743        let session = initialize();
5744        let binding = exact_key_binding(&session);
5745        let request = DynamicQuery::new(ENTITY_NAME);
5746        assert_eq!(
5747            session
5748                .execute_public_exact_count_for_typed_binding(&binding, &request)
5749                .unwrap(),
5750            Some(0),
5751        );
5752
5753        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
5754            STORE_PATH,
5755            AcceptedSchemaRevision::new(2),
5756            BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH, false))]),
5757            BTreeMap::from([
5758                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
5759                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
5760            ]),
5761        );
5762        let store = session
5763            .db
5764            .store_handle(STORE_PATH)
5765            .expect("exact-count store should resolve");
5766        crate::db::commit::publish_accepted_schema_candidate(
5767            STORE_PATH,
5768            store,
5769            AcceptedSchemaRevision::INITIAL,
5770            &candidate,
5771        )
5772        .expect("successor accepted schema should publish");
5773
5774        assert_eq!(
5775            session
5776                .execute_public_exact_count_for_typed_binding(&binding, &request)
5777                .unwrap(),
5778            None,
5779        );
5780    }
5781
5782    #[cfg(feature = "sql")]
5783    fn identity_row_stored_bytes<C: CanisterKind>(
5784        session: &DbSession<C>,
5785        store_path: &'static str,
5786        key: u64,
5787    ) -> u64 {
5788        let data_key = DecodedDataStoreKey::try_from_structural_key(ENTITY_TAG, &Value::Nat64(key))
5789            .expect("identity row key should encode");
5790        let raw_key = data_key.to_raw().expect("identity raw key should encode");
5791        let store = session
5792            .db
5793            .recovered_store(store_path)
5794            .expect("identity store should resolve");
5795        store.with_data(|data_store| {
5796            u64::try_from(
5797                data_store
5798                    .get(&raw_key)
5799                    .expect("inserted identity row should exist")
5800                    .len(),
5801            )
5802            .expect("bounded row length should fit u64")
5803        })
5804    }
5805
5806    #[cfg(feature = "sql")]
5807    fn with_stored_bytes_limit<T>(
5808        limit: u64,
5809        shape_fingerprint_prefix: u64,
5810        operation: impl FnOnce() -> Result<T, crate::db::query::intent::QueryError>,
5811    ) -> Result<T, crate::db::query::intent::QueryError> {
5812        let budget = HardExecutionBudget::uniform_for_tests(
5813            u64::MAX,
5814            HardExecutionFailureHeadroom::new(500, 256),
5815        )
5816        .with_limit_for_tests(
5817            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::StoredBytesRead,
5818            limit,
5819        );
5820        let context = HardExecutionContext::new(
5821            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
5822            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
5823            shape_fingerprint_prefix,
5824        );
5825
5826        with_query_execution_budget_for_tests(budget, context, operation)
5827    }
5828
5829    #[cfg(feature = "sql")]
5830    fn advance_with_exhausted_mutation_predicate_budget(
5831        session: &DbSession<JournaledTestCanister>,
5832        request: &MutationJobAdvanceRequest,
5833    ) -> Result<crate::db::MutationJobAdvanceReceipt, MutationJobError> {
5834        let budget = HardExecutionBudget::uniform_for_tests(
5835            u64::MAX,
5836            HardExecutionFailureHeadroom::new(1_000_000_000, 64 * 1_024),
5837        )
5838        .with_limit_for_tests(
5839            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
5840            0,
5841        );
5842        let context = HardExecutionContext::new(
5843            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
5844            icydb_diagnostic_code::DiagnosticExecutionLane::Mutation,
5845            0x6d75_7461_7465_7465,
5846        );
5847        with_execution_budget_for_tests(
5848            budget,
5849            context,
5850            || session.advance_trusted_mutation_job(request),
5851            |_| MutationJobError::Internal,
5852        )
5853    }
5854
5855    #[cfg(feature = "sql")]
5856    const fn exact_key(value: u64) -> PrimaryKeyValue {
5857        PrimaryKeyValue::Scalar(PrimaryKeyComponent::Nat64(value))
5858    }
5859
5860    #[cfg(feature = "sql")]
5861    fn assert_exact_key_batch<C: CanisterKind>(session: &DbSession<C>) {
5862        let first = insert_exact_key_fixture(session, 41);
5863        let second = insert_exact_key_fixture(session, 42);
5864        let missing = u64::MAX;
5865        let binding = exact_key_binding(session);
5866        let gets_before = DataStore::current_get_call_count();
5867        let result = session
5868            .execute_public_exact_key_batch_for_typed_binding(
5869                &binding,
5870                &[
5871                    exact_key(second),
5872                    exact_key(missing),
5873                    exact_key(first),
5874                    exact_key(second),
5875                ],
5876            )
5877            .expect("exact-key batch should execute")
5878            .expect("exact-key binding should remain current");
5879
5880        assert_eq!(result.positions, vec![0, 1, 2, 0]);
5881        assert_eq!(
5882            result.distinct_rows,
5883            vec![
5884                Some(expected_dynamic_row(second, 42)),
5885                None,
5886                Some(expected_dynamic_row(first, 41)),
5887            ],
5888        );
5889        assert_eq!(
5890            DataStore::current_get_call_count().saturating_sub(gets_before),
5891            3,
5892            "four input positions with one duplicate must perform three physical reads",
5893        );
5894    }
5895
5896    #[cfg(feature = "sql")]
5897    #[test]
5898    fn exact_key_batches_preserve_semantics_across_heap_and_journaled_stores() {
5899        assert_exact_key_batch(&initialize());
5900        assert_exact_key_batch(&initialize_journaled());
5901    }
5902
5903    #[cfg(feature = "sql")]
5904    fn assert_primary_range_materialization_fetches_once<C: CanisterKind>(
5905        session: &DbSession<C>,
5906        store_path: &'static str,
5907    ) {
5908        let key = insert_exact_key_fixture(session, 41);
5909        let stored_bytes = identity_row_stored_bytes(session, store_path, key);
5910
5911        let scalar = DynamicQuery::new(ENTITY_NAME)
5912            .select(["id", "payload"])
5913            .order_by(asc("id"))
5914            .limit(1);
5915        let gets_before = DataStore::current_get_call_count();
5916        let scalar_page = with_stored_bytes_limit(stored_bytes, 0x7072_696d_6172_792d, || {
5917            session.execute_trusted_live_page(&scalar, None)
5918        })
5919        .expect("one scalar primary-range row should fit one payload-read allowance");
5920        assert_eq!(scalar_page.row_count, 1);
5921        assert_eq!(
5922            DataStore::current_get_call_count().saturating_sub(gets_before),
5923            1,
5924            "scalar primary traversal should fetch its emitted row exactly once",
5925        );
5926
5927        let grouped = DynamicQuery::new(ENTITY_NAME)
5928            .group_by("payload")
5929            .aggregate(crate::db::count())
5930            .grouped_limits(10, 16 * 1_024)
5931            .limit(1);
5932        let gets_before = DataStore::current_get_call_count();
5933        let grouped_page = with_stored_bytes_limit(stored_bytes, 0x6772_6f75_7065_642d, || {
5934            session.execute_trusted_dynamic_grouped_query(&grouped)
5935        })
5936        .expect("one grouped primary-range row should fit one payload-read allowance");
5937        assert_eq!(grouped_page.row_count, 1);
5938        assert_eq!(
5939            DataStore::current_get_call_count().saturating_sub(gets_before),
5940            1,
5941            "grouped primary traversal should fetch its source row exactly once",
5942        );
5943    }
5944
5945    #[cfg(feature = "sql")]
5946    #[test]
5947    fn row_materialization_fetches_each_required_payload_at_most_once() {
5948        assert_primary_range_materialization_fetches_once(&initialize(), STORE_PATH);
5949        assert_primary_range_materialization_fetches_once(
5950            &initialize_journaled(),
5951            JOURNALED_STORE_PATH,
5952        );
5953    }
5954
5955    #[cfg(feature = "sql")]
5956    #[test]
5957    fn ordered_grouped_pages_close_a_group_spanning_physical_refills_before_resume() {
5958        let session = initialize();
5959        let mut patches = Vec::new();
5960        for _ in 0..70 {
5961            patches.push(dynamic_payload_patch(10));
5962        }
5963        for _ in 0..3 {
5964            patches.push(dynamic_payload_patch(20));
5965        }
5966        patches.push(dynamic_payload_patch(30));
5967        let inserted = session
5968            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, patches)
5969            .expect("ordered grouped continuation rows should insert");
5970        assert_eq!(inserted.rows.len(), 74);
5971
5972        let query = DynamicQuery::new(ENTITY_NAME)
5973            .group_by("payload")
5974            .aggregate(crate::db::count())
5975            .aggregate(crate::db::sum("id"))
5976            .order_by(asc("payload"))
5977            .grouped_limits(4, 16 * 1_024)
5978            .limit(1);
5979        let expected = [
5980            (10_u64, 70_u64, crate::types::Decimal::new(2_485, 0)),
5981            (20, 3, crate::types::Decimal::new(216, 0)),
5982            (30, 1, crate::types::Decimal::new(74, 0)),
5983        ];
5984        let mut continuation: Option<String> = None;
5985        let mut seen_cursors = std::collections::BTreeSet::new();
5986
5987        for (page_index, (group_key, row_count, id_sum)) in expected.into_iter().enumerate() {
5988            let request = continuation.as_ref().map_or_else(
5989                || query.clone(),
5990                |cursor| query.clone().cursor(cursor.clone()),
5991            );
5992            let entries_before = IndexStore::current_entry_read_count();
5993            let rows_before = DataStore::current_get_call_count();
5994            let page = session
5995                .execute_trusted_dynamic_grouped_query(&request)
5996                .unwrap_or_else(|error| {
5997                    panic!("ordered grouped page {page_index} should execute: {error:?}")
5998                });
5999            let entries_read =
6000                IndexStore::current_entry_read_count().saturating_sub(entries_before);
6001            let rows_read = DataStore::current_get_call_count().saturating_sub(rows_before);
6002
6003            assert_eq!(page.row_count, 1);
6004            let [row] = page.rows.as_slice() else {
6005                panic!("ordered grouped page must contain exactly one closed group")
6006            };
6007            assert_eq!(row.group_key(), &[OutputValue::nat64(group_key)]);
6008            assert_eq!(
6009                row.aggregate_values(),
6010                &[OutputValue::nat64(row_count), OutputValue::decimal(id_sum),],
6011            );
6012            if page_index == 0 {
6013                assert!(
6014                    entries_read.saturating_add(rows_read) >= 70,
6015                    "the first closed group must span the maintained 64-entry physical refill",
6016                );
6017            }
6018
6019            continuation = page.next_cursor;
6020            if page_index + 1 < expected.len() {
6021                let cursor = continuation
6022                    .as_ref()
6023                    .expect("another closed group should retain continuation");
6024                assert!(
6025                    seen_cursors.insert(cursor.clone()),
6026                    "ordered grouped continuation must advance monotonically",
6027                );
6028            } else {
6029                assert_eq!(continuation, None);
6030            }
6031        }
6032    }
6033
6034    #[cfg(feature = "sql")]
6035    #[test]
6036    fn exhaustive_pages_require_and_recompare_the_complete_source_proof() {
6037        let session = initialize();
6038        let first = insert_exact_key_fixture(&session, 41);
6039        let second = insert_exact_key_fixture(&session, 42);
6040        let third = insert_exact_key_fixture(&session, 43);
6041        let query = DynamicQuery::new(ENTITY_NAME)
6042            .select(["id", "payload"])
6043            .order_by(asc("id"));
6044
6045        let page = session
6046            .execute_trusted_exhaustive_page(&query, None, None)
6047            .expect("initial exhaustive page should capture its source proof");
6048        assert_eq!(
6049            page.rows,
6050            vec![
6051                expected_dynamic_row(first, 41),
6052                expected_dynamic_row(second, 42),
6053            ],
6054        );
6055        let continuation = page
6056            .continuation
6057            .as_deref()
6058            .expect("unreturned row should retain exhaustive continuation");
6059        assert!(matches!(
6060            session.execute_trusted_exhaustive_page(&query, Some(continuation), None),
6061            Err(ExhaustiveReadError::Revision(
6062                ReadSetRevisionError::ResumeProofRequired
6063            )),
6064        ));
6065        let resumed = session
6066            .execute_trusted_exhaustive_page(&query, Some(continuation), Some(&page.proof))
6067            .expect("unchanged proof should resume exhaustive traversal");
6068        assert_eq!(resumed.rows, vec![expected_dynamic_row(third, 43)]);
6069        assert_eq!(resumed.continuation, None);
6070
6071        let stale_page = session
6072            .execute_trusted_exhaustive_page(&query, None, None)
6073            .expect("fresh exhaustive page should capture current revision");
6074        let stale_continuation = stale_page
6075            .continuation
6076            .as_deref()
6077            .expect("fresh three-row traversal should retain continuation");
6078        let _ = insert_exact_key_fixture(&session, 44);
6079        assert!(matches!(
6080            session.execute_trusted_exhaustive_page(
6081                &query,
6082                Some(stale_continuation),
6083                Some(&stale_page.proof),
6084            ),
6085            Err(ExhaustiveReadError::Revision(
6086                ReadSetRevisionError::StoreDataChanged { .. }
6087            )),
6088        ));
6089    }
6090
6091    #[cfg(feature = "sql")]
6092    #[test]
6093    fn heap_sources_cannot_back_durable_resumable_jobs() {
6094        let session = initialize();
6095        let proof = session
6096            .capture_read_set_revision_proof(&[ENTITY_NAME])
6097            .expect("heap source proof should capture for one-call exhaustive reads");
6098        let job_id = ResumableJobId::try_from_bytes([70; 32])
6099            .expect("nonzero heap test job identity should admit");
6100
6101        assert!(matches!(
6102            session.start_resumable_job(job_id, proof, Vec::new()),
6103            Err(ResumableJobError::SourceProof(
6104                ReadSetRevisionError::DurableStoreRequired { .. }
6105            )),
6106        ));
6107    }
6108
6109    #[cfg(feature = "sql")]
6110    #[test]
6111    fn proof_and_progress_controls_charge_one_shared_request_scope() {
6112        let (session, root) = initialize_journaled_with_root();
6113        let resource = icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions;
6114        let before = root.observed(resource);
6115        let proof = session
6116            .capture_read_set_revision_proof(&[ENTITY_NAME])
6117            .expect("proof capture should use the retained request scope");
6118        let job_id = ResumableJobId::try_from_bytes([75; 32])
6119            .expect("nonzero accounting job identity should admit");
6120        session
6121            .start_resumable_job(job_id, proof, Vec::new())
6122            .expect("job start should use the same retained request scope");
6123        let _ = session
6124            .resumable_job_state(job_id)
6125            .expect("job load should use the same retained request scope");
6126
6127        assert_eq!(root.observed(resource).saturating_sub(before), 3);
6128    }
6129
6130    #[cfg(feature = "sql")]
6131    #[test]
6132    fn source_proofs_ignore_unrelated_stores_but_bind_access_state_changes() {
6133        let session = initialize();
6134        let proof = session
6135            .capture_read_set_revision_proof(&[ENTITY_NAME])
6136            .expect("source proof should cover only the entity's physical store");
6137        let shared_store_proof = session
6138            .capture_read_set_revision_proof(&[ENTITY_NAME, ENTITY_NAME])
6139            .expect("entities sharing one physical source should deduplicate");
6140        assert_eq!(shared_store_proof, proof);
6141        assert_eq!(shared_store_proof.stores().len(), 1);
6142        let unrelated = session
6143            .db
6144            .store_handle(UNRELATED_STORE_PATH)
6145            .expect("unrelated registered store should resolve");
6146        unrelated.with_data_mut(|store| {
6147            let _ = store.remove(&RawDataStoreKey::from_persisted_bytes(vec![1]));
6148        });
6149        session
6150            .verify_read_set_revision_proof(&proof)
6151            .expect("a nonparticipating store mutation must not invalidate the proof");
6152
6153        let source = session
6154            .db
6155            .store_handle(STORE_PATH)
6156            .expect("participating source store should resolve");
6157        source
6158            .mark_index_building()
6159            .expect("source access-state transition should advance its revision");
6160        assert!(matches!(
6161            session.verify_read_set_revision_proof(&proof),
6162            Err(ExhaustiveReadError::Revision(
6163                ReadSetRevisionError::StoreAccessChanged { .. }
6164            )),
6165        ));
6166    }
6167
6168    #[cfg(feature = "sql")]
6169    #[expect(
6170        clippy::too_many_lines,
6171        reason = "one lifecycle test proves successful replay plus pre-page and post-page source invalidation without sharing progress state across tests"
6172    )]
6173    #[test]
6174    fn journaled_job_advance_is_idempotent_and_revision_checked_on_both_sides() {
6175        let session = initialize_journaled();
6176        let proof = session
6177            .capture_read_set_revision_proof(&[ENTITY_NAME])
6178            .expect("journaled source proof should capture");
6179        let job_id =
6180            ResumableJobId::try_from_bytes([71; 32]).expect("nonzero job identity should admit");
6181        session
6182            .start_resumable_job(job_id, proof, vec![0])
6183            .expect("journaled job should start outside its protected source revision");
6184        let request = ResumableJobAdvanceRequest::new(
6185            job_id,
6186            0,
6187            ResumableJobIdempotencyKey::new("page-0")
6188                .expect("bounded idempotency key should admit"),
6189        );
6190        let calls = Cell::new(0_u8);
6191        let receipt = session
6192            .compare_proof_and_advance(&request, |state| {
6193                calls.set(calls.get() + 1);
6194                assert_eq!(state.application_state, vec![0]);
6195                Ok::<_, ()>(
6196                    ResumableJobAdvance::new(Some("cursor-1".to_string()), vec![1], vec![9])
6197                        .expect("bounded application advance should admit"),
6198                )
6199            })
6200            .expect("unchanged source should advance exactly once");
6201        assert_eq!(calls.get(), 1);
6202        assert_eq!(receipt.status, ResumableJobAdvanceStatus::Advanced);
6203        assert_eq!(receipt.committed_sequence, 1);
6204
6205        let replay = session
6206            .compare_proof_and_advance::<()>(&request, |_| {
6207                panic!("lost-response replay must not execute application work")
6208            })
6209            .expect("same request identity should return its persisted receipt");
6210        assert_eq!(replay, receipt);
6211        let retained = session
6212            .resumable_job_state(job_id)
6213            .expect("advanced state should remain durable");
6214        assert_eq!(retained.sequence, 1);
6215        assert_eq!(retained.application_state, vec![1]);
6216
6217        let _ = insert_exact_key_fixture(&session, 51);
6218        let pre_change_request = ResumableJobAdvanceRequest::new(
6219            job_id,
6220            1,
6221            ResumableJobIdempotencyKey::new("page-1")
6222                .expect("bounded idempotency key should admit"),
6223        );
6224        let pre_change_calls = Cell::new(0_u8);
6225        let invalidated = session
6226            .compare_proof_and_advance::<()>(&pre_change_request, |_| {
6227                pre_change_calls.set(pre_change_calls.get() + 1);
6228                unreachable!("pre-page proof failure must reject before application work")
6229            })
6230            .expect("source drift should persist one replayable invalidation receipt");
6231        assert_eq!(pre_change_calls.get(), 0);
6232        assert_eq!(invalidated.status, ResumableJobAdvanceStatus::Invalidated);
6233        let invalidated_state = session
6234            .resumable_job_state(job_id)
6235            .expect("invalidated job should remain inspectable");
6236        assert_eq!(invalidated_state.status, ResumableJobStatus::Invalidated);
6237        assert_eq!(invalidated_state.continuation, None);
6238        assert_eq!(invalidated_state.application_state, vec![1]);
6239        assert_eq!(
6240            session
6241                .compare_proof_and_advance::<()>(&pre_change_request, |_| {
6242                    panic!("invalidation replay must not execute application work")
6243                })
6244                .expect("lost invalidation reply should replay exactly"),
6245            invalidated,
6246        );
6247
6248        let post_proof = session
6249            .capture_read_set_revision_proof(&[ENTITY_NAME])
6250            .expect("post-change journaled proof should capture");
6251        let post_job_id = ResumableJobId::try_from_bytes([72; 32])
6252            .expect("nonzero post-change job identity should admit");
6253        session
6254            .start_resumable_job(post_job_id, post_proof, vec![7])
6255            .expect("post-change journaled job should start");
6256        let post_request = ResumableJobAdvanceRequest::new(
6257            post_job_id,
6258            0,
6259            ResumableJobIdempotencyKey::new("post-page-0")
6260                .expect("bounded idempotency key should admit"),
6261        );
6262        let post_receipt = session
6263            .compare_proof_and_advance::<()>(&post_request, |_| {
6264                let _ = insert_exact_key_fixture(&session, 52);
6265                Ok(ResumableJobAdvance::new(None, vec![8], vec![10])
6266                    .expect("bounded post-change candidate should admit"))
6267            })
6268            .expect("post-page drift should discard the candidate and persist invalidation");
6269        assert_eq!(post_receipt.status, ResumableJobAdvanceStatus::Invalidated);
6270        let post_state = session
6271            .resumable_job_state(post_job_id)
6272            .expect("post-page invalidation should remain inspectable");
6273        assert_eq!(post_state.status, ResumableJobStatus::Invalidated);
6274        assert_eq!(post_state.application_state, vec![7]);
6275        session
6276            .acknowledge_resumable_job(post_job_id, post_state.sequence)
6277            .expect("terminal job acknowledgement should remove retained progress");
6278        session
6279            .acknowledge_resumable_job(post_job_id, post_state.sequence)
6280            .expect("lost acknowledgement reply should be safely replayable");
6281        assert_eq!(
6282            session.resumable_job_state(post_job_id),
6283            Err(ResumableJobError::NotFound),
6284        );
6285
6286        let completed_job_id = ResumableJobId::try_from_bytes([74; 32])
6287            .expect("nonzero completed job identity should admit");
6288        let completed_proof = session
6289            .capture_read_set_revision_proof(&[ENTITY_NAME])
6290            .expect("completed-job source proof should capture");
6291        session
6292            .start_resumable_job(completed_job_id, completed_proof, Vec::new())
6293            .expect("completed-job fixture should start");
6294        let completed_request = ResumableJobAdvanceRequest::new(
6295            completed_job_id,
6296            0,
6297            ResumableJobIdempotencyKey::new("complete")
6298                .expect("bounded completion key should admit"),
6299        );
6300        let completed_receipt = session
6301            .compare_proof_and_advance::<()>(&completed_request, |_| {
6302                Ok(ResumableJobAdvance::new(None, vec![99], vec![100])
6303                    .expect("bounded terminal advance should admit"))
6304            })
6305            .expect("null continuation should commit terminal completion");
6306        let completed_state = session
6307            .resumable_job_state(completed_job_id)
6308            .expect("completed state should remain replayable before acknowledgement");
6309        assert_eq!(completed_state.status, ResumableJobStatus::Completed);
6310        assert_eq!(
6311            session
6312                .compare_proof_and_advance::<()>(&completed_request, |_| {
6313                    panic!("completed request replay must not execute application work")
6314                })
6315                .expect("completed request should replay until acknowledgement"),
6316            completed_receipt,
6317        );
6318        let after_completion = ResumableJobAdvanceRequest::new(
6319            completed_job_id,
6320            1,
6321            ResumableJobIdempotencyKey::new("after-complete")
6322                .expect("bounded post-completion key should admit"),
6323        );
6324        assert!(matches!(
6325            session.compare_proof_and_advance::<()>(&after_completion, |_| {
6326                panic!("completed jobs cannot execute another page")
6327            }),
6328            Err(CompareProofAndAdvanceError::Protocol(
6329                ResumableJobError::Completed
6330            )),
6331        ));
6332        session
6333            .acknowledge_resumable_job(completed_job_id, completed_state.sequence)
6334            .expect("completed job should acknowledge and free capacity");
6335        session
6336            .acknowledge_resumable_job(completed_job_id, completed_state.sequence)
6337            .expect("completion acknowledgement should be idempotent");
6338
6339        let stale_job_id = ResumableJobId::try_from_bytes([73; 32])
6340            .expect("nonzero stale-sequence job identity should admit");
6341        let stale_proof = session
6342            .capture_read_set_revision_proof(&[ENTITY_NAME])
6343            .expect("stale-sequence source proof should capture");
6344        session
6345            .start_resumable_job(stale_job_id, stale_proof, Vec::new())
6346            .expect("stale-sequence job should start");
6347        let stale_request = ResumableJobAdvanceRequest::new(
6348            stale_job_id,
6349            4,
6350            ResumableJobIdempotencyKey::new("stale").expect("bounded idempotency key should admit"),
6351        );
6352        assert!(matches!(
6353            session.compare_proof_and_advance::<()>(&stale_request, |_| {
6354                panic!("stale sequence must reject before application work")
6355            }),
6356            Err(CompareProofAndAdvanceError::Protocol(
6357                ResumableJobError::StaleSequence {
6358                    expected: 4,
6359                    actual: 0,
6360                }
6361            )),
6362        ));
6363        assert_eq!(
6364            session.acknowledge_resumable_job(stale_job_id, 0),
6365            Err(ResumableJobError::NotTerminal),
6366        );
6367    }
6368
6369    #[cfg(feature = "sql")]
6370    #[test]
6371    fn exact_key_batch_uses_typed_hard_execution_budget() {
6372        let session = initialize();
6373        let binding = exact_key_binding(&session);
6374        let budget =
6375            HardExecutionBudget::uniform_for_tests(0, HardExecutionFailureHeadroom::new(500, 256));
6376        let error = session
6377            .execute_exact_key_batch_with_hard_budget_for_tests(
6378                &binding,
6379                &[exact_key(u64::MAX)],
6380                &budget,
6381            )
6382            .expect_err("zero query budget should reject the exact-key route");
6383
6384        assert!(matches!(
6385            error.diagnostic().detail(),
6386            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
6387                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
6388            })
6389        ));
6390        let facts = error.diagnostic_facts();
6391        assert_eq!(
6392            &facts[..5],
6393            &[
6394                (
6395                    icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
6396                    icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions.raw(),
6397                ),
6398                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 0),
6399                (icydb_diagnostic_code::DiagnosticFactTag::Actual, 1),
6400                (
6401                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionBudgetScope,
6402                    icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution.raw(),
6403                ),
6404                (
6405                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionLane,
6406                    icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead.raw(),
6407                ),
6408            ],
6409        );
6410        assert_eq!(
6411            facts[5].0,
6412            icydb_diagnostic_code::DiagnosticFactTag::QueryShapeFingerprintPrefix,
6413        );
6414        assert_ne!(facts[5].1, 0);
6415    }
6416
6417    #[cfg(feature = "sql")]
6418    fn assert_planned_query_exhausts(
6419        session: &DbSession<TestCanister>,
6420        query: &crate::db::DynamicQuery,
6421        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
6422    ) {
6423        let budget = HardExecutionBudget::uniform_for_tests(
6424            u64::MAX,
6425            HardExecutionFailureHeadroom::new(500, 256),
6426        )
6427        .with_limit_for_tests(resource, 0);
6428        let context = HardExecutionContext::new(
6429            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
6430            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
6431            0x7068_7973_6963_616c,
6432        );
6433        let error = with_query_execution_budget_for_tests(budget, context, || {
6434            session.execute_trusted_live_page(query, None)
6435        })
6436        .expect_err("the injected zero resource allowance should reject planned execution");
6437
6438        assert!(matches!(
6439            error.diagnostic().detail(),
6440            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
6441                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
6442            })
6443        ));
6444        assert_eq!(
6445            error.diagnostic_facts()[0],
6446            (
6447                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
6448                resource.raw(),
6449            ),
6450        );
6451    }
6452
6453    #[cfg(feature = "sql")]
6454    fn assert_grouped_query_exhausts(
6455        session: &DbSession<TestCanister>,
6456        query: &crate::db::DynamicQuery,
6457        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
6458    ) {
6459        let budget = HardExecutionBudget::uniform_for_tests(
6460            u64::MAX,
6461            HardExecutionFailureHeadroom::new(500, 256),
6462        )
6463        .with_limit_for_tests(resource, 0);
6464        let context = HardExecutionContext::new(
6465            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
6466            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
6467            0x6772_6f75_7065_642d,
6468        );
6469        let error = with_query_execution_budget_for_tests(budget, context, || {
6470            session.execute_trusted_dynamic_grouped_query(query)
6471        })
6472        .expect_err("the injected zero resource allowance should reject grouped execution");
6473
6474        assert!(matches!(
6475            error.diagnostic().detail(),
6476            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
6477                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
6478            })
6479        ));
6480        assert_eq!(
6481            error.diagnostic_facts()[0],
6482            (
6483                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
6484                resource.raw(),
6485            ),
6486        );
6487    }
6488
6489    #[cfg(feature = "sql")]
6490    fn assert_sql_query_exhausts(
6491        session: &DbSession<TestCanister>,
6492        sql: &str,
6493        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
6494    ) {
6495        let budget = HardExecutionBudget::uniform_for_tests(
6496            u64::MAX,
6497            HardExecutionFailureHeadroom::new(500, 256),
6498        )
6499        .with_limit_for_tests(resource, 0);
6500        let context = HardExecutionContext::new(
6501            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
6502            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
6503            0x7371_6c2d_736f_7274,
6504        );
6505        let error = with_query_execution_budget_for_tests(budget, context, || {
6506            session.execute_trusted_sql_query(sql)
6507        })
6508        .expect_err("the injected zero resource allowance should reject SQL execution");
6509
6510        assert!(matches!(
6511            error.diagnostic().detail(),
6512            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
6513                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
6514            })
6515        ));
6516        assert_eq!(
6517            error.diagnostic_facts()[0],
6518            (
6519                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
6520                resource.raw(),
6521            ),
6522        );
6523    }
6524
6525    #[cfg(feature = "sql")]
6526    fn assert_sql_query_fits_resource_limit(
6527        session: &DbSession<TestCanister>,
6528        sql: &str,
6529        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
6530        limit: u64,
6531    ) {
6532        let budget = HardExecutionBudget::uniform_for_tests(
6533            u64::MAX,
6534            HardExecutionFailureHeadroom::new(500, 256),
6535        )
6536        .with_limit_for_tests(resource, limit);
6537        let context = HardExecutionContext::new(
6538            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
6539            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
6540            0x7371_6c2d_626f_756e,
6541        );
6542        with_query_execution_budget_for_tests(budget, context, || {
6543            session.execute_trusted_sql_query(sql)
6544        })
6545        .expect("bounded SQL execution should fit its physical-work limit");
6546    }
6547
6548    #[cfg(feature = "sql")]
6549    #[test]
6550    fn planned_read_routes_share_physical_resource_accounting() {
6551        let session = initialize();
6552        let first = insert_exact_key_fixture(&session, 41);
6553        insert_exact_key_fixture(&session, 42);
6554
6555        let fallback = crate::db::DynamicQuery::new(ENTITY_NAME)
6556            .filter(crate::db::FieldRef::new("id").eq(first))
6557            .select(["id", "payload"])
6558            .order_by(crate::db::asc("id"))
6559            .limit(1);
6560        assert_eq!(
6561            session
6562                .execute_trusted_live_page(&fallback, None)
6563                .expect("bounded fallback execution should preserve its result")
6564                .row_count,
6565            1,
6566        );
6567        assert_planned_query_exhausts(
6568            &session,
6569            &fallback,
6570            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::RowsVisited,
6571        );
6572
6573        let covering = crate::db::DynamicQuery::new(ENTITY_NAME)
6574            .filter(crate::db::FieldRef::new("payload").eq(41_u64))
6575            .select(["payload"])
6576            .order_by(crate::db::asc("payload"))
6577            .limit(1);
6578        assert_eq!(
6579            session
6580                .execute_trusted_live_page(&covering, None)
6581                .expect("bounded covering execution should preserve its result")
6582                .row_count,
6583            1,
6584        );
6585        assert_planned_query_exhausts(
6586            &session,
6587            &covering,
6588            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
6589        );
6590
6591        let residual = crate::db::DynamicQuery::new(ENTITY_NAME)
6592            .filter(crate::db::FieldRef::new("payload").eq_field("id"))
6593            .select(["id"])
6594            .order_by(crate::db::asc("id"))
6595            .limit(1);
6596        assert_eq!(
6597            session
6598                .execute_trusted_live_page(&residual, None)
6599                .expect("bounded residual execution should preserve its result")
6600                .row_count,
6601            0,
6602        );
6603        assert_planned_query_exhausts(
6604            &session,
6605            &residual,
6606            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
6607        );
6608
6609        assert_planned_query_exhausts(
6610            &session,
6611            &fallback,
6612            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::ResultBytes,
6613        );
6614
6615        let grouped = crate::db::DynamicQuery::new(ENTITY_NAME)
6616            .group_by("payload")
6617            .aggregate(crate::db::count())
6618            .order_by(crate::db::asc("payload"))
6619            .grouped_limits(10, 16 * 1_024)
6620            .limit(1);
6621        let grouped_result = session
6622            .execute_trusted_dynamic_grouped_query(&grouped)
6623            .expect("bounded grouped execution should preserve its result");
6624        assert_eq!(grouped_result.row_count, 1);
6625        assert!(grouped_result.next_cursor.is_some());
6626        assert_grouped_query_exhausts(
6627            &session,
6628            &grouped,
6629            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctEntries,
6630        );
6631        assert_grouped_query_exhausts(
6632            &session,
6633            &grouped,
6634            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::CursorSteps,
6635        );
6636
6637        assert_sql_query_exhausts(
6638            &session,
6639            "SELECT payload, COUNT(*) AS row_count FROM IdentityRow \
6640             GROUP BY payload ORDER BY row_count DESC, payload ASC LIMIT 1",
6641            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::SortEntries,
6642        );
6643    }
6644
6645    #[cfg(feature = "sql")]
6646    #[test]
6647    fn mutation_execution_budget_exhaustion_terminalizes_forward_and_verify() {
6648        let (session, _root) = initialize_journaled_with_root();
6649        assert_eq!(insert_exact_key_fixture(&session, 41), 1);
6650
6651        for (identity, sql, expected_phase) in [
6652            (
6653                91_u8,
6654                "UPDATE IdentityRow SET payload = 42 WHERE id = 1",
6655                MutationJobPhase::Forward,
6656            ),
6657            (
6658                92_u8,
6659                "UPDATE IdentityRow SET payload = 42 WHERE id = 999",
6660                MutationJobPhase::Verify,
6661            ),
6662        ] {
6663            let job_id = MutationJobId::try_from_bytes([identity; 32])
6664                .expect("budget fixture identity should admit");
6665            let mut state = session
6666                .start_trusted_sql_mutation_job(job_id, sql)
6667                .expect("budget fixture job should start");
6668            if expected_phase == MutationJobPhase::Verify {
6669                let forward = MutationJobAdvanceRequest::new(
6670                    job_id,
6671                    state.sequence,
6672                    MutationJobIdempotencyKey::new(format!("budget-forward-{identity}"))
6673                        .expect("bounded Forward replay identity should admit"),
6674                );
6675                let receipt = session
6676                    .advance_trusted_mutation_job(&forward)
6677                    .expect("nonmatching Forward page should enter Verify");
6678                assert_eq!(receipt.phase, MutationJobPhase::Verify);
6679                state = session
6680                    .mutation_job_state(job_id)
6681                    .expect("Verify predecessor should remain readable");
6682            }
6683            assert_eq!(state.phase, expected_phase);
6684
6685            let request = MutationJobAdvanceRequest::new(
6686                job_id,
6687                state.sequence,
6688                MutationJobIdempotencyKey::new(format!("budget-exhaust-{identity}"))
6689                    .expect("bounded exhaustion replay identity should admit"),
6690            );
6691            let terminal = advance_with_exhausted_mutation_predicate_budget(&session, &request)
6692                .expect("admitted execution-budget failure should commit terminal progress");
6693            assert_eq!(
6694                terminal.status,
6695                MutationJobStatus::RestartRequired(
6696                    MutationJobRestartReason::ExecutionBudgetPolicyExceeded,
6697                ),
6698            );
6699            assert_eq!(terminal.rows_updated, 0);
6700            assert_eq!(
6701                session.advance_trusted_mutation_job(&request),
6702                Ok(terminal.clone()),
6703                "exact terminal replay must not execute the exhausted page again",
6704            );
6705            assert_dynamic_payload(&session, 1, 41);
6706            session
6707                .acknowledge_mutation_job(job_id, terminal.committed_sequence)
6708                .expect("terminal budget fixture should acknowledge");
6709        }
6710    }
6711
6712    fn assert_dynamic_payload<C: CanisterKind>(
6713        session: &DbSession<C>,
6714        key: u64,
6715        expected_payload: u64,
6716    ) {
6717        let unchanged = session
6718            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
6719                entity: ENTITY_NAME.to_string(),
6720                key: InputValue::nat64(key),
6721                patch: dynamic_payload_patch(expected_payload),
6722            })
6723            .expect("the expected row should remain readable through a no-op update");
6724        assert_eq!(unchanged.affected_rows, 0);
6725        assert_eq!(
6726            unchanged.rows,
6727            vec![expected_dynamic_row(key, expected_payload)],
6728        );
6729    }
6730
6731    fn assert_exact_batch_backlog_pressure(
6732        pressure: &InternalError,
6733        before: JournalTailControl,
6734        next_sequence: u64,
6735    ) {
6736        assert_eq!(
6737            pressure.diagnostic().error_code(),
6738            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_CONVERGENCE_BACKLOG_PRESSURE,
6739        );
6740        assert_eq!(
6741            pressure.diagnostic_facts(),
6742            vec![
6743                (
6744                    icydb_diagnostic_code::DiagnosticFactTag::BacklogResource,
6745                    icydb_diagnostic_code::DiagnosticBacklogResource::Batches.raw(),
6746                ),
6747                (icydb_diagnostic_code::DiagnosticFactTag::CurrentCount, 64),
6748                (icydb_diagnostic_code::DiagnosticFactTag::ProposedCount, 1),
6749                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 64),
6750            ],
6751        );
6752        assert_eq!(
6753            crate::db::commit::next_database_commit_sequence()
6754                .expect("pressure must leave the database sequence readable"),
6755            next_sequence,
6756        );
6757        assert!(matches!(
6758            crate::db::commit::observe_commit_control()
6759                .expect("pressure must leave commit control observable"),
6760            crate::db::commit::CommitControlObservation::Present {
6761                marker_present: false,
6762                ..
6763            },
6764        ));
6765        assert_eq!(
6766            JOURNALED_TAIL_STORE.with(|tail| {
6767                tail.borrow()
6768                    .current_tail_control()
6769                    .expect("pressure must preserve the exact tail control")
6770            }),
6771            before,
6772        );
6773    }
6774
6775    fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
6776        values
6777            .iter()
6778            .map(|value| {
6779                AcceptedStructuralMutation::save(
6780                    MutationMode::Insert,
6781                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
6782                    payload_patch(*value),
6783                )
6784            })
6785            .collect()
6786    }
6787
6788    fn atomic_progress_fixture(
6789        identity_byte: u8,
6790    ) -> (
6791        MutationJobRecord,
6792        MutationJobRecord,
6793        MutationProgressRecordOp,
6794    ) {
6795        let job_id = MutationJobId::try_from_bytes([identity_byte; 32])
6796            .expect("nonzero atomic progress job id should admit");
6797        let before = MutationJobRecord::new(job_id, vec![1, identity_byte], vec![2])
6798            .expect("atomic progress predecessor should admit");
6799        let request = MutationJobAdvanceRequest::new(
6800            job_id,
6801            0,
6802            MutationJobIdempotencyKey::new(format!("atomic-{identity_byte}"))
6803                .expect("atomic progress replay key should admit"),
6804        );
6805        let (after, _) = before
6806            .apply_transition(
6807                &request,
6808                MutationJobTransition::new(
6809                    MutationJobStatus::Active,
6810                    MutationJobPhase::Forward,
6811                    vec![3],
6812                    1,
6813                    1,
6814                    0,
6815                ),
6816            )
6817            .expect("atomic progress successor should admit");
6818        let operation = MutationProgressRecordOp::replace(&before, &after)
6819            .expect("atomic progress replacement should admit");
6820        (before, after, operation)
6821    }
6822
6823    fn assert_mutation_facts(
6824        error: &InternalError,
6825        session: &DbSession<TestCanister>,
6826        tail: Vec<(icydb_diagnostic_code::DiagnosticFactTag, u64)>,
6827    ) {
6828        use icydb_diagnostic_code::DiagnosticFactTag as Tag;
6829        let catalog = session
6830            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
6831            .unwrap();
6832        let fingerprint = catalog.fingerprint();
6833        let mut expected = vec![
6834            (
6835                Tag::AcceptedSchemaFingerprintMethod,
6836                u64::from(catalog.fingerprint_method_version()),
6837            ),
6838            (
6839                Tag::AcceptedSchemaFingerprintHigh,
6840                u64::from_be_bytes(fingerprint[..8].try_into().unwrap()),
6841            ),
6842            (
6843                Tag::AcceptedSchemaFingerprintLow,
6844                u64::from_be_bytes(fingerprint[8..].try_into().unwrap()),
6845            ),
6846        ];
6847        expected.extend(tail);
6848        assert_eq!(error.diagnostic_facts(), expected);
6849        assert_eq!(
6850            icydb_diagnostic_code::validate_known_diagnostic_fact_schema(
6851                error.diagnostic().error_code(),
6852                &expected,
6853            ),
6854            Ok(()),
6855        );
6856    }
6857
6858    fn assert_identity_boundary(error: &InternalError) {
6859        assert_eq!(error.class(), ErrorClass::Unsupported);
6860        assert_eq!(error.origin(), ErrorOrigin::Identity);
6861    }
6862
6863    #[test]
6864    fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
6865        let generated = insert_key_exists_after_generation(true);
6866        assert_eq!(generated.class(), ErrorClass::Corruption);
6867        assert_eq!(generated.origin(), ErrorOrigin::Identity);
6868
6869        let ordinary = insert_key_exists_after_generation(false);
6870        assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
6871    }
6872
6873    #[cfg(target_pointer_width = "64")]
6874    #[test]
6875    fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
6876        let error = checked_pre_key_candidate_count(
6877            usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
6878        )
6879        .expect_err("candidate counts beyond u32 must reject");
6880        assert_identity_boundary(&error);
6881    }
6882
6883    #[test]
6884    #[expect(
6885        clippy::too_many_lines,
6886        reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
6887    )]
6888    fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
6889        let session = initialize();
6890        let seeded = session
6891            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
6892            .expect("seed rows should commit");
6893        assert_eq!(seeded.affected_rows, 1);
6894
6895        let split = session
6896            .execute_trusted_dynamic_mutation_batch(vec![
6897                DynamicMutation::Update {
6898                    entity: ENTITY_NAME.to_string(),
6899                    key: InputValue::nat64(1),
6900                    patch: dynamic_payload_patch(60),
6901                },
6902                DynamicMutation::Insert {
6903                    entity: ENTITY_NAME.to_string(),
6904                    patch: dynamic_payload_patch(40),
6905                },
6906            ])
6907            .expect("one holding should split atomically");
6908        assert_eq!(
6909            split.iter().map(|result| result.affected_rows).sum::<u32>(),
6910            2,
6911        );
6912        assert_eq!(
6913            batch_rows(&split),
6914            vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
6915            "split after-images must retain input order and exact quantity",
6916        );
6917
6918        let rejected_split = session
6919            .execute_trusted_dynamic_mutation_batch(vec![
6920                DynamicMutation::Update {
6921                    entity: ENTITY_NAME.to_string(),
6922                    key: InputValue::nat64(1),
6923                    patch: dynamic_payload_patch(50),
6924                },
6925                DynamicMutation::Insert {
6926                    entity: ENTITY_NAME.to_string(),
6927                    patch: DynamicStructuralPatch::new(Vec::new()),
6928                },
6929            ])
6930            .expect_err("an invalid split output must reject the staged source update");
6931        assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
6932        assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
6933        assert_mutation_facts(
6934            &rejected_split,
6935            &session,
6936            vec![
6937                (
6938                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
6939                    ENTITY_TAG.value(),
6940                ),
6941                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 2),
6942                (
6943                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
6944                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
6945                ),
6946                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1),
6947            ],
6948        );
6949        assert_dynamic_payload(&session, 1, 60);
6950        assert_dynamic_payload(&session, 2, 40);
6951
6952        let transfer = session
6953            .execute_trusted_dynamic_mutation_batch(vec![
6954                DynamicMutation::Update {
6955                    entity: ENTITY_NAME.to_string(),
6956                    key: InputValue::nat64(1),
6957                    patch: dynamic_payload_patch(70),
6958                },
6959                DynamicMutation::Update {
6960                    entity: ENTITY_NAME.to_string(),
6961                    key: InputValue::nat64(2),
6962                    patch: dynamic_payload_patch(30),
6963                },
6964            ])
6965            .expect("distinct transfer patches should share one atomic batch");
6966        assert_eq!(
6967            batch_rows(&transfer),
6968            vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
6969            "the transfer must preserve the exact total quantity",
6970        );
6971
6972        let merge = session
6973            .execute_trusted_dynamic_mutation_batch(vec![
6974                DynamicMutation::Delete {
6975                    entity: ENTITY_NAME.to_string(),
6976                    key: InputValue::nat64(2),
6977                },
6978                DynamicMutation::Update {
6979                    entity: ENTITY_NAME.to_string(),
6980                    key: InputValue::nat64(1),
6981                    patch: dynamic_payload_patch(100),
6982                },
6983            ])
6984            .expect("two holdings should merge atomically");
6985        assert_eq!(
6986            batch_rows(&merge),
6987            vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
6988            "delete before-images and update after-images must retain input order",
6989        );
6990
6991        let resplit = session
6992            .execute_trusted_dynamic_mutation_batch(vec![
6993                DynamicMutation::Update {
6994                    entity: ENTITY_NAME.to_string(),
6995                    key: InputValue::nat64(1),
6996                    patch: dynamic_payload_patch(60),
6997                },
6998                DynamicMutation::Insert {
6999                    entity: ENTITY_NAME.to_string(),
7000                    patch: dynamic_payload_patch(40),
7001                },
7002            ])
7003            .expect("the merged holding should split again");
7004        assert_eq!(
7005            batch_rows(&resplit),
7006            vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
7007        );
7008
7009        let rejected_merge = session
7010            .execute_trusted_dynamic_mutation_batch(vec![
7011                DynamicMutation::Delete {
7012                    entity: ENTITY_NAME.to_string(),
7013                    key: InputValue::nat64(3),
7014                },
7015                DynamicMutation::Update {
7016                    entity: ENTITY_NAME.to_string(),
7017                    key: InputValue::nat64(99),
7018                    patch: dynamic_payload_patch(100),
7019                },
7020            ])
7021            .expect_err("a late missing merge target must preserve the earlier staged delete");
7022        assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
7023        assert_dynamic_payload(&session, 1, 60);
7024        assert_dynamic_payload(&session, 3, 40);
7025
7026        SCHEMA_STORE.with(|store| {
7027            let cursor = store
7028                .borrow()
7029                .identity_statement_cursor(
7030                    database_incarnation_id().expect("database incarnation should remain readable"),
7031                    ENTITY_TAG,
7032                    FieldId::new(1),
7033                    &AcceptedFieldKind::Nat64,
7034                )
7035                .expect("mixed Identity state should remain readable");
7036            assert_eq!(cursor.expected_high_water(), 3);
7037            assert!(!cursor.has_allocations());
7038        });
7039    }
7040
7041    #[test]
7042    fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
7043        let session = initialize();
7044        session
7045            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
7046            .expect("the holding fixture should initialize");
7047
7048        let duplicate = session
7049            .execute_trusted_dynamic_mutation_batch(vec![
7050                DynamicMutation::Update {
7051                    entity: ENTITY_NAME.to_string(),
7052                    key: InputValue::nat64(1),
7053                    patch: dynamic_payload_patch(60),
7054                },
7055                DynamicMutation::Delete {
7056                    entity: ENTITY_NAME.to_string(),
7057                    key: InputValue::nat64(1),
7058                },
7059            ])
7060            .expect_err("duplicate targets across operation kinds must reject");
7061        assert!(matches!(
7062            duplicate.diagnostic().detail(),
7063            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
7064                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
7065            }),
7066        ));
7067        assert_eq!(
7068            duplicate.diagnostic_facts(),
7069            vec![
7070                (
7071                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
7072                    ENTITY_TAG.value(),
7073                ),
7074                (
7075                    icydb_diagnostic_code::DiagnosticFactTag::FirstBatchPosition,
7076                    0,
7077                ),
7078                (
7079                    icydb_diagnostic_code::DiagnosticFactTag::DuplicateBatchPosition,
7080                    1,
7081                ),
7082            ],
7083        );
7084        assert_dynamic_payload(&session, 1, 100);
7085    }
7086
7087    #[test]
7088    fn dynamic_insert_batch_checks_count_before_entity_resolution() {
7089        use icydb_diagnostic_code::{DiagnosticDetail, RuntimeBoundaryCode};
7090
7091        let session = initialize();
7092        for (count, boundary) in [
7093            (0, RuntimeBoundaryCode::MutationBatchEmpty),
7094            (
7095                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1,
7096                RuntimeBoundaryCode::MutationBatchTooManyItems,
7097            ),
7098        ] {
7099            let error = session
7100                .execute_trusted_dynamic_insert_batch(
7101                    "",
7102                    (0..count).map(|_| dynamic_payload_patch(10)).collect(),
7103                )
7104                .expect_err("batch count must reject before the empty entity name");
7105            assert_eq!(
7106                error.diagnostic().detail(),
7107                Some(&DiagnosticDetail::RuntimeBoundary { boundary }),
7108            );
7109        }
7110        let error = session
7111            .execute_trusted_dynamic_insert_batch("", vec![dynamic_payload_patch(10)])
7112            .expect_err("an admitted count must still validate the entity name");
7113        assert_eq!(error.class(), ErrorClass::Unsupported);
7114        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
7115    }
7116
7117    #[test]
7118    fn dynamic_insert_batch_preserves_positions_atomicity_and_identity_order() {
7119        use icydb_diagnostic_code::DiagnosticFactTag;
7120
7121        let session = initialize();
7122        let authored_identity = DynamicStructuralPatch::new(vec![(
7123            "id".to_string(),
7124            DynamicWriteCell::Value(InputValue::nat64(99)),
7125        )]);
7126        let error = session
7127            .execute_trusted_dynamic_insert_batch(
7128                ENTITY_NAME,
7129                vec![dynamic_payload_patch(10), authored_identity],
7130            )
7131            .expect_err("a late generated-field write must reject during lowering");
7132        assert!(
7133            error
7134                .diagnostic_facts()
7135                .contains(&(DiagnosticFactTag::BatchPosition, 1))
7136        );
7137
7138        let wrong_type = DynamicStructuralPatch::new(vec![(
7139            "payload".to_string(),
7140            DynamicWriteCell::Value(InputValue::text("invalid".to_string())),
7141        )]);
7142        session
7143            .execute_trusted_dynamic_insert_batch(
7144                ENTITY_NAME,
7145                vec![dynamic_payload_patch(10), wrong_type],
7146            )
7147            .expect_err("late value validation must reject the complete staged batch");
7148        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
7149
7150        let inserted = session
7151            .execute_trusted_dynamic_insert_batch(
7152                ENTITY_NAME,
7153                vec![dynamic_payload_patch(10), dynamic_payload_patch(20)],
7154            )
7155            .expect("rejected batches must not consume generated identities");
7156        assert_eq!(inserted.affected_rows, 2);
7157        assert_eq!(
7158            inserted.rows,
7159            vec![
7160                vec![OutputValue::nat64(1), OutputValue::nat64(10)],
7161                vec![OutputValue::nat64(2), OutputValue::nat64(20)],
7162            ],
7163        );
7164    }
7165
7166    #[test]
7167    fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
7168        let session = initialize();
7169        let empty = session
7170            .execute_trusted_dynamic_mutation_batch(Vec::new())
7171            .expect_err("an empty public batch must reject");
7172        assert!(matches!(
7173            empty.diagnostic().detail(),
7174            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
7175                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
7176            }),
7177        ));
7178        assert_eq!(
7179            empty.diagnostic_facts(),
7180            vec![(icydb_diagnostic_code::DiagnosticFactTag::ActualCount, 0,)],
7181        );
7182
7183        let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
7184            .map(|_| DynamicMutation::Delete {
7185                entity: ENTITY_NAME.to_string(),
7186                key: InputValue::nat64(1),
7187            })
7188            .collect();
7189        let over_bound = session
7190            .execute_trusted_dynamic_mutation_batch(requests)
7191            .expect_err("operation cap plus one must reject before row resolution");
7192        assert!(matches!(
7193            over_bound.diagnostic().detail(),
7194            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
7195                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
7196            }),
7197        ));
7198        assert_eq!(
7199            over_bound.diagnostic_facts(),
7200            vec![
7201                (
7202                    icydb_diagnostic_code::DiagnosticFactTag::ActualCount,
7203                    (MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1) as u64,
7204                ),
7205                (
7206                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
7207                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS as u64,
7208                ),
7209            ],
7210        );
7211    }
7212
7213    #[test]
7214    fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
7215        assert_eq!(
7216            structural_mutation_staged_charge([11, 13, 17])
7217                .expect("the writer-owned formula should sum all three row-image components"),
7218            41,
7219        );
7220        let mut exact = 0;
7221        add_structural_mutation_staged_bytes(
7222            &mut exact,
7223            [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
7224        )
7225        .expect("the exact staged-byte boundary should admit");
7226        assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
7227
7228        let error = add_structural_mutation_staged_bytes(&mut exact, [1])
7229            .expect_err("one byte above the staged-byte boundary must reject");
7230        assert!(matches!(
7231            error.diagnostic().detail(),
7232            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
7233                boundary:
7234                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
7235            }),
7236        ));
7237        assert_eq!(
7238            error.diagnostic_facts(),
7239            vec![
7240                (
7241                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
7242                    (MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1) as u64,
7243                ),
7244                (
7245                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
7246                    MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES as u64,
7247                ),
7248            ],
7249        );
7250
7251        let mut prefix = 0;
7252        assert_eq!(
7253            admit_structural_mutation_staged_charge(
7254                &mut prefix,
7255                [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
7256                AcceptedStructuralMutationPacking::BoundedPrefix,
7257            )
7258            .expect("the exact prefix boundary should calculate"),
7259            AcceptedStructuralMutationStagedAdmission::Admitted,
7260        );
7261        assert_eq!(prefix, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
7262        assert_eq!(
7263            admit_structural_mutation_staged_charge(
7264                &mut prefix,
7265                [1],
7266                AcceptedStructuralMutationPacking::BoundedPrefix,
7267            )
7268            .expect("the next prefix candidate should calculate"),
7269            AcceptedStructuralMutationStagedAdmission::PageFull,
7270        );
7271        assert_eq!(prefix, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
7272
7273        let mut empty_prefix = 0;
7274        assert_eq!(
7275            admit_structural_mutation_staged_charge(
7276                &mut empty_prefix,
7277                [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1],
7278                AcceptedStructuralMutationPacking::BoundedPrefix,
7279            )
7280            .expect("one oversized candidate should classify without mutating the prefix"),
7281            AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy,
7282        );
7283        assert_eq!(empty_prefix, 0);
7284
7285        validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
7286            .expect("the exact result-byte boundary should admit");
7287        let error = validate_structural_mutation_result_bytes(
7288            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
7289        )
7290        .expect_err("one byte above the result-byte boundary must reject");
7291        assert!(matches!(
7292            error.diagnostic().detail(),
7293            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
7294                boundary:
7295                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
7296            }),
7297        ));
7298        assert_eq!(
7299            error.diagnostic_facts(),
7300            vec![
7301                (
7302                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
7303                    (MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1) as u64,
7304                ),
7305                (
7306                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
7307                    MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES as u64,
7308                ),
7309            ],
7310        );
7311    }
7312
7313    #[expect(
7314        clippy::too_many_lines,
7315        reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
7316    )]
7317    #[test]
7318    fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
7319        let session = initialize();
7320        let catalog = session
7321            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
7322            .expect("identity catalog should resolve");
7323        let initial_description = session
7324            .try_describe_entity_by_name(ENTITY_NAME)
7325            .expect("accepted Identity description should resolve");
7326        assert_eq!(
7327            initial_description.entity_tag(),
7328            catalog.identity().entity_tag().value()
7329        );
7330        assert_eq!(
7331            initial_description.accepted_schema_fingerprint_method(),
7332            catalog.fingerprint_method_version()
7333        );
7334        assert_eq!(
7335            initial_description.accepted_schema_fingerprint(),
7336            catalog.fingerprint()
7337        );
7338        let initial_identity = initial_description
7339            .identity()
7340            .expect("accepted Identity policy should be described");
7341        assert_eq!(initial_identity.field(), "id");
7342        assert_eq!(initial_identity.generator(), "Identity::next");
7343        assert_eq!(initial_identity.accepted_kind(), "nat64");
7344        assert_eq!(initial_identity.minimum(), 1);
7345        assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
7346        assert_eq!(initial_identity.high_water(), 0);
7347        assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
7348        assert!(!initial_identity.exhausted());
7349
7350        let rejected = session
7351            .execute_accepted_structural_save_batch(
7352                &catalog,
7353                true,
7354                batch(&[1_000, 2_000]),
7355                Timestamp::from_millis(6),
7356                |_| Err::<(), _>(InternalError::executor_unsupported()),
7357            )
7358            .expect_err("a rejected precommit result must not publish its tentative range");
7359        assert_eq!(rejected.class(), ErrorClass::Unsupported);
7360        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
7361
7362        let rows = session
7363            .execute_accepted_structural_save_batch(
7364                &catalog,
7365                true,
7366                batch(&[10, 20, 30]),
7367                Timestamp::from_millis(7),
7368                Ok,
7369            )
7370            .expect("one accepted batch should commit rows and one identity range");
7371        assert_eq!(
7372            rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
7373            vec![
7374                vec![Value::Nat64(1), Value::Nat64(10)],
7375                vec![Value::Nat64(2), Value::Nat64(20)],
7376                vec![Value::Nat64(3), Value::Nat64(30)],
7377            ],
7378        );
7379
7380        let dynamic = session
7381            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
7382                entity: ENTITY_NAME.to_string(),
7383                patch: DynamicStructuralPatch::new(vec![(
7384                    "payload".to_string(),
7385                    DynamicWriteCell::Value(InputValue::nat64(40)),
7386                )]),
7387            })
7388            .expect("dynamic omission should commit through shared Identity generation");
7389        assert_eq!(dynamic.affected_rows, 1);
7390
7391        for (request, operation) in [
7392            (
7393                DynamicMutation::Insert {
7394                    entity: ENTITY_NAME.to_string(),
7395                    patch: DynamicStructuralPatch::new(vec![
7396                        (
7397                            "id".to_string(),
7398                            DynamicWriteCell::Value(InputValue::nat64(41)),
7399                        ),
7400                        (
7401                            "payload".to_string(),
7402                            DynamicWriteCell::Value(InputValue::nat64(42)),
7403                        ),
7404                    ]),
7405                },
7406                icydb_diagnostic_code::DiagnosticMutationOperation::Insert,
7407            ),
7408            (
7409                DynamicMutation::Update {
7410                    entity: ENTITY_NAME.to_string(),
7411                    key: InputValue::nat64(1),
7412                    patch: DynamicStructuralPatch::new(vec![(
7413                        "id".to_string(),
7414                        DynamicWriteCell::Default,
7415                    )]),
7416                },
7417                icydb_diagnostic_code::DiagnosticMutationOperation::Update,
7418            ),
7419        ] {
7420            let error = session
7421                .execute_trusted_dynamic_mutation(&request)
7422                .expect_err("structural Identity authorship and regeneration must reject");
7423            assert_eq!(error.class(), ErrorClass::Unsupported);
7424            assert_eq!(error.origin(), ErrorOrigin::Executor);
7425            assert_mutation_facts(
7426                &error,
7427                &session,
7428                vec![
7429                    (
7430                        icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
7431                        ENTITY_TAG.value(),
7432                    ),
7433                    (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
7434                    (
7435                        icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
7436                        operation.raw(),
7437                    ),
7438                    (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0),
7439                ],
7440            );
7441        }
7442
7443        let binding = session
7444            .issue_typed_entity_binding(&TYPED_DESCRIPTOR)
7445            .expect("typed output should bind the Identity field");
7446        let typed_patch = binding
7447            .bind_write_ordinals(vec![(1, DynamicWriteCell::Value(InputValue::nat64(50)))])
7448            .expect("typed payload should lower");
7449        let typed = session
7450            .execute_trusted_typed_mutation(
7451                &binding,
7452                DynamicTypedMutation::Insert { patch: typed_patch },
7453            )
7454            .expect("typed omission should commit through shared Identity generation");
7455        assert_eq!(
7456            typed
7457                .expect("typed insert should return one mutation result")
7458                .affected_rows,
7459            1,
7460        );
7461        let explicit_typed_patch = binding
7462            .bind_write_ordinals(vec![
7463                (0, DynamicWriteCell::Value(InputValue::nat64(51))),
7464                (1, DynamicWriteCell::Value(InputValue::nat64(52))),
7465            ])
7466            .expect("the low-level binding should retain exact authored intent");
7467        let explicit_typed_error = session
7468            .execute_trusted_typed_mutation(
7469                &binding,
7470                DynamicTypedMutation::Insert {
7471                    patch: explicit_typed_patch,
7472                },
7473            )
7474            .expect_err("typed Identity authorship must reject before allocation");
7475        assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
7476        assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
7477        assert_mutation_facts(
7478            &explicit_typed_error,
7479            &session,
7480            vec![
7481                (
7482                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
7483                    ENTITY_TAG.value(),
7484                ),
7485                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
7486                (
7487                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
7488                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
7489                ),
7490                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0),
7491            ],
7492        );
7493
7494        let replace_error = session
7495            .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
7496                entity: ENTITY_NAME.to_string(),
7497                key: InputValue::nat64(99),
7498                patch: DynamicStructuralPatch::new(vec![(
7499                    "payload".to_string(),
7500                    DynamicWriteCell::Value(InputValue::nat64(60)),
7501                )]),
7502            })
7503            .expect_err("save-as-insert with a chosen Identity must reject");
7504        assert_eq!(replace_error.class(), ErrorClass::Unsupported);
7505        assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
7506
7507        #[cfg(feature = "sql")]
7508        {
7509            for sql in [
7510                "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
7511                "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
7512            ] {
7513                let _result = session
7514                    .execute_trusted_sql_mutation(sql)
7515                    .expect("SQL omission and DEFAULT should commit Identity generation");
7516            }
7517
7518            let error = session
7519                .execute_trusted_sql_mutation(
7520                    "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
7521                )
7522                .expect_err("an explicit SQL Identity value must reject before allocation");
7523            let diagnostic = error.diagnostic();
7524            assert_eq!(
7525                diagnostic.code(),
7526                icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
7527            );
7528            assert!(matches!(
7529                diagnostic.detail(),
7530                Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
7531                    boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
7532                }),
7533            ));
7534        }
7535
7536        let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
7537        assert_eq!(
7538            DATA_STORE.with(|store| store.borrow().len()),
7539            expected_committed
7540        );
7541        SCHEMA_STORE.with(|store| {
7542            let cursor = store
7543                .borrow()
7544                .identity_statement_cursor(
7545                    database_incarnation_id().expect("database incarnation should remain readable"),
7546                    ENTITY_TAG,
7547                    FieldId::new(1),
7548                    &AcceptedFieldKind::Nat64,
7549                )
7550                .expect("committed writes must leave active state readable");
7551            assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
7552            assert!(!cursor.has_allocations());
7553        });
7554        let committed_description = session
7555            .try_describe_entity_by_name(ENTITY_NAME)
7556            .expect("committed Identity description should resolve");
7557        let committed_identity = committed_description
7558            .identity()
7559            .expect("accepted Identity policy should remain described");
7560        assert_eq!(
7561            committed_identity.high_water(),
7562            u128::from(expected_committed),
7563        );
7564        assert_eq!(
7565            committed_identity.remaining(),
7566            u128::from(u64::MAX - expected_committed),
7567        );
7568        assert!(!committed_identity.exhausted());
7569    }
7570
7571    #[test]
7572    #[expect(
7573        clippy::too_many_lines,
7574        reason = "one ordered scenario proves target/progress atomicity, every interruption wake-up, state-only admission, and successful no-op wake-up behavior"
7575    )]
7576    fn mutation_progress_and_target_rows_recover_as_one_marker_transition() {
7577        let session = initialize_journaled();
7578        let initial_entity_revision = JOURNALED_TAIL_STORE
7579            .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
7580            .expect("direct initial schema publication must install entity revision authority");
7581        assert_eq!(initial_entity_revision, 1);
7582        install_startup_recovery_wakeup(record_startup_wakeup);
7583        let catalog = session
7584            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
7585            .expect("journaled atomic-progress catalog should resolve");
7586        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
7587            .expect("journaled atomic-progress row layout should build");
7588
7589        for (ordinal, interruption) in [
7590            MutationCommitInterruption::MarkerPersisted,
7591            MutationCommitInterruption::JournalPublished,
7592            MutationCommitInterruption::RowsPublished,
7593            MutationCommitInterruption::ProgressReplaced,
7594        ]
7595        .into_iter()
7596        .enumerate()
7597        {
7598            let identity_byte = 31 + u8::try_from(ordinal).expect("small ordinal should fit");
7599            let (before, after, operation) = atomic_progress_fixture(identity_byte);
7600            with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7601                match store.insert_mutation(&before)? {
7602                    InsertMutationJobResult::Inserted => Ok(()),
7603                    InsertMutationJobResult::Occupied(_) => {
7604                        Err(crate::db::MutationJobError::IdentityConflict)
7605                    }
7606                }
7607            })
7608            .expect("atomic predecessor should insert once");
7609
7610            let wakeups_before = STARTUP_WAKEUPS.with(Cell::get);
7611            interrupt_next_mutation_commit_for_tests(interruption);
7612            let interrupted = session.execute_accepted_structural_update_with_mutation_progress(
7613                &catalog,
7614                &descriptor,
7615                batch(&[700 + u64::try_from(ordinal).expect("small ordinal should fit")]),
7616                Timestamp::from_millis(17),
7617                operation,
7618            );
7619            assert!(
7620                interrupted.is_err(),
7621                "selected atomic boundary should interrupt"
7622            );
7623            assert_eq!(
7624                STARTUP_WAKEUPS.with(Cell::get),
7625                wakeups_before.saturating_add(1),
7626                "a normally returned retained-marker error must register its wake-up",
7627            );
7628
7629            forget_recovered_domain_for_tests(&session.db)
7630                .expect("interruption should reset volatile recovery ownership");
7631            let retained_before =
7632                with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7633                    store.load_mutation(before.state().job_id)
7634                })
7635                .expect("pre-driver progress should load");
7636            let row_count_before = JOURNALED_DATA_STORE.with(|store| store.borrow().len());
7637            let pending = session
7638                .db
7639                .ensure_recovered_state()
7640                .expect_err("ordinary admission must not drive retained-marker recovery");
7641            assert_eq!(
7642                pending.diagnostic().error_code(),
7643                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
7644            );
7645            assert_eq!(
7646                with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7647                    store.load_mutation(before.state().job_id)
7648                })
7649                .expect("post-admission progress should load"),
7650                retained_before,
7651            );
7652            assert_eq!(
7653                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
7654                row_count_before,
7655                "state-only admission must not mutate target rows",
7656            );
7657            drive_journaled_recovery_to_completion(&session);
7658            let retained = with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7659                store.load_mutation(before.state().job_id)
7660            })
7661            .expect("recovered successor should load");
7662            assert_eq!(retained, after);
7663            assert_eq!(
7664                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
7665                u64::try_from(ordinal + 1).expect("small row count should fit"),
7666            );
7667            assert_eq!(
7668                JOURNALED_TAIL_STORE
7669                    .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
7670                    .expect("recovery must publish the target entity revision"),
7671                initial_entity_revision
7672                    + u64::try_from(ordinal + 1).expect("small revision delta should fit"),
7673                "target rows, entity revision, and progress must recover as one transition",
7674            );
7675        }
7676
7677        let (before, after, operation) = atomic_progress_fixture(39);
7678        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7679            match store.insert_mutation(&before)? {
7680                InsertMutationJobResult::Inserted => Ok(()),
7681                InsertMutationJobResult::Occupied(_) => {
7682                    Err(crate::db::MutationJobError::IdentityConflict)
7683                }
7684            }
7685        })
7686        .expect("final predecessor should insert once");
7687        let wakeups_before_success = STARTUP_WAKEUPS.with(Cell::get);
7688        session
7689            .execute_accepted_structural_update_with_mutation_progress(
7690                &catalog,
7691                &descriptor,
7692                batch(&[799]),
7693                Timestamp::from_millis(18),
7694                operation,
7695            )
7696            .expect("uninterrupted atomic transition should clear its marker");
7697        assert_eq!(
7698            STARTUP_WAKEUPS.with(Cell::get),
7699            wakeups_before_success.saturating_add(1),
7700            "a successful retained commit must request online convergence",
7701        );
7702        let retained = with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7703            store.load_mutation(before.state().job_id)
7704        })
7705        .expect("final successor should load");
7706        assert_eq!(retained, after);
7707        forget_recovered_domain_for_tests(&session.db)
7708            .expect("post-clear recovery ownership should reset");
7709        let pending = session
7710            .db
7711            .ensure_recovered_state()
7712            .expect_err("an upgrade epoch must remain gated until its driver runs");
7713        assert_eq!(
7714            pending.diagnostic().error_code(),
7715            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
7716        );
7717        drive_journaled_recovery_to_completion(&session);
7718        assert_eq!(
7719            JOURNALED_TAIL_STORE
7720                .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
7721                .expect("uninterrupted transition must retain its entity revision"),
7722            initial_entity_revision + 5,
7723        );
7724    }
7725
7726    fn assert_mixed_entity_recovered_state(session: &DbSession<JournaledTestCanister>) {
7727        for (entity_name, payload) in [
7728            (ENTITY_NAME, 100_u64),
7729            (SECOND_ENTITY_NAME, 1_100),
7730            (THIRD_ENTITY_NAME, 2_100),
7731        ] {
7732            let result = session
7733                .execute_trusted_live_page(
7734                    &DynamicQuery::new(entity_name)
7735                        .filter(crate::db::FieldRef::new("payload").eq(payload))
7736                        .select(["id", "payload"])
7737                        .order_by(crate::db::asc("id"))
7738                        .limit(64),
7739                    None,
7740                )
7741                .expect("every recovered mixed entity should remain queryable");
7742            assert_eq!(result.rows.len(), 1);
7743        }
7744        let retained_relation = session
7745            .execute_trusted_dynamic_mutation_batch(vec![DynamicMutation::Delete {
7746                entity: ENTITY_NAME.to_string(),
7747                key: InputValue::nat64(1),
7748            }])
7749            .expect_err("the recovered reverse relation must protect its target");
7750        assert!(retained_relation.diagnostic_facts().contains(&(
7751            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
7752            icydb_diagnostic_code::DiagnosticConstraintKind::Relation.raw(),
7753        )));
7754        JOURNALED_SCHEMA_STORE.with(|store| {
7755            let store = store.borrow();
7756            for entity_tag in [ENTITY_TAG, SECOND_ENTITY_TAG, THIRD_ENTITY_TAG] {
7757                let cursor = store
7758                    .identity_statement_cursor(
7759                        database_incarnation_id()
7760                            .expect("database incarnation should remain readable"),
7761                        entity_tag,
7762                        FieldId::new(1),
7763                        &AcceptedFieldKind::Nat64,
7764                    )
7765                    .expect("every mixed Identity owner should remain readable");
7766                assert_eq!(cursor.expected_high_water(), 1);
7767                assert!(!cursor.has_allocations());
7768            }
7769        });
7770        JOURNALED_TAIL_STORE.with(|tail| {
7771            let tail = tail.borrow();
7772            assert_eq!(
7773                tail.entity_mutation_revision(ENTITY_TAG)
7774                    .expect("first entity revision should remain readable"),
7775                2,
7776            );
7777            assert_eq!(
7778                tail.entity_mutation_revision(SECOND_ENTITY_TAG)
7779                    .expect("second entity revision should remain readable"),
7780                2,
7781            );
7782            assert_eq!(
7783                tail.entity_mutation_revision(THIRD_ENTITY_TAG)
7784                    .expect("third entity revision should remain readable"),
7785                2,
7786            );
7787        });
7788    }
7789
7790    fn assert_mixed_entity_recovery(interruption: MutationCommitInterruption) {
7791        let session = initialize_journaled_multi_entity();
7792        interrupt_next_mutation_commit_for_tests(interruption);
7793        let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
7794            DynamicMutation::Insert {
7795                entity: ENTITY_NAME.to_string(),
7796                patch: dynamic_payload_patch(100),
7797            },
7798            DynamicMutation::Insert {
7799                entity: SECOND_ENTITY_NAME.to_string(),
7800                patch: related_dynamic_payload_patch(1_100, 1),
7801            },
7802            DynamicMutation::Insert {
7803                entity: THIRD_ENTITY_NAME.to_string(),
7804                patch: dynamic_payload_patch(2_100),
7805            },
7806        ]);
7807        let interruption_error =
7808            interrupted.expect_err("the selected marker boundary should interrupt");
7809        assert_eq!(interruption_error.class(), ErrorClass::InvariantViolation);
7810        if interruption == MutationCommitInterruption::MarkerPersisted {
7811            let (marker_bytes, journal_batch_bytes) =
7812                crate::db::commit::retained_commit_marker_measurement_for_tests()
7813                    .expect("the retained marker measurement should remain readable")
7814                    .expect("marker persistence should retain one marker");
7815            assert_eq!(marker_bytes, 770);
7816            assert_eq!(journal_batch_bytes, vec![740]);
7817        }
7818        if interruption != MutationCommitInterruption::MarkerPersisted {
7819            let retained_batch = JOURNALED_TAIL_STORE.with(|tail| {
7820                let tail = tail.borrow();
7821                let watermark = tail
7822                    .fold_watermark()
7823                    .expect("the interrupted fold watermark should decode")
7824                    .highest_folded_journal_sequence();
7825                tail.next_batch_after(watermark)
7826                    .expect("the interrupted journal tail should decode")
7827                    .expect("the interrupted marker should publish one journal batch")
7828            });
7829            let row_paths = retained_batch
7830                .records()
7831                .iter()
7832                .filter_map(|record| match record {
7833                    JournalRecord::RowPut { entity_path, .. }
7834                    | JournalRecord::RowDelete { entity_path, .. } => Some(entity_path.as_str()),
7835                    _ => None,
7836                })
7837                .collect::<Vec<_>>();
7838            assert_eq!(
7839                row_paths,
7840                vec![ENTITY_SOURCE, SECOND_ENTITY_SOURCE, THIRD_ENTITY_SOURCE],
7841            );
7842        }
7843
7844        forget_recovered_domain_for_tests(&session.db)
7845            .expect("the retained mixed marker should reset volatile recovery ownership");
7846        drive_journaled_recovery_to_completion(&session);
7847        assert_mixed_entity_recovered_state(&session);
7848    }
7849
7850    #[test]
7851    fn mixed_entity_recovery_after_marker_persistence() {
7852        assert_mixed_entity_recovery(MutationCommitInterruption::MarkerPersisted);
7853    }
7854
7855    #[test]
7856    fn mixed_entity_recovery_after_journal_publication() {
7857        assert_mixed_entity_recovery(MutationCommitInterruption::JournalPublished);
7858    }
7859
7860    #[test]
7861    fn mixed_entity_recovery_after_row_prefix_publication() {
7862        assert_mixed_entity_recovery(MutationCommitInterruption::RowPrefixPublished);
7863    }
7864
7865    #[test]
7866    fn mixed_entity_recovery_after_all_rows_publish() {
7867        assert_mixed_entity_recovery(MutationCommitInterruption::RowsPublished);
7868    }
7869
7870    #[test]
7871    fn mixed_entity_recovery_after_state_materialization() {
7872        assert_mixed_entity_recovery(MutationCommitInterruption::StateMaterialized);
7873    }
7874
7875    #[test]
7876    fn startup_recovery_initializes_missing_entity_revisions_from_the_store_revision() {
7877        let session = initialize_journaled();
7878        let catalog = session
7879            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
7880            .expect("journaled predecessor catalog should resolve");
7881        session
7882            .execute_accepted_structural_save_batch(
7883                &catalog,
7884                true,
7885                batch(&[901]),
7886                Timestamp::from_millis(21),
7887                Ok,
7888            )
7889            .expect("predecessor row should advance the store-wide revision");
7890        let baseline = JOURNALED_TAIL_STORE.with(|tail| {
7891            let mut tail = tail.borrow_mut();
7892            let baseline = tail
7893                .data_mutation_revision()
7894                .expect("predecessor store-wide revision should load");
7895            tail.clear_entity_mutation_revisions_for_tests();
7896            baseline
7897        });
7898
7899        forget_recovered_domain_for_tests(&session.db)
7900            .expect("upgrade should reset volatile recovery ownership");
7901        drive_journaled_recovery_to_completion(&session);
7902
7903        let recovered = JOURNALED_TAIL_STORE
7904            .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
7905            .expect("recovery should publish the current entity authority");
7906        assert_eq!(recovered, baseline);
7907    }
7908
7909    #[test]
7910    fn mutation_progress_neither_side_mismatch_blocks_recovery() {
7911        let session = initialize_journaled();
7912        let catalog = session
7913            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
7914            .expect("journaled corruption catalog should resolve");
7915        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
7916            .expect("journaled corruption row layout should build");
7917        let (before, _after, operation) = atomic_progress_fixture(41);
7918        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7919            match store.insert_mutation(&before)? {
7920                InsertMutationJobResult::Inserted => Ok(()),
7921                InsertMutationJobResult::Occupied(_) => {
7922                    Err(crate::db::MutationJobError::IdentityConflict)
7923                }
7924            }
7925        })
7926        .expect("corruption predecessor should insert once");
7927
7928        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::MarkerPersisted);
7929        assert!(
7930            session
7931                .execute_accepted_structural_update_with_mutation_progress(
7932                    &catalog,
7933                    &descriptor,
7934                    batch(&[811]),
7935                    Timestamp::from_millis(19),
7936                    operation,
7937                )
7938                .is_err(),
7939            "marker interruption should retain recovery authority",
7940        );
7941        let (unexpected, _) = before
7942            .apply_transition(
7943                &MutationJobAdvanceRequest::new(
7944                    before.state().job_id,
7945                    0,
7946                    MutationJobIdempotencyKey::new("unexpected-third-state")
7947                        .expect("unexpected replay key should admit"),
7948                ),
7949                MutationJobTransition::new(
7950                    MutationJobStatus::Active,
7951                    MutationJobPhase::Forward,
7952                    vec![99],
7953                    2,
7954                    0,
7955                    0,
7956                ),
7957            )
7958            .expect("unexpected but valid progress state should admit");
7959        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7960            store.replace_mutation(&unexpected)
7961        })
7962        .expect("test should install the neither-side state");
7963
7964        forget_recovered_domain_for_tests(&session.db)
7965            .expect("corrupt recovery ownership should reset");
7966        let error = session
7967            .db
7968            .drive_startup_recovery_page()
7969            .expect_err("neither-side progress must block recovery");
7970        assert_eq!(error.class(), ErrorClass::Corruption);
7971        assert_eq!(error.origin(), ErrorOrigin::Recovery);
7972        assert_eq!(
7973            with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
7974                store.load_mutation(before.state().job_id)
7975            })
7976            .expect("unexpected state should remain inspectable to the test"),
7977            unexpected,
7978        );
7979        assert!(
7980            session.db.drive_startup_recovery_page().is_err(),
7981            "a retained corrupt marker must continue blocking database access",
7982        );
7983    }
7984
7985    #[test]
7986    #[expect(
7987        clippy::too_many_lines,
7988        reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
7989    )]
7990    fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
7991        let session = initialize_journaled();
7992        let catalog = session
7993            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
7994            .expect("journaled identity catalog should resolve");
7995
7996        for (ordinal, interruption) in [
7997            MutationCommitInterruption::MarkerPersisted,
7998            MutationCommitInterruption::JournalPublished,
7999            MutationCommitInterruption::RowsPublished,
8000            MutationCommitInterruption::StateMaterialized,
8001        ]
8002        .into_iter()
8003        .enumerate()
8004        {
8005            interrupt_next_mutation_commit_for_tests(interruption);
8006            let interrupted = session.execute_accepted_structural_save_batch(
8007                &catalog,
8008                true,
8009                batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
8010                Timestamp::from_millis(8),
8011                Ok,
8012            );
8013            assert!(
8014                interrupted.is_err(),
8015                "the selected durable boundary should interrupt",
8016            );
8017
8018            let Err(pending) = session.execute_accepted_structural_save_batch(
8019                &catalog,
8020                true,
8021                batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
8022                Timestamp::from_millis(9),
8023                Ok,
8024            ) else {
8025                panic!("ordinary mutation must not drive retained-marker recovery");
8026            };
8027            assert_eq!(
8028                pending.diagnostic().error_code(),
8029                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
8030            );
8031            drive_journaled_recovery_to_completion(&session);
8032
8033            let committed = session
8034                .execute_accepted_structural_save_batch(
8035                    &catalog,
8036                    true,
8037                    batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
8038                    Timestamp::from_millis(9),
8039                    Ok,
8040                )
8041                .expect("the next mutation must recover before allocating");
8042            let expected_high_water =
8043                u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
8044            assert_eq!(
8045                committed
8046                    .into_iter()
8047                    .map(|row| row.values)
8048                    .collect::<Vec<_>>(),
8049                vec![vec![
8050                    Value::Nat64(expected_high_water),
8051                    Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
8052                ]],
8053            );
8054            assert_eq!(
8055                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
8056                expected_high_water,
8057            );
8058            JOURNALED_SCHEMA_STORE.with(|store| {
8059                let cursor = store
8060                    .borrow()
8061                    .identity_statement_cursor(
8062                        database_incarnation_id()
8063                            .expect("database incarnation should remain readable"),
8064                        ENTITY_TAG,
8065                        FieldId::new(1),
8066                        &AcceptedFieldKind::Nat64,
8067                    )
8068                    .expect("guarded recovery must leave quiescent active state");
8069                assert_eq!(
8070                    cursor.expected_high_water(),
8071                    u128::from(expected_high_water),
8072                );
8073                assert!(!cursor.has_allocations());
8074            });
8075        }
8076
8077        for (ordinal, (interruption, deleted_key)) in [
8078            (MutationCommitInterruption::MarkerPersisted, 2),
8079            (MutationCommitInterruption::JournalPublished, 4),
8080            (MutationCommitInterruption::RowPrefixPublished, 6),
8081            (MutationCommitInterruption::RowsPublished, 8),
8082            (MutationCommitInterruption::StateMaterialized, 7),
8083        ]
8084        .into_iter()
8085        .enumerate()
8086        {
8087            let expected_payload =
8088                501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
8089            interrupt_next_mutation_commit_for_tests(interruption);
8090            let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
8091                DynamicMutation::Update {
8092                    entity: ENTITY_NAME.to_string(),
8093                    key: InputValue::nat64(1),
8094                    patch: dynamic_payload_patch(expected_payload),
8095                },
8096                DynamicMutation::Delete {
8097                    entity: ENTITY_NAME.to_string(),
8098                    key: InputValue::nat64(deleted_key),
8099                },
8100            ]);
8101            assert!(
8102                interrupted.is_err(),
8103                "the selected caller-key mixed publication boundary should interrupt",
8104            );
8105            let pending = session
8106                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
8107                    entity: ENTITY_NAME.to_string(),
8108                    key: InputValue::nat64(1),
8109                    patch: dynamic_payload_patch(expected_payload),
8110                })
8111                .expect_err("ordinary update must not drive retained-marker recovery");
8112            assert_eq!(
8113                pending.diagnostic().error_code(),
8114                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
8115            );
8116            drive_journaled_recovery_to_completion(&session);
8117            let recovered_update = session
8118                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
8119                    entity: ENTITY_NAME.to_string(),
8120                    key: InputValue::nat64(1),
8121                    patch: dynamic_payload_patch(expected_payload),
8122                })
8123                .expect("guarded reentry should complete the marker-authorized mixed batch");
8124            assert_eq!(
8125                recovered_update.affected_rows, 0,
8126                "the recovered update must already expose its admitted final image",
8127            );
8128            let recovered_delete = session
8129                .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
8130                    entity: ENTITY_NAME.to_string(),
8131                    key: InputValue::nat64(deleted_key),
8132                })
8133                .expect_err("the recovered delete must already be materialized");
8134            assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
8135            JOURNALED_SCHEMA_STORE.with(|store| {
8136                let cursor = store
8137                    .borrow()
8138                    .identity_statement_cursor(
8139                        database_incarnation_id()
8140                            .expect("database incarnation should remain readable"),
8141                        ENTITY_TAG,
8142                        FieldId::new(1),
8143                        &AcceptedFieldKind::Nat64,
8144                    )
8145                    .expect("caller-key recovery must preserve active Identity state");
8146                assert_eq!(cursor.expected_high_water(), 8);
8147                assert!(!cursor.has_allocations());
8148            });
8149        }
8150
8151        forget_recovered_domain_for_tests(&session.db)
8152            .expect("the final journal tail should remain recoverable");
8153        session
8154            .db
8155            .drive_startup_recovery_page()
8156            .expect("derived rebuild must not allocate another identity");
8157
8158        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
8159        let index_generation = JOURNALED_INDEX_STORE.with(|store| store.borrow().generation());
8160        let data_len = JOURNALED_DATA_STORE.with(|store| store.borrow().len());
8161        let index_len = JOURNALED_INDEX_STORE.with(|store| store.borrow().len());
8162        forget_recovered_domain_for_tests(&session.db)
8163            .expect("an empty-tail upgrade should reset recovery ownership");
8164        session
8165            .db
8166            .drive_startup_recovery_page()
8167            .expect("an empty-tail upgrade should admit without rebuilding stored rows or indexes");
8168        assert_eq!(
8169            JOURNALED_DATA_STORE.with(|store| store.borrow().generation()),
8170            data_generation
8171                .checked_add(1)
8172                .expect("test generation should advance once"),
8173            "empty-tail recovery must reset the disposable row projection exactly once",
8174        );
8175        assert_eq!(
8176            JOURNALED_INDEX_STORE.with(|store| store.borrow().generation()),
8177            index_generation
8178                .checked_add(1)
8179                .expect("test generation should advance once"),
8180            "empty-tail recovery must reset the disposable index projection exactly once",
8181        );
8182        assert_eq!(
8183            JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
8184            data_len,
8185            "empty-tail recovery must not rebuild or remove authoritative rows",
8186        );
8187        assert_eq!(
8188            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
8189            index_len,
8190            "empty-tail recovery must not clear or rebuild canonical secondary indexes",
8191        );
8192
8193        let quick = execute_quick_integrity(
8194            &session.db,
8195            catalog.inspection_plan(),
8196            catalog.runtime_root_identity().database_incarnation(),
8197        )
8198        .expect("quiescent Identity control inventory should be inspectable");
8199        assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
8200        let row_page = execute_row_integrity_page(
8201            &session.db,
8202            catalog.inspection_plan(),
8203            PhysicalUnitCheckpoint::BeforeFirst,
8204            RowInspectionLimits::standard(),
8205        )
8206        .expect("Identity rows should remain within committed high-water");
8207        assert!(row_page.exhausted());
8208        assert!(row_page.findings().is_empty());
8209
8210        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
8211        assert!(
8212            JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
8213            "derived index rebuild should restore witnesses without allocating identities",
8214        );
8215        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
8216        JOURNALED_SCHEMA_STORE.with(|store| {
8217            let cursor = store
8218                .borrow()
8219                .identity_statement_cursor(
8220                    database_incarnation_id().expect("database incarnation should remain readable"),
8221                    ENTITY_TAG,
8222                    FieldId::new(1),
8223                    &AcceptedFieldKind::Nat64,
8224                )
8225                .expect("folded identity state should reopen without allocating");
8226            assert_eq!(cursor.expected_high_water(), 8);
8227            assert!(!cursor.has_allocations());
8228        });
8229    }
8230
8231    #[test]
8232    fn journaled_online_convergence_drains_the_full_backlog_in_complete_batch_callbacks_without_reallocating_ids()
8233     {
8234        const SUBMISSION: &str = "generated/8899aabbccddeeff";
8235        let session = initialize_journaled();
8236        let catalog = session
8237            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
8238            .expect("journaled identity catalog should resolve");
8239
8240        for payload in 0_u64..64 {
8241            session
8242                .execute_accepted_structural_save_batch(
8243                    &catalog,
8244                    true,
8245                    batch(&[payload]),
8246                    Timestamp::from_millis(8),
8247                    Ok,
8248                )
8249                .unwrap_or_else(|error| {
8250                    panic!("journaled identity fixture row {payload} should commit: {error:?}")
8251                });
8252        }
8253
8254        let before = JOURNALED_TAIL_STORE.with(|tail| {
8255            tail.borrow()
8256                .current_tail_control()
8257                .expect("online backlog control should remain valid")
8258        });
8259        assert_eq!(before.batch_count(), 64);
8260        let next_sequence = crate::db::commit::next_database_commit_sequence()
8261            .expect("database sequence preview should remain readable");
8262        let Err(pressure) = session.execute_accepted_structural_save_batch(
8263            &catalog,
8264            true,
8265            batch(&[64]),
8266            Timestamp::from_millis(8),
8267            Ok,
8268        ) else {
8269            panic!("the exact cumulative batch ceiling should reject one more batch")
8270        };
8271        assert_exact_batch_backlog_pressure(&pressure, before, next_sequence);
8272
8273        for folded_batches in 1..=64 {
8274            let complete = session
8275                .db
8276                .drive_startup_recovery_page()
8277                .expect("online complete-batch callback should commit");
8278            assert_eq!(complete, folded_batches == 64);
8279        }
8280
8281        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
8282        session
8283            .execute_accepted_structural_save_batch(
8284                &catalog,
8285                true,
8286                batch(&[64]),
8287                Timestamp::from_millis(8),
8288                Ok,
8289            )
8290            .expect("drain should make the rejected mutation retryable");
8291        assert!(
8292            session
8293                .db
8294                .drive_startup_recovery_page()
8295                .expect("the retry tail should converge"),
8296        );
8297
8298        assert_eq!(
8299            drive_generated_startup_recovery_page(&session, &JOURNALED_STORE_REGISTRY, SUBMISSION,)
8300                .expect("online convergence should commit"),
8301            GeneratedStartupDriverStep::ApplyGeneratedSchema,
8302            "journal convergence does not complete an unsubmitted generated schema",
8303        );
8304        assert!(
8305            session
8306                .db
8307                .drive_startup_recovery_page()
8308                .expect("the drained journal should remain quiescent"),
8309        );
8310
8311        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 65);
8312        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
8313        assert_dynamic_payload(&session, 1, 0);
8314        assert_dynamic_payload(&session, 65, 64);
8315        JOURNALED_SCHEMA_STORE.with(|store| {
8316            let cursor = store
8317                .borrow()
8318                .identity_statement_cursor(
8319                    database_incarnation_id().expect("database incarnation should remain readable"),
8320                    ENTITY_TAG,
8321                    FieldId::new(1),
8322                    &AcceptedFieldKind::Nat64,
8323                )
8324                .expect("online convergence must preserve active Identity state");
8325            assert_eq!(cursor.expected_high_water(), 65);
8326            assert!(!cursor.has_allocations());
8327        });
8328    }
8329
8330    #[test]
8331    fn journaled_online_convergence_reconstructs_same_key_batches_from_canonical_predecessors() {
8332        let session = initialize_journaled();
8333        session
8334            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
8335                entity: ENTITY_NAME.to_string(),
8336                patch: dynamic_payload_patch(10),
8337            })
8338            .expect("the initial positioned row should commit");
8339        for payload in [20, 30] {
8340            session
8341                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
8342                    entity: ENTITY_NAME.to_string(),
8343                    key: InputValue::nat64(1),
8344                    patch: dynamic_payload_patch(payload),
8345                })
8346                .unwrap_or_else(|error| {
8347                    panic!("the positioned same-key update should commit: {error:?}")
8348                });
8349        }
8350
8351        assert_dynamic_payload(&session, 1, 30);
8352        assert_eq!(
8353            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
8354            1,
8355            "the newest live index effect should hide every predecessor",
8356        );
8357        for folded_batches in 1..=3 {
8358            let complete = session
8359                .db
8360                .drive_startup_recovery_page()
8361                .expect("the positioned same-key batch should converge");
8362            assert_eq!(complete, folded_batches == 3);
8363        }
8364
8365        assert_dynamic_payload(&session, 1, 30);
8366        assert_eq!(
8367            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
8368            1,
8369            "canonical derived state must contain only the newest membership",
8370        );
8371        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
8372    }
8373
8374    #[test]
8375    fn ready_cardinality_combines_durable_base_with_exact_live_delta_and_fold_maintenance() {
8376        let session = initialize_journaled();
8377        session
8378            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
8379                entity: ENTITY_NAME.to_string(),
8380                patch: dynamic_payload_patch(10),
8381            })
8382            .expect("initial cardinality row should commit");
8383        assert!(
8384            session
8385                .db
8386                .drive_startup_recovery_page()
8387                .expect("initial cardinality row should fold"),
8388        );
8389        drive_journaled_cardinality_to_ready(&session);
8390        let handle = session
8391            .db
8392            .store_handle(JOURNALED_STORE_PATH)
8393            .expect("journaled cardinality store should resolve");
8394        let (index_id, prefix_components) = journaled_user_index_prefix();
8395        reset_journaled_cardinality_projections();
8396        assert_eq!(
8397            JOURNALED_DATA_STORE.with(|store| store.borrow().exact_entity_count(ENTITY_TAG)),
8398            None,
8399            "the reopened-style volatile full count must remain unavailable",
8400        );
8401        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
8402
8403        session
8404            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
8405                entity: ENTITY_NAME.to_string(),
8406                patch: dynamic_payload_patch(10),
8407            })
8408            .expect("post-Ready row should commit into the live overlay");
8409        for payload in [20, 10] {
8410            session
8411                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
8412                    entity: ENTITY_NAME.to_string(),
8413                    key: InputValue::nat64(2),
8414                    patch: dynamic_payload_patch(payload),
8415                })
8416                .expect("same-key post-Ready overlay should commit");
8417        }
8418        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
8419        for folded in 1..=3 {
8420            let complete = session
8421                .db
8422                .drive_startup_recovery_page()
8423                .expect("post-Ready row should fold with exact maintenance");
8424            assert_eq!(complete, folded == 3);
8425            assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
8426        }
8427        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
8428        session
8429            .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
8430                entity: ENTITY_NAME.to_string(),
8431                key: InputValue::nat64(2),
8432            })
8433            .expect("post-Ready delete should commit into the live overlay");
8434        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
8435        assert!(
8436            session
8437                .db
8438                .drive_startup_recovery_page()
8439                .expect("post-Ready delete should fold with exact maintenance"),
8440        );
8441        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
8442        mark_journaled_cardinality_building();
8443        assert_eq!(
8444            handle.exact_entity_count(ENTITY_TAG),
8445            None,
8446            "non-Ready evidence must select the conservative path",
8447        );
8448        #[cfg(feature = "sql")]
8449        {
8450            let data_reads_before = DataStore::current_get_call_count();
8451            let crate::db::SqlStatementResult::Projection { rows, .. } = session
8452                .execute_trusted_sql_query("SELECT COUNT(*) FROM IdentityRow")
8453                .expect("non-Ready entity cardinality should retain SQL fallback")
8454            else {
8455                panic!("fallback count should return one projection row")
8456            };
8457            assert_eq!(rows, vec![vec![OutputValue::nat64(1)]]);
8458            assert_eq!(DataStore::current_get_call_count(), data_reads_before);
8459        }
8460    }
8461
8462    #[test]
8463    fn journaled_cardinality_rejects_volatile_counts_and_unfolded_accepted_root_drift() {
8464        let session = initialize_journaled();
8465        session
8466            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
8467                entity: ENTITY_NAME.to_string(),
8468                patch: dynamic_payload_patch(10),
8469            })
8470            .expect("cardinality fixture row should commit");
8471        assert!(
8472            session
8473                .db
8474                .drive_startup_recovery_page()
8475                .expect("cardinality fixture row should fold"),
8476        );
8477        drive_journaled_cardinality_to_ready(&session);
8478        let handle = session
8479            .db
8480            .store_handle(JOURNALED_STORE_PATH)
8481            .expect("journaled cardinality store should resolve");
8482        let (index_id, prefix_components) = journaled_user_index_prefix();
8483        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
8484
8485        assert_eq!(
8486            JOURNALED_DATA_STORE.with(|store| store.borrow().exact_entity_count(ENTITY_TAG)),
8487            Some(1),
8488            "the live full-count cache should be populated before accepted-root drift",
8489        );
8490        assert_eq!(
8491            JOURNALED_INDEX_STORE.with(|store| {
8492                store.borrow().exact_prefix_cardinality(
8493                    data_generation,
8494                    IndexKeyKind::User,
8495                    index_id,
8496                    prefix_components.as_slice(),
8497                )
8498            }),
8499            Some(1),
8500            "the live prefix-count cache should be populated before accepted-root drift",
8501        );
8502        assert_eq!(
8503            JOURNALED_INDEX_STORE.with(|store| {
8504                store.borrow().exact_child_prefixes_for_parent_set(
8505                    data_generation,
8506                    IndexKeyKind::User,
8507                    index_id,
8508                    [prefix_components.as_slice()],
8509                    8,
8510                )
8511            }),
8512            Some(Vec::new()),
8513            "the volatile child-prefix cache should demonstrate the bypass fixture",
8514        );
8515        assert_eq!(
8516            handle.exact_user_index_child_prefixes_for_parent_set(
8517                data_generation,
8518                index_id,
8519                [prefix_components.as_slice()],
8520                8,
8521            ),
8522            None,
8523            "journaled child enumeration must use its conservative route instead of volatile authority",
8524        );
8525        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
8526
8527        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
8528            JOURNALED_STORE_PATH,
8529            AcceptedSchemaRevision::new(2),
8530            BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH, false))]),
8531            BTreeMap::from([
8532                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
8533                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
8534            ]),
8535        );
8536        crate::db::commit::publish_accepted_schema_candidate(
8537            JOURNALED_STORE_PATH,
8538            handle,
8539            AcceptedSchemaRevision::INITIAL,
8540            &candidate,
8541        )
8542        .expect("a successor accepted root should publish into the live overlay");
8543
8544        assert_eq!(
8545            handle.exact_entity_count(ENTITY_TAG),
8546            None,
8547            "an unfolded accepted root must invalidate durable evidence immediately",
8548        );
8549        assert_eq!(
8550            handle.exact_user_index_prefix_count(
8551                data_generation,
8552                IndexKeyKind::User,
8553                index_id,
8554                prefix_components.as_slice(),
8555            ),
8556            None,
8557            "journaled consumers must not fall back to a populated volatile prefix cache",
8558        );
8559    }
8560
8561    #[test]
8562    fn journaled_convergence_uses_final_batch_rows_for_unique_release() {
8563        let session = initialize_journaled_with_unique_payload();
8564        let inserted = session
8565            .execute_trusted_dynamic_insert_batch(
8566                ENTITY_NAME,
8567                vec![dynamic_payload_patch(10), dynamic_payload_patch(20)],
8568            )
8569            .expect("the unique journal fixture should commit");
8570        assert_eq!(
8571            inserted.rows,
8572            vec![expected_dynamic_row(1, 10), expected_dynamic_row(2, 20)],
8573        );
8574        assert!(
8575            session
8576                .db
8577                .drive_startup_recovery_page()
8578                .expect("the unique fixture should become canonical"),
8579        );
8580
8581        let swapped = session
8582            .execute_trusted_dynamic_mutation_batch(vec![
8583                DynamicMutation::Update {
8584                    entity: ENTITY_NAME.to_string(),
8585                    key: InputValue::nat64(1),
8586                    patch: dynamic_payload_patch(20),
8587                },
8588                DynamicMutation::Update {
8589                    entity: ENTITY_NAME.to_string(),
8590                    key: InputValue::nat64(2),
8591                    patch: dynamic_payload_patch(10),
8592                },
8593            ])
8594            .expect("one journal batch should admit a final-row unique swap");
8595        assert_eq!(
8596            batch_rows(&swapped),
8597            vec![expected_dynamic_row(1, 20), expected_dynamic_row(2, 10)],
8598        );
8599        assert!(
8600            session
8601                .db
8602                .drive_startup_recovery_page()
8603                .expect("the unique swap should converge in one complete batch"),
8604        );
8605
8606        let released = session
8607            .execute_trusted_dynamic_mutation_batch(vec![
8608                DynamicMutation::Delete {
8609                    entity: ENTITY_NAME.to_string(),
8610                    key: InputValue::nat64(1),
8611                },
8612                DynamicMutation::Insert {
8613                    entity: ENTITY_NAME.to_string(),
8614                    patch: dynamic_payload_patch(20),
8615                },
8616            ])
8617            .expect("a journaled delete should release its unique value to the final insert");
8618        assert_eq!(
8619            batch_rows(&released),
8620            vec![expected_dynamic_row(1, 20), expected_dynamic_row(3, 20)],
8621        );
8622        assert!(
8623            session
8624                .db
8625                .drive_startup_recovery_page()
8626                .expect("the delete and unique reuse should converge together"),
8627        );
8628
8629        assert_dynamic_payload(&session, 2, 10);
8630        assert_dynamic_payload(&session, 3, 20);
8631        assert_eq!(JOURNALED_INDEX_STORE.with(|store| store.borrow().len()), 2);
8632        assert!(
8633            session
8634                .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(20)],)
8635                .is_err(),
8636            "the converged unique index must remain authoritative",
8637        );
8638    }
8639
8640    #[test]
8641    fn journaled_startup_recovery_completes_one_large_batch_atomically() {
8642        let session = initialize_journaled();
8643        let catalog = session
8644            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
8645            .expect("journaled identity catalog should resolve");
8646        let payloads = (0_u64..129).collect::<Vec<_>>();
8647        session
8648            .execute_accepted_structural_save_batch(
8649                &catalog,
8650                true,
8651                batch(&payloads),
8652                Timestamp::from_millis(9),
8653                Ok,
8654            )
8655            .expect("one large journal batch should commit");
8656
8657        forget_recovered_domain_for_tests(&session.db)
8658            .expect("upgrade should reset recovery ownership");
8659        assert!(
8660            !session
8661                .db
8662                .drive_startup_recovery_page()
8663                .expect("replay should precede folding")
8664        );
8665        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 0);
8666        assert!(
8667            !session
8668                .db
8669                .drive_startup_recovery_page()
8670                .expect("the complete batch recovery page should commit"),
8671        );
8672
8673        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 129);
8674        JOURNALED_TAIL_STORE.with(|tail| {
8675            let tail = tail.borrow();
8676            assert!(!tail.has_stored_batch());
8677        });
8678        assert!(session.db.ensure_recovered_state().is_err());
8679        assert!(
8680            session
8681                .db
8682                .drive_startup_recovery_page()
8683                .expect("verification should finish startup")
8684        );
8685        assert_dynamic_payload(&session, 1, 0);
8686        assert_dynamic_payload(&session, 129, 128);
8687    }
8688
8689    #[test]
8690    fn complete_batch_validation_rejects_a_late_record_before_canonical_writes() {
8691        let session = initialize_journaled();
8692        let catalog = session
8693            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
8694            .expect("journaled identity catalog should resolve");
8695        session
8696            .execute_accepted_structural_save_batch(
8697                &catalog,
8698                true,
8699                batch(&[7]),
8700                Timestamp::from_millis(9),
8701                Ok,
8702            )
8703            .expect("journal batch predecessor should commit");
8704
8705        JOURNALED_TAIL_STORE.with(|tail| {
8706            let mut tail = tail.borrow_mut();
8707            let original = tail
8708                .next_batch_after(JournalSequence::new(0))
8709                .expect("journal batch should decode")
8710                .expect("journal batch should exist");
8711            let mut records = original.records().to_vec();
8712            records.push(
8713                JournalRecord::schema_put(JOURNALED_STORE_PATH, vec![0xff; 8])
8714                    .expect("bounded semantic corruption should build"),
8715            );
8716            let corrupted = JournalBatch::new_with_database_commit_sequence(
8717                original.batch_id(),
8718                original.commit_marker_id(),
8719                original.journal_sequence(),
8720                original.database_commit_sequence(),
8721                records,
8722            )
8723            .expect("current corrupt batch shape should build");
8724            let encoded = encode_journal_batch(&corrupted)
8725                .expect("current corrupt batch envelope should encode");
8726            tail.clear_batches_through(original.journal_sequence());
8727            tail.insert_raw_batch_for_tests(original.journal_sequence(), encoded)
8728                .expect("corrupt persisted batch should replace the predecessor");
8729        });
8730
8731        forget_recovered_domain_for_tests(&session.db)
8732            .expect("upgrade should reset recovery ownership");
8733        assert!(
8734            !session
8735                .db
8736                .drive_startup_recovery_page()
8737                .expect("replay should precede fold validation")
8738        );
8739        let error = session
8740            .db
8741            .drive_startup_recovery_page()
8742            .expect_err("late semantic corruption must fail before fold apply");
8743        assert_eq!(error.class(), ErrorClass::Corruption);
8744        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 0);
8745        JOURNALED_TAIL_STORE.with(|tail| {
8746            let tail = tail.borrow();
8747            assert_eq!(
8748                tail.fold_watermark()
8749                    .expect("watermark should remain readable")
8750                    .highest_folded_journal_sequence(),
8751                JournalSequence::new(0),
8752            );
8753            assert!(tail.has_stored_batch());
8754        });
8755    }
8756
8757    #[test]
8758    fn prepared_batch_row_evidence_rejects_a_late_malformed_row_before_canonical_writes() {
8759        let session = initialize_journaled();
8760        let catalog = session
8761            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
8762            .expect("journaled identity catalog should resolve");
8763        session
8764            .execute_accepted_structural_save_batch(
8765                &catalog,
8766                true,
8767                batch(&[7, 8]),
8768                Timestamp::from_millis(9),
8769                Ok,
8770            )
8771            .expect("two-row journal batch should commit");
8772
8773        JOURNALED_TAIL_STORE.with(|tail| {
8774            let mut tail = tail.borrow_mut();
8775            let original = tail
8776                .next_batch_after(JournalSequence::new(0))
8777                .expect("journal batch should decode")
8778                .expect("journal batch should exist");
8779            let mut records = original.records().to_vec();
8780            let mut row_ordinal = 0_u8;
8781            for record in &mut records {
8782                if let JournalRecord::RowPut { row_bytes, .. } = record {
8783                    row_ordinal = row_ordinal.saturating_add(1);
8784                    if row_ordinal == 2 {
8785                        *row_bytes = vec![0xff; 8];
8786                        break;
8787                    }
8788                }
8789            }
8790            assert_eq!(row_ordinal, 2, "the late row record should be present");
8791            let corrupted = JournalBatch::new_with_database_commit_sequence(
8792                original.batch_id(),
8793                original.commit_marker_id(),
8794                original.journal_sequence(),
8795                original.database_commit_sequence(),
8796                records,
8797            )
8798            .expect("current corrupt batch shape should build");
8799            let encoded = encode_journal_batch(&corrupted)
8800                .expect("current corrupt batch envelope should encode");
8801            tail.clear_batches_through(original.journal_sequence());
8802            tail.insert_raw_batch_for_tests(original.journal_sequence(), encoded)
8803                .expect("corrupt persisted batch should replace the predecessor");
8804        });
8805
8806        forget_recovered_domain_for_tests(&session.db)
8807            .expect("upgrade should reset recovery ownership");
8808        assert!(
8809            !session
8810                .db
8811                .drive_startup_recovery_page()
8812                .expect("replay should precede row preparation")
8813        );
8814        let error = session
8815            .db
8816            .drive_startup_recovery_page()
8817            .expect_err("late malformed row must fail during complete batch preparation");
8818        assert_eq!(error.class(), ErrorClass::Corruption);
8819        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 0);
8820        JOURNALED_TAIL_STORE.with(|tail| {
8821            let tail = tail.borrow();
8822            assert_eq!(
8823                tail.fold_watermark()
8824                    .expect("watermark should remain readable")
8825                    .highest_folded_journal_sequence(),
8826                JournalSequence::new(0),
8827            );
8828            assert!(tail.has_stored_batch());
8829        });
8830    }
8831
8832    #[test]
8833    fn typed_mutation_batch_recovers_as_one_marker_atomic_transition() {
8834        let session = initialize_journaled();
8835        let binding = exact_key_binding(&session);
8836        session
8837            .execute_trusted_same_entity_typed_mutation_batch(
8838                &binding,
8839                vec![
8840                    typed_payload_insert(&binding, 10),
8841                    typed_payload_insert(&binding, 20),
8842                ],
8843            )
8844            .expect("typed recovery fixture should commit")
8845            .expect("typed recovery fixture binding should remain current");
8846        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
8847
8848        let interrupted = session.execute_trusted_same_entity_typed_mutation_batch(
8849            &binding,
8850            vec![typed_payload_delete(1), typed_payload_insert(&binding, 30)],
8851        );
8852        assert!(
8853            interrupted.is_err(),
8854            "typed batch should expose the selected durable interruption",
8855        );
8856        let pending = session
8857            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
8858                entity: ENTITY_NAME.to_string(),
8859                patch: dynamic_payload_patch(30),
8860            })
8861            .expect_err("ordinary writes must not bypass retained-marker recovery");
8862        assert_eq!(
8863            pending.diagnostic().error_code(),
8864            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
8865        );
8866
8867        drive_journaled_recovery_to_completion(&session);
8868        let recovered = session
8869            .execute_trusted_live_page(&crate::db::DynamicQuery::new(ENTITY_NAME), None)
8870            .expect("the recovered typed batch should be readable");
8871        assert_eq!(
8872            recovered.rows,
8873            vec![expected_dynamic_row(2, 20), expected_dynamic_row(3, 30)],
8874        );
8875    }
8876}
8877
8878#[cfg(test)]
8879mod targeted_rule_mutation_tests {
8880    use super::{
8881        DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedMutation, DynamicWriteCell,
8882        TypedEntityDescriptor, TypedFieldType,
8883    };
8884    use crate::{
8885        db::{
8886            TypedFieldDescriptor,
8887            data::{DataStore, encode_input_value_for_candidate_field_contract},
8888            index::IndexStore,
8889            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
8890            schema::{
8891                AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
8892                AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
8893                AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
8894                ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
8895                PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
8896                ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
8897                SchemaRowLayout, SchemaStore, SchemaVersion,
8898                accepted_schema_candidate_with_catalogs_for_tests,
8899                build_record_newtype_composite_catalog_for_tests,
8900                empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
8901            },
8902        },
8903        error::InternalError,
8904        traits::{CanisterKind, Path},
8905        types::EntityTag,
8906        value::InputValue,
8907    };
8908    use icydb_schema::{
8909        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
8910    };
8911    use std::{cell::RefCell, collections::BTreeMap};
8912
8913    const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
8914    const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
8915    const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
8916    const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
8917    const UPDATED_AT_SOURCE: &str =
8918        "session::write::targeted_rule_mutation_tests::Entity::updated_at";
8919    const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
8920    const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
8921    const DEGREE_MEMBER_SOURCE: &str =
8922        "session::write::targeted_rule_mutation_tests::Profile::degree";
8923    const DEGREE_RULE_SOURCE: &str =
8924        "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
8925    const TYPED_DESCRIPTOR: TypedEntityDescriptor = TypedEntityDescriptor::new(
8926        ENTITY_SOURCE,
8927        &[ID_SOURCE],
8928        &[
8929            TypedFieldDescriptor::new(ID_SOURCE, TypedFieldType::Scalar(ScalarType::Nat64), false),
8930            TypedFieldDescriptor::new(
8931                PROFILE_SOURCE,
8932                TypedFieldType::Named(PROFILE_TYPE_SOURCE),
8933                false,
8934            ),
8935            TypedFieldDescriptor::new(
8936                UPDATED_AT_SOURCE,
8937                TypedFieldType::Scalar(ScalarType::Timestamp),
8938                false,
8939            ),
8940        ],
8941    );
8942
8943    struct TestCanister;
8944
8945    impl Path for TestCanister {
8946        const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
8947    }
8948
8949    impl CanisterKind for TestCanister {
8950        fn commit_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
8951            Ok(43)
8952        }
8953        const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
8954        fn startup_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
8955            Ok(49)
8956        }
8957        const STARTUP_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.startup.control.v1";
8958        fn integrity_progress_memory_id() -> Result<u8, ic_memory::RuntimeOpenError> {
8959            Ok(44)
8960        }
8961        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
8962            "icydb.targeted_mutation_tests.integrity.progress.v1";
8963    }
8964
8965    thread_local! {
8966        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
8967        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
8968        static SCHEMA_STORE: RefCell<SchemaStore> =
8969            const { RefCell::new(SchemaStore::init_heap()) };
8970        static STORE_REGISTRY: StoreRegistry = {
8971            let mut registry = StoreRegistry::new();
8972            registry.register_store(
8973                STORE_PATH,
8974                &DATA_STORE,
8975                &INDEX_STORE,
8976                &SCHEMA_STORE,
8977                StoreAllocationIdentities::absent(),
8978                StoreRuntimeStorageCapabilities::heap(),
8979            ).expect("targeted mutation test store should register");
8980            registry
8981        };
8982    }
8983
8984    fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
8985        parse(raw.to_string()).expect("test source identity should admit")
8986    }
8987
8988    fn profile_input(degree: u64) -> InputValue {
8989        InputValue::map(vec![(
8990            InputValue::from("degree"),
8991            InputValue::nat64(degree),
8992        )])
8993    }
8994
8995    fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
8996        DynamicStructuralPatch::new(vec![
8997            (
8998                "id".to_string(),
8999                DynamicWriteCell::Value(InputValue::nat64(id)),
9000            ),
9001            (
9002                "profile".to_string(),
9003                DynamicWriteCell::Value(profile_input(degree)),
9004            ),
9005        ])
9006    }
9007
9008    fn encoded_value(
9009        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
9010        composite_catalog: &AcceptedCompositeCatalog,
9011        name: &str,
9012        kind: &AcceptedFieldKind,
9013        storage_decode: FieldStorageDecode,
9014        leaf_codec: LeafCodec,
9015        value: InputValue,
9016    ) -> Vec<u8> {
9017        let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
9018        encode_input_value_for_candidate_field_contract(
9019            enum_catalog,
9020            composite_catalog,
9021            field,
9022            value,
9023            &mut ValueAdmissionBudget::standard(),
9024        )
9025        .expect("test accepted value should encode")
9026    }
9027
9028    fn nat64_literal(
9029        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
9030        composite_catalog: &AcceptedCompositeCatalog,
9031        value: u64,
9032    ) -> AcceptedCheckLiteralV1 {
9033        let kind = AcceptedFieldKind::Nat64;
9034        AcceptedCheckLiteralV1::from_accepted_parts(
9035            kind.clone(),
9036            FieldStorageDecode::ByKind,
9037            LeafCodec::Scalar(ScalarCodec::Nat64),
9038            encoded_value(
9039                enum_catalog,
9040                composite_catalog,
9041                "degree_bound",
9042                &kind,
9043                FieldStorageDecode::ByKind,
9044                LeafCodec::Scalar(ScalarCodec::Nat64),
9045                InputValue::nat64(value),
9046            ),
9047        )
9048    }
9049
9050    fn targeted_constraint_id(error: &InternalError) -> u32 {
9051        let facts = error.diagnostic_facts();
9052        assert!(facts.contains(&(
9053            icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
9054            icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
9055        )));
9056        assert!(facts.contains(&(icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,)));
9057        assert!(facts.contains(&(
9058            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
9059            icydb_diagnostic_code::DiagnosticConstraintKind::TargetedRule.raw(),
9060        )));
9061        assert_eq!(
9062            facts
9063                .iter()
9064                .filter(|(tag, _)| matches!(
9065                    tag,
9066                    icydb_diagnostic_code::DiagnosticFactTag::RootField
9067                        | icydb_diagnostic_code::DiagnosticFactTag::RecordMember
9068                ))
9069                .copied()
9070                .collect::<Vec<_>>(),
9071            vec![
9072                (icydb_diagnostic_code::DiagnosticFactTag::RootField, 2),
9073                (
9074                    icydb_diagnostic_code::DiagnosticFactTag::RecordMember,
9075                    icydb_diagnostic_code::pack_u32_pair(1, 1),
9076                ),
9077            ]
9078        );
9079        let value = facts
9080            .iter()
9081            .find_map(|(tag, value)| {
9082                (*tag == icydb_diagnostic_code::DiagnosticFactTag::ConstraintId).then_some(*value)
9083            })
9084            .expect("targeted mutation should retain its accepted constraint ID");
9085        u32::try_from(value).expect("accepted constraint ID fits u32")
9086    }
9087
9088    #[expect(
9089        clippy::too_many_lines,
9090        reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
9091    )]
9092    #[test]
9093    fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
9094        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
9095        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
9096        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
9097
9098        let entity_tag = EntityTag::new(93);
9099        let enum_catalog = empty_accepted_enum_catalog_for_tests();
9100        let (composite_catalog, profile_type, degree_type, degree_member) =
9101            build_record_newtype_composite_catalog_for_tests(
9102                "tests::TargetedProfile".to_string(),
9103                "degree".to_string(),
9104                "tests::TargetedDegree".to_string(),
9105                AcceptedFieldKind::Nat64,
9106                &enum_catalog,
9107            )
9108            .expect("targeted mutation composites should close");
9109        let profile_kind = AcceptedFieldKind::Composite {
9110            type_id: profile_type,
9111        };
9112        let profile_default = encoded_value(
9113            &enum_catalog,
9114            &composite_catalog,
9115            "profile",
9116            &profile_kind,
9117            FieldStorageDecode::CatalogValue,
9118            LeafCodec::Structural,
9119            profile_input(12),
9120        );
9121        let fields = vec![
9122            PersistedFieldSnapshot::new_initial(
9123                FieldId::new(1),
9124                "id".to_string(),
9125                SchemaFieldSlot::new(0),
9126                AcceptedFieldKind::Nat64,
9127                Vec::new(),
9128                false,
9129                SchemaInsertDefault::None,
9130                FieldStorageDecode::ByKind,
9131                LeafCodec::Scalar(ScalarCodec::Nat64),
9132            ),
9133            PersistedFieldSnapshot::new_initial(
9134                FieldId::new(2),
9135                "profile".to_string(),
9136                SchemaFieldSlot::new(1),
9137                profile_kind,
9138                vec![PersistedNestedLeafSnapshot::new(
9139                    vec!["degree".to_string()],
9140                    AcceptedFieldKind::Composite {
9141                        type_id: degree_type,
9142                    },
9143                    false,
9144                )],
9145                false,
9146                SchemaInsertDefault::SlotPayload(profile_default),
9147                FieldStorageDecode::CatalogValue,
9148                LeafCodec::Structural,
9149            ),
9150            PersistedFieldSnapshot::new_initial_with_write_policy(
9151                FieldId::new(3),
9152                "updated_at".to_string(),
9153                SchemaFieldSlot::new(2),
9154                AcceptedFieldKind::Timestamp,
9155                Vec::new(),
9156                false,
9157                SchemaInsertDefault::None,
9158                SchemaFieldWritePolicy::from_model_policies(
9159                    None,
9160                    Some(FieldWriteManagement::UpdatedAt),
9161                ),
9162                FieldStorageDecode::ByKind,
9163                LeafCodec::Scalar(ScalarCodec::Timestamp),
9164            ),
9165        ];
9166        let mut snapshot = PersistedSchemaSnapshot::new(
9167            SchemaVersion::initial(),
9168            ENTITY_SOURCE.to_string(),
9169            "TargetedMutation".to_string(),
9170            FieldId::new(1),
9171            SchemaRowLayout::initial(
9172                fields
9173                    .iter()
9174                    .map(|field| (field.id(), field.slot()))
9175                    .collect(),
9176            ),
9177            fields,
9178        );
9179        let constraint_catalog = snapshot
9180            .constraint_catalog()
9181            .clone()
9182            .with_added_targeted_rule(
9183                "profile_degree_multiple".to_string(),
9184                ConstraintOrigin::Generated,
9185                AcceptedRuleTarget::new(
9186                    FieldId::new(2),
9187                    AcceptedNamedTypeIdentity::Composite(degree_type),
9188                ),
9189                AcceptedRuleOperation::MultipleOf {
9190                    divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
9191                },
9192            )
9193            .expect("targeted mutation rule should allocate");
9194        let targeted_rule_id = constraint_catalog
9195            .constraints()
9196            .last()
9197            .expect("targeted mutation rule should persist")
9198            .id();
9199        snapshot = snapshot.with_constraint_catalog(constraint_catalog);
9200
9201        let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
9202        let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
9203        let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
9204        let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
9205        let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
9206        let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
9207        let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
9208        let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
9209        let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
9210            BTreeMap::from([(entity_source, entity_tag)]),
9211            BTreeMap::from([
9212                ((entity_tag, id_source), FieldId::new(1)),
9213                ((entity_tag, profile_source), FieldId::new(2)),
9214                ((entity_tag, updated_at_source), FieldId::new(3)),
9215            ]),
9216            BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
9217            BTreeMap::new(),
9218            BTreeMap::new(),
9219        )
9220        .with_initial_named_types_for_tests(
9221            BTreeMap::from([
9222                (
9223                    profile_type_source,
9224                    AcceptedNamedTypeIdentity::Composite(profile_type),
9225                ),
9226                (
9227                    degree_type_source,
9228                    AcceptedNamedTypeIdentity::Composite(degree_type),
9229                ),
9230            ]),
9231            BTreeMap::new(),
9232            BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
9233        );
9234        let candidate = accepted_schema_candidate_with_catalogs_for_tests(
9235            STORE_PATH,
9236            AcceptedSchemaRevision::INITIAL,
9237            enum_catalog,
9238            composite_catalog,
9239            source_bindings,
9240            BTreeMap::from([(entity_tag, snapshot)]),
9241        );
9242
9243        let session = DbSession::<TestCanister>::new(
9244            &STORE_REGISTRY,
9245            &crate::db::RequestExecutionRoot::__new_runtime_root(),
9246        );
9247        session
9248            .db
9249            .drive_startup_recovery_page()
9250            .expect("targeted mutation test database should initialize");
9251        let store = session
9252            .db
9253            .store_handle(STORE_PATH)
9254            .expect("targeted mutation test store should resolve");
9255        crate::db::commit::publish_accepted_schema_candidate(
9256            STORE_PATH,
9257            store,
9258            AcceptedSchemaRevision::NONE,
9259            &candidate,
9260        )
9261        .expect("targeted mutation candidate should publish");
9262
9263        let dynamic_error = session
9264            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
9265                entity: "TargetedMutation".to_string(),
9266                patch: structural_patch(1, 12),
9267            })
9268            .expect_err("dynamic write must enforce the targeted rule");
9269        assert_eq!(
9270            targeted_constraint_id(&dynamic_error),
9271            targeted_rule_id.get()
9272        );
9273
9274        let binding = session
9275            .issue_typed_entity_binding(&TYPED_DESCRIPTOR)
9276            .expect("targeted typed binding should issue");
9277        let typed_patch = binding
9278            .bind_write_ordinals(vec![
9279                (0, DynamicWriteCell::Value(InputValue::nat64(2))),
9280                (1, DynamicWriteCell::Value(profile_input(12))),
9281            ])
9282            .expect("targeted typed patch should bind");
9283        let typed_error = session
9284            .execute_trusted_typed_mutation(
9285                &binding,
9286                DynamicTypedMutation::Insert { patch: typed_patch },
9287            )
9288            .expect_err("typed write must enforce the targeted rule");
9289        assert_eq!(targeted_constraint_id(&typed_error), targeted_rule_id.get());
9290
9291        #[cfg(feature = "sql")]
9292        {
9293            let sql_error = session
9294                .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
9295                .expect_err("SQL default resolution must enforce the targeted rule");
9296            let crate::db::QueryError::Execute(execute) = sql_error else {
9297                panic!("targeted SQL write should fail at shared execution admission");
9298            };
9299            assert_eq!(
9300                targeted_constraint_id(execute.as_internal()),
9301                targeted_rule_id.get()
9302            );
9303        }
9304
9305        session
9306            .execute_trusted_dynamic_mutation_batch(vec![
9307                DynamicMutation::Insert {
9308                    entity: "TargetedMutation".to_string(),
9309                    patch: structural_patch(4, 5),
9310                },
9311                DynamicMutation::Insert {
9312                    entity: "TargetedMutation".to_string(),
9313                    patch: structural_patch(5, 12),
9314                },
9315            ])
9316            .expect_err("one invalid targeted value must reject the whole batch");
9317        assert_eq!(
9318            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
9319            Some(0),
9320            "no frontend or earlier valid batch row may escape targeted admission",
9321        );
9322
9323        let admitted = session
9324            .execute_trusted_dynamic_mutation_batch(vec![
9325                DynamicMutation::Insert {
9326                    entity: "TargetedMutation".to_string(),
9327                    patch: structural_patch(6, 5),
9328                },
9329                DynamicMutation::Insert {
9330                    entity: "TargetedMutation".to_string(),
9331                    patch: structural_patch(7, 10),
9332                },
9333            ])
9334            .expect("compliant targeted values should share one accepted batch");
9335        let admitted_rows = admitted
9336            .iter()
9337            .flat_map(|result| result.rows.iter())
9338            .collect::<Vec<_>>();
9339        let [first, second] = admitted_rows.as_slice() else {
9340            panic!("the mixed targeted batch should return two rows");
9341        };
9342        let first_timestamp = first
9343            .get(2)
9344            .expect("the first mixed row should contain its managed timestamp");
9345        assert!(matches!(
9346            first_timestamp.as_public(),
9347            crate::value::PublicValue::Timestamp(_)
9348        ));
9349        assert_eq!(
9350            second.get(2),
9351            Some(first_timestamp),
9352            "one accepted mixed batch must materialize one managed timestamp",
9353        );
9354        assert_eq!(
9355            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
9356            Some(2),
9357        );
9358    }
9359}