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