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