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