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