Skip to main content

uqa_sql/routines/
attributes.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The checks `PostgreSQL` makes of routine attributes, each at its own stage: `compute_common_attribute` for each clause in written order, the values `compute_function_attributes` and `AlterFunction` interpret, and the language, transforms, body and ROWS that `CreateFunction` examines.
8
9use super::declaration::RoutineTypeCatalog;
10use crate::ast::{
11    ColumnType, CreateFunction, FunctionBody, RoutineAttributeClause, RoutineAttributeClauses,
12    RoutineBodyError,
13};
14use crate::SQLError;
15
16fn definition_error(message: impl Into<String>) -> SQLError {
17    SQLError::Diagnostic {
18        sqlstate: "42P13".into(),
19        message: message.into(),
20        detail: None,
21        hint: None,
22    }
23}
24
25fn routine_error(sqlstate: &str, message: impl Into<String>) -> SQLError {
26    SQLError::Diagnostic {
27        sqlstate: sqlstate.into(),
28        message: message.into(),
29        detail: None,
30        hint: None,
31    }
32}
33
34/// Reject the first clause, in written order, that a procedure cannot have or that repeats an earlier clause, as `compute_common_attribute` and `compute_function_attributes` do.
35pub fn check_attribute_clauses(
36    clauses: &RoutineAttributeClauses,
37    is_procedure: bool,
38) -> Result<(), SQLError> {
39    let mut seen = Vec::<RoutineAttributeClause>::new();
40    for &clause in &clauses.clauses {
41        if is_procedure && clause.rejected_by_procedures() {
42            return Err(definition_error(
43                "invalid attribute in procedure definition",
44            ));
45        }
46        if !clause.repeatable() && seen.contains(&clause) {
47            return Err(routine_error("42601", "conflicting or redundant options"));
48        }
49        seen.push(clause);
50    }
51    Ok(())
52}
53
54/// `COST must be positive`, which `PostgreSQL` checks of the value it stores in a `float4`.
55pub fn validate_cost(cost: Option<f32>) -> Result<(), SQLError> {
56    if cost.is_some_and(|cost| cost <= 0.0) {
57        return Err(routine_error("22023", "COST must be positive"));
58    }
59    Ok(())
60}
61
62/// `ROWS must be positive`, which `PostgreSQL` checks of the value it stores in a `float4`.
63pub fn validate_rows(rows: Option<f32>) -> Result<(), SQLError> {
64    if rows.is_some_and(|rows| rows <= 0.0) {
65        return Err(routine_error("22023", "ROWS must be positive"));
66    }
67    Ok(())
68}
69
70/// `ROWS is not applicable when function does not return a set`.
71pub fn validate_rows_applicability(rows: Option<f32>, returns_set: bool) -> Result<(), SQLError> {
72    if rows.is_some() && !returns_set {
73        return Err(routine_error(
74            "22023",
75            "ROWS is not applicable when function does not return a set",
76        ));
77    }
78    Ok(())
79}
80
81/// The PARALLEL value `interpret_func_parallel` accepts.
82pub fn validate_parallel(clauses: &RoutineAttributeClauses) -> Result<(), SQLError> {
83    if clauses.invalid_parallel.is_some() {
84        return Err(routine_error(
85            "42601",
86            "parameter \"parallel\" must be SAFE, RESTRICTED, or UNSAFE",
87        ));
88    }
89    Ok(())
90}
91
92/// The language `CreateFunction` looks up: a routine without one reports `no language specified`, and only SQL and PL/pgSQL exist.
93pub fn validate_routine_language(def: &CreateFunction) -> Result<(), SQLError> {
94    if def.language.is_empty() {
95        return Err(definition_error("no language specified"));
96    }
97    if !matches!(def.language.as_str(), "plpgsql" | "sql") {
98        return Err(routine_error(
99            "42704",
100            format!("language \"{}\" does not exist", def.language),
101        ));
102    }
103    Ok(())
104}
105
106/// The transforms of `TRANSFORM FOR TYPE`, which `CreateFunction` looks up for the routine's language after it checks LEAKPROOF: each type must exist, and the first reports that no transform exists for its element type, since no transform exists for any type.
107pub fn validate_transforms(
108    catalog: &dyn RoutineTypeCatalog,
109    def: &CreateFunction,
110    clauses: &RoutineAttributeClauses,
111) -> Result<(), SQLError> {
112    let Some(transform) = clauses.transform_types.first() else {
113        return Ok(());
114    };
115    let ty = catalog
116        .resolve_catalog_column_type(&transform.type_name)
117        .ok_or_else(|| {
118            routine_error(
119                "42704",
120                format!("type \"{}\" does not exist", transform.written),
121            )
122        })?;
123    // `get_base_element_type`: an array, or a domain over one, names its element type.
124    let element = match &ty {
125        ColumnType::Array(element) => element.as_ref(),
126        ColumnType::Domain { base, .. } => match base.as_ref() {
127            ColumnType::Array(element) => element.as_ref(),
128            _ => &ty,
129        },
130        _ => &ty,
131    };
132    Err(routine_error(
133        "42704",
134        format!(
135            "transform for type {} language \"{}\" does not exist",
136            catalog.format_type(element)?,
137            def.language
138        ),
139    ))
140}
141
142/// The body `interpret_AS_clause` accepts: exactly one of an AS body and a SQL-standard body, a SQL-standard body only for SQL, and one AS item for the languages here.
143pub fn validate_body_form(
144    def: &CreateFunction,
145    clauses: &RoutineAttributeClauses,
146) -> Result<(), SQLError> {
147    match clauses.body_error {
148        Some(RoutineBodyError::Missing) => {
149            return Err(definition_error("no function body specified"));
150        }
151        Some(RoutineBodyError::Duplicate) => {
152            return Err(definition_error("duplicate function body specified"));
153        }
154        Some(RoutineBodyError::ExtraAsItems) | None => {}
155    }
156    if matches!(def.body, FunctionBody::Statements(_)) && def.language != "sql" {
157        return Err(definition_error(
158            "inline SQL function body only valid for language SQL",
159        ));
160    }
161    if clauses.body_error == Some(RoutineBodyError::ExtraAsItems) {
162        return Err(definition_error(format!(
163            "only one AS item needed for language \"{}\"",
164            def.language
165        )));
166    }
167    Ok(())
168}
169
170/// A window function, which `PostgreSQL` creates where every other check passes and which this engine cannot run.
171pub fn reject_window_function(
172    def: &CreateFunction,
173    clauses: &RoutineAttributeClauses,
174) -> Result<(), SQLError> {
175    if clauses.clauses.contains(&RoutineAttributeClause::Window) {
176        return Err(SQLError::Unsupported(format!(
177            "{}: WINDOW functions",
178            if def.is_procedure {
179                "CREATE PROCEDURE"
180            } else {
181                "CREATE FUNCTION"
182            }
183        )));
184    }
185    Ok(())
186}