Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 309 additions & 43 deletions crates/rupg-analyze/src/ddl/function.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/rupg-analyze/src/ddl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
18 changes: 16 additions & 2 deletions crates/rupg-analyze/src/ddl/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<ObjRef> {
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<ObjRef>) {
let mut levels = vec![query];
levels.extend_from_slice(outer);
Expand Down Expand Up @@ -281,6 +294,7 @@ fn expr_references(expr: &Expr, levels: &[&Query], refs: &mut Vec<ObjRef>) {
(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),
_ => {}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rupg-analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions crates/rupg-catalog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<u32>,
/// `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<u32>,
/// `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<u8>,
/// `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<String>,
/// 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<FunctionBody>,
}

Expand Down
62 changes: 46 additions & 16 deletions crates/rupg-exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)?,
Expand Down Expand Up @@ -374,8 +378,8 @@ type Rows = Vec<Vec<Value>>;
/// The name and the expression of each check constraint of a domain.
type Checks = Rc<Vec<(String, Expr)>>;

/// 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<RefCell<Vec<(u32, Option<Rc<Expr>>)>>>;
/// 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<RefCell<Vec<((u32, Vec<u32>), Option<Rc<Body>>)>>>;

/// The state of a run that the query and its subqueries share.
#[derive(Default)]
Expand Down Expand Up @@ -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<Option<Rc<Expr>>> {
/// 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<Option<Rc<Body>>> {
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)
}

Expand Down Expand Up @@ -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::<Result<Vec<_>>>()?;
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<u32> = 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)
}
Expand Down Expand Up @@ -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 };
Expand Down
23 changes: 18 additions & 5 deletions crates/rupg-exec/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -465,7 +473,7 @@ fn sequences<'a>(table: &'a Table, catalog: &Catalog) -> Vec<Row<'a>> {
.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<Row<'a>> {
catalog
.functions()
Expand All @@ -482,21 +490,26 @@ fn functions<'a>(table: &'a Table, catalog: &Catalog) -> Vec<Row<'a>> {
.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()
Expand Down
10 changes: 7 additions & 3 deletions crates/rupg-func/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<rupg_analyze::Expr>> {
rupg_analyze::function_body(&reg::SessionEnv(session), func)
pub fn function_body(
func: u32,
inputs: &[u32],
session: &dyn Session,
) -> Result<Option<rupg_analyze::Body>> {
rupg_analyze::function_body(&reg::SessionEnv(session), func, inputs)
}

/// The kernel of the function with this `pg_proc` OID, or `None` if the engine does not have it.
Expand Down
6 changes: 6 additions & 0 deletions crates/rupg-session/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading