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