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