Skip to main content

pharos_native/runtime/feature_runtime/
logical.rs

1//! Declared feature execution through the configured logical database adapter.
2
3use 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
20/// Keeps one selected adapter and schema for the whole declared feature call.
21enum 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
49/// Opens the app's logical entity catalog through its configured backend.
50fn 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
70/// Preserves the logical database error category at the declared feature boundary.
71pub(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
84/// Converts HTML form booleans into the logical field type before any adapter sees them.
85fn 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
122/// Resolves one declared field filter into a typed backend-neutral predicate.
123pub(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
205/// Resolves declared reservation filters without losing an ensured row identity.
206fn 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
251/// Reuses a bounded, trusted request-local query binding without another storage read.
252/// Persistence still checks its captured row values inside the mutation transaction.
253fn 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
287/// Executes declared feature queries without compiling SQL in the request path.
288pub(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
564/// Compiles declared write values while preserving a prior generated-id binding.
565fn 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
603/// Distinguishes unrelated prospective writes from changes to a query's own entity.
604fn 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
616/// Matches a declared equality filter against a transaction-local write.
617fn 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
675/// Completes a planned insert using declared defaults so local reads match its write.
676fn 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
710/// Captures an ensured row through the interface and guards its snapshot before writes.
711fn 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
774/// Builds a bounded reservation status transition from app-owned declarations.
775fn 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
869/// Reuses a saved result only when every declared idempotency field still matches.
870fn 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
900/// Binds a read set in bounded groups without consuming one transaction step per row.
901/// Grouping requires complete identities so matching duplicates cannot hide a stale row.
902fn 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/// Checks declared guards before writes and binds their read set into the transaction.
963#[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
1044/// Finds only declared query references used by one bounded operation.
1045fn 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
1078/// Owns bounded query-dependency resolution while a transaction is planned.
1079struct 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    /// Resolves a declared read and guards it in the eventual mutation transaction.
1091    /// Generated bindings remain transaction-local and never trigger stale reads.
1092    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
1159/// Executes declared writes and guards in one adapter transaction.
1160pub(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                    // The earlier insert or ensure step already owns the record in this
1811                    // transaction. Re-reading it on another connection would be stale.
1812                    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    /// Large declared source sets retain all read assertions in one transaction without widening the per-batch database bound.
2146    #[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    /// Blank optional references become NULL while malformed integer identities remain invalid.
2183    #[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    /// Operational and configuration failures retain their exact public error category.
2235    #[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    /// Cached working rows bypass an unavailable database until a guarded commit needs it.
2251    #[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    /// Copying a transaction-local row preserves its typed generated expression.
2318    #[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    /// Keeps a declared cart cap and its checked row set in one adapter transaction.
2349    #[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    /// Runs every UCAL generated mutation against one configured logical adapter.
2418    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        /// Executes one UCAL action with the selected record and declared input.
2692        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    /// Keeps the local SQLite action suite in the default unit-test gate.
2844    #[test]
2845    fn ucal_generated_mutations_use_logical_select_bindings() {
2846        exercise_ucal_generated_mutations("sqlite");
2847    }
2848
2849    /// Certifies the same UCAL actions against the three local service adapters.
2850    #[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}