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(
1600 &STORE_REGISTRY,
1601 &crate::db::RequestExecutionRoot::__new_runtime_root(),
1602 );
1603 session
1604 .db
1605 .ensure_recovered_state()
1606 .expect("typed adapter test database should initialize");
1607 publish(
1608 &session,
1609 AcceptedSchemaRevision::NONE,
1610 AcceptedSchemaRevision::INITIAL,
1611 BTreeMap::from([(
1612 entity_tag,
1613 snapshot(
1614 ENTITY_SOURCE,
1615 "Entity",
1616 vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1617 ),
1618 )]),
1619 BTreeMap::from([
1620 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1621 ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1622 ]),
1623 );
1624
1625 let initial_catalog = session
1626 .find_accepted_schema_catalog_context_for_entity_source_key(ENTITY_SOURCE)
1627 .expect("initial source catalog lookup should inspect")
1628 .expect("initial source catalog should exist");
1629 assert_eq!(initial_catalog.identity().entity_tag(), entity_tag);
1630 let initial = session
1631 .issue_typed_entity_binding(
1632 entity_source(ENTITY_SOURCE).as_str(),
1633 &[request(ID_SOURCE), request(VALUE_SOURCE)],
1634 )
1635 .expect("initial typed binding should issue");
1636 assert_eq!(initial.field_slot(ID_SOURCE), Some(0));
1637 assert_eq!(initial.field_slot(VALUE_SOURCE), Some(1));
1638 assert_eq!(initial.output_field_slot("value"), Some(1));
1639 let initial_patch = initial
1640 .bind_write_fields(vec![(
1641 VALUE_SOURCE.to_string(),
1642 DynamicWriteCell::Value(InputValue::Nat64(7)),
1643 )])
1644 .expect("source-bound patch should lower");
1645 assert_eq!(
1646 initial_patch.fields(),
1647 &[(2, 1, DynamicWriteCell::Value(InputValue::Nat64(7)))]
1648 );
1649
1650 publish(
1651 &session,
1652 AcceptedSchemaRevision::INITIAL,
1653 AcceptedSchemaRevision::new(2),
1654 BTreeMap::from([
1655 (
1656 entity_tag,
1657 snapshot(
1658 ENTITY_SOURCE,
1659 "RenamedEntity",
1660 vec![
1661 nat64_field(1, "id", 0),
1662 nat64_field(2, "renamed_value", 1),
1663 nat64_field(3, "value", 2),
1664 ],
1665 ),
1666 ),
1667 (
1668 other_entity_tag,
1669 snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1670 ),
1671 ]),
1672 BTreeMap::from([
1673 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1674 ((entity_tag, field_source(VALUE_SOURCE)), FieldId::new(2)),
1675 (
1676 (entity_tag, field_source(REPLACEMENT_SOURCE)),
1677 FieldId::new(3),
1678 ),
1679 (
1680 (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1681 FieldId::new(1),
1682 ),
1683 ]),
1684 );
1685
1686 let stale_authority = session
1687 .ensure_accepted_schema_authority_is_current_for_store_path(
1688 STORE_PATH,
1689 initial_catalog.value_catalog_handle().authority(),
1690 )
1691 .expect_err("the initial accepted authority must be stale after revision two");
1692 assert_eq!(
1693 stale_authority.diagnostic_facts(),
1694 vec![
1695 (
1696 icydb_diagnostic_code::DiagnosticFactTag::ExpectedRevision,
1697 AcceptedSchemaRevision::INITIAL.get(),
1698 ),
1699 (
1700 icydb_diagnostic_code::DiagnosticFactTag::CurrentRevision,
1701 AcceptedSchemaRevision::new(2).get(),
1702 ),
1703 ],
1704 );
1705
1706 assert!(
1707 !session
1708 .typed_entity_binding_is_current(&initial)
1709 .expect("renamed binding currentness should inspect")
1710 );
1711 let renamed = session
1712 .issue_typed_entity_binding(ENTITY_SOURCE, &[request(ID_SOURCE), request(VALUE_SOURCE)])
1713 .expect("renamed source-bound adapter should rebind");
1714 assert_eq!(renamed.entity(), "RenamedEntity");
1715 assert_eq!(renamed.field_slot(VALUE_SOURCE), Some(1));
1716 assert_eq!(renamed.output_field_slot("renamed_value"), Some(1));
1717 assert_eq!(renamed.output_field_slot("value"), None);
1718
1719 publish(
1720 &session,
1721 AcceptedSchemaRevision::new(2),
1722 AcceptedSchemaRevision::new(3),
1723 BTreeMap::from([
1724 (
1725 entity_tag,
1726 snapshot(
1727 ENTITY_SOURCE,
1728 "RenamedEntity",
1729 vec![nat64_field(1, "id", 0), nat64_field(2, "value", 1)],
1730 ),
1731 ),
1732 (
1733 other_entity_tag,
1734 snapshot(OTHER_ENTITY_SOURCE, "Entity", vec![nat64_field(1, "id", 0)]),
1735 ),
1736 ]),
1737 BTreeMap::from([
1738 ((entity_tag, field_source(ID_SOURCE)), FieldId::new(1)),
1739 (
1740 (entity_tag, field_source(REPLACEMENT_SOURCE)),
1741 FieldId::new(2),
1742 ),
1743 (
1744 (other_entity_tag, field_source(OTHER_ID_SOURCE)),
1745 FieldId::new(1),
1746 ),
1747 ]),
1748 );
1749
1750 assert!(matches!(
1751 session.issue_typed_entity_binding(
1752 ENTITY_SOURCE,
1753 &[request(ID_SOURCE), request(VALUE_SOURCE)],
1754 ),
1755 Err(DynamicTypedBindingError::FieldUnavailable),
1756 ));
1757 assert!(
1758 !session
1759 .typed_entity_binding_is_current(&renamed)
1760 .expect("removed source binding should become stale")
1761 );
1762
1763 let replacement = session
1764 .issue_typed_entity_binding(
1765 ENTITY_SOURCE,
1766 &[request(ID_SOURCE), request(REPLACEMENT_SOURCE)],
1767 )
1768 .expect("explicit replacement source should bind");
1769 assert!(
1770 session
1771 .execute_trusted_typed_mutation(
1772 &replacement,
1773 &DynamicTypedMutation::Insert {
1774 patch: initial_patch
1775 },
1776 )
1777 .expect("cross-binding patch should fail closed")
1778 .is_none()
1779 );
1780 let patch = replacement
1781 .bind_write_fields(vec![
1782 (
1783 ID_SOURCE.to_string(),
1784 DynamicWriteCell::Value(InputValue::Nat64(1)),
1785 ),
1786 (
1787 REPLACEMENT_SOURCE.to_string(),
1788 DynamicWriteCell::Value(InputValue::Nat64(9)),
1789 ),
1790 ])
1791 .expect("replacement source write should bind by accepted IDs and slots");
1792 let result = session
1793 .execute_trusted_typed_mutation(&replacement, &DynamicTypedMutation::Insert { patch })
1794 .expect("typed insert should use the accepted mutation pipeline")
1795 .expect("replacement binding should remain current");
1796 assert_eq!(result.entity, "RenamedEntity");
1797 assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1798 assert_eq!(
1799 result.rows,
1800 vec![vec![
1801 crate::value::OutputValue::Nat64(1),
1802 crate::value::OutputValue::Nat64(9)
1803 ]]
1804 );
1805 assert_eq!(result.affected_rows, 1);
1806
1807 let second_patch = replacement
1808 .bind_write_fields(vec![
1809 (
1810 ID_SOURCE.to_string(),
1811 DynamicWriteCell::Value(InputValue::Nat64(2)),
1812 ),
1813 (
1814 REPLACEMENT_SOURCE.to_string(),
1815 DynamicWriteCell::Value(InputValue::Nat64(10)),
1816 ),
1817 ])
1818 .expect("second source-bound patch should lower");
1819 session
1820 .execute_trusted_typed_mutation(
1821 &replacement,
1822 &DynamicTypedMutation::Insert {
1823 patch: second_patch,
1824 },
1825 )
1826 .expect("second typed insert should use the accepted mutation pipeline")
1827 .expect("replacement binding should remain current");
1828
1829 {
1830 let query = crate::db::DynamicQuery::new("RenamedEntity")
1831 .select(["id", "value"])
1832 .order_by(crate::db::asc("id"))
1833 .limit(1);
1834 let result = session
1835 .execute_trusted_dynamic_query(&query)
1836 .expect("SQL-free dynamic execution should use accepted authority");
1837 assert_eq!(result.entity, "RenamedEntity");
1838 assert_eq!(result.columns, vec!["id".to_string(), "value".to_string()]);
1839 assert_eq!(
1840 result.rows,
1841 vec![vec![
1842 crate::value::OutputValue::Nat64(1),
1843 crate::value::OutputValue::Nat64(9)
1844 ]]
1845 );
1846 assert_eq!(result.row_count, 1);
1847 assert_query_diagnostic(
1848 session
1849 .execute_trusted_dynamic_query(&query.cursor("00"))
1850 .expect_err("scalar execution must reject grouped cursor state"),
1851 icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1852 icydb_diagnostic_code::ErrorOrigin::Query,
1853 icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1854 kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1855 },
1856 );
1857 assert_query_diagnostic(
1858 session
1859 .execute_public_dynamic_grouped_query(
1860 &crate::db::DynamicQuery::new("RenamedEntity").grouped_limits(1, 1024),
1861 )
1862 .expect_err("grouped execution must reject scalar query state"),
1863 icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1864 icydb_diagnostic_code::ErrorOrigin::Query,
1865 icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1866 kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1867 },
1868 );
1869
1870 let grouped_query = crate::db::DynamicQuery::new("RenamedEntity")
1871 .filter(crate::db::FieldRef::new("id").eq(1_u64))
1872 .group_by("value")
1873 .aggregate(crate::db::count())
1874 .grouped_limits(1, 1024)
1875 .limit(1);
1876 let grouped = session
1877 .execute_public_dynamic_grouped_query(&grouped_query)
1878 .expect("SQL-free grouped execution should use accepted authority");
1879 let typed_grouped = session
1880 .execute_public_dynamic_grouped_query_for_typed_binding(
1881 &replacement,
1882 &grouped_query,
1883 )
1884 .expect("typed grouped execution should inspect accepted authority")
1885 .expect("replacement binding should remain current");
1886 assert_eq!(typed_grouped, grouped);
1887 assert!(
1888 session
1889 .execute_public_dynamic_grouped_query_for_typed_binding(
1890 &renamed,
1891 &grouped_query,
1892 )
1893 .expect("stale grouped binding should inspect accepted authority")
1894 .is_none(),
1895 "stale typed grouped bindings must fail closed before execution"
1896 );
1897 assert_eq!(grouped.entity, "RenamedEntity");
1898 assert_eq!(grouped.row_count, 1);
1899 assert_eq!(grouped.rows.len(), 1);
1900 assert_eq!(
1901 grouped.rows[0].group_key(),
1902 &[crate::value::OutputValue::Nat64(9)]
1903 );
1904 assert_eq!(
1905 grouped.rows[0].aggregate_values(),
1906 &[crate::value::OutputValue::Nat64(1)]
1907 );
1908 assert_eq!(grouped.next_cursor, None);
1909
1910 assert_query_diagnostic(
1911 session
1912 .execute_public_dynamic_grouped_query(&grouped_query.clone().select(["value"]))
1913 .expect_err("grouped output must reject scalar selection"),
1914 icydb_diagnostic_code::DiagnosticCode::QueryIntent,
1915 icydb_diagnostic_code::ErrorOrigin::Query,
1916 icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1917 kind: icydb_diagnostic_code::QueryErrorKind::Intent,
1918 },
1919 );
1920 assert_query_diagnostic(
1921 session
1922 .execute_public_dynamic_grouped_query(
1923 &crate::db::DynamicQuery::new("RenamedEntity")
1924 .group_by("value")
1925 .aggregate(crate::db::count()),
1926 )
1927 .expect_err("public grouped execution must require explicit limits"),
1928 icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1929 icydb_diagnostic_code::ErrorOrigin::Query,
1930 icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1931 reason:
1932 icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1933 },
1934 );
1935 assert_query_diagnostic(
1936 session
1937 .execute_trusted_dynamic_grouped_query(
1938 &crate::db::DynamicQuery::new("RenamedEntity")
1939 .group_by("value")
1940 .aggregate(crate::db::count())
1941 .grouped_limits(0, 1024),
1942 )
1943 .expect_err("trusted grouped execution must reject zero limits"),
1944 icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1945 icydb_diagnostic_code::ErrorOrigin::Query,
1946 icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1947 reason:
1948 icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryRequiresLimits,
1949 },
1950 );
1951 assert_query_diagnostic(
1952 session
1953 .execute_public_dynamic_grouped_query(&grouped_query.grouped_limits(101, 1024))
1954 .expect_err("public grouped execution must enforce its group budget"),
1955 icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1956 icydb_diagnostic_code::ErrorOrigin::Query,
1957 icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1958 reason:
1959 icydb_diagnostic_code::QueryReadAdmissionCode::GroupedQueryExceedsBudget,
1960 },
1961 );
1962
1963 let paged_query = crate::db::DynamicQuery::new("RenamedEntity")
1964 .group_by("value")
1965 .aggregate(crate::db::count())
1966 .grouped_limits(2, 1024)
1967 .limit(1);
1968 assert_query_diagnostic(
1969 session
1970 .execute_public_dynamic_grouped_query(&paged_query)
1971 .expect_err("public grouped execution must reject an unbounded full scan"),
1972 icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission,
1973 icydb_diagnostic_code::ErrorOrigin::Query,
1974 icydb_diagnostic_code::DiagnosticDetail::QueryReadAdmission {
1975 reason:
1976 icydb_diagnostic_code::QueryReadAdmissionCode::UnboundedFullScanRejected,
1977 },
1978 );
1979 let first_page = session
1980 .execute_trusted_dynamic_grouped_query(&paged_query)
1981 .expect("SQL-free grouped first page should execute");
1982 assert_eq!(first_page.row_count, 1);
1983 assert_eq!(
1984 first_page.rows[0].group_key(),
1985 &[crate::value::OutputValue::Nat64(9)]
1986 );
1987 let cursor = first_page
1988 .next_cursor
1989 .expect("first grouped page should return a continuation cursor");
1990 assert_query_diagnostic(
1991 session
1992 .execute_trusted_dynamic_grouped_query(
1993 &paged_query.clone().cursor(format!("{cursor}0")),
1994 )
1995 .expect_err("tampered grouped cursor must fail closed"),
1996 icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor,
1997 icydb_diagnostic_code::ErrorOrigin::Cursor,
1998 icydb_diagnostic_code::DiagnosticDetail::QueryKind {
1999 kind: icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor,
2000 },
2001 );
2002 let second_page = session
2003 .execute_trusted_dynamic_grouped_query(&paged_query.cursor(cursor))
2004 .expect("SQL-free grouped continuation should execute");
2005 assert_eq!(second_page.row_count, 1);
2006 assert_eq!(
2007 second_page.rows[0].group_key(),
2008 &[crate::value::OutputValue::Nat64(10)]
2009 );
2010 assert_eq!(second_page.next_cursor, None);
2011 }
2012 }
2013}
2014
2015#[cfg(test)]
2016mod mixed_relation_batch_tests {
2017 use super::{DbSession, DynamicMutation, DynamicStructuralPatch, DynamicWriteCell};
2018 use crate::{
2019 db::{
2020 data::DataStore,
2021 index::IndexStore,
2022 registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
2023 schema::{
2024 AcceptedConstraintCatalog, AcceptedFieldKind, AcceptedSchemaRevision, FieldId,
2025 FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2026 PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2027 PersistedRelationEdgeSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
2028 SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore,
2029 SchemaVersion, accepted_schema_candidate_with_field_bindings_for_tests,
2030 },
2031 },
2032 error::ErrorClass,
2033 traits::{CanisterKind, Path},
2034 types::EntityTag,
2035 value::{InputValue, OutputValue},
2036 };
2037 use icydb_schema::FieldSourceKey;
2038 use std::{cell::RefCell, collections::BTreeMap};
2039
2040 const STORE_PATH: &str = "session::write::mixed_relation_batch_tests::Store";
2041 const ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node";
2042 const ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::id";
2043 const PARENT_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::parent_id";
2044 const CODE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Node::code";
2045 const ENTITY_NAME: &str = "MixedRelationNode";
2046 const ENTITY_TAG: EntityTag = EntityTag::new(94);
2047 const OTHER_ENTITY_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other";
2048 const OTHER_ID_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::id";
2049 const OTHER_VALUE_SOURCE: &str = "session::write::mixed_relation_batch_tests::Other::value";
2050 const OTHER_ENTITY_NAME: &str = "MixedRelationOther";
2051 const OTHER_ENTITY_TAG: EntityTag = EntityTag::new(95);
2052
2053 struct TestCanister;
2054
2055 impl Path for TestCanister {
2056 const PATH: &'static str = "session::write::mixed_relation_batch_tests::Canister";
2057 }
2058
2059 impl CanisterKind for TestCanister {
2060 const COMMIT_MEMORY_ID: u8 = 47;
2061 const COMMIT_STABLE_KEY: &'static str = "icydb.mixed_relation_batch_tests.commit.v1";
2062 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 48;
2063 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2064 "icydb.mixed_relation_batch_tests.integrity.progress.v1";
2065 }
2066
2067 thread_local! {
2068 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2069 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2070 static SCHEMA_STORE: RefCell<SchemaStore> =
2071 const { RefCell::new(SchemaStore::init_heap()) };
2072 static STORE_REGISTRY: StoreRegistry = {
2073 let mut registry = StoreRegistry::new();
2074 registry.register_store(
2075 STORE_PATH,
2076 &DATA_STORE,
2077 &INDEX_STORE,
2078 &SCHEMA_STORE,
2079 StoreAllocationIdentities::absent(),
2080 StoreRuntimeStorageCapabilities::heap(),
2081 ).expect("mixed relation test store should register");
2082 registry
2083 };
2084 }
2085
2086 fn source_key(source: &str) -> FieldSourceKey {
2087 FieldSourceKey::try_new(source).expect("mixed relation field source should admit")
2088 }
2089
2090 fn relation_snapshot() -> PersistedSchemaSnapshot {
2091 let fields = vec![
2092 PersistedFieldSnapshot::new_initial(
2093 FieldId::new(1),
2094 "id".to_string(),
2095 SchemaFieldSlot::new(0),
2096 AcceptedFieldKind::Nat64,
2097 Vec::new(),
2098 false,
2099 SchemaInsertDefault::None,
2100 FieldStorageDecode::ByKind,
2101 LeafCodec::Scalar(ScalarCodec::Nat64),
2102 ),
2103 PersistedFieldSnapshot::new_initial(
2104 FieldId::new(2),
2105 "parent_id".to_string(),
2106 SchemaFieldSlot::new(1),
2107 AcceptedFieldKind::Nat64,
2108 Vec::new(),
2109 true,
2110 SchemaInsertDefault::None,
2111 FieldStorageDecode::ByKind,
2112 LeafCodec::Scalar(ScalarCodec::Nat64),
2113 ),
2114 PersistedFieldSnapshot::new_initial(
2115 FieldId::new(3),
2116 "code".to_string(),
2117 SchemaFieldSlot::new(2),
2118 AcceptedFieldKind::Nat64,
2119 Vec::new(),
2120 false,
2121 SchemaInsertDefault::None,
2122 FieldStorageDecode::ByKind,
2123 LeafCodec::Scalar(ScalarCodec::Nat64),
2124 ),
2125 ];
2126 let relation = PersistedRelationEdgeSnapshot::new(
2127 RelationId::new(1).expect("mixed relation identity should be non-zero"),
2128 "parent".to_string(),
2129 ENTITY_SOURCE.to_string(),
2130 vec![FieldId::new(2)],
2131 );
2132 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2133 SchemaVersion::initial(),
2134 ENTITY_SOURCE.to_string(),
2135 ENTITY_NAME.to_string(),
2136 FieldId::new(1),
2137 SchemaRowLayout::initial(
2138 fields
2139 .iter()
2140 .map(|field| (field.id(), field.slot()))
2141 .collect(),
2142 ),
2143 fields,
2144 vec![PersistedIndexSnapshot::new(
2145 SchemaIndexId::new(1).expect("mixed unique index identity should be non-zero"),
2146 1,
2147 "by_code".to_string(),
2148 STORE_PATH.to_string(),
2149 true,
2150 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2151 FieldId::new(3),
2152 SchemaFieldSlot::new(2),
2153 vec!["code".to_string()],
2154 AcceptedFieldKind::Nat64,
2155 false,
2156 )]),
2157 None,
2158 )],
2159 )
2160 .with_relations(vec![relation]);
2161 let constraints = AcceptedConstraintCatalog::initial(
2162 snapshot.fields(),
2163 snapshot.indexes(),
2164 snapshot.relations(),
2165 )
2166 .expect("mixed relation constraints should close");
2167 snapshot.with_constraint_catalog(constraints)
2168 }
2169
2170 fn other_snapshot() -> PersistedSchemaSnapshot {
2171 let fields = vec![
2172 PersistedFieldSnapshot::new_initial(
2173 FieldId::new(1),
2174 "id".to_string(),
2175 SchemaFieldSlot::new(0),
2176 AcceptedFieldKind::Nat64,
2177 Vec::new(),
2178 false,
2179 SchemaInsertDefault::None,
2180 FieldStorageDecode::ByKind,
2181 LeafCodec::Scalar(ScalarCodec::Nat64),
2182 ),
2183 PersistedFieldSnapshot::new_initial(
2184 FieldId::new(2),
2185 "value".to_string(),
2186 SchemaFieldSlot::new(1),
2187 AcceptedFieldKind::Nat64,
2188 Vec::new(),
2189 false,
2190 SchemaInsertDefault::None,
2191 FieldStorageDecode::ByKind,
2192 LeafCodec::Scalar(ScalarCodec::Nat64),
2193 ),
2194 ];
2195 PersistedSchemaSnapshot::new(
2196 SchemaVersion::initial(),
2197 OTHER_ENTITY_SOURCE.to_string(),
2198 OTHER_ENTITY_NAME.to_string(),
2199 FieldId::new(1),
2200 SchemaRowLayout::initial(
2201 fields
2202 .iter()
2203 .map(|field| (field.id(), field.slot()))
2204 .collect(),
2205 ),
2206 fields,
2207 )
2208 }
2209
2210 fn initialize() -> DbSession<TestCanister> {
2211 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2212 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2213 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2214 let session = DbSession::<TestCanister>::new(
2215 &STORE_REGISTRY,
2216 &crate::db::RequestExecutionRoot::__new_runtime_root(),
2217 );
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 accepted_relation_edges_drive_catalog_and_describe_introspection() {
2353 let session = initialize();
2354 let entities = session
2355 .show_entities()
2356 .expect("accepted entity catalog should resolve");
2357 let source = entities
2358 .iter()
2359 .find(|entity| entity.entity_name() == ENTITY_NAME)
2360 .expect("relation source should be listed");
2361 assert_eq!(source.relations(), 1);
2362
2363 let description = session
2364 .try_describe_entity_by_name(ENTITY_NAME)
2365 .expect("accepted relation source should describe");
2366 let [relation] = description.relations() else {
2367 panic!("accepted relation edge should produce one relation row");
2368 };
2369 assert_eq!(relation.field(), "parent_id");
2370 assert_eq!(relation.target_path(), ENTITY_SOURCE);
2371 assert_eq!(relation.target_entity_name(), ENTITY_NAME);
2372 assert_eq!(relation.target_store_path(), STORE_PATH);
2373 assert_eq!(
2374 relation.cardinality(),
2375 crate::db::EntityRelationCardinality::Single,
2376 );
2377 }
2378
2379 #[test]
2380 fn mixed_relation_validation_uses_the_complete_final_row_overlay() {
2381 let session = initialize();
2382 session
2383 .execute_trusted_dynamic_mutation_batch(vec![insert(1, None), insert(2, Some(1))])
2384 .expect("the initial relation should commit");
2385
2386 let blocked = session
2387 .execute_trusted_dynamic_mutation(&delete(1))
2388 .expect_err("an unaffected committed source must block target deletion");
2389 assert_relation_violation(&blocked);
2390
2391 let deleted = session
2392 .execute_trusted_dynamic_mutation_batch(vec![delete(2), delete(1)])
2393 .expect("a source and its target should delete atomically");
2394 assert_eq!(
2395 deleted.rows,
2396 vec![expected_row(2, Some(1)), expected_row(1, None)],
2397 );
2398
2399 session
2400 .execute_trusted_dynamic_mutation_batch(vec![insert(3, None), insert(4, Some(3))])
2401 .expect("the update-away fixture should commit");
2402 let updated_away = session
2403 .execute_trusted_dynamic_mutation_batch(vec![update_parent(4, None), delete(3)])
2404 .expect("an updated final source may release a deleted target");
2405 assert_eq!(
2406 updated_away.rows,
2407 vec![expected_row(4, None), expected_row(3, None)],
2408 );
2409
2410 session
2411 .execute_trusted_dynamic_mutation_batch(vec![insert(5, None), insert(6, Some(5))])
2412 .expect("the retained-reference fixture should commit");
2413 let retained = session
2414 .execute_trusted_dynamic_mutation_batch(vec![update_parent(6, Some(5)), delete(5)])
2415 .expect_err("a final updated source must still block target deletion");
2416 assert_relation_violation(&retained);
2417
2418 session
2419 .execute_trusted_dynamic_mutation(&insert(7, None))
2420 .expect("the inserted-reference fixture target should commit");
2421 let inserted_reference = session
2422 .execute_trusted_dynamic_mutation_batch(vec![insert(8, Some(7)), delete(7)])
2423 .expect_err("a final inserted source must not reference a deleted target");
2424 assert_relation_violation(&inserted_reference);
2425
2426 let inserted_target = session
2427 .execute_trusted_dynamic_mutation_batch(vec![insert(10, Some(9)), insert(9, None)])
2428 .expect("an inserted relation should see its batch-final target");
2429 assert_eq!(
2430 inserted_target.rows,
2431 vec![expected_row(10, Some(9)), expected_row(9, None)],
2432 );
2433
2434 session
2435 .execute_trusted_dynamic_mutation(&insert(11, None))
2436 .expect("the updated-reference fixture source should commit");
2437 let updated_target = session
2438 .execute_trusted_dynamic_mutation_batch(vec![
2439 update_parent(11, Some(12)),
2440 insert(12, None),
2441 ])
2442 .expect("an updated relation should see its batch-final target");
2443 assert_eq!(
2444 updated_target.rows,
2445 vec![expected_row(11, Some(12)), expected_row(12, None)],
2446 );
2447 }
2448
2449 #[test]
2450 fn mixed_batch_rejects_cross_entity_missing_and_collision_then_honors_replace() {
2451 let session = initialize();
2452 session
2453 .execute_trusted_dynamic_mutation(&insert(1, None))
2454 .expect("the primary mixed fixture row should commit");
2455 session
2456 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2457 entity: OTHER_ENTITY_NAME.to_string(),
2458 patch: other_patch(Some(1), 10),
2459 })
2460 .expect("the secondary mixed fixture row should commit");
2461
2462 let mixed_entity = session
2463 .execute_trusted_dynamic_mutation_batch(vec![
2464 update_code(1, 11),
2465 DynamicMutation::Update {
2466 entity: OTHER_ENTITY_NAME.to_string(),
2467 key: InputValue::Nat64(1),
2468 patch: other_patch(None, 11),
2469 },
2470 ])
2471 .expect_err("one atomic batch must not cross accepted entities");
2472 assert_eq!(mixed_entity.class(), ErrorClass::Conflict);
2473 assert_eq!(
2474 mixed_entity.diagnostic_facts(),
2475 vec![
2476 (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
2477 (
2478 icydb_diagnostic_code::DiagnosticFactTag::ExpectedEntityTag,
2479 ENTITY_TAG.value(),
2480 ),
2481 (
2482 icydb_diagnostic_code::DiagnosticFactTag::ActualEntityTag,
2483 OTHER_ENTITY_TAG.value(),
2484 ),
2485 ],
2486 );
2487
2488 let missing = session
2489 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 12), delete(99)])
2490 .expect_err("a late missing delete must reject the earlier staged update");
2491 assert_eq!(missing.class(), ErrorClass::NotFound);
2492
2493 session
2494 .execute_trusted_dynamic_mutation(&insert(2, None))
2495 .expect("the collision fixture should commit");
2496 let collision = session
2497 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 13), insert(2, None)])
2498 .expect_err("an insert collision must reject the earlier staged update");
2499 assert_eq!(collision.class(), ErrorClass::Conflict);
2500 let failures_unchanged = session
2501 .execute_trusted_dynamic_mutation(&update_code(1, 1))
2502 .expect("failed batches must preserve the original unique value");
2503 assert_eq!(failures_unchanged.affected_rows, 0);
2504
2505 let replaced = session
2506 .execute_trusted_dynamic_mutation_batch(vec![
2507 update_code(1, 14),
2508 DynamicMutation::Replace {
2509 entity: ENTITY_NAME.to_string(),
2510 key: InputValue::Nat64(99),
2511 patch: patch(None, None, Some(99)),
2512 },
2513 ])
2514 .expect("ordinary caller-key replace should insert its absent final row");
2515 assert_eq!(
2516 replaced.rows,
2517 vec![
2518 expected_row_with_code(1, None, 14),
2519 expected_row_with_code(99, None, 99),
2520 ],
2521 );
2522
2523 let unchanged = session
2524 .execute_trusted_dynamic_mutation(&update_code(1, 14))
2525 .expect("the successful mixed replace must publish its preceding update");
2526 assert_eq!(unchanged.affected_rows, 0);
2527 let other_unchanged = session
2528 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
2529 entity: OTHER_ENTITY_NAME.to_string(),
2530 key: InputValue::Nat64(1),
2531 patch: other_patch(None, 10),
2532 })
2533 .expect("cross-entity rejection must preserve the secondary row");
2534 assert_eq!(other_unchanged.affected_rows, 0);
2535 }
2536
2537 #[test]
2538 fn mixed_batch_unique_swap_and_delete_release_use_the_final_overlay() {
2539 let session = initialize();
2540 session
2541 .execute_trusted_dynamic_mutation_batch(vec![
2542 insert_with_code(1, None, 10),
2543 insert_with_code(2, None, 20),
2544 ])
2545 .expect("the unique-overlay fixture should commit");
2546
2547 let swapped = session
2548 .execute_trusted_dynamic_mutation_batch(vec![update_code(1, 20), update_code(2, 10)])
2549 .expect("two final rows should atomically swap unique memberships");
2550 assert_eq!(
2551 swapped.rows,
2552 vec![
2553 expected_row_with_code(1, None, 20),
2554 expected_row_with_code(2, None, 10),
2555 ],
2556 );
2557
2558 let released = session
2559 .execute_trusted_dynamic_mutation_batch(vec![delete(1), insert_with_code(3, None, 20)])
2560 .expect("a delete should release unique membership to a final inserted row");
2561 assert_eq!(
2562 released.rows,
2563 vec![
2564 expected_row_with_code(1, None, 20),
2565 expected_row_with_code(3, None, 20),
2566 ],
2567 );
2568 }
2569}
2570
2571#[cfg(test)]
2572mod identity_pre_key_tests {
2573 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2574 use super::DynamicTypedEntityBinding;
2575 use super::{
2576 AcceptedMutationIntentPatch, AcceptedRowLayoutRuntimeContract, AcceptedStructuralMutation,
2577 AcceptedStructuralMutationTarget, DbSession, DynamicMutation, DynamicStructuralPatch,
2578 DynamicTypedFieldBindingRequest, DynamicTypedFieldType, DynamicTypedMutation,
2579 DynamicWriteCell, FieldSlot, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
2580 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES,
2581 add_structural_mutation_staged_bytes, checked_pre_key_candidate_count,
2582 insert_key_exists_after_generation, validate_structural_mutation_result_bytes,
2583 };
2584 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2585 use crate::db::executor::budget::{
2586 HardExecutionBudget, HardExecutionContext, HardExecutionFailureHeadroom,
2587 with_query_execution_budget_for_tests,
2588 };
2589 use crate::{
2590 db::{
2591 commit::{database_incarnation_id, forget_recovered_domain_for_tests},
2592 data::DataStore,
2593 executor::{MutationCommitInterruption, interrupt_next_mutation_commit_for_tests},
2594 index::IndexStore,
2595 integrity::{
2596 PhysicalUnitCheckpoint, QuickIntegrityStatus, RowInspectionLimits,
2597 execute_quick_integrity, execute_row_integrity_page,
2598 },
2599 journal::JournalTailStore,
2600 registry::{
2601 StoreAllocationIdentities, StoreAllocationIdentity, StoreRegistry,
2602 StoreRuntimeStorageCapabilities,
2603 },
2604 schema::{
2605 AcceptedFieldKind, AcceptedSchemaRevision, FieldId, FieldInsertGeneration,
2606 FieldStorageDecode, LeafCodec, PersistedFieldSnapshot,
2607 PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2608 PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy,
2609 SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaStore, SchemaVersion,
2610 accepted_schema_candidate_with_field_bindings_for_tests,
2611 },
2612 write_context::MutationMode,
2613 },
2614 error::{ErrorClass, ErrorOrigin, InternalError},
2615 testing::test_memory,
2616 traits::{CanisterKind, Path},
2617 types::{EntityTag, Timestamp},
2618 value::{InputValue, OutputValue, Value},
2619 };
2620 use icydb_schema::{FieldSourceKey, ScalarType};
2621 use std::{cell::RefCell, collections::BTreeMap, time::Instant};
2622
2623 const STORE_PATH: &str = "session::write::identity_pre_key_tests::Store";
2624 const ENTITY_SOURCE: &str = "session::write::identity_pre_key_tests::Entity";
2625 const ID_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::id";
2626 const PAYLOAD_SOURCE: &str = "session::write::identity_pre_key_tests::Entity::payload";
2627 const ENTITY_NAME: &str = "IdentityRow";
2628 const ENTITY_TAG: EntityTag = EntityTag::new(93);
2629 const JOURNALED_STORE_PATH: &str = "session::write::identity_pre_key_tests::JournaledStore";
2630
2631 struct TestCanister;
2632
2633 impl Path for TestCanister {
2634 const PATH: &'static str = "session::write::identity_pre_key_tests::Canister";
2635 }
2636
2637 impl CanisterKind for TestCanister {
2638 const COMMIT_MEMORY_ID: u8 = 45;
2639 const COMMIT_STABLE_KEY: &'static str = "icydb.identity_pre_key_tests.commit.v1";
2640 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 46;
2641 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2642 "icydb.identity_pre_key_tests.integrity.progress.v1";
2643 }
2644
2645 thread_local! {
2646 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
2647 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
2648 static SCHEMA_STORE: RefCell<SchemaStore> =
2649 const { RefCell::new(SchemaStore::init_heap()) };
2650 static STORE_REGISTRY: StoreRegistry = {
2651 let mut registry = StoreRegistry::new();
2652 registry.register_store(
2653 STORE_PATH,
2654 &DATA_STORE,
2655 &INDEX_STORE,
2656 &SCHEMA_STORE,
2657 StoreAllocationIdentities::absent(),
2658 StoreRuntimeStorageCapabilities::heap(),
2659 ).expect("identity pre-key test store should register");
2660 registry
2661 };
2662 static JOURNALED_DATA_STORE: RefCell<DataStore> =
2663 RefCell::new(DataStore::init_journaled(test_memory(186)));
2664 static JOURNALED_INDEX_STORE: RefCell<IndexStore> =
2665 RefCell::new(IndexStore::init_journaled(test_memory(187)));
2666 static JOURNALED_SCHEMA_STORE: RefCell<SchemaStore> =
2667 RefCell::new(SchemaStore::init_journaled(test_memory(188)));
2668 static JOURNALED_TAIL_STORE: RefCell<JournalTailStore> =
2669 RefCell::new(JournalTailStore::init(test_memory(189)));
2670 static JOURNALED_STORE_REGISTRY: StoreRegistry = {
2671 let mut registry = StoreRegistry::new();
2672 registry.register_journaled_store(
2673 JOURNALED_STORE_PATH,
2674 &JOURNALED_DATA_STORE,
2675 &JOURNALED_INDEX_STORE,
2676 &JOURNALED_SCHEMA_STORE,
2677 &JOURNALED_TAIL_STORE,
2678 StoreAllocationIdentities::new_journaled(
2679 StoreAllocationIdentity::new(186, "icydb.test.identity-range.data.v1"),
2680 StoreAllocationIdentity::new(187, "icydb.test.identity-range.index.v1"),
2681 StoreAllocationIdentity::new(188, "icydb.test.identity-range.schema.v1"),
2682 StoreAllocationIdentity::new(189, "icydb.test.identity-range.journal.v1"),
2683 ),
2684 StoreRuntimeStorageCapabilities::journaled(),
2685 ).expect("identity range journaled store should register");
2686 registry
2687 };
2688 }
2689
2690 struct JournaledTestCanister;
2691
2692 impl Path for JournaledTestCanister {
2693 const PATH: &'static str = "session::write::identity_pre_key_tests::JournaledCanister";
2694 }
2695
2696 impl CanisterKind for JournaledTestCanister {
2697 const COMMIT_MEMORY_ID: u8 = 190;
2698 const COMMIT_STABLE_KEY: &'static str = "icydb.identity_range_tests.commit.v1";
2699 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 191;
2700 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
2701 "icydb.identity_range_tests.integrity.progress.v1";
2702 }
2703
2704 fn source_key(source: &str) -> FieldSourceKey {
2705 FieldSourceKey::try_new(source).expect("identity test field source should admit")
2706 }
2707
2708 fn identity_snapshot(store_path: &str) -> PersistedSchemaSnapshot {
2709 let fields = vec![
2710 PersistedFieldSnapshot::new_initial_with_write_policy(
2711 FieldId::new(1),
2712 "id".to_string(),
2713 SchemaFieldSlot::new(0),
2714 AcceptedFieldKind::Nat64,
2715 Vec::new(),
2716 false,
2717 SchemaInsertDefault::None,
2718 SchemaFieldWritePolicy::from_model_policies(
2719 Some(FieldInsertGeneration::Identity),
2720 None,
2721 ),
2722 FieldStorageDecode::ByKind,
2723 LeafCodec::Scalar(ScalarCodec::Nat64),
2724 ),
2725 PersistedFieldSnapshot::new_initial(
2726 FieldId::new(2),
2727 "payload".to_string(),
2728 SchemaFieldSlot::new(1),
2729 AcceptedFieldKind::Nat64,
2730 Vec::new(),
2731 false,
2732 SchemaInsertDefault::None,
2733 FieldStorageDecode::ByKind,
2734 LeafCodec::Scalar(ScalarCodec::Nat64),
2735 ),
2736 ];
2737 PersistedSchemaSnapshot::new_with_indexes(
2738 SchemaVersion::initial(),
2739 ENTITY_SOURCE.to_string(),
2740 ENTITY_NAME.to_string(),
2741 FieldId::new(1),
2742 SchemaRowLayout::initial(
2743 fields
2744 .iter()
2745 .map(|field| (field.id(), field.slot()))
2746 .collect(),
2747 ),
2748 fields,
2749 vec![PersistedIndexSnapshot::new(
2750 SchemaIndexId::new(1).expect("identity test index ID should admit"),
2751 1,
2752 "by_payload".to_string(),
2753 store_path.to_string(),
2754 false,
2755 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2756 FieldId::new(2),
2757 SchemaFieldSlot::new(1),
2758 vec!["payload".to_string()],
2759 AcceptedFieldKind::Nat64,
2760 false,
2761 )]),
2762 None,
2763 )],
2764 )
2765 }
2766
2767 fn initialize() -> DbSession<TestCanister> {
2768 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
2769 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
2770 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
2771 let session = DbSession::<TestCanister>::new(
2772 &STORE_REGISTRY,
2773 &crate::db::RequestExecutionRoot::__new_runtime_root(),
2774 );
2775 session
2776 .db
2777 .ensure_recovered_state()
2778 .expect("identity pre-key test database should initialize");
2779 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2780 STORE_PATH,
2781 AcceptedSchemaRevision::INITIAL,
2782 BTreeMap::from([(ENTITY_TAG, identity_snapshot(STORE_PATH))]),
2783 BTreeMap::from([
2784 ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2785 ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2786 ]),
2787 );
2788 let store = session
2789 .db
2790 .store_handle(STORE_PATH)
2791 .expect("identity pre-key test store should resolve");
2792 crate::db::commit::publish_accepted_schema_candidate(
2793 STORE_PATH,
2794 store,
2795 AcceptedSchemaRevision::NONE,
2796 &candidate,
2797 )
2798 .expect("identity candidate should publish with explicit zero state");
2799 session
2800 }
2801
2802 fn initialize_journaled() -> DbSession<JournaledTestCanister> {
2803 let session = DbSession::<JournaledTestCanister>::new(
2804 &JOURNALED_STORE_REGISTRY,
2805 &crate::db::RequestExecutionRoot::__new_runtime_root(),
2806 );
2807 session
2808 .db
2809 .ensure_recovered_state()
2810 .expect("journaled identity database should initialize");
2811 let candidate = accepted_schema_candidate_with_field_bindings_for_tests(
2812 JOURNALED_STORE_PATH,
2813 AcceptedSchemaRevision::INITIAL,
2814 BTreeMap::from([(ENTITY_TAG, identity_snapshot(JOURNALED_STORE_PATH))]),
2815 BTreeMap::from([
2816 ((ENTITY_TAG, source_key(ID_SOURCE)), FieldId::new(1)),
2817 ((ENTITY_TAG, source_key(PAYLOAD_SOURCE)), FieldId::new(2)),
2818 ]),
2819 );
2820 let store = session
2821 .db
2822 .store_handle(JOURNALED_STORE_PATH)
2823 .expect("journaled identity store should resolve");
2824 crate::db::commit::publish_accepted_schema_candidate(
2825 JOURNALED_STORE_PATH,
2826 store,
2827 AcceptedSchemaRevision::NONE,
2828 &candidate,
2829 )
2830 .expect("journaled identity candidate should publish");
2831 session
2832 }
2833
2834 fn payload_patch(value: u64) -> AcceptedMutationIntentPatch {
2835 AcceptedMutationIntentPatch::new()
2836 .set_authored(FieldSlot::from_validated_index(1), InputValue::Nat64(value))
2837 }
2838
2839 fn dynamic_payload_patch(value: u64) -> DynamicStructuralPatch {
2840 DynamicStructuralPatch::new(vec![(
2841 "payload".to_string(),
2842 DynamicWriteCell::Value(InputValue::Nat64(value)),
2843 )])
2844 }
2845
2846 fn expected_dynamic_row(id: u64, payload: u64) -> Vec<OutputValue> {
2847 vec![OutputValue::Nat64(id), OutputValue::Nat64(payload)]
2848 }
2849
2850 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2851 fn exact_key_binding<C: CanisterKind>(session: &DbSession<C>) -> DynamicTypedEntityBinding {
2852 session
2853 .issue_typed_entity_binding(
2854 ENTITY_SOURCE,
2855 &[
2856 DynamicTypedFieldBindingRequest::new(
2857 ID_SOURCE.to_string(),
2858 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2859 false,
2860 ),
2861 DynamicTypedFieldBindingRequest::new(
2862 PAYLOAD_SOURCE.to_string(),
2863 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
2864 false,
2865 ),
2866 ],
2867 )
2868 .expect("exact-key test binding should issue")
2869 }
2870
2871 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2872 fn insert_exact_key_fixture<C: CanisterKind>(session: &DbSession<C>, payload: u64) -> u64 {
2873 let output = session
2874 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
2875 entity: ENTITY_NAME.to_string(),
2876 patch: dynamic_payload_patch(payload),
2877 })
2878 .expect("exact-key fixture insert should commit");
2879 match output.rows.as_slice() {
2880 [row] => match row.as_slice() {
2881 [OutputValue::Nat64(id), OutputValue::Nat64(actual_payload)]
2882 if *actual_payload == payload =>
2883 {
2884 *id
2885 }
2886 _ => panic!("exact-key fixture should return its identity and payload"),
2887 },
2888 _ => panic!("exact-key fixture insert should return one row"),
2889 }
2890 }
2891
2892 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2893 fn assert_exact_key_batch<C: CanisterKind>(session: &DbSession<C>) {
2894 let first = insert_exact_key_fixture(session, 41);
2895 let second = insert_exact_key_fixture(session, 42);
2896 let missing = u64::MAX;
2897 let binding = exact_key_binding(session);
2898 let gets_before = DataStore::current_get_call_count();
2899 let result = session
2900 .execute_public_exact_key_batch_for_typed_binding(
2901 &binding,
2902 &[second, missing, first, second],
2903 )
2904 .expect("exact-key batch should execute")
2905 .expect("exact-key binding should remain current");
2906
2907 assert_eq!(result.positions, vec![0, 1, 2, 0]);
2908 assert_eq!(
2909 result.distinct_rows,
2910 vec![
2911 Some(expected_dynamic_row(second, 42)),
2912 None,
2913 Some(expected_dynamic_row(first, 41)),
2914 ],
2915 );
2916 assert_eq!(
2917 DataStore::current_get_call_count().saturating_sub(gets_before),
2918 3,
2919 "four input positions with one duplicate must perform three physical reads",
2920 );
2921 }
2922
2923 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2924 #[test]
2925 fn exact_key_batches_preserve_semantics_across_heap_and_journaled_stores() {
2926 assert_exact_key_batch(&initialize());
2927 assert_exact_key_batch(&initialize_journaled());
2928 }
2929
2930 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2931 #[test]
2932 fn exact_key_batch_uses_typed_hard_execution_budget() {
2933 let session = initialize();
2934 let binding = exact_key_binding(&session);
2935 let budget =
2936 HardExecutionBudget::uniform_for_tests(0, HardExecutionFailureHeadroom::new(500, 256));
2937 let error = session
2938 .execute_exact_key_batch_with_hard_budget_for_tests(&binding, &[u64::MAX], &budget)
2939 .expect_err("zero query budget should reject the exact-key route");
2940
2941 assert!(matches!(
2942 error.diagnostic().detail(),
2943 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2944 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
2945 })
2946 ));
2947 let facts = error.diagnostic_facts();
2948 assert_eq!(
2949 &facts[..5],
2950 &[
2951 (
2952 icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
2953 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::QueryExecutions.raw(),
2954 ),
2955 (icydb_diagnostic_code::DiagnosticFactTag::Limit, 0),
2956 (icydb_diagnostic_code::DiagnosticFactTag::Actual, 1),
2957 (
2958 icydb_diagnostic_code::DiagnosticFactTag::ExecutionBudgetScope,
2959 icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution.raw(),
2960 ),
2961 (
2962 icydb_diagnostic_code::DiagnosticFactTag::ExecutionLane,
2963 icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead.raw(),
2964 ),
2965 ],
2966 );
2967 assert_eq!(
2968 facts[5].0,
2969 icydb_diagnostic_code::DiagnosticFactTag::QueryShapeFingerprintPrefix,
2970 );
2971 assert_ne!(facts[5].1, 0);
2972 }
2973
2974 #[cfg(all(feature = "sql", feature = "diagnostics"))]
2975 fn assert_planned_query_exhausts(
2976 session: &DbSession<TestCanister>,
2977 query: &crate::db::DynamicQuery,
2978 resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
2979 ) {
2980 let budget = HardExecutionBudget::uniform_for_tests(
2981 u64::MAX,
2982 HardExecutionFailureHeadroom::new(500, 256),
2983 )
2984 .with_limit_for_tests(resource, 0);
2985 let context = HardExecutionContext::new(
2986 icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
2987 icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
2988 0x7068_7973_6963_616c,
2989 );
2990 let error = with_query_execution_budget_for_tests(budget, context, || {
2991 session.execute_trusted_dynamic_query(query)
2992 })
2993 .expect_err("the injected zero resource allowance should reject planned execution");
2994
2995 assert!(matches!(
2996 error.diagnostic().detail(),
2997 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
2998 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
2999 })
3000 ));
3001 assert_eq!(
3002 error.diagnostic_facts()[0],
3003 (
3004 icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3005 resource.raw(),
3006 ),
3007 );
3008 }
3009
3010 #[cfg(all(feature = "sql", feature = "diagnostics"))]
3011 fn assert_grouped_query_exhausts(
3012 session: &DbSession<TestCanister>,
3013 query: &crate::db::DynamicQuery,
3014 resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
3015 ) {
3016 let budget = HardExecutionBudget::uniform_for_tests(
3017 u64::MAX,
3018 HardExecutionFailureHeadroom::new(500, 256),
3019 )
3020 .with_limit_for_tests(resource, 0);
3021 let context = HardExecutionContext::new(
3022 icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
3023 icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
3024 0x6772_6f75_7065_642d,
3025 );
3026 let error = with_query_execution_budget_for_tests(budget, context, || {
3027 session.execute_trusted_dynamic_grouped_query(query)
3028 })
3029 .expect_err("the injected zero resource allowance should reject grouped execution");
3030
3031 assert!(matches!(
3032 error.diagnostic().detail(),
3033 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3034 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
3035 })
3036 ));
3037 assert_eq!(
3038 error.diagnostic_facts()[0],
3039 (
3040 icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3041 resource.raw(),
3042 ),
3043 );
3044 }
3045
3046 #[cfg(all(feature = "sql", feature = "diagnostics"))]
3047 fn assert_sql_query_exhausts(
3048 session: &DbSession<TestCanister>,
3049 sql: &str,
3050 resource: icydb_diagnostic_code::DiagnosticExecutionBudgetResource,
3051 ) {
3052 let budget = HardExecutionBudget::uniform_for_tests(
3053 u64::MAX,
3054 HardExecutionFailureHeadroom::new(500, 256),
3055 )
3056 .with_limit_for_tests(resource, 0);
3057 let context = HardExecutionContext::new(
3058 icydb_diagnostic_code::DiagnosticExecutionBudgetScope::Execution,
3059 icydb_diagnostic_code::DiagnosticExecutionLane::TrustedRead,
3060 0x7371_6c2d_736f_7274,
3061 );
3062 let error = with_query_execution_budget_for_tests(budget, context, || {
3063 session.execute_trusted_sql_query(sql)
3064 })
3065 .expect_err("the injected zero resource allowance should reject SQL execution");
3066
3067 assert!(matches!(
3068 error.diagnostic().detail(),
3069 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3070 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
3071 })
3072 ));
3073 assert_eq!(
3074 error.diagnostic_facts()[0],
3075 (
3076 icydb_diagnostic_code::DiagnosticFactTag::BudgetResource,
3077 resource.raw(),
3078 ),
3079 );
3080 }
3081
3082 #[cfg(all(feature = "sql", feature = "diagnostics"))]
3083 #[test]
3084 fn planned_read_routes_share_physical_resource_accounting() {
3085 let session = initialize();
3086 let first = insert_exact_key_fixture(&session, 41);
3087 insert_exact_key_fixture(&session, 42);
3088
3089 let fallback = crate::db::DynamicQuery::new(ENTITY_NAME)
3090 .filter(crate::db::FieldRef::new("id").eq(first))
3091 .select(["id", "payload"])
3092 .order_by(crate::db::asc("id"))
3093 .limit(1);
3094 assert_eq!(
3095 session
3096 .execute_trusted_dynamic_query(&fallback)
3097 .expect("bounded fallback execution should preserve its result")
3098 .row_count,
3099 1,
3100 );
3101 assert_planned_query_exhausts(
3102 &session,
3103 &fallback,
3104 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::RowsVisited,
3105 );
3106
3107 let covering = crate::db::DynamicQuery::new(ENTITY_NAME)
3108 .filter(crate::db::FieldRef::new("payload").eq(41_u64))
3109 .select(["payload"])
3110 .order_by(crate::db::asc("payload"))
3111 .limit(1);
3112 assert_eq!(
3113 session
3114 .execute_trusted_dynamic_query(&covering)
3115 .expect("bounded covering execution should preserve its result")
3116 .row_count,
3117 1,
3118 );
3119 assert_planned_query_exhausts(
3120 &session,
3121 &covering,
3122 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
3123 );
3124
3125 let residual = crate::db::DynamicQuery::new(ENTITY_NAME)
3126 .filter(crate::db::FieldRef::new("payload").eq_field("id"))
3127 .select(["id"])
3128 .order_by(crate::db::asc("id"))
3129 .limit(1);
3130 assert_eq!(
3131 session
3132 .execute_trusted_dynamic_query(&residual)
3133 .expect("bounded residual execution should preserve its result")
3134 .row_count,
3135 0,
3136 );
3137 assert_planned_query_exhausts(
3138 &session,
3139 &residual,
3140 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::PredicateExpressionSteps,
3141 );
3142
3143 assert_planned_query_exhausts(
3144 &session,
3145 &fallback,
3146 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::ResultBytes,
3147 );
3148
3149 let grouped = crate::db::DynamicQuery::new(ENTITY_NAME)
3150 .group_by("payload")
3151 .aggregate(crate::db::count())
3152 .order_by(crate::db::asc("payload"))
3153 .grouped_limits(10, 16 * 1_024)
3154 .limit(1);
3155 let grouped_result = session
3156 .execute_trusted_dynamic_grouped_query(&grouped)
3157 .expect("bounded grouped execution should preserve its result");
3158 assert_eq!(grouped_result.row_count, 1);
3159 assert!(grouped_result.next_cursor.is_some());
3160 assert_grouped_query_exhausts(
3161 &session,
3162 &grouped,
3163 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::GroupDistinctEntries,
3164 );
3165 assert_grouped_query_exhausts(
3166 &session,
3167 &grouped,
3168 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::CursorSteps,
3169 );
3170
3171 assert_sql_query_exhausts(
3172 &session,
3173 "SELECT payload, COUNT(*) AS row_count FROM IdentityRow \
3174 GROUP BY payload ORDER BY row_count DESC, payload ASC LIMIT 1",
3175 icydb_diagnostic_code::DiagnosticExecutionBudgetResource::SortEntries,
3176 );
3177 }
3178
3179 fn assert_dynamic_payload(session: &DbSession<TestCanister>, key: u64, expected_payload: u64) {
3180 let unchanged = session
3181 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3182 entity: ENTITY_NAME.to_string(),
3183 key: InputValue::Nat64(key),
3184 patch: dynamic_payload_patch(expected_payload),
3185 })
3186 .expect("the expected row should remain readable through a no-op update");
3187 assert_eq!(unchanged.affected_rows, 0);
3188 assert_eq!(
3189 unchanged.rows,
3190 vec![expected_dynamic_row(key, expected_payload)],
3191 );
3192 }
3193
3194 fn batch(values: &[u64]) -> Vec<AcceptedStructuralMutation> {
3195 values
3196 .iter()
3197 .map(|value| {
3198 AcceptedStructuralMutation::save(
3199 MutationMode::Insert,
3200 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
3201 payload_patch(*value),
3202 )
3203 })
3204 .collect()
3205 }
3206
3207 fn assert_identity_boundary(error: &InternalError) {
3208 assert_eq!(error.class(), ErrorClass::Unsupported);
3209 assert_eq!(error.origin(), ErrorOrigin::Identity);
3210 }
3211
3212 #[test]
3213 fn generated_candidate_collision_is_identity_corruption_before_generic_uniqueness() {
3214 let generated = insert_key_exists_after_generation(true);
3215 assert_eq!(generated.class(), ErrorClass::Corruption);
3216 assert_eq!(generated.origin(), ErrorOrigin::Identity);
3217
3218 let ordinary = insert_key_exists_after_generation(false);
3219 assert_ne!(ordinary.origin(), ErrorOrigin::Identity);
3220 }
3221
3222 #[cfg(target_pointer_width = "64")]
3223 #[test]
3224 fn pre_key_candidate_count_rejects_values_beyond_the_persisted_u32_bound() {
3225 let error = checked_pre_key_candidate_count(
3226 usize::try_from(u64::from(u32::MAX) + 1).expect("64-bit usize should hold u32 + 1"),
3227 )
3228 .expect_err("candidate counts beyond u32 must reject");
3229 assert_identity_boundary(&error);
3230 }
3231
3232 #[test]
3233 #[expect(
3234 clippy::too_many_lines,
3235 reason = "one holding lifecycle proves split, merge, transfer, late-failure neutrality, result order, and Identity state"
3236 )]
3237 fn mixed_structural_batch_preserves_holding_conservation_and_failure_atomicity() {
3238 let session = initialize();
3239 let seeded = session
3240 .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
3241 .expect("seed rows should commit");
3242 assert_eq!(seeded.affected_rows, 1);
3243
3244 let split = session
3245 .execute_trusted_dynamic_mutation_batch(vec![
3246 DynamicMutation::Update {
3247 entity: ENTITY_NAME.to_string(),
3248 key: InputValue::Nat64(1),
3249 patch: dynamic_payload_patch(60),
3250 },
3251 DynamicMutation::Insert {
3252 entity: ENTITY_NAME.to_string(),
3253 patch: dynamic_payload_patch(40),
3254 },
3255 ])
3256 .expect("one holding should split atomically");
3257 assert_eq!(split.affected_rows, 2);
3258 assert_eq!(
3259 split.rows,
3260 vec![expected_dynamic_row(1, 60), expected_dynamic_row(2, 40),],
3261 "split after-images must retain input order and exact quantity",
3262 );
3263
3264 let rejected_split = session
3265 .execute_trusted_dynamic_mutation_batch(vec![
3266 DynamicMutation::Update {
3267 entity: ENTITY_NAME.to_string(),
3268 key: InputValue::Nat64(1),
3269 patch: dynamic_payload_patch(50),
3270 },
3271 DynamicMutation::Insert {
3272 entity: ENTITY_NAME.to_string(),
3273 patch: DynamicStructuralPatch::new(Vec::new()),
3274 },
3275 ])
3276 .expect_err("an invalid split output must reject the staged source update");
3277 assert_eq!(rejected_split.class(), ErrorClass::Unsupported);
3278 assert_eq!(rejected_split.origin(), ErrorOrigin::Executor);
3279 assert_eq!(
3280 rejected_split.diagnostic_facts(),
3281 vec![
3282 (
3283 icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3284 ENTITY_TAG.value(),
3285 ),
3286 (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 2),
3287 (
3288 icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3289 icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
3290 ),
3291 (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 1,),
3292 ],
3293 );
3294 assert_dynamic_payload(&session, 1, 60);
3295 assert_dynamic_payload(&session, 2, 40);
3296
3297 let transfer = session
3298 .execute_trusted_dynamic_mutation_batch(vec![
3299 DynamicMutation::Update {
3300 entity: ENTITY_NAME.to_string(),
3301 key: InputValue::Nat64(1),
3302 patch: dynamic_payload_patch(70),
3303 },
3304 DynamicMutation::Update {
3305 entity: ENTITY_NAME.to_string(),
3306 key: InputValue::Nat64(2),
3307 patch: dynamic_payload_patch(30),
3308 },
3309 ])
3310 .expect("distinct transfer patches should share one atomic batch");
3311 assert_eq!(
3312 transfer.rows,
3313 vec![expected_dynamic_row(1, 70), expected_dynamic_row(2, 30),],
3314 "the transfer must preserve the exact total quantity",
3315 );
3316
3317 let merge = session
3318 .execute_trusted_dynamic_mutation_batch(vec![
3319 DynamicMutation::Delete {
3320 entity: ENTITY_NAME.to_string(),
3321 key: InputValue::Nat64(2),
3322 },
3323 DynamicMutation::Update {
3324 entity: ENTITY_NAME.to_string(),
3325 key: InputValue::Nat64(1),
3326 patch: dynamic_payload_patch(100),
3327 },
3328 ])
3329 .expect("two holdings should merge atomically");
3330 assert_eq!(
3331 merge.rows,
3332 vec![expected_dynamic_row(2, 30), expected_dynamic_row(1, 100),],
3333 "delete before-images and update after-images must retain input order",
3334 );
3335
3336 let resplit = session
3337 .execute_trusted_dynamic_mutation_batch(vec![
3338 DynamicMutation::Update {
3339 entity: ENTITY_NAME.to_string(),
3340 key: InputValue::Nat64(1),
3341 patch: dynamic_payload_patch(60),
3342 },
3343 DynamicMutation::Insert {
3344 entity: ENTITY_NAME.to_string(),
3345 patch: dynamic_payload_patch(40),
3346 },
3347 ])
3348 .expect("the merged holding should split again");
3349 assert_eq!(
3350 resplit.rows,
3351 vec![expected_dynamic_row(1, 60), expected_dynamic_row(3, 40),],
3352 );
3353
3354 let rejected_merge = session
3355 .execute_trusted_dynamic_mutation_batch(vec![
3356 DynamicMutation::Delete {
3357 entity: ENTITY_NAME.to_string(),
3358 key: InputValue::Nat64(3),
3359 },
3360 DynamicMutation::Update {
3361 entity: ENTITY_NAME.to_string(),
3362 key: InputValue::Nat64(99),
3363 patch: dynamic_payload_patch(100),
3364 },
3365 ])
3366 .expect_err("a late missing merge target must preserve the earlier staged delete");
3367 assert_eq!(rejected_merge.class(), ErrorClass::NotFound);
3368 assert_dynamic_payload(&session, 1, 60);
3369 assert_dynamic_payload(&session, 3, 40);
3370
3371 SCHEMA_STORE.with(|store| {
3372 let cursor = store
3373 .borrow()
3374 .identity_statement_cursor(
3375 database_incarnation_id().expect("database incarnation should remain readable"),
3376 ENTITY_TAG,
3377 FieldId::new(1),
3378 &AcceptedFieldKind::Nat64,
3379 )
3380 .expect("mixed Identity state should remain readable");
3381 assert_eq!(cursor.expected_high_water(), 3);
3382 assert!(!cursor.has_allocations());
3383 });
3384 }
3385
3386 #[test]
3387 fn mixed_structural_batch_rejects_duplicate_holding_targets_without_mutation() {
3388 let session = initialize();
3389 session
3390 .execute_trusted_dynamic_insert_batch(ENTITY_NAME, vec![dynamic_payload_patch(100)])
3391 .expect("the holding fixture should initialize");
3392
3393 let duplicate = session
3394 .execute_trusted_dynamic_mutation_batch(vec![
3395 DynamicMutation::Update {
3396 entity: ENTITY_NAME.to_string(),
3397 key: InputValue::Nat64(1),
3398 patch: dynamic_payload_patch(60),
3399 },
3400 DynamicMutation::Delete {
3401 entity: ENTITY_NAME.to_string(),
3402 key: InputValue::Nat64(1),
3403 },
3404 ])
3405 .expect_err("duplicate targets across operation kinds must reject");
3406 assert!(matches!(
3407 duplicate.diagnostic().detail(),
3408 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3409 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchDuplicateKey,
3410 }),
3411 ));
3412 assert_eq!(
3413 duplicate.diagnostic_facts(),
3414 vec![
3415 (
3416 icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3417 ENTITY_TAG.value(),
3418 ),
3419 (
3420 icydb_diagnostic_code::DiagnosticFactTag::FirstBatchPosition,
3421 0,
3422 ),
3423 (
3424 icydb_diagnostic_code::DiagnosticFactTag::DuplicateBatchPosition,
3425 1,
3426 ),
3427 ],
3428 );
3429 assert_dynamic_payload(&session, 1, 100);
3430 }
3431
3432 #[test]
3433 fn mixed_structural_batch_rejects_empty_and_over_bound_before_resolution() {
3434 let session = initialize();
3435 let empty = session
3436 .execute_trusted_dynamic_mutation_batch(Vec::new())
3437 .expect_err("an empty public batch must reject");
3438 assert!(matches!(
3439 empty.diagnostic().detail(),
3440 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3441 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchEmpty,
3442 }),
3443 ));
3444 assert_eq!(
3445 empty.diagnostic_facts(),
3446 vec![(icydb_diagnostic_code::DiagnosticFactTag::ActualCount, 0,)],
3447 );
3448
3449 let requests = (0..=MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS)
3450 .map(|_| DynamicMutation::Delete {
3451 entity: ENTITY_NAME.to_string(),
3452 key: InputValue::Nat64(1),
3453 })
3454 .collect();
3455 let over_bound = session
3456 .execute_trusted_dynamic_mutation_batch(requests)
3457 .expect_err("operation cap plus one must reject before row resolution");
3458 assert!(matches!(
3459 over_bound.diagnostic().detail(),
3460 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3461 boundary: icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchTooManyItems,
3462 }),
3463 ));
3464 assert_eq!(
3465 over_bound.diagnostic_facts(),
3466 vec![
3467 (
3468 icydb_diagnostic_code::DiagnosticFactTag::ActualCount,
3469 (MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS + 1) as u64,
3470 ),
3471 (
3472 icydb_diagnostic_code::DiagnosticFactTag::Limit,
3473 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS as u64,
3474 ),
3475 ],
3476 );
3477 }
3478
3479 #[test]
3480 fn mixed_structural_batch_staged_byte_bound_uses_checked_exact_boundary() {
3481 let mut exact = 0;
3482 add_structural_mutation_staged_bytes(
3483 &mut exact,
3484 [MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES],
3485 )
3486 .expect("the exact staged-byte boundary should admit");
3487 assert_eq!(exact, MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES);
3488
3489 let error = add_structural_mutation_staged_bytes(&mut exact, [1])
3490 .expect_err("one byte above the staged-byte boundary must reject");
3491 assert!(matches!(
3492 error.diagnostic().detail(),
3493 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3494 boundary:
3495 icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchStagedBytesExceeded,
3496 }),
3497 ));
3498 assert_eq!(
3499 error.diagnostic_facts(),
3500 vec![
3501 (
3502 icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
3503 (MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES + 1) as u64,
3504 ),
3505 (
3506 icydb_diagnostic_code::DiagnosticFactTag::Limit,
3507 MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES as u64,
3508 ),
3509 ],
3510 );
3511
3512 validate_structural_mutation_result_bytes(MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES)
3513 .expect("the exact result-byte boundary should admit");
3514 let error = validate_structural_mutation_result_bytes(
3515 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1,
3516 )
3517 .expect_err("one byte above the result-byte boundary must reject");
3518 assert!(matches!(
3519 error.diagnostic().detail(),
3520 Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
3521 boundary:
3522 icydb_diagnostic_code::RuntimeBoundaryCode::MutationBatchResultBytesExceeded,
3523 }),
3524 ));
3525 assert_eq!(
3526 error.diagnostic_facts(),
3527 vec![
3528 (
3529 icydb_diagnostic_code::DiagnosticFactTag::ActualLength,
3530 (MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES + 1) as u64,
3531 ),
3532 (
3533 icydb_diagnostic_code::DiagnosticFactTag::Limit,
3534 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES as u64,
3535 ),
3536 ],
3537 );
3538 }
3539
3540 #[expect(
3541 clippy::too_many_lines,
3542 reason = "one lifecycle proves shared materialization and every maintained frontend against the same zero-state owner"
3543 )]
3544 #[test]
3545 fn identity_insert_frontends_share_one_committed_range_without_rejected_consumption() {
3546 let session = initialize();
3547 let catalog = session
3548 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3549 .expect("identity catalog should resolve");
3550 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3551 .expect("identity row layout should build");
3552 let initial_description = session
3553 .try_describe_entity_by_name(ENTITY_NAME)
3554 .expect("accepted Identity description should resolve");
3555 assert_eq!(
3556 initial_description.entity_tag(),
3557 catalog.identity().entity_tag().value()
3558 );
3559 assert_eq!(
3560 initial_description.accepted_schema_fingerprint_method(),
3561 catalog.fingerprint_method_version()
3562 );
3563 assert_eq!(
3564 initial_description.accepted_schema_fingerprint(),
3565 catalog.fingerprint()
3566 );
3567 let initial_identity = initial_description
3568 .identity()
3569 .expect("accepted Identity policy should be described");
3570 assert_eq!(initial_identity.field(), "id");
3571 assert_eq!(initial_identity.generator(), "Identity::next");
3572 assert_eq!(initial_identity.accepted_kind(), "nat64");
3573 assert_eq!(initial_identity.minimum(), 1);
3574 assert_eq!(initial_identity.maximum(), u128::from(u64::MAX));
3575 assert_eq!(initial_identity.high_water(), 0);
3576 assert_eq!(initial_identity.remaining(), u128::from(u64::MAX));
3577 assert!(!initial_identity.exhausted());
3578
3579 let rejected = session
3580 .execute_accepted_structural_save_batch(
3581 &catalog,
3582 &descriptor,
3583 batch(&[1_000, 2_000]),
3584 Timestamp::from_millis(6),
3585 |_| Err::<(), _>(InternalError::executor_unsupported()),
3586 )
3587 .expect_err("a rejected precommit result must not publish its tentative range");
3588 assert_eq!(rejected.class(), ErrorClass::Unsupported);
3589 assert_eq!(DATA_STORE.with(|store| store.borrow().len()), 0);
3590
3591 let rows = session
3592 .execute_accepted_structural_save_batch(
3593 &catalog,
3594 &descriptor,
3595 batch(&[10, 20, 30]),
3596 Timestamp::from_millis(7),
3597 Ok,
3598 )
3599 .expect("one accepted batch should commit rows and one identity range");
3600 assert_eq!(
3601 rows.into_iter().map(|row| row.values).collect::<Vec<_>>(),
3602 vec![
3603 vec![Value::Nat64(1), Value::Nat64(10)],
3604 vec![Value::Nat64(2), Value::Nat64(20)],
3605 vec![Value::Nat64(3), Value::Nat64(30)],
3606 ],
3607 );
3608
3609 let dynamic = session
3610 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
3611 entity: ENTITY_NAME.to_string(),
3612 patch: DynamicStructuralPatch::new(vec![(
3613 "payload".to_string(),
3614 DynamicWriteCell::Value(InputValue::Nat64(40)),
3615 )]),
3616 })
3617 .expect("dynamic omission should commit through shared Identity generation");
3618 assert_eq!(dynamic.affected_rows, 1);
3619
3620 for (request, operation) in [
3621 (
3622 DynamicMutation::Insert {
3623 entity: ENTITY_NAME.to_string(),
3624 patch: DynamicStructuralPatch::new(vec![
3625 (
3626 "id".to_string(),
3627 DynamicWriteCell::Value(InputValue::Nat64(41)),
3628 ),
3629 (
3630 "payload".to_string(),
3631 DynamicWriteCell::Value(InputValue::Nat64(42)),
3632 ),
3633 ]),
3634 },
3635 icydb_diagnostic_code::DiagnosticMutationOperation::Insert,
3636 ),
3637 (
3638 DynamicMutation::Update {
3639 entity: ENTITY_NAME.to_string(),
3640 key: InputValue::Nat64(1),
3641 patch: DynamicStructuralPatch::new(vec![(
3642 "id".to_string(),
3643 DynamicWriteCell::Default,
3644 )]),
3645 },
3646 icydb_diagnostic_code::DiagnosticMutationOperation::Update,
3647 ),
3648 ] {
3649 let error = session
3650 .execute_trusted_dynamic_mutation(&request)
3651 .expect_err("structural Identity authorship and regeneration must reject");
3652 assert_eq!(error.class(), ErrorClass::Unsupported);
3653 assert_eq!(error.origin(), ErrorOrigin::Executor);
3654 assert_eq!(
3655 error.diagnostic_facts(),
3656 vec![
3657 (
3658 icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3659 ENTITY_TAG.value(),
3660 ),
3661 (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
3662 (
3663 icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3664 operation.raw(),
3665 ),
3666 (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
3667 ],
3668 );
3669 }
3670
3671 let binding = session
3672 .issue_typed_entity_binding(
3673 ENTITY_SOURCE,
3674 &[
3675 DynamicTypedFieldBindingRequest::new(
3676 ID_SOURCE.to_string(),
3677 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3678 false,
3679 ),
3680 DynamicTypedFieldBindingRequest::new(
3681 PAYLOAD_SOURCE.to_string(),
3682 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
3683 false,
3684 ),
3685 ],
3686 )
3687 .expect("typed output should bind the Identity field");
3688 let typed_patch = binding
3689 .bind_write_fields(vec![(
3690 PAYLOAD_SOURCE.to_string(),
3691 DynamicWriteCell::Value(InputValue::Nat64(50)),
3692 )])
3693 .expect("typed payload should lower");
3694 let typed = session
3695 .execute_trusted_typed_mutation(
3696 &binding,
3697 &DynamicTypedMutation::Insert { patch: typed_patch },
3698 )
3699 .expect("typed omission should commit through shared Identity generation");
3700 assert_eq!(
3701 typed
3702 .expect("typed insert should return one mutation result")
3703 .affected_rows,
3704 1,
3705 );
3706 let explicit_typed_patch = binding
3707 .bind_write_fields(vec![
3708 (
3709 ID_SOURCE.to_string(),
3710 DynamicWriteCell::Value(InputValue::Nat64(51)),
3711 ),
3712 (
3713 PAYLOAD_SOURCE.to_string(),
3714 DynamicWriteCell::Value(InputValue::Nat64(52)),
3715 ),
3716 ])
3717 .expect("the low-level binding should retain exact authored intent");
3718 let explicit_typed_error = session
3719 .execute_trusted_typed_mutation(
3720 &binding,
3721 &DynamicTypedMutation::Insert {
3722 patch: explicit_typed_patch,
3723 },
3724 )
3725 .expect_err("typed Identity authorship must reject before allocation");
3726 assert_eq!(explicit_typed_error.class(), ErrorClass::Unsupported);
3727 assert_eq!(explicit_typed_error.origin(), ErrorOrigin::Executor);
3728 assert_eq!(
3729 explicit_typed_error.diagnostic_facts(),
3730 vec![
3731 (
3732 icydb_diagnostic_code::DiagnosticFactTag::EntityTag,
3733 ENTITY_TAG.value(),
3734 ),
3735 (icydb_diagnostic_code::DiagnosticFactTag::FieldId, 1),
3736 (
3737 icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
3738 icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
3739 ),
3740 (icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,),
3741 ],
3742 );
3743
3744 let replace_error = session
3745 .execute_trusted_dynamic_mutation(&DynamicMutation::Replace {
3746 entity: ENTITY_NAME.to_string(),
3747 key: InputValue::Nat64(99),
3748 patch: DynamicStructuralPatch::new(vec![(
3749 "payload".to_string(),
3750 DynamicWriteCell::Value(InputValue::Nat64(60)),
3751 )]),
3752 })
3753 .expect_err("save-as-insert with a chosen Identity must reject");
3754 assert_eq!(replace_error.class(), ErrorClass::Unsupported);
3755 assert_eq!(replace_error.origin(), ErrorOrigin::Executor);
3756
3757 #[cfg(feature = "sql")]
3758 {
3759 for sql in [
3760 "INSERT INTO IdentityRow (payload) VALUES (70) RETURNING id, payload",
3761 "INSERT INTO IdentityRow (id, payload) VALUES (DEFAULT, 80) RETURNING id",
3762 ] {
3763 let _result = session
3764 .execute_trusted_sql_mutation(sql)
3765 .expect("SQL omission and DEFAULT should commit Identity generation");
3766 }
3767
3768 let error = session
3769 .execute_trusted_sql_mutation(
3770 "INSERT INTO IdentityRow (id, payload) VALUES (42, 90)",
3771 )
3772 .expect_err("an explicit SQL Identity value must reject before allocation");
3773 let diagnostic = error.diagnostic();
3774 assert_eq!(
3775 diagnostic.code(),
3776 icydb_diagnostic_code::DiagnosticCode::QuerySqlWriteBoundary,
3777 );
3778 assert!(matches!(
3779 diagnostic.detail(),
3780 Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary {
3781 boundary: icydb_diagnostic_code::SqlWriteBoundaryCode::ExplicitGeneratedField,
3782 }),
3783 ));
3784 }
3785
3786 let expected_committed = if cfg!(feature = "sql") { 7 } else { 5 };
3787 assert_eq!(
3788 DATA_STORE.with(|store| store.borrow().len()),
3789 expected_committed
3790 );
3791 SCHEMA_STORE.with(|store| {
3792 let cursor = store
3793 .borrow()
3794 .identity_statement_cursor(
3795 database_incarnation_id().expect("database incarnation should remain readable"),
3796 ENTITY_TAG,
3797 FieldId::new(1),
3798 &AcceptedFieldKind::Nat64,
3799 )
3800 .expect("committed writes must leave active state readable");
3801 assert_eq!(cursor.expected_high_water(), u128::from(expected_committed),);
3802 assert!(!cursor.has_allocations());
3803 });
3804 let committed_description = session
3805 .try_describe_entity_by_name(ENTITY_NAME)
3806 .expect("committed Identity description should resolve");
3807 let committed_identity = committed_description
3808 .identity()
3809 .expect("accepted Identity policy should remain described");
3810 assert_eq!(
3811 committed_identity.high_water(),
3812 u128::from(expected_committed),
3813 );
3814 assert_eq!(
3815 committed_identity.remaining(),
3816 u128::from(u64::MAX - expected_committed),
3817 );
3818 assert!(!committed_identity.exhausted());
3819 }
3820
3821 #[test]
3822 #[expect(
3823 clippy::too_many_lines,
3824 reason = "one ordered scenario exercises every durable interruption boundary, guarded recovery, derived rebuild, and both integrity tiers"
3825 )]
3826 fn journaled_identity_recovery_quiesces_every_publication_interruption_before_reallocation() {
3827 let session = initialize_journaled();
3828 let catalog = session
3829 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
3830 .expect("journaled identity catalog should resolve");
3831 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
3832 .expect("journaled identity row layout should build");
3833
3834 for (ordinal, interruption) in [
3835 MutationCommitInterruption::MarkerPersisted,
3836 MutationCommitInterruption::JournalPublished,
3837 MutationCommitInterruption::RowsPublished,
3838 MutationCommitInterruption::StateMaterialized,
3839 ]
3840 .into_iter()
3841 .enumerate()
3842 {
3843 interrupt_next_mutation_commit_for_tests(interruption);
3844 let interrupted = session.execute_accepted_structural_save_batch(
3845 &catalog,
3846 &descriptor,
3847 batch(&[u64::try_from(ordinal).expect("ordinal should fit")]),
3848 Timestamp::from_millis(8),
3849 Ok,
3850 );
3851 assert!(
3852 interrupted.is_err(),
3853 "the selected durable boundary should interrupt",
3854 );
3855
3856 let committed = session
3857 .execute_accepted_structural_save_batch(
3858 &catalog,
3859 &descriptor,
3860 batch(&[100 + u64::try_from(ordinal).expect("ordinal should fit")]),
3861 Timestamp::from_millis(9),
3862 Ok,
3863 )
3864 .expect("the next mutation must recover before allocating");
3865 let expected_high_water =
3866 u64::try_from((ordinal + 1) * 2).expect("small test high-water should fit");
3867 assert_eq!(
3868 committed
3869 .into_iter()
3870 .map(|row| row.values)
3871 .collect::<Vec<_>>(),
3872 vec![vec![
3873 Value::Nat64(expected_high_water),
3874 Value::Nat64(100 + u64::try_from(ordinal).expect("ordinal should fit")),
3875 ]],
3876 );
3877 assert_eq!(
3878 JOURNALED_DATA_STORE.with(|store| store.borrow().len()),
3879 expected_high_water,
3880 );
3881 JOURNALED_SCHEMA_STORE.with(|store| {
3882 let cursor = store
3883 .borrow()
3884 .identity_statement_cursor(
3885 database_incarnation_id()
3886 .expect("database incarnation should remain readable"),
3887 ENTITY_TAG,
3888 FieldId::new(1),
3889 &AcceptedFieldKind::Nat64,
3890 )
3891 .expect("guarded recovery must leave quiescent active state");
3892 assert_eq!(
3893 cursor.expected_high_water(),
3894 u128::from(expected_high_water),
3895 );
3896 assert!(!cursor.has_allocations());
3897 });
3898 }
3899
3900 for (ordinal, (interruption, deleted_key)) in [
3901 (MutationCommitInterruption::MarkerPersisted, 2),
3902 (MutationCommitInterruption::JournalPublished, 4),
3903 (MutationCommitInterruption::RowPrefixPublished, 6),
3904 (MutationCommitInterruption::RowsPublished, 8),
3905 (MutationCommitInterruption::StateMaterialized, 7),
3906 ]
3907 .into_iter()
3908 .enumerate()
3909 {
3910 let expected_payload =
3911 501 + u64::try_from(ordinal).expect("small interruption ordinal should fit");
3912 interrupt_next_mutation_commit_for_tests(interruption);
3913 let interrupted = session.execute_trusted_dynamic_mutation_batch(vec![
3914 DynamicMutation::Update {
3915 entity: ENTITY_NAME.to_string(),
3916 key: InputValue::Nat64(1),
3917 patch: dynamic_payload_patch(expected_payload),
3918 },
3919 DynamicMutation::Delete {
3920 entity: ENTITY_NAME.to_string(),
3921 key: InputValue::Nat64(deleted_key),
3922 },
3923 ]);
3924 assert!(
3925 interrupted.is_err(),
3926 "the selected caller-key mixed publication boundary should interrupt",
3927 );
3928 let recovered_update = session
3929 .execute_trusted_dynamic_mutation(&DynamicMutation::Update {
3930 entity: ENTITY_NAME.to_string(),
3931 key: InputValue::Nat64(1),
3932 patch: dynamic_payload_patch(expected_payload),
3933 })
3934 .expect("guarded reentry should complete the marker-authorized mixed batch");
3935 assert_eq!(
3936 recovered_update.affected_rows, 0,
3937 "the recovered update must already expose its admitted final image",
3938 );
3939 let recovered_delete = session
3940 .execute_trusted_dynamic_mutation(&DynamicMutation::Delete {
3941 entity: ENTITY_NAME.to_string(),
3942 key: InputValue::Nat64(deleted_key),
3943 })
3944 .expect_err("the recovered delete must already be materialized");
3945 assert_eq!(recovered_delete.class(), ErrorClass::NotFound);
3946 JOURNALED_SCHEMA_STORE.with(|store| {
3947 let cursor = store
3948 .borrow()
3949 .identity_statement_cursor(
3950 database_incarnation_id()
3951 .expect("database incarnation should remain readable"),
3952 ENTITY_TAG,
3953 FieldId::new(1),
3954 &AcceptedFieldKind::Nat64,
3955 )
3956 .expect("caller-key recovery must preserve active Identity state");
3957 assert_eq!(cursor.expected_high_water(), 8);
3958 assert!(!cursor.has_allocations());
3959 });
3960 }
3961
3962 forget_recovered_domain_for_tests(&session.db)
3963 .expect("the final journal tail should remain recoverable");
3964 session
3965 .db
3966 .ensure_recovered_state()
3967 .expect("derived rebuild must not allocate another identity");
3968
3969 let quick = execute_quick_integrity(&session.db, catalog.inspection_plan())
3970 .expect("quiescent Identity control inventory should be inspectable");
3971 assert_eq!(quick.status(), &QuickIntegrityStatus::CompleteClean);
3972 let row_page = execute_row_integrity_page(
3973 &session.db,
3974 catalog.inspection_plan(),
3975 PhysicalUnitCheckpoint::BeforeFirst,
3976 RowInspectionLimits::standard(),
3977 )
3978 .expect("Identity rows should remain within committed high-water");
3979 assert!(row_page.exhausted());
3980 assert!(row_page.findings().is_empty());
3981
3982 assert_eq!(JOURNALED_DATA_STORE.with(|store| store.borrow().len()), 3);
3983 assert!(
3984 JOURNALED_INDEX_STORE.with(|store| !store.borrow().is_empty()),
3985 "derived index rebuild should restore witnesses without allocating identities",
3986 );
3987 assert!(!JOURNALED_TAIL_STORE.with(|tail| tail.borrow().has_stored_batch()));
3988 JOURNALED_SCHEMA_STORE.with(|store| {
3989 let cursor = store
3990 .borrow()
3991 .identity_statement_cursor(
3992 database_incarnation_id().expect("database incarnation should remain readable"),
3993 ENTITY_TAG,
3994 FieldId::new(1),
3995 &AcceptedFieldKind::Nat64,
3996 )
3997 .expect("folded identity state should reopen without allocating");
3998 assert_eq!(cursor.expected_high_water(), 8);
3999 assert!(!cursor.has_allocations());
4000 });
4001 }
4002
4003 #[test]
4004 #[ignore = "release-closeout native timing probe for one marker-authorized Identity recovery"]
4005 fn identity_recovery_closeout_reports_guarded_reentry_time() {
4006 let session = initialize_journaled();
4007 let catalog = session
4008 .accepted_schema_catalog_context_for_entity_name(Some(ENTITY_NAME))
4009 .expect("journaled identity catalog should resolve");
4010 let descriptor = AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())
4011 .expect("journaled identity row layout should build");
4012
4013 interrupt_next_mutation_commit_for_tests(MutationCommitInterruption::RowsPublished);
4014 let interrupted = session.execute_accepted_structural_save_batch(
4015 &catalog,
4016 &descriptor,
4017 batch(&[1]),
4018 Timestamp::from_millis(10),
4019 Ok,
4020 );
4021 assert!(
4022 interrupted.is_err(),
4023 "the selected publication boundary should interrupt",
4024 );
4025
4026 let start = Instant::now();
4027 let committed = session
4028 .execute_accepted_structural_save_batch(
4029 &catalog,
4030 &descriptor,
4031 batch(&[2]),
4032 Timestamp::from_millis(11),
4033 Ok,
4034 )
4035 .expect("guarded reentry should recover before allocation");
4036 let elapsed = start.elapsed();
4037 assert_eq!(
4038 committed
4039 .into_iter()
4040 .map(|row| row.values)
4041 .collect::<Vec<_>>(),
4042 vec![vec![Value::Nat64(2), Value::Nat64(2)]],
4043 );
4044
4045 println!(
4046 "identity recovery closeout: guarded_reentry_nanos={}",
4047 elapsed.as_nanos(),
4048 );
4049 }
4050}
4051
4052#[cfg(test)]
4053mod targeted_rule_mutation_tests {
4054 use super::{
4055 DbSession, DynamicMutation, DynamicStructuralPatch, DynamicTypedFieldBindingRequest,
4056 DynamicTypedFieldType, DynamicTypedMutation, DynamicWriteCell,
4057 };
4058 use crate::{
4059 db::{
4060 data::{DataStore, encode_input_value_for_candidate_field_contract},
4061 index::IndexStore,
4062 registry::{StoreAllocationIdentities, StoreRegistry, StoreRuntimeStorageCapabilities},
4063 schema::{
4064 AcceptedCheckLiteralV1, AcceptedCompositeCatalog, AcceptedFieldDecodeContract,
4065 AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation,
4066 AcceptedRuleTarget, AcceptedSchemaRevision, AcceptedSourceBindingCatalog,
4067 ConstraintOrigin, FieldId, FieldStorageDecode, FieldWriteManagement, LeafCodec,
4068 PersistedFieldSnapshot, PersistedNestedLeafSnapshot, PersistedSchemaSnapshot,
4069 ScalarCodec, SchemaFieldSlot, SchemaFieldWritePolicy, SchemaInsertDefault,
4070 SchemaRowLayout, SchemaStore, SchemaVersion,
4071 accepted_schema_candidate_with_catalogs_for_tests,
4072 build_record_newtype_composite_catalog_for_tests,
4073 empty_accepted_enum_catalog_for_tests, enum_catalog::ValueAdmissionBudget,
4074 },
4075 },
4076 error::InternalError,
4077 traits::{CanisterKind, Path},
4078 types::EntityTag,
4079 value::InputValue,
4080 };
4081 use icydb_schema::{
4082 ConstraintSourceKey, EntitySourceKey, FieldSourceKey, ScalarType, TypeSourceKey,
4083 };
4084 use std::{cell::RefCell, collections::BTreeMap};
4085
4086 const STORE_PATH: &str = "session::write::targeted_rule_mutation_tests::Store";
4087 const ENTITY_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity";
4088 const ID_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::id";
4089 const PROFILE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Entity::profile";
4090 const UPDATED_AT_SOURCE: &str =
4091 "session::write::targeted_rule_mutation_tests::Entity::updated_at";
4092 const PROFILE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Profile";
4093 const DEGREE_TYPE_SOURCE: &str = "session::write::targeted_rule_mutation_tests::Degree";
4094 const DEGREE_MEMBER_SOURCE: &str =
4095 "session::write::targeted_rule_mutation_tests::Profile::degree";
4096 const DEGREE_RULE_SOURCE: &str =
4097 "session::write::targeted_rule_mutation_tests::Profile::degree_multiple";
4098
4099 struct TestCanister;
4100
4101 impl Path for TestCanister {
4102 const PATH: &'static str = "session::write::targeted_rule_mutation_tests::Canister";
4103 }
4104
4105 impl CanisterKind for TestCanister {
4106 const COMMIT_MEMORY_ID: u8 = 43;
4107 const COMMIT_STABLE_KEY: &'static str = "icydb.targeted_mutation_tests.commit.v1";
4108 const INTEGRITY_PROGRESS_MEMORY_ID: u8 = 44;
4109 const INTEGRITY_PROGRESS_STABLE_KEY: &'static str =
4110 "icydb.targeted_mutation_tests.integrity.progress.v1";
4111 }
4112
4113 thread_local! {
4114 static DATA_STORE: RefCell<DataStore> = const { RefCell::new(DataStore::init_heap()) };
4115 static INDEX_STORE: RefCell<IndexStore> = const { RefCell::new(IndexStore::init_heap()) };
4116 static SCHEMA_STORE: RefCell<SchemaStore> =
4117 const { RefCell::new(SchemaStore::init_heap()) };
4118 static STORE_REGISTRY: StoreRegistry = {
4119 let mut registry = StoreRegistry::new();
4120 registry.register_store(
4121 STORE_PATH,
4122 &DATA_STORE,
4123 &INDEX_STORE,
4124 &SCHEMA_STORE,
4125 StoreAllocationIdentities::absent(),
4126 StoreRuntimeStorageCapabilities::heap(),
4127 ).expect("targeted mutation test store should register");
4128 registry
4129 };
4130 }
4131
4132 fn source<T, E: std::fmt::Debug>(raw: &str, parse: impl FnOnce(String) -> Result<T, E>) -> T {
4133 parse(raw.to_string()).expect("test source identity should admit")
4134 }
4135
4136 fn profile_input(degree: u64) -> InputValue {
4137 InputValue::Map(vec![(
4138 InputValue::Text("degree".to_string()),
4139 InputValue::Nat64(degree),
4140 )])
4141 }
4142
4143 fn structural_patch(id: u64, degree: u64) -> DynamicStructuralPatch {
4144 DynamicStructuralPatch::new(vec![
4145 (
4146 "id".to_string(),
4147 DynamicWriteCell::Value(InputValue::Nat64(id)),
4148 ),
4149 (
4150 "profile".to_string(),
4151 DynamicWriteCell::Value(profile_input(degree)),
4152 ),
4153 ])
4154 }
4155
4156 fn encoded_value(
4157 enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
4158 composite_catalog: &AcceptedCompositeCatalog,
4159 name: &str,
4160 kind: &AcceptedFieldKind,
4161 storage_decode: FieldStorageDecode,
4162 leaf_codec: LeafCodec,
4163 value: InputValue,
4164 ) -> Vec<u8> {
4165 let field = AcceptedFieldDecodeContract::new(name, kind, false, storage_decode, leaf_codec);
4166 encode_input_value_for_candidate_field_contract(
4167 enum_catalog,
4168 composite_catalog,
4169 field,
4170 value,
4171 &mut ValueAdmissionBudget::standard(),
4172 )
4173 .expect("test accepted value should encode")
4174 }
4175
4176 fn nat64_literal(
4177 enum_catalog: &crate::db::schema::AcceptedEnumCatalog,
4178 composite_catalog: &AcceptedCompositeCatalog,
4179 value: u64,
4180 ) -> AcceptedCheckLiteralV1 {
4181 let kind = AcceptedFieldKind::Nat64;
4182 AcceptedCheckLiteralV1::from_accepted_parts(
4183 kind.clone(),
4184 FieldStorageDecode::ByKind,
4185 LeafCodec::Scalar(ScalarCodec::Nat64),
4186 encoded_value(
4187 enum_catalog,
4188 composite_catalog,
4189 "degree_bound",
4190 &kind,
4191 FieldStorageDecode::ByKind,
4192 LeafCodec::Scalar(ScalarCodec::Nat64),
4193 InputValue::Nat64(value),
4194 ),
4195 )
4196 }
4197
4198 fn targeted_constraint_id(error: &InternalError) -> u32 {
4199 let facts = error.diagnostic_facts();
4200 assert!(facts.contains(&(
4201 icydb_diagnostic_code::DiagnosticFactTag::MutationOperation,
4202 icydb_diagnostic_code::DiagnosticMutationOperation::Insert.raw(),
4203 )));
4204 assert!(facts.contains(&(icydb_diagnostic_code::DiagnosticFactTag::BatchPosition, 0,)));
4205 assert!(facts.contains(&(
4206 icydb_diagnostic_code::DiagnosticFactTag::ConstraintKind,
4207 icydb_diagnostic_code::DiagnosticConstraintKind::TargetedRule.raw(),
4208 )));
4209 assert_eq!(
4210 facts
4211 .iter()
4212 .filter(|(tag, _)| matches!(
4213 tag,
4214 icydb_diagnostic_code::DiagnosticFactTag::RootField
4215 | icydb_diagnostic_code::DiagnosticFactTag::RecordMember
4216 ))
4217 .copied()
4218 .collect::<Vec<_>>(),
4219 vec![
4220 (icydb_diagnostic_code::DiagnosticFactTag::RootField, 2),
4221 (
4222 icydb_diagnostic_code::DiagnosticFactTag::RecordMember,
4223 icydb_diagnostic_code::pack_u32_pair(1, 1),
4224 ),
4225 ]
4226 );
4227 let value = facts
4228 .iter()
4229 .find_map(|(tag, value)| {
4230 (*tag == icydb_diagnostic_code::DiagnosticFactTag::ConstraintId).then_some(*value)
4231 })
4232 .expect("targeted mutation should retain its accepted constraint ID");
4233 u32::try_from(value).expect("accepted constraint ID fits u32")
4234 }
4235
4236 #[expect(
4237 clippy::too_many_lines,
4238 reason = "one end-to-end fixture proves every maintained write frontend converges on the same accepted targeted-rule schedule"
4239 )]
4240 #[test]
4241 fn targeted_rules_converge_across_dynamic_typed_sql_default_timestamp_and_batch_writes() {
4242 DATA_STORE.with(|store| *store.borrow_mut() = DataStore::init_heap());
4243 INDEX_STORE.with(|store| *store.borrow_mut() = IndexStore::init_heap());
4244 SCHEMA_STORE.with(|store| *store.borrow_mut() = SchemaStore::init_heap());
4245
4246 let entity_tag = EntityTag::new(93);
4247 let enum_catalog = empty_accepted_enum_catalog_for_tests();
4248 let (composite_catalog, profile_type, degree_type, degree_member) =
4249 build_record_newtype_composite_catalog_for_tests(
4250 "tests::TargetedProfile".to_string(),
4251 "degree".to_string(),
4252 "tests::TargetedDegree".to_string(),
4253 AcceptedFieldKind::Nat64,
4254 &enum_catalog,
4255 )
4256 .expect("targeted mutation composites should close");
4257 let profile_kind = AcceptedFieldKind::Composite {
4258 type_id: profile_type,
4259 };
4260 let profile_default = encoded_value(
4261 &enum_catalog,
4262 &composite_catalog,
4263 "profile",
4264 &profile_kind,
4265 FieldStorageDecode::CatalogValue,
4266 LeafCodec::Structural,
4267 profile_input(12),
4268 );
4269 let fields = vec![
4270 PersistedFieldSnapshot::new_initial(
4271 FieldId::new(1),
4272 "id".to_string(),
4273 SchemaFieldSlot::new(0),
4274 AcceptedFieldKind::Nat64,
4275 Vec::new(),
4276 false,
4277 SchemaInsertDefault::None,
4278 FieldStorageDecode::ByKind,
4279 LeafCodec::Scalar(ScalarCodec::Nat64),
4280 ),
4281 PersistedFieldSnapshot::new_initial(
4282 FieldId::new(2),
4283 "profile".to_string(),
4284 SchemaFieldSlot::new(1),
4285 profile_kind,
4286 vec![PersistedNestedLeafSnapshot::new(
4287 vec!["degree".to_string()],
4288 AcceptedFieldKind::Composite {
4289 type_id: degree_type,
4290 },
4291 false,
4292 )],
4293 false,
4294 SchemaInsertDefault::SlotPayload(profile_default),
4295 FieldStorageDecode::CatalogValue,
4296 LeafCodec::Structural,
4297 ),
4298 PersistedFieldSnapshot::new_initial_with_write_policy(
4299 FieldId::new(3),
4300 "updated_at".to_string(),
4301 SchemaFieldSlot::new(2),
4302 AcceptedFieldKind::Timestamp,
4303 Vec::new(),
4304 false,
4305 SchemaInsertDefault::None,
4306 SchemaFieldWritePolicy::from_model_policies(
4307 None,
4308 Some(FieldWriteManagement::UpdatedAt),
4309 ),
4310 FieldStorageDecode::ByKind,
4311 LeafCodec::Scalar(ScalarCodec::Timestamp),
4312 ),
4313 ];
4314 let mut snapshot = PersistedSchemaSnapshot::new(
4315 SchemaVersion::initial(),
4316 ENTITY_SOURCE.to_string(),
4317 "TargetedMutation".to_string(),
4318 FieldId::new(1),
4319 SchemaRowLayout::initial(
4320 fields
4321 .iter()
4322 .map(|field| (field.id(), field.slot()))
4323 .collect(),
4324 ),
4325 fields,
4326 );
4327 let constraint_catalog = snapshot
4328 .constraint_catalog()
4329 .clone()
4330 .with_added_targeted_rule(
4331 "profile_degree_multiple".to_string(),
4332 ConstraintOrigin::Generated,
4333 AcceptedRuleTarget::new(
4334 FieldId::new(2),
4335 AcceptedNamedTypeIdentity::Composite(degree_type),
4336 ),
4337 AcceptedRuleOperation::MultipleOf {
4338 divisor: nat64_literal(&enum_catalog, &composite_catalog, 5),
4339 },
4340 )
4341 .expect("targeted mutation rule should allocate");
4342 let targeted_rule_id = constraint_catalog
4343 .constraints()
4344 .last()
4345 .expect("targeted mutation rule should persist")
4346 .id();
4347 snapshot = snapshot.with_constraint_catalog(constraint_catalog);
4348
4349 let entity_source = source(ENTITY_SOURCE, EntitySourceKey::try_new);
4350 let id_source = source(ID_SOURCE, FieldSourceKey::try_new);
4351 let profile_source = source(PROFILE_SOURCE, FieldSourceKey::try_new);
4352 let updated_at_source = source(UPDATED_AT_SOURCE, FieldSourceKey::try_new);
4353 let profile_type_source = source(PROFILE_TYPE_SOURCE, TypeSourceKey::try_new);
4354 let degree_type_source = source(DEGREE_TYPE_SOURCE, TypeSourceKey::try_new);
4355 let degree_member_source = source(DEGREE_MEMBER_SOURCE, FieldSourceKey::try_new);
4356 let degree_rule_source = source(DEGREE_RULE_SOURCE, ConstraintSourceKey::try_new);
4357 let source_bindings = AcceptedSourceBindingCatalog::initial_for_tests(
4358 BTreeMap::from([(entity_source, entity_tag)]),
4359 BTreeMap::from([
4360 ((entity_tag, id_source), FieldId::new(1)),
4361 ((entity_tag, profile_source), FieldId::new(2)),
4362 ((entity_tag, updated_at_source), FieldId::new(3)),
4363 ]),
4364 BTreeMap::from([((entity_tag, degree_rule_source), targeted_rule_id)]),
4365 BTreeMap::new(),
4366 BTreeMap::new(),
4367 )
4368 .with_initial_named_types_for_tests(
4369 BTreeMap::from([
4370 (
4371 profile_type_source,
4372 AcceptedNamedTypeIdentity::Composite(profile_type),
4373 ),
4374 (
4375 degree_type_source,
4376 AcceptedNamedTypeIdentity::Composite(degree_type),
4377 ),
4378 ]),
4379 BTreeMap::new(),
4380 BTreeMap::from([((profile_type, degree_member_source), degree_member)]),
4381 );
4382 let candidate = accepted_schema_candidate_with_catalogs_for_tests(
4383 STORE_PATH,
4384 AcceptedSchemaRevision::INITIAL,
4385 enum_catalog,
4386 composite_catalog,
4387 source_bindings,
4388 BTreeMap::from([(entity_tag, snapshot)]),
4389 );
4390
4391 let session = DbSession::<TestCanister>::new(
4392 &STORE_REGISTRY,
4393 &crate::db::RequestExecutionRoot::__new_runtime_root(),
4394 );
4395 session
4396 .db
4397 .ensure_recovered_state()
4398 .expect("targeted mutation test database should initialize");
4399 let store = session
4400 .db
4401 .store_handle(STORE_PATH)
4402 .expect("targeted mutation test store should resolve");
4403 crate::db::commit::publish_accepted_schema_candidate(
4404 STORE_PATH,
4405 store,
4406 AcceptedSchemaRevision::NONE,
4407 &candidate,
4408 )
4409 .expect("targeted mutation candidate should publish");
4410
4411 let dynamic_error = session
4412 .execute_trusted_dynamic_mutation(&DynamicMutation::Insert {
4413 entity: "TargetedMutation".to_string(),
4414 patch: structural_patch(1, 12),
4415 })
4416 .expect_err("dynamic write must enforce the targeted rule");
4417 assert_eq!(
4418 targeted_constraint_id(&dynamic_error),
4419 targeted_rule_id.get()
4420 );
4421
4422 let binding = session
4423 .issue_typed_entity_binding(
4424 ENTITY_SOURCE,
4425 &[
4426 DynamicTypedFieldBindingRequest::new(
4427 ID_SOURCE.to_string(),
4428 DynamicTypedFieldType::Scalar(ScalarType::Nat64),
4429 false,
4430 ),
4431 DynamicTypedFieldBindingRequest::new(
4432 PROFILE_SOURCE.to_string(),
4433 DynamicTypedFieldType::Named(PROFILE_TYPE_SOURCE.to_string()),
4434 false,
4435 ),
4436 DynamicTypedFieldBindingRequest::new(
4437 UPDATED_AT_SOURCE.to_string(),
4438 DynamicTypedFieldType::Scalar(ScalarType::Timestamp),
4439 false,
4440 ),
4441 ],
4442 )
4443 .expect("targeted typed binding should issue");
4444 let typed_patch = binding
4445 .bind_write_fields(vec![
4446 (
4447 ID_SOURCE.to_string(),
4448 DynamicWriteCell::Value(InputValue::Nat64(2)),
4449 ),
4450 (
4451 PROFILE_SOURCE.to_string(),
4452 DynamicWriteCell::Value(profile_input(12)),
4453 ),
4454 ])
4455 .expect("targeted typed patch should bind");
4456 let typed_error = session
4457 .execute_trusted_typed_mutation(
4458 &binding,
4459 &DynamicTypedMutation::Insert { patch: typed_patch },
4460 )
4461 .expect_err("typed write must enforce the targeted rule");
4462 assert_eq!(targeted_constraint_id(&typed_error), targeted_rule_id.get());
4463
4464 #[cfg(feature = "sql")]
4465 {
4466 let sql_error = session
4467 .execute_trusted_sql_mutation("INSERT INTO TargetedMutation (id) VALUES (3)")
4468 .expect_err("SQL default resolution must enforce the targeted rule");
4469 let crate::db::QueryError::Execute(execute) = sql_error else {
4470 panic!("targeted SQL write should fail at shared execution admission");
4471 };
4472 assert_eq!(
4473 targeted_constraint_id(execute.as_internal()),
4474 targeted_rule_id.get()
4475 );
4476 }
4477
4478 session
4479 .execute_trusted_dynamic_mutation_batch(vec![
4480 DynamicMutation::Insert {
4481 entity: "TargetedMutation".to_string(),
4482 patch: structural_patch(4, 5),
4483 },
4484 DynamicMutation::Insert {
4485 entity: "TargetedMutation".to_string(),
4486 patch: structural_patch(5, 12),
4487 },
4488 ])
4489 .expect_err("one invalid targeted value must reject the whole batch");
4490 assert_eq!(
4491 DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
4492 Some(0),
4493 "no frontend or earlier valid batch row may escape targeted admission",
4494 );
4495
4496 let admitted = session
4497 .execute_trusted_dynamic_mutation_batch(vec![
4498 DynamicMutation::Insert {
4499 entity: "TargetedMutation".to_string(),
4500 patch: structural_patch(6, 5),
4501 },
4502 DynamicMutation::Insert {
4503 entity: "TargetedMutation".to_string(),
4504 patch: structural_patch(7, 10),
4505 },
4506 ])
4507 .expect("compliant targeted values should share one accepted batch");
4508 let [first, second] = admitted.rows.as_slice() else {
4509 panic!("the mixed targeted batch should return two rows");
4510 };
4511 let first_timestamp = first
4512 .get(2)
4513 .expect("the first mixed row should contain its managed timestamp");
4514 assert!(matches!(
4515 first_timestamp,
4516 crate::value::OutputValue::Timestamp(_)
4517 ));
4518 assert_eq!(
4519 second.get(2),
4520 Some(first_timestamp),
4521 "one accepted mixed batch must materialize one managed timestamp",
4522 );
4523 assert_eq!(
4524 DATA_STORE.with(|store| store.borrow().exact_entity_count(entity_tag)),
4525 Some(2),
4526 );
4527 }
4528}