Skip to main content

pharos_runtime_database/
seed_from.rs

1//! Bounded, owner-neutral missing-record adoption shared by every adapter.
2
3use serde_json::{Map, Value};
4
5use crate::operations::{
6    AppSchema, DatabaseError, Query, SchemaChange, entity, validate_field_value,
7    validate_predicate, validate_query,
8};
9
10/// Validates source mappings and constructs bounded target and source reads.
11pub fn queries(schema: &AppSchema, change: &SchemaChange) -> Result<(Query, Query), DatabaseError> {
12    let SchemaChange::SeedFrom {
13        entity: name,
14        absent_filter,
15        source_entity,
16        source_filter,
17        fields,
18        values,
19    } = change
20    else {
21        return Err(DatabaseError::InvalidContract);
22    };
23    let target = entity(schema, name)?;
24    let source = entity(schema, source_entity)?;
25    validate_predicate(target, absent_filter)?;
26    validate_predicate(source, source_filter)?;
27    let mut selection = std::collections::BTreeMap::new();
28    selection_values(absent_filter, &mut selection)?;
29    if selection
30        .iter()
31        .any(|(name, value)| values.get(*name) != Some(*value))
32        || !(target
33            .unique
34            .iter()
35            .any(|key| key.iter().all(|name| selection.contains_key(name.as_str())))
36            || target
37                .identity
38                .fields()
39                .iter()
40                .all(|name| selection.contains_key(*name)))
41    {
42        return Err(DatabaseError::InvalidContract);
43    }
44    if fields.is_empty() || fields.len() + values.len() > 64 {
45        return Err(DatabaseError::InvalidContract);
46    }
47    for (target_name, source_name) in fields {
48        let target_field = target
49            .fields
50            .iter()
51            .find(|field| field.name == *target_name)
52            .ok_or(DatabaseError::InvalidContract)?;
53        let source_field = source
54            .fields
55            .iter()
56            .find(|field| field.name == *source_name)
57            .ok_or(DatabaseError::InvalidContract)?;
58        if values.contains_key(target_name)
59            || target_field.kind != source_field.kind
60            || (!target_field.nullable && source_field.nullable)
61        {
62            return Err(DatabaseError::InvalidContract);
63        }
64    }
65    for (name, value) in values {
66        let field = target
67            .fields
68            .iter()
69            .find(|field| field.name == *name)
70            .ok_or(DatabaseError::InvalidContract)?;
71        validate_field_value(field, value)?;
72    }
73    for field in &target.fields {
74        if !field.nullable
75            && field.default.is_none()
76            && target.identity.generated_field() != Some(field.name.as_str())
77            && !fields.contains_key(&field.name)
78            && !values.contains_key(&field.name)
79        {
80            return Err(DatabaseError::InvalidContract);
81        }
82    }
83    let target_query = Query {
84        entity: name.clone(),
85        fields: target
86            .identity
87            .fields()
88            .into_iter()
89            .map(str::to_owned)
90            .collect(),
91        filter: Some(absent_filter.clone()),
92        order: vec![],
93        limit: 1,
94        offset: 0,
95    };
96    let source_query = Query {
97        entity: source_entity.clone(),
98        fields: fields
99            .values()
100            .cloned()
101            .collect::<std::collections::BTreeSet<_>>()
102            .into_iter()
103            .collect(),
104        filter: Some(source_filter.clone()),
105        order: vec![],
106        limit: 2,
107        offset: 0,
108    };
109    validate_query(schema, &target_query)?;
110    validate_query(schema, &source_query)?;
111    Ok((target_query, source_query))
112}
113
114/// Requires a stable unique literal key so retry cannot create a second target.
115fn selection_values<'a>(
116    predicate: &'a crate::operations::Predicate,
117    values: &mut std::collections::BTreeMap<&'a str, &'a Value>,
118) -> Result<(), DatabaseError> {
119    match predicate {
120        crate::operations::Predicate::Equal(name, value) => {
121            if value.is_null() || values.insert(name, value).is_some() {
122                return Err(DatabaseError::InvalidContract);
123            }
124        }
125        crate::operations::Predicate::All(items) => {
126            for item in items {
127                selection_values(item, values)?;
128            }
129        }
130        _ => return Err(DatabaseError::InvalidContract),
131    }
132    Ok(())
133}
134
135/// Materializes a seed only after exact-one source admission and typed validation.
136pub fn seed(
137    schema: &AppSchema,
138    change: &SchemaChange,
139    rows: &[Map<String, Value>],
140) -> Result<SchemaChange, DatabaseError> {
141    queries(schema, change)?;
142    let SchemaChange::SeedFrom {
143        entity: name,
144        fields,
145        values,
146        ..
147    } = change
148    else {
149        return Err(DatabaseError::InvalidContract);
150    };
151    let [row] = rows else {
152        return Err(DatabaseError::Conflict);
153    };
154    let target = entity(schema, name)?;
155    let mut values = values.clone();
156    for (target_name, source_name) in fields {
157        let value = row.get(source_name).ok_or(DatabaseError::Conflict)?;
158        let field = target
159            .fields
160            .iter()
161            .find(|field| field.name == *target_name)
162            .ok_or(DatabaseError::InvalidContract)?;
163        validate_field_value(field, value)?;
164        values.insert(target_name.clone(), value.clone());
165    }
166    Ok(SchemaChange::Seed {
167        entity: name.clone(),
168        values,
169    })
170}