diff --git a/crates/rupg-analyze/src/ddl/function.rs b/crates/rupg-analyze/src/ddl/function.rs index 3bda5e9..077620a 100644 --- a/crates/rupg-analyze/src/ddl/function.rs +++ b/crates/rupg-analyze/src/ddl/function.rs @@ -1,9 +1,10 @@ -//! `CreateFunction`: `CREATE FUNCTION` for a function in SQL with a `RETURN` body, as `interpret_AS_clause` and `ProcedureCreate` run it. +//! `CreateFunction`: `CREATE FUNCTION` for a function in SQL, as `interpret_AS_clause`, `ProcedureCreate` and `fmgr_sql_validator` run it. The body is a `RETURN` expression, a query in `BEGIN ATOMIC` or a query in a string. //! -//! The catalog keeps the text of the statement and the schemas of `search_path`, and the analyzer reads the body again each time a query calls the function, as it reads the query of a view. Only the setup of a new cluster runs `CREATE FUNCTION`, for the functions of `information_schema`. +//! The catalog keeps the text of the statement and the schemas of `search_path`, and the analyzer reads the body again each time a query calls the function, as it reads the query of a view. The analysis of a call uses the types of the arguments of the call, so a polymorphic argument has its actual type. Only the setup of a new cluster runs `CREATE FUNCTION`, for the functions of `information_schema`. use rupg_catalog::{Function, FunctionBody, ObjRef, PG_PROC}; use rupg_common::{Error, Result, SqlState}; +use rupg_pgcatalog::builtin; use rupg_sql::nodes::{CreateFunctionStmt, DefElem, FunctionParameterMode, Node}; use rupg_types::oid; @@ -11,13 +12,25 @@ use super::{Definer, not_yet, references}; use crate::agg::Kind; use crate::coerce::Context; use crate::expr::{Expr, ExprKind}; +use crate::select::Query; use crate::typename::{names, place}; -use crate::{Analyzer, Env, Params, types}; +use crate::{Analyzer, Env, Params, poly, types}; /// The OID of the language `sql`. const SQL_LANGUAGE: u32 = 14; /// The default `procost` of a function in a language other than C and `internal`. const DEFAULT_COST: u32 = 100; +/// The default `prorows` of a function that returns a set. +const DEFAULT_ROWS: u32 = 1000; + +/// The body of a function in SQL, as the analyzer reads it for the types of the arguments of a call. +#[derive(Debug)] +pub enum Body { + /// A `RETURN` body: the value of the function, with `$1` as the first argument. + Value(Expr), + /// A body with a query, in `BEGIN ATOMIC` or in a string. A function that returns a set gives the rows of the query, and another function gives the first column of the first row, or null when the query gives no row. + Query(Box), +} /// The facts of the options of `CREATE FUNCTION`. struct Options { @@ -25,6 +38,10 @@ struct Options { volatile: u8, parallel: u8, cost: u32, + rows: Option, + support: Option>, + /// The body in a string, from `AS`. + src: Option, } /// The text of the argument of an option. @@ -35,9 +52,31 @@ fn option_text(def: &DefElem) -> Option<&str> { } } +/// The texts of the argument of an option that takes a list of names or of strings. +fn option_list(def: &DefElem) -> Option> { + match &def.arg { + Some(Node::List(list)) => list + .iter() + .map(|n| match n { + Some(Node::String(s)) => Some(s.to_string()), + _ => None, + }) + .collect(), + _ => None, + } +} + /// `compute_function_attributes`: the options of the function. Each option that rupg does not have gives an error. fn options(stmt: &CreateFunctionStmt) -> Result { - let mut out = Options { strict: false, volatile: b'v', parallel: b'u', cost: DEFAULT_COST }; + let mut out = Options { + strict: false, + volatile: b'v', + parallel: b'u', + cost: DEFAULT_COST, + rows: None, + support: None, + src: None, + }; for def in stmt.options.iter().flatten() { let Node::DefElem(def) = def else { continue }; let name = def.defname.as_deref().unwrap_or_default(); @@ -59,21 +98,95 @@ fn options(stmt: &CreateFunctionStmt) -> Result { Some(Node::Integer(cost)) if cost > 0 => out.cost = cost.unsigned_abs(), _ => return Err(bad()), }, + ("rows", _) => match def.arg { + Some(Node::Integer(rows)) if rows > 0 => out.rows = Some(rows.unsigned_abs()), + _ => return Err(bad()), + }, + ("support", _) => out.support = Some(option_list(def).ok_or_else(bad)?), + ("as", _) => match option_list(def).as_deref() { + Some([src]) => out.src = Some(src.clone()), + _ => return Err(bad()), + }, _ => return Err(bad()), } } Ok(out) } -/// The `RETURN` expression of the body of a function, or an error for a body of another form. -fn return_value(stmt: &CreateFunctionStmt) -> Result> { - match &stmt.sql_body { - Some(Node::ReturnStmt(ret)) => Ok(ret.returnval.as_ref()), - Some(_) => Err(not_yet("a function body with BEGIN ATOMIC", None)), - None => Err(not_yet("a function body in a string", None)), +/// The statements of a body in SQL, from the statement that made the function: the `RETURN` expression, the statements of `BEGIN ATOMIC`, or the statements of the string of `AS`. +enum Statements<'s> { + Return(Option<&'s Node>), + Atomic(Vec<&'s Node>), + String(Vec), +} + +/// The body of the statement. +fn statements<'s>(stmt: &'s CreateFunctionStmt, src: Option<&str>) -> Result> { + match (&stmt.sql_body, src) { + (Some(Node::ReturnStmt(ret)), None) => Ok(Statements::Return(ret.returnval.as_ref())), + (Some(Node::List(outer)), None) => { + let Some(Some(Node::List(list))) = outer.first() else { + return Err(Error::internal("a BEGIN ATOMIC body that is not a list")); + }; + Ok(Statements::Atomic(list.iter().flatten().collect())) + } + (Some(_), Some(_)) => Err(Error::new( + SqlState::INVALID_FUNCTION_DEFINITION, + "duplicate function body specified", + )), + (Some(_), None) => Err(Error::internal("a function body of an unknown form")), + (None, Some(src)) => { + let (list, _) = rupg_sql::parse(src).map_err(Error::from)?; + let stmts = list + .into_iter() + .flatten() + .filter_map(|node| match node { + Node::RawStmt(raw) => raw.stmt, + _ => None, + }) + .collect(); + Ok(Statements::String(stmts)) + } + (None, None) => { + Err(Error::new(SqlState::INVALID_FUNCTION_DEFINITION, "no function body specified")) + } } } +/// The types of the columns of the result of a function: the `OUT` parameters of a function that returns `record`, or else the result type. A polymorphic type takes the actual type that `inputs`, the types of the arguments of a call, give. A function that returns `record` with no `OUT` parameter has no known columns. +fn result_types(function: &Function, inputs: &[u32]) -> Result> { + let declared: Vec = if function.rettype == oid::RECORD { + function + .allargtypes + .iter() + .zip(&function.argmodes) + .filter(|(_, mode)| matches!(mode, b'o' | b'b' | b't')) + .map(|(ty, _)| *ty) + .collect() + } else { + vec![function.rettype] + }; + declared + .into_iter() + .map(|ty| { + if types::is_polymorphic(ty) { + poly::resolve(inputs, &mut function.argtypes.clone(), ty) + } else { + Ok(ty) + } + }) + .collect() +} + +/// The error of `check_sql_fn_retval` for a query with the wrong columns. +fn mismatch(rettype: u32, detail: String) -> Error { + Error::new( + SqlState::INVALID_FUNCTION_DEFINITION, + format!("return type mismatch in function declared to return {}", types::name(rettype)), + ) + .with_detail(detail) +} + /// The objects that the body refers to, as `recordDependencyOnExpr` records them: the functions that it calls, and the objects that [`references`] finds. The catalog drops the references to pinned objects. fn body_references(body: &Expr) -> Vec { let mut refs = Vec::new(); @@ -103,24 +216,110 @@ impl Analyzer<'_> { } let actual = expr.ty; self.coerce_to_target(expr, rettype, -1, Context::Assignment, None)?.ok_or_else(|| { + mismatch(rettype, format!("Actual return type is {}.", types::name(actual))) + }) + } + + /// The query of the last statement of a body, as `check_sql_fn_retval` checks it: the columns of the query must be the columns of the result, and each column takes an assignment cast to the type of the result column. The other statements give an error, as rupg runs only one query. + fn query_body(&mut self, stmts: &[&Node], rettype: u32, columns: &[u32]) -> Result { + let [Node::SelectStmt(select)] = stmts else { + return Err(not_yet("a function body that is not one SELECT", None)); + }; + let mut query = self.select(select)?; + query.params.clone_from(&self.params); + let width = query.targets.iter().take_while(|t| !t.junk).count(); + if rettype != oid::RECORD && width != 1 { + return Err(mismatch( + rettype, + "Final statement must return exactly one column.".into(), + )); + } + if rettype == oid::RECORD && !columns.is_empty() && width != columns.len() { + let what = if width > columns.len() { "many" } else { "few" }; + return Err(mismatch(rettype, format!("Final statement returns too {what} columns."))); + } + for (i, &ty) in columns.iter().enumerate() { + let target = &mut query.targets[i]; + let actual = target.expr.ty; + if actual == ty { + continue; + } + let sorted = query.sort.iter().chain(&query.group).chain(&query.distinct); + if sorted.into_iter().any(|s| s.target == i) { + return Err(not_yet("a cast of a sorted column of the result of a function", None)); + } + let cast = + self.coerce_to_target(target.expr.clone(), ty, -1, Context::Assignment, None)?; + target.expr = cast.ok_or_else(|| { + mismatch( + rettype, + format!( + "Final statement returns {} instead of {} at column {}.", + types::name(actual), + types::name(ty), + i + 1 + ), + ) + })?; + } + Ok(query) + } + + /// The body of a function for a call with arguments of the types `inputs`. + fn body( + &mut self, + function: &Function, + stmts: &Statements<'_>, + inputs: &[u32], + ) -> Result { + let columns = result_types(function, inputs)?; + let rettype = match columns.as_slice() { + [ty] if function.rettype != oid::RECORD => *ty, + _ => function.rettype, + }; + Ok(match stmts { + Statements::Return(value) => Body::Value(self.return_body(*value, rettype)?), + Statements::Atomic(list) => { + Body::Query(Box::new(self.query_body(list, rettype, &columns)?)) + } + Statements::String(list) => { + let list: Vec<&Node> = list.iter().collect(); + Body::Query(Box::new(self.query_body(&list, rettype, &columns)?)) + } + }) + } + + /// `interpret_func_support`: the support function with the name, which takes and returns `internal`. + fn support_function(&self, names: &[String]) -> Result { + let parts: Vec<&str> = names.iter().map(String::as_str).collect(); + let (schema, name) = self.split_name(&parts, None)?; + let path = schema.map_or_else(|| self.path.clone(), |schema| vec![schema]); + let found = path.iter().find_map(|&namespace| { + builtin::procs_named(name) + .find(|p| p.namespace == namespace && p.argtypes == [oid::INTERNAL]) + }); + let proc = found.ok_or_else(|| { Error::new( - SqlState::INVALID_FUNCTION_DEFINITION, - format!( - "return type mismatch in function declared to return {}", - types::name(rettype) - ), + SqlState::UNDEFINED_FUNCTION, + format!("function {}(internal) does not exist", parts.join(".")), ) - .with_detail(format!("Actual return type is {}.", types::name(actual))) - }) + })?; + if proc.rettype != oid::INTERNAL { + return Err(Error::new( + SqlState::INVALID_OBJECT_DEFINITION, + format!("support function {} must return type internal", parts.join(".")), + )); + } + Ok(proc.oid) } } -/// The body of a function in SQL as the analyzer reads it from the text that the catalog keeps, with `$1` as the first argument. `None` when the catalog has no function in SQL with the OID. +/// The body of a function in SQL as the analyzer reads it from the text that the catalog keeps, for a call with arguments of the types `inputs`, with `$1` as the first argument. `None` when the catalog has no function in SQL with the OID. /// /// # Errors /// /// The errors of the analysis of the body. -pub fn function_body(env: &dyn Env, oid: u32) -> Result> { +pub fn function_body(env: &dyn Env, oid: u32, inputs: &[u32]) -> Result> { let Some(function) = env.catalog().and_then(|c| c.function(oid)) else { return Ok(None) }; let Some(body) = &function.body else { return Ok(None) }; let (list, _) = rupg_sql::parse(&body.text).map_err(Error::from)?; @@ -133,14 +332,16 @@ pub fn function_body(env: &dyn Env, oid: u32) -> Result> { }); let stmt = stmt.ok_or_else(|| Error::internal("the body of a function is not CREATE FUNCTION"))?; - let params = Params { types: function.argtypes.clone(), variable: false }; + let src = (!function.src.is_empty()).then_some(function.src.as_str()); + let stmts = statements(stmt, src)?; + let params = Params { types: inputs.to_vec(), variable: false }; let mut an = Analyzer::new(env, ¶ms); an.path.clone_from(&body.path); - an.return_body(return_value(stmt)?, function.rettype).map(Some) + an.body(function, &stmts, inputs).map(Some) } impl Definer<'_, '_> { - /// `CreateFunction` for a function in SQL with a `RETURN` body. `text` is the text of the statement, which the catalog keeps. The return value is the OID of the function. + /// `CreateFunction` for a function in SQL. `text` is the text of the statement, which the catalog keeps. The return value is the OID of the function. pub(super) fn create_function(&mut self, stmt: &CreateFunctionStmt, text: &str) -> Result { if stmt.is_procedure { return Err(not_yet("CREATE PROCEDURE", None)); @@ -158,41 +359,82 @@ impl Definer<'_, '_> { }; self.check_create_in(namespace, None)?; let mut argtypes = Vec::new(); + let mut allargtypes = Vec::new(); + let mut argmodes = Vec::new(); let mut argnames = Vec::new(); + let mut outs = Vec::new(); for param in stmt.parameters.iter().flatten() { let Node::FunctionParameter(param) = param else { continue }; let at = place(param.location); - if !matches!( - param.mode, - FunctionParameterMode::FUNC_PARAM_IN | FunctionParameterMode::FUNC_PARAM_DEFAULT - ) { - return Err(not_yet("an argument that is not IN", at)); - } + let mode = match param.mode { + FunctionParameterMode::FUNC_PARAM_IN + | FunctionParameterMode::FUNC_PARAM_DEFAULT => b'i', + FunctionParameterMode::FUNC_PARAM_OUT => b'o', + FunctionParameterMode::FUNC_PARAM_INOUT => b'b', + FunctionParameterMode::FUNC_PARAM_TABLE => b't', + _ => return Err(not_yet("a VARIADIC argument", at)), + }; if param.defexpr.is_some() { return Err(not_yet("a default of an argument", at)); } let ty = param.argType.as_deref().ok_or_else(|| Error::internal("an argument type"))?; - argtypes.push(self.an.type_name(ty)?.0); + let ty = self.an.type_name(ty)?.0; + if matches!(mode, b'i' | b'b') { + argtypes.push(ty); + } + if mode != b'i' { + outs.push(ty); + } + allargtypes.push(ty); + argmodes.push(mode); argnames.push(param.name.as_deref().unwrap_or_default().to_string()); } if argnames.iter().all(String::is_empty) { argnames.clear(); } - let ret = stmt - .returnType - .as_deref() - .ok_or_else(|| not_yet("a function without RETURNS", None))?; - if ret.setof { - return Err(not_yet("a function that returns a set", place(ret.location))); + if argmodes.iter().all(|&mode| mode == b'i') { + allargtypes.clear(); + argmodes.clear(); } - let rettype = self.an.type_name(ret)?.0; + // `interpret_function_parameter_list`: the result type that the `OUT` parameters need. + let required = match outs.as_slice() { + [] => None, + [ty] => Some(*ty), + _ => Some(oid::RECORD), + }; + let (rettype, retset) = match (stmt.returnType.as_deref(), required) { + (Some(ret), _) => { + let rettype = self.an.type_name(ret)?.0; + if let Some(required) = required.filter(|&r| r != rettype) { + return Err(Error::new( + SqlState::INVALID_FUNCTION_DEFINITION, + format!( + "function result type must be {} because of OUT parameters", + types::name(required) + ), + )); + } + (rettype, ret.setof) + } + (None, Some(required)) => (required, false), + (None, None) => { + return Err(Error::new( + SqlState::INVALID_FUNCTION_DEFINITION, + "function result type must be specified", + )); + } + }; let options = options(stmt)?; - let params = Params { types: argtypes.clone(), variable: false }; - let mut an = Analyzer::new(self.an.env, ¶ms); - an.path.clone_from(&self.an.path); - let body = an.return_body(return_value(stmt)?, rettype)?; - self.an.notices.append(&mut an.notices); - let refs = body_references(&body); + if options.rows.is_some() && !retset { + return Err(Error::new( + SqlState::INVALID_PARAMETER_VALUE, + "ROWS is not applicable when function does not return a set", + )); + } + let support = match &options.support { + Some(names) => self.an.support_function(names)?, + None => 0, + }; let function = Function { oid: 0, name: name.to_string(), @@ -200,15 +442,39 @@ impl Definer<'_, '_> { owner: self.user, lang: SQL_LANGUAGE, cost: options.cost, - rows: 0, + rows: if retset { options.rows.unwrap_or(DEFAULT_ROWS) } else { 0 }, + support, strict: options.strict, volatile: options.volatile, parallel: options.parallel, rettype, + retset, argtypes, + allargtypes, + argmodes, argnames, + src: options.src.clone().unwrap_or_default(), body: Some(FunctionBody { text: text.to_string(), path: self.an.path.clone() }), }; + let stmts = statements(stmt, options.src.as_deref())?; + // `fmgr_sql_validator` reads a body in a string only when no argument is polymorphic, and a body in SQL always. + let polymorphic = function.argtypes.iter().any(|&ty| types::is_polymorphic(ty)); + let mut refs = Vec::new(); + if !(polymorphic && matches!(stmts, Statements::String(_))) { + let params = Params { types: function.argtypes.clone(), variable: false }; + let mut an = Analyzer::new(self.an.env, ¶ms); + an.path.clone_from(&self.an.path); + let body = an.body(&function, &stmts, &function.argtypes)?; + self.an.notices.append(&mut an.notices); + refs = match (&body, &stmts) { + (_, Statements::String(_)) => Vec::new(), + (Body::Value(expr), _) => body_references(expr), + (Body::Query(query), _) => super::view::function_references(query), + }; + } + if support != 0 { + refs.push(ObjRef::new(PG_PROC, support)); + } self.catalog.create_function(function, &refs) } } diff --git a/crates/rupg-analyze/src/ddl/mod.rs b/crates/rupg-analyze/src/ddl/mod.rs index 989c473..3be1730 100644 --- a/crates/rupg-analyze/src/ddl/mod.rs +++ b/crates/rupg-analyze/src/ddl/mod.rs @@ -9,7 +9,7 @@ mod table; mod tests; mod view; -pub use function::function_body; +pub use function::{Body, function_body}; pub(crate) use table::check_attribute_type; use rupg_catalog::{Catalog, ObjRef, PG_CLASS, PG_TYPE, RelKind}; diff --git a/crates/rupg-analyze/src/ddl/view.rs b/crates/rupg-analyze/src/ddl/view.rs index eb04ea6..1019979 100644 --- a/crates/rupg-analyze/src/ddl/view.rs +++ b/crates/rupg-analyze/src/ddl/view.rs @@ -2,7 +2,7 @@ //! //! The catalog keeps the text of the statement and the schemas of `search_path`, and the analyzer reads the query again each time a query uses the view. The rule of the view depends on the relations and the columns that the query reads, as `recordDependencyOnExpr` records them. -use rupg_catalog::{Column, MAX_COLUMNS, NewRelation, ObjRef, PG_CLASS, PG_TYPE}; +use rupg_catalog::{Column, MAX_COLUMNS, NewRelation, ObjRef, PG_CLASS, PG_PROC, PG_TYPE}; use rupg_common::{Error, Result, SqlState}; use rupg_pgcatalog::builtin::{self, Named}; use rupg_sql::nodes::{Node, ViewCheckOption, ViewStmt}; @@ -248,7 +248,20 @@ fn type_with_typmod(ty: u32, typmod: i32) -> String { } } -/// `find_expr_references_walker` for the query of a view: each relation of the query, each column that the query reads, and the relation or the type of each `regclass` or `regtype` constant, also in the subqueries. `outer` holds the queries outside `query`, from the nearest. A view in the query is one relation, and the query of the view does not count. +/// The objects that a query in the body of a function refers to, as for a view, each one once. +pub(super) fn function_references(query: &Query) -> Vec { + let mut found = Vec::new(); + query_references(query, &[], &mut found); + let mut refs = Vec::new(); + for object in found { + if !refs.contains(&object) { + refs.push(object); + } + } + refs +} + +/// `find_expr_references_walker` for the query of a view: each relation of the query, each column that the query reads, each function that the query calls, and the relation or the type of each `regclass` or `regtype` constant, also in the subqueries. `outer` holds the queries outside `query`, from the nearest. A view in the query is one relation, and the query of the view does not count. fn query_references(query: &Query, outer: &[&Query], refs: &mut Vec) { let mut levels = vec![query]; levels.extend_from_slice(outer); @@ -281,6 +294,7 @@ fn expr_references(expr: &Expr, levels: &[&Query], refs: &mut Vec) { (ExprKind::Const(Value::Oid(found)), oid::REGTYPE) => { refs.push(ObjRef::new(PG_TYPE, *found)); } + (ExprKind::Func(f), _) => refs.push(ObjRef::new(PG_PROC, f.oid)), (ExprKind::SubLink(sub), _) => query_references(&sub.query, levels, refs), _ => {} } diff --git a/crates/rupg-analyze/src/lib.rs b/crates/rupg-analyze/src/lib.rs index 6202ef8..aa206d2 100644 --- a/crates/rupg-analyze/src/lib.rs +++ b/crates/rupg-analyze/src/lib.rs @@ -37,7 +37,7 @@ use crate::coerce::AtOpt; pub use coerce::{Context, Path, can_coerce, find_path}; pub use colname::figure_colname; -pub use ddl::{Defined, Message, define, function_body, is_definition}; +pub use ddl::{Body, Defined, Message, define, function_body, is_definition}; pub use expr::{ Aggref, BoolOp, BoolTest, Case, CastForm, Expr, ExprKind, Func, FuncForm, SqlValue, SubLink, SubLinkKind, Subscript, Var, diff --git a/crates/rupg-catalog/src/lib.rs b/crates/rupg-catalog/src/lib.rs index 2e077fb..7f7b9dd 100644 --- a/crates/rupg-catalog/src/lib.rs +++ b/crates/rupg-catalog/src/lib.rs @@ -298,6 +298,8 @@ pub struct Function { pub cost: u32, /// `prorows`: the number of rows of a function that returns a set, or 0. pub rows: u32, + /// `prosupport`: the OID of the support function, or 0. + pub support: u32, /// `proisstrict`: true when a null argument gives null without a call. pub strict: bool, /// `provolatile`: `i` immutable, `s` stable or `v` volatile. @@ -306,11 +308,19 @@ pub struct Function { pub parallel: u8, /// `prorettype`: the OID of the result type. pub rettype: u32, - /// `proargtypes`: the types of the arguments. + /// `proretset`: true when the function returns a set. + pub retset: bool, + /// `proargtypes`: the types of the input arguments. pub argtypes: Vec, + /// `proallargtypes`: the types of all the arguments, with the `OUT` arguments. The list is empty when all the arguments are input arguments. + pub allargtypes: Vec, + /// `proargmodes`: the mode of each argument, `i`, `o`, `b` or `t`. The list is empty when all the arguments are input arguments. + pub argmodes: Vec, /// `proargnames`: the name of each argument, with an empty name for an argument without a name. The list is empty when no argument has a name. pub argnames: Vec, - /// The body in SQL, or `None`. + /// `prosrc`: the body in a string, or empty for a body in SQL. + pub src: String, + /// The text of the statement that made the function, or `None`. pub body: Option, } diff --git a/crates/rupg-exec/src/lib.rs b/crates/rupg-exec/src/lib.rs index 2ae6131..f090e29 100644 --- a/crates/rupg-exec/src/lib.rs +++ b/crates/rupg-exec/src/lib.rs @@ -8,8 +8,8 @@ use std::cell::RefCell; use std::rc::Rc; use rupg_analyze::{ - Aggref, BoolOp, BoolTest, Case, Expr, ExprKind, FromFunction, FromItem, Func, Query, SetKind, - SetOp, SetTree, SortGroup, SqlValue, SubLink, SubLinkKind, Subscript, Target, + Aggref, Body, BoolOp, BoolTest, Case, Expr, ExprKind, FromFunction, FromItem, Func, Query, + SetKind, SetOp, SetTree, SortGroup, SqlValue, SubLink, SubLinkKind, Subscript, Target, }; use rupg_common::{Error, Result, SqlState}; use rupg_func::{Call, Kernel, Session, base_type}; @@ -114,7 +114,9 @@ fn check(expr: &Expr) -> Result<()> { check_query(&sub.query, false)?; } ExprKind::Func(f) if f.retset => { - set_kernel(f.oid, expr.location)?; + if !builtin::is_system_proc(f.oid) { + set_kernel(f.oid, expr.location)?; + } f.args.iter().try_for_each(check)?; } ExprKind::Func(f) => { @@ -240,7 +242,9 @@ fn check_query(query: &Query, output: bool) -> Result<()> { for (call, width) in relation.function.iter().flat_map(|f| &f.calls) { match &call.kind { ExprKind::Func(f) if table_function(f.oid, *width) => { - set_kernel(f.oid, call.location)?; + if !builtin::is_system_proc(f.oid) { + set_kernel(f.oid, call.location)?; + } f.args.iter().try_for_each(check)?; } _ => check(call)?, @@ -374,8 +378,8 @@ type Rows = Vec>; /// The name and the expression of each check constraint of a domain. type Checks = Rc>; -/// The body of each function in SQL, by the OID of the function, or `None` for a function of the catalog that is not in SQL. -type Bodies = Rc>)>>>; +/// The body of each function in SQL, by the OID of the function and the types of the arguments of the call, or `None` for a function of the catalog that is not in SQL. +type Bodies = Rc), Option>)>>>; /// The state of a run that the query and its subqueries share. #[derive(Default)] @@ -408,16 +412,22 @@ impl Cache { Ok(checks) } - /// The body of a function in SQL of the catalog of the session, which the analyzer reads once for each run, or `None` for another function. - fn body(&self, func: u32, session: &dyn Session) -> Result>> { + /// The body of a function in SQL of the catalog of the session for a call with arguments of the types `inputs`, which the analyzer reads once for each run, or `None` for another function. + fn body(&self, func: u32, inputs: &[u32], session: &dyn Session) -> Result>> { if session.catalog().and_then(|c| c.function(func)).is_none() { return Ok(None); } - if let Some((_, body)) = self.bodies.borrow().iter().find(|(f, _)| *f == func) { - return Ok(body.clone()); + let kept = self + .bodies + .borrow() + .iter() + .find(|((f, t), _)| *f == func && t == inputs) + .map(|(_, body)| body.clone()); + if let Some(body) = kept { + return Ok(body); } - let body = rupg_func::function_body(func, session)?.map(Rc::new); - self.bodies.borrow_mut().push((func, body.clone())); + let body = rupg_func::function_body(func, inputs, session)?.map(Rc::new); + self.bodies.borrow_mut().push(((func, inputs.to_vec()), body.clone())); Ok(body) } @@ -880,13 +890,20 @@ impl<'a> Eval<'a> { if !table_function(f.oid, width) { return Ok(vec![vec![self.eval(call)?]]); } - let kernel = set_kernel(f.oid, call.location)?; let args = f.args.iter().map(|a| self.eval(a)).collect::>>()?; if strict(f.oid) && args.iter().any(Value::is_null) { let retset = builtin::proc_by_oid(f.oid).is_some_and(|p| p.retset); return Ok(if retset { Vec::new() } else { vec![vec![Value::Null; width]] }); } let types: Vec = f.args.iter().map(|a| a.ty).collect(); + if let Some(body) = self.cache.body(f.oid, &types, self.session)? { + let Body::Query(query) = &*body else { + return Err(Error::internal("a function that returns a set with no query")); + }; + let cache = Cache { bodies: Rc::clone(&self.cache.bodies), ..Cache::default() }; + return run_query(query, &args, self.session, &cache, &[], false); + } + let kernel = set_kernel(f.oid, call.location)?; let c = Call { session: self.session, args: &types, ret: call.ty, variadic: f.variadic }; kernel(&c, &args) } @@ -1182,11 +1199,24 @@ impl<'a> Eval<'a> { if strict(func) && args.iter().any(Value::is_null) { return Ok(Value::Null); } - if let Some(body) = self.cache.body(func, self.session)? { + if let Some(body) = self.cache.body(func, types, self.session)? { // The body runs with a cache of its own, so that a subquery of the body that reads an argument runs again for each call. let cache = Cache { bodies: Rc::clone(&self.cache.bodies), ..Cache::default() }; - let tables = scan::Tables::default(); - return Eval::new(args, self.session, &tables, &cache, &[]).eval(&body); + return match &*body { + Body::Value(expr) => { + let tables = scan::Tables::default(); + Eval::new(args, self.session, &tables, &cache, &[]).eval(expr) + } + // `fmgr_sql` for a function that does not return a set: the first column of the first row, or null. + Body::Query(query) => { + let rows = run_query(query, args, self.session, &cache, &[], false)?; + Ok(rows + .into_iter() + .next() + .and_then(|row| row.into_iter().next()) + .unwrap_or(Value::Null)) + } + }; } let kernel = kernel(func, None)?; let call = Call { session: self.session, args: types, ret, variadic }; diff --git a/crates/rupg-exec/src/user.rs b/crates/rupg-exec/src/user.rs index c2b70ab..cc91810 100644 --- a/crates/rupg-exec/src/user.rs +++ b/crates/rupg-exec/src/user.rs @@ -82,6 +82,14 @@ fn oid_array(values: &[u32]) -> Value { Value::Array(Box::new(Array::one(values.iter().map(|&v| Some(Value::Oid(v))).collect()))) } +/// A `"char"[]`, or null for no element. +fn char_array(values: &[u8]) -> Value { + if values.is_empty() { + return Value::Null; + } + Value::Array(Box::new(Array::one(values.iter().map(|&v| Some(Value::Char(v))).collect()))) +} + /// A `text[]` or an `aclitem[]` of the texts. fn text_array(values: &[String]) -> Value { Value::Array(Box::new(Array::one( @@ -465,7 +473,7 @@ fn sequences<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { .collect() } -/// The functions in SQL. The body in `prosqlbody` is the text of the statement that made the function, and `prosrc` is empty, as for a function with a `RETURN` body. +/// The functions in SQL. A body in a string is in `prosrc`. For a body in SQL, `prosqlbody` has the text of the statement that made the function, and `prosrc` is empty. fn functions<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { catalog .functions() @@ -482,21 +490,26 @@ fn functions<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { .set("procost", Value::Float4(cost)) .set("prorows", Value::Float4(rows)) .set("provariadic", oid(0)) - .set("prosupport", oid(0)) + .set("prosupport", oid(f.support)) .set("prokind", code('f')) .set("prosecdef", Value::Bool(false)) .set("proleakproof", Value::Bool(false)) .set("proisstrict", Value::Bool(f.strict)) - .set("proretset", Value::Bool(false)) + .set("proretset", Value::Bool(f.retset)) .set("provolatile", Value::Char(f.volatile)) .set("proparallel", Value::Char(f.parallel)) .set("pronargs", Value::Int2(i16::try_from(f.argtypes.len()).unwrap_or(i16::MAX))) .set("pronargdefaults", Value::Int2(0)) .set("prorettype", oid(f.rettype)) .set("proargtypes", oid_vector(&f.argtypes)) + .set("proallargtypes", oid_array(&f.allargtypes)) + .set("proargmodes", char_array(&f.argmodes)) .set("proargnames", names) - .set("prosrc", Value::text("")) - .set("prosqlbody", tree(f.body.as_ref().map(|b| &b.text))); + .set("prosrc", Value::text(f.src.clone())) + .set( + "prosqlbody", + tree(f.body.as_ref().filter(|_| f.src.is_empty()).map(|b| &b.text)), + ); row }) .collect() diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index a72e3f2..772fdf8 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -171,13 +171,17 @@ pub(crate) fn bad_value() -> Error { Error::internal("a function got a value of the wrong kind") } -/// The body of a function in SQL of the catalog of the session, as the analyzer reads it, with `$1` as the first argument. `None` for a function that is not in the catalog of the session. +/// The body of a function in SQL of the catalog of the session, as the analyzer reads it for a call with arguments of the types `inputs`, with `$1` as the first argument. `None` for a function that is not in the catalog of the session. /// /// # Errors /// /// The errors of the analysis of the body. -pub fn function_body(func: u32, session: &dyn Session) -> Result> { - rupg_analyze::function_body(®::SessionEnv(session), func) +pub fn function_body( + func: u32, + inputs: &[u32], + session: &dyn Session, +) -> Result> { + rupg_analyze::function_body(®::SessionEnv(session), func, inputs) } /// The kernel of the function with this `pg_proc` OID, or `None` if the engine does not have it. diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index fe73670..a62a8b6 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1913,6 +1913,12 @@ mod tests { big_stack(|| script(include_str!("connection/record_fields.test"))); } + /// The functions of `information_schema` with a query in the body: `_pg_expandarray` with a query in a string and polymorphic `OUT` parameters, and `_pg_index_position` with `BEGIN ATOMIC`. The script calls them in `FROM`, in the target list and with `LATERAL`, also with null and empty arrays, and reads their rows of `pg_proc` and `pg_depend`. `connection/sql_bodies.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn sql_bodies() { + big_stack(|| script(include_str!("connection/sql_bodies.test"))); + } + /// `format_type` of `interval` with each range and precision that a typmod can have, the error for a bad range, `_pg_interval_type` of `information_schema`, and the interval types of a view in `pg_get_viewdef` and `pg_attribute`. `connection/interval_typmods.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn interval_typmods() { diff --git a/crates/rupg-session/src/connection/sql_bodies.test b/crates/rupg-session/src/connection/sql_bodies.test new file mode 100644 index 0000000..9ea95cb --- /dev/null +++ b/crates/rupg-session/src/connection/sql_bodies.test @@ -0,0 +1,157 @@ +> SELECT * FROM information_schema._pg_expandarray(ARRAY['a', 'b', 'c']) +fields x:25:-1:-1,n:23:4:-1 +row a,1 +row b,2 +row c,3 +SELECT 3 +> SELECT information_schema._pg_expandarray(ARRAY[10, 20]) +fields _pg_expandarray:2249:-1:-1 +row (10,1) +row (20,2) +SELECT 2 +> SELECT (information_schema._pg_expandarray(ARRAY[10, 20])).x +fields x:23:4:-1 +row 10 +row 20 +SELECT 2 +> SELECT (information_schema._pg_expandarray(ARRAY[10, 20])).n +fields n:23:4:-1 +row 1 +row 2 +SELECT 2 +> SELECT pg_typeof((information_schema._pg_expandarray(ARRAY[1.5])).x), pg_typeof((information_schema._pg_expandarray(ARRAY[1.5])).n) +fields pg_typeof:2206:4:-1,pg_typeof:2206:4:-1 +row numeric,integer +SELECT 1 +> SELECT * FROM information_schema._pg_expandarray(NULL::int[]) +fields x:23:4:-1,n:23:4:-1 +SELECT 0 +> SELECT * FROM information_schema._pg_expandarray('{}'::text[]) +fields x:25:-1:-1,n:23:4:-1 +SELECT 0 +> SELECT * FROM information_schema._pg_expandarray(ARRAY[[1, 2], [3, 4]]) +fields x:23:4:-1,n:23:4:-1 +row 1,1 +row 2,2 +row 3,3 +row 4,4 +SELECT 4 +> SELECT x, n FROM information_schema._pg_expandarray('1 3 2'::int2vector) +fields x:21:2:-1,n:23:4:-1 +row 1,1 +row 3,2 +row 2,3 +SELECT 3 +> SELECT pg_typeof(x) FROM information_schema._pg_expandarray('1 3 2'::int2vector) LIMIT 1 +fields pg_typeof:2206:4:-1 +row smallint +SELECT 1 +> SELECT * FROM information_schema._pg_expandarray(ARRAY['a', NULL]) +fields x:25:-1:-1,n:23:4:-1 +row a,1 +row NULL,2 +SELECT 2 +> SELECT * FROM information_schema._pg_expandarray(ARRAY['a']) AS t(p, q) +fields p:25:-1:-1,q:23:4:-1 +row a,1 +SELECT 1 +> SELECT * FROM information_schema._pg_expandarray(ARRAY['a']) AS t +fields x:25:-1:-1,n:23:4:-1 +row a,1 +SELECT 1 +> SELECT * FROM information_schema._pg_expandarray(1) +ERROR 42883 function information_schema._pg_expandarray(integer) does not exist at 15 +> SELECT * FROM information_schema._pg_expandarray('{1,2}') +ERROR 42804 could not determine polymorphic type because input has type unknown +> SELECT proname, proretset, prorows, prosupport, proargtypes::regtype[], proallargtypes::regtype[], proargmodes, proargnames, prorettype::regtype, provolatile, proisstrict, proparallel, prokind, procost, pronargs FROM pg_proc WHERE oid = 'information_schema._pg_expandarray'::regproc +fields proname@pg_proc.2:19:64:-1,proretset@pg_proc.14:16:1:-1,prorows@pg_proc.7:700:4:-1,prosupport@pg_proc.9:24:4:-1,proargtypes:2211:-1:-1,proallargtypes:2211:-1:-1,proargmodes@pg_proc.22:1002:-1:-1,proargnames@pg_proc.23:1009:-1:-1,prorettype:2206:4:-1,provolatile@pg_proc.15:18:1:-1,proisstrict@pg_proc.13:16:1:-1,proparallel@pg_proc.16:18:1:-1,prokind@pg_proc.10:18:1:-1,procost@pg_proc.6:700:4:-1,pronargs@pg_proc.17:21:2:-1 +row _pg_expandarray,t,100,array_unnest_support,[0:0]={anyarray},{anyarray,anyelement,integer},{i,o,o},{"",x,n},record,i,t,s,f,100,1 +SELECT 1 +> SELECT proname, proretset, prorows, prosupport, proargtypes::regtype[], proallargtypes::regtype[], proargmodes, proargnames, prorettype::regtype, provolatile, proisstrict, proparallel, prokind, procost, pronargs FROM pg_proc WHERE oid = 'information_schema._pg_index_position'::regproc +fields proname@pg_proc.2:19:64:-1,proretset@pg_proc.14:16:1:-1,prorows@pg_proc.7:700:4:-1,prosupport@pg_proc.9:24:4:-1,proargtypes:2211:-1:-1,proallargtypes:2211:-1:-1,proargmodes@pg_proc.22:1002:-1:-1,proargnames@pg_proc.23:1009:-1:-1,prorettype:2206:4:-1,provolatile@pg_proc.15:18:1:-1,proisstrict@pg_proc.13:16:1:-1,proparallel@pg_proc.16:18:1:-1,prokind@pg_proc.10:18:1:-1,procost@pg_proc.6:700:4:-1,pronargs@pg_proc.17:21:2:-1 +row _pg_index_position,f,0,-,[0:1]={oid,smallint},NULL,NULL,NULL,integer,s,t,u,f,100,2 +SELECT 1 +> SELECT prosrc, prosqlbody IS NULL FROM pg_proc WHERE oid = 'information_schema._pg_expandarray'::regproc +fields prosrc@pg_proc.26:25:-1:-1,?column?:16:1:-1 +row SELECT * FROM pg_catalog.unnest($1) WITH ORDINALITY,t +SELECT 1 +> SELECT prosrc, prosqlbody IS NULL FROM pg_proc WHERE oid = 'information_schema._pg_index_position'::regproc +fields prosrc@pg_proc.26:25:-1:-1,?column?:16:1:-1 +row ,f +SELECT 1 +> SELECT pg_get_function_arguments('information_schema._pg_expandarray'::regproc), pg_get_function_result('information_schema._pg_expandarray'::regproc), pg_get_function_identity_arguments('information_schema._pg_expandarray'::regproc) +fields pg_get_function_arguments:25:-1:-1,pg_get_function_result:25:-1:-1,pg_get_function_identity_arguments:25:-1:-1 +row anyarray, OUT x anyelement, OUT n integer,SETOF record,anyarray, OUT x anyelement, OUT n integer +SELECT 1 +> SELECT pg_get_function_arguments('information_schema._pg_index_position'::regproc), pg_get_function_result('information_schema._pg_index_position'::regproc) +fields pg_get_function_arguments:25:-1:-1,pg_get_function_result:25:-1:-1 +row oid, smallint,integer +SELECT 1 +> SELECT c.relname, information_schema._pg_index_position(i.indexrelid, 1::smallint), information_schema._pg_index_position(i.indexrelid, 2::smallint), information_schema._pg_index_position(i.indexrelid, 3::smallint) FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid WHERE c.relname IN ('pg_class_oid_index', 'pg_class_relname_nsp_index', 'pg_attribute_relid_attnum_index', 'pg_depend_reference_index') ORDER BY 1 +fields relname@pg_class.2:19:64:-1,_pg_index_position:23:4:-1,_pg_index_position:23:4:-1,_pg_index_position:23:4:-1 +row pg_attribute_relid_attnum_index,1,NULL,NULL +row pg_class_oid_index,1,NULL,NULL +row pg_class_relname_nsp_index,NULL,1,2 +row pg_depend_reference_index,NULL,NULL,NULL +SELECT 4 +> SELECT information_schema._pg_index_position(0, 1::smallint) +fields _pg_index_position:23:4:-1 +row NULL +SELECT 1 +> SELECT information_schema._pg_index_position(NULL, 1::smallint) +fields _pg_index_position:23:4:-1 +row NULL +SELECT 1 +> SELECT information_schema._pg_index_position('pg_class_oid_index'::regclass, NULL) +fields _pg_index_position:23:4:-1 +row NULL +SELECT 1 +> SELECT pg_typeof(information_schema._pg_index_position(0, 1::smallint)) +fields pg_typeof:2206:4:-1 +row integer +SELECT 1 +> SELECT objid::regprocedure, refobjid::regprocedure, deptype FROM pg_depend WHERE classid = 'pg_proc'::regclass AND objid IN ('information_schema._pg_expandarray'::regproc, 'information_schema._pg_index_position'::regproc) AND refclassid = 'pg_proc'::regclass ORDER BY 1, 2 +fields objid:2202:4:-1,refobjid:2202:4:-1,deptype@pg_depend.7:18:1:-1 +row information_schema._pg_index_position(oid,smallint),information_schema._pg_expandarray(anyarray),n +SELECT 1 +> SELECT (y.a).x, (y.a).n FROM (SELECT information_schema._pg_expandarray(ARRAY['p', 'q']) AS a) y +fields x:25:-1:-1,n:23:4:-1 +row p,1 +row q,2 +SELECT 2 +> SELECT information_schema._pg_expandarray(ARRAY['p', 'q']) AS a, 1 AS b +fields a:2249:-1:-1,b:23:4:-1 +row (p,1),1 +row (q,2),1 +SELECT 2 +> SELECT count(*) FROM information_schema._pg_expandarray(ARRAY[1, 2, 3]) e, information_schema._pg_expandarray(ARRAY[4, 5]) f +fields count:20:8:-1 +row 6 +SELECT 1 +> SELECT x FROM information_schema._pg_expandarray(ARRAY[3, 1, 2]) ORDER BY x +fields x:23:4:-1 +row 1 +row 2 +row 3 +SELECT 3 +> SELECT a.x, b.x FROM information_schema._pg_expandarray(ARRAY[1, 2]) a, LATERAL information_schema._pg_expandarray(ARRAY[a.x * 10, a.x * 100]) b +fields x:23:4:-1,x:23:4:-1 +row 1,10 +row 1,100 +row 2,20 +row 2,200 +SELECT 4 +> SELECT proname, proretset, prokind FROM pg_proc WHERE pronamespace = 'information_schema'::regnamespace ORDER BY 1 +fields proname@pg_proc.2:19:64:-1,proretset@pg_proc.14:16:1:-1,prokind@pg_proc.10:18:1:-1 +row _pg_char_max_length,f,f +row _pg_char_octet_length,f,f +row _pg_datetime_precision,f,f +row _pg_expandarray,t,f +row _pg_index_position,f,f +row _pg_interval_type,f,f +row _pg_numeric_precision,f,f +row _pg_numeric_precision_radix,f,f +row _pg_numeric_scale,f,f +row _pg_truetypid,f,f +row _pg_truetypmod,f,f +SELECT 11 diff --git a/crates/rupg-session/src/system.rs b/crates/rupg-session/src/system.rs index 389b99b..9881e2a 100644 --- a/crates/rupg-session/src/system.rs +++ b/crates/rupg-session/src/system.rs @@ -6,7 +6,7 @@ //! //! The `GRANT` and `REVOKE` statements of the script on views set the privileges of these views. Then each view without privileges gets `SELECT` for PUBLIC, as `setup_privileges` of `initdb` gives it. The statements on catalogs changed the static rows already. //! -//! `information_schema.sql` runs next, with `search_path` set to `information_schema` by the script. rupg makes the schema with its privileges, the five domains and the functions in SQL with a `RETURN` body, with the OIDs of PostgreSQL 19. The other functions, the tables and the views of the script are not done yet. +//! `information_schema.sql` runs next, with `search_path` set to `information_schema` by the script. rupg makes the schema with its privileges, the five domains and the eleven functions in SQL, with the OIDs of PostgreSQL 19. The tables and the views of the script are not done yet. //! //! These parts of `system_views.sql` are not done yet: the rules `pg_settings_u` and `pg_settings_n` on `pg_settings`. Their OIDs stay unused. @@ -135,8 +135,10 @@ const DOMAIN_OIDS: [(&str, u32); 5] = [ ("yes_or_no", 13376), ]; -/// The OID of each function of `information_schema` in PostgreSQL 19 that rupg makes. The OID 13357 is the TOAST value of the long body of `_pg_char_octet_length`. The functions `_pg_expandarray` and `_pg_index_position` take the OIDs 13351 and 13352 and are not done yet. -const FUNCTION_OIDS: [(&str, u32); 9] = [ +/// The OID of each function of `information_schema` in PostgreSQL 19. The OID 13357 is the TOAST value of the long body of `_pg_char_octet_length`. +const FUNCTION_OIDS: [(&str, u32); 11] = [ + ("_pg_expandarray", 13351), + ("_pg_index_position", 13352), ("_pg_truetypid", 13353), ("_pg_truetypmod", 13354), ("_pg_char_max_length", 13355), @@ -341,6 +343,10 @@ fn add_system_proc(catalog: &Catalog, oid: u32) -> Result<()> { let names: Vec<&'static str> = f.argnames.iter().map(|n| &*n.clone().leak()).collect(); &*names.leak() }); + let allargtypes: Option<&'static [u32]> = + (!f.allargtypes.is_empty()).then(|| &*f.allargtypes.clone().leak()); + let argmodes: Option<&'static [u8]> = + (!f.argmodes.is_empty()).then(|| &*f.argmodes.clone().leak()); builtin::add_system_proc(builtin::ProcRow { oid, name: f.name.clone().leak(), @@ -349,14 +355,14 @@ fn add_system_proc(catalog: &Catalog, oid: u32) -> Result<()> { variadic: 0, kind: b'f', strict: f.strict, - retset: false, + retset: f.retset, volatile: f.volatile, nargs: i16::try_from(f.argtypes.len()).unwrap_or(i16::MAX), nargdefaults: 0, rettype: f.rettype, argtypes: f.argtypes.clone().leak(), - allargtypes: None, - argmodes: None, + allargtypes, + argmodes, argnames, argdefaults: None, src: "",