1use pharos_runtime_architect::database::ArchitectStore;
4use pharos_runtime_database::DbBackendConfig;
5use pharos_runtime_database::operations::{
6 AppSchema, DatabaseError, DatabaseOperations, FieldDefault, FieldType, Mutation, Predicate,
7 Query, ReservationExpiry, SortKey, Transaction, WriteValue,
8};
9use serde_json::{Map, Value};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[cfg(not(target_os = "wasi"))]
13use super::populate_generated_values;
14use super::{
15 FeatureRequestContext, FeatureRuntimeError, query_binding_value, resolve_value,
16 transaction_operations,
17};
18use crate::config::AppRuntime;
19
20enum LogicalStore {
22 Architect(ArchitectStore),
23 App(crate::runtime::database::LogicalAppStore),
24}
25
26impl LogicalStore {
27 fn config(&self) -> &DbBackendConfig {
28 match self {
29 Self::Architect(store) => store.config(),
30 Self::App(store) => &store.config,
31 }
32 }
33
34 fn schema(&self) -> &AppSchema {
35 match self {
36 Self::Architect(store) => store.schema(),
37 Self::App(store) => &store.schema,
38 }
39 }
40
41 fn adapter(&self) -> &dyn DatabaseOperations {
42 match self {
43 Self::Architect(store) => store.adapter(),
44 Self::App(store) => store.adapter.as_ref(),
45 }
46 }
47}
48
49fn store_for_app(app: &AppRuntime) -> Result<LogicalStore, FeatureRuntimeError> {
51 let app_id = app
52 .app_id
53 .as_deref()
54 .ok_or_else(|| FeatureRuntimeError::InvalidContract("app id".to_owned()))?;
55 let store =
56 crate::runtime::database::runtime_logical_app_store(app).map_err(map_database_error)?;
57 if app_id == "architect" {
58 return ArchitectStore::new(store.config, store.adapter, store.schema)
59 .map(LogicalStore::Architect)
60 .map_err(map_database_error);
61 }
62 if app.is_editor_preview && ArchitectStore::supports_preview_schema(&store.schema) {
63 return ArchitectStore::new_isolated_preview(store.config, store.adapter, store.schema)
64 .map(LogicalStore::Architect)
65 .map_err(map_database_error);
66 }
67 Ok(LogicalStore::App(store))
68}
69
70pub(crate) fn map_database_error(error: DatabaseError) -> FeatureRuntimeError {
72 match error {
73 DatabaseError::MissingRecord => {
74 FeatureRuntimeError::MissingRecord("database record".to_owned())
75 }
76 DatabaseError::Conflict => FeatureRuntimeError::Conflict("database conflict".to_owned()),
77 DatabaseError::InvalidInput => {
78 FeatureRuntimeError::InvalidInput("database operation".to_owned())
79 }
80 _ => FeatureRuntimeError::Database(error),
81 }
82}
83
84fn field_value(
86 schema: &AppSchema,
87 entity: &str,
88 field: &str,
89 value: Value,
90) -> Result<Value, FeatureRuntimeError> {
91 let Some(declaration) = schema
92 .entities
93 .iter()
94 .find(|entry| entry.name == entity)
95 .and_then(|entry| entry.fields.iter().find(|entry| entry.name == field))
96 else {
97 return Ok(value);
98 };
99 if declaration.kind == FieldType::Integer
100 && let Value::String(text) = &value
101 {
102 if declaration.nullable && text.trim().is_empty() {
103 return Ok(Value::Null);
104 }
105 return text
106 .parse::<i64>()
107 .map(Value::from)
108 .map_err(|_| FeatureRuntimeError::InvalidInput(field.to_owned()));
109 }
110 if declaration.kind != FieldType::Boolean || value.is_null() || value.is_boolean() {
111 return Ok(value);
112 }
113 match value {
114 Value::Number(number) if number.as_i64() == Some(0) => Ok(Value::Bool(false)),
115 Value::Number(number) if number.as_i64() == Some(1) => Ok(Value::Bool(true)),
116 Value::String(text) if text == "0" => Ok(Value::Bool(false)),
117 Value::String(text) if text == "1" => Ok(Value::Bool(true)),
118 _ => Err(FeatureRuntimeError::InvalidInput(field.to_owned())),
119 }
120}
121
122pub(super) fn predicate_for_filters(
124 filters: Option<&Map<String, Value>>,
125 context: &FeatureRequestContext,
126 schema: &AppSchema,
127 entity: &str,
128) -> Result<Option<Predicate>, FeatureRuntimeError> {
129 let mut parts = Vec::new();
130 for (field, declaration) in filters.into_iter().flat_map(|values| values.iter()) {
131 let operator = declaration
132 .get("operator")
133 .and_then(Value::as_str)
134 .unwrap_or("eq");
135 if operator == "is_null" {
136 let predicate = Predicate::IsNull(field.clone());
137 let declaration = pharos_runtime_database::operations::entity(schema, entity)
138 .map_err(map_database_error)?;
139 pharos_runtime_database::operations::validate_predicate(declaration, &predicate)
140 .map_err(map_database_error)?;
141 parts.push(predicate);
142 continue;
143 }
144 let source = declaration.get("value").unwrap_or(declaration);
145 let optional = declaration
146 .get("optional")
147 .and_then(Value::as_bool)
148 .unwrap_or(false);
149 let Some(value) = resolve_value(source, context)? else {
150 if optional {
151 continue;
152 }
153 return Err(FeatureRuntimeError::InvalidInput(field.clone()));
154 };
155 if optional && value.as_str() == Some("") {
156 continue;
157 }
158 if operator == "in" {
159 let values = value
160 .as_array()
161 .filter(|values| values.len() <= 64)
162 .ok_or_else(|| FeatureRuntimeError::InvalidInput(field.clone()))?;
163 let values = values
164 .iter()
165 .map(|value| field_value(schema, entity, field, value.clone()))
166 .collect::<Result<Vec<_>, _>>()?;
167 let predicate = Predicate::In(field.clone(), values);
168 let declaration = pharos_runtime_database::operations::entity(schema, entity)
169 .map_err(map_database_error)?;
170 pharos_runtime_database::operations::validate_predicate(declaration, &predicate)
171 .map_err(map_database_error)?;
172 parts.push(predicate);
173 continue;
174 }
175 let value = field_value(schema, entity, field, value)?;
176 let part = match operator {
177 "eq" => Predicate::Equal(field.clone(), value),
178 "ne" => Predicate::NotEqual(field.clone(), value),
179 "gt" => Predicate::GreaterThan(field.clone(), value),
180 "gte" => Predicate::AtLeast(field.clone(), value),
181 "lt" => Predicate::LessThan(field.clone(), value),
182 "lte" => Predicate::AtMost(field.clone(), value),
183 "contains" => Predicate::Contains(
184 field.clone(),
185 value
186 .as_str()
187 .ok_or_else(|| FeatureRuntimeError::InvalidInput(field.clone()))?
188 .to_owned(),
189 ),
190 _ => {
191 return Err(FeatureRuntimeError::InvalidContract(format!(
192 "{field}.{operator}"
193 )));
194 }
195 };
196 parts.push(part);
197 }
198 Ok(match parts.len() {
199 0 => None,
200 1 => parts.pop(),
201 _ => Some(Predicate::All(parts)),
202 })
203}
204
205fn predicate_for_transaction_filters(
207 filters: &Map<String, Value>,
208 context: &FeatureRequestContext,
209 bindings: &BTreeMap<String, usize>,
210 schema: &AppSchema,
211 entity: &str,
212) -> Result<Predicate, FeatureRuntimeError> {
213 if filters.is_empty() || filters.len() > 32 {
214 return Err(FeatureRuntimeError::InvalidContract(
215 "transaction filters".to_owned(),
216 ));
217 }
218 let mut parts = Vec::with_capacity(filters.len());
219 for (field, declaration) in filters {
220 let source = declaration.get("value").unwrap_or(declaration);
221 let generated = source
222 .as_str()
223 .and_then(|text| text.strip_prefix("query."))
224 .and_then(|path| path.strip_suffix(".id"))
225 .and_then(|name| bindings.get(name));
226 if let Some(step) = generated {
227 if declaration
228 .get("operator")
229 .and_then(Value::as_str)
230 .is_some_and(|operator| operator != "eq")
231 {
232 return Err(FeatureRuntimeError::InvalidContract(
233 "generated identity filter operator".to_owned(),
234 ));
235 }
236 parts.push(Predicate::GeneratedEqual(field.clone(), *step));
237 continue;
238 }
239 let single = Map::from_iter([(field.clone(), declaration.clone())]);
240 let part = predicate_for_filters(Some(&single), context, schema, entity)?
241 .ok_or_else(|| FeatureRuntimeError::InvalidContract("transaction filter".to_owned()))?;
242 parts.push(part);
243 }
244 Ok(if parts.len() == 1 {
245 parts.remove(0)
246 } else {
247 Predicate::All(parts)
248 })
249}
250
251fn working_query_rows(
254 context: &FeatureRequestContext,
255 query_id: &str,
256) -> Result<Option<Vec<Map<String, Value>>>, FeatureRuntimeError> {
257 let Some(binding) = context.bindings.get(query_id) else {
258 return Ok(None);
259 };
260 let rows = match binding {
261 Value::Object(row) => vec![row.clone()],
262 Value::Array(rows) if rows.len() <= 100 => rows
263 .iter()
264 .map(|row| {
265 row.as_object().cloned().ok_or_else(|| {
266 FeatureRuntimeError::InvalidContract("working query row".to_owned())
267 })
268 })
269 .collect::<Result<Vec<_>, _>>()?,
270 Value::Null => Vec::new(),
271 _ => {
272 return Err(FeatureRuntimeError::InvalidContract(
273 "working query binding".to_owned(),
274 ));
275 }
276 };
277 let bytes = serde_json::to_vec(&rows)
278 .map_err(|_| FeatureRuntimeError::InvalidContract("working query encoding".to_owned()))?;
279 if bytes.len() > 8 * 1024 * 1024 {
280 return Err(FeatureRuntimeError::Database(
281 DatabaseError::CapacityExceeded,
282 ));
283 }
284 Ok(Some(rows))
285}
286
287pub(super) fn execute_logical_queries(
289 app: &AppRuntime,
290 feature: &Value,
291 query_ids: &[String],
292 context: &FeatureRequestContext,
293) -> Result<BTreeMap<String, Vec<Map<String, Value>>>, FeatureRuntimeError> {
294 if query_ids.len() > 32 {
295 return Err(FeatureRuntimeError::InvalidContract(
296 "query count".to_owned(),
297 ));
298 }
299 let store = store_for_app(app)?;
300 let declarations = feature
301 .get("queries")
302 .and_then(Value::as_object)
303 .ok_or_else(|| FeatureRuntimeError::InvalidContract("feature.queries".to_owned()))?;
304 let mut output = BTreeMap::new();
305 let mut scoped = context.clone();
306 let mut owned_tasks = None;
307 for query_id in query_ids {
308 let declaration = declarations
309 .get(query_id)
310 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.clone()))?;
311 if let Some(rows) = working_query_rows(&scoped, query_id)? {
312 output.insert(query_id.clone(), rows);
313 continue;
314 }
315 let entity = declaration
316 .get("entity")
317 .and_then(Value::as_str)
318 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{query_id}.entity")))?;
319 let fields = declaration
320 .get("fields")
321 .and_then(Value::as_array)
322 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{query_id}.fields")))?
323 .iter()
324 .map(|value| {
325 value.as_str().map(str::to_owned).ok_or_else(|| {
326 FeatureRuntimeError::InvalidContract(format!("{query_id}.fields"))
327 })
328 })
329 .collect::<Result<Vec<_>, _>>()?;
330 if fields.is_empty() || fields.len() > 64 {
331 return Err(FeatureRuntimeError::InvalidContract(query_id.clone()));
332 }
333 let limit = declaration.get("limit").and_then(Value::as_u64).unwrap_or(
334 if declaration.get("result").and_then(Value::as_str) == Some("many") {
335 24
336 } else {
337 1
338 },
339 );
340 if limit == 0 || limit > 1000 {
341 return Err(FeatureRuntimeError::InvalidContract(format!(
342 "{query_id}.limit"
343 )));
344 }
345 let order = declaration
346 .get("order_by")
347 .and_then(Value::as_array)
348 .into_iter()
349 .flatten()
350 .map(|item| {
351 Ok(SortKey {
352 field: item
353 .get("field")
354 .and_then(Value::as_str)
355 .ok_or_else(|| {
356 FeatureRuntimeError::InvalidContract(format!("{query_id}.order"))
357 })?
358 .to_owned(),
359 descending: item.get("direction").and_then(Value::as_str) == Some("desc"),
360 })
361 })
362 .collect::<Result<Vec<_>, FeatureRuntimeError>>()?;
363 let mut filter = predicate_for_filters(
364 declaration.get("filters").and_then(Value::as_object),
365 &scoped,
366 store.schema(),
367 entity,
368 )?;
369 if let Some(scope) = declaration.get("actor_task_field") {
370 let field = scope.as_str().ok_or_else(|| {
371 FeatureRuntimeError::InvalidContract(format!("{query_id}.actor_task_field"))
372 })?;
373 if !matches!(
374 (entity, field),
375 ("architect_task", "id")
376 | ("architect_task_event", "task_id")
377 | ("architect_context_item_view", "task_id")
378 ) {
379 return Err(FeatureRuntimeError::InvalidContract(format!(
380 "{query_id}.actor_task_field"
381 )));
382 }
383 let LogicalStore::Architect(architect) = &store else {
384 return Err(FeatureRuntimeError::InvalidContract(entity.to_owned()));
385 };
386 if owned_tasks.is_none() {
387 owned_tasks = Some(
388 pharos_runtime_architect::task_ownership::owned_task_ids(
389 architect,
390 context.actor_id,
391 )
392 .map_err(map_database_error)?,
393 );
394 }
395 let ids = owned_tasks
396 .as_ref()
397 .ok_or_else(|| FeatureRuntimeError::InvalidContract("task ownership".to_owned()))?;
398 let current_ids = if declaration.get("latest_app_attempt") == Some(&Value::Bool(true)) {
399 if entity != "architect_task" || field != "id" {
400 return Err(FeatureRuntimeError::InvalidContract(format!(
401 "{query_id}.latest_app_attempt"
402 )));
403 }
404 Some(
405 pharos_runtime_architect::task_ownership::latest_app_task_ids(architect, ids)
406 .map_err(map_database_error)?,
407 )
408 } else {
409 None
410 };
411 let owner_filter = pharos_runtime_architect::task_ownership::id_filter(
412 field,
413 current_ids.as_ref().unwrap_or(ids),
414 )
415 .map_err(map_database_error)?;
416 filter = Some(match filter {
417 Some(filter) => Predicate::All(vec![filter, owner_filter]),
418 None => owner_filter,
419 });
420 }
421 let mut rows = if entity.ends_with("_view") {
422 let LogicalStore::Architect(architect) = &store else {
423 return Err(FeatureRuntimeError::InvalidContract(entity.to_owned()));
424 };
425 pharos_runtime_architect::projections::query_view(
426 architect,
427 entity,
428 filter,
429 &order,
430 limit as u32,
431 )
432 .map_err(map_database_error)?
433 } else {
434 store
435 .adapter()
436 .query(
437 store.config(),
438 store.schema(),
439 &Query {
440 entity: entity.to_owned(),
441 fields: fields.clone(),
442 filter,
443 order,
444 limit: limit as u32,
445 offset: 0,
446 },
447 )
448 .map_err(map_database_error)?
449 };
450 if query_id == "recent_tasks"
451 && entity == "architect_task"
452 && let LogicalStore::Architect(architect) = &store
453 {
454 let ids = rows
455 .iter()
456 .filter_map(|row| row.get("id").and_then(Value::as_i64))
457 .collect::<Vec<_>>();
458 let metadata =
459 pharos_runtime_architect::task_repository::task_action_metadata(architect, &ids)
460 .map_err(|_| {
461 FeatureRuntimeError::InvalidContract(
462 "recent_tasks.accepted_source".to_owned(),
463 )
464 })?;
465 for row in &mut rows {
466 if let Some(task) = row
467 .get("id")
468 .and_then(Value::as_i64)
469 .and_then(|id| metadata.get(&id))
470 && !task.validation_summary.is_empty()
471 {
472 row.insert(
473 "validation_summary".to_owned(),
474 Value::String(task.validation_summary.clone()),
475 );
476 }
477 let accepted = row
478 .get("id")
479 .and_then(Value::as_i64)
480 .and_then(|id| metadata.get(&id))
481 .is_some_and(|task| task.validated);
482 row.insert(
483 "accepted_source_status".to_owned(),
484 Value::String(
485 if accepted {
486 "validated"
487 } else {
488 "not validated"
489 }
490 .to_owned(),
491 ),
492 );
493 row.insert(
494 "validate_accepted_source".to_owned(),
495 Value::Bool(
496 accepted
497 && row
498 .get("id")
499 .and_then(Value::as_i64)
500 .and_then(|id| metadata.get(&id))
501 .is_some_and(|task| task.source_validation_due)
502 && row.get("task_status").and_then(Value::as_str) == Some("queued")
503 && row.get("app_model_digest").and_then(Value::as_str)
504 == Some("pending")
505 && row.get("brief").and_then(Value::as_str)
506 == Some(
507 pharos_runtime_architect::task_repository::EMPTY_INTAKE_BRIEF,
508 ),
509 ),
510 );
511 }
512 let candidates = rows
513 .iter()
514 .filter(|row| {
515 row.get("validate_accepted_source").and_then(Value::as_bool) == Some(true)
516 })
517 .filter_map(|row| row.get("id").and_then(Value::as_i64))
518 .filter_map(|id| {
519 metadata
520 .get(&id)
521 .map(|task| (id, task.source_version_created_at.clone()))
522 })
523 .collect::<Vec<_>>();
524 let eligible = pharos_runtime_architect::source_validation_inputs::snapshot_guards(
525 architect,
526 &candidates,
527 )
528 .map_err(|_| {
529 FeatureRuntimeError::InvalidContract("recent_tasks.pending_context".to_owned())
530 })?;
531 for row in &mut rows {
532 let allowed = row.get("validate_accepted_source").and_then(Value::as_bool)
533 == Some(true)
534 && row
535 .get("id")
536 .and_then(Value::as_i64)
537 .is_some_and(|id| eligible.contains_key(&id));
538 row.insert(
539 "validate_accepted_source".to_owned(),
540 Value::String(if allowed { "true" } else { "false" }.to_owned()),
541 );
542 }
543 }
544 for row in &rows {
545 if fields.iter().any(|field| !row.contains_key(field)) {
546 return Err(FeatureRuntimeError::InvalidContract(format!(
547 "{query_id}.fields"
548 )));
549 }
550 }
551 let result_kind = declaration
552 .get("result")
553 .and_then(Value::as_str)
554 .unwrap_or("many");
555 scoped.bindings.insert(
556 query_id.clone(),
557 query_binding_value(query_id, result_kind, &rows)?,
558 );
559 output.insert(query_id.clone(), rows);
560 }
561 Ok(output)
562}
563
564fn write_values(
566 values: &Map<String, Value>,
567 context: &FeatureRequestContext,
568 bindings: &BTreeMap<String, usize>,
569 row_generated: &BTreeMap<String, WriteValue>,
570 schema: &AppSchema,
571 entity: &str,
572) -> Result<BTreeMap<String, WriteValue>, FeatureRuntimeError> {
573 values
574 .iter()
575 .map(|(field, source)| {
576 if let Some(path) = source.as_str().and_then(|text| text.strip_prefix("query."))
577 && let Some(name) = path.strip_suffix(".id")
578 && let Some(step) = bindings.get(name)
579 {
580 return Ok((field.clone(), WriteValue::GeneratedId(*step)));
581 }
582 if let Some(name) = source.as_str().and_then(|text| text.strip_prefix("row."))
583 && let Some(step) = row_generated.get(name)
584 {
585 return Ok((field.clone(), step.clone()));
586 }
587 let value = resolve_value(source, context)?
588 .ok_or_else(|| FeatureRuntimeError::InvalidInput(field.clone()))?;
589 Ok((
590 field.clone(),
591 WriteValue::Literal(field_value(schema, entity, field, value)?),
592 ))
593 })
594 .collect()
595}
596
597#[derive(Clone)]
598struct InsertedRow {
599 fields: Map<String, Value>,
600 generated: BTreeMap<String, WriteValue>,
601}
602
603fn planned_write_targets(steps: &[Mutation], entity: &str) -> bool {
605 steps.iter().any(|step| match step {
606 Mutation::Insert { entity: target, .. }
607 | Mutation::InsertIfAbsent { entity: target, .. }
608 | Mutation::Upsert { entity: target, .. }
609 | Mutation::Update { entity: target, .. }
610 | Mutation::Delete { entity: target, .. }
611 | Mutation::Reserve { entity: target, .. } => target == entity,
612 Mutation::RequireCount { .. } => false,
613 })
614}
615
616fn local_row_matches(
618 row: &InsertedRow,
619 filters: &Map<String, Value>,
620 context: &FeatureRequestContext,
621 bindings: &BTreeMap<String, usize>,
622) -> Result<bool, FeatureRuntimeError> {
623 for (field, source) in filters {
624 if let Some(path) = source.as_str().and_then(|text| text.strip_prefix("query."))
625 && let Some(name) = path.strip_suffix(".id")
626 && let Some(step) = bindings.get(name)
627 {
628 if row.generated.get(field) != Some(&WriteValue::GeneratedId(*step)) {
629 return Ok(false);
630 }
631 continue;
632 }
633 let value = resolve_value(source, context)?.ok_or_else(|| {
634 FeatureRuntimeError::InvalidContract("local select filter".to_owned())
635 })?;
636 if row.fields.get(field) != Some(&value) {
637 return Ok(false);
638 }
639 }
640 Ok(true)
641}
642
643fn inserted_row(
644 values: &BTreeMap<String, WriteValue>,
645 identity: Option<&str>,
646 step: usize,
647 ensured_unique_key: Option<&[String]>,
648) -> InsertedRow {
649 let mut fields = Map::new();
650 let mut generated = BTreeMap::new();
651 for (name, value) in values {
652 match value {
653 WriteValue::Literal(value) => {
654 fields.insert(name.clone(), value.clone());
655 }
656 WriteValue::GeneratedId(_)
657 | WriteValue::GeneratedText { .. }
658 | WriteValue::GeneratedJson { .. } => {
659 fields.insert(name.clone(), Value::Null);
660 generated.insert(name.clone(), value.clone());
661 }
662 }
663 }
664 if let Some(identity) = identity {
665 fields.insert(identity.to_owned(), Value::Null);
666 generated.insert(identity.to_owned(), WriteValue::GeneratedId(step));
667 }
668 if let Some(unique_key) = ensured_unique_key {
669 fields.retain(|name, _| Some(name.as_str()) == identity || unique_key.contains(name));
670 generated.retain(|name, _| Some(name.as_str()) == identity || unique_key.contains(name));
671 }
672 InsertedRow { fields, generated }
673}
674
675fn complete_insert_values(
677 schema: &AppSchema,
678 entity: &str,
679 values: &mut BTreeMap<String, WriteValue>,
680 context: &FeatureRequestContext,
681) -> Result<(), FeatureRuntimeError> {
682 let declaration = schema
683 .entities
684 .iter()
685 .find(|entry| entry.name == entity)
686 .ok_or_else(|| FeatureRuntimeError::InvalidContract(entity.to_owned()))?;
687 for field in &declaration.fields {
688 if values.contains_key(&field.name)
689 || declaration.identity.generated_field() == Some(field.name.as_str())
690 {
691 continue;
692 }
693 let value = match &field.default {
694 Some(FieldDefault::Literal(value)) => value.clone(),
695 Some(FieldDefault::CurrentTimestamp) => {
696 resolve_value(&Value::String("server.now".to_owned()), context)?
697 .ok_or_else(|| FeatureRuntimeError::InvalidContract(field.name.clone()))?
698 }
699 None if field.nullable => Value::Null,
700 None => continue,
701 };
702 values.insert(
703 field.name.clone(),
704 WriteValue::Literal(field_value(schema, entity, &field.name, value)?),
705 );
706 }
707 Ok(())
708}
709
710fn ensured_existing_row(
712 store: &LogicalStore,
713 entity: &str,
714 unique_key: &[String],
715 values: &BTreeMap<String, WriteValue>,
716 steps: &mut Vec<Mutation>,
717) -> Result<Option<Map<String, Value>>, FeatureRuntimeError> {
718 let predicates = unique_key
719 .iter()
720 .map(|field| {
721 let Some(WriteValue::Literal(value)) = values.get(field) else {
722 return Err(FeatureRuntimeError::InvalidContract(
723 "ensure requires a resolved unique key".to_owned(),
724 ));
725 };
726 Ok(Predicate::Equal(field.clone(), value.clone()))
727 })
728 .collect::<Result<Vec<_>, _>>()?;
729 let filter = Predicate::All(predicates);
730 let declaration = store
731 .schema()
732 .entities
733 .iter()
734 .find(|entry| entry.name == entity)
735 .ok_or_else(|| FeatureRuntimeError::InvalidContract(entity.to_owned()))?;
736 let mut rows = store
737 .adapter()
738 .query(
739 store.config(),
740 store.schema(),
741 &Query {
742 entity: entity.to_owned(),
743 fields: declaration
744 .fields
745 .iter()
746 .map(|field| field.name.clone())
747 .collect(),
748 filter: Some(filter.clone()),
749 order: Vec::new(),
750 limit: 2,
751 offset: 0,
752 },
753 )
754 .map_err(map_database_error)?;
755 if rows.len() > 1 {
756 return Err(FeatureRuntimeError::InvalidContract(
757 "ensure unique key matched multiple records".to_owned(),
758 ));
759 }
760 steps.push(Mutation::RequireCount {
761 entity: entity.to_owned(),
762 filter,
763 minimum: rows.len() as u64,
764 maximum: Some(rows.len() as u64),
765 });
766 if let Some(row) = rows.pop() {
767 append_read_guards(store.schema(), entity, &[&row], steps)?;
768 Ok(Some(row))
769 } else {
770 Ok(None)
771 }
772}
773
774fn reservation_transition(
776 kind: &str,
777 operation: &Value,
778 context: &FeatureRequestContext,
779 schema: &AppSchema,
780) -> Result<(Mutation, Mutation), FeatureRuntimeError> {
781 let entity = operation
782 .get("entity")
783 .and_then(Value::as_str)
784 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.entity")))?;
785 let status_field = operation
786 .get("status_field")
787 .and_then(Value::as_str)
788 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.status_field")))?;
789 let statuses = operation
790 .get("active_statuses")
791 .and_then(Value::as_array)
792 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.active_statuses")))?;
793 if statuses.is_empty()
794 || statuses.len() > 16
795 || statuses.iter().any(|status| {
796 status
797 .as_str()
798 .is_none_or(|text| text.is_empty() || text.len() > 64)
799 })
800 {
801 return Err(FeatureRuntimeError::InvalidContract(format!(
802 "{kind}.active_statuses"
803 )));
804 }
805 let owner = predicate_for_filters(
806 operation.get("owner_filters").and_then(Value::as_object),
807 context,
808 schema,
809 entity,
810 )?
811 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.owner_filters")))?;
812 let scope = predicate_for_filters(
813 operation.get("scope_filters").and_then(Value::as_object),
814 context,
815 schema,
816 entity,
817 )?
818 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.scope_filters")))?;
819 let capacity = operation
820 .get("capacity")
821 .and_then(Value::as_u64)
822 .filter(|value| *value > 0 && *value <= 100_000)
823 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.capacity")))?;
824 let status = operation
825 .get("to_status")
826 .and_then(Value::as_str)
827 .filter(|text| !text.is_empty() && text.len() <= 64)
828 .ok_or_else(|| FeatureRuntimeError::InvalidContract(format!("{kind}.to_status")))?;
829 let filter = Predicate::All(vec![
830 owner,
831 scope,
832 Predicate::In(status_field.to_owned(), statuses.clone()),
833 ]);
834 let mut values = operation
835 .get("values")
836 .and_then(Value::as_object)
837 .map(|values| {
838 write_values(
839 values,
840 context,
841 &BTreeMap::new(),
842 &BTreeMap::new(),
843 schema,
844 entity,
845 )
846 })
847 .transpose()?
848 .unwrap_or_default();
849 values.insert(
850 status_field.to_owned(),
851 WriteValue::Literal(Value::String(status.to_owned())),
852 );
853 Ok((
854 Mutation::RequireCount {
855 entity: entity.to_owned(),
856 filter: filter.clone(),
857 minimum: 0,
858 maximum: Some(capacity),
859 },
860 Mutation::Update {
861 entity: entity.to_owned(),
862 filter,
863 values,
864 expected_rows: None,
865 },
866 ))
867}
868
869fn prior_idempotent_result(
871 app: &AppRuntime,
872 feature: &Value,
873 idempotency: &Value,
874 context: &FeatureRequestContext,
875) -> Result<bool, FeatureRuntimeError> {
876 let query_id = idempotency
877 .get("query")
878 .and_then(Value::as_str)
879 .ok_or_else(|| FeatureRuntimeError::InvalidContract("idempotency.query".to_owned()))?;
880 let matches = idempotency
881 .get("matches")
882 .and_then(Value::as_object)
883 .ok_or_else(|| FeatureRuntimeError::InvalidContract("idempotency.matches".to_owned()))?;
884 let rows = execute_logical_queries(app, feature, &[query_id.to_owned()], context)?;
885 let Some(row) = rows.get(query_id).and_then(|rows| rows.first()) else {
886 return Ok(false);
887 };
888 for (field, expected_source) in matches {
889 let expected = resolve_value(expected_source, context)?
890 .ok_or_else(|| FeatureRuntimeError::InvalidInput(field.to_owned()))?;
891 if row.get(field) != Some(&expected) {
892 return Err(FeatureRuntimeError::Conflict(
893 "idempotency key reused with different input".to_owned(),
894 ));
895 }
896 }
897 Ok(true)
898}
899
900fn append_read_guards(
903 schema: &AppSchema,
904 entity_name: &str,
905 rows: &[&Map<String, Value>],
906 steps: &mut Vec<Mutation>,
907) -> Result<(), FeatureRuntimeError> {
908 if rows.len() > 1000 {
909 return Err(map_database_error(DatabaseError::CapacityExceeded));
910 }
911 let typed_rows = rows
912 .iter()
913 .map(|row| {
914 row.iter()
915 .map(|(field, value)| {
916 Ok((
917 field.clone(),
918 field_value(schema, entity_name, field, value.clone())?,
919 ))
920 })
921 .collect::<Result<Map<String, Value>, FeatureRuntimeError>>()
922 })
923 .collect::<Result<Vec<_>, _>>()?;
924 let identity = pharos_runtime_database::operations::entity(schema, entity_name)
925 .map_err(map_database_error)?
926 .identity
927 .fields();
928 if typed_rows
929 .iter()
930 .all(|row| identity.iter().all(|field| row.contains_key(*field)))
931 {
932 let mut seen = BTreeSet::new();
933 for row in &typed_rows {
934 let key = identity
935 .iter()
936 .map(|field| &row[*field])
937 .collect::<Vec<_>>();
938 let digest = pharos_runtime_database::operations::operation_fingerprint(&key)
939 .map_err(map_database_error)?;
940 if !seen.insert(digest) {
941 return Err(map_database_error(DatabaseError::InvalidInput));
942 }
943 }
944 }
945 let mut planned = Vec::new();
946 for chunk in typed_rows.chunks(100) {
947 let references = chunk.iter().collect::<Vec<_>>();
948 planned.extend(
949 pharos_runtime_database::operations::guard_read_set(schema, entity_name, &references)
950 .map_err(map_database_error)?,
951 );
952 }
953 if steps.len().saturating_add(planned.len())
954 > pharos_runtime_database::operations::MAX_TRANSACTION_STEPS
955 {
956 return Err(map_database_error(DatabaseError::CapacityExceeded));
957 }
958 steps.extend(planned);
959 Ok(())
960}
961
962#[cfg(not(target_os = "wasi"))]
964fn bind_guard_preconditions(
965 app: &AppRuntime,
966 feature: &Value,
967 guard_contract: Option<&Value>,
968 context: &mut FeatureRequestContext,
969 schema: &AppSchema,
970 steps: &mut Vec<Mutation>,
971 action_id: &str,
972) -> Result<(), FeatureRuntimeError> {
973 let Some(contract) = guard_contract.filter(|value| !value.is_null()) else {
974 return Ok(());
975 };
976 let guards = contract
977 .get("guards")
978 .and_then(Value::as_array)
979 .ok_or_else(|| FeatureRuntimeError::InvalidContract("guard contract".to_owned()))?;
980 if guards.len() > 16 {
981 return Err(FeatureRuntimeError::InvalidContract(
982 "guard count".to_owned(),
983 ));
984 }
985 let queries = feature
986 .get("queries")
987 .and_then(Value::as_object)
988 .ok_or_else(|| FeatureRuntimeError::InvalidContract("feature.queries".to_owned()))?;
989 let mut guarded = BTreeSet::new();
990 for guard in guards {
991 let query_id = guard
992 .get("query")
993 .and_then(Value::as_str)
994 .ok_or_else(|| FeatureRuntimeError::InvalidContract("guard.query".to_owned()))?;
995 if !guarded.insert(query_id.to_owned()) {
996 continue;
997 }
998 let declaration = queries
999 .get(query_id)
1000 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1001 let entity = declaration
1002 .get("entity")
1003 .and_then(Value::as_str)
1004 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1005 let generated = BTreeMap::new();
1006 for dependency in operation_query_references(guard)?
1007 .into_iter()
1008 .chain(std::iter::once(query_id.to_owned()))
1009 {
1010 QueryDependencyPlan {
1011 app,
1012 feature,
1013 context,
1014 generated: &generated,
1015 schema,
1016 steps,
1017 visiting: BTreeSet::new(),
1018 }
1019 .bind(&dependency)?;
1020 }
1021 let binding = context
1022 .bindings
1023 .get(query_id)
1024 .ok_or_else(|| FeatureRuntimeError::MissingRecord(query_id.to_owned()))?;
1025 let rows = super::guard_binding_rows(query_id, binding)?;
1026 let filter = predicate_for_filters(
1027 declaration.get("filters").and_then(Value::as_object),
1028 context,
1029 schema,
1030 entity,
1031 )?
1032 .ok_or_else(|| FeatureRuntimeError::InvalidContract("guard.filters".to_owned()))?;
1033 steps.push(Mutation::RequireCount {
1034 entity: entity.to_owned(),
1035 filter,
1036 minimum: rows.len() as u64,
1037 maximum: Some(rows.len() as u64),
1038 });
1039 append_read_guards(schema, entity, &rows, steps)?;
1040 }
1041 super::evaluate_guard_contract(action_id, guard_contract, context)
1042}
1043
1044fn operation_query_references(value: &Value) -> Result<BTreeSet<String>, FeatureRuntimeError> {
1046 let mut references = BTreeSet::new();
1047 let mut pending = vec![value];
1048 let mut nodes = 0;
1049 while let Some(value) = pending.pop() {
1050 nodes += 1;
1051 if nodes > 8192 {
1052 return Err(FeatureRuntimeError::InvalidContract(
1053 "operation query references".to_owned(),
1054 ));
1055 }
1056 match value {
1057 Value::String(value) => {
1058 if let Some(reference) = value.strip_prefix("query.") {
1059 let name = reference.split('.').next().unwrap_or_default();
1060 if !pharos_runtime_database::operations::valid_identifier(name)
1061 || references.len() >= 32
1062 {
1063 return Err(FeatureRuntimeError::InvalidContract(
1064 "query reference".to_owned(),
1065 ));
1066 }
1067 references.insert(name.to_owned());
1068 }
1069 }
1070 Value::Array(values) => pending.extend(values),
1071 Value::Object(values) => pending.extend(values.values()),
1072 _ => {}
1073 }
1074 }
1075 Ok(references)
1076}
1077
1078struct QueryDependencyPlan<'a> {
1080 app: &'a AppRuntime,
1081 feature: &'a Value,
1082 context: &'a mut FeatureRequestContext,
1083 generated: &'a BTreeMap<String, usize>,
1084 schema: &'a AppSchema,
1085 steps: &'a mut Vec<Mutation>,
1086 visiting: BTreeSet<String>,
1087}
1088
1089impl QueryDependencyPlan<'_> {
1090 fn bind(&mut self, query_id: &str) -> Result<(), FeatureRuntimeError> {
1093 if self.context.bindings.contains_key(query_id) || self.generated.contains_key(query_id) {
1094 return Ok(());
1095 }
1096 if self.visiting.len() >= 32 || !self.visiting.insert(query_id.to_owned()) {
1097 return Err(FeatureRuntimeError::InvalidContract(
1098 "cyclic query dependency".to_owned(),
1099 ));
1100 }
1101 let feature = self.feature;
1102 let declaration = feature
1103 .get("queries")
1104 .and_then(|queries| queries.get(query_id))
1105 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1106 for dependency in operation_query_references(declaration)? {
1107 self.bind(&dependency)?;
1108 }
1109 let entity = declaration
1110 .get("entity")
1111 .and_then(Value::as_str)
1112 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1113 if planned_write_targets(self.steps, entity) {
1114 return Err(FeatureRuntimeError::InvalidContract(
1115 "query dependency after write requires transactional select".to_owned(),
1116 ));
1117 }
1118 let results =
1119 execute_logical_queries(self.app, feature, &[query_id.to_owned()], self.context)?;
1120 let rows = results
1121 .get(query_id)
1122 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1123 let filter = predicate_for_filters(
1124 declaration.get("filters").and_then(Value::as_object),
1125 self.context,
1126 self.schema,
1127 entity,
1128 )?;
1129 if let Some(filter) = filter {
1130 self.steps.push(Mutation::RequireCount {
1131 entity: entity.to_owned(),
1132 filter,
1133 minimum: rows.len() as u64,
1134 maximum: Some(rows.len() as u64),
1135 });
1136 }
1137 append_read_guards(
1138 self.schema,
1139 entity,
1140 &rows.iter().collect::<Vec<_>>(),
1141 self.steps,
1142 )?;
1143 self.context.bindings.insert(
1144 query_id.to_owned(),
1145 query_binding_value(
1146 query_id,
1147 declaration
1148 .get("result")
1149 .and_then(Value::as_str)
1150 .unwrap_or("many"),
1151 rows,
1152 )?,
1153 );
1154 self.visiting.remove(query_id);
1155 Ok(())
1156 }
1157}
1158
1159pub(super) fn execute_logical_transaction(
1161 app: &AppRuntime,
1162 feature: &Value,
1163 transaction: &Value,
1164 guard_contract: Option<&Value>,
1165 mut context: FeatureRequestContext,
1166 action_id: &str,
1167) -> Result<(), FeatureRuntimeError> {
1168 #[cfg(target_os = "wasi")]
1169 let _ = guard_contract;
1170 let idempotency = transaction.get("idempotency");
1171 if let Some(contract) = idempotency
1172 && prior_idempotent_result(app, feature, contract, &context)?
1173 {
1174 return Ok(());
1175 }
1176 #[cfg(not(target_os = "wasi"))]
1177 populate_generated_values(transaction, &mut context)?;
1178 let store = store_for_app(app)?;
1179 let queries = feature
1180 .get("queries")
1181 .and_then(Value::as_object)
1182 .ok_or_else(|| FeatureRuntimeError::InvalidContract("feature.queries".to_owned()))?;
1183 let declared_steps = transaction_operations(transaction)?;
1184 let reserve_error = declared_steps
1185 .iter()
1186 .find(|(kind, _)| kind == "reserve")
1187 .map(|(_, operation)| {
1188 let code = operation
1189 .get("on_conflict")
1190 .and_then(|value| value.get("code"))
1191 .and_then(Value::as_str)
1192 .filter(|text| !text.is_empty() && text.len() <= 128)
1193 .unwrap_or("reservation_unavailable");
1194 let message = operation
1195 .get("on_conflict")
1196 .and_then(|value| value.get("message"))
1197 .and_then(Value::as_str)
1198 .filter(|text| !text.is_empty() && text.len() <= 256)
1199 .unwrap_or("The requested reservation is not available.");
1200 (code.to_owned(), message.to_owned())
1201 });
1202 let mut bindings = BTreeMap::new();
1203 let mut steps = Vec::with_capacity(declared_steps.len());
1204 let mut inserted = BTreeMap::<String, Vec<InsertedRow>>::new();
1205 let mut inserted_queries = BTreeMap::<String, Vec<InsertedRow>>::new();
1206 let mut guard_bound = false;
1207 for (kind, operation) in declared_steps {
1208 for query_id in operation_query_references(&operation)? {
1209 QueryDependencyPlan {
1210 app,
1211 feature,
1212 context: &mut context,
1213 generated: &bindings,
1214 schema: store.schema(),
1215 steps: &mut steps,
1216 visiting: BTreeSet::new(),
1217 }
1218 .bind(&query_id)?;
1219 }
1220 if !guard_bound && !matches!(kind.as_str(), "require" | "select" | "assert") {
1221 #[cfg(not(target_os = "wasi"))]
1222 bind_guard_preconditions(
1223 app,
1224 feature,
1225 guard_contract,
1226 &mut context,
1227 store.schema(),
1228 &mut steps,
1229 action_id,
1230 )?;
1231 guard_bound = true;
1232 }
1233 if kind == "reserve" {
1234 let entity = operation
1235 .get("entity")
1236 .and_then(Value::as_str)
1237 .ok_or_else(|| FeatureRuntimeError::InvalidContract("reserve.entity".to_owned()))?;
1238 let status_field = operation
1239 .get("status_field")
1240 .and_then(Value::as_str)
1241 .ok_or_else(|| {
1242 FeatureRuntimeError::InvalidContract("reserve.status_field".to_owned())
1243 })?;
1244 let statuses = operation
1245 .get("active_statuses")
1246 .and_then(Value::as_array)
1247 .filter(|values| !values.is_empty() && values.len() <= 16)
1248 .ok_or_else(|| {
1249 FeatureRuntimeError::InvalidContract("reserve.active_statuses".to_owned())
1250 })?
1251 .clone();
1252 let owner_filters = operation
1253 .get("owner_filters")
1254 .and_then(Value::as_object)
1255 .ok_or_else(|| {
1256 FeatureRuntimeError::InvalidContract("reserve.owner_filters".to_owned())
1257 })?;
1258 let owner_filter = predicate_for_transaction_filters(
1259 owner_filters,
1260 &context,
1261 &bindings,
1262 store.schema(),
1263 entity,
1264 )?;
1265 let scope_filters = operation
1266 .get("scope_filters")
1267 .and_then(Value::as_object)
1268 .ok_or_else(|| {
1269 FeatureRuntimeError::InvalidContract("reserve.scope_filters".to_owned())
1270 })?;
1271 let scope_filter = predicate_for_transaction_filters(
1272 scope_filters,
1273 &context,
1274 &bindings,
1275 store.schema(),
1276 entity,
1277 )?;
1278 let lock_scope_filter = operation
1279 .get("lock_scope_filters")
1280 .map(|filters| {
1281 predicate_for_transaction_filters(
1282 filters.as_object().ok_or_else(|| {
1283 FeatureRuntimeError::InvalidContract(
1284 "reserve.lock_scope_filters".to_owned(),
1285 )
1286 })?,
1287 &context,
1288 &bindings,
1289 store.schema(),
1290 entity,
1291 )
1292 })
1293 .transpose()?;
1294 let capacity = operation
1295 .get("capacity")
1296 .and_then(|value| {
1297 value.as_u64().or_else(|| {
1298 resolve_value(value, &context)
1299 .ok()
1300 .flatten()
1301 .and_then(|value| value.as_u64())
1302 })
1303 })
1304 .filter(|value| *value <= 100_000)
1305 .ok_or_else(|| {
1306 FeatureRuntimeError::InvalidContract("reserve.capacity".to_owned())
1307 })?;
1308 let expiry = operation
1309 .get("expires_after_seconds")
1310 .and_then(Value::as_u64)
1311 .map(|seconds| {
1312 if seconds == 0 || seconds > 604_800 {
1313 return Err(FeatureRuntimeError::InvalidContract(
1314 "reserve.expires_after_seconds".to_owned(),
1315 ));
1316 }
1317 let cutoff = resolve_value(
1318 &serde_json::json!({"server_now_minus_seconds":seconds}),
1319 &context,
1320 )?
1321 .ok_or_else(|| {
1322 FeatureRuntimeError::InvalidContract("reserve.expiry".to_owned())
1323 })?;
1324 let now = resolve_value(&Value::String("server.now".to_owned()), &context)?
1325 .ok_or_else(|| {
1326 FeatureRuntimeError::InvalidContract("reserve.expiry".to_owned())
1327 })?;
1328 let declared = store
1329 .schema()
1330 .entities
1331 .iter()
1332 .find(|entry| entry.name == entity)
1333 .ok_or_else(|| {
1334 FeatureRuntimeError::InvalidContract("reserve.entity".to_owned())
1335 })?;
1336 let released_at_name = operation
1337 .get("expires_at_field")
1338 .and_then(Value::as_str)
1339 .unwrap_or("released_at");
1340 let reason_name = operation
1341 .get("expires_reason_field")
1342 .and_then(Value::as_str)
1343 .unwrap_or("release_reason");
1344 Ok(ReservationExpiry {
1345 filter: operation
1346 .get("expires_filters")
1347 .map(|filters| {
1348 predicate_for_transaction_filters(
1349 filters.as_object().ok_or_else(|| {
1350 FeatureRuntimeError::InvalidContract(
1351 "reserve.expires_filters".to_owned(),
1352 )
1353 })?,
1354 &context,
1355 &bindings,
1356 store.schema(),
1357 entity,
1358 )
1359 })
1360 .transpose()?,
1361 timestamp_field: operation
1362 .get("expires_from_field")
1363 .and_then(Value::as_str)
1364 .unwrap_or("reserved_at")
1365 .to_owned(),
1366 cutoff,
1367 statuses: operation
1368 .get("expire_statuses")
1369 .and_then(Value::as_array)
1370 .cloned()
1371 .unwrap_or_else(|| vec![Value::String("reserved".to_owned())]),
1372 to_status: Value::String(
1373 operation
1374 .get("expires_to_status")
1375 .and_then(Value::as_str)
1376 .unwrap_or("released")
1377 .to_owned(),
1378 ),
1379 released_at: declared
1380 .fields
1381 .iter()
1382 .any(|field| field.name == released_at_name)
1383 .then(|| (released_at_name.to_owned(), now)),
1384 reason: declared
1385 .fields
1386 .iter()
1387 .any(|field| field.name == reason_name)
1388 .then(|| {
1389 (
1390 reason_name.to_owned(),
1391 Value::String(
1392 operation
1393 .get("expires_reason")
1394 .and_then(Value::as_str)
1395 .unwrap_or("reservation_expired")
1396 .to_owned(),
1397 ),
1398 )
1399 }),
1400 })
1401 })
1402 .transpose()?;
1403 let values = operation
1404 .get("values")
1405 .and_then(Value::as_object)
1406 .ok_or_else(|| FeatureRuntimeError::InvalidContract("reserve.values".to_owned()))?;
1407 steps.push(Mutation::Reserve {
1408 entity: entity.to_owned(),
1409 status_field: status_field.to_owned(),
1410 active_statuses: statuses,
1411 owner_filter: Box::new(owner_filter),
1412 scope_filter: Box::new(scope_filter),
1413 lock_scope_filter: lock_scope_filter.map(Box::new),
1414 capacity,
1415 expiry: expiry.map(Box::new),
1416 values: write_values(
1417 values,
1418 &context,
1419 &bindings,
1420 &BTreeMap::new(),
1421 store.schema(),
1422 entity,
1423 )?,
1424 });
1425 continue;
1426 }
1427 if matches!(kind.as_str(), "confirm" | "release") {
1428 let (guard, update) =
1429 reservation_transition(&kind, &operation, &context, store.schema())?;
1430 steps.push(guard);
1431 steps.push(update);
1432 continue;
1433 }
1434 if kind == "bind_inserted" {
1435 let query_id = operation
1436 .get("query")
1437 .and_then(Value::as_str)
1438 .ok_or_else(|| {
1439 FeatureRuntimeError::InvalidContract("bind_inserted.query".to_owned())
1440 })?;
1441 let declaration = queries
1442 .get(query_id)
1443 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1444 let entity = declaration
1445 .get("entity")
1446 .and_then(Value::as_str)
1447 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1448 let rows = inserted
1449 .get(entity)
1450 .cloned()
1451 .ok_or_else(|| FeatureRuntimeError::MissingRecord(query_id.to_owned()))?;
1452 if rows.len() > 100 {
1453 return Err(FeatureRuntimeError::InvalidContract(
1454 "bind_inserted.limit".to_owned(),
1455 ));
1456 }
1457 context.bindings.insert(
1458 query_id.to_owned(),
1459 Value::Array(
1460 rows.iter()
1461 .map(|row| Value::Object(row.fields.clone()))
1462 .collect(),
1463 ),
1464 );
1465 inserted_queries.insert(query_id.to_owned(), rows);
1466 continue;
1467 }
1468 if kind == "insert_from" {
1469 let query_id = operation
1470 .get("query")
1471 .and_then(Value::as_str)
1472 .ok_or_else(|| {
1473 FeatureRuntimeError::InvalidContract("insert_from.query".to_owned())
1474 })?;
1475 let entity = operation
1476 .get("entity")
1477 .and_then(Value::as_str)
1478 .ok_or_else(|| {
1479 FeatureRuntimeError::InvalidContract("insert_from.entity".to_owned())
1480 })?;
1481 let values = operation
1482 .get("values")
1483 .and_then(Value::as_object)
1484 .ok_or_else(|| {
1485 FeatureRuntimeError::InvalidContract("insert_from.values".to_owned())
1486 })?;
1487 let max_rows = operation
1488 .get("max_rows")
1489 .and_then(Value::as_u64)
1490 .unwrap_or(20);
1491 if max_rows == 0 || max_rows > 100 {
1492 return Err(FeatureRuntimeError::InvalidContract(
1493 "insert_from.max_rows".to_owned(),
1494 ));
1495 }
1496 let source = context
1497 .bindings
1498 .get(query_id)
1499 .and_then(Value::as_array)
1500 .ok_or_else(|| FeatureRuntimeError::MissingRecord(query_id.to_owned()))?
1501 .clone();
1502 if source.len() > max_rows as usize || steps.len().saturating_add(source.len()) > 64 {
1503 return Err(FeatureRuntimeError::InvalidContract(
1504 "insert_from.limit".to_owned(),
1505 ));
1506 }
1507 let source_generated = inserted_queries.get(query_id);
1508 let identity = store
1509 .schema()
1510 .entities
1511 .iter()
1512 .find(|entry| entry.name == entity)
1513 .ok_or_else(|| FeatureRuntimeError::InvalidContract(entity.to_owned()))?
1514 .identity
1515 .generated_field()
1516 .map(str::to_owned);
1517 for (index, row) in source.into_iter().enumerate() {
1518 context.row = row.as_object().cloned().ok_or_else(|| {
1519 FeatureRuntimeError::InvalidContract("insert_from.row".to_owned())
1520 })?;
1521 let generated = source_generated
1522 .and_then(|rows| rows.get(index))
1523 .map(|row| &row.generated);
1524 let mut values = write_values(
1525 values,
1526 &context,
1527 &bindings,
1528 generated.unwrap_or(&BTreeMap::new()),
1529 store.schema(),
1530 entity,
1531 )?;
1532 complete_insert_values(store.schema(), entity, &mut values, &context)?;
1533 let step = steps.len();
1534 inserted
1535 .entry(entity.to_owned())
1536 .or_default()
1537 .push(inserted_row(&values, identity.as_deref(), step, None));
1538 steps.push(Mutation::Insert {
1539 entity: entity.to_owned(),
1540 values,
1541 });
1542 }
1543 context.row.clear();
1544 continue;
1545 }
1546 if kind == "select" {
1547 let query_id = operation
1548 .get("query")
1549 .and_then(Value::as_str)
1550 .ok_or_else(|| FeatureRuntimeError::InvalidContract("select.query".to_owned()))?;
1551 let query = queries
1552 .get(query_id)
1553 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1554 let entity = query
1555 .get("entity")
1556 .and_then(Value::as_str)
1557 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1558 if planned_write_targets(&steps, entity) {
1559 let candidate_rows = inserted.get(entity).ok_or_else(|| {
1560 FeatureRuntimeError::InvalidContract(
1561 "select after write has no transaction-local record".to_owned(),
1562 )
1563 })?;
1564 let filters = query
1565 .get("filters")
1566 .and_then(Value::as_object)
1567 .ok_or_else(|| {
1568 FeatureRuntimeError::InvalidContract("local select filters".to_owned())
1569 })?;
1570 let rows = candidate_rows
1571 .iter()
1572 .filter_map(
1573 |row| match local_row_matches(row, filters, &context, &bindings) {
1574 Ok(true) => Some(Ok(row.clone())),
1575 Ok(false) => None,
1576 Err(error) => Some(Err(error)),
1577 },
1578 )
1579 .collect::<Result<Vec<_>, _>>()?;
1580 let many = query.get("result").and_then(Value::as_str) == Some("many");
1581 if !many && rows.is_empty() {
1582 return Err(FeatureRuntimeError::MissingRecord(query_id.to_owned()));
1583 }
1584 if rows.len() > 100 || (!many && rows.len() != 1) {
1585 return Err(FeatureRuntimeError::InvalidContract(
1586 "select after write has an invalid transaction-local record count"
1587 .to_owned(),
1588 ));
1589 }
1590 let requested_fields = query
1591 .get("fields")
1592 .and_then(Value::as_array)
1593 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1594 if rows.iter().any(|row| {
1595 requested_fields.iter().any(|field| {
1596 field
1597 .as_str()
1598 .is_none_or(|field| !row.fields.contains_key(field))
1599 })
1600 }) {
1601 return Err(FeatureRuntimeError::InvalidContract(
1602 "transaction-local record lacks a selected field".to_owned(),
1603 ));
1604 }
1605 if many {
1606 context.bindings.insert(
1607 query_id.to_owned(),
1608 Value::Array(
1609 rows.iter()
1610 .map(|row| Value::Object(row.fields.clone()))
1611 .collect(),
1612 ),
1613 );
1614 inserted_queries.insert(query_id.to_owned(), rows.clone());
1615 } else {
1616 if let Some(WriteValue::GeneratedId(step)) = rows[0].generated.get("id") {
1617 bindings.insert(query_id.to_owned(), *step);
1618 }
1619 context
1620 .bindings
1621 .insert(query_id.to_owned(), Value::Object(rows[0].fields.clone()));
1622 }
1623 continue;
1624 }
1625 if query.get("result").and_then(Value::as_str) == Some("many") {
1626 return Err(FeatureRuntimeError::InvalidContract(
1627 "select.many".to_owned(),
1628 ));
1629 }
1630 let rows = execute_logical_queries(app, feature, &[query_id.to_owned()], &context)?;
1631 let row = rows
1632 .get(query_id)
1633 .and_then(|rows| rows.first())
1634 .ok_or_else(|| FeatureRuntimeError::MissingRecord(query_id.to_owned()))?;
1635 let entity = query
1636 .get("entity")
1637 .and_then(Value::as_str)
1638 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1639 let identity = store
1640 .schema()
1641 .entities
1642 .iter()
1643 .find(|entry| entry.name == entity)
1644 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?
1645 .identity
1646 .fields();
1647 if identity.iter().any(|field| !row.contains_key(*field)) {
1648 return Err(FeatureRuntimeError::InvalidContract(format!(
1649 "{query_id}.identity"
1650 )));
1651 }
1652 let predicates = row
1653 .iter()
1654 .map(|(field, value)| {
1655 Ok(Predicate::Equal(
1656 field.clone(),
1657 field_value(store.schema(), entity, field, value.clone())?,
1658 ))
1659 })
1660 .collect::<Result<Vec<_>, FeatureRuntimeError>>()?;
1661 steps.push(Mutation::RequireCount {
1662 entity: entity.to_owned(),
1663 filter: Predicate::All(predicates),
1664 minimum: 1,
1665 maximum: None,
1666 });
1667 context
1668 .bindings
1669 .insert(query_id.to_owned(), Value::Object(row.clone()));
1670 continue;
1671 }
1672 let entity = operation
1673 .get("entity")
1674 .and_then(Value::as_str)
1675 .unwrap_or_default();
1676 let mut step = match kind.as_str() {
1677 "allocate_sequence" => {
1678 let counter = operation
1679 .get("counter")
1680 .and_then(Value::as_object)
1681 .ok_or_else(|| {
1682 FeatureRuntimeError::InvalidContract("allocate_sequence.counter".to_owned())
1683 })?;
1684 let counter_entity =
1685 counter
1686 .get("entity")
1687 .and_then(Value::as_str)
1688 .ok_or_else(|| {
1689 FeatureRuntimeError::InvalidContract("counter.entity".to_owned())
1690 })?;
1691 let counter_field =
1692 counter
1693 .get("field")
1694 .and_then(Value::as_str)
1695 .ok_or_else(|| {
1696 FeatureRuntimeError::InvalidContract("counter.field".to_owned())
1697 })?;
1698 let scope = counter
1699 .get("scope")
1700 .and_then(Value::as_object)
1701 .ok_or_else(|| {
1702 FeatureRuntimeError::InvalidContract("counter.scope".to_owned())
1703 })?
1704 .iter()
1705 .map(|(field, source)| {
1706 Ok((
1707 field.clone(),
1708 resolve_value(source, &context)?
1709 .ok_or_else(|| FeatureRuntimeError::InvalidInput(field.clone()))?,
1710 ))
1711 })
1712 .collect::<Result<BTreeMap<_, _>, FeatureRuntimeError>>()?;
1713 let initial = if let Some(seed) = counter.get("seed").and_then(Value::as_object) {
1714 let seed_entity =
1715 seed.get("entity").and_then(Value::as_str).ok_or_else(|| {
1716 FeatureRuntimeError::InvalidContract("counter.seed.entity".to_owned())
1717 })?;
1718 let seed_field =
1719 seed.get("field").and_then(Value::as_str).ok_or_else(|| {
1720 FeatureRuntimeError::InvalidContract("counter.seed.field".to_owned())
1721 })?;
1722 let filter = predicate_for_filters(
1723 seed.get("filters").and_then(Value::as_object),
1724 &context,
1725 store.schema(),
1726 seed_entity,
1727 )?;
1728 let maximum = store
1729 .adapter()
1730 .aggregate(
1731 store.config(),
1732 store.schema(),
1733 &pharos_runtime_database::operations::AggregateQuery {
1734 entity: seed_entity.to_owned(),
1735 filter,
1736 aggregate: pharos_runtime_database::operations::Aggregate::Maximum(
1737 seed_field.to_owned(),
1738 ),
1739 },
1740 )
1741 .map_err(map_database_error)?;
1742 if maximum.is_null() {
1743 0
1744 } else {
1745 maximum.as_i64().ok_or_else(|| {
1746 FeatureRuntimeError::InvalidContract("counter.seed.value".to_owned())
1747 })?
1748 }
1749 } else {
1750 0
1751 };
1752 let maximum = counter
1753 .get("maximum")
1754 .and_then(Value::as_i64)
1755 .ok_or_else(|| {
1756 FeatureRuntimeError::InvalidContract("counter.maximum".to_owned())
1757 })?;
1758 let (value, counter_steps) =
1759 pharos_runtime_database::sequence::plan_sequence_allocation(
1760 store.adapter(),
1761 store.config(),
1762 store.schema(),
1763 pharos_runtime_database::sequence::SequenceAllocation {
1764 entity: counter_entity,
1765 field: counter_field,
1766 scope,
1767 initial,
1768 maximum,
1769 },
1770 )
1771 .map_err(map_database_error)?;
1772 steps.extend(counter_steps);
1773 context
1774 .generated
1775 .insert("sequence_value".to_owned(), Value::from(value));
1776 if let Some(binding) = operation.get("bind").and_then(Value::as_str)
1777 && bindings.insert(binding.to_owned(), steps.len()).is_some()
1778 {
1779 return Err(FeatureRuntimeError::InvalidContract(
1780 "allocate_sequence.bind".to_owned(),
1781 ));
1782 }
1783 Mutation::Insert {
1784 entity: entity.to_owned(),
1785 values: write_values(
1786 operation
1787 .get("values")
1788 .and_then(Value::as_object)
1789 .ok_or_else(|| {
1790 FeatureRuntimeError::InvalidContract(
1791 "allocate_sequence.values".to_owned(),
1792 )
1793 })?,
1794 &context,
1795 &bindings,
1796 &BTreeMap::new(),
1797 store.schema(),
1798 entity,
1799 )?,
1800 }
1801 }
1802 "require" => {
1803 let query_id = operation
1804 .get("query")
1805 .and_then(Value::as_str)
1806 .ok_or_else(|| {
1807 FeatureRuntimeError::InvalidContract("require.query".to_owned())
1808 })?;
1809 if bindings.contains_key(query_id) {
1810 continue;
1813 }
1814 let query = queries
1815 .get(query_id)
1816 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1817 let entity = query
1818 .get("entity")
1819 .and_then(Value::as_str)
1820 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1821 if planned_write_targets(&steps, entity) {
1822 return Err(FeatureRuntimeError::InvalidContract(
1823 "require after write requires transactional read".to_owned(),
1824 ));
1825 }
1826 let filter = predicate_for_filters(
1827 query.get("filters").and_then(Value::as_object),
1828 &context,
1829 store.schema(),
1830 entity,
1831 )?
1832 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1833 let rows = execute_logical_queries(app, feature, &[query_id.to_owned()], &context)?;
1834 let rows = rows
1835 .get(query_id)
1836 .ok_or_else(|| FeatureRuntimeError::InvalidContract(query_id.to_owned()))?;
1837 if rows.is_empty() {
1838 return Err(FeatureRuntimeError::MissingRecord(query_id.to_owned()));
1839 }
1840 let result_kind = query
1841 .get("result")
1842 .and_then(Value::as_str)
1843 .unwrap_or("many");
1844 context.bindings.insert(
1845 query_id.to_owned(),
1846 query_binding_value(query_id, result_kind, rows)?,
1847 );
1848 steps.push(Mutation::RequireCount {
1849 entity: entity.to_owned(),
1850 filter,
1851 minimum: rows.len() as u64,
1852 maximum: Some(rows.len() as u64),
1853 });
1854 let guarded_rows = rows.iter().collect::<Vec<_>>();
1855 append_read_guards(store.schema(), entity, &guarded_rows, &mut steps)?;
1856 continue;
1857 }
1858 "insert" => {
1859 let values = operation
1860 .get("values")
1861 .and_then(Value::as_object)
1862 .ok_or_else(|| {
1863 FeatureRuntimeError::InvalidContract("insert.values".to_owned())
1864 })?;
1865 if let Some(binding) = operation.get("bind").and_then(Value::as_str) {
1866 bindings.insert(binding.to_owned(), steps.len());
1867 }
1868 Mutation::Insert {
1869 entity: entity.to_owned(),
1870 values: write_values(
1871 values,
1872 &context,
1873 &bindings,
1874 &BTreeMap::new(),
1875 store.schema(),
1876 entity,
1877 )?,
1878 }
1879 }
1880 "upsert" => {
1881 let update_on_conflict = match operation.get("update_on_conflict") {
1882 None => false,
1883 Some(Value::Bool(value)) => *value,
1884 Some(_) => {
1885 return Err(FeatureRuntimeError::InvalidContract(
1886 "upsert.update_on_conflict".to_owned(),
1887 ));
1888 }
1889 };
1890 if let Some(binding) = operation.get("bind").and_then(Value::as_str)
1891 && (binding.is_empty()
1892 || update_on_conflict
1893 || bindings.insert(binding.to_owned(), steps.len()).is_some())
1894 {
1895 return Err(FeatureRuntimeError::InvalidContract(
1896 "upsert.bind".to_owned(),
1897 ));
1898 }
1899 let unique_key = operation
1900 .get("unique_by")
1901 .and_then(Value::as_array)
1902 .ok_or_else(|| {
1903 FeatureRuntimeError::InvalidContract("upsert.unique_by".to_owned())
1904 })?
1905 .iter()
1906 .map(|field| {
1907 field.as_str().map(str::to_owned).ok_or_else(|| {
1908 FeatureRuntimeError::InvalidContract("upsert.unique_by".to_owned())
1909 })
1910 })
1911 .collect::<Result<Vec<_>, _>>()?;
1912 let values = operation
1913 .get("values")
1914 .and_then(Value::as_object)
1915 .ok_or_else(|| {
1916 FeatureRuntimeError::InvalidContract("upsert.values".to_owned())
1917 })?;
1918 let values = write_values(
1919 values,
1920 &context,
1921 &bindings,
1922 &BTreeMap::new(),
1923 store.schema(),
1924 entity,
1925 )?;
1926 if update_on_conflict {
1927 Mutation::Upsert {
1928 entity: entity.to_owned(),
1929 unique_key,
1930 values,
1931 }
1932 } else {
1933 Mutation::InsertIfAbsent {
1934 entity: entity.to_owned(),
1935 unique_key,
1936 values,
1937 }
1938 }
1939 }
1940 "update" => {
1941 let filter = predicate_for_transaction_filters(
1942 operation
1943 .get("filters")
1944 .and_then(Value::as_object)
1945 .ok_or_else(|| {
1946 FeatureRuntimeError::InvalidContract("update.filters".to_owned())
1947 })?,
1948 &context,
1949 &bindings,
1950 store.schema(),
1951 entity,
1952 )?;
1953 let values = operation
1954 .get("values")
1955 .and_then(Value::as_object)
1956 .ok_or_else(|| {
1957 FeatureRuntimeError::InvalidContract("update.values".to_owned())
1958 })?;
1959 let expected_rows = steps
1960 .iter()
1961 .rev()
1962 .find_map(|step| match step {
1963 Mutation::RequireCount {
1964 entity: guarded_entity,
1965 filter: guarded_filter,
1966 minimum,
1967 maximum: Some(maximum),
1968 } if guarded_entity == entity
1969 && guarded_filter == &filter
1970 && minimum == maximum =>
1971 {
1972 Some(*minimum)
1973 }
1974 _ => None,
1975 })
1976 .unwrap_or(1);
1977 Mutation::Update {
1978 entity: entity.to_owned(),
1979 filter,
1980 values: write_values(
1981 values,
1982 &context,
1983 &bindings,
1984 &BTreeMap::new(),
1985 store.schema(),
1986 entity,
1987 )?,
1988 expected_rows: Some(expected_rows),
1989 }
1990 }
1991 "delete" => {
1992 let filter = predicate_for_transaction_filters(
1993 operation
1994 .get("filters")
1995 .and_then(Value::as_object)
1996 .ok_or_else(|| {
1997 FeatureRuntimeError::InvalidContract("delete.filters".to_owned())
1998 })?,
1999 &context,
2000 &bindings,
2001 store.schema(),
2002 entity,
2003 )?;
2004 Mutation::Delete {
2005 entity: entity.to_owned(),
2006 filter,
2007 expected_rows: None,
2008 }
2009 }
2010 _ => return Err(FeatureRuntimeError::InvalidContract(kind)),
2011 };
2012 if let Mutation::Insert { entity, values }
2013 | Mutation::InsertIfAbsent { entity, values, .. } = &mut step
2014 {
2015 complete_insert_values(store.schema(), entity, values, &context)?;
2016 }
2017 let ensured_row = if let Mutation::InsertIfAbsent {
2018 entity,
2019 unique_key,
2020 values,
2021 } = &step
2022 {
2023 Some(ensured_existing_row(
2024 &store, entity, unique_key, values, &mut steps,
2025 )?)
2026 } else {
2027 None
2028 };
2029 if ensured_row.is_some()
2030 && let Some(binding) = operation.get("bind").and_then(Value::as_str)
2031 {
2032 bindings.insert(binding.to_owned(), steps.len());
2033 }
2034 if let Mutation::Insert { entity, values }
2035 | Mutation::InsertIfAbsent { entity, values, .. } = &step
2036 {
2037 let identity = store
2038 .schema()
2039 .entities
2040 .iter()
2041 .find(|entry| entry.name == *entity)
2042 .and_then(|entry| entry.identity.generated_field());
2043 let mut row = inserted_row(values, identity, steps.len(), None);
2044 if let Some(Some(fields)) = ensured_row {
2045 row.fields = fields;
2046 }
2047 inserted.entry(entity.clone()).or_default().push(row);
2048 }
2049 steps.push(step);
2050 }
2051 if !guard_bound {
2052 #[cfg(not(target_os = "wasi"))]
2053 bind_guard_preconditions(
2054 app,
2055 feature,
2056 guard_contract,
2057 &mut context,
2058 store.schema(),
2059 &mut steps,
2060 action_id,
2061 )?;
2062 }
2063 let effect_input = context
2064 .input
2065 .iter()
2066 .map(|(key, value)| {
2067 (
2068 key.clone(),
2069 if value.is_null() {
2070 None
2071 } else {
2072 Some(
2073 value
2074 .as_str()
2075 .map(ToOwned::to_owned)
2076 .unwrap_or_else(|| value.to_string()),
2077 )
2078 },
2079 )
2080 })
2081 .collect::<BTreeMap<_, _>>();
2082 let effect_request = crate::runtime::http::HttpRequest {
2083 method: "POST".to_owned(),
2084 target: context.request_target.clone(),
2085 cookie: None,
2086 content_type: None,
2087 headers: String::new(),
2088 body: Vec::new(),
2089 body_file_path: None,
2090 body_len: 0,
2091 is_multipart: false,
2092 };
2093 let effects = crate::runtime::dynamic::declared_effect_steps(
2094 app,
2095 store.schema(),
2096 &effect_request,
2097 action_id,
2098 &effect_input,
2099 0,
2100 context.actor_id,
2101 )
2102 .map_err(|_| FeatureRuntimeError::InvalidContract("declared action effects".to_owned()))?;
2103 let resource = crate::runtime::dynamic::action_resource_binding(app, store.schema(), action_id);
2104 let outcome = pharos_runtime_database::effects::transact_with_resource_effects(
2105 store.adapter(),
2106 store.config(),
2107 store.schema(),
2108 &Transaction {
2109 steps,
2110 idempotency_key: None,
2111 },
2112 &effects,
2113 resource.as_ref(),
2114 );
2115 if matches!(outcome, Err(DatabaseError::Conflict))
2116 && let Some(contract) = idempotency
2117 {
2118 for attempt in 0..3 {
2119 if prior_idempotent_result(app, feature, contract, &context)? {
2120 return Ok(());
2121 }
2122 if attempt < 2 {
2123 std::thread::sleep(std::time::Duration::from_millis(25));
2124 }
2125 }
2126 }
2127 if matches!(outcome, Err(DatabaseError::CapacityExceeded))
2128 && let Some((code, message)) = reserve_error
2129 {
2130 return Err(FeatureRuntimeError::ValidationReport(
2131 super::validation_report(
2132 action_id,
2133 vec![code.clone()],
2134 None,
2135 Some((code.clone(), code, message)),
2136 ),
2137 ));
2138 }
2139 outcome.map_err(map_database_error)?;
2140 Ok(())
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145 #[test]
2147 fn read_guards_batch_full_source_sets_without_widening_scope() {
2148 let schema=serde_json::from_value(serde_json::json!({"app_id":"guard_test","entities":[{"name":"record","identity":{"auto_integer":"id"},"fields":[{"name":"id","kind":"integer","nullable":false,"constraints":[]},{"name":"name","kind":"text","nullable":false,"constraints":[]}],"unique":[],"indexes":[],"conditional_indexes":[]}]})).unwrap();
2149 let mut rows = (0..125)
2150 .map(|id| {
2151 serde_json::json!({"id":id,"name":"pending"})
2152 .as_object()
2153 .unwrap()
2154 .clone()
2155 })
2156 .collect::<Vec<_>>();
2157 let mut steps = Vec::new();
2158 super::append_read_guards(
2159 &schema,
2160 "record",
2161 &rows.iter().collect::<Vec<_>>(),
2162 &mut steps,
2163 )
2164 .unwrap();
2165 assert!(steps.len() > 1 && steps.len() < 125);
2166 rows[124]["id"] = serde_json::json!(0);
2167 let mut rejected = Vec::new();
2168 assert!(
2169 super::append_read_guards(
2170 &schema,
2171 "record",
2172 &rows.iter().collect::<Vec<_>>(),
2173 &mut rejected
2174 )
2175 .is_err()
2176 );
2177 assert!(rejected.is_empty());
2178 let excess = vec![&rows[0]; 1001];
2179 assert!(super::append_read_guards(&schema, "record", &excess, &mut rejected).is_err());
2180 assert!(rejected.is_empty());
2181 }
2182 #[test]
2184 fn optional_integer_references_accept_blank_without_relaxing_required_values() {
2185 use pharos_runtime_database::operations::{AppSchema, Entity, Field, FieldType, Identity};
2186 let field = Field {
2187 name: "account_id".into(),
2188 kind: FieldType::Integer,
2189 nullable: true,
2190 max_length: None,
2191 default: None,
2192 references: None,
2193 on_delete: None,
2194 constraints: vec![],
2195 };
2196 let mut schema = AppSchema {
2197 app_id: "sample".into(),
2198 entities: vec![Entity {
2199 name: "contact".into(),
2200 identity: Identity::AutoInteger("account_id".into()),
2201 fields: vec![field],
2202 unique: vec![],
2203 indexes: vec![],
2204 conditional_indexes: vec![],
2205 }],
2206 };
2207 assert_eq!(
2208 super::field_value(&schema, "contact", "account_id", json!("")).unwrap(),
2209 Value::Null
2210 );
2211 assert_eq!(
2212 super::field_value(&schema, "contact", "account_id", json!("42")).unwrap(),
2213 json!(42)
2214 );
2215 assert!(super::field_value(&schema, "contact", "account_id", json!("42 OR 1=1")).is_err());
2216 schema.entities[0].fields[0].nullable = false;
2217 assert!(super::field_value(&schema, "contact", "account_id", json!("")).is_err());
2218 }
2219 use std::path::PathBuf;
2220 use std::time::{SystemTime, UNIX_EPOCH};
2221
2222 use pharos_runtime_compiler::database::load_source as load_contract;
2223 use pharos_runtime_database::DbBackendConfig;
2224 #[cfg(not(target_os = "wasi"))]
2225 use pharos_runtime_database::operations::Mutation;
2226 use pharos_runtime_database::operations::{Predicate, Query};
2227 use serde_json::{Value, json};
2228
2229 #[cfg(not(target_os = "wasi"))]
2230 use super::bind_guard_preconditions;
2231 use super::{FeatureRequestContext, execute_logical_transaction};
2232 use crate::config::AppRuntime;
2233
2234 #[test]
2236 fn database_error_mapping_preserves_operational_categories() {
2237 for error in [
2238 pharos_runtime_database::operations::DatabaseError::CapacityExceeded,
2239 pharos_runtime_database::operations::DatabaseError::InvalidContract,
2240 pharos_runtime_database::operations::DatabaseError::AccessDenied,
2241 pharos_runtime_database::operations::DatabaseError::Unavailable,
2242 pharos_runtime_database::operations::DatabaseError::StorageFailure,
2243 pharos_runtime_database::operations::DatabaseError::Unsupported,
2244 ] {
2245 assert!(matches!(super::map_database_error(error),
2246 super::FeatureRuntimeError::Database(actual) if actual == error));
2247 }
2248 }
2249
2250 #[test]
2252 fn working_queries_reuse_rows_without_opening_storage() {
2253 let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2254 let temporary = repo
2255 .join("tmp")
2256 .join(format!("pharos-working-query-{}", std::process::id()));
2257 std::fs::create_dir_all(temporary.join("data/contracts"))
2258 .expect("working contract directory");
2259 pharos_runtime_compiler::database::materialize_json(
2260 repo.join("apps/ucal/data/contracts/database.yml"),
2261 temporary.join("data/contracts/database.json"),
2262 )
2263 .expect("logical contract");
2264 std::fs::write(
2265 temporary.join("pharos.app.yml"),
2266 "app_id: ucal\ndatabase:\n migration_contract: data/contracts/database.json\n",
2267 )
2268 .expect("manifest");
2269 let config = temporary.join("runtime.conf");
2270 let missing_database = temporary.join("missing/database.sqlite3");
2271 std::fs::write(
2272 &config,
2273 format!(
2274 "db_backend=sqlite\ndb_path={}\n",
2275 missing_database.display()
2276 ),
2277 )
2278 .expect("configuration");
2279 let app = AppRuntime {
2280 app_id: Some("ucal".to_owned()),
2281 app_root: Some(temporary.display().to_string()),
2282 runtime_conf_path: Some(config.display().to_string()),
2283 ..AppRuntime::default()
2284 };
2285 let feature = json!({"queries":{"loaded":{"entity":"business","fields":["id","name"],"result":"many"}}});
2286 let mut context = FeatureRequestContext::default();
2287 context.bindings.insert(
2288 "loaded".to_owned(),
2289 json!([{"id":1,"name":"kept in process"}]),
2290 );
2291 for _ in 0..3 {
2292 let rows =
2293 super::execute_logical_queries(&app, &feature, &["loaded".to_owned()], &context)
2294 .expect("reuse existing query");
2295 assert_eq!(rows["loaded"][0]["name"], "kept in process");
2296 }
2297 assert!(
2298 !missing_database.exists(),
2299 "cached reads must not open or create storage"
2300 );
2301 context.bindings.clear();
2302 assert!(
2303 super::execute_logical_queries(&app, &feature, &["loaded".to_owned()], &context)
2304 .is_err(),
2305 "missing records require authoritative storage"
2306 );
2307 context
2308 .bindings
2309 .insert("loaded".to_owned(), Value::Array(vec![json!({}); 101]));
2310 assert!(
2311 super::working_query_rows(&context, "loaded").is_err(),
2312 "working views remain bounded"
2313 );
2314 std::fs::remove_dir_all(temporary).expect("clean working test");
2315 }
2316
2317 #[test]
2319 fn inserted_rows_preserve_generated_text_references() {
2320 use pharos_runtime_database::operations::{AppSchema, WriteValue};
2321 use std::collections::BTreeMap;
2322 let expression = WriteValue::GeneratedText {
2323 index: 3,
2324 prefix: "metric:".to_owned(),
2325 };
2326 let row = super::inserted_row(
2327 &BTreeMap::from([("resource_id".to_owned(), expression.clone())]),
2328 Some("id"),
2329 4,
2330 None,
2331 );
2332 let values = super::write_values(
2333 &serde_json::Map::from_iter([("resource_id".to_owned(), json!("row.resource_id"))]),
2334 &FeatureRequestContext::default(),
2335 &BTreeMap::new(),
2336 &row.generated,
2337 &AppSchema {
2338 app_id: "expression_test".to_owned(),
2339 entities: Vec::new(),
2340 },
2341 "outbox",
2342 )
2343 .expect("transaction-local expression");
2344 assert_eq!(values["resource_id"], expression);
2345 assert_eq!(row.generated["id"], WriteValue::GeneratedId(4));
2346 }
2347
2348 #[cfg(not(target_os = "wasi"))]
2350 #[test]
2351 fn logical_cart_guard_adds_transactional_read_checks() {
2352 let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2353 .join("apps/commerce_spa/data/contracts/database.yml");
2354 let schema = load_contract(&source, "commerce_spa")
2355 .expect("Commerce logical contract")
2356 .schema;
2357 let feature = json!({"queries": {"current_cart_items": {
2358 "entity": "shopping_cart_item",
2359 "fields": ["id", "user_id", "price_minor"],
2360 "filters": {"user_id": "subject.user_id"},
2361 "result": "many"
2362 }}});
2363 let guard = json!({"guards": [{
2364 "guard_id": "cart_total_cap", "kind": "sum", "query": "current_cart_items",
2365 "field": "price_minor", "add": "query.published_product.price_minor",
2366 "maximum": 6000
2367 }]});
2368 let mut context = FeatureRequestContext {
2369 actor_id: 42,
2370 ..FeatureRequestContext::default()
2371 };
2372 context.bindings.insert(
2373 "current_cart_items".to_owned(),
2374 json!([{"id": 1, "user_id": 42, "price_minor": 3000}]),
2375 );
2376 context
2377 .bindings
2378 .insert("published_product".to_owned(), json!({"price_minor": 2900}));
2379 let mut steps = Vec::new();
2380 bind_guard_preconditions(
2381 &AppRuntime::default(),
2382 &feature,
2383 Some(&guard),
2384 &mut context,
2385 &schema,
2386 &mut steps,
2387 "add_pack",
2388 )
2389 .expect("guard accepts a cart below the cap");
2390 assert_eq!(steps.len(), 2);
2391 assert!(matches!(
2392 steps[0],
2393 Mutation::RequireCount {
2394 minimum: 1,
2395 maximum: Some(1),
2396 ..
2397 }
2398 ));
2399 context
2400 .bindings
2401 .insert("published_product".to_owned(), json!({"price_minor": 3100}));
2402 let rejected = bind_guard_preconditions(
2403 &AppRuntime::default(),
2404 &feature,
2405 Some(&guard),
2406 &mut context,
2407 &schema,
2408 &mut Vec::new(),
2409 "add_pack",
2410 );
2411 assert!(matches!(
2412 rejected,
2413 Err(super::FeatureRuntimeError::ValidationReport(_))
2414 ));
2415 }
2416
2417 fn exercise_ucal_generated_mutations(backend: &str) {
2419 let repository = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2420 let nonce = SystemTime::now()
2421 .duration_since(UNIX_EPOCH)
2422 .expect("system clock")
2423 .as_nanos();
2424 let temporary = repository
2425 .join("tmp/database-operation-tests")
2426 .join(format!(
2427 "ucal_feature_{backend}_{}_{nonce}",
2428 std::process::id()
2429 ));
2430 let contract_dir = temporary.join("data/contracts");
2431 std::fs::create_dir_all(&contract_dir).expect("isolated app contract directory");
2432 std::fs::write(
2433 temporary.join("pharos.app.yml"),
2434 "app_id: ucal\ndatabase:\n migration_contract: data/contracts/database.json\n",
2435 )
2436 .expect("isolated logical app manifest");
2437 let source = repository.join("apps/ucal/data/contracts/database.yml");
2438 pharos_runtime_compiler::database::materialize_json(
2439 &source,
2440 contract_dir.join("database.json"),
2441 )
2442 .expect("isolated logical app contract");
2443 let path = temporary.join("ucal.sqlite3");
2444 let database = format!("ucal_feature_{}_{nonce}", std::process::id());
2445 let config = match backend {
2446 "sqlite" => DbBackendConfig {
2447 backend: "sqlite".to_owned(),
2448 sqlite_path: Some(path),
2449 ..DbBackendConfig::default()
2450 },
2451 "postgresql" | "mysql" | "mongodb" => DbBackendConfig {
2452 backend: backend.to_owned(),
2453 host: "127.0.0.1".to_owned(),
2454 port: match backend {
2455 "postgresql" => "25432",
2456 "mysql" => "23306",
2457 _ => "27018",
2458 }
2459 .to_owned(),
2460 database,
2461 user: match backend {
2462 "postgresql" => "pharos_parity",
2463 "mysql" => "root",
2464 _ => "",
2465 }
2466 .to_owned(),
2467 password_env_name: match backend {
2468 "postgresql" => "PHAROS_DB_PARITY_PG_PASSWORD",
2469 "mysql" => "PHAROS_TEST_MYSQL_ROOT_PASSWORD",
2470 _ => "",
2471 }
2472 .to_owned(),
2473 tls_mode: "disable".to_owned(),
2474 options: (backend == "mongodb").then(|| "directConnection=true".to_owned()),
2475 ..DbBackendConfig::default()
2476 },
2477 _ => panic!("unsupported test backend"),
2478 };
2479 let adapter = crate::runtime::database::operations::resolve(&config)
2480 .expect("configured database adapter");
2481 let contract = load_contract(&source, "ucal").expect("UCAL contract");
2482 adapter
2483 .ensure_database(&config, &contract.schema)
2484 .expect("isolated UCAL database");
2485 adapter
2486 .apply_contract(&config, &contract)
2487 .expect("UCAL logical schema and seeds");
2488 let runtime_conf = temporary.join("runtime.conf");
2489 std::fs::write(
2490 &runtime_conf,
2491 format!(
2492 "db_backend={}\ndb_path={}\ndb_host={}\ndb_port={}\ndb_name={}\ndb_user={}\ndb_password_env={}\ndb_tls_mode={}\ndb_options={}\n",
2493 config.backend,
2494 config.sqlite_path.as_ref().map(|path| path.display().to_string()).unwrap_or_default(),
2495 config.host,
2496 config.port,
2497 config.database,
2498 config.user,
2499 config.password_env_name,
2500 config.tls_mode,
2501 config.options.as_deref().unwrap_or_default(),
2502 ),
2503 )
2504 .expect("isolated runtime config");
2505 let app = AppRuntime {
2506 app_id: Some("ucal".to_owned()),
2507 app_root: Some(temporary.to_string_lossy().into_owned()),
2508 runtime_conf_path: Some(runtime_conf.to_string_lossy().into_owned()),
2509 ..AppRuntime::default()
2510 };
2511 assert!(
2512 crate::runtime::database::selected_logical_contract(&temporary, "ucal")
2513 .expect("logical app selection")
2514 .is_some()
2515 );
2516 let feature: Value = serde_json::from_slice(
2517 &std::fs::read(repository.join("apps/ucal/data/contracts/features.json"))
2518 .expect("UCAL feature contract"),
2519 )
2520 .expect("UCAL feature JSON");
2521 let transaction = &feature["actions"]["api_create_service"]["transaction"];
2522 let context = FeatureRequestContext {
2523 path_params: serde_json::from_value(json!({"business_slug":"lotus-studio"}))
2524 .expect("path parameters"),
2525 input: serde_json::from_value(json!({
2526 "name":"Logical service",
2527 "service_type":"appointment",
2528 "location_type":"business",
2529 "duration_minutes":30,
2530 "buffer_minutes":0,
2531 "cost":"15.50",
2532 "description":"Adapter-backed action",
2533 "sort_order":1,
2534 "image_url":""
2535 }))
2536 .expect("declared input"),
2537 ..FeatureRequestContext::default()
2538 };
2539 execute_logical_transaction(
2540 &app,
2541 &feature,
2542 transaction,
2543 None,
2544 context,
2545 "api_create_service",
2546 )
2547 .expect("logical select and insert");
2548 let rows = adapter
2549 .query(
2550 &config,
2551 &contract.schema,
2552 &Query {
2553 entity: "service".to_owned(),
2554 fields: vec!["id".to_owned(), "business_id".to_owned(), "cost".to_owned()],
2555 filter: Some(Predicate::Equal(
2556 "name".to_owned(),
2557 json!("Logical service"),
2558 )),
2559 order: Vec::new(),
2560 limit: 1,
2561 offset: 0,
2562 },
2563 )
2564 .expect("read inserted service");
2565 assert_eq!(rows.len(), 1);
2566 assert_eq!(rows[0]["business_id"], json!(1));
2567 assert_eq!(rows[0]["cost"], json!("15.50"));
2568 let upsert_feature = json!({"queries":{}});
2569 let upsert = json!({"steps":[{"upsert":{
2570 "entity":"business", "unique_by":["slug"], "update_on_conflict":true,
2571 "values":{"slug":"lotus-studio", "name":"Logical studio", "owner_id":{"literal":1}}
2572 }}]});
2573 execute_logical_transaction(
2574 &app,
2575 &upsert_feature,
2576 &upsert,
2577 None,
2578 FeatureRequestContext::default(),
2579 "update_business",
2580 )
2581 .expect("logical upsert updates an existing business");
2582 let insert_if_absent = json!({"steps":[{"upsert":{
2583 "entity":"business", "unique_by":["slug"],
2584 "values":{"slug":"lotus-studio", "name":"Ignored change", "owner_id":{"literal":1}}
2585 }}]});
2586 execute_logical_transaction(
2587 &app,
2588 &upsert_feature,
2589 &insert_if_absent,
2590 None,
2591 FeatureRequestContext::default(),
2592 "insert_business_if_absent",
2593 )
2594 .expect("logical insert-if-absent preserves the existing business");
2595 let businesses = adapter
2596 .query(
2597 &config,
2598 &contract.schema,
2599 &Query {
2600 entity: "business".to_owned(),
2601 fields: vec!["id".to_owned(), "name".to_owned()],
2602 filter: Some(Predicate::Equal("slug".to_owned(), json!("lotus-studio"))),
2603 order: Vec::new(),
2604 limit: 2,
2605 offset: 0,
2606 },
2607 )
2608 .expect("read upserted business");
2609 assert_eq!(businesses.len(), 1);
2610 assert_eq!(businesses[0]["name"], json!("Logical studio"));
2611 let insert_new_business = json!({"steps":[{"upsert":{
2612 "entity":"business", "unique_by":["slug"],
2613 "values":{"slug":"logical-new-studio", "name":"New studio", "owner_id":{"literal":1}}
2614 }}]});
2615 execute_logical_transaction(
2616 &app,
2617 &upsert_feature,
2618 &insert_new_business,
2619 None,
2620 FeatureRequestContext::default(),
2621 "insert_business_if_absent",
2622 )
2623 .expect("logical insert-if-absent creates a missing business");
2624 let inserted = adapter
2625 .query(
2626 &config,
2627 &contract.schema,
2628 &Query {
2629 entity: "business".to_owned(),
2630 fields: vec!["id".to_owned(), "name".to_owned()],
2631 filter: Some(Predicate::Equal(
2632 "slug".to_owned(),
2633 json!("logical-new-studio"),
2634 )),
2635 order: Vec::new(),
2636 limit: 1,
2637 offset: 0,
2638 },
2639 )
2640 .expect("read insert-if-absent business");
2641 assert_eq!(inserted.len(), 1);
2642 assert_eq!(inserted[0]["name"], json!("New studio"));
2643 let required_feature = json!({"queries":{"selected_business":{
2644 "entity":"business", "fields":["id", "slug", "name"],
2645 "filters":{"slug":"path.business_slug"}, "result":"one"
2646 }}});
2647 let guarded_update = json!({"steps":[
2648 {"require":{"query":"selected_business"}},
2649 {"update":{"entity":"business", "filters":{"id":"query.selected_business.id"},
2650 "values":{"name":"Guarded studio"}}}
2651 ]});
2652 execute_logical_transaction(
2653 &app,
2654 &required_feature,
2655 &guarded_update,
2656 None,
2657 FeatureRequestContext {
2658 path_params: serde_json::from_value(json!({"business_slug":"lotus-studio"}))
2659 .expect("business path"),
2660 ..FeatureRequestContext::default()
2661 },
2662 "update_business",
2663 )
2664 .expect("logical require binds and guards the selected business");
2665 let service_id = rows[0]["id"].as_i64().expect("created service id");
2666 let cross_business = FeatureRequestContext {
2667 path_params: serde_json::from_value(json!({
2668 "business_slug":"north-wellness", "service_id":service_id
2669 }))
2670 .expect("cross-business path"),
2671 input: serde_json::from_value(json!({
2672 "name":"Unauthorized change", "service_type":"appointment",
2673 "location_type":"business", "duration_minutes":30, "buffer_minutes":0,
2674 "cost":"0.00", "description":"", "sort_order":0, "image_url":""
2675 }))
2676 .expect("cross-business input"),
2677 ..FeatureRequestContext::default()
2678 };
2679 assert!(matches!(
2680 execute_logical_transaction(
2681 &app,
2682 &feature,
2683 &feature["actions"]["api_update_service"]["transaction"],
2684 None,
2685 cross_business,
2686 "api_update_service",
2687 ),
2688 Err(super::FeatureRuntimeError::MissingRecord(_))
2689 ));
2690
2691 fn apply(app: &AppRuntime, feature: &Value, action: &str, input: Value, selected: Value) {
2693 let mut path: serde_json::Map<String, Value> =
2694 serde_json::from_value(json!({"business_slug":"lotus-studio"}))
2695 .expect("business path");
2696 path.extend(
2697 serde_json::from_value::<serde_json::Map<String, Value>>(selected)
2698 .expect("selected path"),
2699 );
2700 let context = FeatureRequestContext {
2701 path_params: path,
2702 input: serde_json::from_value(input).expect("declared input"),
2703 ..FeatureRequestContext::default()
2704 };
2705 execute_logical_transaction(
2706 app,
2707 feature,
2708 &feature["actions"][action]["transaction"],
2709 None,
2710 context,
2711 action,
2712 )
2713 .unwrap_or_else(|error| panic!("{action}: {error:?}"));
2714 }
2715
2716 let service_input = json!({
2717 "name":"Updated service", "service_type":"appointment", "location_type":"business",
2718 "duration_minutes":45, "buffer_minutes":5, "cost":"22.00",
2719 "description":"Updated through adapter", "sort_order":2, "image_url":""
2720 });
2721 let service_path = json!({"service_id": service_id});
2722 apply(
2723 &app,
2724 &feature,
2725 "api_update_service",
2726 service_input,
2727 service_path.clone(),
2728 );
2729 apply(
2730 &app,
2731 &feature,
2732 "api_delete_service",
2733 json!({}),
2734 service_path.clone(),
2735 );
2736 apply(
2737 &app,
2738 &feature,
2739 "api_restore_service",
2740 json!({}),
2741 service_path,
2742 );
2743
2744 let availability_input =
2745 json!({"day_of_week":"Weekends","start_time":"19:00","end_time":"20:00"});
2746 apply(
2747 &app,
2748 &feature,
2749 "api_create_availability",
2750 availability_input,
2751 json!({}),
2752 );
2753 let availability = adapter
2754 .query(
2755 &config,
2756 &contract.schema,
2757 &Query {
2758 entity: "business_availability".to_owned(),
2759 fields: vec!["id".to_owned()],
2760 filter: Some(Predicate::All(vec![
2761 Predicate::Equal("business_id".to_owned(), json!(1)),
2762 Predicate::Equal("day_of_week".to_owned(), json!("Weekends")),
2763 Predicate::Equal("start_time".to_owned(), json!("19:00")),
2764 ])),
2765 order: Vec::new(),
2766 limit: 1,
2767 offset: 0,
2768 },
2769 )
2770 .expect("created availability");
2771 let availability_id = availability[0]["id"].as_i64().expect("availability id");
2772 let availability_path = json!({"availability_id":availability_id});
2773 apply(
2774 &app,
2775 &feature,
2776 "api_update_availability",
2777 json!({
2778 "day_of_week":"Weekends","start_time":"20:00","end_time":"21:00"
2779 }),
2780 availability_path.clone(),
2781 );
2782 apply(
2783 &app,
2784 &feature,
2785 "api_delete_availability",
2786 json!({}),
2787 availability_path,
2788 );
2789
2790 apply(
2791 &app,
2792 &feature,
2793 "api_create_technician",
2794 json!({
2795 "name":"Logical technician","email":"logical@example.invalid"
2796 }),
2797 json!({}),
2798 );
2799 let technicians = adapter
2800 .query(
2801 &config,
2802 &contract.schema,
2803 &Query {
2804 entity: "technician".to_owned(),
2805 fields: vec!["id".to_owned()],
2806 filter: Some(Predicate::Equal(
2807 "email".to_owned(),
2808 json!("logical@example.invalid"),
2809 )),
2810 order: Vec::new(),
2811 limit: 1,
2812 offset: 0,
2813 },
2814 )
2815 .expect("created technician");
2816 let technician_id = technicians[0]["id"].as_i64().expect("technician id");
2817 let technician_path = json!({"technician_id":technician_id});
2818 apply(
2819 &app,
2820 &feature,
2821 "api_update_technician",
2822 json!({
2823 "name":"Updated technician","email":"updated@example.invalid","is_active":true
2824 }),
2825 technician_path.clone(),
2826 );
2827 apply(
2828 &app,
2829 &feature,
2830 "api_delete_technician",
2831 json!({}),
2832 technician_path.clone(),
2833 );
2834 apply(
2835 &app,
2836 &feature,
2837 "api_restore_technician",
2838 json!({}),
2839 technician_path,
2840 );
2841 }
2842
2843 #[test]
2845 fn ucal_generated_mutations_use_logical_select_bindings() {
2846 exercise_ucal_generated_mutations("sqlite");
2847 }
2848
2849 #[test]
2851 #[ignore = "requires local PostgreSQL, MySQL, and MongoDB test containers"]
2852 fn ucal_generated_mutations_cross_backend() {
2853 for backend in ["postgresql", "mysql", "mongodb"] {
2854 exercise_ucal_generated_mutations(backend);
2855 }
2856 }
2857}