Skip to main content

icydb_core/db/session/
write.rs

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