Skip to main content

sim_relation_plan/
admission.rs

1use crate::{
2    datum::{MutationDatum, QueryDatum},
3    model::*,
4};
5use sim_kernel::Symbol;
6use sim_relation_core::{
7    BindingName, ColumnName, DomainCatalog, DomainId, DomainTrait, FieldName, FieldType,
8    RelationId, RowType, TableName,
9};
10use sim_relation_schema::{Constraint, Schema, Table};
11use std::collections::{BTreeMap, BTreeSet};
12
13type Scope = BTreeMap<BindingName, RowType>;
14struct Admit<'a> {
15    schema: &'a Schema,
16    domains: &'a DomainCatalog,
17    params: &'a RowType,
18    limits: AdmissionLimits,
19}
20impl<'a> Admit<'a> {
21    fn table(&self, name: &TableName) -> Result<&'a Table, AdmissionError> {
22        self.schema
23            .tables()
24            .iter()
25            .find(|t| t.name() == name)
26            .ok_or_else(|| AdmissionError::UnknownTable(name.clone()))
27    }
28    fn table_row(&self, table: &Table, nullable: bool) -> Result<RowType, AdmissionError> {
29        RowType::new(table.columns().iter().map(|c| FieldType {
30            name: FieldName::new(c.name().symbol().clone()).expect("validated"),
31            domain: c.domain().clone(),
32            nullable: nullable || c.nullable(),
33        }))
34        .map_err(|_| AdmissionError::DuplicateName)
35    }
36    fn bind(scope: &mut Scope, name: BindingName, row: RowType) -> Result<(), AdmissionError> {
37        if scope.insert(name.clone(), row).is_some() {
38            Err(AdmissionError::AmbiguousBinding(name))
39        } else {
40            Ok(())
41        }
42    }
43    fn visible(&self, rel: &Rel, row: &RowType, scope: &mut Scope) -> Result<(), AdmissionError> {
44        match rel {
45            Rel::Scan { table, bind, .. } => Self::bind(
46                scope,
47                bind.clone(),
48                self.table_row(self.table(table)?, false)?,
49            ),
50            Rel::Values { bind, row_type, .. } => Self::bind(scope, bind.clone(), row_type.clone()),
51            Rel::Project { bind, .. } | Rel::Group { bind, .. } => {
52                Self::bind(scope, bind.clone(), row.clone())
53            }
54            Rel::Join {
55                left, right, kind, ..
56            } => {
57                let l = self.rel(left, scope)?;
58                self.visible(left, &l, scope)?;
59                let left_names: BTreeSet<_> = scope.keys().cloned().collect();
60                let r = self.rel(right, scope)?;
61                self.visible(right, &r, scope)?;
62                if *kind == JoinKind::Left {
63                    for (name, row) in scope.iter_mut() {
64                        if !left_names.contains(name) {
65                            *row = RowType::new(row.fields().iter().cloned().map(|mut field| {
66                                field.nullable = true;
67                                field
68                            }))
69                            .expect("changing nullability preserves field names");
70                        }
71                    }
72                }
73                Ok(())
74            }
75            Rel::Filter { input, .. }
76            | Rel::Distinct(input)
77            | Rel::Order { input, .. }
78            | Rel::Limit { input, .. } => self.visible(input, row, scope),
79            Rel::Set { .. } => Ok(()),
80        }
81    }
82    fn scalar(&self, value: &Scalar, scope: &Scope) -> Result<FieldType, AdmissionError> {
83        let bool_ty = || FieldType {
84            name: fname("value"),
85            domain: sim_relation_core::BaseDomain::Bool.id(),
86            nullable: false,
87        };
88        match value {
89            Scalar::Field(r) => scope
90                .get(&r.binding)
91                .ok_or_else(|| AdmissionError::UnresolvedBinding(r.binding.clone()))?
92                .fields()
93                .iter()
94                .find(|f| f.name == r.field)
95                .cloned()
96                .ok_or_else(|| AdmissionError::UnresolvedField(r.clone())),
97            Scalar::Literal(c) => Ok(FieldType {
98                name: fname("value"),
99                domain: c.domain().clone(),
100                nullable: c.value().is_none(),
101            }),
102            Scalar::Param(p) => self
103                .params
104                .fields()
105                .iter()
106                .find(|f| f.name.symbol() == p.symbol())
107                .cloned()
108                .ok_or_else(|| AdmissionError::UnresolvedParameter(p.clone())),
109            Scalar::Exists(q) => {
110                self.rel(q, scope)?;
111                Ok(bool_ty())
112            }
113            Scalar::InQuery { value, query } => {
114                let l = self.scalar(value, scope)?;
115                let q = self.rel(query, scope)?;
116                if q.fields().len() != 1 {
117                    return Err(AdmissionError::ScalarQueryArity);
118                }
119                compatible(&l, &q.fields()[0])?;
120                Ok(bool_ty())
121            }
122            Scalar::ScalarQuery(q) => {
123                let row = self.rel(q, scope)?;
124                if row.fields().len() != 1 {
125                    return Err(AdmissionError::ScalarQueryArity);
126                }
127                Ok(row.fields()[0].clone())
128            }
129            Scalar::Case {
130                branches,
131                otherwise,
132            } => {
133                let mut out = None;
134                let mut nullable = otherwise.is_none();
135                for (p, v) in branches {
136                    require_bool(&self.scalar(p, scope)?)?;
137                    let t = self.scalar(v, scope)?;
138                    if let Some(old) = &out {
139                        compatible(old, &t)?;
140                    }
141                    nullable |= t.nullable;
142                    out = Some(t);
143                }
144                if let Some(v) = otherwise {
145                    let t = self.scalar(v, scope)?;
146                    if let Some(old) = &out {
147                        compatible(old, &t)?;
148                    }
149                    nullable |= t.nullable;
150                    out = Some(t);
151                }
152                let mut out = out.ok_or(AdmissionError::TypeError("empty case"))?;
153                out.nullable = nullable;
154                Ok(out)
155            }
156            Scalar::Call(op, args) => self.call(*op, args, scope),
157        }
158    }
159    fn call(
160        &self,
161        op: ScalarOp,
162        args: &[Scalar],
163        scope: &Scope,
164    ) -> Result<FieldType, AdmissionError> {
165        let ts: Vec<_> = args
166            .iter()
167            .map(|v| self.scalar(v, scope))
168            .collect::<Result<_, _>>()?;
169        let arity = |n| {
170            if ts.len() == n {
171                Ok(())
172            } else {
173                Err(AdmissionError::TypeError("operator arity"))
174            }
175        };
176        match op {
177            ScalarOp::Not | ScalarOp::IsNull => {
178                arity(1)?;
179                if op == ScalarOp::Not {
180                    require_bool(&ts[0])?;
181                }
182                Ok(FieldType {
183                    name: fname("value"),
184                    domain: sim_relation_core::BaseDomain::Bool.id(),
185                    nullable: false,
186                })
187            }
188            ScalarOp::And | ScalarOp::Or => {
189                if ts.len() < 2 {
190                    return Err(AdmissionError::TypeError("operator arity"));
191                }
192                for t in &ts {
193                    require_bool(t)?;
194                }
195                Ok(FieldType {
196                    name: fname("value"),
197                    domain: sim_relation_core::BaseDomain::Bool.id(),
198                    nullable: ts.iter().any(|t| t.nullable),
199                })
200            }
201            ScalarOp::Eq
202            | ScalarOp::Ne
203            | ScalarOp::Lt
204            | ScalarOp::Le
205            | ScalarOp::Gt
206            | ScalarOp::Ge => {
207                arity(2)?;
208                compatible(&ts[0], &ts[1])?;
209                let needed = if matches!(op, ScalarOp::Eq | ScalarOp::Ne) {
210                    DomainTrait::Equatable
211                } else {
212                    DomainTrait::Ordered
213                };
214                require_trait(self.domains, &ts[0].domain, needed)?;
215                Ok(FieldType {
216                    name: fname("value"),
217                    domain: sim_relation_core::BaseDomain::Bool.id(),
218                    nullable: ts.iter().any(|t| t.nullable),
219                })
220            }
221            ScalarOp::Add | ScalarOp::Sub | ScalarOp::Mul | ScalarOp::Div => {
222                arity(2)?;
223                compatible(&ts[0], &ts[1])?;
224                require_trait(self.domains, &ts[0].domain, DomainTrait::Ordered)?;
225                let mut t = ts[0].clone();
226                t.nullable = ts.iter().any(|t| t.nullable);
227                Ok(t)
228            }
229            ScalarOp::Coalesce => {
230                if ts.is_empty() {
231                    return Err(AdmissionError::TypeError("empty coalesce"));
232                }
233                for t in &ts[1..] {
234                    compatible(&ts[0], t)?;
235                }
236                let mut t = ts[0].clone();
237                t.nullable = ts.iter().all(|t| t.nullable);
238                Ok(t)
239            }
240        }
241    }
242    fn aggregate(&self, a: &Aggregate, scope: &Scope) -> Result<FieldType, AdmissionError> {
243        match a {
244            Aggregate::CountAll | Aggregate::Count(_) => {
245                if let Aggregate::Count(v) = a {
246                    self.scalar(v, scope)?;
247                }
248                Ok(FieldType {
249                    name: fname("value"),
250                    domain: sim_relation_core::BaseDomain::I64.id(),
251                    nullable: false,
252                })
253            }
254            Aggregate::Sum(v) | Aggregate::Min(v) | Aggregate::Max(v) => {
255                let mut t = self.scalar(v, scope)?;
256                require_trait(self.domains, &t.domain, DomainTrait::Ordered)?;
257                t.nullable = true;
258                Ok(t)
259            }
260        }
261    }
262    fn rel(&self, rel: &Rel, outer: &Scope) -> Result<RowType, AdmissionError> {
263        match rel {
264            Rel::Scan { table, bind, .. } => {
265                let row = self.table_row(self.table(table)?, false)?;
266                let mut s = outer.clone();
267                Self::bind(&mut s, bind.clone(), row.clone())?;
268                Ok(row)
269            }
270            Rel::Values {
271                bind,
272                row_type,
273                rows,
274            } => {
275                if rows.len() > self.limits.max_literal_rows {
276                    return Err(AdmissionError::LiteralRowLimit {
277                        limit: self.limits.max_literal_rows,
278                        actual: rows.len(),
279                    });
280                }
281                if rows.iter().any(|r| r.row_type() != row_type) {
282                    return Err(AdmissionError::TypeError("values row type"));
283                }
284                let mut s = outer.clone();
285                Self::bind(&mut s, bind.clone(), row_type.clone())?;
286                Ok(row_type.clone())
287            }
288            Rel::Project {
289                input,
290                bind,
291                fields,
292            } => {
293                let input_ty = self.rel(input, outer)?;
294                let mut s = outer.clone();
295                self.visible(input, &input_ty, &mut s)?;
296                let row = named_row(fields.iter().map(|n| (&n.name, &n.scalar)), |v| {
297                    self.scalar(v, &s)
298                })?;
299                Self::bind(&mut s, bind.clone(), row.clone())?;
300                Ok(row)
301            }
302            Rel::Filter { input, predicate } => {
303                let row = self.rel(input, outer)?;
304                let mut s = outer.clone();
305                self.visible(input, &row, &mut s)?;
306                require_bool(&self.scalar(predicate, &s)?)?;
307                Ok(row)
308            }
309            Rel::Join {
310                left,
311                right,
312                kind,
313                on,
314            } => {
315                let l = self.rel(left, outer)?;
316                let mut s = outer.clone();
317                self.visible(left, &l, &mut s)?;
318                let r = self.rel(right, &s)?;
319                self.visible(right, &r, &mut s)?;
320                if *kind != JoinKind::Cross {
321                    require_bool(&self.scalar(on, &s)?)?;
322                }
323                let mut fields = Vec::new();
324                for (binding, row) in &s {
325                    if outer.contains_key(binding) {
326                        continue;
327                    }
328                    for f in row.fields() {
329                        let mut f = f.clone();
330                        f.name = FieldName::new(Symbol::qualified(
331                            binding.symbol().name.clone(),
332                            f.name.symbol().name.clone(),
333                        ))
334                        .expect("qualified");
335                        if *kind == JoinKind::Left && !scope_contains(left, binding) {
336                            f.nullable = true;
337                        }
338                        fields.push(f);
339                    }
340                }
341                RowType::new(fields).map_err(|_| AdmissionError::DuplicateName)
342            }
343            Rel::Group {
344                input,
345                bind,
346                keys,
347                aggregates,
348                having,
349            } => {
350                let inrow = self.rel(input, outer)?;
351                let mut s = outer.clone();
352                self.visible(input, &inrow, &mut s)?;
353                let mut fields = Vec::new();
354                for n in keys {
355                    let mut t = self.scalar(&n.scalar, &s)?;
356                    t.name = n.name.clone();
357                    fields.push(t);
358                }
359                for n in aggregates {
360                    let mut t = self.aggregate(&n.aggregate, &s)?;
361                    t.name = n.name.clone();
362                    fields.push(t);
363                }
364                let row = RowType::new(fields).map_err(|_| AdmissionError::DuplicateName)?;
365                let mut hs = outer.clone();
366                Self::bind(&mut hs, bind.clone(), row.clone())?;
367                if let Some(v) = having {
368                    require_bool(&self.scalar(v, &hs)?)?;
369                }
370                Ok(row)
371            }
372            Rel::Set { inputs, .. } => {
373                if inputs.len() < 2 {
374                    return Err(AdmissionError::IncompatibleSet);
375                }
376                let first = self.rel(&inputs[0], outer)?;
377                for v in &inputs[1..] {
378                    let r = self.rel(v, outer)?;
379                    if r.fields().len() != first.fields().len() {
380                        return Err(AdmissionError::IncompatibleSet);
381                    }
382                    for (a, b) in first.fields().iter().zip(r.fields()) {
383                        compatible(a, b).map_err(|_| AdmissionError::IncompatibleSet)?;
384                    }
385                }
386                Ok(first)
387            }
388            Rel::Distinct(v) => self.rel(v, outer),
389            Rel::Order { input, keys } => {
390                let row = self.rel(input, outer)?;
391                let mut s = outer.clone();
392                self.visible(input, &row, &mut s)?;
393                for k in keys {
394                    let t = self.scalar(&k.scalar, &s)?;
395                    require_trait(self.domains, &t.domain, DomainTrait::Ordered)?;
396                }
397                Ok(row)
398            }
399            Rel::Limit { input, .. } => self.rel(input, outer),
400        }
401    }
402}
403
404/// Admits a complete query against immutable schema, catalog, and parameter contracts.
405pub fn admit_query(
406    raw: Rel,
407    schema: &Schema,
408    domains: &DomainCatalog,
409    parameters: RowType,
410    limits: AdmissionLimits,
411) -> Result<CheckedQuery, AdmissionError> {
412    let a = Admit {
413        schema,
414        domains,
415        params: &parameters,
416        limits,
417    };
418    let output = a.rel(&raw, &Scope::new())?;
419    let schema_id = schema
420        .id()
421        .map_err(|_| AdmissionError::TypeError("schema identity"))?;
422    let catalog_id =
423        RelationId::of(domains).map_err(|_| AdmissionError::TypeError("catalog identity"))?;
424    let plan_id = RelationId::of(&QueryDatum {
425        raw: &raw,
426        parameters: &parameters,
427    })
428    .map_err(|_| AdmissionError::TypeError("plan identity"))?;
429    Ok(CheckedQuery {
430        schema_id,
431        catalog_id,
432        parameters,
433        output,
434        plan_id,
435        raw,
436    })
437}
438/// Admits a complete insert, update, or delete plan.
439pub fn admit_mutation(
440    raw: Mutation,
441    schema: &Schema,
442    domains: &DomainCatalog,
443    parameters: RowType,
444    limits: AdmissionLimits,
445) -> Result<CheckedMutation, AdmissionError> {
446    let a = Admit {
447        schema,
448        domains,
449        params: &parameters,
450        limits,
451    };
452    let mut scope = Scope::new();
453    let output = match &raw {
454        Mutation::Insert {
455            table,
456            columns,
457            input,
458            conflict,
459            returning,
460        } => {
461            let t = a.table(table)?;
462            let input_ty = a.rel(input, &scope)?;
463            if columns.len() != input_ty.fields().len() {
464                return Err(AdmissionError::TypeError("insert arity"));
465            }
466            let mut seen = BTreeSet::new();
467            for (c, f) in columns.iter().zip(input_ty.fields()) {
468                if !seen.insert(c) {
469                    return Err(AdmissionError::DuplicateName);
470                }
471                let target = t
472                    .columns()
473                    .iter()
474                    .find(|x| x.name() == c)
475                    .ok_or(AdmissionError::TypeError("unknown insert column"))?;
476                compatible(
477                    &FieldType {
478                        name: fname("target"),
479                        domain: target.domain().clone(),
480                        nullable: target.nullable(),
481                    },
482                    f,
483                )?;
484            }
485            for c in t.columns() {
486                if !c.nullable()
487                    && !c.has_default()
488                    && !c.is_generated()
489                    && !columns.contains(c.name())
490                {
491                    return Err(AdmissionError::MissingRequiredInsertField(c.name().clone()));
492                }
493            }
494            validate_conflict(conflict, t, &a, &mut scope)?;
495            if !scope.contains_key(&bname("target")) {
496                let target_row = a.table_row(t, false)?;
497                Admit::bind(&mut scope, bname("target"), target_row)?;
498            }
499            named_row(returning.iter().map(|n| (&n.name, &n.scalar)), |v| {
500                a.scalar(v, &scope)
501            })?
502        }
503        Mutation::Update {
504            table,
505            bind,
506            assignments,
507            predicate,
508            returning,
509        } => {
510            let t = a.table(table)?;
511            Admit::bind(&mut scope, bind.clone(), a.table_row(t, false)?)?;
512            validate_assignments(assignments, t, &a, &scope)?;
513            if let Some(v) = predicate {
514                require_bool(&a.scalar(v, &scope)?)?;
515            }
516            named_row(returning.iter().map(|n| (&n.name, &n.scalar)), |v| {
517                a.scalar(v, &scope)
518            })?
519        }
520        Mutation::Delete {
521            table,
522            bind,
523            predicate,
524            returning,
525        } => {
526            let t = a.table(table)?;
527            Admit::bind(&mut scope, bind.clone(), a.table_row(t, false)?)?;
528            if let Some(v) = predicate {
529                require_bool(&a.scalar(v, &scope)?)?;
530            }
531            named_row(returning.iter().map(|n| (&n.name, &n.scalar)), |v| {
532                a.scalar(v, &scope)
533            })?
534        }
535    };
536    let schema_id = schema
537        .id()
538        .map_err(|_| AdmissionError::TypeError("schema identity"))?;
539    let catalog_id =
540        RelationId::of(domains).map_err(|_| AdmissionError::TypeError("catalog identity"))?;
541    let plan_id = RelationId::of(&MutationDatum(&raw))
542        .map_err(|_| AdmissionError::TypeError("plan identity"))?;
543    Ok(CheckedMutation {
544        schema_id,
545        catalog_id,
546        parameters,
547        output,
548        plan_id,
549        raw,
550    })
551}
552
553fn validate_assignments(
554    v: &[(ColumnName, Scalar)],
555    t: &Table,
556    a: &Admit<'_>,
557    s: &Scope,
558) -> Result<(), AdmissionError> {
559    let mut seen = BTreeSet::new();
560    for (c, x) in v {
561        if !seen.insert(c) {
562            return Err(AdmissionError::DuplicateName);
563        }
564        let col = t
565            .columns()
566            .iter()
567            .find(|z| z.name() == c)
568            .ok_or(AdmissionError::TypeError("unknown assignment column"))?;
569        compatible(
570            &FieldType {
571                name: fname("target"),
572                domain: col.domain().clone(),
573                nullable: col.nullable(),
574            },
575            &a.scalar(x, s)?,
576        )?;
577    }
578    Ok(())
579}
580fn validate_conflict(
581    c: &ConflictAction,
582    t: &Table,
583    a: &Admit<'_>,
584    s: &mut Scope,
585) -> Result<(), AdmissionError> {
586    let (target, updates, pred) = match c {
587        ConflictAction::Fail => return Ok(()),
588        ConflictAction::DoNothing { target } => (target, None, None),
589        ConflictAction::DoUpdate {
590            target,
591            assignments,
592            predicate,
593        } => (target, Some(assignments), predicate.as_ref()),
594    };
595    let valid = match target {
596        ConflictTarget::PrimaryKey => t
597            .constraints()
598            .iter()
599            .any(|c| matches!(c, Constraint::Primary(_))),
600        ConflictTarget::UniqueConstraint(n) => t
601            .constraints()
602            .iter()
603            .any(|c| matches!(c,Constraint::Unique(v)if &v.name==n)),
604        ConflictTarget::Columns(cols) => t.constraints().iter().any(|c| match c {
605            Constraint::Primary(v) => v.columns == *cols,
606            Constraint::Unique(v) => v.columns == *cols,
607            _ => false,
608        }),
609    };
610    if !valid {
611        return Err(AdmissionError::UnsafeConflictTarget);
612    }
613    if let Some(v) = updates {
614        Admit::bind(s, bname("target"), a.table_row(t, false)?)?;
615        Admit::bind(s, bname("excluded"), a.table_row(t, false)?)?;
616        validate_assignments(v, t, a, s)?;
617        if let Some(p) = pred {
618            require_bool(&a.scalar(p, s)?)?;
619        }
620    }
621    Ok(())
622}
623fn named_row<'a, T: 'a>(
624    values: impl Iterator<Item = (&'a FieldName, &'a T)>,
625    mut check: impl FnMut(&T) -> Result<FieldType, AdmissionError>,
626) -> Result<RowType, AdmissionError> {
627    let mut out = Vec::new();
628    for (n, v) in values {
629        let mut t = check(v)?;
630        t.name = n.clone();
631        out.push(t);
632    }
633    RowType::new(out).map_err(|_| AdmissionError::DuplicateName)
634}
635fn compatible(a: &FieldType, b: &FieldType) -> Result<(), AdmissionError> {
636    if a.domain == b.domain {
637        Ok(())
638    } else {
639        Err(AdmissionError::TypeError("domain mismatch"))
640    }
641}
642fn require_bool(v: &FieldType) -> Result<(), AdmissionError> {
643    if v.domain == sim_relation_core::BaseDomain::Bool.id() {
644        Ok(())
645    } else {
646        Err(AdmissionError::TypeError("boolean required"))
647    }
648}
649fn require_trait(c: &DomainCatalog, d: &DomainId, t: DomainTrait) -> Result<(), AdmissionError> {
650    if c.get(d).is_some_and(|x| x.traits().contains(&t)) {
651        Ok(())
652    } else {
653        Err(AdmissionError::TypeError("domain trait missing"))
654    }
655}
656fn scope_contains(rel: &Rel, binding: &BindingName) -> bool {
657    match rel {
658        Rel::Scan { bind, .. }
659        | Rel::Values { bind, .. }
660        | Rel::Project { bind, .. }
661        | Rel::Group { bind, .. } => bind == binding,
662        Rel::Join { left, right, .. } => {
663            scope_contains(left, binding) || scope_contains(right, binding)
664        }
665        Rel::Filter { input, .. }
666        | Rel::Distinct(input)
667        | Rel::Order { input, .. }
668        | Rel::Limit { input, .. } => scope_contains(input, binding),
669        Rel::Set { .. } => false,
670    }
671}
672fn fname(v: &str) -> FieldName {
673    FieldName::new(Symbol::new(v)).expect("literal")
674}
675fn bname(v: &str) -> BindingName {
676    BindingName::new(Symbol::new(v)).expect("literal")
677}