1use super::AcceptedSchemaCatalogContext;
8use crate::{
9 db::{
10 DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
11 DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedFieldBindingRequest,
12 DynamicTypedFieldType, DynamicTypedMutation, DynamicTypedStructuralPatch, DynamicWriteCell,
13 commit::{CommitRowOp, database_incarnation_id},
14 data::{
15 AcceptedMutationIntentPatch, AcceptedPreKeyInsert, DecodedDataStoreKey, FieldSlot,
16 RawRow, StructuralRowContract, StructuralSlotReader,
17 canonical_row_from_raw_row_with_accepted_decode_contract,
18 resolve_existing_replace_structural_patch_with_accepted_contract,
19 resolve_insert_structural_patch_with_accepted_contract,
20 resolve_update_structural_patch_with_accepted_contract,
21 },
22 executor::{
23 AcceptedMutationConstraintScheduler, commit_structural_row_ops_with_window_for_path,
24 mutation_key_exists_error,
25 },
26 schema::{
27 AcceptedFieldKind, AcceptedIdentityAllocation, AcceptedRowLayoutRuntimeContract,
28 FieldId, FieldInsertGeneration, IdentityStatementCursor, lower_field_type,
29 output_value_from_runtime,
30 },
31 write_context::{AcceptedWriteContext, MutationMode},
32 },
33 error::InternalError,
34 metrics::sink::{MetricsEvent, SaveMutationKind, record},
35 traits::CanisterKind,
36 types::{CurrentTimestamp, Timestamp},
37 value::{InputValue, Value},
38};
39use icydb_schema::{EntitySourceKey, FieldSourceKey, FieldType, TypeSourceKey};
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42struct AcceptedIdentityInsertField {
43 field_id: FieldId,
44 field_slot: usize,
45 accepted_kind: AcceptedFieldKind,
46}
47
48pub(in crate::db::session) enum AcceptedStructuralMutationTarget {
51 ResolveFromAfterImage,
52 Expected(Box<DecodedDataStoreKey>),
53}
54
55impl AcceptedStructuralMutationTarget {
56 pub(in crate::db::session) fn expected(key: DecodedDataStoreKey) -> Self {
57 Self::Expected(Box::new(key))
58 }
59}
60
61pub(in crate::db::session) enum AcceptedStructuralMutation {
64 Save {
65 mode: MutationMode,
66 target: AcceptedStructuralMutationTarget,
67 patch: AcceptedMutationIntentPatch,
68 },
69 Delete {
70 key: Box<DecodedDataStoreKey>,
71 },
72}
73
74impl AcceptedStructuralMutation {
75 pub(in crate::db::session) const fn save(
76 mode: MutationMode,
77 target: AcceptedStructuralMutationTarget,
78 patch: AcceptedMutationIntentPatch,
79 ) -> Self {
80 Self::Save {
81 mode,
82 target,
83 patch,
84 }
85 }
86
87 pub(in crate::db::session) fn delete(key: DecodedDataStoreKey) -> Self {
88 Self::Delete { key: Box::new(key) }
89 }
90}
91
92struct OrderedAcceptedStructuralMutation {
93 input_ordinal: u32,
94 intent: AcceptedStructuralMutation,
95}
96
97const MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS: usize = 4_096;
98const MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES: usize = 16 * 1024 * 1024;
99const MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES: usize = 1024 * 1024;
100
101fn add_structural_mutation_staged_bytes(
102 total: &mut usize,
103 lengths: impl IntoIterator<Item = usize>,
104) -> Result<(), InternalError> {
105 for length in lengths {
106 *total = total
107 .checked_add(length)
108 .ok_or_else(InternalError::mutation_batch_staged_bytes_exceeded)?;
109 if *total > MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES {
110 return Err(InternalError::mutation_batch_staged_bytes_exceeded());
111 }
112 }
113 Ok(())
114}
115
116fn validate_structural_mutation_result_bytes(encoded_bytes: usize) -> Result<(), InternalError> {
117 if encoded_bytes > MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES {
118 return Err(InternalError::mutation_batch_result_bytes_exceeded());
119 }
120 Ok(())
121}
122
123pub(in crate::db::session) struct AcceptedStructuralMutationRow {
125 values: Vec<Value>,
126 logical_changed: bool,
127}
128
129impl AcceptedStructuralMutationRow {
130 #[cfg(feature = "sql")]
131 pub(in crate::db::session) fn into_values(self) -> Vec<Value> {
132 self.values
133 }
134
135 pub(in crate::db::session) const fn logical_changed(&self) -> bool {
136 self.logical_changed
137 }
138}
139
140const fn dynamic_mutation_mode(request: &DynamicMutation) -> Option<MutationMode> {
141 match request {
142 DynamicMutation::Insert { .. } => Some(MutationMode::Insert),
143 DynamicMutation::Update { .. } => Some(MutationMode::Update),
144 DynamicMutation::Replace { .. } => Some(MutationMode::Replace),
145 DynamicMutation::Delete { .. } => None,
146 }
147}
148
149const fn dynamic_typed_mutation_mode(request: &DynamicTypedMutation) -> MutationMode {
150 match request {
151 DynamicTypedMutation::Insert { .. } => MutationMode::Insert,
152 DynamicTypedMutation::Update { .. } => MutationMode::Update,
153 DynamicTypedMutation::Replace { .. } => MutationMode::Replace,
154 }
155}
156
157const fn dynamic_write_context(operation_timestamp: Timestamp) -> AcceptedWriteContext {
158 AcceptedWriteContext::new(operation_timestamp)
159}
160
161fn insert_key_exists_after_generation(identity_generated: bool) -> InternalError {
162 if identity_generated {
163 InternalError::identity_state_corruption()
164 } else {
165 mutation_key_exists_error()
166 }
167}
168
169fn dynamic_key(
170 entity_tag: crate::types::EntityTag,
171 key: &InputValue,
172) -> Result<DecodedDataStoreKey, InternalError> {
173 let value = key
174 .clone()
175 .try_into_runtime_non_enum()
176 .ok_or_else(InternalError::executor_unsupported)?;
177 DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
178}
179
180fn lower_dynamic_patch(
181 entity_path: &str,
182 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
183 patch: &DynamicStructuralPatch,
184 mode: MutationMode,
185) -> Result<AcceptedMutationIntentPatch, InternalError> {
186 let mut lowered = AcceptedMutationIntentPatch::new();
187 for (field_name, cell) in patch.fields() {
188 let slot = descriptor
189 .field_slot_index_by_name(field_name)
190 .ok_or_else(|| {
191 InternalError::mutation_structural_field_unknown(entity_path, field_name)
192 })?;
193 let field = descriptor
194 .field_for_slot_index(slot)
195 .ok_or_else(InternalError::executor_invariant)?;
196 if !matches!(cell, DynamicWriteCell::Omitted)
197 && (field.write_policy().insert_generation().is_some()
198 || field.write_policy().write_management().is_some())
199 {
200 return Err(InternalError::mutation_database_owned_field_explicit(
201 entity_path,
202 field.name(),
203 ));
204 }
205 let slot = FieldSlot::from_validated_index(slot);
206 lowered = match cell {
207 DynamicWriteCell::Omitted => lowered,
208 DynamicWriteCell::Default => match mode {
209 MutationMode::Insert | MutationMode::Replace => {
210 lowered.set_explicit_insert_default(slot)
211 }
212 MutationMode::Update => lowered.set_explicit_update_default(slot),
213 },
214 DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
215 DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
216 };
217 }
218 Ok(lowered)
219}
220
221fn lower_dynamic_mutation_intent(
222 entity_tag: crate::types::EntityTag,
223 entity_path: &str,
224 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
225 request: &DynamicMutation,
226) -> Result<(AcceptedStructuralMutation, Option<SaveMutationKind>), InternalError> {
227 match request {
228 DynamicMutation::Insert { patch, .. } => Ok((
229 AcceptedStructuralMutation::save(
230 MutationMode::Insert,
231 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
232 lower_dynamic_patch(entity_path, descriptor, patch, MutationMode::Insert)?,
233 ),
234 Some(SaveMutationKind::Insert),
235 )),
236 DynamicMutation::Update { key, patch, .. }
237 | DynamicMutation::Replace { key, patch, .. } => {
238 let mode =
239 dynamic_mutation_mode(request).ok_or_else(InternalError::executor_invariant)?;
240 let kind = match mode {
241 MutationMode::Insert => SaveMutationKind::Insert,
242 MutationMode::Replace => SaveMutationKind::Replace,
243 MutationMode::Update => SaveMutationKind::Update,
244 };
245 Ok((
246 AcceptedStructuralMutation::save(
247 mode,
248 AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
249 lower_dynamic_patch(entity_path, descriptor, patch, mode)?,
250 ),
251 Some(kind),
252 ))
253 }
254 DynamicMutation::Delete { key, .. } => Ok((
255 AcceptedStructuralMutation::delete(dynamic_key(entity_tag, key)?),
256 None,
257 )),
258 }
259}
260
261fn lower_typed_patch(
262 entity_path: &str,
263 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
264 patch: &DynamicTypedStructuralPatch,
265 mode: MutationMode,
266) -> Result<AcceptedMutationIntentPatch, InternalError> {
267 let mut lowered = AcceptedMutationIntentPatch::new();
268 for (field_id, slot, cell) in patch.fields() {
269 let slot_index = usize::from(*slot);
270 let field = descriptor
271 .field_for_slot_index(slot_index)
272 .ok_or_else(InternalError::store_invariant)?;
273 if field.field_id().get() != *field_id {
274 return Err(InternalError::store_invariant());
275 }
276 if !matches!(cell, DynamicWriteCell::Omitted)
277 && (field.write_policy().insert_generation().is_some()
278 || field.write_policy().write_management().is_some())
279 {
280 return Err(InternalError::mutation_database_owned_field_explicit(
281 entity_path,
282 field.name(),
283 ));
284 }
285 let slot = FieldSlot::from_validated_index(slot_index);
286 lowered = match cell {
287 DynamicWriteCell::Omitted => lowered,
288 DynamicWriteCell::Default => match mode {
289 MutationMode::Insert | MutationMode::Replace => {
290 lowered.set_explicit_insert_default(slot)
291 }
292 MutationMode::Update => lowered.set_explicit_update_default(slot),
293 },
294 DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::Null),
295 DynamicWriteCell::Value(value) => lowered.set_authored(slot, value.clone()),
296 };
297 }
298 Ok(lowered)
299}
300
301fn preserve_dynamic_replacement_identity(
302 key: &DecodedDataStoreKey,
303 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
304 mut patch: AcceptedMutationIntentPatch,
305) -> Result<AcceptedMutationIntentPatch, InternalError> {
306 let primary_key_slots = descriptor.primary_key_slot_indices();
307 let runtime_key = key.primary_key_runtime_value();
308 let components = match runtime_key {
309 Value::List(values) if primary_key_slots.len() > 1 => values,
310 value if primary_key_slots.len() == 1 => vec![value],
311 _ => return Err(InternalError::executor_invariant()),
312 };
313 if components.len() != primary_key_slots.len() {
314 return Err(InternalError::executor_invariant());
315 }
316
317 for (slot, value) in primary_key_slots.iter().copied().zip(components) {
318 let _ = descriptor
319 .field_for_slot_index(slot)
320 .ok_or_else(InternalError::executor_invariant)?;
321 let has_explicit_intent = patch
322 .entries()
323 .iter()
324 .any(|entry| entry.slot().index() == slot);
325 if has_explicit_intent {
326 continue;
327 }
328 let value = InputValue::try_from_runtime_non_enum(&value)
329 .ok_or_else(InternalError::executor_invariant)?;
330 patch =
331 patch.set_preserved_replacement_identity(FieldSlot::from_validated_index(slot), value);
332 }
333
334 Ok(patch)
335}
336
337fn accepted_identity_insert_field(
341 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
342) -> Result<Option<AcceptedIdentityInsertField>, InternalError> {
343 let mut identity = None;
344 for field in descriptor.fields() {
345 if field.write_policy().insert_generation() != Some(FieldInsertGeneration::Identity) {
346 continue;
347 }
348 let field_slot = usize::from(field.slot().get());
349 if identity
350 .replace(AcceptedIdentityInsertField {
351 field_id: field.field_id(),
352 field_slot,
353 accepted_kind: field.kind().clone(),
354 })
355 .is_some()
356 || descriptor.primary_key_slot_indices() != [field_slot]
357 {
358 return Err(InternalError::identity_corruption());
359 }
360 }
361 Ok(identity)
362}
363
364fn checked_pre_key_candidate_count(count: usize) -> Result<u32, InternalError> {
365 u32::try_from(count).map_err(|_| InternalError::identity_candidate_count_exhausted())
366}
367
368fn validate_identity_materialization(
369 entity_tag: crate::types::EntityTag,
370 identity_field: &AcceptedIdentityInsertField,
371 candidate: &AcceptedPreKeyInsert,
372 allocation: &AcceptedIdentityAllocation,
373 data_key: &DecodedDataStoreKey,
374 reader: &StructuralSlotReader<'_>,
375) -> Result<(), InternalError> {
376 let owner = allocation.owner();
377 let slot_value = reader.required_cached_value(identity_field.field_slot)?;
378 if candidate.entity_tag() != entity_tag
379 || candidate.input_ordinal() != allocation.input_ordinal()
380 || owner.entity_tag() != entity_tag
381 || owner.field_id() != identity_field.field_id
382 || allocation.field_slot() != identity_field.field_slot
383 || slot_value != allocation.value()
384 || data_key.primary_key_runtime_value() != *allocation.value()
385 {
386 return Err(InternalError::identity_corruption());
387 }
388 Ok(())
389}
390
391fn data_key_from_row(
392 entity_tag: crate::types::EntityTag,
393 contract: &StructuralRowContract,
394 row: &RawRow,
395) -> Result<DecodedDataStoreKey, InternalError> {
396 let reader =
397 StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
398 let values = contract
399 .primary_key_slot_indices()
400 .iter()
401 .map(|slot| reader.required_cached_value(*slot).cloned())
402 .collect::<Result<Vec<_>, _>>()?;
403 let value = match values.as_slice() {
404 [value] => value.clone(),
405 _ => Value::List(values),
406 };
407 DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
408}
409
410#[cfg(feature = "sql")]
411pub(in crate::db::session) fn structural_data_key_from_runtime_values(
412 entity_tag: crate::types::EntityTag,
413 values: Vec<Value>,
414) -> Result<DecodedDataStoreKey, InternalError> {
415 let value = match values.as_slice() {
416 [value] => value.clone(),
417 _ => Value::List(values),
418 };
419 DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
420}
421
422fn validated_existing_row(
423 store: crate::db::registry::StoreHandle,
424 data_key: &DecodedDataStoreKey,
425 contract: &StructuralRowContract,
426) -> Result<Option<RawRow>, InternalError> {
427 let raw_key = data_key.to_raw()?;
428 let row = store.with_data(|data| data.get(&raw_key));
429 if let Some(row) = row.as_ref() {
430 let reader =
431 StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(row, contract)?;
432 reader.validate_primary_key(data_key)?;
433 }
434 Ok(row)
435}
436
437fn prepare_dynamic_mutation_result(
438 catalog: &AcceptedSchemaCatalogContext,
439 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
440 rows: Vec<AcceptedStructuralMutationRow>,
441 enforce_mixed_batch_result_bound: bool,
442) -> Result<DynamicMutationResult, InternalError> {
443 let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
444 total
445 .checked_add(u32::from(row.logical_changed()))
446 .ok_or_else(InternalError::executor_invariant)
447 })?;
448 let columns = descriptor
449 .fields()
450 .iter()
451 .map(|field| field.name().to_string())
452 .collect();
453 let rows = rows
454 .into_iter()
455 .map(|row| {
456 row.values
457 .iter()
458 .map(|value| {
459 output_value_from_runtime(catalog.enum_catalog(), value)
460 .map_err(|_| InternalError::store_invariant())
461 })
462 .collect::<Result<Vec<_>, _>>()
463 })
464 .collect::<Result<Vec<_>, _>>()?;
465 let result = DynamicMutationResult {
466 entity: catalog.snapshot().entity_name().to_string(),
467 columns,
468 rows,
469 affected_rows,
470 };
471 if enforce_mixed_batch_result_bound {
472 let encoded =
473 candid::encode_one(&result).map_err(|_| InternalError::executor_invariant())?;
474 validate_structural_mutation_result_bytes(encoded.len())?;
475 }
476 Ok(result)
477}
478
479fn dynamic_typed_field_type(
480 field_type: DynamicTypedFieldType,
481) -> Result<FieldType, DynamicTypedBindingError> {
482 match field_type {
483 DynamicTypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
484 DynamicTypedFieldType::List(item) => {
485 Ok(FieldType::List(Box::new(dynamic_typed_field_type(*item)?)))
486 }
487 DynamicTypedFieldType::Named(source_key) => TypeSourceKey::try_new(source_key)
488 .map(FieldType::Named)
489 .map_err(|_| DynamicTypedBindingError::FieldUnavailable),
490 }
491}
492
493fn typed_adapter_field_kind_matches(
494 accepted: &AcceptedFieldKind,
495 expected: &AcceptedFieldKind,
496) -> bool {
497 if accepted == expected {
498 return true;
499 }
500 match (accepted, expected) {
501 (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
502 typed_adapter_field_kind_matches(key_kind, expected)
503 }
504 (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
505 typed_adapter_field_kind_matches(accepted, expected)
506 }
507 _ => false,
508 }
509}
510
511impl<C: CanisterKind> DbSession<C> {
512 pub fn issue_typed_entity_binding(
514 &self,
515 entity_source_key: &str,
516 field_requests: &[DynamicTypedFieldBindingRequest],
517 ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
518 let entity_source = EntitySourceKey::try_new(entity_source_key)
519 .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?;
520 let field_requests = field_requests
521 .iter()
522 .map(|request| {
523 Ok((
524 FieldSourceKey::try_new(request.source_key.clone())
525 .map_err(|_| DynamicTypedBindingError::FieldUnavailable)?,
526 dynamic_typed_field_type(request.field_type.clone())?,
527 request.nullable,
528 ))
529 })
530 .collect::<Result<Vec<_>, DynamicTypedBindingError>>()?;
531 let catalog = self
532 .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
533 .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
534 let identity = catalog.identity();
535 if identity.entity_path() != entity_source.as_str() {
536 return Err(InternalError::store_invariant().into());
537 }
538 let store = self.db.recovered_store(identity.store_path())?;
539 let bundle = store
540 .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
541 .ok_or_else(InternalError::store_invariant)?;
542 let entity_tag = identity.entity_tag();
543 if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
544 || bundle.revision() != catalog.revision()
545 {
546 return Err(InternalError::store_invariant().into());
547 }
548 let snapshot = bundle
549 .entity_snapshots()
550 .get(&entity_tag)
551 .ok_or_else(InternalError::store_invariant)?;
552 let descriptor =
553 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
554 let mut fields = Vec::with_capacity(field_requests.len());
555 for (source, field_type, nullable) in &field_requests {
556 let field_id = bundle
557 .source_bindings()
558 .field(entity_tag, source)
559 .ok_or(DynamicTypedBindingError::FieldUnavailable)?;
560 let field = snapshot
561 .fields()
562 .iter()
563 .find(|field| field.id() == field_id)
564 .ok_or_else(InternalError::store_invariant)?;
565 let runtime_field = descriptor
566 .field_for_slot_index(usize::from(field.slot().get()))
567 .ok_or_else(InternalError::store_invariant)?;
568 if runtime_field.field_id() != field_id {
569 return Err(InternalError::store_invariant().into());
570 }
571 let expected_kind = lower_field_type(field_type, |source| {
572 bundle.source_bindings().named_type(source)
573 })
574 .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
575 if field.nullable() != *nullable
576 || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
577 {
578 return Err(DynamicTypedBindingError::IncompatibleField);
579 }
580 fields.push((
581 source.as_str().to_string(),
582 field_id.get(),
583 field.slot().get(),
584 field.name().to_string(),
585 ));
586 }
587 let adapter_names = bundle.typed_adapter_names()?;
588
589 DynamicTypedEntityBinding::new(
590 database_incarnation_id()?.to_bytes(),
591 entity_source.as_str().to_string(),
592 snapshot.entity_name().to_string(),
593 entity_tag.value(),
594 catalog.revision().get(),
595 catalog.fingerprint(),
596 descriptor.current_layout_version().get(),
597 fields,
598 adapter_names.named_types,
599 adapter_names.enum_variants,
600 adapter_names.composite_fields,
601 )
602 .map_err(Into::into)
603 }
604
605 pub(in crate::db::session) fn current_typed_entity_binding_catalog(
606 &self,
607 binding: &DynamicTypedEntityBinding,
608 ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
609 if database_incarnation_id()?.to_bytes() != binding.database_incarnation {
610 return Ok(None);
611 }
612 let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
613 binding.entity_source.as_str(),
614 )?
615 else {
616 return Ok(None);
617 };
618 let descriptor =
619 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
620 let identity = catalog.identity();
621 if identity.entity_path() != binding.entity_source.as_str()
622 || identity.entity_tag().value() != binding.entity_tag
623 || catalog.revision().get() != binding.accepted_revision
624 || catalog.fingerprint() != binding.accepted_fingerprint
625 || descriptor.current_layout_version().get() != binding.entity_generation
626 {
627 return Ok(None);
628 }
629 let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
630 .map_err(|_| InternalError::store_invariant())?;
631 let store = self.db.recovered_store(identity.store_path())?;
632 let bundle = store
633 .with_schema(crate::db::schema::SchemaStore::current_accepted_schema_bundle)?
634 .ok_or_else(InternalError::store_invariant)?;
635 if bundle.revision() != catalog.revision()
636 || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
637 {
638 return Ok(None);
639 }
640 let snapshot = bundle
641 .entity_snapshots()
642 .get(&identity.entity_tag())
643 .ok_or_else(InternalError::store_invariant)?;
644 for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings() {
645 let source = FieldSourceKey::try_new(source_key)
646 .map_err(|_| InternalError::store_invariant())?;
647 let Some(field_id) = bundle
648 .source_bindings()
649 .field(identity.entity_tag(), &source)
650 else {
651 return Ok(None);
652 };
653 let Some(field) = snapshot
654 .fields()
655 .iter()
656 .find(|field| field.id() == field_id)
657 else {
658 return Err(InternalError::store_invariant());
659 };
660 if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
661 return Ok(None);
662 }
663 }
664 Ok(Some(catalog))
665 }
666
667 pub fn typed_entity_binding_is_current(
669 &self,
670 binding: &DynamicTypedEntityBinding,
671 ) -> Result<bool, InternalError> {
672 self.current_typed_entity_binding_catalog(binding)
673 .map(|catalog| catalog.is_some())
674 }
675
676 #[cfg(feature = "sql")]
679 pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
680 &self,
681 catalog: &AcceptedSchemaCatalogContext,
682 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
683 keys: Vec<DecodedDataStoreKey>,
684 precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
685 ) -> Result<Vec<Vec<Value>>, InternalError> {
686 let mutations = keys
687 .into_iter()
688 .map(AcceptedStructuralMutation::delete)
689 .collect();
690 self.execute_accepted_structural_mutation_batch_inner(
691 catalog,
692 descriptor,
693 mutations,
694 Timestamp::now(),
695 false,
696 |rows| {
697 let rows = rows
698 .into_iter()
699 .map(AcceptedStructuralMutationRow::into_values)
700 .collect::<Vec<_>>();
701 precommit_validation(rows.as_slice())?;
702 Ok(rows)
703 },
704 )
705 }
706
707 pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
715 &self,
716 catalog: &AcceptedSchemaCatalogContext,
717 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
718 mutations: Vec<AcceptedStructuralMutation>,
719 operation_timestamp: Timestamp,
720 precommit_preparation: impl FnOnce(
721 Vec<AcceptedStructuralMutationRow>,
722 ) -> Result<T, InternalError>,
723 ) -> Result<T, InternalError> {
724 self.execute_accepted_structural_mutation_batch_inner(
725 catalog,
726 descriptor,
727 mutations,
728 operation_timestamp,
729 false,
730 precommit_preparation,
731 )
732 }
733
734 #[cfg(feature = "sql")]
736 pub(in crate::db::session) fn execute_accepted_structural_update_prefix(
737 &self,
738 catalog: &AcceptedSchemaCatalogContext,
739 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
740 mutations: Vec<AcceptedStructuralMutation>,
741 operation_timestamp: Timestamp,
742 ) -> Result<usize, InternalError> {
743 self.execute_accepted_structural_mutation_batch_inner(
744 catalog,
745 descriptor,
746 mutations,
747 operation_timestamp,
748 true,
749 |rows| Ok(rows.len()),
750 )
751 }
752
753 #[expect(
754 clippy::too_many_lines,
755 reason = "one phased owner keeps accepted authority, mutation context, precommit preparation, output capture, and commit staging inseparable"
756 )]
757 fn execute_accepted_structural_mutation_batch_inner<T>(
758 &self,
759 catalog: &AcceptedSchemaCatalogContext,
760 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
761 mutations: Vec<AcceptedStructuralMutation>,
762 operation_timestamp: Timestamp,
763 largest_journaled_prefix: bool,
764 precommit_preparation: impl FnOnce(
765 Vec<AcceptedStructuralMutationRow>,
766 ) -> Result<T, InternalError>,
767 ) -> Result<T, InternalError> {
768 let identity = catalog.identity();
769 let entity_path = identity.entity_path();
770 let store_path = identity.store_path();
771 let row_decode_contract =
772 descriptor.row_decode_contract(catalog.value_catalog_handle().clone());
773 let row_contract = StructuralRowContract::from_accepted_decode_contract(
774 entity_path,
775 row_decode_contract.clone(),
776 );
777 let store = self.db.recovered_store(store_path)?;
778 let write_context = dynamic_write_context(operation_timestamp);
779 let identity_field = accepted_identity_insert_field(descriptor)?;
780 let identity_incarnation = identity_field
781 .as_ref()
782 .map(|_| database_incarnation_id())
783 .transpose()?;
784 if mutations.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
785 return Err(InternalError::mutation_batch_too_many_items());
786 }
787 let identity_candidate_count = mutations
788 .iter()
789 .filter(|mutation| {
790 matches!(
791 mutation,
792 AcceptedStructuralMutation::Save {
793 mode: MutationMode::Insert,
794 target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
795 ..
796 }
797 )
798 })
799 .count();
800 let _ = checked_pre_key_candidate_count(identity_candidate_count)?;
801 let mutations = mutations
802 .into_iter()
803 .enumerate()
804 .map(|(input_index, intent)| {
805 u32::try_from(input_index)
806 .map(|input_ordinal| OrderedAcceptedStructuralMutation {
807 input_ordinal,
808 intent,
809 })
810 .map_err(|_| InternalError::mutation_batch_too_many_items())
811 })
812 .collect::<Result<Vec<_>, _>>()?;
813 let mut identity_cursor: Option<IdentityStatementCursor> = None;
814 let mut identity_insert_ordinal = 0_u32;
815 let mut scheduler = AcceptedMutationConstraintScheduler::new(
816 entity_path,
817 row_decode_contract.clone(),
818 catalog.fingerprint(),
819 catalog.accepted_row_constraints(),
820 mutations.len(),
821 );
822 let mut output = Vec::with_capacity(mutations.len());
823 let mut staged_bytes = 0_usize;
824
825 for mutation in mutations {
826 let batch_input_ordinal = mutation.input_ordinal;
827 let mutation = mutation.intent;
828 let AcceptedStructuralMutation::Save {
829 mode,
830 target,
831 patch: authored_patch,
832 } = mutation
833 else {
834 let AcceptedStructuralMutation::Delete { key } = mutation else {
835 return Err(InternalError::executor_invariant());
836 };
837 let before = validated_existing_row(store, &key, &row_contract)?
838 .ok_or_else(|| InternalError::store_not_found(&key))?;
839 let raw_key = key.to_raw()?;
840 let canonical_before = canonical_row_from_raw_row_with_accepted_decode_contract(
841 entity_path,
842 row_decode_contract.clone(),
843 &before,
844 )?;
845 add_structural_mutation_staged_bytes(
846 &mut staged_bytes,
847 [
848 raw_key.as_bytes().len(),
849 canonical_before.as_raw_row().as_bytes().len(),
850 ],
851 )?;
852 scheduler.schedule_delete(CommitRowOp::new(
853 entity_path,
854 raw_key,
855 Some(canonical_before.as_raw_row().as_bytes().to_vec()),
856 None,
857 catalog.fingerprint(),
858 ))?;
859 let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
860 canonical_before.as_raw_row(),
861 &row_contract,
862 )?;
863 let mut values = Vec::with_capacity(descriptor.fields().len());
864 for field in descriptor.fields() {
865 values.push(
866 reader
867 .required_cached_value(usize::from(field.slot().get()))?
868 .clone(),
869 );
870 }
871 output.push(AcceptedStructuralMutationRow {
872 values,
873 logical_changed: true,
874 });
875 continue;
876 };
877 let (expected_key, pre_key_insert, mut keyed_patch) = match target {
878 AcceptedStructuralMutationTarget::ResolveFromAfterImage => {
879 let candidate_ordinal =
880 if identity_field.is_some() && matches!(mode, MutationMode::Insert) {
881 identity_insert_ordinal
882 } else {
883 batch_input_ordinal
884 };
885 (
886 None,
887 Some(AcceptedPreKeyInsert::new(
888 identity.entity_tag(),
889 authored_patch,
890 candidate_ordinal,
891 )),
892 None,
893 )
894 }
895 AcceptedStructuralMutationTarget::Expected(key) => {
896 (Some(*key), None, Some(authored_patch))
897 }
898 };
899 if matches!(mode, MutationMode::Replace)
900 && let Some(key) = expected_key.as_ref()
901 {
902 let patch = keyed_patch
903 .take()
904 .ok_or_else(InternalError::executor_invariant)?;
905 keyed_patch = Some(preserve_dynamic_replacement_identity(
906 key, descriptor, patch,
907 )?);
908 }
909 let patch = pre_key_insert
910 .as_ref()
911 .map(AcceptedPreKeyInsert::fields)
912 .or(keyed_patch.as_ref())
913 .ok_or_else(InternalError::executor_invariant)?;
914 let before = expected_key
915 .as_ref()
916 .map(|key| validated_existing_row(store, key, &row_contract))
917 .transpose()?
918 .flatten();
919 match mode {
920 MutationMode::Insert if before.is_some() => {
921 return Err(mutation_key_exists_error());
922 }
923 MutationMode::Update if before.is_none() => {
924 let key = expected_key
925 .as_ref()
926 .ok_or_else(InternalError::executor_invariant)?;
927 return Err(InternalError::store_not_found(key));
928 }
929 MutationMode::Insert | MutationMode::Replace | MutationMode::Update => {}
930 }
931
932 let identity_allocation = if let Some(identity_field) = identity_field.as_ref()
933 && matches!(mode, MutationMode::Insert)
934 && before.is_none()
935 {
936 let candidate = pre_key_insert.as_ref().ok_or_else(|| {
937 let field_name = descriptor
938 .field_for_slot_index(identity_field.field_slot)
939 .map_or("", |field| field.name());
940 InternalError::mutation_database_owned_field_explicit(entity_path, field_name)
941 })?;
942 if identity_cursor.is_none() {
943 let incarnation = identity_incarnation
944 .ok_or_else(InternalError::identity_state_corruption)?;
945 identity_cursor = Some(store.with_schema(|schema_store| {
946 schema_store.identity_statement_cursor(
947 incarnation,
948 identity.entity_tag(),
949 identity_field.field_id,
950 &identity_field.accepted_kind,
951 )
952 })?);
953 }
954 let allocation = identity_cursor
955 .as_mut()
956 .ok_or_else(InternalError::identity_state_corruption)?
957 .allocate(identity_field.field_slot, candidate.input_ordinal())?;
958 identity_insert_ordinal = identity_insert_ordinal
959 .checked_add(1)
960 .ok_or_else(InternalError::identity_candidate_count_exhausted)?;
961 Some(allocation)
962 } else if let Some(identity_field) = identity_field.as_ref()
963 && matches!(mode, MutationMode::Replace)
964 && before.is_none()
965 {
966 let field_name = descriptor
967 .field_for_slot_index(identity_field.field_slot)
968 .map_or("", |field| field.name());
969 return Err(InternalError::mutation_database_owned_field_explicit(
970 entity_path,
971 field_name,
972 ));
973 } else {
974 None
975 };
976
977 let resolved = match (mode, before.as_ref()) {
978 (MutationMode::Insert | MutationMode::Replace, None) => {
979 resolve_insert_structural_patch_with_accepted_contract(
980 entity_path,
981 row_decode_contract.clone(),
982 catalog.fingerprint(),
983 catalog.accepted_row_constraints(),
984 patch,
985 write_context,
986 identity_allocation.as_ref(),
987 )?
988 }
989 (MutationMode::Update, Some(before)) => {
990 resolve_update_structural_patch_with_accepted_contract(
991 entity_path,
992 row_decode_contract.clone(),
993 catalog.fingerprint(),
994 catalog.accepted_row_constraints(),
995 before,
996 patch,
997 write_context,
998 )?
999 }
1000 (MutationMode::Replace, Some(before)) => {
1001 resolve_existing_replace_structural_patch_with_accepted_contract(
1002 entity_path,
1003 row_decode_contract.clone(),
1004 catalog.fingerprint(),
1005 catalog.accepted_row_constraints(),
1006 before,
1007 patch,
1008 write_context,
1009 )?
1010 }
1011 (MutationMode::Insert, Some(_)) | (MutationMode::Update, None) => {
1012 return Err(InternalError::executor_invariant());
1013 }
1014 };
1015 let (after, provenance) = resolved.into_parts();
1016 let reader = StructuralSlotReader::from_raw_row_with_validated_borrowed_contract(
1017 after.as_raw_row(),
1018 &row_contract,
1019 )?;
1020 let data_key = match expected_key {
1021 Some(key) => {
1022 reader.validate_primary_key(&key)?;
1023 key
1024 }
1025 None => {
1026 data_key_from_row(identity.entity_tag(), &row_contract, after.as_raw_row())?
1027 }
1028 };
1029 if let Some(allocation) = identity_allocation.as_ref() {
1030 validate_identity_materialization(
1031 identity.entity_tag(),
1032 identity_field
1033 .as_ref()
1034 .ok_or_else(InternalError::identity_corruption)?,
1035 pre_key_insert
1036 .as_ref()
1037 .ok_or_else(InternalError::identity_corruption)?,
1038 allocation,
1039 &data_key,
1040 &reader,
1041 )?;
1042 }
1043 if matches!(mode, MutationMode::Insert)
1044 && validated_existing_row(store, &data_key, &row_contract)?.is_some()
1045 {
1046 return Err(insert_key_exists_after_generation(
1047 identity_allocation.is_some(),
1048 ));
1049 }
1050 let raw_key = data_key.to_raw()?;
1051 let canonical_before = before
1052 .as_ref()
1053 .map(|before| {
1054 canonical_row_from_raw_row_with_accepted_decode_contract(
1055 entity_path,
1056 row_decode_contract.clone(),
1057 before,
1058 )
1059 })
1060 .transpose()?;
1061 let logical_changed = canonical_before.as_ref().is_none_or(|before| {
1062 before.as_raw_row().as_bytes() != after.as_raw_row().as_bytes()
1063 });
1064 let physical_changed = before
1065 .as_ref()
1066 .is_none_or(|before| before.as_bytes() != after.as_raw_row().as_bytes());
1067 add_structural_mutation_staged_bytes(
1068 &mut staged_bytes,
1069 [
1070 raw_key.as_bytes().len(),
1071 canonical_before
1072 .as_ref()
1073 .map_or(0, |before| before.as_raw_row().as_bytes().len()),
1074 after.as_raw_row().as_bytes().len(),
1075 ],
1076 )?;
1077 let row_op = physical_changed.then(|| {
1078 CommitRowOp::new(
1079 entity_path,
1080 raw_key.clone(),
1081 canonical_before
1082 .as_ref()
1083 .map(|before| before.as_raw_row().as_bytes().to_vec()),
1084 Some(after.as_raw_row().as_bytes().to_vec()),
1085 catalog.fingerprint(),
1086 )
1087 });
1088 scheduler.schedule_save_after_image(
1089 mode,
1090 &data_key,
1091 after.as_raw_row(),
1092 provenance.as_slice(),
1093 row_op,
1094 )?;
1095 if physical_changed {
1096 #[cfg(feature = "sql")]
1097 if largest_journaled_prefix
1098 && !crate::db::commit::journaled_row_ops_fit_commit_window(scheduler.rows())
1099 {
1100 scheduler.pop_last_save_row()?;
1101 if output.is_empty() {
1102 return Err(InternalError::query_sql_write_boundary(
1103 icydb_diagnostic_code::SqlWriteBoundaryCode::ResumableUpdateSingleRowResourceExceeded,
1104 ));
1105 }
1106 break;
1107 }
1108 }
1109
1110 let mut values = Vec::with_capacity(descriptor.fields().len());
1111 for field in descriptor.fields() {
1112 values.push(
1113 reader
1114 .required_cached_value(usize::from(field.slot().get()))?
1115 .clone(),
1116 );
1117 }
1118 output.push(AcceptedStructuralMutationRow {
1119 values,
1120 logical_changed,
1121 });
1122 }
1123
1124 #[cfg(not(feature = "sql"))]
1125 let _ = largest_journaled_prefix;
1126
1127 let batch = scheduler.finish();
1128 let prepared = precommit_preparation(output)?;
1129 let identity_ranges = identity_cursor
1130 .map(IdentityStatementCursor::into_range_advance)
1131 .transpose()?
1132 .into_iter()
1133 .flatten()
1134 .collect::<Vec<_>>();
1135 if batch.is_empty() && !identity_ranges.is_empty() {
1136 return Err(InternalError::identity_corruption());
1137 }
1138 if !batch.is_empty() {
1139 commit_structural_row_ops_with_window_for_path(
1140 &self.db,
1141 entity_path,
1142 batch,
1143 identity_ranges,
1144 "accepted_structural_batch_apply",
1145 )?;
1146 }
1147 Ok(prepared)
1148 }
1149
1150 fn execute_one_accepted_save_mutation(
1151 &self,
1152 catalog: &AcceptedSchemaCatalogContext,
1153 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
1154 mode: MutationMode,
1155 target: AcceptedStructuralMutationTarget,
1156 patch: AcceptedMutationIntentPatch,
1157 ) -> Result<DynamicMutationResult, InternalError> {
1158 let identity = catalog.identity();
1159 let entity_path = identity.entity_path();
1160 let result = self.execute_accepted_structural_save_batch(
1161 catalog,
1162 descriptor,
1163 vec![AcceptedStructuralMutation::save(mode, target, patch)],
1164 Timestamp::now(),
1165 |rows| prepare_dynamic_mutation_result(catalog, descriptor, rows, false),
1166 )?;
1167 record(MetricsEvent::SaveMutation {
1168 entity_path: entity_path.into(),
1169 kind: match mode {
1170 MutationMode::Insert => SaveMutationKind::Insert,
1171 MutationMode::Replace => SaveMutationKind::Replace,
1172 MutationMode::Update => SaveMutationKind::Update,
1173 },
1174 rows_touched: u64::from(result.affected_rows),
1175 });
1176 Ok(result)
1177 }
1178
1179 pub fn execute_trusted_dynamic_mutation(
1186 &self,
1187 request: &DynamicMutation,
1188 ) -> Result<DynamicMutationResult, InternalError> {
1189 self.execute_trusted_dynamic_mutation_batch_with_result_policy(vec![request.clone()], false)
1190 }
1191
1192 pub fn execute_trusted_dynamic_mutation_batch(
1198 &self,
1199 requests: Vec<DynamicMutation>,
1200 ) -> Result<DynamicMutationResult, InternalError> {
1201 self.execute_trusted_dynamic_mutation_batch_with_result_policy(requests, true)
1202 }
1203
1204 fn execute_trusted_dynamic_mutation_batch_with_result_policy(
1205 &self,
1206 requests: Vec<DynamicMutation>,
1207 enforce_mixed_batch_result_bound: bool,
1208 ) -> Result<DynamicMutationResult, InternalError> {
1209 if requests.is_empty() {
1210 return Err(InternalError::mutation_batch_empty());
1211 }
1212 if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1213 return Err(InternalError::mutation_batch_too_many_items());
1214 }
1215 let first = requests
1216 .first()
1217 .ok_or_else(InternalError::mutation_batch_empty)?;
1218 if first.entity().is_empty() {
1219 return Err(InternalError::executor_unsupported());
1220 }
1221 let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
1222 let accepted_identity = catalog.identity();
1223 let descriptor =
1224 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1225 let mut mutations = Vec::with_capacity(requests.len());
1226 let mut save_kinds = Vec::with_capacity(requests.len());
1227
1228 for request in &requests {
1229 if request.entity().is_empty() {
1230 return Err(InternalError::executor_unsupported());
1231 }
1232 let item_catalog =
1233 self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
1234 if item_catalog.identity() != accepted_identity {
1235 return Err(InternalError::mutation_batch_entity_mismatch());
1236 }
1237 let (mutation, save_kind) = lower_dynamic_mutation_intent(
1238 accepted_identity.entity_tag(),
1239 accepted_identity.entity_path(),
1240 &descriptor,
1241 request,
1242 )?;
1243 mutations.push(mutation);
1244 save_kinds.push(save_kind);
1245 }
1246
1247 let entity_path = accepted_identity.entity_path_handle();
1248 let (result, metrics) = self.execute_accepted_structural_mutation_batch_inner(
1249 &catalog,
1250 &descriptor,
1251 mutations,
1252 Timestamp::now(),
1253 false,
1254 |rows| {
1255 if rows.len() != save_kinds.len() {
1256 return Err(InternalError::executor_invariant());
1257 }
1258 let metrics = rows
1259 .iter()
1260 .zip(save_kinds)
1261 .filter_map(|(row, kind)| kind.map(|kind| (kind, row.logical_changed())))
1262 .collect::<Vec<_>>();
1263 let result = prepare_dynamic_mutation_result(
1264 &catalog,
1265 &descriptor,
1266 rows,
1267 enforce_mixed_batch_result_bound,
1268 )?;
1269 Ok((result, metrics))
1270 },
1271 )?;
1272 for (kind, logical_changed) in metrics {
1273 record(MetricsEvent::SaveMutation {
1274 entity_path: entity_path.clone(),
1275 kind,
1276 rows_touched: u64::from(logical_changed),
1277 });
1278 }
1279 Ok(result)
1280 }
1281
1282 #[doc(hidden)]
1285 pub fn execute_trusted_typed_mutation(
1286 &self,
1287 binding: &DynamicTypedEntityBinding,
1288 request: &DynamicTypedMutation,
1289 ) -> Result<Option<DynamicMutationResult>, InternalError> {
1290 let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
1291 return Ok(None);
1292 };
1293 let identity = catalog.identity();
1294 let descriptor =
1295 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1296 let mode = dynamic_typed_mutation_mode(request);
1297 let (target, patch) = match request {
1298 DynamicTypedMutation::Insert { patch } => (
1299 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1300 patch,
1301 ),
1302 DynamicTypedMutation::Update { key, patch }
1303 | DynamicTypedMutation::Replace { key, patch } => (
1304 AcceptedStructuralMutationTarget::expected(dynamic_key(
1305 identity.entity_tag(),
1306 key,
1307 )?),
1308 patch,
1309 ),
1310 };
1311 if !patch.is_bound_to(binding) {
1312 return Ok(None);
1313 }
1314 let patch = lower_typed_patch(identity.entity_path(), &descriptor, patch, mode)?;
1315 self.execute_one_accepted_save_mutation(&catalog, &descriptor, mode, target, patch)
1316 .map(Some)
1317 }
1318
1319 pub fn execute_trusted_dynamic_insert_batch(
1325 &self,
1326 entity: &str,
1327 patches: Vec<DynamicStructuralPatch>,
1328 ) -> Result<DynamicMutationResult, InternalError> {
1329 let mutations = patches
1330 .into_iter()
1331 .map(|patch| DynamicMutation::Insert {
1332 entity: entity.to_string(),
1333 patch,
1334 })
1335 .collect();
1336 self.execute_trusted_dynamic_mutation_batch_with_result_policy(mutations, false)
1337 }
1338}
1339
1340#[cfg(test)]
1341mod typed_adapter_tests {
1342 use super::{
1343 AcceptedFieldKind, DbSession, DynamicTypedBindingError, DynamicTypedFieldBindingRequest,
1344 DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell, dynamic_typed_field_type,
1345 typed_adapter_field_kind_matches,
1346 };
1347 use crate::{
1348 db::{
1349 data::DataStore,
1350 index::IndexStore,
1351 registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1352 schema::{
1353 AcceptedSchemaRevision, FieldId, FieldStorageDecode, LeafCodec,
1354 PersistedFieldSnapshot, PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot,
1355 SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
1356 accepted_schema_candidate_with_field_bindings_for_tests,
1357 },
1358 },
1359 traits::{CanisterKind, Path},
1360 types::EntityTag,
1361 value::InputValue,
1362 };
1363 use icydb_schema::{EntitySourceKey, FieldSourceKey, ScalarType};
1364 use std::{cell::RefCell, collections::BTreeMap};
1365
1366 const STORE_PATH: &str = "session::write::typed_adapter_tests::Store";
1367 const ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::Entity";
1368 const OTHER_ENTITY_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity";
1369 const ID_SOURCE: &str = "session::write::typed_adapter_tests::Entity::id";
1370 const VALUE_SOURCE: &str = "session::write::typed_adapter_tests::Entity::value";
1371 const REPLACEMENT_SOURCE: &str =
1372 "session::write::typed_adapter_tests::Entity::replacement_value";
1373 const OTHER_ID_SOURCE: &str = "session::write::typed_adapter_tests::OtherEntity::id";
1374
1375 struct TestCanister;
1376
1377 impl Path for TestCanister {
1378 const PATH: &'static str = "session::write::typed_adapter_tests::Canister";
1379 }
1380
1381 impl CanisterKind for TestCanister {
1382 const COMMIT_MEMORY_ID: u8 = 41;
1383 const COMMIT_STABLE_KEY: &'static str = "icydb.typed_adapter_tests.commit.v1";
1384 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 42;
1385 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1386 "icydb.typed_adapter_tests.integrity.progress.v1";
1387 }
1388
1389 thread_local! {
1390 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1391 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1392 static SCHEMA_STORE: RefCell<SchemaStore> =
1393 const { RefCell::new(SchemaStore::init_heap()) };
1394 static STORE_REGISTRY: StoreRegistry = {
1395 let mut registry = StoreRegistry::new();
1396 registry.register_store(
1397 STORE_PATH,
1398 &DATA_STORE,
1399 &INDEX_STORE,
1400 &SCHEMA_STORE,
1401 StoreAllocationIdentities::absent(),
1402 StoreRuntimeStorageCapabilities::heap(),
1403 ).expect("typed adapter test store should register");
1404 registry
1405 };
1406 }
1407
1408 fn nat64_field(id: u32, name: &str, slot: u16) -> PersistedFieldSnapshot {
1409 PersistedFieldSnapshot::new_initial(
1410 FieldId::new(id),
1411 name.to_string(),
1412 SchemaFieldSlot::new(slot),
1413 AcceptedFieldKind::Nat64,
1414 Vec::new(),
1415 false,
1416 SchemaInsertDefault::None,
1417 FieldStorageDecode::ByKind,
1418 LeafCodec::Scalar(ScalarCodec::Nat64),
1419 )
1420 }
1421
1422 fn snapshot(
1423 entity_source: &str,
1424 entity_name: &str,
1425 fields: Vec<PersistedFieldSnapshot>,
1426 ) -> PersistedSchemaSnapshot {
1427 let layout = SchemaRowLayout::initial(
1428 fields
1429 .iter()
1430 .map(|field| (field.id(), field.slot()))
1431 .collect(),
1432 );
1433 PersistedSchemaSnapshot::new(
1434 SchemaVersion::initial(),
1435 entity_source.to_string(),
1436 entity_name.to_string(),
1437 FieldId::new(1),
1438 layout,
1439 fields,
1440 )
1441 }
1442
1443 fn field_source(source: &str) -> FieldSourceKey {
1444 FieldSourceKey::try_new(source).expect("typed field source should admit")
1445 }
1446
1447 fn entity_source(source: &str) -> EntitySourceKey {
1448 EntitySourceKey::try_new(source).expect("typed entity source should admit")
1449 }
1450
1451 fn publish(
1452 session: &DbSession<TestCanister>,
1453 expected: AcceptedSchemaRevision,
1454 revision: AcceptedSchemaRevision,
1455 snapshots: BTreeMap<EntityTag, PersistedSchemaSnapshot>,
1456 fields: BTreeMap<(EntityTag, FieldSourceKey), FieldId>,
1457 ) {
1458 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
1459 STORE_PATH, revision, snapshots, fields,
1460 );
1461 let store = session
1462 .db
1463 .store_handle(STORE_PATH)
1464 .expect("typed adapter test store should resolve");
1465 crate::db::commit::publish_accepted_schema_candidate(
1466 STORE_PATH, store, expected, &candidate,
1467 )
1468 .expect("typed binding candidate should publish");
1469 }
1470
1471 fn request(source: &str) -> DynamicTypedFieldBindingRequest {
1472 DynamicTypedFieldBindingRequest::new(
1473 source.to_string(),
1474 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
1475 false,
1476 )
1477 }
1478
1479 #[test]
1480 fn typed_adapter_kind_matching_is_exact_but_accepts_relation_key_wrappers() {
1481 let relation = AcceptedFieldKind::Relation {
1482 target_path: "test::Target".to_string(),
1483 target_entity_name: "Target".to_string(),
1484 target_entity_tag: EntityTag::new(7),
1485 target_store_path: "test::Store".to_string(),
1486 key_kind: Box::new(AcceptedFieldKind::Nat64),
1487 };
1488
1489 assert!(typed_adapter_field_kind_matches(
1490 &relation,
1491 &AcceptedFieldKind::Nat64,
1492 ));
1493 assert!(typed_adapter_field_kind_matches(
1494 &AcceptedFieldKind::List(Box::new(relation)),
1495 &AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Nat64)),
1496 ));
1497 assert!(!typed_adapter_field_kind_matches(
1498 &AcceptedFieldKind::Nat64,
1499 &AcceptedFieldKind::Nat32,
1500 ));
1501 }
1502
1503 #[test]
1504 fn typed_adapter_field_contract_rejects_invalid_named_source_identity() {
1505 assert!(matches!(
1506 dynamic_typed_field_type(DynamicTypedFieldType::Named(String::new())),
1507 Err(DynamicTypedBindingError::FieldUnavailable),
1508 ));
1509 assert!(matches!(
1510 dynamic_typed_field_type(DynamicTypedFieldType::Scalar(ScalarType::Nat16)),
1511 Ok(icydb_schema::FieldType::Scalar(ScalarType::Nat16)),
1512 ));
1513 }
1514
1515 #[expect(clippy::too_many_lines)]
1518 #[test]
1519 fn typed_binding_uses_accepted_ids_and_slots_across_renames_and_name_reuse() {
1520 let entity_tag = EntityTag::new(91);
1521 let other_entity_tag = EntityTag::new(92);
1522 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1523 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1524 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1525
1526 let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
1527 session
1528 .db
1529 .ensure_recovered_state()
1530 .expect("typed adapter test database should initialize");
1531 publish(
1532 &session,
1533 AcceptedSchemaRevision::NONE,
1534 AcceptedSchemaRevision::INITIAL,
1535 BTreeMap::from([(
1536 entity_tag,
1537 snapshot(
1538 ENTITY_SOURCE,
1539 "Entity",
1540 vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1541 ),
1542 )]),
1543 BTreeMap::from([
1544 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1545 ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1546 ]),
1547 );
1548
1549 let initial_catalog = session
1550 .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1551 .expect("initial source catalog lookup should inspect")
1552 .expect("initial source catalog should exist");
1553 assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1554 let initial = session
1555 .issue_typed_entity_binding(
1556 entity_source(ENTITY_SOURCE).as_str(),
1557 &[request(ID_SOURCE), request(VALUE_SOURCE)],
1558 )
1559 .expect("initial typed binding should issue");
1560 assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1561 assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1562 assert_eq!(initial.output_field_slot("value"), Some(1));
1563 let initial_patch = initial
1564 .bind_write_fields(vec![(
1565 VALUE_SOURCE.to_string(),
1566 DynamicWriteCell::Value(InputValue::Nat64(7)),
1567 )])
1568 .expect("source-bound patch should lower");
1569 assert_eq!(
1570 initial_patch.fields(),
1571 &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1572 );
1573
1574 publish(
1575 &session,
1576 AcceptedSchemaRevision::INITIAL,
1577 AcceptedSchemaRevision::new(2),
1578 BTreeMap::from([
1579 (
1580 entity_tag,
1581 snapshot(
1582 ENTITY_SOURCE,
1583 "RenamedEntity",
1584 vec![
1585 nat64_field(1, "id", 0),
1586 nat64_field(2, "renamed_value", 1),
1587 nat64_field(3, "value", 2),
1588 ],
1589 ),
1590 ),
1591 (
1592 other_entity_tag,
1593 snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1594 ),
1595 ]),
1596 BTreeMap::from([
1597 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1598 ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1599 (
1600 (entity_tag, field_source(REPLACEMENT_SOURCE)),
1601 FieldId::new(3),
1602 ),
1603 (
1604 (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1605 FieldId::new(1),
1606 ),
1607 ]),
1608 );
1609
1610 assert!(
1611 !session
1612 .typed_entity_binding_is_current(&initial)
1613 .expect("renamed binding currentness should inspect")
1614 );
1615 let renamed = session
1616 .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1617 .expect("renamed source-bound adapter should rebind");
1618 assert_eq!(renamed.entity(), "RenamedEntity");
1619 assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1620 assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1621 assert_eq!(renamed.output_field_slot("value"), None);
1622
1623 publish(
1624 &session,
1625 AcceptedSchemaRevision::new(2),
1626 AcceptedSchemaRevision::new(3),
1627 BTreeMap::from([
1628 (
1629 entity_tag,
1630 snapshot(
1631 ENTITY_SOURCE,
1632 "RenamedEntity",
1633 vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1634 ),
1635 ),
1636 (
1637 other_entity_tag,
1638 snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1639 ),
1640 ]),
1641 BTreeMap::from([
1642 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1643 (
1644 (entity_tag, field_source(REPLACEMENT_SOURCE)),
1645 FieldId::new(2),
1646 ),
1647 (
1648 (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1649 FieldId::new(1),
1650 ),
1651 ]),
1652 );
1653
1654 assert!(matches!(
1655 session.issue_typed_entity_binding(
1656 ENTITY_SOURCE,
1657 &[request(ID_SOURCE), request(VALUE_SOURCE)],
1658 ),
1659 Err(DynamicTypedBindingError::FieldUnavailable),
1660 ));
1661 assert!(
1662 !session
1663 .typed_entity_binding_is_current(&renamed)
1664 .expect("removed source binding should become stale")
1665 );
1666
1667 let replacement = session
1668 .issue_typed_entity_binding(
1669 ENTITY_SOURCE,
1670 &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
1671 )
1672 .expect("explicit replacement source should bind");
1673 assert!(
1674 session
1675 .execute_trusted_typed_mutation(
1676 &replacement,
1677 &DynamicTypedMutation::Insert {
1678 patch: initial_patch
1679 },
1680 )
1681 .expect("cross-binding patch should fail closed")
1682 .is_none()
1683 );
1684 let patch = replacement
1685 .bind_write_fields(vec![
1686 (
1687 ID_SOURCE.to_string(),
1688 DynamicWriteCell::Value(InputValue::Nat64(1)),
1689 ),
1690 (
1691 REPLACEMENT_SOURCE.to_string(),
1692 DynamicWriteCell::Value(InputValue::Nat64(9)),
1693 ),
1694 ])
1695 .expect("replacement source write should bind by accepted IDs and slots");
1696 let result = session
1697 .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
1698 .expect("typed insert should use the accepted mutation pipeline")
1699 .expect("replacement binding should remain current");
1700 assert_eq!(result.entity, "RenamedEntity");
1701 assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1702 assert_eq!(
1703 result.rows,
1704 vec![vec![
1705 crate::value::OutputValue::Nat64(1),
1706 crate::value::OutputValue::Nat64(9)
1707 ]]
1708 );
1709 assert_eq!(result.affected_rows, 1);
1710
1711 #[cfg(feature = "query")]
1712 {
1713 let query = crate::db::DynamicQuery::new("RenamedEntity")
1714 .select(["id", "value"])
1715 .order_by(crate::db::asc("id"))
1716 .limit(1);
1717 let result = session
1718 .execute_trusted_dynamic_query(&query)
1719 .expect("query-only dynamic execution should use accepted authority");
1720 assert_eq!(result.entity, "RenamedEntity");
1721 assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1722 assert_eq!(
1723 result.rows,
1724 vec![vec![
1725 crate::value::OutputValue::Nat64(1),
1726 crate::value::OutputValue::Nat64(9)
1727 ]]
1728 );
1729 assert_eq!(result.row_count, 1);
1730 }
1731 }
1732}
1733
1734#[cfg(test)]
1735mod mixed_relation_batch_tests {
1736 use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
1737 use crate::{
1738 db::{
1739 data::DataStore,
1740 index::IndexStore,
1741 registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
1742 schema::{
1743 AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
1744 FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
1745 PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
1746 PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
1747 SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
1748 SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
1749 },
1750 },
1751 error::{ConstraintDiagnosticKind, ErrorClass},
1752 traits::{CanisterKind, Path},
1753 types::EntityTag,
1754 value::{InputValue, OutputValue},
1755 };
1756 use icydb_schema::FieldSourceKey;
1757 use std::{cell::RefCell, collections::BTreeMap};
1758
1759 const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
1760 const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
1761 const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
1762 const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
1763 const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
1764 const ENTITY_NAME: &str = "MixedRelationNode";
1765 const ENTITY_TAG: EntityTag = EntityTag::new(94);
1766 const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
1767 const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
1768 const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
1769 const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
1770 const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
1771
1772 struct TestCanister;
1773
1774 impl Path for TestCanister {
1775 const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
1776 }
1777
1778 impl CanisterKind for TestCanister {
1779 const COMMIT_MEMORY_ID: u8 = 47;
1780 const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
1781 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
1782 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
1783 "icydb.mixed_relation_batch_tests.integrity.progress.v1";
1784 }
1785
1786 thread_local! {
1787 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
1788 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
1789 static SCHEMA_STORE: RefCell<SchemaStore> =
1790 const { RefCell::new(SchemaStore::init_heap()) };
1791 static STORE_REGISTRY: StoreRegistry = {
1792 let mut registry = StoreRegistry::new();
1793 registry.register_store(
1794 STORE_PATH,
1795 &DATA_STORE,
1796 &INDEX_STORE,
1797 &SCHEMA_STORE,
1798 StoreAllocationIdentities::absent(),
1799 StoreRuntimeStorageCapabilities::heap(),
1800 ).expect("mixed relation test store should register");
1801 registry
1802 };
1803 }
1804
1805 fn source_key(source: &str) -> FieldSourceKey {
1806 FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
1807 }
1808
1809 fn relation_snapshot() -> PersistedSchemaSnapshot {
1810 let fields = vec![
1811 PersistedFieldSnapshot::new_initial(
1812 FieldId::new(1),
1813 "id".to_string(),
1814 SchemaFieldSlot::new(0),
1815 AcceptedFieldKind::Nat64,
1816 Vec::new(),
1817 false,
1818 SchemaInsertDefault::None,
1819 FieldStorageDecode::ByKind,
1820 LeafCodec::Scalar(ScalarCodec::Nat64),
1821 ),
1822 PersistedFieldSnapshot::new_initial(
1823 FieldId::new(2),
1824 "parent_id".to_string(),
1825 SchemaFieldSlot::new(1),
1826 AcceptedFieldKind::Relation {
1827 target_path: ENTITY_SOURCE.to_string(),
1828 target_entity_name: ENTITY_NAME.to_string(),
1829 target_entity_tag: ENTITY_TAG,
1830 target_store_path: STORE_PATH.to_string(),
1831 key_kind: Box::new(AcceptedFieldKind::Nat64),
1832 },
1833 Vec::new(),
1834 true,
1835 SchemaInsertDefault::None,
1836 FieldStorageDecode::ByKind,
1837 LeafCodec::Scalar(ScalarCodec::Nat64),
1838 ),
1839 PersistedFieldSnapshot::new_initial(
1840 FieldId::new(3),
1841 "code".to_string(),
1842 SchemaFieldSlot::new(2),
1843 AcceptedFieldKind::Nat64,
1844 Vec::new(),
1845 false,
1846 SchemaInsertDefault::None,
1847 FieldStorageDecode::ByKind,
1848 LeafCodec::Scalar(ScalarCodec::Nat64),
1849 ),
1850 ];
1851 let relation = PersistedRelationEdgeSnapshot::new(
1852 RelationId::new(1).expect("mixed relation identity should be non-zero"),
1853 "parent".to_string(),
1854 ENTITY_SOURCE.to_string(),
1855 vec![FieldId::new(2)],
1856 );
1857 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
1858 SchemaVersion::initial(),
1859 ENTITY_SOURCE.to_string(),
1860 ENTITY_NAME.to_string(),
1861 FieldId::new(1),
1862 SchemaRowLayout::initial(
1863 fields
1864 .iter()
1865 .map(|field| (field.id(), field.slot()))
1866 .collect(),
1867 ),
1868 fields,
1869 vec![PersistedIndexSnapshot::new(
1870 SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
1871 1,
1872 "by_code".to_string(),
1873 STORE_PATH.to_string(),
1874 true,
1875 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
1876 FieldId::new(3),
1877 SchemaFieldSlot::new(2),
1878 vec!["code".to_string()],
1879 AcceptedFieldKind::Nat64,
1880 false,
1881 )]),
1882 None,
1883 )],
1884 )
1885 .with_relations(vec![relation]);
1886 let constraints = AcceptedConstraintCatalog::initial(
1887 snapshot.fields(),
1888 snapshot.indexes(),
1889 snapshot.relations(),
1890 )
1891 .expect("mixed relation constraints should close");
1892 snapshot.with_constraint_catalog(constraints)
1893 }
1894
1895 fn other_snapshot() -> PersistedSchemaSnapshot {
1896 let fields = vec![
1897 PersistedFieldSnapshot::new_initial(
1898 FieldId::new(1),
1899 "id".to_string(),
1900 SchemaFieldSlot::new(0),
1901 AcceptedFieldKind::Nat64,
1902 Vec::new(),
1903 false,
1904 SchemaInsertDefault::None,
1905 FieldStorageDecode::ByKind,
1906 LeafCodec::Scalar(ScalarCodec::Nat64),
1907 ),
1908 PersistedFieldSnapshot::new_initial(
1909 FieldId::new(2),
1910 "value".to_string(),
1911 SchemaFieldSlot::new(1),
1912 AcceptedFieldKind::Nat64,
1913 Vec::new(),
1914 false,
1915 SchemaInsertDefault::None,
1916 FieldStorageDecode::ByKind,
1917 LeafCodec::Scalar(ScalarCodec::Nat64),
1918 ),
1919 ];
1920 PersistedSchemaSnapshot::new(
1921 SchemaVersion::initial(),
1922 OTHER_ENTITY_SOURCE.to_string(),
1923 OTHER_ENTITY_NAME.to_string(),
1924 FieldId::new(1),
1925 SchemaRowLayout::initial(
1926 fields
1927 .iter()
1928 .map(|field| (field.id(), field.slot()))
1929 .collect(),
1930 ),
1931 fields,
1932 )
1933 }
1934
1935 fn initialize() -> DbSession<TestCanister> {
1936 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
1937 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
1938 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
1939 let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
1940 session
1941 .db
1942 .ensure_recovered_state()
1943 .expect("mixed relation database should initialize");
1944 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
1945 STORE_PATH,
1946 AcceptedSchemaRevision::INITIAL,
1947 BTreeMap::from([
1948 (ENTITY_TAG, relation_snapshot()),
1949 (OTHER_ENTITY_TAG, other_snapshot()),
1950 ]),
1951 BTreeMap::from([
1952 ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
1953 ((ENTITY_TAG, source_key(PARENT_SOURCE)), FieldId::new(2)),
1954 ((ENTITY_TAG, source_key(CODE_SOURCE)), FieldId::new(3)),
1955 (
1956 (OTHER_ENTITY_TAG, source_key(OTHER_ID_SOURCE)),
1957 FieldId::new(1),
1958 ),
1959 (
1960 (OTHER_ENTITY_TAG, source_key(OTHER_VALUE_SOURCE)),
1961 FieldId::new(2),
1962 ),
1963 ]),
1964 );
1965 let store = session
1966 .db
1967 .store_handle(STORE_PATH)
1968 .expect("mixed relation store should resolve");
1969 crate::db::commit::publish_accepted_schema_candidate(
1970 STORE_PATH,
1971 store,
1972 AcceptedSchemaRevision::NONE,
1973 &candidate,
1974 )
1975 .expect("mixed relation candidate should publish");
1976 session
1977 }
1978
1979 fn patch(id: Option<u64>, parent: Option<u64>, code: Option<u64>) -> DynamicStructuralPatch {
1980 let mut fields = Vec::new();
1981 if let Some(id) = id {
1982 fields.push((
1983 "id".to_string(),
1984 DynamicWriteCell::Value(InputValue::Nat64(id)),
1985 ));
1986 }
1987 fields.push((
1988 "parent_id".to_string(),
1989 parent.map_or(DynamicWriteCell::Null, |parent| {
1990 DynamicWriteCell::Value(InputValue::Nat64(parent))
1991 }),
1992 ));
1993 if let Some(code) = code {
1994 fields.push((
1995 "code".to_string(),
1996 DynamicWriteCell::Value(InputValue::Nat64(code)),
1997 ));
1998 }
1999 DynamicStructuralPatch::new(fields)
2000 }
2001
2002 fn insert(id: u64, parent: Option<u64>) -> DynamicMutation {
2003 insert_with_code(id, parent, id)
2004 }
2005
2006 fn insert_with_code(id: u64, parent: Option<u64>, code: u64) -> DynamicMutation {
2007 DynamicMutation::Insert {
2008 entity: ENTITY_NAME.to_string(),
2009 patch: patch(Some(id), parent, Some(code)),
2010 }
2011 }
2012
2013 fn update_parent(id: u64, parent: Option<u64>) -> DynamicMutation {
2014 DynamicMutation::Update {
2015 entity: ENTITY_NAME.to_string(),
2016 key: InputValue::Nat64(id),
2017 patch: patch(None, parent, None),
2018 }
2019 }
2020
2021 fn update_code(id: u64, code: u64) -> DynamicMutation {
2022 DynamicMutation::Update {
2023 entity: ENTITY_NAME.to_string(),
2024 key: InputValue::Nat64(id),
2025 patch: DynamicStructuralPatch::new(vec![(
2026 "code".to_string(),
2027 DynamicWriteCell::Value(InputValue::Nat64(code)),
2028 )]),
2029 }
2030 }
2031
2032 fn delete(id: u64) -> DynamicMutation {
2033 DynamicMutation::Delete {
2034 entity: ENTITY_NAME.to_string(),
2035 key: InputValue::Nat64(id),
2036 }
2037 }
2038
2039 fn expected_row(id: u64, parent: Option<u64>) -> Vec<OutputValue> {
2040 expected_row_with_code(id, parent, id)
2041 }
2042
2043 fn expected_row_with_code(id: u64, parent: Option<u64>, code: u64) -> Vec<OutputValue> {
2044 vec![
2045 OutputValue::Nat64(id),
2046 parent.map_or(OutputValue::Null, OutputValue::Nat64),
2047 OutputValue::Nat64(code),
2048 ]
2049 }
2050
2051 fn other_patch(id: Option<u64>, value: u64) -> DynamicStructuralPatch {
2052 let mut fields = Vec::new();
2053 if let Some(id) = id {
2054 fields.push((
2055 "id".to_string(),
2056 DynamicWriteCell::Value(InputValue::Nat64(id)),
2057 ));
2058 }
2059 fields.push((
2060 "value".to_string(),
2061 DynamicWriteCell::Value(InputValue::Nat64(value)),
2062 ));
2063 DynamicStructuralPatch::new(fields)
2064 }
2065
2066 fn assert_relation_violation(error: &crate::error::InternalError) {
2067 let diagnostic = error
2068 .constraint_diagnostic()
2069 .expect("relation violations should retain their accepted constraint");
2070 assert_eq!(
2071 diagnostic.constraint_kind(),
2072 ConstraintDiagnosticKind::Relation,
2073 );
2074 }
2075
2076 #[test]
2077 fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
2078 let session = initialize();
2079 session
2080 .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
2081 .expect("the initial relation should commit");
2082
2083 let blocked = session
2084 .execute_trusted_dynamic_mutation(&delete(1))
2085 .expect_err("an unaffected committed source must block target deletion");
2086 assert_relation_violation(&blocked);
2087
2088 let deleted = session
2089 .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
2090 .expect("a source and its target should delete atomically");
2091 assert_eq!(
2092 deleted.rows,
2093 vec![expected_row(2, Some(1)), expected_row(1, None)],
2094 );
2095
2096 session
2097 .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
2098 .expect("the update-away fixture should commit");
2099 let updated_away = session
2100 .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
2101 .expect("an updated final source may release a deleted target");
2102 assert_eq!(
2103 updated_away.rows,
2104 vec![expected_row(4, None), expected_row(3, None)],
2105 );
2106
2107 session
2108 .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
2109 .expect("the retained-reference fixture should commit");
2110 let retained = session
2111 .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
2112 .expect_err("a final updated source must still block target deletion");
2113 assert_relation_violation(&retained);
2114
2115 session
2116 .execute_trusted_dynamic_mutation(&insert(7, None))
2117 .expect("the inserted-reference fixture target should commit");
2118 let inserted_reference = session
2119 .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
2120 .expect_err("a final inserted source must not reference a deleted target");
2121 assert_relation_violation(&inserted_reference);
2122
2123 let inserted_target = session
2124 .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
2125 .expect("an inserted relation should see its batch-final target");
2126 assert_eq!(
2127 inserted_target.rows,
2128 vec![expected_row(10, Some(9)), expected_row(9, None)],
2129 );
2130
2131 session
2132 .execute_trusted_dynamic_mutation(&insert(11, None))
2133 .expect("the updated-reference fixture source should commit");
2134 let updated_target = session
2135 .execute_trusted_dynamic_mutation_batch(vec![
2136 update_parent(11, Some(12)),
2137 insert(12, None),
2138 ])
2139 .expect("an updated relation should see its batch-final target");
2140 assert_eq!(
2141 updated_target.rows,
2142 vec![expected_row(11, Some(12)), expected_row(12, None)],
2143 );
2144 }
2145
2146 #[test]
2147 fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
2148 let session = initialize();
2149 session
2150 .execute_trusted_dynamic_mutation(&insert(1, None))
2151 .expect("the primary mixed fixture row should commit");
2152 session
2153 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2154 entity: OTHER_ENTITY_NAME.to_string(),
2155 patch: other_patch(Some(1), 10),
2156 })
2157 .expect("the secondary mixed fixture row should commit");
2158
2159 let mixed_entity = session
2160 .execute_trusted_dynamic_mutation_batch(vec![
2161 update_code(1, 11),
2162 DynamicMutation::Update {
2163 entity: OTHER_ENTITY_NAME.to_string(),
2164 key: InputValue::Nat64(1),
2165 patch: other_patch(None, 11),
2166 },
2167 ])
2168 .expect_err("one atomic batch must not cross accepted entities");
2169 assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
2170
2171 let missing = session
2172 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
2173 .expect_err("a late missing delete must reject the earlier staged update");
2174 assert_eq!(missing.class(), ErrorClass::NotFound);
2175
2176 session
2177 .execute_trusted_dynamic_mutation(&insert(2, None))
2178 .expect("the collision fixture should commit");
2179 let collision = session
2180 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
2181 .expect_err("an insert collision must reject the earlier staged update");
2182 assert_eq!(collision.class(), ErrorClass::Conflict);
2183 let failures_unchanged = session
2184 .execute_trusted_dynamic_mutation(&update_code(1, 1))
2185 .expect("failed batches must preserve the original unique value");
2186 assert_eq!(failures_unchanged.affected_rows, 0);
2187
2188 let replaced = session
2189 .execute_trusted_dynamic_mutation_batch(vec![
2190 update_code(1, 14),
2191 DynamicMutation::Replace {
2192 entity: ENTITY_NAME.to_string(),
2193 key: InputValue::Nat64(99),
2194 patch: patch(None, None, Some(99)),
2195 },
2196 ])
2197 .expect("ordinary caller-key replace should insert its absent final row");
2198 assert_eq!(
2199 replaced.rows,
2200 vec![
2201 expected_row_with_code(1, None, 14),
2202 expected_row_with_code(99, None, 99),
2203 ],
2204 );
2205
2206 let unchanged = session
2207 .execute_trusted_dynamic_mutation(&update_code(1, 14))
2208 .expect("the successful mixed replace must publish its preceding update");
2209 assert_eq!(unchanged.affected_rows, 0);
2210 let other_unchanged = session
2211 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2212 entity: OTHER_ENTITY_NAME.to_string(),
2213 key: InputValue::Nat64(1),
2214 patch: other_patch(None, 10),
2215 })
2216 .expect("cross-entity rejection must preserve the secondary row");
2217 assert_eq!(other_unchanged.affected_rows, 0);
2218 }
2219
2220 #[test]
2221 fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
2222 let session = initialize();
2223 session
2224 .execute_trusted_dynamic_mutation_batch(vec![
2225 insert_with_code(1, None, 10),
2226 insert_with_code(2, None, 20),
2227 ])
2228 .expect("the unique-overlay fixture should commit");
2229
2230 let swapped = session
2231 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
2232 .expect("two final rows should atomically swap unique memberships");
2233 assert_eq!(
2234 swapped.rows,
2235 vec![
2236 expected_row_with_code(1, None, 20),
2237 expected_row_with_code(2, None, 10),
2238 ],
2239 );
2240
2241 let released = session
2242 .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
2243 .expect("a delete should release unique membership to a final inserted row");
2244 assert_eq!(
2245 released.rows,
2246 vec![
2247 expected_row_with_code(1, None, 20),
2248 expected_row_with_code(3, None, 20),
2249 ],
2250 );
2251 }
2252}
2253
2254#[cfg(test)]
2255mod identity_pre_key_tests {
2256 use super::{
2257 AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
2258 AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
2259 DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
2260 DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
2261 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
2262 add_structural_mutation_staged_bytes, checked_pre_key_candidate_count,
2263 insert_key_exists_after_generation, validate_structural_mutation_result_bytes,
2264 };
2265 use crate::{
2266 db::{
2267 commit::{database_incarnation_id, forget_recovered_domain_for_tests},
2268 data::DataStore,
2269 executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
2270 index::IndexStore,
2271 integrity::{
2272 PhysicalUnitCheckpoint, QuickIntegrityStatus, RowInspectionLimits,
2273 execute_quick_integrity, execute_row_integrity_page,
2274 },
2275 journal::JournalTailStore,
2276 registry::{
2277 StoreAllocationIdentities, StoreAllocationIdentity, StoreRegistry,
2278 StoreRuntimeStorageCapabilities,
2279 },
2280 schema::{
2281 AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
2282 FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2283 PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2284 PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
2285 SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2286 accepted_schema_candidate_with_field_bindings_for_tests,
2287 },
2288 write_context::MutationMode,
2289 },
2290 error::{ErrorClass, ErrorOrigin, InternalError},
2291 testing::test_memory,
2292 traits::{CanisterKind, Path},
2293 types::{EntityTag, Timestamp},
2294 value::{InputValue, OutputValue, Value},
2295 };
2296 use icydb_schema::{FieldSourceKey, ScalarType};
2297 use std::{cell::RefCell, collections::BTreeMap, time::Instant};
2298
2299 const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
2300 const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
2301 const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
2302 const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
2303 const ENTITY_NAME: &str = "IdentityRow";
2304 const ENTITY_TAG: EntityTag = EntityTag::new(93);
2305 const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
2306
2307 struct TestCanister;
2308
2309 impl Path for TestCanister {
2310 const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
2311 }
2312
2313 impl CanisterKind for TestCanister {
2314 const COMMIT_MEMORY_ID: u8 = 45;
2315 const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
2316 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
2317 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2318 "icydb.identity_pre_key_tests.integrity.progress.v1";
2319 }
2320
2321 thread_local! {
2322 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2323 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2324 static SCHEMA_STORE: RefCell<SchemaStore> =
2325 const { RefCell::new(SchemaStore::init_heap()) };
2326 static STORE_REGISTRY: StoreRegistry = {
2327 let mut registry = StoreRegistry::new();
2328 registry.register_store(
2329 STORE_PATH,
2330 &DATA_STORE,
2331 &INDEX_STORE,
2332 &SCHEMA_STORE,
2333 StoreAllocationIdentities::absent(),
2334 StoreRuntimeStorageCapabilities::heap(),
2335 ).expect("identity pre-key test store should register");
2336 registry
2337 };
2338 static JOURNALED_DATA_STORE: RefCell<DataStore> =
2339 RefCell::new(DataStore::init_journaled(test_memory(186)));
2340 static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
2341 RefCell::new(IndexStore::init_journaled(test_memory(187)));
2342 static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
2343 RefCell::new(SchemaStore::init_journaled(test_memory(188)));
2344 static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
2345 RefCell::new(JournalTailStore::init(test_memory(189)));
2346 static JOURNALED_STORE_REGISTRY: StoreRegistry = {
2347 let mut registry = StoreRegistry::new();
2348 registry.register_journaled_store(
2349 JOURNALED_STORE_PATH,
2350 &JOURNALED_DATA_STORE,
2351 &JOURNALED_INDEX_STORE,
2352 &JOURNALED_SCHEMA_STORE,
2353 &JOURNALED_TAIL_STORE,
2354 StoreAllocationIdentities::new_journaled(
2355 StoreAllocationIdentity::new(186, "icydb.test.identity-range.data.v1"),
2356 StoreAllocationIdentity::new(187, "icydb.test.identity-range.index.v1"),
2357 StoreAllocationIdentity::new(188, "icydb.test.identity-range.schema.v1"),
2358 StoreAllocationIdentity::new(189, "icydb.test.identity-range.journal.v1"),
2359 ),
2360 StoreRuntimeStorageCapabilities::journaled(),
2361 ).expect("identity range journaled store should register");
2362 registry
2363 };
2364 }
2365
2366 struct JournaledTestCanister;
2367
2368 impl Path for JournaledTestCanister {
2369 const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
2370 }
2371
2372 impl CanisterKind for JournaledTestCanister {
2373 const COMMIT_MEMORY_ID: u8 = 190;
2374 const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
2375 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
2376 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2377 "icydb.identity_range_tests.integrity.progress.v1";
2378 }
2379
2380 fn source_key(source: &str) -> FieldSourceKey {
2381 FieldSourceKey::try_new(source).expect("identity test field source should admit")
2382 }
2383
2384 fn identity_snapshot(store_path: &str) -> PersistedSchemaSnapshot {
2385 let fields = vec![
2386 PersistedFieldSnapshot::new_initial_with_write_policy(
2387 FieldId::new(1),
2388 "id".to_string(),
2389 SchemaFieldSlot::new(0),
2390 AcceptedFieldKind::Nat64,
2391 Vec::new(),
2392 false,
2393 SchemaInsertDefault::None,
2394 SchemaFieldWritePolicy::from_model_policies(
2395 Some(FieldInsertGeneration::Identity),
2396 None,
2397 ),
2398 FieldStorageDecode::ByKind,
2399 LeafCodec::Scalar(ScalarCodec::Nat64),
2400 ),
2401 PersistedFieldSnapshot::new_initial(
2402 FieldId::new(2),
2403 "payload".to_string(),
2404 SchemaFieldSlot::new(1),
2405 AcceptedFieldKind::Nat64,
2406 Vec::new(),
2407 false,
2408 SchemaInsertDefault::None,
2409 FieldStorageDecode::ByKind,
2410 LeafCodec::Scalar(ScalarCodec::Nat64),
2411 ),
2412 ];
2413 PersistedSchemaSnapshot::new_with_indexes(
2414 SchemaVersion::initial(),
2415 ENTITY_SOURCE.to_string(),
2416 ENTITY_NAME.to_string(),
2417 FieldId::new(1),
2418 SchemaRowLayout::initial(
2419 fields
2420 .iter()
2421 .map(|field| (field.id(), field.slot()))
2422 .collect(),
2423 ),
2424 fields,
2425 vec![PersistedIndexSnapshot::new(
2426 SchemaIndexId::new(1).expect("identity test index ID should admit"),
2427 1,
2428 "by_payload".to_string(),
2429 store_path.to_string(),
2430 false,
2431 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2432 FieldId::new(2),
2433 SchemaFieldSlot::new(1),
2434 vec!["payload".to_string()],
2435 AcceptedFieldKind::Nat64,
2436 false,
2437 )]),
2438 None,
2439 )],
2440 )
2441 }
2442
2443 fn initialize() -> DbSession<TestCanister> {
2444 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2445 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2446 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2447 let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
2448 session
2449 .db
2450 .ensure_recovered_state()
2451 .expect("identity pre-key test database should initialize");
2452 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2453 STORE_PATH,
2454 AcceptedSchemaRevision::INITIAL,
2455 BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH))]),
2456 BTreeMap::from([
2457 ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2458 ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2459 ]),
2460 );
2461 let store = session
2462 .db
2463 .store_handle(STORE_PATH)
2464 .expect("identity pre-key test store should resolve");
2465 crate::db::commit::publish_accepted_schema_candidate(
2466 STORE_PATH,
2467 store,
2468 AcceptedSchemaRevision::NONE,
2469 &candidate,
2470 )
2471 .expect("identity candidate should publish with explicit zero state");
2472 session
2473 }
2474
2475 fn initialize_journaled() -> DbSession<JournaledTestCanister> {
2476 let session = DbSession::<JournaledTestCanister>::new(&JOURNALED_STORE_REGISTRY);
2477 session
2478 .db
2479 .ensure_recovered_state()
2480 .expect("journaled identity database should initialize");
2481 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2482 JOURNALED_STORE_PATH,
2483 AcceptedSchemaRevision::INITIAL,
2484 BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH))]),
2485 BTreeMap::from([
2486 ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2487 ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2488 ]),
2489 );
2490 let store = session
2491 .db
2492 .store_handle(JOURNALED_STORE_PATH)
2493 .expect("journaled identity store should resolve");
2494 crate::db::commit::publish_accepted_schema_candidate(
2495 JOURNALED_STORE_PATH,
2496 store,
2497 AcceptedSchemaRevision::NONE,
2498 &candidate,
2499 )
2500 .expect("journaled identity candidate should publish");
2501 session
2502 }
2503
2504 fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
2505 AcceptedMutationIntentPatch::new()
2506 .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
2507 }
2508
2509 fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
2510 DynamicStructuralPatch::new(vec![(
2511 "payload".to_string(),
2512 DynamicWriteCell::Value(InputValue::Nat64(value)),
2513 )])
2514 }
2515
2516 fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
2517 vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
2518 }
2519
2520 fn assert_dynamic_payload(session: &DbSession<TestCanister>, key: u64, expected_payload: u64) {
2521 let unchanged = session
2522 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2523 entity: ENTITY_NAME.to_string(),
2524 key: InputValue::Nat64(key),
2525 patch: dynamic_payload_patch(expected_payload),
2526 })
2527 .expect("the expected row should remain readable through a no-op update");
2528 assert_eq!(unchanged.affected_rows, 0);
2529 assert_eq!(
2530 unchanged.rows,
2531 vec![expected_dynamic_row(key, expected_payload)],
2532 );
2533 }
2534
2535 fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
2536 values
2537 .iter()
2538 .map(|value| {
2539 AcceptedStructuralMutation::save(
2540 MutationMode::Insert,
2541 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
2542 payload_patch(*value),
2543 )
2544 })
2545 .collect()
2546 }
2547
2548 fn assert_identity_boundary(error: &InternalError) {
2549 assert_eq!(error.class(), ErrorClass::Unsupported);
2550 assert_eq!(error.origin(), ErrorOrigin::Identity);
2551 }
2552
2553 #[test]
2554 fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
2555 let generated = insert_key_exists_after_generation(true);
2556 assert_eq!(generated.class(), ErrorClass::Corruption);
2557 assert_eq!(generated.origin(), ErrorOrigin::Identity);
2558
2559 let ordinary = insert_key_exists_after_generation(false);
2560 assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
2561 }
2562
2563 #[cfg(target_pointer_width = "64")]
2564 #[test]
2565 fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
2566 let error = checked_pre_key_candidate_count(
2567 usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
2568 )
2569 .expect_err("candidate counts beyond u32 must reject");
2570 assert_identity_boundary(&error);
2571 }
2572
2573 #[test]
2574 #[expect(
2575 clippy::too_many_lines,
2576 reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
2577 )]
2578 fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
2579 let session = initialize();
2580 let seeded = session
2581 .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2582 .expect("seed rows should commit");
2583 assert_eq!(seeded.affected_rows, 1);
2584
2585 let split = session
2586 .execute_trusted_dynamic_mutation_batch(vec![
2587 DynamicMutation::Update {
2588 entity: ENTITY_NAME.to_string(),
2589 key: InputValue::Nat64(1),
2590 patch: dynamic_payload_patch(60),
2591 },
2592 DynamicMutation::Insert {
2593 entity: ENTITY_NAME.to_string(),
2594 patch: dynamic_payload_patch(40),
2595 },
2596 ])
2597 .expect("one holding should split atomically");
2598 assert_eq!(split.affected_rows, 2);
2599 assert_eq!(
2600 split.rows,
2601 vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
2602 "split after-images must retain input order and exact quantity",
2603 );
2604
2605 let rejected_split = session
2606 .execute_trusted_dynamic_mutation_batch(vec![
2607 DynamicMutation::Update {
2608 entity: ENTITY_NAME.to_string(),
2609 key: InputValue::Nat64(1),
2610 patch: dynamic_payload_patch(50),
2611 },
2612 DynamicMutation::Insert {
2613 entity: ENTITY_NAME.to_string(),
2614 patch: DynamicStructuralPatch::new(Vec::new()),
2615 },
2616 ])
2617 .expect_err("an invalid split output must reject the staged source update");
2618 assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
2619 assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
2620 assert_dynamic_payload(&session, 1, 60);
2621 assert_dynamic_payload(&session, 2, 40);
2622
2623 let transfer = session
2624 .execute_trusted_dynamic_mutation_batch(vec![
2625 DynamicMutation::Update {
2626 entity: ENTITY_NAME.to_string(),
2627 key: InputValue::Nat64(1),
2628 patch: dynamic_payload_patch(70),
2629 },
2630 DynamicMutation::Update {
2631 entity: ENTITY_NAME.to_string(),
2632 key: InputValue::Nat64(2),
2633 patch: dynamic_payload_patch(30),
2634 },
2635 ])
2636 .expect("distinct transfer patches should share one atomic batch");
2637 assert_eq!(
2638 transfer.rows,
2639 vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
2640 "the transfer must preserve the exact total quantity",
2641 );
2642
2643 let merge = session
2644 .execute_trusted_dynamic_mutation_batch(vec![
2645 DynamicMutation::Delete {
2646 entity: ENTITY_NAME.to_string(),
2647 key: InputValue::Nat64(2),
2648 },
2649 DynamicMutation::Update {
2650 entity: ENTITY_NAME.to_string(),
2651 key: InputValue::Nat64(1),
2652 patch: dynamic_payload_patch(100),
2653 },
2654 ])
2655 .expect("two holdings should merge atomically");
2656 assert_eq!(
2657 merge.rows,
2658 vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
2659 "delete before-images and update after-images must retain input order",
2660 );
2661
2662 let resplit = session
2663 .execute_trusted_dynamic_mutation_batch(vec![
2664 DynamicMutation::Update {
2665 entity: ENTITY_NAME.to_string(),
2666 key: InputValue::Nat64(1),
2667 patch: dynamic_payload_patch(60),
2668 },
2669 DynamicMutation::Insert {
2670 entity: ENTITY_NAME.to_string(),
2671 patch: dynamic_payload_patch(40),
2672 },
2673 ])
2674 .expect("the merged holding should split again");
2675 assert_eq!(
2676 resplit.rows,
2677 vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
2678 );
2679
2680 let rejected_merge = session
2681 .execute_trusted_dynamic_mutation_batch(vec![
2682 DynamicMutation::Delete {
2683 entity: ENTITY_NAME.to_string(),
2684 key: InputValue::Nat64(3),
2685 },
2686 DynamicMutation::Update {
2687 entity: ENTITY_NAME.to_string(),
2688 key: InputValue::Nat64(99),
2689 patch: dynamic_payload_patch(100),
2690 },
2691 ])
2692 .expect_err("a late missing merge target must preserve the earlier staged delete");
2693 assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
2694 assert_dynamic_payload(&session, 1, 60);
2695 assert_dynamic_payload(&session, 3, 40);
2696
2697 SCHEMA_STORE.with(|store| {
2698 let cursor = store
2699 .borrow()
2700 .identity_statement_cursor(
2701 database_incarnation_id().expect("database incarnation should remain readable"),
2702 ENTITY_TAG,
2703 FieldId::new(1),
2704 &AcceptedFieldKind::Nat64,
2705 )
2706 .expect("mixed Identity state should remain readable");
2707 assert_eq!(cursor.expected_high_water(), 3);
2708 assert!(!cursor.has_allocations());
2709 });
2710 }
2711
2712 #[test]
2713 fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
2714 let session = initialize();
2715 session
2716 .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
2717 .expect("the holding fixture should initialize");
2718
2719 let duplicate = session
2720 .execute_trusted_dynamic_mutation_batch(vec![
2721 DynamicMutation::Update {
2722 entity: ENTITY_NAME.to_string(),
2723 key: InputValue::Nat64(1),
2724 patch: dynamic_payload_patch(60),
2725 },
2726 DynamicMutation::Delete {
2727 entity: ENTITY_NAME.to_string(),
2728 key: InputValue::Nat64(1),
2729 },
2730 ])
2731 .expect_err("duplicate targets across operation kinds must reject");
2732 assert!(matches!(
2733 duplicate.diagnostic().detail(),
2734 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2735 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
2736 }),
2737 ));
2738 assert_dynamic_payload(&session, 1, 100);
2739 }
2740
2741 #[test]
2742 fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
2743 let session = initialize();
2744 let empty = session
2745 .execute_trusted_dynamic_mutation_batch(Vec::new())
2746 .expect_err("an empty public batch must reject");
2747 assert!(matches!(
2748 empty.diagnostic().detail(),
2749 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2750 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
2751 }),
2752 ));
2753
2754 let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
2755 .map(|_| DynamicMutation::Delete {
2756 entity: ENTITY_NAME.to_string(),
2757 key: InputValue::Nat64(1),
2758 })
2759 .collect();
2760 let over_bound = session
2761 .execute_trusted_dynamic_mutation_batch(requests)
2762 .expect_err("operation cap plus one must reject before row resolution");
2763 assert!(matches!(
2764 over_bound.diagnostic().detail(),
2765 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2766 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
2767 }),
2768 ));
2769 }
2770
2771 #[test]
2772 fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
2773 let mut exact = 0;
2774 add_structural_mutation_staged_bytes(
2775 &mut exact,
2776 [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
2777 )
2778 .expect("the exact staged-byte boundary should admit");
2779 assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
2780
2781 let error = add_structural_mutation_staged_bytes(&mut exact, [1])
2782 .expect_err("one byte above the staged-byte boundary must reject");
2783 assert!(matches!(
2784 error.diagnostic().detail(),
2785 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2786 boundary:
2787 icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
2788 }),
2789 ));
2790
2791 validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
2792 .expect("the exact result-byte boundary should admit");
2793 let error = validate_structural_mutation_result_bytes(
2794 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
2795 )
2796 .expect_err("one byte above the result-byte boundary must reject");
2797 assert!(matches!(
2798 error.diagnostic().detail(),
2799 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2800 boundary:
2801 icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
2802 }),
2803 ));
2804 }
2805
2806 #[expect(
2807 clippy::too_many_lines,
2808 reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
2809 )]
2810 #[test]
2811 fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
2812 let session = initialize();
2813 let catalog = session
2814 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
2815 .expect("identity catalog should resolve");
2816 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
2817 .expect("identity row layout should build");
2818 let initial_description = session
2819 .try_describe_entity_by_name(ENTITY_NAME)
2820 .expect("accepted Identity description should resolve");
2821 let initial_identity = initial_description
2822 .identity()
2823 .expect("accepted Identity policy should be described");
2824 assert_eq!(initial_identity.field(), "id");
2825 assert_eq!(initial_identity.generator(), "Identity::next");
2826 assert_eq!(initial_identity.accepted_kind(), "nat64");
2827 assert_eq!(initial_identity.minimum(), 1);
2828 assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
2829 assert_eq!(initial_identity.high_water(), 0);
2830 assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
2831 assert!(!initial_identity.exhausted());
2832
2833 let rejected = session
2834 .execute_accepted_structural_save_batch(
2835 &catalog,
2836 &descriptor,
2837 batch(&[1_000, 2_000]),
2838 Timestamp::from_millis(6),
2839 |_| Err::<(), _>(InternalError::executor_unsupported()),
2840 )
2841 .expect_err("a rejected precommit result must not publish its tentative range");
2842 assert_eq!(rejected.class(), ErrorClass::Unsupported);
2843 assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
2844
2845 let rows = session
2846 .execute_accepted_structural_save_batch(
2847 &catalog,
2848 &descriptor,
2849 batch(&[10, 20, 30]),
2850 Timestamp::from_millis(7),
2851 Ok,
2852 )
2853 .expect("one accepted batch should commit rows and one identity range");
2854 assert_eq!(
2855 rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
2856 vec![
2857 vec![Value::Nat64(1), Value::Nat64(10)],
2858 vec![Value::Nat64(2), Value::Nat64(20)],
2859 vec![Value::Nat64(3), Value::Nat64(30)],
2860 ],
2861 );
2862
2863 let dynamic = session
2864 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2865 entity: ENTITY_NAME.to_string(),
2866 patch: DynamicStructuralPatch::new(vec![(
2867 "payload".to_string(),
2868 DynamicWriteCell::Value(InputValue::Nat64(40)),
2869 )]),
2870 })
2871 .expect("dynamic omission should commit through shared Identity generation");
2872 assert_eq!(dynamic.affected_rows, 1);
2873
2874 for request in [
2875 DynamicMutation::Insert {
2876 entity: ENTITY_NAME.to_string(),
2877 patch: DynamicStructuralPatch::new(vec![
2878 (
2879 "id".to_string(),
2880 DynamicWriteCell::Value(InputValue::Nat64(41)),
2881 ),
2882 (
2883 "payload".to_string(),
2884 DynamicWriteCell::Value(InputValue::Nat64(42)),
2885 ),
2886 ]),
2887 },
2888 DynamicMutation::Update {
2889 entity: ENTITY_NAME.to_string(),
2890 key: InputValue::Nat64(1),
2891 patch: DynamicStructuralPatch::new(vec![(
2892 "id".to_string(),
2893 DynamicWriteCell::Default,
2894 )]),
2895 },
2896 ] {
2897 let error = session
2898 .execute_trusted_dynamic_mutation(&request)
2899 .expect_err("structural Identity authorship and regeneration must reject");
2900 assert_eq!(error.class(), ErrorClass::Unsupported);
2901 assert_eq!(error.origin(), ErrorOrigin::Executor);
2902 }
2903
2904 let binding = session
2905 .issue_typed_entity_binding(
2906 ENTITY_SOURCE,
2907 &[
2908 DynamicTypedFieldBindingRequest::new(
2909 ID_SOURCE.to_string(),
2910 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2911 false,
2912 ),
2913 DynamicTypedFieldBindingRequest::new(
2914 PAYLOAD_SOURCE.to_string(),
2915 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2916 false,
2917 ),
2918 ],
2919 )
2920 .expect("typed output should bind the Identity field");
2921 let typed_patch = binding
2922 .bind_write_fields(vec![(
2923 PAYLOAD_SOURCE.to_string(),
2924 DynamicWriteCell::Value(InputValue::Nat64(50)),
2925 )])
2926 .expect("typed payload should lower");
2927 let typed = session
2928 .execute_trusted_typed_mutation(
2929 &binding,
2930 &DynamicTypedMutation::Insert { patch: typed_patch },
2931 )
2932 .expect("typed omission should commit through shared Identity generation");
2933 assert_eq!(
2934 typed
2935 .expect("typed insert should return one mutation result")
2936 .affected_rows,
2937 1,
2938 );
2939 let explicit_typed_patch = binding
2940 .bind_write_fields(vec![
2941 (
2942 ID_SOURCE.to_string(),
2943 DynamicWriteCell::Value(InputValue::Nat64(51)),
2944 ),
2945 (
2946 PAYLOAD_SOURCE.to_string(),
2947 DynamicWriteCell::Value(InputValue::Nat64(52)),
2948 ),
2949 ])
2950 .expect("the low-level binding should retain exact authored intent");
2951 let explicit_typed_error = session
2952 .execute_trusted_typed_mutation(
2953 &binding,
2954 &DynamicTypedMutation::Insert {
2955 patch: explicit_typed_patch,
2956 },
2957 )
2958 .expect_err("typed Identity authorship must reject before allocation");
2959 assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
2960 assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
2961
2962 let replace_error = session
2963 .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
2964 entity: ENTITY_NAME.to_string(),
2965 key: InputValue::Nat64(99),
2966 patch: DynamicStructuralPatch::new(vec![(
2967 "payload".to_string(),
2968 DynamicWriteCell::Value(InputValue::Nat64(60)),
2969 )]),
2970 })
2971 .expect_err("save-as-insert with a chosen Identity must reject");
2972 assert_eq!(replace_error.class(), ErrorClass::Unsupported);
2973 assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
2974
2975 #[cfg(feature = "sql")]
2976 {
2977 for sql in [
2978 "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
2979 "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
2980 ] {
2981 let _result = session
2982 .execute_trusted_sql_mutation(sql)
2983 .expect("SQL omission and DEFAULT should commit Identity generation");
2984 }
2985
2986 let error = session
2987 .execute_trusted_sql_mutation(
2988 "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
2989 )
2990 .expect_err("an explicit SQL Identity value must reject before allocation");
2991 let diagnostic = error.diagnostic();
2992 assert_eq!(
2993 diagnostic.code(),
2994 icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
2995 );
2996 assert!(matches!(
2997 diagnostic.detail(),
2998 Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
2999 boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
3000 }),
3001 ));
3002 }
3003
3004 let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
3005 assert_eq!(
3006 DATA_STORE.with(|store| store.borrow().len()),
3007 expected_committed
3008 );
3009 SCHEMA_STORE.with(|store| {
3010 let cursor = store
3011 .borrow()
3012 .identity_statement_cursor(
3013 database_incarnation_id().expect("database incarnation should remain readable"),
3014 ENTITY_TAG,
3015 FieldId::new(1),
3016 &AcceptedFieldKind::Nat64,
3017 )
3018 .expect("committed writes must leave active state readable");
3019 assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
3020 assert!(!cursor.has_allocations());
3021 });
3022 let committed_description = session
3023 .try_describe_entity_by_name(ENTITY_NAME)
3024 .expect("committed Identity description should resolve");
3025 let committed_identity = committed_description
3026 .identity()
3027 .expect("accepted Identity policy should remain described");
3028 assert_eq!(
3029 committed_identity.high_water(),
3030 u128::from(expected_committed),
3031 );
3032 assert_eq!(
3033 committed_identity.remaining(),
3034 u128::from(u64::MAX - expected_committed),
3035 );
3036 assert!(!committed_identity.exhausted());
3037 }
3038
3039 #[test]
3040 #[expect(
3041 clippy::too_many_lines,
3042 reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
3043 )]
3044 fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
3045 let session = initialize_journaled();
3046 let catalog = session
3047 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3048 .expect("journaled identity catalog should resolve");
3049 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3050 .expect("journaled identity row layout should build");
3051
3052 for (ordinal, interruption) in [
3053 MutationCommitInterruption::MarkerPersisted,
3054 MutationCommitInterruption::JournalPublished,
3055 MutationCommitInterruption::RowsPublished,
3056 MutationCommitInterruption::StateMaterialized,
3057 ]
3058 .into_iter()
3059 .enumerate()
3060 {
3061 interrupt_next_mutation_commit_for_tests(interruption);
3062 let interrupted = session.execute_accepted_structural_save_batch(
3063 &catalog,
3064 &descriptor,
3065 batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
3066 Timestamp::from_millis(8),
3067 Ok,
3068 );
3069 assert!(
3070 interrupted.is_err(),
3071 "the selected durable boundary should interrupt",
3072 );
3073
3074 let committed = session
3075 .execute_accepted_structural_save_batch(
3076 &catalog,
3077 &descriptor,
3078 batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
3079 Timestamp::from_millis(9),
3080 Ok,
3081 )
3082 .expect("the next mutation must recover before allocating");
3083 let expected_high_water =
3084 u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
3085 assert_eq!(
3086 committed
3087 .into_iter()
3088 .map(|row| row.values)
3089 .collect::<Vec<_>>(),
3090 vec![vec![
3091 Value::Nat64(expected_high_water),
3092 Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
3093 ]],
3094 );
3095 assert_eq!(
3096 JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
3097 expected_high_water,
3098 );
3099 JOURNALED_SCHEMA_STORE.with(|store| {
3100 let cursor = store
3101 .borrow()
3102 .identity_statement_cursor(
3103 database_incarnation_id()
3104 .expect("database incarnation should remain readable"),
3105 ENTITY_TAG,
3106 FieldId::new(1),
3107 &AcceptedFieldKind::Nat64,
3108 )
3109 .expect("guarded recovery must leave quiescent active state");
3110 assert_eq!(
3111 cursor.expected_high_water(),
3112 u128::from(expected_high_water),
3113 );
3114 assert!(!cursor.has_allocations());
3115 });
3116 }
3117
3118 for (ordinal, (interruption, deleted_key)) in [
3119 (MutationCommitInterruption::MarkerPersisted, 2),
3120 (MutationCommitInterruption::JournalPublished, 4),
3121 (MutationCommitInterruption::RowPrefixPublished, 6),
3122 (MutationCommitInterruption::RowsPublished, 8),
3123 (MutationCommitInterruption::StateMaterialized, 7),
3124 ]
3125 .into_iter()
3126 .enumerate()
3127 {
3128 let expected_payload =
3129 501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
3130 interrupt_next_mutation_commit_for_tests(interruption);
3131 let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
3132 DynamicMutation::Update {
3133 entity: ENTITY_NAME.to_string(),
3134 key: InputValue::Nat64(1),
3135 patch: dynamic_payload_patch(expected_payload),
3136 },
3137 DynamicMutation::Delete {
3138 entity: ENTITY_NAME.to_string(),
3139 key: InputValue::Nat64(deleted_key),
3140 },
3141 ]);
3142 assert!(
3143 interrupted.is_err(),
3144 "the selected caller-key mixed publication boundary should interrupt",
3145 );
3146 let recovered_update = session
3147 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3148 entity: ENTITY_NAME.to_string(),
3149 key: InputValue::Nat64(1),
3150 patch: dynamic_payload_patch(expected_payload),
3151 })
3152 .expect("guarded reentry should complete the marker-authorized mixed batch");
3153 assert_eq!(
3154 recovered_update.affected_rows, 0,
3155 "the recovered update must already expose its admitted final image",
3156 );
3157 let recovered_delete = session
3158 .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
3159 entity: ENTITY_NAME.to_string(),
3160 key: InputValue::Nat64(deleted_key),
3161 })
3162 .expect_err("the recovered delete must already be materialized");
3163 assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
3164 JOURNALED_SCHEMA_STORE.with(|store| {
3165 let cursor = store
3166 .borrow()
3167 .identity_statement_cursor(
3168 database_incarnation_id()
3169 .expect("database incarnation should remain readable"),
3170 ENTITY_TAG,
3171 FieldId::new(1),
3172 &AcceptedFieldKind::Nat64,
3173 )
3174 .expect("caller-key recovery must preserve active Identity state");
3175 assert_eq!(cursor.expected_high_water(), 8);
3176 assert!(!cursor.has_allocations());
3177 });
3178 }
3179
3180 forget_recovered_domain_for_tests(&session.db)
3181 .expect("the final journal tail should remain recoverable");
3182 session
3183 .db
3184 .ensure_recovered_state()
3185 .expect("derived rebuild must not allocate another identity");
3186
3187 let quick = execute_quick_integrity(&session.db, catalog.inspection_plan())
3188 .expect("quiescent Identity control inventory should be inspectable");
3189 assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
3190 let row_page = execute_row_integrity_page(
3191 &session.db,
3192 catalog.inspection_plan(),
3193 PhysicalUnitCheckpoint::BeforeFirst,
3194 RowInspectionLimits::standard(),
3195 )
3196 .expect("Identity rows should remain within committed high-water");
3197 assert!(row_page.exhausted());
3198 assert!(row_page.findings().is_empty());
3199
3200 assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
3201 assert!(
3202 JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
3203 "derived index rebuild should restore witnesses without allocating identities",
3204 );
3205 assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
3206 JOURNALED_SCHEMA_STORE.with(|store| {
3207 let cursor = store
3208 .borrow()
3209 .identity_statement_cursor(
3210 database_incarnation_id().expect("database incarnation should remain readable"),
3211 ENTITY_TAG,
3212 FieldId::new(1),
3213 &AcceptedFieldKind::Nat64,
3214 )
3215 .expect("folded identity state should reopen without allocating");
3216 assert_eq!(cursor.expected_high_water(), 8);
3217 assert!(!cursor.has_allocations());
3218 });
3219 }
3220
3221 #[test]
3222 #[ignore = "release-closeout native timing probe for one marker-authorized Identity recovery"]
3223 fn identity_recovery_closeout_reports_guarded_reentry_time() {
3224 let session = initialize_journaled();
3225 let catalog = session
3226 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3227 .expect("journaled identity catalog should resolve");
3228 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3229 .expect("journaled identity row layout should build");
3230
3231 interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
3232 let interrupted = session.execute_accepted_structural_save_batch(
3233 &catalog,
3234 &descriptor,
3235 batch(&[1]),
3236 Timestamp::from_millis(10),
3237 Ok,
3238 );
3239 assert!(
3240 interrupted.is_err(),
3241 "the selected publication boundary should interrupt",
3242 );
3243
3244 let start = Instant::now();
3245 let committed = session
3246 .execute_accepted_structural_save_batch(
3247 &catalog,
3248 &descriptor,
3249 batch(&[2]),
3250 Timestamp::from_millis(11),
3251 Ok,
3252 )
3253 .expect("guarded reentry should recover before allocation");
3254 let elapsed = start.elapsed();
3255 assert_eq!(
3256 committed
3257 .into_iter()
3258 .map(|row| row.values)
3259 .collect::<Vec<_>>(),
3260 vec![vec![Value::Nat64(2), Value::Nat64(2)]],
3261 );
3262
3263 println!(
3264 "identity recovery closeout: guarded_reentry_nanos={}",
3265 elapsed.as_nanos(),
3266 );
3267 }
3268}
3269
3270#[cfg(test)]
3271mod targeted_rule_mutation_tests {
3272 use super::{
3273 DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
3274 DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
3275 };
3276 use crate::{
3277 db::{
3278 data::{DataStore, encode_input_value_for_candidate_field_contract},
3279 index::IndexStore,
3280 registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
3281 schema::{
3282 AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
3283 AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
3284 AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
3285 ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
3286 PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
3287 ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
3288 SchemaRowLayout, SchemaStore, SchemaVersion,
3289 accepted_schema_candidate_with_catalogs_for_tests,
3290 build_record_newtype_composite_catalog_for_tests,
3291 empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
3292 },
3293 },
3294 error::{
3295 ConstraintDiagnostic, ConstraintDiagnosticKind, ConstraintValuePathComponent,
3296 InternalError,
3297 },
3298 traits::{CanisterKind, Path},
3299 types::EntityTag,
3300 value::InputValue,
3301 };
3302 use icydb_schema::{
3303 ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
3304 };
3305 use std::{cell::RefCell, collections::BTreeMap};
3306
3307 const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
3308 const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
3309 const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
3310 const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
3311 const UPDATED_AT_SOURCE: &str =
3312 "session::write::targeted_rule_mutation_tests::Entity::updated_at";
3313 const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
3314 const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
3315 const DEGREE_MEMBER_SOURCE: &str =
3316 "session::write::targeted_rule_mutation_tests::Profile::degree";
3317 const DEGREE_RULE_SOURCE: &str =
3318 "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
3319
3320 struct TestCanister;
3321
3322 impl Path for TestCanister {
3323 const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
3324 }
3325
3326 impl CanisterKind for TestCanister {
3327 const COMMIT_MEMORY_ID: u8 = 43;
3328 const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
3329 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
3330 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
3331 "icydb.targeted_mutation_tests.integrity.progress.v1";
3332 }
3333
3334 thread_local! {
3335 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
3336 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
3337 static SCHEMA_STORE: RefCell<SchemaStore> =
3338 const { RefCell::new(SchemaStore::init_heap()) };
3339 static STORE_REGISTRY: StoreRegistry = {
3340 let mut registry = StoreRegistry::new();
3341 registry.register_store(
3342 STORE_PATH,
3343 &DATA_STORE,
3344 &INDEX_STORE,
3345 &SCHEMA_STORE,
3346 StoreAllocationIdentities::absent(),
3347 StoreRuntimeStorageCapabilities::heap(),
3348 ).expect("targeted mutation test store should register");
3349 registry
3350 };
3351 }
3352
3353 fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
3354 parse(raw.to_string()).expect("test source identity should admit")
3355 }
3356
3357 fn profile_input(degree: u64) -> InputValue {
3358 InputValue::Map(vec![(
3359 InputValue::Text("degree".to_string()),
3360 InputValue::Nat64(degree),
3361 )])
3362 }
3363
3364 fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
3365 DynamicStructuralPatch::new(vec![
3366 (
3367 "id".to_string(),
3368 DynamicWriteCell::Value(InputValue::Nat64(id)),
3369 ),
3370 (
3371 "profile".to_string(),
3372 DynamicWriteCell::Value(profile_input(degree)),
3373 ),
3374 ])
3375 }
3376
3377 fn encoded_value(
3378 enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3379 composite_catalog: &AcceptedCompositeCatalog,
3380 name: &str,
3381 kind: &AcceptedFieldKind,
3382 storage_decode: FieldStorageDecode,
3383 leaf_codec: LeafCodec,
3384 value: InputValue,
3385 ) -> Vec<u8> {
3386 let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
3387 encode_input_value_for_candidate_field_contract(
3388 enum_catalog,
3389 composite_catalog,
3390 field,
3391 value,
3392 &mut ValueAdmissionBudget::standard(),
3393 )
3394 .expect("test accepted value should encode")
3395 }
3396
3397 fn nat64_literal(
3398 enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
3399 composite_catalog: &AcceptedCompositeCatalog,
3400 value: u64,
3401 ) -> AcceptedCheckLiteralV1 {
3402 let kind = AcceptedFieldKind::Nat64;
3403 AcceptedCheckLiteralV1::from_accepted_parts(
3404 kind.clone(),
3405 FieldStorageDecode::ByKind,
3406 LeafCodec::Scalar(ScalarCodec::Nat64),
3407 encoded_value(
3408 enum_catalog,
3409 composite_catalog,
3410 "degree_bound",
3411 &kind,
3412 FieldStorageDecode::ByKind,
3413 LeafCodec::Scalar(ScalarCodec::Nat64),
3414 InputValue::Nat64(value),
3415 ),
3416 )
3417 }
3418
3419 fn targeted_diagnostic(error: &InternalError) -> &ConstraintDiagnostic {
3420 let diagnostic = error
3421 .constraint_diagnostic()
3422 .expect("targeted mutation should retain a public diagnostic");
3423 assert_eq!(
3424 diagnostic.constraint_kind(),
3425 ConstraintDiagnosticKind::TargetedRule
3426 );
3427 assert_eq!(diagnostic.field_paths(), &["profile".to_string()]);
3428 assert_eq!(
3429 diagnostic
3430 .value_path()
3431 .expect("targeted mutation should retain its typed value path")
3432 .components(),
3433 &[
3434 ConstraintValuePathComponent::RootField { field_id: 2 },
3435 ConstraintValuePathComponent::RecordMember {
3436 composite_type_id: 1,
3437 member_id: 1,
3438 },
3439 ],
3440 );
3441 diagnostic
3442 }
3443
3444 #[expect(
3445 clippy::too_many_lines,
3446 reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
3447 )]
3448 #[test]
3449 fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
3450 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
3451 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
3452 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
3453
3454 let entity_tag = EntityTag::new(93);
3455 let enum_catalog = empty_accepted_enum_catalog_for_tests();
3456 let (composite_catalog, profile_type, degree_type, degree_member) =
3457 build_record_newtype_composite_catalog_for_tests(
3458 "tests::TargetedProfile".to_string(),
3459 "degree".to_string(),
3460 "tests::TargetedDegree".to_string(),
3461 AcceptedFieldKind::Nat64,
3462 &enum_catalog,
3463 )
3464 .expect("targeted mutation composites should close");
3465 let profile_kind = AcceptedFieldKind::Composite {
3466 type_id: profile_type,
3467 };
3468 let profile_default = encoded_value(
3469 &enum_catalog,
3470 &composite_catalog,
3471 "profile",
3472 &profile_kind,
3473 FieldStorageDecode::CatalogValue,
3474 LeafCodec::Structural,
3475 profile_input(12),
3476 );
3477 let fields = vec![
3478 PersistedFieldSnapshot::new_initial(
3479 FieldId::new(1),
3480 "id".to_string(),
3481 SchemaFieldSlot::new(0),
3482 AcceptedFieldKind::Nat64,
3483 Vec::new(),
3484 false,
3485 SchemaInsertDefault::None,
3486 FieldStorageDecode::ByKind,
3487 LeafCodec::Scalar(ScalarCodec::Nat64),
3488 ),
3489 PersistedFieldSnapshot::new_initial(
3490 FieldId::new(2),
3491 "profile".to_string(),
3492 SchemaFieldSlot::new(1),
3493 profile_kind,
3494 vec![PersistedNestedLeafSnapshot::new(
3495 vec!["degree".to_string()],
3496 AcceptedFieldKind::Composite {
3497 type_id: degree_type,
3498 },
3499 false,
3500 )],
3501 false,
3502 SchemaInsertDefault::SlotPayload(profile_default),
3503 FieldStorageDecode::CatalogValue,
3504 LeafCodec::Structural,
3505 ),
3506 PersistedFieldSnapshot::new_initial_with_write_policy(
3507 FieldId::new(3),
3508 "updated_at".to_string(),
3509 SchemaFieldSlot::new(2),
3510 AcceptedFieldKind::Timestamp,
3511 Vec::new(),
3512 false,
3513 SchemaInsertDefault::None,
3514 SchemaFieldWritePolicy::from_model_policies(
3515 None,
3516 Some(FieldWriteManagement::UpdatedAt),
3517 ),
3518 FieldStorageDecode::ByKind,
3519 LeafCodec::Scalar(ScalarCodec::Timestamp),
3520 ),
3521 ];
3522 let mut snapshot = PersistedSchemaSnapshot::new(
3523 SchemaVersion::initial(),
3524 ENTITY_SOURCE.to_string(),
3525 "TargetedMutation".to_string(),
3526 FieldId::new(1),
3527 SchemaRowLayout::initial(
3528 fields
3529 .iter()
3530 .map(|field| (field.id(), field.slot()))
3531 .collect(),
3532 ),
3533 fields,
3534 );
3535 let constraint_catalog = snapshot
3536 .constraint_catalog()
3537 .clone()
3538 .with_added_targeted_rule(
3539 "profile_degree_multiple".to_string(),
3540 ConstraintOrigin::Generated,
3541 AcceptedRuleTarget::new(
3542 FieldId::new(2),
3543 AcceptedNamedTypeIdentity::Composite(degree_type),
3544 ),
3545 AcceptedRuleOperation::MultipleOf {
3546 divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
3547 },
3548 )
3549 .expect("targeted mutation rule should allocate");
3550 let targeted_rule_id = constraint_catalog
3551 .constraints()
3552 .last()
3553 .expect("targeted mutation rule should persist")
3554 .id();
3555 snapshot = snapshot.with_constraint_catalog(constraint_catalog);
3556
3557 let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
3558 let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
3559 let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
3560 let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
3561 let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
3562 let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
3563 let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
3564 let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
3565 let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
3566 BTreeMap::from([(entity_source, entity_tag)]),
3567 BTreeMap::from([
3568 ((entity_tag, id_source), FieldId::new(1)),
3569 ((entity_tag, profile_source), FieldId::new(2)),
3570 ((entity_tag, updated_at_source), FieldId::new(3)),
3571 ]),
3572 BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
3573 BTreeMap::new(),
3574 BTreeMap::new(),
3575 )
3576 .with_initial_named_types_for_tests(
3577 BTreeMap::from([
3578 (
3579 profile_type_source,
3580 AcceptedNamedTypeIdentity::Composite(profile_type),
3581 ),
3582 (
3583 degree_type_source,
3584 AcceptedNamedTypeIdentity::Composite(degree_type),
3585 ),
3586 ]),
3587 BTreeMap::new(),
3588 BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
3589 );
3590 let candidate = accepted_schema_candidate_with_catalogs_for_tests(
3591 STORE_PATH,
3592 AcceptedSchemaRevision::INITIAL,
3593 enum_catalog,
3594 composite_catalog,
3595 source_bindings,
3596 BTreeMap::from([(entity_tag, snapshot)]),
3597 );
3598
3599 let session = DbSession::<TestCanister>::new(&STORE_REGISTRY);
3600 session
3601 .db
3602 .ensure_recovered_state()
3603 .expect("targeted mutation test database should initialize");
3604 let store = session
3605 .db
3606 .store_handle(STORE_PATH)
3607 .expect("targeted mutation test store should resolve");
3608 crate::db::commit::publish_accepted_schema_candidate(
3609 STORE_PATH,
3610 store,
3611 AcceptedSchemaRevision::NONE,
3612 &candidate,
3613 )
3614 .expect("targeted mutation candidate should publish");
3615
3616 let dynamic_error = session
3617 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3618 entity: "TargetedMutation".to_string(),
3619 patch: structural_patch(1, 12),
3620 })
3621 .expect_err("dynamic write must enforce the targeted rule");
3622 let dynamic_diagnostic = targeted_diagnostic(&dynamic_error);
3623 assert_eq!(dynamic_diagnostic.constraint_id(), targeted_rule_id.get());
3624
3625 let binding = session
3626 .issue_typed_entity_binding(
3627 ENTITY_SOURCE,
3628 &[
3629 DynamicTypedFieldBindingRequest::new(
3630 ID_SOURCE.to_string(),
3631 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3632 false,
3633 ),
3634 DynamicTypedFieldBindingRequest::new(
3635 PROFILE_SOURCE.to_string(),
3636 DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
3637 false,
3638 ),
3639 DynamicTypedFieldBindingRequest::new(
3640 UPDATED_AT_SOURCE.to_string(),
3641 DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
3642 false,
3643 ),
3644 ],
3645 )
3646 .expect("targeted typed binding should issue");
3647 let typed_patch = binding
3648 .bind_write_fields(vec![
3649 (
3650 ID_SOURCE.to_string(),
3651 DynamicWriteCell::Value(InputValue::Nat64(2)),
3652 ),
3653 (
3654 PROFILE_SOURCE.to_string(),
3655 DynamicWriteCell::Value(profile_input(12)),
3656 ),
3657 ])
3658 .expect("targeted typed patch should bind");
3659 let typed_error = session
3660 .execute_trusted_typed_mutation(
3661 &binding,
3662 &DynamicTypedMutation::Insert { patch: typed_patch },
3663 )
3664 .expect_err("typed write must enforce the targeted rule");
3665 assert_eq!(
3666 targeted_diagnostic(&typed_error).constraint_id(),
3667 targeted_rule_id.get()
3668 );
3669
3670 #[cfg(feature = "sql")]
3671 {
3672 let sql_error = session
3673 .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
3674 .expect_err("SQL default resolution must enforce the targeted rule");
3675 let crate::db::QueryError::Execute(execute) = sql_error else {
3676 panic!("targeted SQL write should fail at shared execution admission");
3677 };
3678 assert_eq!(
3679 targeted_diagnostic(execute.as_internal()).constraint_id(),
3680 targeted_rule_id.get()
3681 );
3682 }
3683
3684 session
3685 .execute_trusted_dynamic_mutation_batch(vec![
3686 DynamicMutation::Insert {
3687 entity: "TargetedMutation".to_string(),
3688 patch: structural_patch(4, 5),
3689 },
3690 DynamicMutation::Insert {
3691 entity: "TargetedMutation".to_string(),
3692 patch: structural_patch(5, 12),
3693 },
3694 ])
3695 .expect_err("one invalid targeted value must reject the whole batch");
3696 assert_eq!(
3697 DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3698 Some(0),
3699 "no frontend or earlier valid batch row may escape targeted admission",
3700 );
3701
3702 let admitted = session
3703 .execute_trusted_dynamic_mutation_batch(vec![
3704 DynamicMutation::Insert {
3705 entity: "TargetedMutation".to_string(),
3706 patch: structural_patch(6, 5),
3707 },
3708 DynamicMutation::Insert {
3709 entity: "TargetedMutation".to_string(),
3710 patch: structural_patch(7, 10),
3711 },
3712 ])
3713 .expect("compliant targeted values should share one accepted batch");
3714 let [first, second] = admitted.rows.as_slice() else {
3715 panic!("the mixed targeted batch should return two rows");
3716 };
3717 let first_timestamp = first
3718 .get(2)
3719 .expect("the first mixed row should contain its managed timestamp");
3720 assert!(matches!(
3721 first_timestamp,
3722 crate::value::OutputValue::Timestamp(_)
3723 ));
3724 assert_eq!(
3725 second.get(2),
3726 Some(first_timestamp),
3727 "one accepted mixed batch must materialize one managed timestamp",
3728 );
3729 assert_eq!(
3730 DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
3731 Some(2),
3732 );
3733 }
3734}