1#[cfg(test)]
8mod identity_pre_key_tests;
9#[cfg(test)]
10mod key_handoff_tests;
11#[cfg(test)]
12mod mixed_relation_batch_tests;
13#[cfg(test)]
14mod output_handoff_tests;
15#[cfg(test)]
16mod targeted_rule_mutation_tests;
17#[cfg(test)]
18mod typed_adapter_tests;
19
20#[cfg(test)]
21use crate::db::executor::mutation::structural::{
22 AcceptedStructuralMutationPacking, AcceptedStructuralMutationStagedAdmission,
23 MAX_STRUCTURAL_MUTATION_BATCH_STAGED_BYTES, add_structural_mutation_staged_bytes,
24 admit_structural_mutation_staged_charge, data_key_from_validated_reader,
25 insert_key_exists_after_generation, into_mutation_output_values,
26 structural_mutation_staged_charge,
27};
28use crate::db::executor::{
29 AcceptedSchemaCatalogContext,
30 mutation::structural::{
31 AcceptedStructuralMutation, AcceptedStructuralMutationBatchItem,
32 AcceptedStructuralMutationCommitDirective, AcceptedStructuralMutationCommitOptions,
33 AcceptedStructuralMutationRow, AcceptedStructuralMutationTarget,
34 MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES, MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
35 execute_accepted_structural_mutation_batch, mutation_diagnostic_context,
36 validate_structural_mutation_result_bytes,
37 },
38};
39#[cfg(any(feature = "sql", test))]
40use crate::value::Value;
41
42#[cfg(test)]
43use crate::db::{
44 data::{RawRow, StructuralRowContract, StructuralSlotReader},
45 executor::mutation::structural::{
46 MAX_STRUCTURAL_MUTATION_BATCH_RESULT_BYTES, checked_pre_key_candidate_count,
47 },
48 integrity::MutationProgressRecordOp,
49 schema::FieldId,
50};
51use crate::{
52 db::{
53 DbSession, DynamicMutation, DynamicMutationResult, DynamicStructuralPatch,
54 DynamicTypedBindingError, DynamicTypedEntityBinding, DynamicTypedMutation,
55 DynamicTypedStructuralPatch, DynamicWriteCell, TypedEntityDescriptor, TypedFieldType,
56 commit::database_incarnation_id,
57 data::{AcceptedMutationIntentPatch, DecodedDataStoreKey, FieldSlot},
58 schema::{
59 AcceptedFieldKind, AcceptedRowLayoutRuntimeContract, AcceptedRowLayoutRuntimeField,
60 lower_field_type, output_value_from_runtime,
61 },
62 write_context::MutationMode,
63 },
64 error::{InternalError, MutationDiagnosticContext},
65 traits::CanisterKind,
66 types::{CurrentTimestamp, Timestamp},
67 value::InputValue,
68};
69use icydb_schema::{
70 EntitySourceKey, FieldSourceKey, FieldType, SchemaContractError, TypeSourceKey,
71};
72
73fn dynamic_key(
74 entity_tag: crate::types::EntityTag,
75 key: InputValue,
76) -> Result<DecodedDataStoreKey, InternalError> {
77 let value = key
78 .try_into_runtime_non_enum()
79 .ok_or_else(InternalError::executor_unsupported)?;
80 DecodedDataStoreKey::try_from_structural_key(entity_tag, &value)
81}
82
83fn lower_resolved_write_cell(
84 lowered: AcceptedMutationIntentPatch,
85 field: &AcceptedRowLayoutRuntimeField<'_>,
86 cell: DynamicWriteCell,
87 mode: MutationMode,
88 mutation_context: MutationDiagnosticContext,
89) -> Result<AcceptedMutationIntentPatch, InternalError> {
90 if (matches!(cell, DynamicWriteCell::Null | DynamicWriteCell::Value(_))
93 || (matches!(cell, DynamicWriteCell::Default) && !matches!(mode, MutationMode::Insert)))
94 && (field.write_policy().insert_generation().is_some()
95 || field.write_policy().write_management().is_some())
96 {
97 return Err(InternalError::mutation_database_owned_field_explicit(
98 mutation_context,
99 field.field_id().get(),
100 ));
101 }
102
103 let slot = FieldSlot::from_validated_index(usize::from(field.slot().get()));
104 Ok(match cell {
105 DynamicWriteCell::Omitted => lowered,
106 DynamicWriteCell::Default => match mode {
107 MutationMode::Insert | MutationMode::Replace => {
108 lowered.set_explicit_insert_default(slot)
109 }
110 MutationMode::Update => lowered.set_explicit_update_default(slot),
111 },
112 DynamicWriteCell::Null => lowered.set_authored(slot, InputValue::null()),
113 DynamicWriteCell::Value(value) => lowered.set_authored(slot, value),
114 })
115}
116
117fn lower_dynamic_patch(
118 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
119 patch: DynamicStructuralPatch,
120 mode: MutationMode,
121 mutation_context: MutationDiagnosticContext,
122) -> Result<AcceptedMutationIntentPatch, InternalError> {
123 let mut lowered = AcceptedMutationIntentPatch::new();
124 for (field_name, cell) in patch.into_fields() {
125 let slot = descriptor
126 .field_slot_index_by_name(&field_name)
127 .ok_or_else(InternalError::executor_unsupported)?;
128 let field = descriptor
129 .field_for_slot_index(slot)
130 .ok_or_else(InternalError::executor_invariant)?;
131 lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
132 }
133 Ok(lowered)
134}
135
136fn lower_dynamic_save_intent(
137 catalog: &AcceptedSchemaCatalogContext,
138 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
139 patch: DynamicStructuralPatch,
140 mode: MutationMode,
141 target: AcceptedStructuralMutationTarget,
142 batch_position: u32,
143) -> Result<AcceptedStructuralMutation, InternalError> {
144 Ok(AcceptedStructuralMutation::save(
145 mode,
146 target,
147 lower_dynamic_patch(
148 descriptor,
149 patch,
150 mode,
151 mutation_diagnostic_context(catalog, mode, batch_position),
152 )?,
153 ))
154}
155
156fn lower_dynamic_mutation_intent(
157 catalog: &AcceptedSchemaCatalogContext,
158 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
159 request: DynamicMutation,
160 batch_position: u32,
161) -> Result<AcceptedStructuralMutation, InternalError> {
162 let entity_tag = catalog.identity().entity_tag();
163 match request {
164 DynamicMutation::Insert { patch, .. } => lower_dynamic_save_intent(
165 catalog,
166 descriptor,
167 patch,
168 MutationMode::Insert,
169 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
170 batch_position,
171 ),
172 DynamicMutation::Update { key, patch, .. } => lower_dynamic_save_intent(
173 catalog,
174 descriptor,
175 patch,
176 MutationMode::Update,
177 AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
178 batch_position,
179 ),
180 DynamicMutation::Replace { key, patch, .. } => lower_dynamic_save_intent(
181 catalog,
182 descriptor,
183 patch,
184 MutationMode::Replace,
185 AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
186 batch_position,
187 ),
188 DynamicMutation::Delete { key, .. } => Ok(AcceptedStructuralMutation::delete(dynamic_key(
189 entity_tag, key,
190 )?)),
191 }
192}
193
194fn lower_typed_patch(
195 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
196 binding: &DynamicTypedEntityBinding,
197 patch: DynamicTypedStructuralPatch,
198 mode: MutationMode,
199 mutation_context: MutationDiagnosticContext,
200) -> Result<AcceptedMutationIntentPatch, InternalError> {
201 let mut lowered = AcceptedMutationIntentPatch::new();
202 for (descriptor_ordinal, cell) in patch.into_fields() {
203 let (field_id, slot) = binding
204 .field_identity_binding(descriptor_ordinal)
205 .ok_or_else(InternalError::store_invariant)?;
206 let slot_index = usize::from(slot);
207 let field = descriptor
208 .field_for_slot_index(slot_index)
209 .ok_or_else(InternalError::store_invariant)?;
210 if field.field_id().get() != field_id {
211 return Err(InternalError::store_invariant());
212 }
213 lowered = lower_resolved_write_cell(lowered, field, cell, mode, mutation_context)?;
214 }
215 Ok(lowered)
216}
217
218fn lower_typed_mutation_intent(
219 catalog: &AcceptedSchemaCatalogContext,
220 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
221 binding: &DynamicTypedEntityBinding,
222 request: DynamicTypedMutation,
223 batch_position: u32,
224) -> Result<Option<AcceptedStructuralMutation>, InternalError> {
225 let entity_tag = catalog.identity().entity_tag();
226 let (mode, target, patch) = match request {
227 DynamicTypedMutation::Insert { patch } => (
228 MutationMode::Insert,
229 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
230 patch,
231 ),
232 DynamicTypedMutation::Update { key, patch } => (
233 MutationMode::Update,
234 AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
235 patch,
236 ),
237 DynamicTypedMutation::Replace { key, patch } => (
238 MutationMode::Replace,
239 AcceptedStructuralMutationTarget::expected(dynamic_key(entity_tag, key)?),
240 patch,
241 ),
242 DynamicTypedMutation::Delete { key } => {
243 return Ok(Some(AcceptedStructuralMutation::delete(dynamic_key(
244 entity_tag, key,
245 )?)));
246 }
247 };
248 if !patch.is_bound_to(binding) {
249 return Ok(None);
250 }
251 let patch = lower_typed_patch(
252 descriptor,
253 binding,
254 patch,
255 mode,
256 mutation_diagnostic_context(catalog, mode, batch_position),
257 )?;
258 Ok(Some(AcceptedStructuralMutation::save(mode, target, patch)))
259}
260
261fn prepare_dynamic_mutation_result(
262 catalog: &AcceptedSchemaCatalogContext,
263 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
264 rows: Vec<AcceptedStructuralMutationRow>,
265 enforce_mixed_batch_result_bound: bool,
266) -> Result<DynamicMutationResult, InternalError> {
267 let affected_rows = rows.iter().try_fold(0_u32, |total, row| {
268 total
269 .checked_add(u32::from(row.logical_changed()))
270 .ok_or_else(InternalError::executor_invariant)
271 })?;
272 let columns = descriptor
273 .fields()
274 .iter()
275 .map(|field| field.name().to_string())
276 .collect();
277 let rows = rows
278 .into_iter()
279 .map(|row| {
280 row.values
281 .into_iter()
282 .map(|value| {
283 output_value_from_runtime(catalog.enum_catalog(), value)
284 .map_err(|_| InternalError::store_invariant())
285 })
286 .collect::<Result<Vec<_>, _>>()
287 })
288 .collect::<Result<Vec<_>, _>>()?;
289 let result = DynamicMutationResult {
290 entity: catalog.snapshot().entity_name().to_string(),
291 columns,
292 rows,
293 affected_rows,
294 };
295 if enforce_mixed_batch_result_bound {
296 let encoded_bytes = icydb_schema::wire::encoded_reply_len(&result)
297 .map_err(|_| InternalError::executor_invariant())?;
298 validate_structural_mutation_result_bytes(encoded_bytes)?;
299 }
300 Ok(result)
301}
302
303fn typed_descriptor_field_type(
304 field_type: TypedFieldType,
305) -> Result<FieldType, SchemaContractError> {
306 match field_type {
307 TypedFieldType::Scalar(scalar) => Ok(FieldType::Scalar(scalar)),
308 TypedFieldType::List(item) => Ok(FieldType::List(Box::new(typed_descriptor_field_type(
309 *item,
310 )?))),
311 TypedFieldType::Named(source_key) => {
312 TypeSourceKey::try_new(source_key.to_string()).map(FieldType::Named)
313 }
314 }
315}
316
317fn typed_adapter_field_kind_matches(
318 accepted: &AcceptedFieldKind,
319 expected: &AcceptedFieldKind,
320) -> bool {
321 if accepted == expected {
322 return true;
323 }
324 match (accepted, expected) {
325 (AcceptedFieldKind::Relation { key_kind, .. }, expected) => {
326 typed_adapter_field_kind_matches(key_kind, expected)
327 }
328 (AcceptedFieldKind::List(accepted), AcceptedFieldKind::List(expected)) => {
329 typed_adapter_field_kind_matches(accepted, expected)
330 }
331 _ => false,
332 }
333}
334
335impl<C: CanisterKind> DbSession<C> {
336 pub fn issue_typed_entity_binding(
338 &self,
339 descriptor: &TypedEntityDescriptor,
340 ) -> Result<DynamicTypedEntityBinding, DynamicTypedBindingError> {
341 let unavailable = |field_source| {
342 DynamicTypedBindingError::source_unavailable(descriptor.entity_source_key, field_source)
343 };
344 let entity_source = EntitySourceKey::try_new(descriptor.entity_source_key)
345 .map_err(|_| unavailable(None))?;
346 let catalog = self
347 .find_accepted_schema_catalog_context_for_entity_source_key(entity_source.as_str())?
348 .ok_or_else(|| unavailable(None))?;
349 let identity = catalog.identity();
350 if identity.entity_path() != entity_source.as_str() {
351 return Err(InternalError::store_invariant().into());
352 }
353 let store = self.db.recovered_store(identity.store_path())?;
354 store.with_schema(|schema| {
358 let bundle = schema
359 .borrow_accepted_schema_bundle_for_authority(
360 catalog.value_catalog_handle().authority(),
361 )?
362 .ok_or_else(InternalError::store_invariant)?;
363 let entity_tag = identity.entity_tag();
364 if bundle.source_bindings().entity(&entity_source) != Some(entity_tag)
365 || bundle.revision() != catalog.revision()
366 {
367 return Err(InternalError::store_invariant().into());
368 }
369 let snapshot = bundle
370 .entity_snapshots()
371 .get(&entity_tag)
372 .ok_or_else(InternalError::store_invariant)?;
373 if descriptor.primary_key_source_keys.len() != snapshot.primary_key_field_ids().len() {
374 return Err(DynamicTypedBindingError::IncompatibleField);
375 }
376 for (source_key, accepted_field_id) in descriptor
377 .primary_key_source_keys
378 .iter()
379 .zip(snapshot.primary_key_field_ids())
380 {
381 let source = FieldSourceKey::try_new((*source_key).to_string())
382 .map_err(|_| unavailable(Some(*source_key)))?;
383 let descriptor_field_id = bundle
384 .source_bindings()
385 .field(entity_tag, &source)
386 .ok_or_else(|| unavailable(Some(*source_key)))?;
387 if descriptor_field_id != *accepted_field_id {
388 return Err(DynamicTypedBindingError::IncompatibleField);
389 }
390 }
391 let row_contract = catalog.inspection_plan().row_contract();
392 let mut fields = Vec::with_capacity(descriptor.fields.len());
393 for field_descriptor in descriptor.fields {
394 let source = FieldSourceKey::try_new(field_descriptor.source_key.to_string())
395 .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
396 let field_id = bundle
397 .source_bindings()
398 .field(entity_tag, &source)
399 .ok_or_else(|| unavailable(Some(field_descriptor.source_key)))?;
400 let field = snapshot
401 .fields()
402 .iter()
403 .find(|field| field.id() == field_id)
404 .ok_or_else(InternalError::store_invariant)?;
405 let runtime_field = row_contract
406 .required_accepted_field_contract(usize::from(field.slot().get()))?;
407 if runtime_field.field_id() != field_id {
408 return Err(InternalError::store_invariant().into());
409 }
410 let field_type = typed_descriptor_field_type(field_descriptor.field_type)
411 .map_err(|_| unavailable(Some(field_descriptor.source_key)))?;
412 let expected_kind = lower_field_type(&field_type, bundle.source_bindings())
413 .map_err(|_| DynamicTypedBindingError::IncompatibleField)?;
414 if field.nullable() != field_descriptor.nullable
415 || !typed_adapter_field_kind_matches(field.kind(), &expected_kind)
416 {
417 return Err(DynamicTypedBindingError::IncompatibleField);
418 }
419 fields.push((
420 source.as_str().to_string(),
421 field_id.get(),
422 field.slot().get(),
423 field.name().to_string(),
424 ));
425 }
426 let adapter_names = bundle.typed_adapter_names()?;
427
428 DynamicTypedEntityBinding::new(
429 database_incarnation_id()?.to_bytes(),
430 entity_source.as_str().to_string(),
431 snapshot.entity_name().to_string(),
432 entity_tag.value(),
433 catalog.revision().get(),
434 catalog.fingerprint(),
435 row_contract.current_layout_version().get(),
436 fields,
437 adapter_names.named_types,
438 adapter_names.enum_variants,
439 adapter_names.composite_fields,
440 )
441 .map_err(Into::into)
442 })
443 }
444
445 pub(in crate::db::session) fn current_typed_entity_binding_catalog(
446 &self,
447 binding: &DynamicTypedEntityBinding,
448 ) -> Result<Option<AcceptedSchemaCatalogContext>, InternalError> {
449 self.db.ensure_recovered_state()?;
453 let incarnation = database_incarnation_id()?.to_bytes();
454 if incarnation != binding.database_incarnation {
455 return Ok(None);
456 }
457 let Some(catalog) = self.find_accepted_schema_catalog_context_for_entity_source_key(
458 binding.entity_source.as_str(),
459 )?
460 else {
461 return Ok(None);
462 };
463 self.typed_entity_binding_matches_catalog(binding, &catalog, incarnation)
464 .map(|current| current.then_some(catalog))
465 }
466
467 fn typed_entity_binding_matches_catalog(
468 &self,
469 binding: &DynamicTypedEntityBinding,
470 catalog: &AcceptedSchemaCatalogContext,
471 incarnation: [u8; 16],
472 ) -> Result<bool, InternalError> {
473 if incarnation != binding.database_incarnation {
474 return Ok(false);
475 }
476 let row_contract = catalog.inspection_plan().row_contract();
477 let identity = catalog.identity();
478 if identity.entity_path() != binding.entity_source.as_str()
479 || identity.entity_tag().value() != binding.entity_tag
480 || catalog.revision().get() != binding.accepted_revision
481 || catalog.fingerprint() != binding.accepted_fingerprint
482 || row_contract.current_layout_version().get() != binding.entity_generation
483 {
484 return Ok(false);
485 }
486 let entity_source = EntitySourceKey::try_new(binding.entity_source.clone())
487 .map_err(|_| InternalError::store_invariant())?;
488 let store = self.db.recovered_store(identity.store_path())?;
489 store.with_schema(|schema| {
492 let bundle = schema
493 .borrow_accepted_schema_bundle_for_authority(
494 catalog.value_catalog_handle().authority(),
495 )?
496 .ok_or_else(InternalError::store_invariant)?;
497 if bundle.revision() != catalog.revision()
498 || bundle.source_bindings().entity(&entity_source) != Some(identity.entity_tag())
499 {
500 return Ok(false);
501 }
502 let snapshot = bundle
503 .entity_snapshots()
504 .get(&identity.entity_tag())
505 .ok_or_else(InternalError::store_invariant)?;
506 for (source_key, expected_field_id, expected_slot) in binding.field_identity_bindings()
507 {
508 let source = FieldSourceKey::try_new(source_key)
509 .map_err(|_| InternalError::store_invariant())?;
510 let Some(field_id) = bundle
511 .source_bindings()
512 .field(identity.entity_tag(), &source)
513 else {
514 return Ok(false);
515 };
516 let Some(field) = snapshot
517 .fields()
518 .iter()
519 .find(|field| field.id() == field_id)
520 else {
521 return Err(InternalError::store_invariant());
522 };
523 if field_id.get() != expected_field_id || field.slot().get() != expected_slot {
524 return Ok(false);
525 }
526 }
527
528 Ok(true)
529 })
530 }
531
532 pub fn typed_entity_binding_is_current(
534 &self,
535 binding: &DynamicTypedEntityBinding,
536 ) -> Result<bool, InternalError> {
537 self.current_typed_entity_binding_catalog(binding)
538 .map(|catalog| catalog.is_some())
539 }
540
541 #[cfg(feature = "sql")]
544 pub(in crate::db::session) fn execute_accepted_structural_delete_batch(
545 &self,
546 catalog: &AcceptedSchemaCatalogContext,
547 capture_output_values: bool,
548 keys: Vec<DecodedDataStoreKey>,
549 precommit_validation: impl FnOnce(&[Vec<Value>]) -> Result<(), InternalError>,
550 ) -> Result<Vec<Vec<Value>>, InternalError> {
551 let mutations = keys
552 .into_iter()
553 .map(AcceptedStructuralMutation::delete)
554 .collect::<Vec<_>>();
555 let mutation_capacity = mutations.len();
556 let mut mutations = mutations.into_iter();
557 execute_accepted_structural_mutation_batch(
558 &self.db,
559 catalog,
560 mutation_capacity,
561 0,
562 || {
563 Ok(mutations
564 .next()
565 .map(|mutation| AcceptedStructuralMutationBatchItem {
566 catalog: catalog.clone(),
567 mutation,
568 }))
569 },
570 Timestamp::now(),
571 AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
572 |rows, _report| {
573 let rows = rows
574 .into_iter()
575 .map(AcceptedStructuralMutationRow::into_values)
576 .collect::<Vec<_>>();
577 precommit_validation(rows.as_slice())?;
578 Ok((rows, AcceptedStructuralMutationCommitDirective::Standard))
579 },
580 )
581 }
582
583 pub(in crate::db::session) fn execute_accepted_structural_save_batch<T>(
592 &self,
593 catalog: &AcceptedSchemaCatalogContext,
594 capture_output_values: bool,
595 mutations: Vec<AcceptedStructuralMutation>,
596 operation_timestamp: Timestamp,
597 precommit_preparation: impl FnOnce(
598 Vec<AcceptedStructuralMutationRow>,
599 ) -> Result<T, InternalError>,
600 ) -> Result<T, InternalError> {
601 let mutation_capacity = mutations.len();
602 let identity_candidate_count = mutations
603 .iter()
604 .filter(|mutation| {
605 matches!(
606 mutation,
607 AcceptedStructuralMutation::Save {
608 mode: MutationMode::Insert,
609 target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
610 ..
611 }
612 )
613 })
614 .count();
615 let mut mutations = mutations.into_iter();
616 execute_accepted_structural_mutation_batch(
617 &self.db,
618 catalog,
619 mutation_capacity,
620 identity_candidate_count,
621 || {
622 Ok(mutations
623 .next()
624 .map(|mutation| AcceptedStructuralMutationBatchItem {
625 catalog: catalog.clone(),
626 mutation,
627 }))
628 },
629 operation_timestamp,
630 AcceptedStructuralMutationCommitOptions::standard(capture_output_values),
631 |rows, _report| {
632 precommit_preparation(rows).map(|prepared| {
633 (
634 prepared,
635 AcceptedStructuralMutationCommitDirective::Standard,
636 )
637 })
638 },
639 )
640 }
641
642 #[cfg(test)]
644 pub(in crate::db::session) fn execute_accepted_structural_update_with_mutation_progress(
645 &self,
646 catalog: &AcceptedSchemaCatalogContext,
647 mutations: Vec<AcceptedStructuralMutation>,
648 operation_timestamp: Timestamp,
649 mutation_progress: MutationProgressRecordOp,
650 ) -> Result<usize, InternalError> {
651 let mutation_capacity = mutations.len();
652 let mut mutations = mutations.into_iter();
653 execute_accepted_structural_mutation_batch(
654 &self.db,
655 catalog,
656 mutation_capacity,
657 0,
658 || {
659 Ok(mutations
660 .next()
661 .map(|mutation| AcceptedStructuralMutationBatchItem {
662 catalog: catalog.clone(),
663 mutation,
664 }))
665 },
666 operation_timestamp,
667 AcceptedStructuralMutationCommitOptions::with_mutation_progress(),
668 |rows, _report| {
669 Ok((
670 rows.len(),
671 AcceptedStructuralMutationCommitDirective::WithMutationProgress(
672 mutation_progress,
673 ),
674 ))
675 },
676 )
677 }
678
679 fn execute_lowered_dynamic_mutation_batch(
680 &self,
681 catalog: &AcceptedSchemaCatalogContext,
682 descriptor: &AcceptedRowLayoutRuntimeContract<'_>,
683 mutations: Vec<AcceptedStructuralMutation>,
684 enforce_mixed_batch_result_bound: bool,
685 ) -> Result<DynamicMutationResult, InternalError> {
686 self.execute_accepted_structural_save_batch(
687 catalog,
688 true,
689 mutations,
690 Timestamp::now(),
691 |rows| {
692 prepare_dynamic_mutation_result(
693 catalog,
694 descriptor,
695 rows,
696 enforce_mixed_batch_result_bound,
697 )
698 },
699 )
700 }
701
702 pub fn execute_trusted_dynamic_mutation(
709 &self,
710 request: &DynamicMutation,
711 ) -> Result<DynamicMutationResult, InternalError> {
712 if request.entity().is_empty() {
713 return Err(InternalError::executor_unsupported());
714 }
715 let catalog =
716 self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
717 let descriptor =
718 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
719 let mutation = lower_dynamic_mutation_intent(&catalog, &descriptor, request.clone(), 0)?;
720
721 self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
722 }
723
724 pub fn execute_trusted_dynamic_mutation_batch(
730 &self,
731 requests: Vec<DynamicMutation>,
732 ) -> Result<Vec<DynamicMutationResult>, InternalError> {
733 self.execute_trusted_dynamic_mutation_batch_mixed(requests)
734 }
735
736 fn execute_trusted_dynamic_mutation_batch_mixed(
737 &self,
738 requests: Vec<DynamicMutation>,
739 ) -> Result<Vec<DynamicMutationResult>, InternalError> {
740 if requests.is_empty() {
741 return Err(InternalError::mutation_batch_empty());
742 }
743 if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
744 return Err(InternalError::mutation_batch_too_many_items(
745 requests.len(),
746 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
747 ));
748 }
749 let first = requests
750 .first()
751 .ok_or_else(InternalError::mutation_batch_empty)?;
752 if first.entity().is_empty() {
753 return Err(InternalError::executor_unsupported());
754 }
755 let anchor_catalog =
756 self.accepted_schema_catalog_context_for_entity_name(Some(first.entity()))?;
757 let anchor_identity = anchor_catalog.identity();
758 let mut entity_tags = std::collections::BTreeSet::new();
759 let mut items = Vec::with_capacity(requests.len());
760 let mut result_catalogs = Vec::with_capacity(requests.len());
761 let mut identity_candidate_count = 0_usize;
762
763 let request_count = requests.len();
764 for (batch_position, request) in requests.into_iter().enumerate() {
765 let batch_position = u32::try_from(batch_position).map_err(|_| {
766 InternalError::mutation_batch_too_many_items(
767 request_count,
768 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
769 )
770 })?;
771 if request.entity().is_empty() {
772 return Err(InternalError::executor_unsupported());
773 }
774 let item_catalog = anchor_catalog
775 .for_entity_name(request.entity())
776 .ok_or_else(|| InternalError::unsupported_entity_path(request.entity()))?;
777 let item_identity = item_catalog.identity();
778 if item_identity.store_path() != anchor_identity.store_path() {
779 return Err(InternalError::mutation_batch_store_mismatch(
780 batch_position,
781 anchor_identity.entity_tag().value(),
782 item_identity.entity_tag().value(),
783 ));
784 }
785 entity_tags.insert(item_identity.entity_tag());
786 if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
787 return Err(InternalError::mutation_batch_too_many_entities(
788 entity_tags.len(),
789 MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
790 ));
791 }
792 let descriptor =
793 AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
794 let mutation =
795 lower_dynamic_mutation_intent(&item_catalog, &descriptor, request, batch_position)?;
796 if matches!(
797 mutation,
798 AcceptedStructuralMutation::Save {
799 mode: MutationMode::Insert,
800 target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
801 ..
802 }
803 ) {
804 identity_candidate_count = identity_candidate_count.saturating_add(1);
805 }
806 result_catalogs.push(item_catalog.clone());
807 items.push(AcceptedStructuralMutationBatchItem {
808 catalog: item_catalog,
809 mutation,
810 });
811 }
812
813 self.execute_lowered_mixed_mutation_batch(
814 &anchor_catalog,
815 items,
816 result_catalogs,
817 identity_candidate_count,
818 )
819 }
820
821 fn execute_lowered_mixed_mutation_batch(
822 &self,
823 anchor_catalog: &AcceptedSchemaCatalogContext,
824 items: Vec<AcceptedStructuralMutationBatchItem>,
825 result_catalogs: Vec<AcceptedSchemaCatalogContext>,
826 identity_candidate_count: usize,
827 ) -> Result<Vec<DynamicMutationResult>, InternalError> {
828 if items.len() != result_catalogs.len() {
829 return Err(InternalError::executor_invariant());
830 }
831 let mutation_count = items.len();
832 let mut items = items.into_iter();
833 execute_accepted_structural_mutation_batch(
834 &self.db,
835 anchor_catalog,
836 mutation_count,
837 identity_candidate_count,
838 || Ok(items.next()),
839 Timestamp::now(),
840 AcceptedStructuralMutationCommitOptions::standard(true),
841 |rows, _report| {
842 if rows.len() != result_catalogs.len() {
843 return Err(InternalError::executor_invariant());
844 }
845 let mut results = Vec::with_capacity(rows.len());
846 for (row, catalog) in rows.into_iter().zip(result_catalogs.iter()) {
847 let descriptor =
848 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
849 results.push(prepare_dynamic_mutation_result(
850 catalog,
851 &descriptor,
852 vec![row],
853 false,
854 )?);
855 }
856 let encoded_bytes = icydb_schema::wire::encoded_reply_len(&results)
857 .map_err(|_| InternalError::executor_invariant())?;
858 validate_structural_mutation_result_bytes(encoded_bytes)?;
859 Ok((results, AcceptedStructuralMutationCommitDirective::Standard))
860 },
861 )
862 }
863
864 #[doc(hidden)]
867 pub fn execute_trusted_typed_mutation(
868 &self,
869 binding: &DynamicTypedEntityBinding,
870 request: DynamicTypedMutation,
871 ) -> Result<Option<DynamicMutationResult>, InternalError> {
872 let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
873 return Ok(None);
874 };
875 let descriptor =
876 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
877 let Some(mutation) =
878 lower_typed_mutation_intent(&catalog, &descriptor, binding, request, 0)?
879 else {
880 return Ok(None);
881 };
882 self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, vec![mutation], false)
883 .map(Some)
884 }
885
886 #[doc(hidden)]
890 pub fn execute_trusted_same_entity_typed_mutation_batch(
891 &self,
892 binding: &DynamicTypedEntityBinding,
893 requests: Vec<DynamicTypedMutation>,
894 ) -> Result<Option<DynamicMutationResult>, InternalError> {
895 if requests.is_empty() {
896 return Err(InternalError::mutation_batch_empty());
897 }
898 if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
899 return Err(InternalError::mutation_batch_too_many_items(
900 requests.len(),
901 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
902 ));
903 }
904 let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
905 return Ok(None);
906 };
907 let descriptor =
908 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
909 let mut mutations = Vec::with_capacity(requests.len());
910 let request_count = requests.len();
911 for (batch_position, request) in requests.into_iter().enumerate() {
912 let batch_position = u32::try_from(batch_position).map_err(|_| {
913 InternalError::mutation_batch_too_many_items(
914 request_count,
915 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
916 )
917 })?;
918 let Some(mutation) = lower_typed_mutation_intent(
919 &catalog,
920 &descriptor,
921 binding,
922 request,
923 batch_position,
924 )?
925 else {
926 return Ok(None);
927 };
928 mutations.push(mutation);
929 }
930
931 self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, true)
932 .map(Some)
933 }
934
935 #[doc(hidden)]
939 pub fn execute_trusted_typed_mutation_batch(
940 &self,
941 requests: Vec<(DynamicTypedEntityBinding, DynamicTypedMutation)>,
942 ) -> Result<Option<Vec<DynamicMutationResult>>, InternalError> {
943 if requests.is_empty() {
944 return Err(InternalError::mutation_batch_empty());
945 }
946 if requests.len() > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
947 return Err(InternalError::mutation_batch_too_many_items(
948 requests.len(),
949 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
950 ));
951 }
952 let first_binding = requests
953 .first()
954 .map(|(binding, _)| binding)
955 .ok_or_else(InternalError::mutation_batch_empty)?;
956 let Some(catalog) = self.current_typed_entity_binding_catalog(first_binding)? else {
957 return Ok(None);
958 };
959 let anchor_identity = catalog.identity();
960 let mut entity_tags = std::collections::BTreeSet::new();
961 let mut items = Vec::with_capacity(requests.len());
962 let mut result_catalogs = Vec::with_capacity(requests.len());
963 let mut identity_candidate_count = 0_usize;
964
965 let request_count = requests.len();
966 for (batch_position, (binding, request)) in requests.into_iter().enumerate() {
967 let batch_position = u32::try_from(batch_position).map_err(|_| {
968 InternalError::mutation_batch_too_many_items(
969 request_count,
970 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
971 )
972 })?;
973 let Some(item_catalog) = catalog.for_entity_path(binding.entity_source.as_str()) else {
974 return Ok(None);
975 };
976 if !self.typed_entity_binding_matches_catalog(
977 &binding,
978 &item_catalog,
979 database_incarnation_id()?.to_bytes(),
980 )? {
981 return Ok(None);
982 }
983 let item_identity = item_catalog.identity();
984 if item_identity.store_path() != anchor_identity.store_path() {
985 return Err(InternalError::mutation_batch_store_mismatch(
986 batch_position,
987 anchor_identity.entity_tag().value(),
988 item_identity.entity_tag().value(),
989 ));
990 }
991 entity_tags.insert(item_identity.entity_tag());
992 if entity_tags.len() > MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES {
993 return Err(InternalError::mutation_batch_too_many_entities(
994 entity_tags.len(),
995 MAX_STRUCTURAL_MUTATION_BATCH_ENTITIES,
996 ));
997 }
998 let descriptor =
999 AcceptedRowLayoutRuntimeContract::from_accepted_schema(item_catalog.snapshot())?;
1000 let Some(mutation) = lower_typed_mutation_intent(
1001 &item_catalog,
1002 &descriptor,
1003 &binding,
1004 request,
1005 batch_position,
1006 )?
1007 else {
1008 return Ok(None);
1009 };
1010 if matches!(
1011 mutation,
1012 AcceptedStructuralMutation::Save {
1013 mode: MutationMode::Insert,
1014 target: AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1015 ..
1016 }
1017 ) {
1018 identity_candidate_count = identity_candidate_count.saturating_add(1);
1019 }
1020 result_catalogs.push(item_catalog.clone());
1021 items.push(AcceptedStructuralMutationBatchItem {
1022 catalog: item_catalog,
1023 mutation,
1024 });
1025 }
1026
1027 self.execute_lowered_mixed_mutation_batch(
1028 &catalog,
1029 items,
1030 result_catalogs,
1031 identity_candidate_count,
1032 )
1033 .map(Some)
1034 }
1035
1036 pub fn execute_trusted_dynamic_insert_batch(
1042 &self,
1043 entity: &str,
1044 patches: Vec<DynamicStructuralPatch>,
1045 ) -> Result<DynamicMutationResult, InternalError> {
1046 let patch_count = patches.len();
1047 if patch_count == 0 {
1048 return Err(InternalError::mutation_batch_empty());
1049 }
1050 if patch_count > MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS {
1051 return Err(InternalError::mutation_batch_too_many_items(
1052 patch_count,
1053 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1054 ));
1055 }
1056 if entity.is_empty() {
1057 return Err(InternalError::executor_unsupported());
1058 }
1059 let catalog = self.accepted_schema_catalog_context_for_entity_name(Some(entity))?;
1060 let descriptor =
1061 AcceptedRowLayoutRuntimeContract::from_accepted_schema(catalog.snapshot())?;
1062 let mut mutations = Vec::with_capacity(patch_count);
1063 for (batch_position, patch) in patches.into_iter().enumerate() {
1066 let batch_position = u32::try_from(batch_position).map_err(|_| {
1067 InternalError::mutation_batch_too_many_items(
1068 patch_count,
1069 MAX_STRUCTURAL_MUTATION_BATCH_OPERATIONS,
1070 )
1071 })?;
1072 mutations.push(lower_dynamic_save_intent(
1073 &catalog,
1074 &descriptor,
1075 patch,
1076 MutationMode::Insert,
1077 AcceptedStructuralMutationTarget::ResolveFromAfterImage,
1078 batch_position,
1079 )?);
1080 }
1081
1082 self.execute_lowered_dynamic_mutation_batch(&catalog, &descriptor, mutations, false)
1083 }
1084}