Skip to main content

icydb_core/db/session/
write.rs

1//! Module: db::session::write
2//! Responsibility: session-owned typed write APIs for insert, replace, update,
3//! and structural mutation entrypoints over the shared save pipeline.
4//! Does not own: commit staging, mutation execution, or persistence encoding.
5//! Boundary: keeps public session write semantics above the executor save surface.
6
7use super::AcceptedSchemaCatalogContext;
8use crate::{
9    db::{
10        DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
11        DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedFieldBindingRequest,
12        DynamicTypedFieldType, DynamicTypedMutation, DynamicTypedStructuralPatch, DynamicWriteCell,
13        commit::{CommitRowOp, database_incarnation_id},
14        data::{
15            AcceptedMutationIntentPatch, AcceptedPreKeyInsert, DecodedDataStoreKey, FieldSlot,
16            RawRow, StructuralRowContract, StructuralSlotReader,
17            canonical_row_from_raw_row_with_accepted_decode_contract,
18            resolve_existing_replace_structural_patch_with_accepted_contract,
19            resolve_insert_structural_patch_with_accepted_contract,
20            resolve_update_structural_patch_with_accepted_contract,
21        },
22        executor::{
23            AcceptedMutationConstraintScheduler,
24            commit_structural_row_ops_with_mutation_progress_for_path,
25            commit_structural_row_ops_with_window_for_path, mutation_key_exists_error,
26        },
27        integrity::MutationProgressRecordOp,
28        schema::{
29            AcceptedFieldKind, AcceptedIdentityAllocation, AcceptedRowLayoutRuntimeContract,
30            FieldId, FieldInsertGeneration, IdentityStatementCursor, lower_field_type,
31            output_value_from_runtime,
32        },
33        write_context::{AcceptedWriteContext, MutationMode},
34    },
35    error::{InternalError, MutationDiagnosticContext},
36    metrics::sink::{MetricsEvent, SaveMutationKind, record},
37    traits::CanisterKind,
38    types::{CurrentTimestamp, Timestamp},
39    value::{InputValue, Value},
40};
41use icydb_schema::{EntitySourceKey, FieldSourceKey, FieldType, TypeSourceKey};
42
43#[derive(Clone, Debug, Eq, PartialEq)]
44struct AcceptedIdentityInsertField {
45    field_id: FieldId,
46    field_slot: usize,
47    accepted_kind: AcceptedFieldKind,
48}
49
50struct AcceptedStructuralMutationCommitOptions {
51    capture_output_values: bool,
52    packing: AcceptedStructuralMutationPacking,
53}
54
55impl AcceptedStructuralMutationCommitOptions {
56    const fn standard() -> Self {
57        Self {
58            capture_output_values: true,
59            packing: AcceptedStructuralMutationPacking::Complete,
60        }
61    }
62
63    #[cfg(test)]
64    const fn with_mutation_progress() -> Self {
65        Self {
66            capture_output_values: false,
67            packing: AcceptedStructuralMutationPacking::Complete,
68        }
69    }
70
71    const fn bounded_prefix() -> Self {
72        Self {
73            capture_output_values: false,
74            packing: AcceptedStructuralMutationPacking::BoundedPrefix,
75        }
76    }
77}
78
79#[derive(Clone, Copy)]
80enum AcceptedStructuralMutationPacking {
81    Complete,
82    BoundedPrefix,
83}
84
85pub(in crate::db::session) enum AcceptedStructuralMutationCommitDirective {
86    Standard,
87    WithMutationProgress(MutationProgressRecordOp),
88    Skip,
89}
90
91/// Accepted row identity carried by a structural mutation after frontend
92/// lowering but before the canonical after-image exists.
93pub(in crate::db::session) enum AcceptedStructuralMutationTarget {
94    ResolveFromAfterImage,
95    Expected(Box<DecodedDataStoreKey>),
96    ExpectedLoaded(AcceptedLoadedStructuralRow),
97}
98
99/// One retained row whose accepted key relationship was validated by the
100/// synchronous operation that loaded it.
101pub(in crate::db::session) struct AcceptedLoadedStructuralRow {
102    key: Box<DecodedDataStoreKey>,
103    row: RawRow,
104}
105
106impl AcceptedLoadedStructuralRow {
107    pub(in crate::db::session) fn from_validated_parts(
108        key: DecodedDataStoreKey,
109        row: RawRow,
110    ) -> Self {
111        Self {
112            key: Box::new(key),
113            row,
114        }
115    }
116
117    fn into_parts(self) -> (DecodedDataStoreKey, RawRow) {
118        (*self.key, self.row)
119    }
120}
121
122impl AcceptedStructuralMutationTarget {
123    pub(in crate::db::session) fn expected(key: DecodedDataStoreKey) -> Self {
124        Self::Expected(Box::new(key))
125    }
126
127    /// Retain a row loaded by the same synchronous operation so mutation
128    /// materialization does not perform a duplicate backend point read.
129    pub(in crate::db::session) const fn expected_loaded(row: AcceptedLoadedStructuralRow) -> Self {
130        Self::ExpectedLoaded(row)
131    }
132}
133
134/// One accepted structural mutation intent ready for shared batch
135/// materialization.
136pub(in crate::db::session) enum AcceptedStructuralMutation {
137    Save {
138        mode: MutationMode,
139        target: AcceptedStructuralMutationTarget,
140        patch: AcceptedMutationIntentPatch,
141    },
142    Delete {
143        key: Box<DecodedDataStoreKey>,
144    },
145}
146
147impl AcceptedStructuralMutation {
148    pub(in crate::db::session) const fn save(
149        mode: MutationMode,
150        target: AcceptedStructuralMutationTarget,
151        patch: AcceptedMutationIntentPatch,
152    ) -> Self {
153        Self::Save {
154            mode,
155            target,
156            patch,
157        }
158    }
159
160    pub(in crate::db::session) fn delete(key: DecodedDataStoreKey) -> Self {
161        Self::Delete { key: Box::new(key) }
162    }
163}
164
165const MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS: usize = 4_096;
166pub(in crate::db::session) const STRUCTURAL_MUTATION_BATCH_STAGED_BYTES_POLICY: u32 =
167    16 * 1024 * 1024;
168pub(in crate::db::session) const MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES: usize =
169    STRUCTURAL_MUTATION_BATCH_STAGED_BYTES_POLICY as usize;
170const MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES: usize = 1024 * 1024;
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub(in crate::db::session) struct AcceptedStructuralMutationPackingReport {
174    admitted_mutations: usize,
175    stopped_before_candidate: bool,
176    candidate_exceeds_batch_policy: bool,
177}
178
179impl AcceptedStructuralMutationPackingReport {
180    #[must_use]
181    pub(in crate::db::session) const fn admitted_mutations(self) -> usize {
182        self.admitted_mutations
183    }
184
185    #[must_use]
186    pub(in crate::db::session) const fn stopped_before_candidate(self) -> bool {
187        self.stopped_before_candidate
188    }
189
190    #[must_use]
191    pub(in crate::db::session) const fn candidate_exceeds_batch_policy(self) -> bool {
192        self.candidate_exceeds_batch_policy
193    }
194}
195
196fn structural_mutation_staged_charge(
197    lengths: impl IntoIterator<Item = usize>,
198) -> Result<usize, InternalError> {
199    lengths.into_iter().try_fold(0_usize, |total, length| {
200        total.checked_add(length).ok_or_else(|| {
201            InternalError::mutation_batch_staged_bytes_exceeded(
202                None,
203                MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
204            )
205        })
206    })
207}
208
209fn add_structural_mutation_staged_bytes(
210    total: &mut usize,
211    lengths: impl IntoIterator<Item = usize>,
212) -> Result<(), InternalError> {
213    let charge = structural_mutation_staged_charge(lengths)?;
214    *total = total.checked_add(charge).ok_or_else(|| {
215        InternalError::mutation_batch_staged_bytes_exceeded(
216            None,
217            MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
218        )
219    })?;
220    if *total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
221        return Err(InternalError::mutation_batch_staged_bytes_exceeded(
222            Some(*total),
223            MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
224        ));
225    }
226    Ok(())
227}
228
229fn admit_structural_mutation_staged_charge(
230    total: &mut usize,
231    lengths: impl IntoIterator<Item = usize>,
232    packing: AcceptedStructuralMutationPacking,
233) -> Result<AcceptedStructuralMutationStagedAdmission, InternalError> {
234    if matches!(packing, AcceptedStructuralMutationPacking::Complete) {
235        add_structural_mutation_staged_bytes(total, lengths)?;
236        return Ok(AcceptedStructuralMutationStagedAdmission::Admitted);
237    }
238
239    let charge = structural_mutation_staged_charge(lengths)?;
240    if charge > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
241        return Ok(AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy);
242    }
243    let Some(next_total) = total.checked_add(charge) else {
244        return Ok(AcceptedStructuralMutationStagedAdmission::PageFull);
245    };
246    if next_total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
247        return Ok(AcceptedStructuralMutationStagedAdmission::PageFull);
248    }
249    *total = next_total;
250    Ok(AcceptedStructuralMutationStagedAdmission::Admitted)
251}
252
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254enum AcceptedStructuralMutationStagedAdmission {
255    Admitted,
256    PageFull,
257    CandidateExceedsPolicy,
258}
259
260fn validate_structural_mutation_result_bytes(encoded_bytes: usize) -> Result<(), InternalError> {
261    if encoded_bytes > MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES {
262        return Err(InternalError::mutation_batch_result_bytes_exceeded(
263            encoded_bytes,
264            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES,
265        ));
266    }
267    Ok(())
268}
269
270/// One canonical row produced by structural mutation materialization.
271pub(in crate::db::session) struct AcceptedStructuralMutationRow {
272    values: Vec<Value>,
273    logical_changed: bool,
274}
275
276impl AcceptedStructuralMutationRow {
277    #[cfg(any(feature = "sql", test))]
278    pub(in crate::db::session) fn into_values(self) -> Vec<Value> {
279        self.values
280    }
281
282    pub(in crate::db::session) const fn logical_changed(&self) -> bool {
283        self.logical_changed
284    }
285}
286
287const fn dynamic_mutation_mode(request: &DynamicMutation) -> Option<MutationMode> {
288    match request {
289        DynamicMutation::Insert { .. } => Some(MutationMode::Insert),
290        DynamicMutation::Update { .. } => Some(MutationMode::Update),
291        DynamicMutation::Replace { .. } => Some(MutationMode::Replace),
292        DynamicMutation::Delete { .. } => None,
293    }
294}
295
296const fn dynamic_typed_mutation_mode(request: &DynamicTypedMutation) -> MutationMode {
297    match request {
298        DynamicTypedMutation::Insert { .. } => MutationMode::Insert,
299        DynamicTypedMutation::Update { .. } => MutationMode::Update,
300        DynamicTypedMutation::Replace { .. } => MutationMode::Replace,
301    }
302}
303
304const fn diagnostic_mutation_operation(
305    mode: MutationMode,
306) -> icydb_diagnostic_code::DiagnosticMutationOperation {
307    match mode {
308        MutationMode::Insert => icydb_diagnostic_code::DiagnosticMutationOperation::Insert,
309        MutationMode::Replace => icydb_diagnostic_code::DiagnosticMutationOperation::Replace,
310        MutationMode::Update => icydb_diagnostic_code::DiagnosticMutationOperation::Update,
311    }
312}
313
314const fn mutation_diagnostic_context(
315    entity_tag: crate::types::EntityTag,
316    mode: MutationMode,
317    batch_position: u32,
318) -> MutationDiagnosticContext {
319    MutationDiagnosticContext::new(
320        entity_tag.value(),
321        diagnostic_mutation_operation(mode),
322        batch_position,
323    )
324}
325
326const fn dynamic_write_context(operation_timestamp: Timestamp) -> AcceptedWriteContext {
327    AcceptedWriteContext::new(operation_timestamp)
328}
329
330fn insert_key_exists_after_generation(identity_generated: bool) -> InternalError {
331    if identity_generated {
332        InternalError::identity_state_corruption()
333    } else {
334        mutation_key_exists_error()
335    }
336}
337
338fn dynamic_key(
339    entity_tag: crate::types::EntityTag,
340    key: &InputValue,
341) -> Result<DecodedDataStoreKey, InternalError> {
342    let value = key
343        .clone()
344        .try_into_runtime_non_enum()
345        .ok_or_else(InternalError::executor_unsupported)?;
346    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
347}
348
349fn lower_dynamic_patch(
350    entity_path: &str,
351    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
352    patch: &DynamicStructuralPatch,
353    mode: MutationMode,
354    mutation_context: MutationDiagnosticContext,
355) -> Result<AcceptedMutationIntentPatch, InternalError> {
356    let mut lowered = AcceptedMutationIntentPatch::new();
357    for (field_name, cell) in patch.fields() {
358        let slot = descriptor
359            .field_slot_index_by_name(field_name)
360            .ok_or_else(|| {
361                InternalError::mutation_structural_field_unknown(entity_path, field_name)
362            })?;
363        let field = descriptor
364            .field_for_slot_index(slot)
365            .ok_or_else(InternalError::executor_invariant)?;
366        if !matches!(cell, DynamicWriteCell::Omitted)
367            && (field.write_policy().insert_generation().is_some()
368                || field.write_policy().write_management().is_some())
369        {
370            return Err(InternalError::mutation_database_owned_field_explicit(
371                mutation_context,
372                field.field_id().get(),
373            ));
374        }
375        let slot = FieldSlot::from_validated_index(slot);
376        lowered = match cell {
377            DynamicWriteCell::Omitted => lowered,
378            DynamicWriteCell::Default => match mode {
379                MutationMode::Insert | MutationMode::Replace => {
380                    lowered.set_explicit_insert_default(slot)
381                }
382                MutationMode::Update => lowered.set_explicit_update_default(slot),
383            },
384            DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
385            DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
386        };
387    }
388    Ok(lowered)
389}
390
391fn lower_dynamic_mutation_intent(
392    entity_tag: crate::types::EntityTag,
393    entity_path: &str,
394    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
395    request: &DynamicMutation,
396    batch_position: u32,
397) -> Result<(AcceptedStructuralMutation, Option<SaveMutationKind>), InternalError> {
398    match request {
399        DynamicMutation::Insert { patch, .. } => {
400            let mode = MutationMode::Insert;
401            Ok((
402                AcceptedStructuralMutation::save(
403                    mode,
404                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
405                    lower_dynamic_patch(
406                        entity_path,
407                        descriptor,
408                        patch,
409                        mode,
410                        mutation_diagnostic_context(entity_tag, mode, batch_position),
411                    )?,
412                ),
413                Some(SaveMutationKind::Insert),
414            ))
415        }
416        DynamicMutation::Update { key, patch, .. }
417        | DynamicMutation::Replace { key, patch, .. } => {
418            let mode =
419                dynamic_mutation_mode(request).ok_or_else(InternalError::executor_invariant)?;
420            let kind = match mode {
421                MutationMode::Insert => SaveMutationKind::Insert,
422                MutationMode::Replace => SaveMutationKind::Replace,
423                MutationMode::Update => SaveMutationKind::Update,
424            };
425            Ok((
426                AcceptedStructuralMutation::save(
427                    mode,
428                    AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
429                    lower_dynamic_patch(
430                        entity_path,
431                        descriptor,
432                        patch,
433                        mode,
434                        mutation_diagnostic_context(entity_tag, mode, batch_position),
435                    )?,
436                ),
437                Some(kind),
438            ))
439        }
440        DynamicMutation::Delete { key, .. } => Ok((
441            AcceptedStructuralMutation::delete(dynamic_key(entity_tag, key)?),
442            None,
443        )),
444    }
445}
446
447fn lower_typed_patch(
448    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
449    patch: &DynamicTypedStructuralPatch,
450    mode: MutationMode,
451    mutation_context: MutationDiagnosticContext,
452) -> Result<AcceptedMutationIntentPatch, InternalError> {
453    let mut lowered = AcceptedMutationIntentPatch::new();
454    for (field_id, slot, cell) in patch.fields() {
455        let slot_index = usize::from(*slot);
456        let field = descriptor
457            .field_for_slot_index(slot_index)
458            .ok_or_else(InternalError::store_invariant)?;
459        if field.field_id().get() != *field_id {
460            return Err(InternalError::store_invariant());
461        }
462        if !matches!(cell, DynamicWriteCell::Omitted)
463            && (field.write_policy().insert_generation().is_some()
464                || field.write_policy().write_management().is_some())
465        {
466            return Err(InternalError::mutation_database_owned_field_explicit(
467                mutation_context,
468                field.field_id().get(),
469            ));
470        }
471        let slot = FieldSlot::from_validated_index(slot_index);
472        lowered = match cell {
473            DynamicWriteCell::Omitted => lowered,
474            DynamicWriteCell::Default => match mode {
475                MutationMode::Insert | MutationMode::Replace => {
476                    lowered.set_explicit_insert_default(slot)
477                }
478                MutationMode::Update => lowered.set_explicit_update_default(slot),
479            },
480            DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
481            DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
482        };
483    }
484    Ok(lowered)
485}
486
487fn preserve_dynamic_replacement_identity(
488    key: &DecodedDataStoreKey,
489    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
490    mut patch: AcceptedMutationIntentPatch,
491) -> Result<AcceptedMutationIntentPatch, InternalError> {
492    let primary_key_slots = descriptor.primary_key_slot_indices();
493    let runtime_key = key.primary_key_runtime_value();
494    let components = match runtime_key {
495        Value::List(values) if primary_key_slots.len() > 1 => values,
496        value if primary_key_slots.len() == 1 => vec![value],
497        _ => return Err(InternalError::executor_invariant()),
498    };
499    if components.len() != primary_key_slots.len() {
500        return Err(InternalError::executor_invariant());
501    }
502
503    for (slot, value) in primary_key_slots.iter().copied().zip(components) {
504        let _ = descriptor
505            .field_for_slot_index(slot)
506            .ok_or_else(InternalError::executor_invariant)?;
507        let has_explicit_intent = patch
508            .entries()
509            .iter()
510            .any(|entry| entry.slot().index() == slot);
511        if has_explicit_intent {
512            continue;
513        }
514        let value = InputValue::try_from_runtime_non_enum(&value)
515            .ok_or_else(InternalError::executor_invariant)?;
516        patch =
517            patch.set_preserved_replacement_identity(FieldSlot::from_validated_index(slot), value);
518    }
519
520    Ok(patch)
521}
522
523// Locate the sole accepted Identity owner that is eligible to resolve a
524// keyless insert. Accepted-schema integrity already freezes the exact shape;
525// this runtime check fails closed if a malformed contract reaches execution.
526fn accepted_identity_insert_field(
527    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
528) -> Result<Option<AcceptedIdentityInsertField>, InternalError> {
529    let mut identity = None;
530    for field in descriptor.fields() {
531        if field.write_policy().insert_generation() != Some(FieldInsertGeneration::Identity) {
532            continue;
533        }
534        let field_slot = usize::from(field.slot().get());
535        if identity
536            .replace(AcceptedIdentityInsertField {
537                field_id: field.field_id(),
538                field_slot,
539                accepted_kind: field.kind().clone(),
540            })
541            .is_some()
542            || descriptor.primary_key_slot_indices() != [field_slot]
543        {
544            return Err(InternalError::identity_corruption());
545        }
546    }
547    Ok(identity)
548}
549
550fn checked_pre_key_candidate_count(count: usize) -> Result<u32, InternalError> {
551    u32::try_from(count).map_err(|_| InternalError::identity_candidate_count_exhausted())
552}
553
554fn validate_identity_materialization(
555    entity_tag: crate::types::EntityTag,
556    identity_field: &AcceptedIdentityInsertField,
557    candidate: &AcceptedPreKeyInsert,
558    allocation: &AcceptedIdentityAllocation,
559    data_key: &DecodedDataStoreKey,
560    reader: &StructuralSlotReader<'_>,
561) -> Result<(), InternalError> {
562    let owner = allocation.owner();
563    let slot_value = reader.required_cached_value(identity_field.field_slot)?;
564    if candidate.entity_tag() != entity_tag
565        || candidate.input_ordinal() != allocation.input_ordinal()
566        || owner.entity_tag() != entity_tag
567        || owner.field_id() != identity_field.field_id
568        || allocation.field_slot() != identity_field.field_slot
569        || slot_value != allocation.value()
570        || data_key.primary_key_runtime_value() != *allocation.value()
571    {
572        return Err(InternalError::identity_corruption());
573    }
574    Ok(())
575}
576
577fn data_key_from_row(
578    entity_tag: crate::types::EntityTag,
579    contract: &StructuralRowContract,
580    row: &RawRow,
581) -> Result<DecodedDataStoreKey, InternalError> {
582    let reader =
583        StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
584    let values = contract
585        .primary_key_slot_indices()
586        .iter()
587        .map(|slot| reader.required_cached_value(*slot).cloned())
588        .collect::<Result<Vec<_>, _>>()?;
589    let value = match values.as_slice() {
590        [value] => value.clone(),
591        _ => Value::List(values),
592    };
593    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
594}
595
596#[cfg(feature = "sql")]
597pub(in crate::db::session) fn structural_data_key_from_runtime_values(
598    entity_tag: crate::types::EntityTag,
599    values: Vec<Value>,
600) -> Result<DecodedDataStoreKey, InternalError> {
601    let value = match values.as_slice() {
602        [value] => value.clone(),
603        _ => Value::List(values),
604    };
605    DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
606}
607
608fn validated_existing_row(
609    store: crate::db::registry::StoreHandle,
610    data_key: &DecodedDataStoreKey,
611    contract: &StructuralRowContract,
612) -> Result<Option<RawRow>, InternalError> {
613    let raw_key = data_key.to_raw()?;
614    let row = store.with_data(|data| data.get(&raw_key));
615    if let Some(row) = row.as_ref() {
616        let reader =
617            StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
618        reader.validate_primary_key(data_key)?;
619    }
620    Ok(row)
621}
622
623fn prepare_dynamic_mutation_result(
624    catalog: &AcceptedSchemaCatalogContext,
625    descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
626    rows: Vec<AcceptedStructuralMutationRow>,
627    enforce_mixed_batch_result_bound: bool,
628) -> Result<DynamicMutationResult, InternalError> {
629    let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
630        total
631            .checked_add(u32::from(row.logical_changed()))
632            .ok_or_else(InternalError::executor_invariant)
633    })?;
634    let columns = descriptor
635        .fields()
636        .iter()
637        .map(|field| field.name().to_string())
638        .collect();
639    let rows = rows
640        .into_iter()
641        .map(|row| {
642            row.values
643                .iter()
644                .map(|value| {
645                    output_value_from_runtime(catalog.enum_catalog(), value)
646                        .map_err(|_| InternalError::store_invariant())
647                })
648                .collect::<Result<Vec<_>, _>>()
649        })
650        .collect::<Result<Vec<_>, _>>()?;
651    let result = DynamicMutationResult {
652        entity: catalog.snapshot().entity_name().to_string(),
653        columns,
654        rows,
655        affected_rows,
656    };
657    if enforce_mixed_batch_result_bound {
658        let encoded =
659            candid::encode_one(&result).map_err(|_| InternalError::executor_invariant())?;
660        validate_structural_mutation_result_bytes(encoded.len())?;
661    }
662    Ok(result)
663}
664
665fn dynamic_typed_field_type(
666    field_type: DynamicTypedFieldType,
667) -> Result<FieldType, DynamicTypedBindingError> {
668    match field_type {
669        DynamicTypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
670        DynamicTypedFieldType::List(item) => {
671            Ok(FieldType::List(Box::new(dynamic_typed_field_type(*item)?)))
672        }
673        DynamicTypedFieldType::Named(source_key) => TypeSourceKey::try_new(source_key)
674            .map(FieldType::Named)
675            .map_err(|_| DynamicTypedBindingError::FieldUnavailable),
676    }
677}
678
679fn typed_adapter_field_kind_matches(
680    accepted: &AcceptedFieldKind,
681    expected: &AcceptedFieldKind,
682) -> bool {
683    if accepted == expected {
684        return true;
685    }
686    match (accepted, expected) {
687        (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
688            typed_adapter_field_kind_matches(key_kind, expected)
689        }
690        (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
691            typed_adapter_field_kind_matches(accepted, expected)
692        }
693        _ => false,
694    }
695}
696
697impl<C: CanisterKind> DbSession<C> {
698    /// Issue one opaque accepted binding for immutable generated source keys.
699    pub fn issue_typed_entity_binding(
700        &self,
701        entity_source_key: &str,
702        field_requests: &[DynamicTypedFieldBindingRequest],
703    ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
704        let entity_source = EntitySourceKey::try_new(entity_source_key)
705            .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?;
706        let field_requests = field_requests
707            .iter()
708            .map(|request| {
709                Ok((
710                    FieldSourceKey::try_new(request.source_key.clone())
711                        .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?,
712                    dynamic_typed_field_type(request.field_type.clone())?,
713                    request.nullable,
714                ))
715            })
716            .collect::<Result<Vec<_>, DynamicTypedBindingError>>()?;
717        let catalog = self
718            .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
719            .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
720        let identity = catalog.identity();
721        if identity.entity_path() != entity_source.as_str() {
722            return Err(InternalError::store_invariant().into());
723        }
724        let store = self.db.recovered_store(identity.store_path())?;
725        let bundle = store
726            .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
727            .ok_or_else(InternalError::store_invariant)?;
728        let entity_tag = identity.entity_tag();
729        if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
730            || bundle.revision() != catalog.revision()
731        {
732            return Err(InternalError::store_invariant().into());
733        }
734        let snapshot = bundle
735            .entity_snapshots()
736            .get(&entity_tag)
737            .ok_or_else(InternalError::store_invariant)?;
738        let row_contract = catalog.inspection_plan().row_contract();
739        let mut fields = Vec::with_capacity(field_requests.len());
740        for (source, field_type, nullable) in &field_requests {
741            let field_id = bundle
742                .source_bindings()
743                .field(entity_tag, source)
744                .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
745            let field = snapshot
746                .fields()
747                .iter()
748                .find(|field| field.id() == field_id)
749                .ok_or_else(InternalError::store_invariant)?;
750            let runtime_field =
751                row_contract.required_accepted_field_contract(usize::from(field.slot().get()))?;
752            if runtime_field.field_id() != field_id {
753                return Err(InternalError::store_invariant().into());
754            }
755            let expected_kind = lower_field_type(field_type, bundle.source_bindings())
756                .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
757            if field.nullable() != *nullable
758                || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
759            {
760                return Err(DynamicTypedBindingError::IncompatibleField);
761            }
762            fields.push((
763                source.as_str().to_string(),
764                field_id.get(),
765                field.slot().get(),
766                field.name().to_string(),
767            ));
768        }
769        let adapter_names = bundle.typed_adapter_names()?;
770
771        DynamicTypedEntityBinding::new(
772            database_incarnation_id()?.to_bytes(),
773            entity_source.as_str().to_string(),
774            snapshot.entity_name().to_string(),
775            entity_tag.value(),
776            catalog.revision().get(),
777            catalog.fingerprint(),
778            row_contract.current_layout_version().get(),
779            fields,
780            adapter_names.named_types,
781            adapter_names.enum_variants,
782            adapter_names.composite_fields,
783        )
784        .map_err(Into::into)
785    }
786
787    pub(in crate::db::session) fn current_typed_entity_binding_catalog(
788        &self,
789        binding: &DynamicTypedEntityBinding,
790    ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
791        if database_incarnation_id()?.to_bytes() != binding.database_incarnation {
792            return Ok(None);
793        }
794        let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
795            binding.entity_source.as_str(),
796        )?
797        else {
798            return Ok(None);
799        };
800        let row_contract = catalog.inspection_plan().row_contract();
801        let identity = catalog.identity();
802        if identity.entity_path() != binding.entity_source.as_str()
803            || identity.entity_tag().value() != binding.entity_tag
804            || catalog.revision().get() != binding.accepted_revision
805            || catalog.fingerprint() != binding.accepted_fingerprint
806            || row_contract.current_layout_version().get() != binding.entity_generation
807        {
808            return Ok(None);
809        }
810        let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
811            .map_err(|_| InternalError::store_invariant())?;
812        let store = self.db.recovered_store(identity.store_path())?;
813        let bundle = store
814            .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
815            .ok_or_else(InternalError::store_invariant)?;
816        if bundle.revision() != catalog.revision()
817            || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
818        {
819            return Ok(None);
820        }
821        let snapshot = bundle
822            .entity_snapshots()
823            .get(&identity.entity_tag())
824            .ok_or_else(InternalError::store_invariant)?;
825        for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings() {
826            let source = FieldSourceKey::try_new(source_key)
827                .map_err(|_| InternalError::store_invariant())?;
828            let Some(field_id) = bundle
829                .source_bindings()
830                .field(identity.entity_tag(), &source)
831            else {
832                return Ok(None);
833            };
834            let Some(field) = snapshot
835                .fields()
836                .iter()
837                .find(|field| field.id() == field_id)
838            else {
839                return Err(InternalError::store_invariant());
840            };
841            if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
842                return Ok(None);
843            }
844        }
845        Ok(Some(catalog))
846    }
847
848    /// Verify that an opaque typed binding still names the exact accepted authority.
849    pub fn typed_entity_binding_is_current(
850        &self,
851        binding: &DynamicTypedEntityBinding,
852    ) -> Result<bool, InternalError> {
853        self.current_typed_entity_binding_catalog(binding)
854            .map(|catalog| catalog.is_some())
855    }
856
857    /// Materialize one accepted delete batch, run bounded frontend validation,
858    /// then commit it atomically.
859    #[cfg(feature = "sql")]
860    pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
861        &self,
862        catalog: &AcceptedSchemaCatalogContext,
863        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
864        keys: Vec<DecodedDataStoreKey>,
865        precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
866    ) -> Result<Vec<Vec<Value>>, InternalError> {
867        let mutations = keys
868            .into_iter()
869            .map(AcceptedStructuralMutation::delete)
870            .collect::<Vec<_>>();
871        let mutation_capacity = mutations.len();
872        let mut mutations = mutations.into_iter();
873        self.execute_accepted_structural_mutation_batch_inner(
874            catalog,
875            descriptor,
876            mutation_capacity,
877            0,
878            || Ok(mutations.next()),
879            Timestamp::now(),
880            AcceptedStructuralMutationCommitOptions::standard(),
881            |rows, _report| {
882                let rows = rows
883                    .into_iter()
884                    .map(AcceptedStructuralMutationRow::into_values)
885                    .collect::<Vec<_>>();
886                precommit_validation(rows.as_slice())?;
887                Ok((rows, AcceptedStructuralMutationCommitDirective::Standard))
888            },
889        )
890    }
891
892    /// Materialize one accepted structural batch, let its caller prepare and
893    /// validate the final after-images, then commit atomically.
894    ///
895    /// The caller freezes one operation timestamp and supplies frontend-lowered
896    /// intent only. Accepted defaults, generated values, managed timestamps,
897    /// constraints, relations, row encoding, and commit preparation remain
898    /// owned by this database boundary.
899    pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
900        &self,
901        catalog: &AcceptedSchemaCatalogContext,
902        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
903        mutations: Vec<AcceptedStructuralMutation>,
904        operation_timestamp: Timestamp,
905        precommit_preparation: impl FnOnce(
906            Vec<AcceptedStructuralMutationRow>,
907        ) -> Result<T, InternalError>,
908    ) -> Result<T, InternalError> {
909        let mutation_capacity = mutations.len();
910        let identity_candidate_count = mutations
911            .iter()
912            .filter(|mutation| {
913                matches!(
914                    mutation,
915                    AcceptedStructuralMutation::Save {
916                        mode: MutationMode::Insert,
917                        target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
918                        ..
919                    }
920                )
921            })
922            .count();
923        let mut mutations = mutations.into_iter();
924        self.execute_accepted_structural_mutation_batch_inner(
925            catalog,
926            descriptor,
927            mutation_capacity,
928            identity_candidate_count,
929            || Ok(mutations.next()),
930            operation_timestamp,
931            AcceptedStructuralMutationCommitOptions::standard(),
932            |rows, _report| {
933                precommit_preparation(rows).map(|prepared| {
934                    (
935                        prepared,
936                        AcceptedStructuralMutationCommitDirective::Standard,
937                    )
938                })
939            },
940        )
941    }
942
943    /// Commit one complete accepted update page and its exact durable progress successor.
944    #[cfg(test)]
945    pub(in crate::db::session) fn execute_accepted_structural_update_with_mutation_progress(
946        &self,
947        catalog: &AcceptedSchemaCatalogContext,
948        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
949        mutations: Vec<AcceptedStructuralMutation>,
950        operation_timestamp: Timestamp,
951        mutation_progress: MutationProgressRecordOp,
952    ) -> Result<usize, InternalError> {
953        let mutation_capacity = mutations.len();
954        let mut mutations = mutations.into_iter();
955        self.execute_accepted_structural_mutation_batch_inner(
956            catalog,
957            descriptor,
958            mutation_capacity,
959            0,
960            || Ok(mutations.next()),
961            operation_timestamp,
962            AcceptedStructuralMutationCommitOptions::with_mutation_progress(),
963            |rows, _report| {
964                Ok((
965                    rows.len(),
966                    AcceptedStructuralMutationCommitDirective::WithMutationProgress(
967                        mutation_progress,
968                    ),
969                ))
970            },
971        )
972    }
973
974    /// Pack a checkpoint-aware update prefix using the writer's exact staging
975    /// charge, then apply the caller's atomic commit decision.
976    #[cfg(any(feature = "sql", test))]
977    pub(in crate::db::session) fn execute_accepted_structural_update_bounded_prefix<T>(
978        &self,
979        catalog: &AcceptedSchemaCatalogContext,
980        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
981        mutation_capacity: usize,
982        mut next_mutation: impl FnMut() -> Result<Option<AcceptedStructuralMutation>, InternalError>,
983        operation_timestamp: Timestamp,
984        precommit_preparation: impl FnOnce(
985            AcceptedStructuralMutationPackingReport,
986        ) -> Result<
987            (T, AcceptedStructuralMutationCommitDirective),
988            InternalError,
989        >,
990    ) -> Result<T, InternalError> {
991        self.execute_accepted_structural_mutation_batch_inner(
992            catalog,
993            descriptor,
994            mutation_capacity,
995            0,
996            &mut next_mutation,
997            operation_timestamp,
998            AcceptedStructuralMutationCommitOptions::bounded_prefix(),
999            |rows, report| {
1000                if rows.len() != report.admitted_mutations() {
1001                    return Err(InternalError::executor_invariant());
1002                }
1003                precommit_preparation(report)
1004            },
1005        )
1006    }
1007
1008    #[expect(
1009        clippy::too_many_arguments,
1010        clippy::too_many_lines,
1011        reason = "one phased owner keeps accepted authority, mutation context, precommit preparation, output capture, and commit staging inseparable"
1012    )]
1013    fn execute_accepted_structural_mutation_batch_inner<T>(
1014        &self,
1015        catalog: &AcceptedSchemaCatalogContext,
1016        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1017        mutation_capacity: usize,
1018        identity_candidate_count: usize,
1019        mut next_mutation: impl FnMut() -> Result<Option<AcceptedStructuralMutation>, InternalError>,
1020        operation_timestamp: Timestamp,
1021        options: AcceptedStructuralMutationCommitOptions,
1022        precommit_preparation: impl FnOnce(
1023            Vec<AcceptedStructuralMutationRow>,
1024            AcceptedStructuralMutationPackingReport,
1025        ) -> Result<
1026            (T, AcceptedStructuralMutationCommitDirective),
1027            InternalError,
1028        >,
1029    ) -> Result<T, InternalError> {
1030        let identity = catalog.identity();
1031        let AcceptedStructuralMutationCommitOptions {
1032            capture_output_values,
1033            packing,
1034        } = options;
1035        let entity_path = identity.entity_path();
1036        let store_path = identity.store_path();
1037        let row_decode_contract =
1038            descriptor.row_decode_contract(catalog.value_catalog_handle().clone());
1039        let row_contract = StructuralRowContract::from_accepted_decode_contract(
1040            entity_path,
1041            row_decode_contract.clone(),
1042        );
1043        let store = self.db.recovered_store(store_path)?;
1044        let write_context = dynamic_write_context(operation_timestamp);
1045        let identity_field = accepted_identity_insert_field(descriptor)?;
1046        let identity_incarnation = identity_field
1047            .as_ref()
1048            .map(|_| database_incarnation_id())
1049            .transpose()?;
1050        if mutation_capacity > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1051            return Err(InternalError::mutation_batch_too_many_items(
1052                mutation_capacity,
1053                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1054            ));
1055        }
1056        let _ = checked_pre_key_candidate_count(identity_candidate_count)?;
1057        let mut identity_cursor: Option<IdentityStatementCursor> = None;
1058        let mut identity_insert_ordinal = 0_u32;
1059        let mut scheduler = AcceptedMutationConstraintScheduler::new(
1060            entity_path,
1061            identity.entity_tag(),
1062            row_decode_contract.clone(),
1063            catalog.fingerprint(),
1064            catalog.fingerprint_method_version(),
1065            catalog.accepted_row_constraints(),
1066            mutation_capacity,
1067        );
1068        let mut output = Vec::with_capacity(mutation_capacity);
1069        let mut staged_bytes = 0_usize;
1070        let mut stopped_before_candidate = false;
1071        let mut candidate_exceeds_batch_policy = false;
1072        let mut input_index = 0_usize;
1073
1074        while let Some(mutation) = next_mutation()? {
1075            if input_index >= mutation_capacity {
1076                return Err(InternalError::mutation_batch_too_many_items(
1077                    input_index.saturating_add(1),
1078                    mutation_capacity,
1079                ));
1080            }
1081            let batch_input_ordinal = u32::try_from(input_index).map_err(|_| {
1082                InternalError::mutation_batch_too_many_items(
1083                    mutation_capacity,
1084                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1085                )
1086            })?;
1087            input_index = input_index.saturating_add(1);
1088            let AcceptedStructuralMutation::Save {
1089                mode,
1090                target,
1091                patch: authored_patch,
1092            } = mutation
1093            else {
1094                let AcceptedStructuralMutation::Delete { key } = mutation else {
1095                    return Err(InternalError::executor_invariant());
1096                };
1097                let before = validated_existing_row(store, &key, &row_contract)?
1098                    .ok_or_else(|| InternalError::store_not_found(&key))?;
1099                let raw_key = key.to_raw()?;
1100                let canonical_before = canonical_row_from_raw_row_with_accepted_decode_contract(
1101                    entity_path,
1102                    row_decode_contract.clone(),
1103                    &before,
1104                )?;
1105                let admission = admit_structural_mutation_staged_charge(
1106                    &mut staged_bytes,
1107                    [
1108                        raw_key.as_bytes().len(),
1109                        canonical_before.as_raw_row().as_bytes().len(),
1110                    ],
1111                    packing,
1112                )?;
1113                match admission {
1114                    AcceptedStructuralMutationStagedAdmission::Admitted => {}
1115                    AcceptedStructuralMutationStagedAdmission::PageFull => {
1116                        stopped_before_candidate = true;
1117                        break;
1118                    }
1119                    AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy => {
1120                        stopped_before_candidate = true;
1121                        candidate_exceeds_batch_policy = true;
1122                        break;
1123                    }
1124                }
1125                scheduler.schedule_delete(
1126                    CommitRowOp::new(
1127                        entity_path,
1128                        raw_key,
1129                        Some(canonical_before.as_raw_row().as_bytes().to_vec()),
1130                        None,
1131                        catalog.fingerprint(),
1132                    ),
1133                    batch_input_ordinal,
1134                )?;
1135                let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1136                    canonical_before.as_raw_row(),
1137                    &row_contract,
1138                )?;
1139                let values = if capture_output_values {
1140                    let mut values = Vec::with_capacity(descriptor.fields().len());
1141                    for field in descriptor.fields() {
1142                        values.push(
1143                            reader
1144                                .required_cached_value(usize::from(field.slot().get()))?
1145                                .clone(),
1146                        );
1147                    }
1148                    values
1149                } else {
1150                    Vec::new()
1151                };
1152                output.push(AcceptedStructuralMutationRow {
1153                    values,
1154                    logical_changed: true,
1155                });
1156                continue;
1157            };
1158            let mutation_context =
1159                mutation_diagnostic_context(identity.entity_tag(), mode, batch_input_ordinal);
1160            let (expected_key, preloaded_before, pre_key_insert, mut keyed_patch) = match target {
1161                AcceptedStructuralMutationTarget::ResolveFromAfterImage => {
1162                    let candidate_ordinal =
1163                        if identity_field.is_some() && matches!(mode, MutationMode::Insert) {
1164                            identity_insert_ordinal
1165                        } else {
1166                            batch_input_ordinal
1167                        };
1168                    (
1169                        None,
1170                        None,
1171                        Some(AcceptedPreKeyInsert::new(
1172                            identity.entity_tag(),
1173                            authored_patch,
1174                            candidate_ordinal,
1175                        )),
1176                        None,
1177                    )
1178                }
1179                AcceptedStructuralMutationTarget::Expected(key) => {
1180                    (Some(*key), None, None, Some(authored_patch))
1181                }
1182                AcceptedStructuralMutationTarget::ExpectedLoaded(loaded) => {
1183                    let (key, row) = loaded.into_parts();
1184                    (Some(key), Some(row), None, Some(authored_patch))
1185                }
1186            };
1187            if matches!(mode, MutationMode::Replace)
1188                && let Some(key) = expected_key.as_ref()
1189            {
1190                let patch = keyed_patch
1191                    .take()
1192                    .ok_or_else(InternalError::executor_invariant)?;
1193                keyed_patch = Some(preserve_dynamic_replacement_identity(
1194                    key, descriptor, patch,
1195                )?);
1196            }
1197            let patch = pre_key_insert
1198                .as_ref()
1199                .map(AcceptedPreKeyInsert::fields)
1200                .or(keyed_patch.as_ref())
1201                .ok_or_else(InternalError::executor_invariant)?;
1202            let before = match (expected_key.as_ref(), preloaded_before) {
1203                (Some(_), Some(row)) => Some(row),
1204                (Some(key), None) => validated_existing_row(store, key, &row_contract)?,
1205                (None, None) => None,
1206                (None, Some(_)) => return Err(InternalError::executor_invariant()),
1207            };
1208            match mode {
1209                MutationMode::Insert if before.is_some() => {
1210                    return Err(mutation_key_exists_error());
1211                }
1212                MutationMode::Update if before.is_none() => {
1213                    let key = expected_key
1214                        .as_ref()
1215                        .ok_or_else(InternalError::executor_invariant)?;
1216                    return Err(InternalError::store_not_found(key));
1217                }
1218                MutationMode::Insert | MutationMode::Replace | MutationMode::Update => {}
1219            }
1220
1221            let identity_allocation = if let Some(identity_field) = identity_field.as_ref()
1222                && matches!(mode, MutationMode::Insert)
1223                && before.is_none()
1224            {
1225                let candidate = pre_key_insert.as_ref().ok_or_else(|| {
1226                    InternalError::mutation_database_owned_field_explicit(
1227                        mutation_context,
1228                        identity_field.field_id.get(),
1229                    )
1230                })?;
1231                if identity_cursor.is_none() {
1232                    let incarnation = identity_incarnation
1233                        .ok_or_else(InternalError::identity_state_corruption)?;
1234                    identity_cursor = Some(store.with_schema(|schema_store| {
1235                        schema_store.identity_statement_cursor(
1236                            incarnation,
1237                            identity.entity_tag(),
1238                            identity_field.field_id,
1239                            &identity_field.accepted_kind,
1240                        )
1241                    })?);
1242                }
1243                let allocation = identity_cursor
1244                    .as_mut()
1245                    .ok_or_else(InternalError::identity_state_corruption)?
1246                    .allocate(identity_field.field_slot, candidate.input_ordinal())?;
1247                identity_insert_ordinal = identity_insert_ordinal
1248                    .checked_add(1)
1249                    .ok_or_else(InternalError::identity_candidate_count_exhausted)?;
1250                Some(allocation)
1251            } else if let Some(identity_field) = identity_field.as_ref()
1252                && matches!(mode, MutationMode::Replace)
1253                && before.is_none()
1254            {
1255                return Err(InternalError::mutation_database_owned_field_explicit(
1256                    mutation_context,
1257                    identity_field.field_id.get(),
1258                ));
1259            } else {
1260                None
1261            };
1262
1263            let resolved = match (mode, before.as_ref()) {
1264                (MutationMode::Insert | MutationMode::Replace, None) => {
1265                    resolve_insert_structural_patch_with_accepted_contract(
1266                        entity_path,
1267                        row_decode_contract.clone(),
1268                        catalog.fingerprint(),
1269                        catalog.accepted_row_constraints(),
1270                        patch,
1271                        write_context,
1272                        mutation_context,
1273                        identity_allocation.as_ref(),
1274                    )?
1275                }
1276                (MutationMode::Update, Some(before)) => {
1277                    resolve_update_structural_patch_with_accepted_contract(
1278                        entity_path,
1279                        row_decode_contract.clone(),
1280                        catalog.fingerprint(),
1281                        catalog.accepted_row_constraints(),
1282                        before,
1283                        patch,
1284                        write_context,
1285                        mutation_context,
1286                    )?
1287                }
1288                (MutationMode::Replace, Some(before)) => {
1289                    resolve_existing_replace_structural_patch_with_accepted_contract(
1290                        entity_path,
1291                        row_decode_contract.clone(),
1292                        catalog.fingerprint(),
1293                        catalog.accepted_row_constraints(),
1294                        before,
1295                        patch,
1296                        write_context,
1297                        mutation_context,
1298                    )?
1299                }
1300                (MutationMode::Insert, Some(_)) | (MutationMode::Update, None) => {
1301                    return Err(InternalError::executor_invariant());
1302                }
1303            };
1304            let (after, provenance) = resolved.into_parts();
1305            let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1306                after.as_raw_row(),
1307                &row_contract,
1308            )?;
1309            let data_key = match expected_key {
1310                Some(key) => {
1311                    reader.validate_primary_key(&key)?;
1312                    key
1313                }
1314                None => {
1315                    data_key_from_row(identity.entity_tag(), &row_contract, after.as_raw_row())?
1316                }
1317            };
1318            if let Some(allocation) = identity_allocation.as_ref() {
1319                validate_identity_materialization(
1320                    identity.entity_tag(),
1321                    identity_field
1322                        .as_ref()
1323                        .ok_or_else(InternalError::identity_corruption)?,
1324                    pre_key_insert
1325                        .as_ref()
1326                        .ok_or_else(InternalError::identity_corruption)?,
1327                    allocation,
1328                    &data_key,
1329                    &reader,
1330                )?;
1331            }
1332            if matches!(mode, MutationMode::Insert)
1333                && validated_existing_row(store, &data_key, &row_contract)?.is_some()
1334            {
1335                return Err(insert_key_exists_after_generation(
1336                    identity_allocation.is_some(),
1337                ));
1338            }
1339            let raw_key = data_key.to_raw()?;
1340            let canonical_before = before
1341                .as_ref()
1342                .map(|before| {
1343                    canonical_row_from_raw_row_with_accepted_decode_contract(
1344                        entity_path,
1345                        row_decode_contract.clone(),
1346                        before,
1347                    )
1348                })
1349                .transpose()?;
1350            let logical_changed = canonical_before.as_ref().is_none_or(|before| {
1351                before.as_raw_row().as_bytes() != after.as_raw_row().as_bytes()
1352            });
1353            let physical_changed = before
1354                .as_ref()
1355                .is_none_or(|before| before.as_bytes() != after.as_raw_row().as_bytes());
1356            let admission = admit_structural_mutation_staged_charge(
1357                &mut staged_bytes,
1358                [
1359                    raw_key.as_bytes().len(),
1360                    canonical_before
1361                        .as_ref()
1362                        .map_or(0, |before| before.as_raw_row().as_bytes().len()),
1363                    after.as_raw_row().as_bytes().len(),
1364                ],
1365                packing,
1366            )?;
1367            match admission {
1368                AcceptedStructuralMutationStagedAdmission::Admitted => {}
1369                AcceptedStructuralMutationStagedAdmission::PageFull => {
1370                    stopped_before_candidate = true;
1371                    break;
1372                }
1373                AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy => {
1374                    stopped_before_candidate = true;
1375                    candidate_exceeds_batch_policy = true;
1376                    break;
1377                }
1378            }
1379            let row_op = physical_changed.then(|| {
1380                CommitRowOp::new(
1381                    entity_path,
1382                    raw_key.clone(),
1383                    canonical_before
1384                        .as_ref()
1385                        .map(|before| before.as_raw_row().as_bytes().to_vec()),
1386                    Some(after.as_raw_row().as_bytes().to_vec()),
1387                    catalog.fingerprint(),
1388                )
1389            });
1390            scheduler.schedule_save_after_image(
1391                mode,
1392                &data_key,
1393                after.as_raw_row(),
1394                provenance.as_slice(),
1395                row_op,
1396                batch_input_ordinal,
1397            )?;
1398            let values = if capture_output_values {
1399                let mut values = Vec::with_capacity(descriptor.fields().len());
1400                for field in descriptor.fields() {
1401                    values.push(
1402                        reader
1403                            .required_cached_value(usize::from(field.slot().get()))?
1404                            .clone(),
1405                    );
1406                }
1407                values
1408            } else {
1409                Vec::new()
1410            };
1411            output.push(AcceptedStructuralMutationRow {
1412                values,
1413                logical_changed,
1414            });
1415        }
1416
1417        let report = AcceptedStructuralMutationPackingReport {
1418            admitted_mutations: output.len(),
1419            stopped_before_candidate,
1420            candidate_exceeds_batch_policy,
1421        };
1422        let batch = scheduler.finish();
1423        let (prepared, commit_directive) = precommit_preparation(output, report)?;
1424        let identity_ranges = identity_cursor
1425            .map(IdentityStatementCursor::into_range_advance)
1426            .transpose()?
1427            .into_iter()
1428            .flatten()
1429            .collect::<Vec<_>>();
1430        if !matches!(
1431            commit_directive,
1432            AcceptedStructuralMutationCommitDirective::Skip
1433        ) && batch.is_empty()
1434            && !identity_ranges.is_empty()
1435        {
1436            return Err(InternalError::identity_corruption());
1437        }
1438        match commit_directive {
1439            AcceptedStructuralMutationCommitDirective::Skip => {}
1440            AcceptedStructuralMutationCommitDirective::Standard if batch.is_empty() => {}
1441            AcceptedStructuralMutationCommitDirective::Standard => {
1442                commit_structural_row_ops_with_window_for_path(
1443                    &self.db,
1444                    entity_path,
1445                    batch,
1446                    identity_ranges,
1447                    "accepted_structural_batch_apply",
1448                )?;
1449            }
1450            AcceptedStructuralMutationCommitDirective::WithMutationProgress(operation)
1451                if batch.is_empty() =>
1452            {
1453                let _ = operation;
1454                return Err(InternalError::executor_invariant());
1455            }
1456            AcceptedStructuralMutationCommitDirective::WithMutationProgress(operation) => {
1457                commit_structural_row_ops_with_mutation_progress_for_path(
1458                    &self.db,
1459                    entity_path,
1460                    batch,
1461                    identity_ranges,
1462                    operation,
1463                    "accepted_structural_batch_apply",
1464                )?;
1465            }
1466        }
1467        Ok(prepared)
1468    }
1469
1470    fn execute_one_accepted_save_mutation(
1471        &self,
1472        catalog: &AcceptedSchemaCatalogContext,
1473        descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1474        mode: MutationMode,
1475        target: AcceptedStructuralMutationTarget,
1476        patch: AcceptedMutationIntentPatch,
1477    ) -> Result<DynamicMutationResult, InternalError> {
1478        let identity = catalog.identity();
1479        let entity_path = identity.entity_path();
1480        let result = self.execute_accepted_structural_save_batch(
1481            catalog,
1482            descriptor,
1483            vec![AcceptedStructuralMutation::save(mode, target, patch)],
1484            Timestamp::now(),
1485            |rows| prepare_dynamic_mutation_result(catalog, descriptor, rows, false),
1486        )?;
1487        record(MetricsEvent::SaveMutation {
1488            entity_path: entity_path.into(),
1489            kind: match mode {
1490                MutationMode::Insert => SaveMutationKind::Insert,
1491                MutationMode::Replace => SaveMutationKind::Replace,
1492                MutationMode::Update => SaveMutationKind::Update,
1493            },
1494            rows_touched: u64::from(result.affected_rows),
1495        });
1496        Ok(result)
1497    }
1498
1499    /// Execute one trusted entity-name-driven structural mutation.
1500    ///
1501    /// This lane resolves public values, defaults, generation, management,
1502    /// constraints, relations, and commit preparation from accepted schema.
1503    /// It never materializes a generated entity or invokes application
1504    /// validators/normalizers.
1505    pub fn execute_trusted_dynamic_mutation(
1506        &self,
1507        request: &DynamicMutation,
1508    ) -> Result<DynamicMutationResult, InternalError> {
1509        self.execute_trusted_dynamic_mutation_batch_with_result_policy(vec![request.clone()], false)
1510    }
1511
1512    /// Execute one bounded same-entity structural mutation batch atomically.
1513    ///
1514    /// Every item binds to the same accepted catalog identity, shares one
1515    /// operation timestamp, and is projected to its public result before the
1516    /// commit marker can be published.
1517    pub fn execute_trusted_dynamic_mutation_batch(
1518        &self,
1519        requests: Vec<DynamicMutation>,
1520    ) -> Result<DynamicMutationResult, InternalError> {
1521        self.execute_trusted_dynamic_mutation_batch_with_result_policy(requests, true)
1522    }
1523
1524    fn execute_trusted_dynamic_mutation_batch_with_result_policy(
1525        &self,
1526        requests: Vec<DynamicMutation>,
1527        enforce_mixed_batch_result_bound: bool,
1528    ) -> Result<DynamicMutationResult, InternalError> {
1529        if requests.is_empty() {
1530            return Err(InternalError::mutation_batch_empty());
1531        }
1532        if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1533            return Err(InternalError::mutation_batch_too_many_items(
1534                requests.len(),
1535                MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1536            ));
1537        }
1538        let first = requests
1539            .first()
1540            .ok_or_else(InternalError::mutation_batch_empty)?;
1541        if first.entity().is_empty() {
1542            return Err(InternalError::executor_unsupported());
1543        }
1544        let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
1545        let accepted_identity = catalog.identity();
1546        let descriptor =
1547            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1548        let mut mutations = Vec::with_capacity(requests.len());
1549        let mut save_kinds = Vec::with_capacity(requests.len());
1550
1551        for (batch_position, request) in requests.iter().enumerate() {
1552            let batch_position = u32::try_from(batch_position).map_err(|_| {
1553                InternalError::mutation_batch_too_many_items(
1554                    requests.len(),
1555                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1556                )
1557            })?;
1558            if request.entity().is_empty() {
1559                return Err(InternalError::executor_unsupported());
1560            }
1561            let item_catalog =
1562                self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
1563            if item_catalog.identity() != accepted_identity {
1564                return Err(InternalError::mutation_batch_entity_mismatch(
1565                    batch_position,
1566                    accepted_identity.entity_tag().value(),
1567                    item_catalog.identity().entity_tag().value(),
1568                ));
1569            }
1570            let (mutation, save_kind) = lower_dynamic_mutation_intent(
1571                accepted_identity.entity_tag(),
1572                accepted_identity.entity_path(),
1573                &descriptor,
1574                request,
1575                batch_position,
1576            )?;
1577            mutations.push(mutation);
1578            save_kinds.push(save_kind);
1579        }
1580
1581        let entity_path = accepted_identity.entity_path_handle();
1582        let (result, metrics) = self.execute_accepted_structural_save_batch(
1583            &catalog,
1584            &descriptor,
1585            mutations,
1586            Timestamp::now(),
1587            |rows| {
1588                if rows.len() != save_kinds.len() {
1589                    return Err(InternalError::executor_invariant());
1590                }
1591                let metrics = rows
1592                    .iter()
1593                    .zip(save_kinds)
1594                    .filter_map(|(row, kind)| kind.map(|kind| (kind, row.logical_changed())))
1595                    .collect::<Vec<_>>();
1596                let result = prepare_dynamic_mutation_result(
1597                    &catalog,
1598                    &descriptor,
1599                    rows,
1600                    enforce_mixed_batch_result_bound,
1601                )?;
1602                Ok((result, metrics))
1603            },
1604        )?;
1605        for (kind, logical_changed) in metrics {
1606            record(MetricsEvent::SaveMutation {
1607                entity_path: entity_path.clone(),
1608                kind,
1609                rows_touched: u64::from(logical_changed),
1610            });
1611        }
1612        Ok(result)
1613    }
1614
1615    /// Execute one generated typed write through immutable accepted entity and
1616    /// field identities. `None` means the opaque binding is stale.
1617    #[doc(hidden)]
1618    pub fn execute_trusted_typed_mutation(
1619        &self,
1620        binding: &DynamicTypedEntityBinding,
1621        request: &DynamicTypedMutation,
1622    ) -> Result<Option<DynamicMutationResult>, InternalError> {
1623        let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
1624            return Ok(None);
1625        };
1626        let identity = catalog.identity();
1627        let descriptor =
1628            AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1629        let mode = dynamic_typed_mutation_mode(request);
1630        let (target, patch) = match request {
1631            DynamicTypedMutation::Insert { patch } => (
1632                AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1633                patch,
1634            ),
1635            DynamicTypedMutation::Update { key, patch }
1636            | DynamicTypedMutation::Replace { key, patch } => (
1637                AcceptedStructuralMutationTarget::expected(dynamic_key(
1638                    identity.entity_tag(),
1639                    key,
1640                )?),
1641                patch,
1642            ),
1643        };
1644        if !patch.is_bound_to(binding) {
1645            return Ok(None);
1646        }
1647        let patch = lower_typed_patch(
1648            &descriptor,
1649            patch,
1650            mode,
1651            mutation_diagnostic_context(identity.entity_tag(), mode, 0),
1652        )?;
1653        self.execute_one_accepted_save_mutation(&catalog, &descriptor, mode, target, patch)
1654            .map(Some)
1655    }
1656
1657    /// Execute one trusted atomic insert batch from entity-name-driven patches.
1658    ///
1659    /// Every patch is lowered against the same accepted snapshot and shares
1660    /// one operation timestamp before the canonical structural batch owner
1661    /// stages any durable effect.
1662    pub fn execute_trusted_dynamic_insert_batch(
1663        &self,
1664        entity: &str,
1665        patches: Vec<DynamicStructuralPatch>,
1666    ) -> Result<DynamicMutationResult, InternalError> {
1667        let mutations = patches
1668            .into_iter()
1669            .map(|patch| DynamicMutation::Insert {
1670                entity: entity.to_string(),
1671                patch,
1672            })
1673            .collect();
1674        self.execute_trusted_dynamic_mutation_batch_with_result_policy(mutations, false)
1675    }
1676}
1677
1678#[cfg(test)]
1679mod typed_adapter_tests {
1680    use super::{
1681        AcceptedFieldKind, DbSession, DynamicTypedBindingError, DynamicTypedFieldBindingRequest,
1682        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell, dynamic_typed_field_type,
1683        typed_adapter_field_kind_matches,
1684    };
1685    use crate::{
1686        db::{
1687            data::DataStore,
1688            index::IndexStore,
1689            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1690            schema::{
1691                AcceptedSchemaRevision, FieldId, FieldStorageDecode, LeafCodec,
1692                PersistedFieldSnapshot, PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot,
1693                SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
1694                accepted_schema_candidate_with_field_bindings_for_tests,
1695            },
1696        },
1697        traits::{CanisterKind, Path},
1698        types::EntityTag,
1699        value::InputValue,
1700    };
1701    use icydb_schema::{EntitySourceKey, FieldSourceKey, ScalarType};
1702    use std::{cell::RefCell, collections::BTreeMap};
1703
1704    const STORE_PATH: &str = "session::write::typed_adapter_tests::Store";
1705    const ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::Entity";
1706    const OTHER_ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity";
1707    const ID_SOURCE: &str = "session::write::typed_adapter_tests::Entity::id";
1708    const VALUE_SOURCE: &str = "session::write::typed_adapter_tests::Entity::value";
1709    const REPLACEMENT_SOURCE: &str =
1710        "session::write::typed_adapter_tests::Entity::replacement_value";
1711    const OTHER_ID_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity::id";
1712
1713    struct TestCanister;
1714
1715    impl Path for TestCanister {
1716        const PATH: &'static str = "session::write::typed_adapter_tests::Canister";
1717    }
1718
1719    impl CanisterKind for TestCanister {
1720        const COMMIT_MEMORY_ID: u8 = 41;
1721        const COMMIT_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.commit.v1";
1722        const STARTUP_MEMORY_ID: u8 = 49;
1723        const STARTUP_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.startup.control.v1";
1724        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 42;
1725        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1726            "icydb.typed_adapter_tests.integrity.progress.v1";
1727    }
1728
1729    thread_local! {
1730        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1731        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1732        static SCHEMA_STORE: RefCell<SchemaStore> =
1733            const { RefCell::new(SchemaStore::init_heap()) };
1734        static STORE_REGISTRY: StoreRegistry = {
1735            let mut registry = StoreRegistry::new();
1736            registry.register_store(
1737                STORE_PATH,
1738                &DATA_STORE,
1739                &INDEX_STORE,
1740                &SCHEMA_STORE,
1741                StoreAllocationIdentities::absent(),
1742                StoreRuntimeStorageCapabilities::heap(),
1743            ).expect("typed adapter test store should register");
1744            registry
1745        };
1746    }
1747
1748    fn nat64_field(id: u32, name: &str, slot: u16) -> PersistedFieldSnapshot {
1749        PersistedFieldSnapshot::new_initial(
1750            FieldId::new(id),
1751            name.to_string(),
1752            SchemaFieldSlot::new(slot),
1753            AcceptedFieldKind::Nat64,
1754            Vec::new(),
1755            false,
1756            SchemaInsertDefault::None,
1757            FieldStorageDecode::ByKind,
1758            LeafCodec::Scalar(ScalarCodec::Nat64),
1759        )
1760    }
1761
1762    fn snapshot(
1763        entity_source: &str,
1764        entity_name: &str,
1765        fields: Vec<PersistedFieldSnapshot>,
1766    ) -> PersistedSchemaSnapshot {
1767        let layout = SchemaRowLayout::initial(
1768            fields
1769                .iter()
1770                .map(|field| (field.id(), field.slot()))
1771                .collect(),
1772        );
1773        PersistedSchemaSnapshot::new(
1774            SchemaVersion::initial(),
1775            entity_source.to_string(),
1776            entity_name.to_string(),
1777            FieldId::new(1),
1778            layout,
1779            fields,
1780        )
1781    }
1782
1783    fn field_source(source: &str) -> FieldSourceKey {
1784        FieldSourceKey::try_new(source).expect("typed field source should admit")
1785    }
1786
1787    fn entity_source(source: &str) -> EntitySourceKey {
1788        EntitySourceKey::try_new(source).expect("typed entity source should admit")
1789    }
1790
1791    fn publish(
1792        session: &DbSession<TestCanister>,
1793        expected: AcceptedSchemaRevision,
1794        revision: AcceptedSchemaRevision,
1795        snapshots: BTreeMap<EntityTag, PersistedSchemaSnapshot>,
1796        fields: BTreeMap<(EntityTag, FieldSourceKey), FieldId>,
1797    ) {
1798        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
1799            STORE_PATH, revision, snapshots, fields,
1800        );
1801        let store = session
1802            .db
1803            .store_handle(STORE_PATH)
1804            .expect("typed adapter test store should resolve");
1805        crate::db::commit::publish_accepted_schema_candidate(
1806            STORE_PATH, store, expected, &candidate,
1807        )
1808        .expect("typed binding candidate should publish");
1809    }
1810
1811    fn request(source: &str) -> DynamicTypedFieldBindingRequest {
1812        DynamicTypedFieldBindingRequest::new(
1813            source.to_string(),
1814            DynamicTypedFieldType::Scalar(ScalarType::Nat64),
1815            false,
1816        )
1817    }
1818
1819    fn assert_query_diagnostic(
1820        error: crate::db::QueryError,
1821        code: icydb_diagnostic_code::DiagnosticCode,
1822        origin: icydb_diagnostic_code::ErrorOrigin,
1823        detail: icydb_diagnostic_code::DiagnosticDetail,
1824    ) {
1825        let diagnostic = error.diagnostic();
1826        assert_eq!(diagnostic.code(), code);
1827        assert_eq!(diagnostic.origin(), origin);
1828        assert_eq!(diagnostic.detail(), Some(&detail));
1829    }
1830
1831    #[test]
1832    fn typed_adapter_kind_matching_is_exact_but_accepts_relation_key_wrappers() {
1833        let relation = AcceptedFieldKind::Relation {
1834            target_path: "test::Target".to_string(),
1835            target_entity_name: "Target".to_string(),
1836            target_entity_tag: EntityTag::new(7),
1837            target_store_path: "test::Store".to_string(),
1838            key_kind: Box::new(AcceptedFieldKind::Nat64),
1839        };
1840
1841        assert!(typed_adapter_field_kind_matches(
1842            &relation,
1843            &AcceptedFieldKind::Nat64,
1844        ));
1845        assert!(typed_adapter_field_kind_matches(
1846            &AcceptedFieldKind::List(Box::new(relation)),
1847            &AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Nat64)),
1848        ));
1849        assert!(!typed_adapter_field_kind_matches(
1850            &AcceptedFieldKind::Nat64,
1851            &AcceptedFieldKind::Nat32,
1852        ));
1853    }
1854
1855    #[test]
1856    fn typed_adapter_field_contract_rejects_invalid_named_source_identity() {
1857        assert!(matches!(
1858            dynamic_typed_field_type(DynamicTypedFieldType::Named(String::new())),
1859            Err(DynamicTypedBindingError::FieldUnavailable),
1860        ));
1861        assert!(matches!(
1862            dynamic_typed_field_type(DynamicTypedFieldType::Scalar(ScalarType::Nat16)),
1863            Ok(icydb_schema::FieldType::Scalar(ScalarType::Nat16)),
1864        ));
1865    }
1866
1867    // Keep the full rename, stale-binding, and old-name-reuse lifecycle in one
1868    // regression so each issued binding is checked against the next revision.
1869    #[expect(clippy::too_many_lines)]
1870    #[test]
1871    fn typed_binding_uses_accepted_ids_and_slots_across_renames_and_name_reuse() {
1872        let entity_tag = EntityTag::new(91);
1873        let other_entity_tag = EntityTag::new(92);
1874        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1875        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1876        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1877
1878        let session = DbSession::<TestCanister>::new(
1879            &STORE_REGISTRY,
1880            &crate::db::RequestExecutionRoot::__new_runtime_root(),
1881        );
1882        session
1883            .db
1884            .drive_startup_recovery_page()
1885            .expect("typed adapter test database should initialize");
1886        publish(
1887            &session,
1888            AcceptedSchemaRevision::NONE,
1889            AcceptedSchemaRevision::INITIAL,
1890            BTreeMap::from([(
1891                entity_tag,
1892                snapshot(
1893                    ENTITY_SOURCE,
1894                    "Entity",
1895                    vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1896                ),
1897            )]),
1898            BTreeMap::from([
1899                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1900                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1901            ]),
1902        );
1903
1904        let initial_catalog = session
1905            .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1906            .expect("initial source catalog lookup should inspect")
1907            .expect("initial source catalog should exist");
1908        assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1909        let initial = session
1910            .issue_typed_entity_binding(
1911                entity_source(ENTITY_SOURCE).as_str(),
1912                &[request(ID_SOURCE), request(VALUE_SOURCE)],
1913            )
1914            .expect("initial typed binding should issue");
1915        assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1916        assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1917        assert_eq!(initial.output_field_slot("value"), Some(1));
1918        let initial_patch = initial
1919            .bind_write_fields(vec![(
1920                VALUE_SOURCE.to_string(),
1921                DynamicWriteCell::Value(InputValue::Nat64(7)),
1922            )])
1923            .expect("source-bound patch should lower");
1924        assert_eq!(
1925            initial_patch.fields(),
1926            &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1927        );
1928
1929        publish(
1930            &session,
1931            AcceptedSchemaRevision::INITIAL,
1932            AcceptedSchemaRevision::new(2),
1933            BTreeMap::from([
1934                (
1935                    entity_tag,
1936                    snapshot(
1937                        ENTITY_SOURCE,
1938                        "RenamedEntity",
1939                        vec![
1940                            nat64_field(1, "id", 0),
1941                            nat64_field(2, "renamed_value", 1),
1942                            nat64_field(3, "value", 2),
1943                        ],
1944                    ),
1945                ),
1946                (
1947                    other_entity_tag,
1948                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1949                ),
1950            ]),
1951            BTreeMap::from([
1952                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1953                ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1954                (
1955                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
1956                    FieldId::new(3),
1957                ),
1958                (
1959                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1960                    FieldId::new(1),
1961                ),
1962            ]),
1963        );
1964
1965        let stale_authority = session
1966            .ensure_accepted_schema_authority_is_current_for_store_path(
1967                STORE_PATH,
1968                initial_catalog.value_catalog_handle().authority(),
1969            )
1970            .expect_err("the initial accepted authority must be stale after revision two");
1971        assert_eq!(
1972            stale_authority.diagnostic_facts(),
1973            vec![
1974                (
1975                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedRevision,
1976                    AcceptedSchemaRevision::INITIAL.get(),
1977                ),
1978                (
1979                    icydb_diagnostic_code::DiagnosticFactTag::CurrentRevision,
1980                    AcceptedSchemaRevision::new(2).get(),
1981                ),
1982            ],
1983        );
1984
1985        assert!(
1986            !session
1987                .typed_entity_binding_is_current(&initial)
1988                .expect("renamed binding currentness should inspect")
1989        );
1990        let renamed = session
1991            .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1992            .expect("renamed source-bound adapter should rebind");
1993        assert_eq!(renamed.entity(), "RenamedEntity");
1994        assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1995        assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1996        assert_eq!(renamed.output_field_slot("value"), None);
1997
1998        publish(
1999            &session,
2000            AcceptedSchemaRevision::new(2),
2001            AcceptedSchemaRevision::new(3),
2002            BTreeMap::from([
2003                (
2004                    entity_tag,
2005                    snapshot(
2006                        ENTITY_SOURCE,
2007                        "RenamedEntity",
2008                        vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
2009                    ),
2010                ),
2011                (
2012                    other_entity_tag,
2013                    snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
2014                ),
2015            ]),
2016            BTreeMap::from([
2017                ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
2018                (
2019                    (entity_tag, field_source(REPLACEMENT_SOURCE)),
2020                    FieldId::new(2),
2021                ),
2022                (
2023                    (other_entity_tag, field_source(OTHER_ID_SOURCE)),
2024                    FieldId::new(1),
2025                ),
2026            ]),
2027        );
2028
2029        assert!(matches!(
2030            session.issue_typed_entity_binding(
2031                ENTITY_SOURCE,
2032                &[request(ID_SOURCE), request(VALUE_SOURCE)],
2033            ),
2034            Err(DynamicTypedBindingError::FieldUnavailable),
2035        ));
2036        assert!(
2037            !session
2038                .typed_entity_binding_is_current(&renamed)
2039                .expect("removed source binding should become stale")
2040        );
2041
2042        let replacement = session
2043            .issue_typed_entity_binding(
2044                ENTITY_SOURCE,
2045                &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
2046            )
2047            .expect("explicit replacement source should bind");
2048        assert!(
2049            session
2050                .execute_trusted_typed_mutation(
2051                    &replacement,
2052                    &DynamicTypedMutation::Insert {
2053                        patch: initial_patch
2054                    },
2055                )
2056                .expect("cross-binding patch should fail closed")
2057                .is_none()
2058        );
2059        let patch = replacement
2060            .bind_write_fields(vec![
2061                (
2062                    ID_SOURCE.to_string(),
2063                    DynamicWriteCell::Value(InputValue::Nat64(1)),
2064                ),
2065                (
2066                    REPLACEMENT_SOURCE.to_string(),
2067                    DynamicWriteCell::Value(InputValue::Nat64(9)),
2068                ),
2069            ])
2070            .expect("replacement source write should bind by accepted IDs and slots");
2071        let result = session
2072            .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
2073            .expect("typed insert should use the accepted mutation pipeline")
2074            .expect("replacement binding should remain current");
2075        assert_eq!(result.entity, "RenamedEntity");
2076        assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
2077        assert_eq!(
2078            result.rows,
2079            vec![vec![
2080                crate::value::OutputValue::Nat64(1),
2081                crate::value::OutputValue::Nat64(9)
2082            ]]
2083        );
2084        assert_eq!(result.affected_rows, 1);
2085
2086        let second_patch = replacement
2087            .bind_write_fields(vec![
2088                (
2089                    ID_SOURCE.to_string(),
2090                    DynamicWriteCell::Value(InputValue::Nat64(2)),
2091                ),
2092                (
2093                    REPLACEMENT_SOURCE.to_string(),
2094                    DynamicWriteCell::Value(InputValue::Nat64(10)),
2095                ),
2096            ])
2097            .expect("second source-bound patch should lower");
2098        session
2099            .execute_trusted_typed_mutation(
2100                &replacement,
2101                &DynamicTypedMutation::Insert {
2102                    patch: second_patch,
2103                },
2104            )
2105            .expect("second typed insert should use the accepted mutation pipeline")
2106            .expect("replacement binding should remain current");
2107
2108        {
2109            let query = crate::db::DynamicQuery::new("RenamedEntity")
2110                .select(["id", "value"])
2111                .order_by(crate::db::asc("id"))
2112                .limit(1);
2113            let result = session
2114                .execute_trusted_live_page(&query, None)
2115                .expect("SQL-free dynamic execution should use accepted authority");
2116            assert_eq!(result.entity, "RenamedEntity");
2117            assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
2118            assert_eq!(
2119                result.rows,
2120                vec![vec![
2121                    crate::value::OutputValue::Nat64(1),
2122                    crate::value::OutputValue::Nat64(9)
2123                ]]
2124            );
2125            assert_eq!(result.row_count, 1);
2126            assert_query_diagnostic(
2127                session
2128                    .execute_trusted_live_page(&query.cursor("00"), None)
2129                    .expect_err("scalar execution must reject grouped cursor state"),
2130                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
2131                icydb_diagnostic_code::ErrorOrigin::Query,
2132                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
2133                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
2134                },
2135            );
2136            assert_query_diagnostic(
2137                session
2138                    .execute_public_dynamic_grouped_query(
2139                        &crate::db::DynamicQuery::new("RenamedEntity").grouped_limits(1, 1024),
2140                    )
2141                    .expect_err("grouped execution must reject scalar query state"),
2142                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
2143                icydb_diagnostic_code::ErrorOrigin::Query,
2144                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
2145                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
2146                },
2147            );
2148
2149            let grouped_query = crate::db::DynamicQuery::new("RenamedEntity")
2150                .filter(crate::db::FieldRef::new("id").eq(1_u64))
2151                .group_by("value")
2152                .aggregate(crate::db::count())
2153                .grouped_limits(1, 16 * 1024)
2154                .limit(1);
2155            let grouped = session
2156                .execute_public_dynamic_grouped_query(&grouped_query)
2157                .expect("SQL-free grouped execution should use accepted authority");
2158            let typed_grouped = session
2159                .execute_public_dynamic_grouped_query_for_typed_binding(
2160                    &replacement,
2161                    &grouped_query,
2162                )
2163                .expect("typed grouped execution should inspect accepted authority")
2164                .expect("replacement binding should remain current");
2165            assert_eq!(typed_grouped, grouped);
2166            assert!(
2167                session
2168                    .execute_public_dynamic_grouped_query_for_typed_binding(
2169                        &renamed,
2170                        &grouped_query,
2171                    )
2172                    .expect("stale grouped binding should inspect accepted authority")
2173                    .is_none(),
2174                "stale typed grouped bindings must fail closed before execution"
2175            );
2176            assert_eq!(grouped.entity, "RenamedEntity");
2177            assert_eq!(grouped.row_count, 1);
2178            assert_eq!(grouped.rows.len(), 1);
2179            assert_eq!(
2180                grouped.rows[0].group_key(),
2181                &[crate::value::OutputValue::Nat64(9)]
2182            );
2183            assert_eq!(
2184                grouped.rows[0].aggregate_values(),
2185                &[crate::value::OutputValue::Nat64(1)]
2186            );
2187            assert_eq!(grouped.next_cursor, None);
2188
2189            let grouped_state_error = session
2190                .execute_trusted_dynamic_grouped_query(&grouped_query.clone().grouped_limits(1, 1))
2191                .expect_err("grouped retained state must respect its explicit byte ceiling");
2192            assert!(matches!(
2193                grouped_state_error.diagnostic().detail(),
2194                Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2195                    boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
2196                })
2197            ));
2198            assert_eq!(
2199                grouped_state_error.diagnostic_facts()[0],
2200                (
2201                    icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
2202                    icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctStateBytes.raw(),
2203                ),
2204            );
2205
2206            assert_query_diagnostic(
2207                session
2208                    .execute_public_dynamic_grouped_query(&grouped_query.clone().select(["value"]))
2209                    .expect_err("grouped output must reject scalar selection"),
2210                icydb_diagnostic_code::DiagnosticCode::QueryIntent,
2211                icydb_diagnostic_code::ErrorOrigin::Query,
2212                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
2213                    kind: icydb_diagnostic_code::QueryErrorKind::Intent,
2214                },
2215            );
2216            assert_query_diagnostic(
2217                session
2218                    .execute_public_dynamic_grouped_query(
2219                        &crate::db::DynamicQuery::new("RenamedEntity")
2220                            .group_by("value")
2221                            .aggregate(crate::db::count()),
2222                    )
2223                    .expect_err("public grouped execution must require explicit limits"),
2224                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
2225                icydb_diagnostic_code::ErrorOrigin::Query,
2226                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
2227                    reason:
2228                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
2229                },
2230            );
2231            assert_query_diagnostic(
2232                session
2233                    .execute_trusted_dynamic_grouped_query(
2234                        &crate::db::DynamicQuery::new("RenamedEntity")
2235                            .group_by("value")
2236                            .aggregate(crate::db::count())
2237                            .grouped_limits(0, 1024),
2238                    )
2239                    .expect_err("trusted grouped execution must reject zero limits"),
2240                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
2241                icydb_diagnostic_code::ErrorOrigin::Query,
2242                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
2243                    reason:
2244                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
2245                },
2246            );
2247            assert_query_diagnostic(
2248                session
2249                    .execute_public_dynamic_grouped_query(&grouped_query.grouped_limits(101, 1024))
2250                    .expect_err("public grouped execution must enforce its group budget"),
2251                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
2252                icydb_diagnostic_code::ErrorOrigin::Query,
2253                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
2254                    reason:
2255                        icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryExceedsBudget,
2256                },
2257            );
2258
2259            let paged_query = crate::db::DynamicQuery::new("RenamedEntity")
2260                .group_by("value")
2261                .aggregate(crate::db::count())
2262                .grouped_limits(2, 16 * 1024)
2263                .limit(1);
2264            assert_query_diagnostic(
2265                session
2266                    .execute_public_dynamic_grouped_query(&paged_query)
2267                    .expect_err("public grouped execution must reject an unbounded full scan"),
2268                icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
2269                icydb_diagnostic_code::ErrorOrigin::Query,
2270                icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
2271                    reason:
2272                        icydb_diagnostic_code::QueryReadAdmissionCode::UnboundedFullScanRejected,
2273                },
2274            );
2275            let first_page = session
2276                .execute_trusted_dynamic_grouped_query(&paged_query)
2277                .expect("SQL-free grouped first page should execute");
2278            assert_eq!(first_page.row_count, 1);
2279            assert_eq!(
2280                first_page.rows[0].group_key(),
2281                &[crate::value::OutputValue::Nat64(9)]
2282            );
2283            let cursor = first_page
2284                .next_cursor
2285                .expect("first grouped page should return a continuation cursor");
2286            assert_query_diagnostic(
2287                session
2288                    .execute_trusted_dynamic_grouped_query(
2289                        &paged_query.clone().cursor(format!("{cursor}0")),
2290                    )
2291                    .expect_err("tampered grouped cursor must fail closed"),
2292                icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
2293                icydb_diagnostic_code::ErrorOrigin::Cursor,
2294                icydb_diagnostic_code::DiagnosticDetail::QueryKind {
2295                    kind: icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor,
2296                },
2297            );
2298            let second_page = session
2299                .execute_trusted_dynamic_grouped_query(&paged_query.cursor(cursor))
2300                .expect("SQL-free grouped continuation should execute");
2301            assert_eq!(second_page.row_count, 1);
2302            assert_eq!(
2303                second_page.rows[0].group_key(),
2304                &[crate::value::OutputValue::Nat64(10)]
2305            );
2306            assert_eq!(second_page.next_cursor, None);
2307        }
2308    }
2309}
2310
2311#[cfg(test)]
2312mod mixed_relation_batch_tests {
2313    use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
2314    use crate::{
2315        db::{
2316            DynamicQuery, asc,
2317            data::DataStore,
2318            desc,
2319            index::IndexStore,
2320            query::expr::FilterExpr,
2321            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
2322            schema::{
2323                AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
2324                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2325                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2326                PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
2327                SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
2328                SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
2329            },
2330        },
2331        error::ErrorClass,
2332        traits::{CanisterKind, Path},
2333        types::EntityTag,
2334        value::{InputValue, OutputValue},
2335    };
2336    use icydb_schema::FieldSourceKey;
2337    use std::{cell::RefCell, collections::BTreeMap};
2338
2339    const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
2340    const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
2341    const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
2342    const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
2343    const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
2344    const ENTITY_NAME: &str = "MixedRelationNode";
2345    const ENTITY_TAG: EntityTag = EntityTag::new(94);
2346    const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
2347    const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
2348    const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
2349    const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
2350    const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
2351
2352    struct TestCanister;
2353
2354    impl Path for TestCanister {
2355        const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
2356    }
2357
2358    impl CanisterKind for TestCanister {
2359        const COMMIT_MEMORY_ID: u8 = 47;
2360        const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
2361        const STARTUP_MEMORY_ID: u8 = 50;
2362        const STARTUP_STABLE_KEY: &'static str =
2363            "icydb.mixed_relation_batch_tests.startup.control.v1";
2364        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
2365        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2366            "icydb.mixed_relation_batch_tests.integrity.progress.v1";
2367    }
2368
2369    thread_local! {
2370        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2371        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2372        static SCHEMA_STORE: RefCell<SchemaStore> =
2373            const { RefCell::new(SchemaStore::init_heap()) };
2374        static STORE_REGISTRY: StoreRegistry = {
2375            let mut registry = StoreRegistry::new();
2376            registry.register_store(
2377                STORE_PATH,
2378                &DATA_STORE,
2379                &INDEX_STORE,
2380                &SCHEMA_STORE,
2381                StoreAllocationIdentities::absent(),
2382                StoreRuntimeStorageCapabilities::heap(),
2383            ).expect("mixed relation test store should register");
2384            registry
2385        };
2386    }
2387
2388    fn source_key(source: &str) -> FieldSourceKey {
2389        FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
2390    }
2391
2392    fn relation_snapshot() -> PersistedSchemaSnapshot {
2393        let fields = vec![
2394            PersistedFieldSnapshot::new_initial(
2395                FieldId::new(1),
2396                "id".to_string(),
2397                SchemaFieldSlot::new(0),
2398                AcceptedFieldKind::Nat64,
2399                Vec::new(),
2400                false,
2401                SchemaInsertDefault::None,
2402                FieldStorageDecode::ByKind,
2403                LeafCodec::Scalar(ScalarCodec::Nat64),
2404            ),
2405            PersistedFieldSnapshot::new_initial(
2406                FieldId::new(2),
2407                "parent_id".to_string(),
2408                SchemaFieldSlot::new(1),
2409                AcceptedFieldKind::Nat64,
2410                Vec::new(),
2411                true,
2412                SchemaInsertDefault::None,
2413                FieldStorageDecode::ByKind,
2414                LeafCodec::Scalar(ScalarCodec::Nat64),
2415            ),
2416            PersistedFieldSnapshot::new_initial(
2417                FieldId::new(3),
2418                "code".to_string(),
2419                SchemaFieldSlot::new(2),
2420                AcceptedFieldKind::Nat64,
2421                Vec::new(),
2422                false,
2423                SchemaInsertDefault::None,
2424                FieldStorageDecode::ByKind,
2425                LeafCodec::Scalar(ScalarCodec::Nat64),
2426            ),
2427        ];
2428        let relation = PersistedRelationEdgeSnapshot::new(
2429            RelationId::new(1).expect("mixed relation identity should be non-zero"),
2430            "parent".to_string(),
2431            ENTITY_SOURCE.to_string(),
2432            vec![FieldId::new(2)],
2433        );
2434        let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2435            SchemaVersion::initial(),
2436            ENTITY_SOURCE.to_string(),
2437            ENTITY_NAME.to_string(),
2438            FieldId::new(1),
2439            SchemaRowLayout::initial(
2440                fields
2441                    .iter()
2442                    .map(|field| (field.id(), field.slot()))
2443                    .collect(),
2444            ),
2445            fields,
2446            vec![PersistedIndexSnapshot::new(
2447                SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
2448                1,
2449                "by_code".to_string(),
2450                STORE_PATH.to_string(),
2451                true,
2452                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2453                    FieldId::new(3),
2454                    SchemaFieldSlot::new(2),
2455                    vec!["code".to_string()],
2456                    AcceptedFieldKind::Nat64,
2457                    false,
2458                )]),
2459                None,
2460            )],
2461        )
2462        .with_relations(vec![relation]);
2463        let constraints = AcceptedConstraintCatalog::initial(
2464            snapshot.fields(),
2465            snapshot.indexes(),
2466            snapshot.relations(),
2467        )
2468        .expect("mixed relation constraints should close");
2469        snapshot.with_constraint_catalog(constraints)
2470    }
2471
2472    fn other_snapshot() -> PersistedSchemaSnapshot {
2473        let fields = vec![
2474            PersistedFieldSnapshot::new_initial(
2475                FieldId::new(1),
2476                "id".to_string(),
2477                SchemaFieldSlot::new(0),
2478                AcceptedFieldKind::Nat64,
2479                Vec::new(),
2480                false,
2481                SchemaInsertDefault::None,
2482                FieldStorageDecode::ByKind,
2483                LeafCodec::Scalar(ScalarCodec::Nat64),
2484            ),
2485            PersistedFieldSnapshot::new_initial(
2486                FieldId::new(2),
2487                "value".to_string(),
2488                SchemaFieldSlot::new(1),
2489                AcceptedFieldKind::Nat64,
2490                Vec::new(),
2491                false,
2492                SchemaInsertDefault::None,
2493                FieldStorageDecode::ByKind,
2494                LeafCodec::Scalar(ScalarCodec::Nat64),
2495            ),
2496        ];
2497        PersistedSchemaSnapshot::new(
2498            SchemaVersion::initial(),
2499            OTHER_ENTITY_SOURCE.to_string(),
2500            OTHER_ENTITY_NAME.to_string(),
2501            FieldId::new(1),
2502            SchemaRowLayout::initial(
2503                fields
2504                    .iter()
2505                    .map(|field| (field.id(), field.slot()))
2506                    .collect(),
2507            ),
2508            fields,
2509        )
2510    }
2511
2512    fn initialize() -> DbSession<TestCanister> {
2513        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2514        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2515        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2516        let session = DbSession::<TestCanister>::new(
2517            &STORE_REGISTRY,
2518            &crate::db::RequestExecutionRoot::__new_runtime_root(),
2519        );
2520        session
2521            .db
2522            .drive_startup_recovery_page()
2523            .expect("mixed relation database should initialize");
2524        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2525            STORE_PATH,
2526            AcceptedSchemaRevision::INITIAL,
2527            BTreeMap::from([
2528                (ENTITY_TAG, relation_snapshot()),
2529                (OTHER_ENTITY_TAG, other_snapshot()),
2530            ]),
2531            BTreeMap::from([
2532                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2533                ((ENTITY_TAG, source_key(PARENT_SOURCE)), FieldId::new(2)),
2534                ((ENTITY_TAG, source_key(CODE_SOURCE)), FieldId::new(3)),
2535                (
2536                    (OTHER_ENTITY_TAG, source_key(OTHER_ID_SOURCE)),
2537                    FieldId::new(1),
2538                ),
2539                (
2540                    (OTHER_ENTITY_TAG, source_key(OTHER_VALUE_SOURCE)),
2541                    FieldId::new(2),
2542                ),
2543            ]),
2544        );
2545        let store = session
2546            .db
2547            .store_handle(STORE_PATH)
2548            .expect("mixed relation store should resolve");
2549        crate::db::commit::publish_accepted_schema_candidate(
2550            STORE_PATH,
2551            store,
2552            AcceptedSchemaRevision::NONE,
2553            &candidate,
2554        )
2555        .expect("mixed relation candidate should publish");
2556        session
2557    }
2558
2559    fn patch(id: Option<u64>, parent: Option<u64>, code: Option<u64>) -> DynamicStructuralPatch {
2560        let mut fields = Vec::new();
2561        if let Some(id) = id {
2562            fields.push((
2563                "id".to_string(),
2564                DynamicWriteCell::Value(InputValue::Nat64(id)),
2565            ));
2566        }
2567        fields.push((
2568            "parent_id".to_string(),
2569            parent.map_or(DynamicWriteCell::Null, |parent| {
2570                DynamicWriteCell::Value(InputValue::Nat64(parent))
2571            }),
2572        ));
2573        if let Some(code) = code {
2574            fields.push((
2575                "code".to_string(),
2576                DynamicWriteCell::Value(InputValue::Nat64(code)),
2577            ));
2578        }
2579        DynamicStructuralPatch::new(fields)
2580    }
2581
2582    fn insert(id: u64, parent: Option<u64>) -> DynamicMutation {
2583        insert_with_code(id, parent, id)
2584    }
2585
2586    fn insert_with_code(id: u64, parent: Option<u64>, code: u64) -> DynamicMutation {
2587        DynamicMutation::Insert {
2588            entity: ENTITY_NAME.to_string(),
2589            patch: patch(Some(id), parent, Some(code)),
2590        }
2591    }
2592
2593    fn update_parent(id: u64, parent: Option<u64>) -> DynamicMutation {
2594        DynamicMutation::Update {
2595            entity: ENTITY_NAME.to_string(),
2596            key: InputValue::Nat64(id),
2597            patch: patch(None, parent, None),
2598        }
2599    }
2600
2601    fn update_code(id: u64, code: u64) -> DynamicMutation {
2602        DynamicMutation::Update {
2603            entity: ENTITY_NAME.to_string(),
2604            key: InputValue::Nat64(id),
2605            patch: DynamicStructuralPatch::new(vec![(
2606                "code".to_string(),
2607                DynamicWriteCell::Value(InputValue::Nat64(code)),
2608            )]),
2609        }
2610    }
2611
2612    fn delete(id: u64) -> DynamicMutation {
2613        DynamicMutation::Delete {
2614            entity: ENTITY_NAME.to_string(),
2615            key: InputValue::Nat64(id),
2616        }
2617    }
2618
2619    fn expected_row(id: u64, parent: Option<u64>) -> Vec<OutputValue> {
2620        expected_row_with_code(id, parent, id)
2621    }
2622
2623    fn expected_row_with_code(id: u64, parent: Option<u64>, code: u64) -> Vec<OutputValue> {
2624        vec![
2625            OutputValue::Nat64(id),
2626            parent.map_or(OutputValue::Null, OutputValue::Nat64),
2627            OutputValue::Nat64(code),
2628        ]
2629    }
2630
2631    fn other_patch(id: Option<u64>, value: u64) -> DynamicStructuralPatch {
2632        let mut fields = Vec::new();
2633        if let Some(id) = id {
2634            fields.push((
2635                "id".to_string(),
2636                DynamicWriteCell::Value(InputValue::Nat64(id)),
2637            ));
2638        }
2639        fields.push((
2640            "value".to_string(),
2641            DynamicWriteCell::Value(InputValue::Nat64(value)),
2642        ));
2643        DynamicStructuralPatch::new(fields)
2644    }
2645
2646    fn assert_relation_violation(error: &crate::error::InternalError) {
2647        assert!(error.diagnostic_facts().contains(&(
2648            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
2649            icydb_diagnostic_code::DiagnosticConstraintKind::Relation.raw(),
2650        )));
2651    }
2652
2653    #[test]
2654    fn live_pages_resume_mixed_projection_from_authenticated_hidden_order_values() {
2655        let session = initialize();
2656        session
2657            .execute_trusted_dynamic_mutation_batch(vec![
2658                insert_with_code(1, None, 10),
2659                insert_with_code(2, Some(1), 20),
2660                insert_with_code(3, None, 30),
2661            ])
2662            .expect("live-page rows should insert");
2663        let query = DynamicQuery::new(ENTITY_NAME)
2664            .select(["id"])
2665            .order_by(desc("code"));
2666
2667        let first = session
2668            .execute_public_live_page(&query, None)
2669            .expect("initial live page should execute");
2670        assert_eq!(
2671            first.rows,
2672            vec![vec![OutputValue::Nat64(3)], vec![OutputValue::Nat64(2)]]
2673        );
2674        let cursor = first
2675            .continuation
2676            .as_deref()
2677            .expect("unreturned matching row should produce continuation");
2678        let second = session
2679            .execute_public_live_page(&query, Some(cursor))
2680            .expect("authenticated live continuation should resume");
2681        assert_eq!(second.rows, vec![vec![OutputValue::Nat64(1)]]);
2682        assert_eq!(second.continuation, None);
2683
2684        let total_limit = session
2685            .execute_public_live_page(&query.clone().limit(2), None)
2686            .expect("total live-page limit should execute");
2687        assert_eq!(
2688            total_limit.rows,
2689            vec![vec![OutputValue::Nat64(3)], vec![OutputValue::Nat64(2)]],
2690        );
2691        assert_eq!(
2692            total_limit.continuation, None,
2693            "query LIMIT is a total traversal window rather than a page size",
2694        );
2695
2696        let three_row_window = query.clone().limit(3);
2697        let limited_first = session
2698            .execute_public_live_page(&three_row_window, None)
2699            .expect("first total-window page should execute");
2700        let limited_cursor = limited_first
2701            .continuation
2702            .as_deref()
2703            .expect("a partially consumed total window should continue");
2704        let limited_second = session
2705            .execute_public_live_page(&three_row_window, Some(limited_cursor))
2706            .expect("remaining total window should preserve the plan signature");
2707        assert_eq!(limited_second.rows, vec![vec![OutputValue::Nat64(1)]]);
2708        assert_eq!(limited_second.continuation, None);
2709
2710        let mixed_order = DynamicQuery::new(ENTITY_NAME)
2711            .select(["id"])
2712            .order_by(desc("parent_id"))
2713            .order_by(asc("id"));
2714        let mixed_first = session
2715            .execute_trusted_live_page(&mixed_order, None)
2716            .expect("mixed-direction nullable order should execute");
2717        assert_eq!(
2718            mixed_first.rows,
2719            vec![vec![OutputValue::Nat64(2)], vec![OutputValue::Nat64(1)]],
2720        );
2721        let mixed_cursor = mixed_first
2722            .continuation
2723            .as_deref()
2724            .expect("duplicate null order values should retain continuation");
2725        let mixed_second = session
2726            .execute_trusted_live_page(&mixed_order, Some(mixed_cursor))
2727            .expect("mixed-direction nullable order should resume");
2728        assert_eq!(mixed_second.rows, vec![vec![OutputValue::Nat64(3)]]);
2729        assert_eq!(mixed_second.continuation, None);
2730
2731        let mismatched_window = session
2732            .execute_public_live_page(&query.clone().limit(3), Some(cursor))
2733            .expect_err("a changed total limit must invalidate the continuation");
2734        assert_eq!(
2735            mismatched_window.diagnostic_code(),
2736            icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
2737        );
2738
2739        let mut tampered = cursor.as_bytes().to_vec();
2740        let last = tampered.len().saturating_sub(1);
2741        tampered[last] = if tampered[last] == b'0' { b'1' } else { b'0' };
2742        let tampered = String::from_utf8(tampered).expect("hex cursor should remain UTF-8");
2743        let error = session
2744            .execute_public_live_page(&query, Some(tampered.as_str()))
2745            .expect_err("tampered cursor must fail closed");
2746        assert_eq!(
2747            error.diagnostic_code(),
2748            icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
2749        );
2750    }
2751
2752    #[test]
2753    fn live_pages_resume_across_changed_output_work_envelopes() {
2754        let session = initialize();
2755        session
2756            .execute_trusted_dynamic_mutation_batch(vec![
2757                insert(1, None),
2758                insert(2, None),
2759                insert(3, None),
2760            ])
2761            .expect("output-envelope rows should insert");
2762        let query = DynamicQuery::new(ENTITY_NAME)
2763            .select(["id"])
2764            .order_by(desc("code"));
2765        let first = session
2766            .execute_trusted_live_page_with_result_bytes_limit_for_tests(&query, None, 32)
2767            .expect("small output envelope should publish the first bounded page");
2768        assert_eq!(first.rows, vec![vec![OutputValue::Nat64(3)]]);
2769        let continuation = first
2770            .continuation
2771            .expect("small output envelope should leave authenticated progress");
2772
2773        let second = session
2774            .execute_trusted_live_page_with_result_bytes_limit_for_tests(
2775                &query,
2776                Some(continuation.as_str()),
2777                64,
2778            )
2779            .unwrap_or_else(|error| {
2780                panic!(
2781                    "larger output envelope should resume the same query: {error:?}, facts={:?}",
2782                    error.diagnostic_facts(),
2783                )
2784            });
2785        assert_eq!(
2786            second.rows,
2787            vec![vec![OutputValue::Nat64(2)], vec![OutputValue::Nat64(1)]]
2788        );
2789        let second_continuation = second
2790            .continuation
2791            .as_deref()
2792            .expect("an exact-full page still needs to prove physical exhaustion");
2793        assert_ne!(first.work.envelope_identity, second.work.envelope_identity);
2794
2795        let terminal = session
2796            .execute_trusted_live_page_with_result_bytes_limit_for_tests(
2797                &query,
2798                Some(second_continuation),
2799                48,
2800            )
2801            .expect("a third finite envelope should prove exhaustion without replaying rows");
2802        assert!(terminal.rows.is_empty());
2803        assert_eq!(terminal.continuation, None);
2804        assert_ne!(
2805            second.work.envelope_identity,
2806            terminal.work.envelope_identity
2807        );
2808
2809        assert_eq!(
2810            [first.rows, second.rows, terminal.rows].concat(),
2811            vec![
2812                vec![OutputValue::Nat64(3)],
2813                vec![OutputValue::Nat64(2)],
2814                vec![OutputValue::Nat64(1)],
2815            ]
2816        );
2817    }
2818
2819    #[test]
2820    fn distinct_live_pages_resume_adjacent_groups_and_global_replay_end_to_end() {
2821        let session = initialize();
2822        session
2823            .execute_trusted_dynamic_mutation_batch(vec![
2824                insert(1, None),
2825                insert(2, None),
2826                insert(3, Some(1)),
2827                insert(4, Some(2)),
2828                insert(5, Some(1)),
2829                insert(6, Some(3)),
2830                insert(7, Some(2)),
2831            ])
2832            .expect("DISTINCT continuation rows should insert atomically");
2833
2834        let adjacent = DynamicQuery::new(ENTITY_NAME)
2835            .select(["parent_id"])
2836            .order_by(asc("parent_id"))
2837            .order_by(asc("id"))
2838            .distinct_for_internal_execution();
2839        let global = DynamicQuery::new(ENTITY_NAME)
2840            .select(["parent_id"])
2841            .order_by(asc("id"))
2842            .distinct_for_internal_execution();
2843
2844        let traverse = |query: &DynamicQuery, strategy: &str| {
2845            let mut continuation = None;
2846            let mut rows = Vec::new();
2847            let mut cursors = std::collections::BTreeSet::new();
2848            let mut pages = 0_u32;
2849            let mut entries_visited = 0_u64;
2850            loop {
2851                let page = session
2852                    .execute_trusted_live_page(query, continuation.as_deref())
2853                    .unwrap_or_else(|error| {
2854                        panic!("{strategy} DISTINCT page should execute: {error:?}")
2855                    });
2856                pages = pages.saturating_add(1);
2857                entries_visited = entries_visited.saturating_add(page.work.entries_visited);
2858                assert_eq!(page.row_count as usize, page.rows.len());
2859                assert_eq!(page.work.result_rows, page.row_count);
2860                rows.extend(page.rows);
2861                let Some(cursor) = page.continuation else {
2862                    break;
2863                };
2864                assert!(
2865                    cursors.insert(cursor.clone()),
2866                    "{strategy} DISTINCT continuation must advance monotonically",
2867                );
2868                continuation = Some(cursor);
2869                assert!(pages < 8, "{strategy} DISTINCT traversal must terminate");
2870            }
2871
2872            (rows, pages, entries_visited)
2873        };
2874
2875        let expected = vec![
2876            vec![OutputValue::Null],
2877            vec![OutputValue::Nat64(1)],
2878            vec![OutputValue::Nat64(2)],
2879            vec![OutputValue::Nat64(3)],
2880        ];
2881        let (adjacent_rows, adjacent_pages, adjacent_entries) = traverse(&adjacent, "adjacent");
2882        let (global_rows, global_pages, global_entries) = traverse(&global, "global");
2883
2884        assert_eq!(adjacent_rows, expected);
2885        assert_eq!(global_rows, expected);
2886        assert_eq!(adjacent_pages, 2);
2887        assert_eq!(global_pages, 2);
2888        assert!(adjacent_entries > 0);
2889        assert!(global_entries > 0);
2890    }
2891
2892    #[test]
2893    fn selective_live_pages_publish_monotonic_empty_physical_progress() {
2894        let session = initialize();
2895        session
2896            .execute_trusted_dynamic_mutation_batch(
2897                (1..=9)
2898                    .map(|id| {
2899                        let parent = match id {
2900                            1 => Some(2),
2901                            9 => Some(1),
2902                            _ => None,
2903                        };
2904                        insert(id, parent)
2905                    })
2906                    .collect(),
2907            )
2908            .expect("selective live-page rows should insert");
2909        let query = DynamicQuery::new(ENTITY_NAME)
2910            .select(["id"])
2911            .filter(FilterExpr::eq("parent_id", 1_u64))
2912            .order_by(asc("id"))
2913            .limit(1);
2914
2915        let first = session
2916            .execute_trusted_live_page(&query, None)
2917            .expect("first selective page should stop with physical progress");
2918        assert!(first.rows.is_empty());
2919        assert_eq!(first.work.entries_visited, 4);
2920        let first_cursor = first
2921            .continuation
2922            .expect("filtered physical progress must return a continuation");
2923
2924        let second = session
2925            .execute_trusted_live_page(&query, Some(first_cursor.as_str()))
2926            .expect("second selective page should resume after the first physical frontier");
2927        assert!(second.rows.is_empty());
2928        assert_eq!(second.work.entries_visited, 4);
2929        let second_cursor = second
2930            .continuation
2931            .expect("second filtered frontier must remain resumable");
2932        assert_ne!(second_cursor, first_cursor);
2933
2934        let third = session
2935            .execute_trusted_live_page(&query, Some(second_cursor.as_str()))
2936            .expect("final selective page should return the late match");
2937        assert_eq!(third.rows, vec![vec![OutputValue::Nat64(9)]]);
2938        assert_eq!(third.work.entries_visited, 1);
2939        assert_eq!(third.continuation, None);
2940
2941        let descending = DynamicQuery::new(ENTITY_NAME)
2942            .select(["id"])
2943            .filter(FilterExpr::eq("parent_id", 2_u64))
2944            .order_by(desc("id"))
2945            .limit(1);
2946        let descending_first = session
2947            .execute_trusted_live_page(&descending, None)
2948            .expect("descending selective page should stop with physical progress");
2949        assert!(descending_first.rows.is_empty());
2950        let descending_first_cursor = descending_first
2951            .continuation
2952            .expect("descending filtered progress must return a continuation");
2953        let descending_second = session
2954            .execute_trusted_live_page(&descending, Some(descending_first_cursor.as_str()))
2955            .expect("descending progress should resume after its physical frontier");
2956        assert!(descending_second.rows.is_empty());
2957        let descending_second_cursor = descending_second
2958            .continuation
2959            .expect("descending second frontier must remain resumable");
2960        assert_ne!(descending_second_cursor, descending_first_cursor);
2961        let descending_third = session
2962            .execute_trusted_live_page(&descending, Some(descending_second_cursor.as_str()))
2963            .expect("descending final page should return the late match");
2964        assert_eq!(descending_third.rows, vec![vec![OutputValue::Nat64(1)]]);
2965        assert_eq!(descending_third.continuation, None);
2966    }
2967
2968    #[test]
2969    fn accepted_relation_edges_drive_catalog_and_describe_introspection() {
2970        let session = initialize();
2971        let entities = session
2972            .show_entities()
2973            .expect("accepted entity catalog should resolve");
2974        let source = entities
2975            .iter()
2976            .find(|entity| entity.entity_name() == ENTITY_NAME)
2977            .expect("relation source should be listed");
2978        assert_eq!(source.relations(), 1);
2979
2980        let description = session
2981            .try_describe_entity_by_name(ENTITY_NAME)
2982            .expect("accepted relation source should describe");
2983        let [relation] = description.relations() else {
2984            panic!("accepted relation edge should produce one relation row");
2985        };
2986        assert_eq!(relation.field(), "parent_id");
2987        assert_eq!(relation.target_path(), ENTITY_SOURCE);
2988        assert_eq!(relation.target_entity_name(), ENTITY_NAME);
2989        assert_eq!(relation.target_store_path(), STORE_PATH);
2990        assert_eq!(
2991            relation.cardinality(),
2992            crate::db::EntityRelationCardinality::Single,
2993        );
2994    }
2995
2996    #[test]
2997    fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
2998        let session = initialize();
2999        session
3000            .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
3001            .expect("the initial relation should commit");
3002
3003        let blocked = session
3004            .execute_trusted_dynamic_mutation(&delete(1))
3005            .expect_err("an unaffected committed source must block target deletion");
3006        assert_relation_violation(&blocked);
3007
3008        let deleted = session
3009            .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
3010            .expect("a source and its target should delete atomically");
3011        assert_eq!(
3012            deleted.rows,
3013            vec![expected_row(2, Some(1)), expected_row(1, None)],
3014        );
3015
3016        session
3017            .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
3018            .expect("the update-away fixture should commit");
3019        let updated_away = session
3020            .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
3021            .expect("an updated final source may release a deleted target");
3022        assert_eq!(
3023            updated_away.rows,
3024            vec![expected_row(4, None), expected_row(3, None)],
3025        );
3026
3027        session
3028            .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
3029            .expect("the retained-reference fixture should commit");
3030        let retained = session
3031            .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
3032            .expect_err("a final updated source must still block target deletion");
3033        assert_relation_violation(&retained);
3034
3035        session
3036            .execute_trusted_dynamic_mutation(&insert(7, None))
3037            .expect("the inserted-reference fixture target should commit");
3038        let inserted_reference = session
3039            .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
3040            .expect_err("a final inserted source must not reference a deleted target");
3041        assert_relation_violation(&inserted_reference);
3042
3043        let inserted_target = session
3044            .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
3045            .expect("an inserted relation should see its batch-final target");
3046        assert_eq!(
3047            inserted_target.rows,
3048            vec![expected_row(10, Some(9)), expected_row(9, None)],
3049        );
3050
3051        session
3052            .execute_trusted_dynamic_mutation(&insert(11, None))
3053            .expect("the updated-reference fixture source should commit");
3054        let updated_target = session
3055            .execute_trusted_dynamic_mutation_batch(vec![
3056                update_parent(11, Some(12)),
3057                insert(12, None),
3058            ])
3059            .expect("an updated relation should see its batch-final target");
3060        assert_eq!(
3061            updated_target.rows,
3062            vec![expected_row(11, Some(12)), expected_row(12, None)],
3063        );
3064    }
3065
3066    #[test]
3067    fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
3068        let session = initialize();
3069        session
3070            .execute_trusted_dynamic_mutation(&insert(1, None))
3071            .expect("the primary mixed fixture row should commit");
3072        session
3073            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3074                entity: OTHER_ENTITY_NAME.to_string(),
3075                patch: other_patch(Some(1), 10),
3076            })
3077            .expect("the secondary mixed fixture row should commit");
3078
3079        let mixed_entity = session
3080            .execute_trusted_dynamic_mutation_batch(vec![
3081                update_code(1, 11),
3082                DynamicMutation::Update {
3083                    entity: OTHER_ENTITY_NAME.to_string(),
3084                    key: InputValue::Nat64(1),
3085                    patch: other_patch(None, 11),
3086                },
3087            ])
3088            .expect_err("one atomic batch must not cross accepted entities");
3089        assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
3090        assert_eq!(
3091            mixed_entity.diagnostic_facts(),
3092            vec![
3093                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
3094                (
3095                    icydb_diagnostic_code::DiagnosticFactTag::ExpectedEntityTag,
3096                    ENTITY_TAG.value(),
3097                ),
3098                (
3099                    icydb_diagnostic_code::DiagnosticFactTag::ActualEntityTag,
3100                    OTHER_ENTITY_TAG.value(),
3101                ),
3102            ],
3103        );
3104
3105        let missing = session
3106            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
3107            .expect_err("a late missing delete must reject the earlier staged update");
3108        assert_eq!(missing.class(), ErrorClass::NotFound);
3109
3110        session
3111            .execute_trusted_dynamic_mutation(&insert(2, None))
3112            .expect("the collision fixture should commit");
3113        let collision = session
3114            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
3115            .expect_err("an insert collision must reject the earlier staged update");
3116        assert_eq!(collision.class(), ErrorClass::Conflict);
3117        let failures_unchanged = session
3118            .execute_trusted_dynamic_mutation(&update_code(1, 1))
3119            .expect("failed batches must preserve the original unique value");
3120        assert_eq!(failures_unchanged.affected_rows, 0);
3121
3122        let replaced = session
3123            .execute_trusted_dynamic_mutation_batch(vec![
3124                update_code(1, 14),
3125                DynamicMutation::Replace {
3126                    entity: ENTITY_NAME.to_string(),
3127                    key: InputValue::Nat64(99),
3128                    patch: patch(None, None, Some(99)),
3129                },
3130            ])
3131            .expect("ordinary caller-key replace should insert its absent final row");
3132        assert_eq!(
3133            replaced.rows,
3134            vec![
3135                expected_row_with_code(1, None, 14),
3136                expected_row_with_code(99, None, 99),
3137            ],
3138        );
3139
3140        let unchanged = session
3141            .execute_trusted_dynamic_mutation(&update_code(1, 14))
3142            .expect("the successful mixed replace must publish its preceding update");
3143        assert_eq!(unchanged.affected_rows, 0);
3144        let other_unchanged = session
3145            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3146                entity: OTHER_ENTITY_NAME.to_string(),
3147                key: InputValue::Nat64(1),
3148                patch: other_patch(None, 10),
3149            })
3150            .expect("cross-entity rejection must preserve the secondary row");
3151        assert_eq!(other_unchanged.affected_rows, 0);
3152    }
3153
3154    #[test]
3155    fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
3156        let session = initialize();
3157        session
3158            .execute_trusted_dynamic_mutation_batch(vec![
3159                insert_with_code(1, None, 10),
3160                insert_with_code(2, None, 20),
3161            ])
3162            .expect("the unique-overlay fixture should commit");
3163
3164        let swapped = session
3165            .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
3166            .expect("two final rows should atomically swap unique memberships");
3167        assert_eq!(
3168            swapped.rows,
3169            vec![
3170                expected_row_with_code(1, None, 20),
3171                expected_row_with_code(2, None, 10),
3172            ],
3173        );
3174
3175        let released = session
3176            .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
3177            .expect("a delete should release unique membership to a final inserted row");
3178        assert_eq!(
3179            released.rows,
3180            vec![
3181                expected_row_with_code(1, None, 20),
3182                expected_row_with_code(3, None, 20),
3183            ],
3184        );
3185    }
3186}
3187
3188#[cfg(test)]
3189mod identity_pre_key_tests {
3190    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3191    use super::DynamicTypedEntityBinding;
3192    use super::{
3193        AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
3194        AcceptedStructuralMutationPacking, AcceptedStructuralMutationStagedAdmission,
3195        AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
3196        DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
3197        DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
3198        MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
3199        MutationProgressRecordOp, add_structural_mutation_staged_bytes,
3200        admit_structural_mutation_staged_charge, checked_pre_key_candidate_count,
3201        insert_key_exists_after_generation, structural_mutation_staged_charge,
3202        validate_structural_mutation_result_bytes,
3203    };
3204    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3205    use crate::db::data::DecodedDataStoreKey;
3206    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3207    use crate::db::executor::budget::{
3208        HardExecutionBudget, HardExecutionContext, HardExecutionFailureHeadroom,
3209        with_query_execution_budget_for_tests,
3210    };
3211    use crate::db::mutation_job::{MutationJobRecord, MutationJobTransition};
3212    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3213    use crate::db::{
3214        CompareProofAndAdvanceError, DynamicQuery, ExhaustiveReadError, RawDataStoreKey,
3215        ReadSetRevisionError, ResumableJobAdvance, ResumableJobAdvanceRequest,
3216        ResumableJobAdvanceStatus, ResumableJobError, ResumableJobId, ResumableJobIdempotencyKey,
3217        ResumableJobStatus, asc,
3218    };
3219    use crate::{
3220        db::{
3221            GeneratedStartupDriverStep, MutationJobAdvanceRequest, MutationJobId,
3222            MutationJobIdempotencyKey, MutationJobPhase, MutationJobStatus,
3223            commit::{
3224                database_incarnation_id, forget_recovered_domain_for_tests,
3225                install_startup_recovery_wakeup,
3226            },
3227            data::DataStore,
3228            drive_generated_startup_recovery_page,
3229            executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
3230            index::{IndexId, IndexKey, IndexKeyKind, IndexStore, IndexStoreVisit},
3231            integrity::{
3232                InsertMutationJobResult, PhysicalUnitCheckpoint, QuickIntegrityStatus,
3233                RowInspectionLimits, execute_quick_integrity, execute_row_integrity_page,
3234                with_mutation_progress_store,
3235            },
3236            journal::{
3237                JournalBatch, JournalRecord, JournalSequence, JournalTailControl, JournalTailStore,
3238                encode_journal_batch,
3239            },
3240            registry::{
3241                StoreAllocationIdentities, StoreAllocationIdentity, StoreHandle, StoreRegistry,
3242                StoreRuntimeStorageCapabilities,
3243            },
3244            schema::{
3245                AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
3246                FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
3247                PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
3248                PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
3249                SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
3250                accepted_schema_candidate_with_field_bindings_for_tests,
3251                cardinality_build::{
3252                    CardinalityBuildAuthority, CardinalityGenerationPageOutcome,
3253                    drive_cardinality_generation_page,
3254                },
3255                cardinality_generation::{CardinalityGenerationHeader, CardinalityGenerationState},
3256            },
3257            write_context::MutationMode,
3258        },
3259        error::{ErrorClass, ErrorOrigin, InternalError},
3260        testing::test_memory,
3261        traits::{CanisterKind, Path},
3262        types::{EntityTag, Timestamp},
3263        value::{InputValue, OutputValue, Value},
3264    };
3265    use icydb_schema::{FieldSourceKey, ScalarType};
3266    use std::{
3267        cell::{Cell, RefCell},
3268        collections::BTreeMap,
3269        time::Instant,
3270    };
3271
3272    const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
3273    const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
3274    const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
3275    const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
3276    const ENTITY_NAME: &str = "IdentityRow";
3277    const ENTITY_TAG: EntityTag = EntityTag::new(93);
3278    const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
3279    const UNRELATED_STORE_PATH: &str = "session::write::identity_pre_key_tests::UnrelatedStore";
3280
3281    struct TestCanister;
3282
3283    impl Path for TestCanister {
3284        const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
3285    }
3286
3287    impl CanisterKind for TestCanister {
3288        const COMMIT_MEMORY_ID: u8 = 45;
3289        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
3290        const STARTUP_MEMORY_ID: u8 = 49;
3291        const STARTUP_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.startup.control.v1";
3292        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
3293        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3294            "icydb.identity_pre_key_tests.integrity.progress.v1";
3295    }
3296
3297    thread_local! {
3298        static STARTUP_WAKEUPS: Cell<u32> = const { Cell::new(0) };
3299        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3300        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3301        static SCHEMA_STORE: RefCell<SchemaStore> =
3302            const { RefCell::new(SchemaStore::init_heap()) };
3303        static UNRELATED_DATA_STORE: RefCell<DataStore> =
3304            const { RefCell::new(DataStore::init_heap()) };
3305        static UNRELATED_INDEX_STORE: RefCell<IndexStore> =
3306            const { RefCell::new(IndexStore::init_heap()) };
3307        static UNRELATED_SCHEMA_STORE: RefCell<SchemaStore> =
3308            const { RefCell::new(SchemaStore::init_heap()) };
3309        static STORE_REGISTRY: StoreRegistry = {
3310            let mut registry = StoreRegistry::new();
3311            registry.register_store(
3312                STORE_PATH,
3313                &DATA_STORE,
3314                &INDEX_STORE,
3315                &SCHEMA_STORE,
3316                StoreAllocationIdentities::absent(),
3317                StoreRuntimeStorageCapabilities::heap(),
3318            ).expect("identity pre-key test store should register");
3319            registry.register_store(
3320                UNRELATED_STORE_PATH,
3321                &UNRELATED_DATA_STORE,
3322                &UNRELATED_INDEX_STORE,
3323                &UNRELATED_SCHEMA_STORE,
3324                StoreAllocationIdentities::absent(),
3325                StoreRuntimeStorageCapabilities::heap(),
3326            ).expect("unrelated identity test store should register");
3327            registry
3328        };
3329        static JOURNALED_DATA_STORE: RefCell<DataStore> =
3330            RefCell::new(DataStore::init_journaled(test_memory(186)));
3331        static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
3332            RefCell::new(IndexStore::init_journaled(test_memory(187)));
3333        static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
3334            RefCell::new(SchemaStore::init_journaled(test_memory(188)));
3335        static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
3336            RefCell::new(JournalTailStore::init(test_memory(189)));
3337        static JOURNALED_STORE_REGISTRY: StoreRegistry = {
3338            let mut registry = StoreRegistry::new();
3339            registry.register_journaled_store(
3340                JOURNALED_STORE_PATH,
3341                &JOURNALED_DATA_STORE,
3342                &JOURNALED_INDEX_STORE,
3343                &JOURNALED_SCHEMA_STORE,
3344                &JOURNALED_TAIL_STORE,
3345                StoreAllocationIdentities::new_journaled(
3346                    StoreAllocationIdentity::new(186, "icydb.test.identity_range.data.v1"),
3347                    StoreAllocationIdentity::new(187, "icydb.test.identity_range.index.v1"),
3348                    StoreAllocationIdentity::new(188, "icydb.test.identity_range.schema.v1"),
3349                    StoreAllocationIdentity::new(189, "icydb.test.identity_range.journal.v1"),
3350                ),
3351                StoreRuntimeStorageCapabilities::journaled(),
3352            ).expect("identity range journaled store should register");
3353            registry
3354        };
3355    }
3356
3357    fn record_startup_wakeup() {
3358        STARTUP_WAKEUPS.with(|wakeups| wakeups.set(wakeups.get().saturating_add(1)));
3359    }
3360
3361    struct JournaledTestCanister;
3362
3363    impl Path for JournaledTestCanister {
3364        const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
3365    }
3366
3367    impl CanisterKind for JournaledTestCanister {
3368        const COMMIT_MEMORY_ID: u8 = 190;
3369        const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
3370        const STARTUP_MEMORY_ID: u8 = 192;
3371        const STARTUP_STABLE_KEY: &'static str = "icydb.identity_range_tests.startup.control.v1";
3372        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
3373        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3374            "icydb.identity_range_tests.integrity.progress.v1";
3375    }
3376
3377    fn source_key(source: &str) -> FieldSourceKey {
3378        FieldSourceKey::try_new(source).expect("identity test field source should admit")
3379    }
3380
3381    fn identity_snapshot(store_path: &str, payload_unique: bool) -> PersistedSchemaSnapshot {
3382        let fields = vec![
3383            PersistedFieldSnapshot::new_initial_with_write_policy(
3384                FieldId::new(1),
3385                "id".to_string(),
3386                SchemaFieldSlot::new(0),
3387                AcceptedFieldKind::Nat64,
3388                Vec::new(),
3389                false,
3390                SchemaInsertDefault::None,
3391                SchemaFieldWritePolicy::from_model_policies(
3392                    Some(FieldInsertGeneration::Identity),
3393                    None,
3394                ),
3395                FieldStorageDecode::ByKind,
3396                LeafCodec::Scalar(ScalarCodec::Nat64),
3397            ),
3398            PersistedFieldSnapshot::new_initial(
3399                FieldId::new(2),
3400                "payload".to_string(),
3401                SchemaFieldSlot::new(1),
3402                AcceptedFieldKind::Nat64,
3403                Vec::new(),
3404                false,
3405                SchemaInsertDefault::None,
3406                FieldStorageDecode::ByKind,
3407                LeafCodec::Scalar(ScalarCodec::Nat64),
3408            ),
3409        ];
3410        PersistedSchemaSnapshot::new_with_indexes(
3411            SchemaVersion::initial(),
3412            ENTITY_SOURCE.to_string(),
3413            ENTITY_NAME.to_string(),
3414            FieldId::new(1),
3415            SchemaRowLayout::initial(
3416                fields
3417                    .iter()
3418                    .map(|field| (field.id(), field.slot()))
3419                    .collect(),
3420            ),
3421            fields,
3422            vec![PersistedIndexSnapshot::new(
3423                SchemaIndexId::new(1).expect("identity test index ID should admit"),
3424                1,
3425                "by_payload".to_string(),
3426                store_path.to_string(),
3427                payload_unique,
3428                PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
3429                    FieldId::new(2),
3430                    SchemaFieldSlot::new(1),
3431                    vec!["payload".to_string()],
3432                    AcceptedFieldKind::Nat64,
3433                    false,
3434                )]),
3435                None,
3436            )],
3437        )
3438    }
3439
3440    fn initialize() -> DbSession<TestCanister> {
3441        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3442        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3443        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3444        UNRELATED_DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3445        UNRELATED_INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3446        UNRELATED_SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3447        let session = DbSession::<TestCanister>::new(
3448            &STORE_REGISTRY,
3449            &crate::db::RequestExecutionRoot::__new_runtime_root(),
3450        );
3451        session
3452            .db
3453            .drive_startup_recovery_page()
3454            .expect("identity pre-key test database should initialize");
3455        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
3456            STORE_PATH,
3457            AcceptedSchemaRevision::INITIAL,
3458            BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH, false))]),
3459            BTreeMap::from([
3460                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
3461                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
3462            ]),
3463        );
3464        let store = session
3465            .db
3466            .store_handle(STORE_PATH)
3467            .expect("identity pre-key test store should resolve");
3468        crate::db::commit::publish_accepted_schema_candidate(
3469            STORE_PATH,
3470            store,
3471            AcceptedSchemaRevision::NONE,
3472            &candidate,
3473        )
3474        .expect("identity candidate should publish with explicit zero state");
3475        session
3476    }
3477
3478    fn initialize_journaled_with_root_and_payload_uniqueness(
3479        payload_unique: bool,
3480    ) -> (
3481        DbSession<JournaledTestCanister>,
3482        crate::db::RequestExecutionRoot,
3483    ) {
3484        let root = crate::db::RequestExecutionRoot::__new_runtime_root();
3485        let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY, &root);
3486        session
3487            .db
3488            .drive_startup_recovery_page()
3489            .expect("journaled identity database should initialize");
3490        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
3491            JOURNALED_STORE_PATH,
3492            AcceptedSchemaRevision::INITIAL,
3493            BTreeMap::from([(
3494                ENTITY_TAG,
3495                identity_snapshot(JOURNALED_STORE_PATH, payload_unique),
3496            )]),
3497            BTreeMap::from([
3498                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
3499                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
3500            ]),
3501        );
3502        let store = session
3503            .db
3504            .store_handle(JOURNALED_STORE_PATH)
3505            .expect("journaled identity store should resolve");
3506        crate::db::commit::publish_accepted_schema_candidate(
3507            JOURNALED_STORE_PATH,
3508            store,
3509            AcceptedSchemaRevision::NONE,
3510            &candidate,
3511        )
3512        .expect("journaled identity candidate should publish");
3513        (session, root)
3514    }
3515
3516    fn initialize_journaled_with_root() -> (
3517        DbSession<JournaledTestCanister>,
3518        crate::db::RequestExecutionRoot,
3519    ) {
3520        initialize_journaled_with_root_and_payload_uniqueness(false)
3521    }
3522
3523    fn initialize_journaled() -> DbSession<JournaledTestCanister> {
3524        initialize_journaled_with_root().0
3525    }
3526
3527    fn initialize_journaled_with_unique_payload() -> DbSession<JournaledTestCanister> {
3528        initialize_journaled_with_root_and_payload_uniqueness(true).0
3529    }
3530
3531    fn drive_journaled_recovery_to_completion(session: &DbSession<JournaledTestCanister>) {
3532        for _ in 0..8 {
3533            if session
3534                .db
3535                .drive_startup_recovery_page()
3536                .expect("dedicated driver recovery should remain valid")
3537            {
3538                return;
3539            }
3540        }
3541        panic!("dedicated driver recovery should quiesce within eight complete batches");
3542    }
3543
3544    fn drive_journaled_cardinality_to_ready(session: &DbSession<JournaledTestCanister>) {
3545        let handle = session
3546            .db
3547            .store_handle(JOURNALED_STORE_PATH)
3548            .expect("journaled cardinality store should resolve");
3549        for _ in 0..8 {
3550            let outcome = handle
3551                .with_data(|data| {
3552                    handle.with_index(|index| {
3553                        handle.with_schema_mut(|schema| {
3554                            drive_cardinality_generation_page(data, index, schema, |schema| {
3555                                let watermark = JOURNALED_TAIL_STORE
3556                                    .with(|tail| tail.borrow().fold_watermark())?;
3557                                CardinalityBuildAuthority::derive(
3558                                    schema,
3559                                    database_incarnation_id()?,
3560                                    handle.allocation_identities(),
3561                                    watermark,
3562                                )
3563                            })
3564                        })
3565                    })
3566                })
3567                .expect("bounded cardinality generation should advance");
3568            if outcome == CardinalityGenerationPageOutcome::Quiescent {
3569                return;
3570            }
3571        }
3572        panic!("cardinality generation should become Ready within eight bounded pages");
3573    }
3574
3575    fn journaled_user_index_prefix() -> (IndexId, Vec<Vec<u8>>) {
3576        JOURNALED_INDEX_STORE.with(|store| {
3577            let mut selected = None;
3578            store
3579                .borrow()
3580                .visit_entries(|raw_key, _value| {
3581                    let key = IndexKey::try_from_raw(raw_key)
3582                        .expect("accepted user index key should decode");
3583                    if key.key_kind() != IndexKeyKind::User {
3584                        return Ok::<_, InternalError>(IndexStoreVisit::Continue);
3585                    }
3586                    let components = (0..key.component_count())
3587                        .map(|index| {
3588                            key.component(index)
3589                                .expect("accepted index component should exist")
3590                                .to_vec()
3591                        })
3592                        .collect::<Vec<_>>();
3593                    selected = Some((*key.index_id(), components));
3594                    Ok(IndexStoreVisit::Stop)
3595                })
3596                .expect("accepted user index should be inspectable");
3597            selected.expect("the cardinality fixture should contain one user index entry")
3598        })
3599    }
3600
3601    fn reset_journaled_cardinality_projections() -> u64 {
3602        JOURNALED_DATA_STORE.with(|store| {
3603            store
3604                .borrow_mut()
3605                .reset_journaled_live_projection()
3606                .expect("row projection should reset without a count scan");
3607        });
3608        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
3609        let fold_watermark = JOURNALED_TAIL_STORE
3610            .with(|store| store.borrow().fold_watermark())
3611            .expect("journal watermark should remain current-form");
3612        JOURNALED_INDEX_STORE.with(|store| {
3613            store
3614                .borrow_mut()
3615                .reset_journaled_live_projection(data_generation, fold_watermark)
3616                .expect("index projection should reset without a count scan");
3617        });
3618        data_generation
3619    }
3620
3621    fn assert_journaled_cardinality(
3622        handle: StoreHandle,
3623        index_id: IndexId,
3624        prefix_components: &[Vec<u8>],
3625        expected: u64,
3626    ) {
3627        assert_eq!(handle.exact_entity_count(ENTITY_TAG), Some(expected));
3628        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
3629        assert_eq!(
3630            handle.exact_user_index_prefix_count(
3631                data_generation,
3632                IndexKeyKind::User,
3633                index_id,
3634                prefix_components,
3635            ),
3636            Some(expected),
3637        );
3638    }
3639
3640    fn mark_journaled_cardinality_building() {
3641        let current = JOURNALED_SCHEMA_STORE.with(|store| {
3642            store
3643                .borrow()
3644                .cardinality_generation_header()
3645                .expect("Ready header should decode")
3646                .expect("Ready header should exist")
3647        });
3648        JOURNALED_SCHEMA_STORE.with(|store| {
3649            store
3650                .borrow_mut()
3651                .write_cardinality_generation_header(CardinalityGenerationHeader::new(
3652                    current.generation(),
3653                    CardinalityGenerationState::Building,
3654                    current.slot(),
3655                    current.source(),
3656                ))
3657                .expect("Building fallback fixture should persist");
3658        });
3659    }
3660
3661    fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
3662        AcceptedMutationIntentPatch::new()
3663            .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
3664    }
3665
3666    fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
3667        DynamicStructuralPatch::new(vec![(
3668            "payload".to_string(),
3669            DynamicWriteCell::Value(InputValue::Nat64(value)),
3670        )])
3671    }
3672
3673    fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
3674        vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
3675    }
3676
3677    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3678    fn exact_key_binding<C: CanisterKind>(session: &DbSession<C>) -> DynamicTypedEntityBinding {
3679        session
3680            .issue_typed_entity_binding(
3681                ENTITY_SOURCE,
3682                &[
3683                    DynamicTypedFieldBindingRequest::new(
3684                        ID_SOURCE.to_string(),
3685                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3686                        false,
3687                    ),
3688                    DynamicTypedFieldBindingRequest::new(
3689                        PAYLOAD_SOURCE.to_string(),
3690                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3691                        false,
3692                    ),
3693                ],
3694            )
3695            .expect("exact-key test binding should issue")
3696    }
3697
3698    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3699    fn insert_exact_key_fixture<C: CanisterKind>(session: &DbSession<C>, payload: u64) -> u64 {
3700        let output = session
3701            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3702                entity: ENTITY_NAME.to_string(),
3703                patch: dynamic_payload_patch(payload),
3704            })
3705            .expect("exact-key fixture insert should commit");
3706        match output.rows.as_slice() {
3707            [row] => match row.as_slice() {
3708                [OutputValue::Nat64(id), OutputValue::Nat64(actual_payload)]
3709                    if *actual_payload == payload =>
3710                {
3711                    *id
3712                }
3713                _ => panic!("exact-key fixture should return its identity and payload"),
3714            },
3715            _ => panic!("exact-key fixture insert should return one row"),
3716        }
3717    }
3718
3719    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3720    fn identity_row_stored_bytes<C: CanisterKind>(
3721        session: &DbSession<C>,
3722        store_path: &'static str,
3723        key: u64,
3724    ) -> u64 {
3725        let data_key = DecodedDataStoreKey::try_from_structural_key(ENTITY_TAG, &Value::Nat64(key))
3726            .expect("identity row key should encode");
3727        let raw_key = data_key.to_raw().expect("identity raw key should encode");
3728        let store = session
3729            .db
3730            .recovered_store(store_path)
3731            .expect("identity store should resolve");
3732        store.with_data(|data_store| {
3733            u64::try_from(
3734                data_store
3735                    .get(&raw_key)
3736                    .expect("inserted identity row should exist")
3737                    .len(),
3738            )
3739            .expect("bounded row length should fit u64")
3740        })
3741    }
3742
3743    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3744    fn with_stored_bytes_limit<T>(
3745        limit: u64,
3746        shape_fingerprint_prefix: u64,
3747        operation: impl FnOnce() -> Result<T, crate::db::query::intent::QueryError>,
3748    ) -> Result<T, crate::db::query::intent::QueryError> {
3749        let budget = HardExecutionBudget::uniform_for_tests(
3750            u64::MAX,
3751            HardExecutionFailureHeadroom::new(500, 256),
3752        )
3753        .with_limit_for_tests(
3754            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::StoredBytesRead,
3755            limit,
3756        );
3757        let context = HardExecutionContext::new(
3758            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
3759            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
3760            shape_fingerprint_prefix,
3761        );
3762
3763        with_query_execution_budget_for_tests(budget, context, operation)
3764    }
3765
3766    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3767    fn assert_exact_key_batch<C: CanisterKind>(session: &DbSession<C>) {
3768        let first = insert_exact_key_fixture(session, 41);
3769        let second = insert_exact_key_fixture(session, 42);
3770        let missing = u64::MAX;
3771        let binding = exact_key_binding(session);
3772        let gets_before = DataStore::current_get_call_count();
3773        let result = session
3774            .execute_public_exact_key_batch_for_typed_binding(
3775                &binding,
3776                &[second, missing, first, second],
3777            )
3778            .expect("exact-key batch should execute")
3779            .expect("exact-key binding should remain current");
3780
3781        assert_eq!(result.positions, vec![0, 1, 2, 0]);
3782        assert_eq!(
3783            result.distinct_rows,
3784            vec![
3785                Some(expected_dynamic_row(second, 42)),
3786                None,
3787                Some(expected_dynamic_row(first, 41)),
3788            ],
3789        );
3790        assert_eq!(
3791            DataStore::current_get_call_count().saturating_sub(gets_before),
3792            3,
3793            "four input positions with one duplicate must perform three physical reads",
3794        );
3795    }
3796
3797    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3798    #[test]
3799    fn exact_key_batches_preserve_semantics_across_heap_and_journaled_stores() {
3800        assert_exact_key_batch(&initialize());
3801        assert_exact_key_batch(&initialize_journaled());
3802    }
3803
3804    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3805    fn assert_primary_range_materialization_fetches_once<C: CanisterKind>(
3806        session: &DbSession<C>,
3807        store_path: &'static str,
3808    ) {
3809        let key = insert_exact_key_fixture(session, 41);
3810        let stored_bytes = identity_row_stored_bytes(session, store_path, key);
3811
3812        let scalar = DynamicQuery::new(ENTITY_NAME)
3813            .select(["id", "payload"])
3814            .order_by(asc("id"))
3815            .limit(1);
3816        let gets_before = DataStore::current_get_call_count();
3817        let scalar_page = with_stored_bytes_limit(stored_bytes, 0x7072_696d_6172_792d, || {
3818            session.execute_trusted_live_page(&scalar, None)
3819        })
3820        .expect("one scalar primary-range row should fit one payload-read allowance");
3821        assert_eq!(scalar_page.row_count, 1);
3822        assert_eq!(
3823            DataStore::current_get_call_count().saturating_sub(gets_before),
3824            1,
3825            "scalar primary traversal should fetch its emitted row exactly once",
3826        );
3827
3828        let grouped = DynamicQuery::new(ENTITY_NAME)
3829            .group_by("payload")
3830            .aggregate(crate::db::count())
3831            .grouped_limits(10, 16 * 1_024)
3832            .limit(1);
3833        let gets_before = DataStore::current_get_call_count();
3834        let grouped_page = with_stored_bytes_limit(stored_bytes, 0x6772_6f75_7065_642d, || {
3835            session.execute_trusted_dynamic_grouped_query(&grouped)
3836        })
3837        .expect("one grouped primary-range row should fit one payload-read allowance");
3838        assert_eq!(grouped_page.row_count, 1);
3839        assert_eq!(
3840            DataStore::current_get_call_count().saturating_sub(gets_before),
3841            1,
3842            "grouped primary traversal should fetch its source row exactly once",
3843        );
3844    }
3845
3846    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3847    #[test]
3848    fn row_materialization_fetches_each_required_payload_at_most_once() {
3849        assert_primary_range_materialization_fetches_once(&initialize(), STORE_PATH);
3850        assert_primary_range_materialization_fetches_once(
3851            &initialize_journaled(),
3852            JOURNALED_STORE_PATH,
3853        );
3854    }
3855
3856    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3857    #[test]
3858    fn ordered_grouped_pages_close_a_group_spanning_physical_refills_before_resume() {
3859        let session = initialize();
3860        let mut patches = Vec::new();
3861        for _ in 0..70 {
3862            patches.push(dynamic_payload_patch(10));
3863        }
3864        for _ in 0..3 {
3865            patches.push(dynamic_payload_patch(20));
3866        }
3867        patches.push(dynamic_payload_patch(30));
3868        let inserted = session
3869            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, patches)
3870            .expect("ordered grouped continuation rows should insert");
3871        assert_eq!(inserted.rows.len(), 74);
3872
3873        let query = DynamicQuery::new(ENTITY_NAME)
3874            .group_by("payload")
3875            .aggregate(crate::db::count())
3876            .aggregate(crate::db::sum("id"))
3877            .order_by(asc("payload"))
3878            .grouped_limits(4, 16 * 1_024)
3879            .limit(1);
3880        let expected = [
3881            (10_u64, 70_u64, crate::types::Decimal::new(2_485, 0)),
3882            (20, 3, crate::types::Decimal::new(216, 0)),
3883            (30, 1, crate::types::Decimal::new(74, 0)),
3884        ];
3885        let mut continuation: Option<String> = None;
3886        let mut seen_cursors = std::collections::BTreeSet::new();
3887
3888        for (page_index, (group_key, row_count, id_sum)) in expected.into_iter().enumerate() {
3889            let request = continuation.as_ref().map_or_else(
3890                || query.clone(),
3891                |cursor| query.clone().cursor(cursor.clone()),
3892            );
3893            let entries_before = IndexStore::current_entry_read_count();
3894            let rows_before = DataStore::current_get_call_count();
3895            let page = session
3896                .execute_trusted_dynamic_grouped_query(&request)
3897                .unwrap_or_else(|error| {
3898                    panic!("ordered grouped page {page_index} should execute: {error:?}")
3899                });
3900            let entries_read =
3901                IndexStore::current_entry_read_count().saturating_sub(entries_before);
3902            let rows_read = DataStore::current_get_call_count().saturating_sub(rows_before);
3903
3904            assert_eq!(page.row_count, 1);
3905            let [row] = page.rows.as_slice() else {
3906                panic!("ordered grouped page must contain exactly one closed group")
3907            };
3908            assert_eq!(row.group_key(), &[OutputValue::Nat64(group_key)]);
3909            assert_eq!(
3910                row.aggregate_values(),
3911                &[OutputValue::Nat64(row_count), OutputValue::Decimal(id_sum),],
3912            );
3913            if page_index == 0 {
3914                assert!(
3915                    entries_read.saturating_add(rows_read) >= 70,
3916                    "the first closed group must span the maintained 64-entry physical refill",
3917                );
3918            }
3919
3920            continuation = page.next_cursor;
3921            if page_index + 1 < expected.len() {
3922                let cursor = continuation
3923                    .as_ref()
3924                    .expect("another closed group should retain continuation");
3925                assert!(
3926                    seen_cursors.insert(cursor.clone()),
3927                    "ordered grouped continuation must advance monotonically",
3928                );
3929            } else {
3930                assert_eq!(continuation, None);
3931            }
3932        }
3933    }
3934
3935    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3936    #[test]
3937    fn exhaustive_pages_require_and_recompare_the_complete_source_proof() {
3938        let session = initialize();
3939        let first = insert_exact_key_fixture(&session, 41);
3940        let second = insert_exact_key_fixture(&session, 42);
3941        let third = insert_exact_key_fixture(&session, 43);
3942        let query = DynamicQuery::new(ENTITY_NAME)
3943            .select(["id", "payload"])
3944            .order_by(asc("id"));
3945
3946        let page = session
3947            .execute_trusted_exhaustive_page(&query, None, None)
3948            .expect("initial exhaustive page should capture its source proof");
3949        assert_eq!(
3950            page.rows,
3951            vec![
3952                expected_dynamic_row(first, 41),
3953                expected_dynamic_row(second, 42),
3954            ],
3955        );
3956        let continuation = page
3957            .continuation
3958            .as_deref()
3959            .expect("unreturned row should retain exhaustive continuation");
3960        assert!(matches!(
3961            session.execute_trusted_exhaustive_page(&query, Some(continuation), None),
3962            Err(ExhaustiveReadError::Revision(
3963                ReadSetRevisionError::ResumeProofRequired
3964            )),
3965        ));
3966        let resumed = session
3967            .execute_trusted_exhaustive_page(&query, Some(continuation), Some(&page.proof))
3968            .expect("unchanged proof should resume exhaustive traversal");
3969        assert_eq!(resumed.rows, vec![expected_dynamic_row(third, 43)]);
3970        assert_eq!(resumed.continuation, None);
3971
3972        let stale_page = session
3973            .execute_trusted_exhaustive_page(&query, None, None)
3974            .expect("fresh exhaustive page should capture current revision");
3975        let stale_continuation = stale_page
3976            .continuation
3977            .as_deref()
3978            .expect("fresh three-row traversal should retain continuation");
3979        let _ = insert_exact_key_fixture(&session, 44);
3980        assert!(matches!(
3981            session.execute_trusted_exhaustive_page(
3982                &query,
3983                Some(stale_continuation),
3984                Some(&stale_page.proof),
3985            ),
3986            Err(ExhaustiveReadError::Revision(
3987                ReadSetRevisionError::StoreDataChanged { .. }
3988            )),
3989        ));
3990    }
3991
3992    #[cfg(all(feature = "sql", feature = "diagnostics"))]
3993    #[test]
3994    fn heap_sources_cannot_back_durable_resumable_jobs() {
3995        let session = initialize();
3996        let proof = session
3997            .capture_read_set_revision_proof(&[ENTITY_NAME])
3998            .expect("heap source proof should capture for one-call exhaustive reads");
3999        let job_id = ResumableJobId::try_from_bytes([70; 32])
4000            .expect("nonzero heap test job identity should admit");
4001
4002        assert!(matches!(
4003            session.start_resumable_job(job_id, proof, Vec::new()),
4004            Err(ResumableJobError::SourceProof(
4005                ReadSetRevisionError::DurableStoreRequired { .. }
4006            )),
4007        ));
4008    }
4009
4010    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4011    #[test]
4012    fn proof_and_progress_controls_charge_one_shared_request_scope() {
4013        let (session, root) = initialize_journaled_with_root();
4014        let resource = icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions;
4015        let before = root.observed(resource);
4016        let proof = session
4017            .capture_read_set_revision_proof(&[ENTITY_NAME])
4018            .expect("proof capture should use the retained request scope");
4019        let job_id = ResumableJobId::try_from_bytes([75; 32])
4020            .expect("nonzero accounting job identity should admit");
4021        session
4022            .start_resumable_job(job_id, proof, Vec::new())
4023            .expect("job start should use the same retained request scope");
4024        let _ = session
4025            .resumable_job_state(job_id)
4026            .expect("job load should use the same retained request scope");
4027
4028        assert_eq!(root.observed(resource).saturating_sub(before), 3);
4029    }
4030
4031    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4032    #[test]
4033    fn source_proofs_ignore_unrelated_stores_but_bind_access_state_changes() {
4034        let session = initialize();
4035        let proof = session
4036            .capture_read_set_revision_proof(&[ENTITY_NAME])
4037            .expect("source proof should cover only the entity's physical store");
4038        let shared_store_proof = session
4039            .capture_read_set_revision_proof(&[ENTITY_NAME, ENTITY_NAME])
4040            .expect("entities sharing one physical source should deduplicate");
4041        assert_eq!(shared_store_proof, proof);
4042        assert_eq!(shared_store_proof.stores().len(), 1);
4043        let unrelated = session
4044            .db
4045            .store_handle(UNRELATED_STORE_PATH)
4046            .expect("unrelated registered store should resolve");
4047        unrelated.with_data_mut(|store| {
4048            let _ = store.remove(&RawDataStoreKey::from_persisted_bytes(vec![1]));
4049        });
4050        session
4051            .verify_read_set_revision_proof(&proof)
4052            .expect("a nonparticipating store mutation must not invalidate the proof");
4053
4054        let source = session
4055            .db
4056            .store_handle(STORE_PATH)
4057            .expect("participating source store should resolve");
4058        source
4059            .mark_index_building()
4060            .expect("source access-state transition should advance its revision");
4061        assert!(matches!(
4062            session.verify_read_set_revision_proof(&proof),
4063            Err(ExhaustiveReadError::Revision(
4064                ReadSetRevisionError::StoreAccessChanged { .. }
4065            )),
4066        ));
4067    }
4068
4069    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4070    #[expect(
4071        clippy::too_many_lines,
4072        reason = "one lifecycle test proves successful replay plus pre-page and post-page source invalidation without sharing progress state across tests"
4073    )]
4074    #[test]
4075    fn journaled_job_advance_is_idempotent_and_revision_checked_on_both_sides() {
4076        let session = initialize_journaled();
4077        let proof = session
4078            .capture_read_set_revision_proof(&[ENTITY_NAME])
4079            .expect("journaled source proof should capture");
4080        let job_id =
4081            ResumableJobId::try_from_bytes([71; 32]).expect("nonzero job identity should admit");
4082        session
4083            .start_resumable_job(job_id, proof, vec![0])
4084            .expect("journaled job should start outside its protected source revision");
4085        let request = ResumableJobAdvanceRequest::new(
4086            job_id,
4087            0,
4088            ResumableJobIdempotencyKey::new("page-0")
4089                .expect("bounded idempotency key should admit"),
4090        );
4091        let calls = Cell::new(0_u8);
4092        let receipt = session
4093            .compare_proof_and_advance(&request, |state| {
4094                calls.set(calls.get() + 1);
4095                assert_eq!(state.application_state, vec![0]);
4096                Ok::<_, ()>(
4097                    ResumableJobAdvance::new(Some("cursor-1".to_string()), vec![1], vec![9])
4098                        .expect("bounded application advance should admit"),
4099                )
4100            })
4101            .expect("unchanged source should advance exactly once");
4102        assert_eq!(calls.get(), 1);
4103        assert_eq!(receipt.status, ResumableJobAdvanceStatus::Advanced);
4104        assert_eq!(receipt.committed_sequence, 1);
4105
4106        let replay = session
4107            .compare_proof_and_advance::<()>(&request, |_| {
4108                panic!("lost-response replay must not execute application work")
4109            })
4110            .expect("same request identity should return its persisted receipt");
4111        assert_eq!(replay, receipt);
4112        let retained = session
4113            .resumable_job_state(job_id)
4114            .expect("advanced state should remain durable");
4115        assert_eq!(retained.sequence, 1);
4116        assert_eq!(retained.application_state, vec![1]);
4117
4118        let _ = insert_exact_key_fixture(&session, 51);
4119        let pre_change_request = ResumableJobAdvanceRequest::new(
4120            job_id,
4121            1,
4122            ResumableJobIdempotencyKey::new("page-1")
4123                .expect("bounded idempotency key should admit"),
4124        );
4125        let pre_change_calls = Cell::new(0_u8);
4126        let invalidated = session
4127            .compare_proof_and_advance::<()>(&pre_change_request, |_| {
4128                pre_change_calls.set(pre_change_calls.get() + 1);
4129                unreachable!("pre-page proof failure must reject before application work")
4130            })
4131            .expect("source drift should persist one replayable invalidation receipt");
4132        assert_eq!(pre_change_calls.get(), 0);
4133        assert_eq!(invalidated.status, ResumableJobAdvanceStatus::Invalidated);
4134        let invalidated_state = session
4135            .resumable_job_state(job_id)
4136            .expect("invalidated job should remain inspectable");
4137        assert_eq!(invalidated_state.status, ResumableJobStatus::Invalidated);
4138        assert_eq!(invalidated_state.continuation, None);
4139        assert_eq!(invalidated_state.application_state, vec![1]);
4140        assert_eq!(
4141            session
4142                .compare_proof_and_advance::<()>(&pre_change_request, |_| {
4143                    panic!("invalidation replay must not execute application work")
4144                })
4145                .expect("lost invalidation reply should replay exactly"),
4146            invalidated,
4147        );
4148
4149        let post_proof = session
4150            .capture_read_set_revision_proof(&[ENTITY_NAME])
4151            .expect("post-change journaled proof should capture");
4152        let post_job_id = ResumableJobId::try_from_bytes([72; 32])
4153            .expect("nonzero post-change job identity should admit");
4154        session
4155            .start_resumable_job(post_job_id, post_proof, vec![7])
4156            .expect("post-change journaled job should start");
4157        let post_request = ResumableJobAdvanceRequest::new(
4158            post_job_id,
4159            0,
4160            ResumableJobIdempotencyKey::new("post-page-0")
4161                .expect("bounded idempotency key should admit"),
4162        );
4163        let post_receipt = session
4164            .compare_proof_and_advance::<()>(&post_request, |_| {
4165                let _ = insert_exact_key_fixture(&session, 52);
4166                Ok(ResumableJobAdvance::new(None, vec![8], vec![10])
4167                    .expect("bounded post-change candidate should admit"))
4168            })
4169            .expect("post-page drift should discard the candidate and persist invalidation");
4170        assert_eq!(post_receipt.status, ResumableJobAdvanceStatus::Invalidated);
4171        let post_state = session
4172            .resumable_job_state(post_job_id)
4173            .expect("post-page invalidation should remain inspectable");
4174        assert_eq!(post_state.status, ResumableJobStatus::Invalidated);
4175        assert_eq!(post_state.application_state, vec![7]);
4176        session
4177            .acknowledge_resumable_job(post_job_id, post_state.sequence)
4178            .expect("terminal job acknowledgement should remove retained progress");
4179        session
4180            .acknowledge_resumable_job(post_job_id, post_state.sequence)
4181            .expect("lost acknowledgement reply should be safely replayable");
4182        assert_eq!(
4183            session.resumable_job_state(post_job_id),
4184            Err(ResumableJobError::NotFound),
4185        );
4186
4187        let completed_job_id = ResumableJobId::try_from_bytes([74; 32])
4188            .expect("nonzero completed job identity should admit");
4189        let completed_proof = session
4190            .capture_read_set_revision_proof(&[ENTITY_NAME])
4191            .expect("completed-job source proof should capture");
4192        session
4193            .start_resumable_job(completed_job_id, completed_proof, Vec::new())
4194            .expect("completed-job fixture should start");
4195        let completed_request = ResumableJobAdvanceRequest::new(
4196            completed_job_id,
4197            0,
4198            ResumableJobIdempotencyKey::new("complete")
4199                .expect("bounded completion key should admit"),
4200        );
4201        let completed_receipt = session
4202            .compare_proof_and_advance::<()>(&completed_request, |_| {
4203                Ok(ResumableJobAdvance::new(None, vec![99], vec![100])
4204                    .expect("bounded terminal advance should admit"))
4205            })
4206            .expect("null continuation should commit terminal completion");
4207        let completed_state = session
4208            .resumable_job_state(completed_job_id)
4209            .expect("completed state should remain replayable before acknowledgement");
4210        assert_eq!(completed_state.status, ResumableJobStatus::Completed);
4211        assert_eq!(
4212            session
4213                .compare_proof_and_advance::<()>(&completed_request, |_| {
4214                    panic!("completed request replay must not execute application work")
4215                })
4216                .expect("completed request should replay until acknowledgement"),
4217            completed_receipt,
4218        );
4219        let after_completion = ResumableJobAdvanceRequest::new(
4220            completed_job_id,
4221            1,
4222            ResumableJobIdempotencyKey::new("after-complete")
4223                .expect("bounded post-completion key should admit"),
4224        );
4225        assert!(matches!(
4226            session.compare_proof_and_advance::<()>(&after_completion, |_| {
4227                panic!("completed jobs cannot execute another page")
4228            }),
4229            Err(CompareProofAndAdvanceError::Protocol(
4230                ResumableJobError::Completed
4231            )),
4232        ));
4233        session
4234            .acknowledge_resumable_job(completed_job_id, completed_state.sequence)
4235            .expect("completed job should acknowledge and free capacity");
4236        session
4237            .acknowledge_resumable_job(completed_job_id, completed_state.sequence)
4238            .expect("completion acknowledgement should be idempotent");
4239
4240        let stale_job_id = ResumableJobId::try_from_bytes([73; 32])
4241            .expect("nonzero stale-sequence job identity should admit");
4242        let stale_proof = session
4243            .capture_read_set_revision_proof(&[ENTITY_NAME])
4244            .expect("stale-sequence source proof should capture");
4245        session
4246            .start_resumable_job(stale_job_id, stale_proof, Vec::new())
4247            .expect("stale-sequence job should start");
4248        let stale_request = ResumableJobAdvanceRequest::new(
4249            stale_job_id,
4250            4,
4251            ResumableJobIdempotencyKey::new("stale").expect("bounded idempotency key should admit"),
4252        );
4253        assert!(matches!(
4254            session.compare_proof_and_advance::<()>(&stale_request, |_| {
4255                panic!("stale sequence must reject before application work")
4256            }),
4257            Err(CompareProofAndAdvanceError::Protocol(
4258                ResumableJobError::StaleSequence {
4259                    expected: 4,
4260                    actual: 0,
4261                }
4262            )),
4263        ));
4264        assert_eq!(
4265            session.acknowledge_resumable_job(stale_job_id, 0),
4266            Err(ResumableJobError::NotTerminal),
4267        );
4268    }
4269
4270    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4271    #[test]
4272    fn exact_key_batch_uses_typed_hard_execution_budget() {
4273        let session = initialize();
4274        let binding = exact_key_binding(&session);
4275        let budget =
4276            HardExecutionBudget::uniform_for_tests(0, HardExecutionFailureHeadroom::new(500, 256));
4277        let error = session
4278            .execute_exact_key_batch_with_hard_budget_for_tests(&binding, &[u64::MAX], &budget)
4279            .expect_err("zero query budget should reject the exact-key route");
4280
4281        assert!(matches!(
4282            error.diagnostic().detail(),
4283            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4284                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
4285            })
4286        ));
4287        let facts = error.diagnostic_facts();
4288        assert_eq!(
4289            &facts[..5],
4290            &[
4291                (
4292                    icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
4293                    icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions.raw(),
4294                ),
4295                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 0),
4296                (icydb_diagnostic_code::DiagnosticFactTag::Actual, 1),
4297                (
4298                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionBudgetScope,
4299                    icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution.raw(),
4300                ),
4301                (
4302                    icydb_diagnostic_code::DiagnosticFactTag::ExecutionLane,
4303                    icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead.raw(),
4304                ),
4305            ],
4306        );
4307        assert_eq!(
4308            facts[5].0,
4309            icydb_diagnostic_code::DiagnosticFactTag::QueryShapeFingerprintPrefix,
4310        );
4311        assert_ne!(facts[5].1, 0);
4312    }
4313
4314    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4315    fn assert_planned_query_exhausts(
4316        session: &DbSession<TestCanister>,
4317        query: &crate::db::DynamicQuery,
4318        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
4319    ) {
4320        let budget = HardExecutionBudget::uniform_for_tests(
4321            u64::MAX,
4322            HardExecutionFailureHeadroom::new(500, 256),
4323        )
4324        .with_limit_for_tests(resource, 0);
4325        let context = HardExecutionContext::new(
4326            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
4327            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
4328            0x7068_7973_6963_616c,
4329        );
4330        let error = with_query_execution_budget_for_tests(budget, context, || {
4331            session.execute_trusted_live_page(query, None)
4332        })
4333        .expect_err("the injected zero resource allowance should reject planned execution");
4334
4335        assert!(matches!(
4336            error.diagnostic().detail(),
4337            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4338                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
4339            })
4340        ));
4341        assert_eq!(
4342            error.diagnostic_facts()[0],
4343            (
4344                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
4345                resource.raw(),
4346            ),
4347        );
4348    }
4349
4350    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4351    fn assert_grouped_query_exhausts(
4352        session: &DbSession<TestCanister>,
4353        query: &crate::db::DynamicQuery,
4354        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
4355    ) {
4356        let budget = HardExecutionBudget::uniform_for_tests(
4357            u64::MAX,
4358            HardExecutionFailureHeadroom::new(500, 256),
4359        )
4360        .with_limit_for_tests(resource, 0);
4361        let context = HardExecutionContext::new(
4362            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
4363            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
4364            0x6772_6f75_7065_642d,
4365        );
4366        let error = with_query_execution_budget_for_tests(budget, context, || {
4367            session.execute_trusted_dynamic_grouped_query(query)
4368        })
4369        .expect_err("the injected zero resource allowance should reject grouped execution");
4370
4371        assert!(matches!(
4372            error.diagnostic().detail(),
4373            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4374                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
4375            })
4376        ));
4377        assert_eq!(
4378            error.diagnostic_facts()[0],
4379            (
4380                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
4381                resource.raw(),
4382            ),
4383        );
4384    }
4385
4386    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4387    fn assert_sql_query_exhausts(
4388        session: &DbSession<TestCanister>,
4389        sql: &str,
4390        resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
4391    ) {
4392        let budget = HardExecutionBudget::uniform_for_tests(
4393            u64::MAX,
4394            HardExecutionFailureHeadroom::new(500, 256),
4395        )
4396        .with_limit_for_tests(resource, 0);
4397        let context = HardExecutionContext::new(
4398            icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
4399            icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
4400            0x7371_6c2d_736f_7274,
4401        );
4402        let error = with_query_execution_budget_for_tests(budget, context, || {
4403            session.execute_trusted_sql_query(sql)
4404        })
4405        .expect_err("the injected zero resource allowance should reject SQL execution");
4406
4407        assert!(matches!(
4408            error.diagnostic().detail(),
4409            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4410                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
4411            })
4412        ));
4413        assert_eq!(
4414            error.diagnostic_facts()[0],
4415            (
4416                icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
4417                resource.raw(),
4418            ),
4419        );
4420    }
4421
4422    #[cfg(all(feature = "sql", feature = "diagnostics"))]
4423    #[test]
4424    fn planned_read_routes_share_physical_resource_accounting() {
4425        let session = initialize();
4426        let first = insert_exact_key_fixture(&session, 41);
4427        insert_exact_key_fixture(&session, 42);
4428
4429        let fallback = crate::db::DynamicQuery::new(ENTITY_NAME)
4430            .filter(crate::db::FieldRef::new("id").eq(first))
4431            .select(["id", "payload"])
4432            .order_by(crate::db::asc("id"))
4433            .limit(1);
4434        assert_eq!(
4435            session
4436                .execute_trusted_live_page(&fallback, None)
4437                .expect("bounded fallback execution should preserve its result")
4438                .row_count,
4439            1,
4440        );
4441        assert_planned_query_exhausts(
4442            &session,
4443            &fallback,
4444            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::RowsVisited,
4445        );
4446
4447        let covering = crate::db::DynamicQuery::new(ENTITY_NAME)
4448            .filter(crate::db::FieldRef::new("payload").eq(41_u64))
4449            .select(["payload"])
4450            .order_by(crate::db::asc("payload"))
4451            .limit(1);
4452        assert_eq!(
4453            session
4454                .execute_trusted_live_page(&covering, None)
4455                .expect("bounded covering execution should preserve its result")
4456                .row_count,
4457            1,
4458        );
4459        assert_planned_query_exhausts(
4460            &session,
4461            &covering,
4462            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
4463        );
4464
4465        let residual = crate::db::DynamicQuery::new(ENTITY_NAME)
4466            .filter(crate::db::FieldRef::new("payload").eq_field("id"))
4467            .select(["id"])
4468            .order_by(crate::db::asc("id"))
4469            .limit(1);
4470        assert_eq!(
4471            session
4472                .execute_trusted_live_page(&residual, None)
4473                .expect("bounded residual execution should preserve its result")
4474                .row_count,
4475            0,
4476        );
4477        assert_planned_query_exhausts(
4478            &session,
4479            &residual,
4480            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
4481        );
4482
4483        assert_planned_query_exhausts(
4484            &session,
4485            &fallback,
4486            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::ResultBytes,
4487        );
4488
4489        let grouped = crate::db::DynamicQuery::new(ENTITY_NAME)
4490            .group_by("payload")
4491            .aggregate(crate::db::count())
4492            .order_by(crate::db::asc("payload"))
4493            .grouped_limits(10, 16 * 1_024)
4494            .limit(1);
4495        let grouped_result = session
4496            .execute_trusted_dynamic_grouped_query(&grouped)
4497            .expect("bounded grouped execution should preserve its result");
4498        assert_eq!(grouped_result.row_count, 1);
4499        assert!(grouped_result.next_cursor.is_some());
4500        assert_grouped_query_exhausts(
4501            &session,
4502            &grouped,
4503            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctEntries,
4504        );
4505        assert_grouped_query_exhausts(
4506            &session,
4507            &grouped,
4508            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::CursorSteps,
4509        );
4510
4511        assert_sql_query_exhausts(
4512            &session,
4513            "SELECT payload, COUNT(*) AS row_count FROM IdentityRow \
4514             GROUP BY payload ORDER BY row_count DESC, payload ASC LIMIT 1",
4515            icydb_diagnostic_code::DiagnosticExecutionBudgetResource::SortEntries,
4516        );
4517    }
4518
4519    fn assert_dynamic_payload<C: CanisterKind>(
4520        session: &DbSession<C>,
4521        key: u64,
4522        expected_payload: u64,
4523    ) {
4524        let unchanged = session
4525            .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
4526                entity: ENTITY_NAME.to_string(),
4527                key: InputValue::Nat64(key),
4528                patch: dynamic_payload_patch(expected_payload),
4529            })
4530            .expect("the expected row should remain readable through a no-op update");
4531        assert_eq!(unchanged.affected_rows, 0);
4532        assert_eq!(
4533            unchanged.rows,
4534            vec![expected_dynamic_row(key, expected_payload)],
4535        );
4536    }
4537
4538    fn assert_exact_batch_backlog_pressure(
4539        pressure: &InternalError,
4540        before: JournalTailControl,
4541        next_sequence: u64,
4542    ) {
4543        assert_eq!(
4544            pressure.diagnostic().error_code(),
4545            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_CONVERGENCE_BACKLOG_PRESSURE,
4546        );
4547        assert_eq!(
4548            pressure.diagnostic_facts(),
4549            vec![
4550                (
4551                    icydb_diagnostic_code::DiagnosticFactTag::BacklogResource,
4552                    icydb_diagnostic_code::DiagnosticBacklogResource::Batches.raw(),
4553                ),
4554                (icydb_diagnostic_code::DiagnosticFactTag::CurrentCount, 38),
4555                (icydb_diagnostic_code::DiagnosticFactTag::ProposedCount, 1),
4556                (icydb_diagnostic_code::DiagnosticFactTag::Limit, 38),
4557            ],
4558        );
4559        assert_eq!(
4560            crate::db::commit::next_database_commit_sequence()
4561                .expect("pressure must leave the database sequence readable"),
4562            next_sequence,
4563        );
4564        assert!(matches!(
4565            crate::db::commit::observe_commit_control()
4566                .expect("pressure must leave commit control observable"),
4567            crate::db::commit::CommitControlObservation::Present {
4568                marker_present: false,
4569                ..
4570            },
4571        ));
4572        assert_eq!(
4573            JOURNALED_TAIL_STORE.with(|tail| {
4574                tail.borrow()
4575                    .current_tail_control()
4576                    .expect("pressure must preserve the exact tail control")
4577            }),
4578            before,
4579        );
4580    }
4581
4582    fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
4583        values
4584            .iter()
4585            .map(|value| {
4586                AcceptedStructuralMutation::save(
4587                    MutationMode::Insert,
4588                    AcceptedStructuralMutationTarget::ResolveFromAfterImage,
4589                    payload_patch(*value),
4590                )
4591            })
4592            .collect()
4593    }
4594
4595    fn atomic_progress_fixture(
4596        identity_byte: u8,
4597    ) -> (
4598        MutationJobRecord,
4599        MutationJobRecord,
4600        MutationProgressRecordOp,
4601    ) {
4602        let job_id = MutationJobId::try_from_bytes([identity_byte; 32])
4603            .expect("nonzero atomic progress job id should admit");
4604        let before = MutationJobRecord::new(job_id, vec![1, identity_byte], vec![2])
4605            .expect("atomic progress predecessor should admit");
4606        let request = MutationJobAdvanceRequest::new(
4607            job_id,
4608            0,
4609            MutationJobIdempotencyKey::new(format!("atomic-{identity_byte}"))
4610                .expect("atomic progress replay key should admit"),
4611        );
4612        let (after, _) = before
4613            .apply_transition(
4614                &request,
4615                MutationJobTransition::new(
4616                    MutationJobStatus::Active,
4617                    MutationJobPhase::Forward,
4618                    vec![3],
4619                    1,
4620                    1,
4621                    0,
4622                ),
4623            )
4624            .expect("atomic progress successor should admit");
4625        let operation = MutationProgressRecordOp::replace(&before, &after)
4626            .expect("atomic progress replacement should admit");
4627        (before, after, operation)
4628    }
4629
4630    fn assert_identity_boundary(error: &InternalError) {
4631        assert_eq!(error.class(), ErrorClass::Unsupported);
4632        assert_eq!(error.origin(), ErrorOrigin::Identity);
4633    }
4634
4635    #[test]
4636    fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
4637        let generated = insert_key_exists_after_generation(true);
4638        assert_eq!(generated.class(), ErrorClass::Corruption);
4639        assert_eq!(generated.origin(), ErrorOrigin::Identity);
4640
4641        let ordinary = insert_key_exists_after_generation(false);
4642        assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
4643    }
4644
4645    #[cfg(target_pointer_width = "64")]
4646    #[test]
4647    fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
4648        let error = checked_pre_key_candidate_count(
4649            usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
4650        )
4651        .expect_err("candidate counts beyond u32 must reject");
4652        assert_identity_boundary(&error);
4653    }
4654
4655    #[test]
4656    #[expect(
4657        clippy::too_many_lines,
4658        reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
4659    )]
4660    fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
4661        let session = initialize();
4662        let seeded = session
4663            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
4664            .expect("seed rows should commit");
4665        assert_eq!(seeded.affected_rows, 1);
4666
4667        let split = session
4668            .execute_trusted_dynamic_mutation_batch(vec![
4669                DynamicMutation::Update {
4670                    entity: ENTITY_NAME.to_string(),
4671                    key: InputValue::Nat64(1),
4672                    patch: dynamic_payload_patch(60),
4673                },
4674                DynamicMutation::Insert {
4675                    entity: ENTITY_NAME.to_string(),
4676                    patch: dynamic_payload_patch(40),
4677                },
4678            ])
4679            .expect("one holding should split atomically");
4680        assert_eq!(split.affected_rows, 2);
4681        assert_eq!(
4682            split.rows,
4683            vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
4684            "split after-images must retain input order and exact quantity",
4685        );
4686
4687        let rejected_split = session
4688            .execute_trusted_dynamic_mutation_batch(vec![
4689                DynamicMutation::Update {
4690                    entity: ENTITY_NAME.to_string(),
4691                    key: InputValue::Nat64(1),
4692                    patch: dynamic_payload_patch(50),
4693                },
4694                DynamicMutation::Insert {
4695                    entity: ENTITY_NAME.to_string(),
4696                    patch: DynamicStructuralPatch::new(Vec::new()),
4697                },
4698            ])
4699            .expect_err("an invalid split output must reject the staged source update");
4700        assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
4701        assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
4702        assert_eq!(
4703            rejected_split.diagnostic_facts(),
4704            vec![
4705                (
4706                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
4707                    ENTITY_TAG.value(),
4708                ),
4709                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 2),
4710                (
4711                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
4712                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
4713                ),
4714                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
4715            ],
4716        );
4717        assert_dynamic_payload(&session, 1, 60);
4718        assert_dynamic_payload(&session, 2, 40);
4719
4720        let transfer = session
4721            .execute_trusted_dynamic_mutation_batch(vec![
4722                DynamicMutation::Update {
4723                    entity: ENTITY_NAME.to_string(),
4724                    key: InputValue::Nat64(1),
4725                    patch: dynamic_payload_patch(70),
4726                },
4727                DynamicMutation::Update {
4728                    entity: ENTITY_NAME.to_string(),
4729                    key: InputValue::Nat64(2),
4730                    patch: dynamic_payload_patch(30),
4731                },
4732            ])
4733            .expect("distinct transfer patches should share one atomic batch");
4734        assert_eq!(
4735            transfer.rows,
4736            vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
4737            "the transfer must preserve the exact total quantity",
4738        );
4739
4740        let merge = session
4741            .execute_trusted_dynamic_mutation_batch(vec![
4742                DynamicMutation::Delete {
4743                    entity: ENTITY_NAME.to_string(),
4744                    key: InputValue::Nat64(2),
4745                },
4746                DynamicMutation::Update {
4747                    entity: ENTITY_NAME.to_string(),
4748                    key: InputValue::Nat64(1),
4749                    patch: dynamic_payload_patch(100),
4750                },
4751            ])
4752            .expect("two holdings should merge atomically");
4753        assert_eq!(
4754            merge.rows,
4755            vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
4756            "delete before-images and update after-images must retain input order",
4757        );
4758
4759        let resplit = session
4760            .execute_trusted_dynamic_mutation_batch(vec![
4761                DynamicMutation::Update {
4762                    entity: ENTITY_NAME.to_string(),
4763                    key: InputValue::Nat64(1),
4764                    patch: dynamic_payload_patch(60),
4765                },
4766                DynamicMutation::Insert {
4767                    entity: ENTITY_NAME.to_string(),
4768                    patch: dynamic_payload_patch(40),
4769                },
4770            ])
4771            .expect("the merged holding should split again");
4772        assert_eq!(
4773            resplit.rows,
4774            vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
4775        );
4776
4777        let rejected_merge = session
4778            .execute_trusted_dynamic_mutation_batch(vec![
4779                DynamicMutation::Delete {
4780                    entity: ENTITY_NAME.to_string(),
4781                    key: InputValue::Nat64(3),
4782                },
4783                DynamicMutation::Update {
4784                    entity: ENTITY_NAME.to_string(),
4785                    key: InputValue::Nat64(99),
4786                    patch: dynamic_payload_patch(100),
4787                },
4788            ])
4789            .expect_err("a late missing merge target must preserve the earlier staged delete");
4790        assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
4791        assert_dynamic_payload(&session, 1, 60);
4792        assert_dynamic_payload(&session, 3, 40);
4793
4794        SCHEMA_STORE.with(|store| {
4795            let cursor = store
4796                .borrow()
4797                .identity_statement_cursor(
4798                    database_incarnation_id().expect("database incarnation should remain readable"),
4799                    ENTITY_TAG,
4800                    FieldId::new(1),
4801                    &AcceptedFieldKind::Nat64,
4802                )
4803                .expect("mixed Identity state should remain readable");
4804            assert_eq!(cursor.expected_high_water(), 3);
4805            assert!(!cursor.has_allocations());
4806        });
4807    }
4808
4809    #[test]
4810    fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
4811        let session = initialize();
4812        session
4813            .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
4814            .expect("the holding fixture should initialize");
4815
4816        let duplicate = session
4817            .execute_trusted_dynamic_mutation_batch(vec![
4818                DynamicMutation::Update {
4819                    entity: ENTITY_NAME.to_string(),
4820                    key: InputValue::Nat64(1),
4821                    patch: dynamic_payload_patch(60),
4822                },
4823                DynamicMutation::Delete {
4824                    entity: ENTITY_NAME.to_string(),
4825                    key: InputValue::Nat64(1),
4826                },
4827            ])
4828            .expect_err("duplicate targets across operation kinds must reject");
4829        assert!(matches!(
4830            duplicate.diagnostic().detail(),
4831            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4832                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
4833            }),
4834        ));
4835        assert_eq!(
4836            duplicate.diagnostic_facts(),
4837            vec![
4838                (
4839                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
4840                    ENTITY_TAG.value(),
4841                ),
4842                (
4843                    icydb_diagnostic_code::DiagnosticFactTag::FirstBatchPosition,
4844                    0,
4845                ),
4846                (
4847                    icydb_diagnostic_code::DiagnosticFactTag::DuplicateBatchPosition,
4848                    1,
4849                ),
4850            ],
4851        );
4852        assert_dynamic_payload(&session, 1, 100);
4853    }
4854
4855    #[test]
4856    fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
4857        let session = initialize();
4858        let empty = session
4859            .execute_trusted_dynamic_mutation_batch(Vec::new())
4860            .expect_err("an empty public batch must reject");
4861        assert!(matches!(
4862            empty.diagnostic().detail(),
4863            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4864                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
4865            }),
4866        ));
4867        assert_eq!(
4868            empty.diagnostic_facts(),
4869            vec![(icydb_diagnostic_code::DiagnosticFactTag::ActualCount, 0,)],
4870        );
4871
4872        let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
4873            .map(|_| DynamicMutation::Delete {
4874                entity: ENTITY_NAME.to_string(),
4875                key: InputValue::Nat64(1),
4876            })
4877            .collect();
4878        let over_bound = session
4879            .execute_trusted_dynamic_mutation_batch(requests)
4880            .expect_err("operation cap plus one must reject before row resolution");
4881        assert!(matches!(
4882            over_bound.diagnostic().detail(),
4883            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4884                boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
4885            }),
4886        ));
4887        assert_eq!(
4888            over_bound.diagnostic_facts(),
4889            vec![
4890                (
4891                    icydb_diagnostic_code::DiagnosticFactTag::ActualCount,
4892                    (MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1) as u64,
4893                ),
4894                (
4895                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
4896                    MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS as u64,
4897                ),
4898            ],
4899        );
4900    }
4901
4902    #[test]
4903    fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
4904        assert_eq!(
4905            structural_mutation_staged_charge([11, 13, 17])
4906                .expect("the writer-owned formula should sum all three row-image components"),
4907            41,
4908        );
4909        let mut exact = 0;
4910        add_structural_mutation_staged_bytes(
4911            &mut exact,
4912            [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
4913        )
4914        .expect("the exact staged-byte boundary should admit");
4915        assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
4916
4917        let error = add_structural_mutation_staged_bytes(&mut exact, [1])
4918            .expect_err("one byte above the staged-byte boundary must reject");
4919        assert!(matches!(
4920            error.diagnostic().detail(),
4921            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4922                boundary:
4923                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
4924            }),
4925        ));
4926        assert_eq!(
4927            error.diagnostic_facts(),
4928            vec![
4929                (
4930                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
4931                    (MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1) as u64,
4932                ),
4933                (
4934                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
4935                    MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES as u64,
4936                ),
4937            ],
4938        );
4939
4940        let mut prefix = 0;
4941        assert_eq!(
4942            admit_structural_mutation_staged_charge(
4943                &mut prefix,
4944                [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
4945                AcceptedStructuralMutationPacking::BoundedPrefix,
4946            )
4947            .expect("the exact prefix boundary should calculate"),
4948            AcceptedStructuralMutationStagedAdmission::Admitted,
4949        );
4950        assert_eq!(prefix, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
4951        assert_eq!(
4952            admit_structural_mutation_staged_charge(
4953                &mut prefix,
4954                [1],
4955                AcceptedStructuralMutationPacking::BoundedPrefix,
4956            )
4957            .expect("the next prefix candidate should calculate"),
4958            AcceptedStructuralMutationStagedAdmission::PageFull,
4959        );
4960        assert_eq!(prefix, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
4961
4962        let mut empty_prefix = 0;
4963        assert_eq!(
4964            admit_structural_mutation_staged_charge(
4965                &mut empty_prefix,
4966                [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1],
4967                AcceptedStructuralMutationPacking::BoundedPrefix,
4968            )
4969            .expect("one oversized candidate should classify without mutating the prefix"),
4970            AcceptedStructuralMutationStagedAdmission::CandidateExceedsPolicy,
4971        );
4972        assert_eq!(empty_prefix, 0);
4973
4974        validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
4975            .expect("the exact result-byte boundary should admit");
4976        let error = validate_structural_mutation_result_bytes(
4977            MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
4978        )
4979        .expect_err("one byte above the result-byte boundary must reject");
4980        assert!(matches!(
4981            error.diagnostic().detail(),
4982            Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
4983                boundary:
4984                    icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
4985            }),
4986        ));
4987        assert_eq!(
4988            error.diagnostic_facts(),
4989            vec![
4990                (
4991                    icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
4992                    (MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1) as u64,
4993                ),
4994                (
4995                    icydb_diagnostic_code::DiagnosticFactTag::Limit,
4996                    MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES as u64,
4997                ),
4998            ],
4999        );
5000    }
5001
5002    #[expect(
5003        clippy::too_many_lines,
5004        reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
5005    )]
5006    #[test]
5007    fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
5008        let session = initialize();
5009        let catalog = session
5010            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5011            .expect("identity catalog should resolve");
5012        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5013            .expect("identity row layout should build");
5014        let initial_description = session
5015            .try_describe_entity_by_name(ENTITY_NAME)
5016            .expect("accepted Identity description should resolve");
5017        assert_eq!(
5018            initial_description.entity_tag(),
5019            catalog.identity().entity_tag().value()
5020        );
5021        assert_eq!(
5022            initial_description.accepted_schema_fingerprint_method(),
5023            catalog.fingerprint_method_version()
5024        );
5025        assert_eq!(
5026            initial_description.accepted_schema_fingerprint(),
5027            catalog.fingerprint()
5028        );
5029        let initial_identity = initial_description
5030            .identity()
5031            .expect("accepted Identity policy should be described");
5032        assert_eq!(initial_identity.field(), "id");
5033        assert_eq!(initial_identity.generator(), "Identity::next");
5034        assert_eq!(initial_identity.accepted_kind(), "nat64");
5035        assert_eq!(initial_identity.minimum(), 1);
5036        assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
5037        assert_eq!(initial_identity.high_water(), 0);
5038        assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
5039        assert!(!initial_identity.exhausted());
5040
5041        let rejected = session
5042            .execute_accepted_structural_save_batch(
5043                &catalog,
5044                &descriptor,
5045                batch(&[1_000, 2_000]),
5046                Timestamp::from_millis(6),
5047                |_| Err::<(), _>(InternalError::executor_unsupported()),
5048            )
5049            .expect_err("a rejected precommit result must not publish its tentative range");
5050        assert_eq!(rejected.class(), ErrorClass::Unsupported);
5051        assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
5052
5053        let rows = session
5054            .execute_accepted_structural_save_batch(
5055                &catalog,
5056                &descriptor,
5057                batch(&[10, 20, 30]),
5058                Timestamp::from_millis(7),
5059                Ok,
5060            )
5061            .expect("one accepted batch should commit rows and one identity range");
5062        assert_eq!(
5063            rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
5064            vec![
5065                vec![Value::Nat64(1), Value::Nat64(10)],
5066                vec![Value::Nat64(2), Value::Nat64(20)],
5067                vec![Value::Nat64(3), Value::Nat64(30)],
5068            ],
5069        );
5070
5071        let dynamic = session
5072            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
5073                entity: ENTITY_NAME.to_string(),
5074                patch: DynamicStructuralPatch::new(vec![(
5075                    "payload".to_string(),
5076                    DynamicWriteCell::Value(InputValue::Nat64(40)),
5077                )]),
5078            })
5079            .expect("dynamic omission should commit through shared Identity generation");
5080        assert_eq!(dynamic.affected_rows, 1);
5081
5082        for (request, operation) in [
5083            (
5084                DynamicMutation::Insert {
5085                    entity: ENTITY_NAME.to_string(),
5086                    patch: DynamicStructuralPatch::new(vec![
5087                        (
5088                            "id".to_string(),
5089                            DynamicWriteCell::Value(InputValue::Nat64(41)),
5090                        ),
5091                        (
5092                            "payload".to_string(),
5093                            DynamicWriteCell::Value(InputValue::Nat64(42)),
5094                        ),
5095                    ]),
5096                },
5097                icydb_diagnostic_code::DiagnosticMutationOperation::Insert,
5098            ),
5099            (
5100                DynamicMutation::Update {
5101                    entity: ENTITY_NAME.to_string(),
5102                    key: InputValue::Nat64(1),
5103                    patch: DynamicStructuralPatch::new(vec![(
5104                        "id".to_string(),
5105                        DynamicWriteCell::Default,
5106                    )]),
5107                },
5108                icydb_diagnostic_code::DiagnosticMutationOperation::Update,
5109            ),
5110        ] {
5111            let error = session
5112                .execute_trusted_dynamic_mutation(&request)
5113                .expect_err("structural Identity authorship and regeneration must reject");
5114            assert_eq!(error.class(), ErrorClass::Unsupported);
5115            assert_eq!(error.origin(), ErrorOrigin::Executor);
5116            assert_eq!(
5117                error.diagnostic_facts(),
5118                vec![
5119                    (
5120                        icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
5121                        ENTITY_TAG.value(),
5122                    ),
5123                    (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
5124                    (
5125                        icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
5126                        operation.raw(),
5127                    ),
5128                    (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
5129                ],
5130            );
5131        }
5132
5133        let binding = session
5134            .issue_typed_entity_binding(
5135                ENTITY_SOURCE,
5136                &[
5137                    DynamicTypedFieldBindingRequest::new(
5138                        ID_SOURCE.to_string(),
5139                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
5140                        false,
5141                    ),
5142                    DynamicTypedFieldBindingRequest::new(
5143                        PAYLOAD_SOURCE.to_string(),
5144                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
5145                        false,
5146                    ),
5147                ],
5148            )
5149            .expect("typed output should bind the Identity field");
5150        let typed_patch = binding
5151            .bind_write_fields(vec![(
5152                PAYLOAD_SOURCE.to_string(),
5153                DynamicWriteCell::Value(InputValue::Nat64(50)),
5154            )])
5155            .expect("typed payload should lower");
5156        let typed = session
5157            .execute_trusted_typed_mutation(
5158                &binding,
5159                &DynamicTypedMutation::Insert { patch: typed_patch },
5160            )
5161            .expect("typed omission should commit through shared Identity generation");
5162        assert_eq!(
5163            typed
5164                .expect("typed insert should return one mutation result")
5165                .affected_rows,
5166            1,
5167        );
5168        let explicit_typed_patch = binding
5169            .bind_write_fields(vec![
5170                (
5171                    ID_SOURCE.to_string(),
5172                    DynamicWriteCell::Value(InputValue::Nat64(51)),
5173                ),
5174                (
5175                    PAYLOAD_SOURCE.to_string(),
5176                    DynamicWriteCell::Value(InputValue::Nat64(52)),
5177                ),
5178            ])
5179            .expect("the low-level binding should retain exact authored intent");
5180        let explicit_typed_error = session
5181            .execute_trusted_typed_mutation(
5182                &binding,
5183                &DynamicTypedMutation::Insert {
5184                    patch: explicit_typed_patch,
5185                },
5186            )
5187            .expect_err("typed Identity authorship must reject before allocation");
5188        assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
5189        assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
5190        assert_eq!(
5191            explicit_typed_error.diagnostic_facts(),
5192            vec![
5193                (
5194                    icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
5195                    ENTITY_TAG.value(),
5196                ),
5197                (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
5198                (
5199                    icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
5200                    icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
5201                ),
5202                (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
5203            ],
5204        );
5205
5206        let replace_error = session
5207            .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
5208                entity: ENTITY_NAME.to_string(),
5209                key: InputValue::Nat64(99),
5210                patch: DynamicStructuralPatch::new(vec![(
5211                    "payload".to_string(),
5212                    DynamicWriteCell::Value(InputValue::Nat64(60)),
5213                )]),
5214            })
5215            .expect_err("save-as-insert with a chosen Identity must reject");
5216        assert_eq!(replace_error.class(), ErrorClass::Unsupported);
5217        assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
5218
5219        #[cfg(feature = "sql")]
5220        {
5221            for sql in [
5222                "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
5223                "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
5224            ] {
5225                let _result = session
5226                    .execute_trusted_sql_mutation(sql)
5227                    .expect("SQL omission and DEFAULT should commit Identity generation");
5228            }
5229
5230            let error = session
5231                .execute_trusted_sql_mutation(
5232                    "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
5233                )
5234                .expect_err("an explicit SQL Identity value must reject before allocation");
5235            let diagnostic = error.diagnostic();
5236            assert_eq!(
5237                diagnostic.code(),
5238                icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
5239            );
5240            assert!(matches!(
5241                diagnostic.detail(),
5242                Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
5243                    boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
5244                }),
5245            ));
5246        }
5247
5248        let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
5249        assert_eq!(
5250            DATA_STORE.with(|store| store.borrow().len()),
5251            expected_committed
5252        );
5253        SCHEMA_STORE.with(|store| {
5254            let cursor = store
5255                .borrow()
5256                .identity_statement_cursor(
5257                    database_incarnation_id().expect("database incarnation should remain readable"),
5258                    ENTITY_TAG,
5259                    FieldId::new(1),
5260                    &AcceptedFieldKind::Nat64,
5261                )
5262                .expect("committed writes must leave active state readable");
5263            assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
5264            assert!(!cursor.has_allocations());
5265        });
5266        let committed_description = session
5267            .try_describe_entity_by_name(ENTITY_NAME)
5268            .expect("committed Identity description should resolve");
5269        let committed_identity = committed_description
5270            .identity()
5271            .expect("accepted Identity policy should remain described");
5272        assert_eq!(
5273            committed_identity.high_water(),
5274            u128::from(expected_committed),
5275        );
5276        assert_eq!(
5277            committed_identity.remaining(),
5278            u128::from(u64::MAX - expected_committed),
5279        );
5280        assert!(!committed_identity.exhausted());
5281    }
5282
5283    #[test]
5284    #[expect(
5285        clippy::too_many_lines,
5286        reason = "one ordered scenario proves target/progress atomicity, every interruption wake-up, state-only admission, and successful no-op wake-up behavior"
5287    )]
5288    fn mutation_progress_and_target_rows_recover_as_one_marker_transition() {
5289        let session = initialize_journaled();
5290        let initial_entity_revision = JOURNALED_TAIL_STORE
5291            .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
5292            .expect("direct initial schema publication must install entity revision authority");
5293        assert_eq!(initial_entity_revision, 1);
5294        install_startup_recovery_wakeup(record_startup_wakeup);
5295        let catalog = session
5296            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5297            .expect("journaled atomic-progress catalog should resolve");
5298        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5299            .expect("journaled atomic-progress row layout should build");
5300
5301        for (ordinal, interruption) in [
5302            MutationCommitInterruption::MarkerPersisted,
5303            MutationCommitInterruption::JournalPublished,
5304            MutationCommitInterruption::RowsPublished,
5305            MutationCommitInterruption::ProgressReplaced,
5306        ]
5307        .into_iter()
5308        .enumerate()
5309        {
5310            let identity_byte = 31 + u8::try_from(ordinal).expect("small ordinal should fit");
5311            let (before, after, operation) = atomic_progress_fixture(identity_byte);
5312            with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5313                match store.insert_mutation(&before)? {
5314                    InsertMutationJobResult::Inserted => Ok(()),
5315                    InsertMutationJobResult::Occupied(_) => {
5316                        Err(crate::db::MutationJobError::IdentityConflict)
5317                    }
5318                }
5319            })
5320            .expect("atomic predecessor should insert once");
5321
5322            let wakeups_before = STARTUP_WAKEUPS.with(Cell::get);
5323            interrupt_next_mutation_commit_for_tests(interruption);
5324            let interrupted = session.execute_accepted_structural_update_with_mutation_progress(
5325                &catalog,
5326                &descriptor,
5327                batch(&[700 + u64::try_from(ordinal).expect("small ordinal should fit")]),
5328                Timestamp::from_millis(17),
5329                operation,
5330            );
5331            assert!(
5332                interrupted.is_err(),
5333                "selected atomic boundary should interrupt"
5334            );
5335            assert_eq!(
5336                STARTUP_WAKEUPS.with(Cell::get),
5337                wakeups_before.saturating_add(1),
5338                "a normally returned retained-marker error must register its wake-up",
5339            );
5340
5341            forget_recovered_domain_for_tests(&session.db)
5342                .expect("interruption should reset volatile recovery ownership");
5343            let retained_before =
5344                with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5345                    store.load_mutation(before.state().job_id)
5346                })
5347                .expect("pre-driver progress should load");
5348            let row_count_before = JOURNALED_DATA_STORE.with(|store| store.borrow().len());
5349            let pending = session
5350                .db
5351                .ensure_recovered_state()
5352                .expect_err("ordinary admission must not drive retained-marker recovery");
5353            assert_eq!(
5354                pending.diagnostic().error_code(),
5355                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
5356            );
5357            assert_eq!(
5358                with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5359                    store.load_mutation(before.state().job_id)
5360                })
5361                .expect("post-admission progress should load"),
5362                retained_before,
5363            );
5364            assert_eq!(
5365                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
5366                row_count_before,
5367                "state-only admission must not mutate target rows",
5368            );
5369            assert!(
5370                session
5371                    .db
5372                    .drive_startup_recovery_page()
5373                    .expect("dedicated driver should finish target and progress together"),
5374            );
5375            let retained = with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5376                store.load_mutation(before.state().job_id)
5377            })
5378            .expect("recovered successor should load");
5379            assert_eq!(retained, after);
5380            assert_eq!(
5381                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
5382                u64::try_from(ordinal + 1).expect("small row count should fit"),
5383            );
5384        }
5385
5386        let (before, after, operation) = atomic_progress_fixture(39);
5387        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5388            match store.insert_mutation(&before)? {
5389                InsertMutationJobResult::Inserted => Ok(()),
5390                InsertMutationJobResult::Occupied(_) => {
5391                    Err(crate::db::MutationJobError::IdentityConflict)
5392                }
5393            }
5394        })
5395        .expect("final predecessor should insert once");
5396        let wakeups_before_success = STARTUP_WAKEUPS.with(Cell::get);
5397        session
5398            .execute_accepted_structural_update_with_mutation_progress(
5399                &catalog,
5400                &descriptor,
5401                batch(&[799]),
5402                Timestamp::from_millis(18),
5403                operation,
5404            )
5405            .expect("uninterrupted atomic transition should clear its marker");
5406        assert_eq!(
5407            STARTUP_WAKEUPS.with(Cell::get),
5408            wakeups_before_success.saturating_add(1),
5409            "a successful retained commit must request online convergence",
5410        );
5411        let retained = with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5412            store.load_mutation(before.state().job_id)
5413        })
5414        .expect("final successor should load");
5415        assert_eq!(retained, after);
5416        forget_recovered_domain_for_tests(&session.db)
5417            .expect("post-clear recovery ownership should reset");
5418        let pending = session
5419            .db
5420            .ensure_recovered_state()
5421            .expect_err("an upgrade epoch must remain gated until its driver runs");
5422        assert_eq!(
5423            pending.diagnostic().error_code(),
5424            icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
5425        );
5426        assert!(
5427            session
5428                .db
5429                .drive_startup_recovery_page()
5430                .expect("post-clear driver recovery should fold the retained batch"),
5431        );
5432    }
5433
5434    #[test]
5435    fn startup_recovery_initializes_missing_entity_revisions_from_the_store_revision() {
5436        let session = initialize_journaled();
5437        let catalog = session
5438            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5439            .expect("journaled predecessor catalog should resolve");
5440        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5441            .expect("journaled predecessor row layout should build");
5442        session
5443            .execute_accepted_structural_save_batch(
5444                &catalog,
5445                &descriptor,
5446                batch(&[901]),
5447                Timestamp::from_millis(21),
5448                Ok,
5449            )
5450            .expect("predecessor row should advance the store-wide revision");
5451        let baseline = JOURNALED_TAIL_STORE.with(|tail| {
5452            let mut tail = tail.borrow_mut();
5453            let baseline = tail
5454                .data_mutation_revision()
5455                .expect("predecessor store-wide revision should load");
5456            tail.clear_entity_mutation_revisions_for_tests();
5457            baseline
5458        });
5459
5460        forget_recovered_domain_for_tests(&session.db)
5461            .expect("upgrade should reset volatile recovery ownership");
5462        drive_journaled_recovery_to_completion(&session);
5463
5464        let recovered = JOURNALED_TAIL_STORE
5465            .with(|tail| tail.borrow().entity_mutation_revision(ENTITY_TAG))
5466            .expect("recovery should publish the current entity authority");
5467        assert_eq!(recovered, baseline);
5468    }
5469
5470    #[test]
5471    fn mutation_progress_neither_side_mismatch_blocks_recovery() {
5472        let session = initialize_journaled();
5473        let catalog = session
5474            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5475            .expect("journaled corruption catalog should resolve");
5476        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5477            .expect("journaled corruption row layout should build");
5478        let (before, _after, operation) = atomic_progress_fixture(41);
5479        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5480            match store.insert_mutation(&before)? {
5481                InsertMutationJobResult::Inserted => Ok(()),
5482                InsertMutationJobResult::Occupied(_) => {
5483                    Err(crate::db::MutationJobError::IdentityConflict)
5484                }
5485            }
5486        })
5487        .expect("corruption predecessor should insert once");
5488
5489        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::MarkerPersisted);
5490        assert!(
5491            session
5492                .execute_accepted_structural_update_with_mutation_progress(
5493                    &catalog,
5494                    &descriptor,
5495                    batch(&[811]),
5496                    Timestamp::from_millis(19),
5497                    operation,
5498                )
5499                .is_err(),
5500            "marker interruption should retain recovery authority",
5501        );
5502        let (unexpected, _) = before
5503            .apply_transition(
5504                &MutationJobAdvanceRequest::new(
5505                    before.state().job_id,
5506                    0,
5507                    MutationJobIdempotencyKey::new("unexpected-third-state")
5508                        .expect("unexpected replay key should admit"),
5509                ),
5510                MutationJobTransition::new(
5511                    MutationJobStatus::Active,
5512                    MutationJobPhase::Forward,
5513                    vec![99],
5514                    2,
5515                    0,
5516                    0,
5517                ),
5518            )
5519            .expect("unexpected but valid progress state should admit");
5520        with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5521            store.replace_mutation(&unexpected)
5522        })
5523        .expect("test should install the neither-side state");
5524
5525        forget_recovered_domain_for_tests(&session.db)
5526            .expect("corrupt recovery ownership should reset");
5527        let error = session
5528            .db
5529            .drive_startup_recovery_page()
5530            .expect_err("neither-side progress must block recovery");
5531        assert_eq!(error.class(), ErrorClass::Corruption);
5532        assert_eq!(error.origin(), ErrorOrigin::Recovery);
5533        assert_eq!(
5534            with_mutation_progress_store::<JournaledTestCanister, _>(|store| {
5535                store.load_mutation(before.state().job_id)
5536            })
5537            .expect("unexpected state should remain inspectable to the test"),
5538            unexpected,
5539        );
5540        assert!(
5541            session.db.drive_startup_recovery_page().is_err(),
5542            "a retained corrupt marker must continue blocking database access",
5543        );
5544    }
5545
5546    #[test]
5547    #[expect(
5548        clippy::too_many_lines,
5549        reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
5550    )]
5551    fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
5552        let session = initialize_journaled();
5553        let catalog = session
5554            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5555            .expect("journaled identity catalog should resolve");
5556        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5557            .expect("journaled identity row layout should build");
5558
5559        for (ordinal, interruption) in [
5560            MutationCommitInterruption::MarkerPersisted,
5561            MutationCommitInterruption::JournalPublished,
5562            MutationCommitInterruption::RowsPublished,
5563            MutationCommitInterruption::StateMaterialized,
5564        ]
5565        .into_iter()
5566        .enumerate()
5567        {
5568            interrupt_next_mutation_commit_for_tests(interruption);
5569            let interrupted = session.execute_accepted_structural_save_batch(
5570                &catalog,
5571                &descriptor,
5572                batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
5573                Timestamp::from_millis(8),
5574                Ok,
5575            );
5576            assert!(
5577                interrupted.is_err(),
5578                "the selected durable boundary should interrupt",
5579            );
5580
5581            let Err(pending) = session.execute_accepted_structural_save_batch(
5582                &catalog,
5583                &descriptor,
5584                batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
5585                Timestamp::from_millis(9),
5586                Ok,
5587            ) else {
5588                panic!("ordinary mutation must not drive retained-marker recovery");
5589            };
5590            assert_eq!(
5591                pending.diagnostic().error_code(),
5592                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
5593            );
5594            drive_journaled_recovery_to_completion(&session);
5595
5596            let committed = session
5597                .execute_accepted_structural_save_batch(
5598                    &catalog,
5599                    &descriptor,
5600                    batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
5601                    Timestamp::from_millis(9),
5602                    Ok,
5603                )
5604                .expect("the next mutation must recover before allocating");
5605            let expected_high_water =
5606                u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
5607            assert_eq!(
5608                committed
5609                    .into_iter()
5610                    .map(|row| row.values)
5611                    .collect::<Vec<_>>(),
5612                vec![vec![
5613                    Value::Nat64(expected_high_water),
5614                    Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
5615                ]],
5616            );
5617            assert_eq!(
5618                JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
5619                expected_high_water,
5620            );
5621            JOURNALED_SCHEMA_STORE.with(|store| {
5622                let cursor = store
5623                    .borrow()
5624                    .identity_statement_cursor(
5625                        database_incarnation_id()
5626                            .expect("database incarnation should remain readable"),
5627                        ENTITY_TAG,
5628                        FieldId::new(1),
5629                        &AcceptedFieldKind::Nat64,
5630                    )
5631                    .expect("guarded recovery must leave quiescent active state");
5632                assert_eq!(
5633                    cursor.expected_high_water(),
5634                    u128::from(expected_high_water),
5635                );
5636                assert!(!cursor.has_allocations());
5637            });
5638        }
5639
5640        for (ordinal, (interruption, deleted_key)) in [
5641            (MutationCommitInterruption::MarkerPersisted, 2),
5642            (MutationCommitInterruption::JournalPublished, 4),
5643            (MutationCommitInterruption::RowPrefixPublished, 6),
5644            (MutationCommitInterruption::RowsPublished, 8),
5645            (MutationCommitInterruption::StateMaterialized, 7),
5646        ]
5647        .into_iter()
5648        .enumerate()
5649        {
5650            let expected_payload =
5651                501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
5652            interrupt_next_mutation_commit_for_tests(interruption);
5653            let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
5654                DynamicMutation::Update {
5655                    entity: ENTITY_NAME.to_string(),
5656                    key: InputValue::Nat64(1),
5657                    patch: dynamic_payload_patch(expected_payload),
5658                },
5659                DynamicMutation::Delete {
5660                    entity: ENTITY_NAME.to_string(),
5661                    key: InputValue::Nat64(deleted_key),
5662                },
5663            ]);
5664            assert!(
5665                interrupted.is_err(),
5666                "the selected caller-key mixed publication boundary should interrupt",
5667            );
5668            let pending = session
5669                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
5670                    entity: ENTITY_NAME.to_string(),
5671                    key: InputValue::Nat64(1),
5672                    patch: dynamic_payload_patch(expected_payload),
5673                })
5674                .expect_err("ordinary update must not drive retained-marker recovery");
5675            assert_eq!(
5676                pending.diagnostic().error_code(),
5677                icydb_diagnostic_code::ErrorCode::RUNTIME_BOUNDARY_DATABASE_STARTUP_RECOVERY_PENDING,
5678            );
5679            drive_journaled_recovery_to_completion(&session);
5680            let recovered_update = session
5681                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
5682                    entity: ENTITY_NAME.to_string(),
5683                    key: InputValue::Nat64(1),
5684                    patch: dynamic_payload_patch(expected_payload),
5685                })
5686                .expect("guarded reentry should complete the marker-authorized mixed batch");
5687            assert_eq!(
5688                recovered_update.affected_rows, 0,
5689                "the recovered update must already expose its admitted final image",
5690            );
5691            let recovered_delete = session
5692                .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
5693                    entity: ENTITY_NAME.to_string(),
5694                    key: InputValue::Nat64(deleted_key),
5695                })
5696                .expect_err("the recovered delete must already be materialized");
5697            assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
5698            JOURNALED_SCHEMA_STORE.with(|store| {
5699                let cursor = store
5700                    .borrow()
5701                    .identity_statement_cursor(
5702                        database_incarnation_id()
5703                            .expect("database incarnation should remain readable"),
5704                        ENTITY_TAG,
5705                        FieldId::new(1),
5706                        &AcceptedFieldKind::Nat64,
5707                    )
5708                    .expect("caller-key recovery must preserve active Identity state");
5709                assert_eq!(cursor.expected_high_water(), 8);
5710                assert!(!cursor.has_allocations());
5711            });
5712        }
5713
5714        forget_recovered_domain_for_tests(&session.db)
5715            .expect("the final journal tail should remain recoverable");
5716        session
5717            .db
5718            .drive_startup_recovery_page()
5719            .expect("derived rebuild must not allocate another identity");
5720
5721        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
5722        let index_generation = JOURNALED_INDEX_STORE.with(|store| store.borrow().generation());
5723        let data_len = JOURNALED_DATA_STORE.with(|store| store.borrow().len());
5724        let index_len = JOURNALED_INDEX_STORE.with(|store| store.borrow().len());
5725        forget_recovered_domain_for_tests(&session.db)
5726            .expect("an empty-tail upgrade should reset recovery ownership");
5727        session
5728            .db
5729            .drive_startup_recovery_page()
5730            .expect("an empty-tail upgrade should admit without rebuilding stored rows or indexes");
5731        assert_eq!(
5732            JOURNALED_DATA_STORE.with(|store| store.borrow().generation()),
5733            data_generation
5734                .checked_add(1)
5735                .expect("test generation should advance once"),
5736            "empty-tail recovery must reset the disposable row projection exactly once",
5737        );
5738        assert_eq!(
5739            JOURNALED_INDEX_STORE.with(|store| store.borrow().generation()),
5740            index_generation
5741                .checked_add(1)
5742                .expect("test generation should advance once"),
5743            "empty-tail recovery must reset the disposable index projection exactly once",
5744        );
5745        assert_eq!(
5746            JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
5747            data_len,
5748            "empty-tail recovery must not rebuild or remove authoritative rows",
5749        );
5750        assert_eq!(
5751            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
5752            index_len,
5753            "empty-tail recovery must not clear or rebuild canonical secondary indexes",
5754        );
5755
5756        let quick = execute_quick_integrity(
5757            &session.db,
5758            catalog.inspection_plan(),
5759            catalog.runtime_root_identity().database_incarnation(),
5760        )
5761        .expect("quiescent Identity control inventory should be inspectable");
5762        assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
5763        let row_page = execute_row_integrity_page(
5764            &session.db,
5765            catalog.inspection_plan(),
5766            PhysicalUnitCheckpoint::BeforeFirst,
5767            RowInspectionLimits::standard(),
5768        )
5769        .expect("Identity rows should remain within committed high-water");
5770        assert!(row_page.exhausted());
5771        assert!(row_page.findings().is_empty());
5772
5773        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
5774        assert!(
5775            JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
5776            "derived index rebuild should restore witnesses without allocating identities",
5777        );
5778        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
5779        JOURNALED_SCHEMA_STORE.with(|store| {
5780            let cursor = store
5781                .borrow()
5782                .identity_statement_cursor(
5783                    database_incarnation_id().expect("database incarnation should remain readable"),
5784                    ENTITY_TAG,
5785                    FieldId::new(1),
5786                    &AcceptedFieldKind::Nat64,
5787                )
5788                .expect("folded identity state should reopen without allocating");
5789            assert_eq!(cursor.expected_high_water(), 8);
5790            assert!(!cursor.has_allocations());
5791        });
5792    }
5793
5794    #[test]
5795    fn journaled_online_convergence_drains_the_full_backlog_in_complete_batch_callbacks_without_reallocating_ids()
5796     {
5797        const SUBMISSION: &str = "generated/8899aabbccddeeff";
5798        let session = initialize_journaled();
5799        let catalog = session
5800            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
5801            .expect("journaled identity catalog should resolve");
5802        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
5803            .expect("journaled identity row layout should build");
5804
5805        for payload in 0_u64..38 {
5806            session
5807                .execute_accepted_structural_save_batch(
5808                    &catalog,
5809                    &descriptor,
5810                    batch(&[payload]),
5811                    Timestamp::from_millis(8),
5812                    Ok,
5813                )
5814                .unwrap_or_else(|error| {
5815                    panic!("journaled identity fixture row {payload} should commit: {error:?}")
5816                });
5817        }
5818
5819        let before = JOURNALED_TAIL_STORE.with(|tail| {
5820            tail.borrow()
5821                .current_tail_control()
5822                .expect("online backlog control should remain valid")
5823        });
5824        assert_eq!(before.batch_count(), 38);
5825        let next_sequence = crate::db::commit::next_database_commit_sequence()
5826            .expect("database sequence preview should remain readable");
5827        let Err(pressure) = session.execute_accepted_structural_save_batch(
5828            &catalog,
5829            &descriptor,
5830            batch(&[38]),
5831            Timestamp::from_millis(8),
5832            Ok,
5833        ) else {
5834            panic!("the exact cumulative batch ceiling should reject one more batch")
5835        };
5836        assert_exact_batch_backlog_pressure(&pressure, before, next_sequence);
5837
5838        for folded_batches in 1..=38 {
5839            let complete = session
5840                .db
5841                .drive_startup_recovery_page()
5842                .expect("online complete-batch callback should commit");
5843            assert_eq!(complete, folded_batches == 38);
5844        }
5845
5846        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
5847        session
5848            .execute_accepted_structural_save_batch(
5849                &catalog,
5850                &descriptor,
5851                batch(&[38]),
5852                Timestamp::from_millis(8),
5853                Ok,
5854            )
5855            .expect("drain should make the rejected mutation retryable");
5856        assert!(
5857            session
5858                .db
5859                .drive_startup_recovery_page()
5860                .expect("the retry tail should converge"),
5861        );
5862
5863        assert_eq!(
5864            drive_generated_startup_recovery_page(&session, &JOURNALED_STORE_REGISTRY, SUBMISSION,)
5865                .expect("online convergence should commit"),
5866            GeneratedStartupDriverStep::Terminal,
5867            "the quiescent generated driver should stop",
5868        );
5869
5870        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 39);
5871        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
5872        assert_dynamic_payload(&session, 1, 0);
5873        assert_dynamic_payload(&session, 39, 38);
5874        JOURNALED_SCHEMA_STORE.with(|store| {
5875            let cursor = store
5876                .borrow()
5877                .identity_statement_cursor(
5878                    database_incarnation_id().expect("database incarnation should remain readable"),
5879                    ENTITY_TAG,
5880                    FieldId::new(1),
5881                    &AcceptedFieldKind::Nat64,
5882                )
5883                .expect("online convergence must preserve active Identity state");
5884            assert_eq!(cursor.expected_high_water(), 39);
5885            assert!(!cursor.has_allocations());
5886        });
5887    }
5888
5889    #[test]
5890    fn journaled_online_convergence_reconstructs_same_key_batches_from_canonical_predecessors() {
5891        let session = initialize_journaled();
5892        session
5893            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
5894                entity: ENTITY_NAME.to_string(),
5895                patch: dynamic_payload_patch(10),
5896            })
5897            .expect("the initial positioned row should commit");
5898        for payload in [20, 30] {
5899            session
5900                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
5901                    entity: ENTITY_NAME.to_string(),
5902                    key: InputValue::Nat64(1),
5903                    patch: dynamic_payload_patch(payload),
5904                })
5905                .unwrap_or_else(|error| {
5906                    panic!("the positioned same-key update should commit: {error:?}")
5907                });
5908        }
5909
5910        assert_dynamic_payload(&session, 1, 30);
5911        assert_eq!(
5912            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
5913            1,
5914            "the newest live index effect should hide every predecessor",
5915        );
5916        for folded_batches in 1..=3 {
5917            let complete = session
5918                .db
5919                .drive_startup_recovery_page()
5920                .expect("the positioned same-key batch should converge");
5921            assert_eq!(complete, folded_batches == 3);
5922        }
5923
5924        assert_dynamic_payload(&session, 1, 30);
5925        assert_eq!(
5926            JOURNALED_INDEX_STORE.with(|store| store.borrow().len()),
5927            1,
5928            "canonical derived state must contain only the newest membership",
5929        );
5930        assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
5931    }
5932
5933    #[test]
5934    fn ready_cardinality_combines_durable_base_with_exact_live_delta_and_fold_maintenance() {
5935        let session = initialize_journaled();
5936        session
5937            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
5938                entity: ENTITY_NAME.to_string(),
5939                patch: dynamic_payload_patch(10),
5940            })
5941            .expect("initial cardinality row should commit");
5942        assert!(
5943            session
5944                .db
5945                .drive_startup_recovery_page()
5946                .expect("initial cardinality row should fold"),
5947        );
5948        drive_journaled_cardinality_to_ready(&session);
5949        let handle = session
5950            .db
5951            .store_handle(JOURNALED_STORE_PATH)
5952            .expect("journaled cardinality store should resolve");
5953        let (index_id, prefix_components) = journaled_user_index_prefix();
5954        reset_journaled_cardinality_projections();
5955        assert_eq!(
5956            JOURNALED_DATA_STORE.with(|store| store.borrow().exact_entity_count(ENTITY_TAG)),
5957            None,
5958            "the reopened-style volatile full count must remain unavailable",
5959        );
5960        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
5961
5962        session
5963            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
5964                entity: ENTITY_NAME.to_string(),
5965                patch: dynamic_payload_patch(10),
5966            })
5967            .expect("post-Ready row should commit into the live overlay");
5968        for payload in [20, 10] {
5969            session
5970                .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
5971                    entity: ENTITY_NAME.to_string(),
5972                    key: InputValue::Nat64(2),
5973                    patch: dynamic_payload_patch(payload),
5974                })
5975                .expect("same-key post-Ready overlay should commit");
5976        }
5977        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
5978        for folded in 1..=3 {
5979            let complete = session
5980                .db
5981                .drive_startup_recovery_page()
5982                .expect("post-Ready row should fold with exact maintenance");
5983            assert_eq!(complete, folded == 3);
5984            assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
5985        }
5986        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 2);
5987        session
5988            .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
5989                entity: ENTITY_NAME.to_string(),
5990                key: InputValue::Nat64(2),
5991            })
5992            .expect("post-Ready delete should commit into the live overlay");
5993        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
5994        assert!(
5995            session
5996                .db
5997                .drive_startup_recovery_page()
5998                .expect("post-Ready delete should fold with exact maintenance"),
5999        );
6000        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
6001        mark_journaled_cardinality_building();
6002        assert_eq!(
6003            handle.exact_entity_count(ENTITY_TAG),
6004            None,
6005            "non-Ready evidence must select the conservative path",
6006        );
6007    }
6008
6009    #[test]
6010    fn journaled_cardinality_rejects_volatile_counts_and_unfolded_accepted_root_drift() {
6011        let session = initialize_journaled();
6012        session
6013            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
6014                entity: ENTITY_NAME.to_string(),
6015                patch: dynamic_payload_patch(10),
6016            })
6017            .expect("cardinality fixture row should commit");
6018        assert!(
6019            session
6020                .db
6021                .drive_startup_recovery_page()
6022                .expect("cardinality fixture row should fold"),
6023        );
6024        drive_journaled_cardinality_to_ready(&session);
6025        let handle = session
6026            .db
6027            .store_handle(JOURNALED_STORE_PATH)
6028            .expect("journaled cardinality store should resolve");
6029        let (index_id, prefix_components) = journaled_user_index_prefix();
6030        let data_generation = JOURNALED_DATA_STORE.with(|store| store.borrow().generation());
6031
6032        assert_eq!(
6033            JOURNALED_DATA_STORE.with(|store| store.borrow().exact_entity_count(ENTITY_TAG)),
6034            Some(1),
6035            "the live full-count cache should be populated before accepted-root drift",
6036        );
6037        assert_eq!(
6038            JOURNALED_INDEX_STORE.with(|store| {
6039                store.borrow().exact_prefix_cardinality(
6040                    data_generation,
6041                    IndexKeyKind::User,
6042                    index_id,
6043                    prefix_components.as_slice(),
6044                )
6045            }),
6046            Some(1),
6047            "the live prefix-count cache should be populated before accepted-root drift",
6048        );
6049        assert_eq!(
6050            JOURNALED_INDEX_STORE.with(|store| {
6051                store.borrow().exact_child_prefixes_for_parent_set(
6052                    data_generation,
6053                    IndexKeyKind::User,
6054                    index_id,
6055                    [prefix_components.as_slice()],
6056                    8,
6057                )
6058            }),
6059            Some(Vec::new()),
6060            "the volatile child-prefix cache should demonstrate the bypass fixture",
6061        );
6062        assert_eq!(
6063            handle.exact_user_index_child_prefixes_for_parent_set(
6064                data_generation,
6065                index_id,
6066                [prefix_components.as_slice()],
6067                8,
6068            ),
6069            None,
6070            "journaled child enumeration must use its conservative route instead of volatile authority",
6071        );
6072        assert_journaled_cardinality(handle, index_id, prefix_components.as_slice(), 1);
6073
6074        let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
6075            JOURNALED_STORE_PATH,
6076            AcceptedSchemaRevision::new(2),
6077            BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH, false))]),
6078            BTreeMap::from([
6079                ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
6080                ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
6081            ]),
6082        );
6083        crate::db::commit::publish_accepted_schema_candidate(
6084            JOURNALED_STORE_PATH,
6085            handle,
6086            AcceptedSchemaRevision::INITIAL,
6087            &candidate,
6088        )
6089        .expect("a successor accepted root should publish into the live overlay");
6090
6091        assert_eq!(
6092            handle.exact_entity_count(ENTITY_TAG),
6093            None,
6094            "an unfolded accepted root must invalidate durable evidence immediately",
6095        );
6096        assert_eq!(
6097            handle.exact_user_index_prefix_count(
6098                data_generation,
6099                IndexKeyKind::User,
6100                index_id,
6101                prefix_components.as_slice(),
6102            ),
6103            None,
6104            "journaled consumers must not fall back to a populated volatile prefix cache",
6105        );
6106    }
6107
6108    #[test]
6109    fn journaled_convergence_uses_final_batch_rows_for_unique_release() {
6110        let session = initialize_journaled_with_unique_payload();
6111        let inserted = session
6112            .execute_trusted_dynamic_insert_batch(
6113                ENTITY_NAME,
6114                vec![dynamic_payload_patch(10), dynamic_payload_patch(20)],
6115            )
6116            .expect("the unique journal fixture should commit");
6117        assert_eq!(
6118            inserted.rows,
6119            vec![expected_dynamic_row(1, 10), expected_dynamic_row(2, 20)],
6120        );
6121        assert!(
6122            session
6123                .db
6124                .drive_startup_recovery_page()
6125                .expect("the unique fixture should become canonical"),
6126        );
6127
6128        let swapped = session
6129            .execute_trusted_dynamic_mutation_batch(vec![
6130                DynamicMutation::Update {
6131                    entity: ENTITY_NAME.to_string(),
6132                    key: InputValue::Nat64(1),
6133                    patch: dynamic_payload_patch(20),
6134                },
6135                DynamicMutation::Update {
6136                    entity: ENTITY_NAME.to_string(),
6137                    key: InputValue::Nat64(2),
6138                    patch: dynamic_payload_patch(10),
6139                },
6140            ])
6141            .expect("one journal batch should admit a final-row unique swap");
6142        assert_eq!(
6143            swapped.rows,
6144            vec![expected_dynamic_row(1, 20), expected_dynamic_row(2, 10)],
6145        );
6146        assert!(
6147            session
6148                .db
6149                .drive_startup_recovery_page()
6150                .expect("the unique swap should converge in one complete batch"),
6151        );
6152
6153        let released = session
6154            .execute_trusted_dynamic_mutation_batch(vec![
6155                DynamicMutation::Delete {
6156                    entity: ENTITY_NAME.to_string(),
6157                    key: InputValue::Nat64(1),
6158                },
6159                DynamicMutation::Insert {
6160                    entity: ENTITY_NAME.to_string(),
6161                    patch: dynamic_payload_patch(20),
6162                },
6163            ])
6164            .expect("a journaled delete should release its unique value to the final insert");
6165        assert_eq!(
6166            released.rows,
6167            vec![expected_dynamic_row(1, 20), expected_dynamic_row(3, 20)],
6168        );
6169        assert!(
6170            session
6171                .db
6172                .drive_startup_recovery_page()
6173                .expect("the delete and unique reuse should converge together"),
6174        );
6175
6176        assert_dynamic_payload(&session, 2, 10);
6177        assert_dynamic_payload(&session, 3, 20);
6178        assert_eq!(JOURNALED_INDEX_STORE.with(|store| store.borrow().len()), 2);
6179        assert!(
6180            session
6181                .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(20)],)
6182                .is_err(),
6183            "the converged unique index must remain authoritative",
6184        );
6185    }
6186
6187    #[test]
6188    fn journaled_startup_recovery_completes_one_large_batch_atomically() {
6189        let session = initialize_journaled();
6190        let catalog = session
6191            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
6192            .expect("journaled identity catalog should resolve");
6193        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
6194            .expect("journaled identity row layout should build");
6195        let payloads = (0_u64..129).collect::<Vec<_>>();
6196        session
6197            .execute_accepted_structural_save_batch(
6198                &catalog,
6199                &descriptor,
6200                batch(&payloads),
6201                Timestamp::from_millis(9),
6202                Ok,
6203            )
6204            .expect("one large journal batch should commit");
6205
6206        forget_recovered_domain_for_tests(&session.db)
6207            .expect("upgrade should reset recovery ownership");
6208        assert!(
6209            session
6210                .db
6211                .drive_startup_recovery_page()
6212                .expect("the complete batch recovery page should commit"),
6213        );
6214
6215        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 129);
6216        JOURNALED_TAIL_STORE.with(|tail| {
6217            let tail = tail.borrow();
6218            assert!(!tail.has_stored_batch());
6219        });
6220        assert_dynamic_payload(&session, 1, 0);
6221        assert_dynamic_payload(&session, 129, 128);
6222    }
6223
6224    #[test]
6225    fn complete_batch_validation_rejects_a_late_record_before_canonical_writes() {
6226        let session = initialize_journaled();
6227        let catalog = session
6228            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
6229            .expect("journaled identity catalog should resolve");
6230        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
6231            .expect("journaled identity row layout should build");
6232        session
6233            .execute_accepted_structural_save_batch(
6234                &catalog,
6235                &descriptor,
6236                batch(&[7]),
6237                Timestamp::from_millis(9),
6238                Ok,
6239            )
6240            .expect("journal batch predecessor should commit");
6241
6242        JOURNALED_TAIL_STORE.with(|tail| {
6243            let mut tail = tail.borrow_mut();
6244            let original = tail
6245                .next_batch_after(JournalSequence::new(0))
6246                .expect("journal batch should decode")
6247                .expect("journal batch should exist");
6248            let mut records = original.records().to_vec();
6249            records.push(
6250                JournalRecord::schema_put(JOURNALED_STORE_PATH, vec![0xff; 8])
6251                    .expect("bounded semantic corruption should build"),
6252            );
6253            let corrupted = JournalBatch::new_with_database_commit_sequence(
6254                original.batch_id(),
6255                original.commit_marker_id(),
6256                original.journal_sequence(),
6257                original.database_commit_sequence(),
6258                records,
6259            )
6260            .expect("current corrupt batch shape should build");
6261            let encoded = encode_journal_batch(&corrupted)
6262                .expect("current corrupt batch envelope should encode");
6263            tail.clear_batches_through(original.journal_sequence());
6264            tail.insert_raw_batch_for_tests(original.journal_sequence(), encoded)
6265                .expect("corrupt persisted batch should replace the predecessor");
6266        });
6267
6268        forget_recovered_domain_for_tests(&session.db)
6269            .expect("upgrade should reset recovery ownership");
6270        let error = session
6271            .db
6272            .drive_startup_recovery_page()
6273            .expect_err("late semantic corruption must fail before fold apply");
6274        assert_eq!(error.class(), ErrorClass::Corruption);
6275        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 0);
6276        JOURNALED_TAIL_STORE.with(|tail| {
6277            let tail = tail.borrow();
6278            assert_eq!(
6279                tail.fold_watermark()
6280                    .expect("watermark should remain readable")
6281                    .highest_folded_journal_sequence(),
6282                JournalSequence::new(0),
6283            );
6284            assert!(tail.has_stored_batch());
6285        });
6286    }
6287
6288    #[test]
6289    fn prepared_batch_row_evidence_rejects_a_late_malformed_row_before_canonical_writes() {
6290        let session = initialize_journaled();
6291        let catalog = session
6292            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
6293            .expect("journaled identity catalog should resolve");
6294        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
6295            .expect("journaled identity row layout should build");
6296        session
6297            .execute_accepted_structural_save_batch(
6298                &catalog,
6299                &descriptor,
6300                batch(&[7, 8]),
6301                Timestamp::from_millis(9),
6302                Ok,
6303            )
6304            .expect("two-row journal batch should commit");
6305
6306        JOURNALED_TAIL_STORE.with(|tail| {
6307            let mut tail = tail.borrow_mut();
6308            let original = tail
6309                .next_batch_after(JournalSequence::new(0))
6310                .expect("journal batch should decode")
6311                .expect("journal batch should exist");
6312            let mut records = original.records().to_vec();
6313            let mut row_ordinal = 0_u8;
6314            for record in &mut records {
6315                if let JournalRecord::RowPut { row_bytes, .. } = record {
6316                    row_ordinal = row_ordinal.saturating_add(1);
6317                    if row_ordinal == 2 {
6318                        *row_bytes = vec![0xff; 8];
6319                        break;
6320                    }
6321                }
6322            }
6323            assert_eq!(row_ordinal, 2, "the late row record should be present");
6324            let corrupted = JournalBatch::new_with_database_commit_sequence(
6325                original.batch_id(),
6326                original.commit_marker_id(),
6327                original.journal_sequence(),
6328                original.database_commit_sequence(),
6329                records,
6330            )
6331            .expect("current corrupt batch shape should build");
6332            let encoded = encode_journal_batch(&corrupted)
6333                .expect("current corrupt batch envelope should encode");
6334            tail.clear_batches_through(original.journal_sequence());
6335            tail.insert_raw_batch_for_tests(original.journal_sequence(), encoded)
6336                .expect("corrupt persisted batch should replace the predecessor");
6337        });
6338
6339        forget_recovered_domain_for_tests(&session.db)
6340            .expect("upgrade should reset recovery ownership");
6341        let error = session
6342            .db
6343            .drive_startup_recovery_page()
6344            .expect_err("late malformed row must fail during complete batch preparation");
6345        assert_eq!(error.class(), ErrorClass::Corruption);
6346        assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 0);
6347        JOURNALED_TAIL_STORE.with(|tail| {
6348            let tail = tail.borrow();
6349            assert_eq!(
6350                tail.fold_watermark()
6351                    .expect("watermark should remain readable")
6352                    .highest_folded_journal_sequence(),
6353                JournalSequence::new(0),
6354            );
6355            assert!(tail.has_stored_batch());
6356        });
6357    }
6358
6359    #[test]
6360    #[ignore = "release-closeout native timing probe for one marker-authorized driver recovery"]
6361    fn identity_recovery_closeout_reports_driver_time() {
6362        let session = initialize_journaled();
6363        let catalog = session
6364            .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
6365            .expect("journaled identity catalog should resolve");
6366        let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
6367            .expect("journaled identity row layout should build");
6368
6369        interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
6370        let interrupted = session.execute_accepted_structural_save_batch(
6371            &catalog,
6372            &descriptor,
6373            batch(&[1]),
6374            Timestamp::from_millis(10),
6375            Ok,
6376        );
6377        assert!(
6378            interrupted.is_err(),
6379            "the selected publication boundary should interrupt",
6380        );
6381
6382        let start = Instant::now();
6383        assert!(
6384            session
6385                .db
6386                .drive_startup_recovery_page()
6387                .expect("dedicated driver should recover before allocation"),
6388        );
6389        let committed = session
6390            .execute_accepted_structural_save_batch(
6391                &catalog,
6392                &descriptor,
6393                batch(&[2]),
6394                Timestamp::from_millis(11),
6395                Ok,
6396            )
6397            .expect("post-recovery allocation should commit");
6398        let elapsed = start.elapsed();
6399        assert_eq!(
6400            committed
6401                .into_iter()
6402                .map(|row| row.values)
6403                .collect::<Vec<_>>(),
6404            vec![vec![Value::Nat64(2), Value::Nat64(2)]],
6405        );
6406
6407        println!(
6408            "identity recovery closeout: driver_nanos={}",
6409            elapsed.as_nanos(),
6410        );
6411    }
6412}
6413
6414#[cfg(test)]
6415mod targeted_rule_mutation_tests {
6416    use super::{
6417        DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
6418        DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
6419    };
6420    use crate::{
6421        db::{
6422            data::{DataStore, encode_input_value_for_candidate_field_contract},
6423            index::IndexStore,
6424            registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
6425            schema::{
6426                AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
6427                AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
6428                AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
6429                ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
6430                PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
6431                ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
6432                SchemaRowLayout, SchemaStore, SchemaVersion,
6433                accepted_schema_candidate_with_catalogs_for_tests,
6434                build_record_newtype_composite_catalog_for_tests,
6435                empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
6436            },
6437        },
6438        error::InternalError,
6439        traits::{CanisterKind, Path},
6440        types::EntityTag,
6441        value::InputValue,
6442    };
6443    use icydb_schema::{
6444        ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
6445    };
6446    use std::{cell::RefCell, collections::BTreeMap};
6447
6448    const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
6449    const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
6450    const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
6451    const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
6452    const UPDATED_AT_SOURCE: &str =
6453        "session::write::targeted_rule_mutation_tests::Entity::updated_at";
6454    const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
6455    const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
6456    const DEGREE_MEMBER_SOURCE: &str =
6457        "session::write::targeted_rule_mutation_tests::Profile::degree";
6458    const DEGREE_RULE_SOURCE: &str =
6459        "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
6460
6461    struct TestCanister;
6462
6463    impl Path for TestCanister {
6464        const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
6465    }
6466
6467    impl CanisterKind for TestCanister {
6468        const COMMIT_MEMORY_ID: u8 = 43;
6469        const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
6470        const STARTUP_MEMORY_ID: u8 = 49;
6471        const STARTUP_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.startup.control.v1";
6472        const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
6473        const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
6474            "icydb.targeted_mutation_tests.integrity.progress.v1";
6475    }
6476
6477    thread_local! {
6478        static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
6479        static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
6480        static SCHEMA_STORE: RefCell<SchemaStore> =
6481            const { RefCell::new(SchemaStore::init_heap()) };
6482        static STORE_REGISTRY: StoreRegistry = {
6483            let mut registry = StoreRegistry::new();
6484            registry.register_store(
6485                STORE_PATH,
6486                &DATA_STORE,
6487                &INDEX_STORE,
6488                &SCHEMA_STORE,
6489                StoreAllocationIdentities::absent(),
6490                StoreRuntimeStorageCapabilities::heap(),
6491            ).expect("targeted mutation test store should register");
6492            registry
6493        };
6494    }
6495
6496    fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
6497        parse(raw.to_string()).expect("test source identity should admit")
6498    }
6499
6500    fn profile_input(degree: u64) -> InputValue {
6501        InputValue::Map(vec![(
6502            InputValue::Text("degree".to_string()),
6503            InputValue::Nat64(degree),
6504        )])
6505    }
6506
6507    fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
6508        DynamicStructuralPatch::new(vec![
6509            (
6510                "id".to_string(),
6511                DynamicWriteCell::Value(InputValue::Nat64(id)),
6512            ),
6513            (
6514                "profile".to_string(),
6515                DynamicWriteCell::Value(profile_input(degree)),
6516            ),
6517        ])
6518    }
6519
6520    fn encoded_value(
6521        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
6522        composite_catalog: &AcceptedCompositeCatalog,
6523        name: &str,
6524        kind: &AcceptedFieldKind,
6525        storage_decode: FieldStorageDecode,
6526        leaf_codec: LeafCodec,
6527        value: InputValue,
6528    ) -> Vec<u8> {
6529        let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
6530        encode_input_value_for_candidate_field_contract(
6531            enum_catalog,
6532            composite_catalog,
6533            field,
6534            value,
6535            &mut ValueAdmissionBudget::standard(),
6536        )
6537        .expect("test accepted value should encode")
6538    }
6539
6540    fn nat64_literal(
6541        enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
6542        composite_catalog: &AcceptedCompositeCatalog,
6543        value: u64,
6544    ) -> AcceptedCheckLiteralV1 {
6545        let kind = AcceptedFieldKind::Nat64;
6546        AcceptedCheckLiteralV1::from_accepted_parts(
6547            kind.clone(),
6548            FieldStorageDecode::ByKind,
6549            LeafCodec::Scalar(ScalarCodec::Nat64),
6550            encoded_value(
6551                enum_catalog,
6552                composite_catalog,
6553                "degree_bound",
6554                &kind,
6555                FieldStorageDecode::ByKind,
6556                LeafCodec::Scalar(ScalarCodec::Nat64),
6557                InputValue::Nat64(value),
6558            ),
6559        )
6560    }
6561
6562    fn targeted_constraint_id(error: &InternalError) -> u32 {
6563        let facts = error.diagnostic_facts();
6564        assert!(facts.contains(&(
6565            icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
6566            icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
6567        )));
6568        assert!(facts.contains(&(icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,)));
6569        assert!(facts.contains(&(
6570            icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
6571            icydb_diagnostic_code::DiagnosticConstraintKind::TargetedRule.raw(),
6572        )));
6573        assert_eq!(
6574            facts
6575                .iter()
6576                .filter(|(tag, _)| matches!(
6577                    tag,
6578                    icydb_diagnostic_code::DiagnosticFactTag::RootField
6579                        | icydb_diagnostic_code::DiagnosticFactTag::RecordMember
6580                ))
6581                .copied()
6582                .collect::<Vec<_>>(),
6583            vec![
6584                (icydb_diagnostic_code::DiagnosticFactTag::RootField, 2),
6585                (
6586                    icydb_diagnostic_code::DiagnosticFactTag::RecordMember,
6587                    icydb_diagnostic_code::pack_u32_pair(1, 1),
6588                ),
6589            ]
6590        );
6591        let value = facts
6592            .iter()
6593            .find_map(|(tag, value)| {
6594                (*tag == icydb_diagnostic_code::DiagnosticFactTag::ConstraintId).then_some(*value)
6595            })
6596            .expect("targeted mutation should retain its accepted constraint ID");
6597        u32::try_from(value).expect("accepted constraint ID fits u32")
6598    }
6599
6600    #[expect(
6601        clippy::too_many_lines,
6602        reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
6603    )]
6604    #[test]
6605    fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
6606        DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
6607        INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
6608        SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
6609
6610        let entity_tag = EntityTag::new(93);
6611        let enum_catalog = empty_accepted_enum_catalog_for_tests();
6612        let (composite_catalog, profile_type, degree_type, degree_member) =
6613            build_record_newtype_composite_catalog_for_tests(
6614                "tests::TargetedProfile".to_string(),
6615                "degree".to_string(),
6616                "tests::TargetedDegree".to_string(),
6617                AcceptedFieldKind::Nat64,
6618                &enum_catalog,
6619            )
6620            .expect("targeted mutation composites should close");
6621        let profile_kind = AcceptedFieldKind::Composite {
6622            type_id: profile_type,
6623        };
6624        let profile_default = encoded_value(
6625            &enum_catalog,
6626            &composite_catalog,
6627            "profile",
6628            &profile_kind,
6629            FieldStorageDecode::CatalogValue,
6630            LeafCodec::Structural,
6631            profile_input(12),
6632        );
6633        let fields = vec![
6634            PersistedFieldSnapshot::new_initial(
6635                FieldId::new(1),
6636                "id".to_string(),
6637                SchemaFieldSlot::new(0),
6638                AcceptedFieldKind::Nat64,
6639                Vec::new(),
6640                false,
6641                SchemaInsertDefault::None,
6642                FieldStorageDecode::ByKind,
6643                LeafCodec::Scalar(ScalarCodec::Nat64),
6644            ),
6645            PersistedFieldSnapshot::new_initial(
6646                FieldId::new(2),
6647                "profile".to_string(),
6648                SchemaFieldSlot::new(1),
6649                profile_kind,
6650                vec![PersistedNestedLeafSnapshot::new(
6651                    vec!["degree".to_string()],
6652                    AcceptedFieldKind::Composite {
6653                        type_id: degree_type,
6654                    },
6655                    false,
6656                )],
6657                false,
6658                SchemaInsertDefault::SlotPayload(profile_default),
6659                FieldStorageDecode::CatalogValue,
6660                LeafCodec::Structural,
6661            ),
6662            PersistedFieldSnapshot::new_initial_with_write_policy(
6663                FieldId::new(3),
6664                "updated_at".to_string(),
6665                SchemaFieldSlot::new(2),
6666                AcceptedFieldKind::Timestamp,
6667                Vec::new(),
6668                false,
6669                SchemaInsertDefault::None,
6670                SchemaFieldWritePolicy::from_model_policies(
6671                    None,
6672                    Some(FieldWriteManagement::UpdatedAt),
6673                ),
6674                FieldStorageDecode::ByKind,
6675                LeafCodec::Scalar(ScalarCodec::Timestamp),
6676            ),
6677        ];
6678        let mut snapshot = PersistedSchemaSnapshot::new(
6679            SchemaVersion::initial(),
6680            ENTITY_SOURCE.to_string(),
6681            "TargetedMutation".to_string(),
6682            FieldId::new(1),
6683            SchemaRowLayout::initial(
6684                fields
6685                    .iter()
6686                    .map(|field| (field.id(), field.slot()))
6687                    .collect(),
6688            ),
6689            fields,
6690        );
6691        let constraint_catalog = snapshot
6692            .constraint_catalog()
6693            .clone()
6694            .with_added_targeted_rule(
6695                "profile_degree_multiple".to_string(),
6696                ConstraintOrigin::Generated,
6697                AcceptedRuleTarget::new(
6698                    FieldId::new(2),
6699                    AcceptedNamedTypeIdentity::Composite(degree_type),
6700                ),
6701                AcceptedRuleOperation::MultipleOf {
6702                    divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
6703                },
6704            )
6705            .expect("targeted mutation rule should allocate");
6706        let targeted_rule_id = constraint_catalog
6707            .constraints()
6708            .last()
6709            .expect("targeted mutation rule should persist")
6710            .id();
6711        snapshot = snapshot.with_constraint_catalog(constraint_catalog);
6712
6713        let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
6714        let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
6715        let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
6716        let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
6717        let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
6718        let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
6719        let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
6720        let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
6721        let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
6722            BTreeMap::from([(entity_source, entity_tag)]),
6723            BTreeMap::from([
6724                ((entity_tag, id_source), FieldId::new(1)),
6725                ((entity_tag, profile_source), FieldId::new(2)),
6726                ((entity_tag, updated_at_source), FieldId::new(3)),
6727            ]),
6728            BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
6729            BTreeMap::new(),
6730            BTreeMap::new(),
6731        )
6732        .with_initial_named_types_for_tests(
6733            BTreeMap::from([
6734                (
6735                    profile_type_source,
6736                    AcceptedNamedTypeIdentity::Composite(profile_type),
6737                ),
6738                (
6739                    degree_type_source,
6740                    AcceptedNamedTypeIdentity::Composite(degree_type),
6741                ),
6742            ]),
6743            BTreeMap::new(),
6744            BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
6745        );
6746        let candidate = accepted_schema_candidate_with_catalogs_for_tests(
6747            STORE_PATH,
6748            AcceptedSchemaRevision::INITIAL,
6749            enum_catalog,
6750            composite_catalog,
6751            source_bindings,
6752            BTreeMap::from([(entity_tag, snapshot)]),
6753        );
6754
6755        let session = DbSession::<TestCanister>::new(
6756            &STORE_REGISTRY,
6757            &crate::db::RequestExecutionRoot::__new_runtime_root(),
6758        );
6759        session
6760            .db
6761            .drive_startup_recovery_page()
6762            .expect("targeted mutation test database should initialize");
6763        let store = session
6764            .db
6765            .store_handle(STORE_PATH)
6766            .expect("targeted mutation test store should resolve");
6767        crate::db::commit::publish_accepted_schema_candidate(
6768            STORE_PATH,
6769            store,
6770            AcceptedSchemaRevision::NONE,
6771            &candidate,
6772        )
6773        .expect("targeted mutation candidate should publish");
6774
6775        let dynamic_error = session
6776            .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
6777                entity: "TargetedMutation".to_string(),
6778                patch: structural_patch(1, 12),
6779            })
6780            .expect_err("dynamic write must enforce the targeted rule");
6781        assert_eq!(
6782            targeted_constraint_id(&dynamic_error),
6783            targeted_rule_id.get()
6784        );
6785
6786        let binding = session
6787            .issue_typed_entity_binding(
6788                ENTITY_SOURCE,
6789                &[
6790                    DynamicTypedFieldBindingRequest::new(
6791                        ID_SOURCE.to_string(),
6792                        DynamicTypedFieldType::Scalar(ScalarType::Nat64),
6793                        false,
6794                    ),
6795                    DynamicTypedFieldBindingRequest::new(
6796                        PROFILE_SOURCE.to_string(),
6797                        DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
6798                        false,
6799                    ),
6800                    DynamicTypedFieldBindingRequest::new(
6801                        UPDATED_AT_SOURCE.to_string(),
6802                        DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
6803                        false,
6804                    ),
6805                ],
6806            )
6807            .expect("targeted typed binding should issue");
6808        let typed_patch = binding
6809            .bind_write_fields(vec![
6810                (
6811                    ID_SOURCE.to_string(),
6812                    DynamicWriteCell::Value(InputValue::Nat64(2)),
6813                ),
6814                (
6815                    PROFILE_SOURCE.to_string(),
6816                    DynamicWriteCell::Value(profile_input(12)),
6817                ),
6818            ])
6819            .expect("targeted typed patch should bind");
6820        let typed_error = session
6821            .execute_trusted_typed_mutation(
6822                &binding,
6823                &DynamicTypedMutation::Insert { patch: typed_patch },
6824            )
6825            .expect_err("typed write must enforce the targeted rule");
6826        assert_eq!(targeted_constraint_id(&typed_error), targeted_rule_id.get());
6827
6828        #[cfg(feature = "sql")]
6829        {
6830            let sql_error = session
6831                .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
6832                .expect_err("SQL default resolution must enforce the targeted rule");
6833            let crate::db::QueryError::Execute(execute) = sql_error else {
6834                panic!("targeted SQL write should fail at shared execution admission");
6835            };
6836            assert_eq!(
6837                targeted_constraint_id(execute.as_internal()),
6838                targeted_rule_id.get()
6839            );
6840        }
6841
6842        session
6843            .execute_trusted_dynamic_mutation_batch(vec![
6844                DynamicMutation::Insert {
6845                    entity: "TargetedMutation".to_string(),
6846                    patch: structural_patch(4, 5),
6847                },
6848                DynamicMutation::Insert {
6849                    entity: "TargetedMutation".to_string(),
6850                    patch: structural_patch(5, 12),
6851                },
6852            ])
6853            .expect_err("one invalid targeted value must reject the whole batch");
6854        assert_eq!(
6855            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
6856            Some(0),
6857            "no frontend or earlier valid batch row may escape targeted admission",
6858        );
6859
6860        let admitted = session
6861            .execute_trusted_dynamic_mutation_batch(vec![
6862                DynamicMutation::Insert {
6863                    entity: "TargetedMutation".to_string(),
6864                    patch: structural_patch(6, 5),
6865                },
6866                DynamicMutation::Insert {
6867                    entity: "TargetedMutation".to_string(),
6868                    patch: structural_patch(7, 10),
6869                },
6870            ])
6871            .expect("compliant targeted values should share one accepted batch");
6872        let [first, second] = admitted.rows.as_slice() else {
6873            panic!("the mixed targeted batch should return two rows");
6874        };
6875        let first_timestamp = first
6876            .get(2)
6877            .expect("the first mixed row should contain its managed timestamp");
6878        assert!(matches!(
6879            first_timestamp,
6880            crate::value::OutputValue::Timestamp(_)
6881        ));
6882        assert_eq!(
6883            second.get(2),
6884            Some(first_timestamp),
6885            "one accepted mixed batch must materialize one managed timestamp",
6886        );
6887        assert_eq!(
6888            DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
6889            Some(2),
6890        );
6891    }
6892}