Skip to main content

icydb_core/db/session/
write.rs

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