From 685df95cc48d7c7fc3dc55ec28aa71ce034a8b69 Mon Sep 17 00:00:00 2001 From: tamnd Date: Sat, 10 Oct 2026 02:30:23 +0700 Subject: [PATCH] Run set-returning functions in the target list --- crates/rupg-analyze/src/agg.rs | 38 +- crates/rupg-analyze/src/coerce.rs | 2 +- crates/rupg-analyze/src/ddl/table.rs | 8 +- crates/rupg-analyze/src/expr.rs | 10 + crates/rupg-analyze/src/from.rs | 2 + crates/rupg-analyze/src/lib.rs | 3 + crates/rupg-analyze/src/node.rs | 4 + crates/rupg-analyze/src/resolve.rs | 72 +++- crates/rupg-analyze/src/select.rs | 5 + crates/rupg-analyze/src/sublink.rs | 2 + crates/rupg-analyze/src/transform.rs | 9 +- crates/rupg-analyze/src/values.rs | 3 +- crates/rupg-exec/src/agg.rs | 3 +- crates/rupg-exec/src/fold.rs | 9 +- crates/rupg-exec/src/lib.rs | 126 +++++- crates/rupg-func/src/acl.rs | 11 + crates/rupg-func/src/deparse.rs | 13 +- crates/rupg-func/src/srf.rs | 2 +- crates/rupg-session/src/connection.rs | 8 +- .../src/connection/system_views.test | 169 +++++++- .../src/connection/target_srf.test | 367 ++++++++++++++++++ crates/rupg-session/src/system.rs | 12 +- 22 files changed, 818 insertions(+), 60 deletions(-) create mode 100644 crates/rupg-session/src/connection/target_srf.test diff --git a/crates/rupg-analyze/src/agg.rs b/crates/rupg-analyze/src/agg.rs index d8aa421..14b2e96 100644 --- a/crates/rupg-analyze/src/agg.rs +++ b/crates/rupg-analyze/src/agg.rs @@ -37,6 +37,8 @@ pub(crate) enum Kind { FromFunction, /// `EXPR_KIND_VALUES`. Values, + /// `EXPR_KIND_VALUES_SINGLE`: a `VALUES` list with one row. + ValuesSingle, /// `EXPR_KIND_EXECUTE_PARAMETER`. ExecuteParameter, } @@ -61,7 +63,7 @@ impl Kind { Kind::IndexExpression => "index expressions", Kind::IndexPredicate => "index predicates", Kind::FromFunction => "functions in FROM", - Kind::Values => "VALUES", + Kind::Values | Kind::ValuesSingle => "VALUES", Kind::ExecuteParameter => "EXECUTE parameters", } } @@ -108,6 +110,19 @@ pub(crate) struct Parts<'a> { pub(crate) null_treatment: bool, } +/// The hint of the errors of a call of a function that gives a set inside an expression that cannot take a set. +pub(crate) const SRF_HINT: &str = + "You might be able to move the set-returning function into a LATERAL FROM item."; + +/// `check_agg_arguments_walker`: the first aggregate call or call of a function that gives a set in the arguments of an aggregate, outside their subqueries. +fn nested(expr: &Expr) -> Option<&Expr> { + match &expr.kind { + ExprKind::Agg(_) => Some(expr), + ExprKind::Func(f) if f.retset => Some(expr), + _ => expr.children().into_iter().find_map(nested), + } +} + /// An error of an aggregate call in a wrong place, with the SQLSTATE `42803`. pub(crate) fn grouping_error(message: String, at: Option) -> Error { Error::new(SqlState::GROUPING_ERROR, message).at_opt(at) @@ -165,12 +180,20 @@ impl Analyzer<'_> { }; let args: Vec = targets.into_iter().map(|t| t.expr).collect(); let filter = parts.filter.map(Box::new); - // check_agg_arguments: an aggregate call in the arguments is of the same level. - if let Some(inner) = args.iter().chain(filter.as_deref()).find_map(Expr::first_agg) { - return Err(grouping_error( - "aggregate function calls cannot be nested".into(), - inner.location, - )); + // check_agg_arguments: an aggregate call in the arguments is of the same level, and a set cannot be an argument. + if let Some(inner) = args.iter().chain(filter.as_deref()).find_map(nested) { + if let ExprKind::Agg(_) = inner.kind { + return Err(grouping_error( + "aggregate function calls cannot be nested".into(), + inner.location, + )); + } + return Err(Error::new( + SqlState::FEATURE_NOT_SUPPORTED, + "aggregate function calls cannot contain set-returning function calls", + ) + .with_hint(SRF_HINT) + .at_opt(inner.location)); } // check_agg_arguments: an aggregate whose arguments read only the columns of outer queries belongs to the outer query. let level = args @@ -215,6 +238,7 @@ impl Analyzer<'_> { | Kind::IndexPredicate | Kind::FromFunction | Kind::Values + | Kind::ValuesSingle | Kind::ExecuteParameter => { return Err(grouping_error( format!("aggregate functions are not allowed in {}", self.kind.name()), diff --git a/crates/rupg-analyze/src/coerce.rs b/crates/rupg-analyze/src/coerce.rs index b366d1f..ceca7f7 100644 --- a/crates/rupg-analyze/src/coerce.rs +++ b/crates/rupg-analyze/src/coerce.rs @@ -495,7 +495,7 @@ fn build_cast( -1 }; Expr { - kind: ExprKind::Func(Func { oid: func, args, form, variadic: false }), + kind: ExprKind::Func(Func { oid: func, args, form, variadic: false, retset: false }), ty: target, typmod, location, diff --git a/crates/rupg-analyze/src/ddl/table.rs b/crates/rupg-analyze/src/ddl/table.rs index c7741fe..39f7787 100644 --- a/crates/rupg-analyze/src/ddl/table.rs +++ b/crates/rupg-analyze/src/ddl/table.rs @@ -902,6 +902,12 @@ fn nextval(sequence: u32) -> Result { .next() .ok_or_else(|| Error::internal("function nextval does not exist"))?; let arg = Expr::constant(Value::Oid(sequence), oid::REGCLASS, -1, None); - let func = Func { oid: proc.oid, args: vec![arg], form: FuncForm::Call, variadic: false }; + let func = Func { + oid: proc.oid, + args: vec![arg], + form: FuncForm::Call, + variadic: false, + retset: false, + }; Ok(Expr::new(ExprKind::Func(func), proc.rettype)) } diff --git a/crates/rupg-analyze/src/expr.rs b/crates/rupg-analyze/src/expr.rs index 173371d..ed1bbcd 100644 --- a/crates/rupg-analyze/src/expr.rs +++ b/crates/rupg-analyze/src/expr.rs @@ -136,6 +136,8 @@ pub struct Func { pub form: FuncForm, /// True when the last argument is the array of a variadic function, as `funcvariadic` of `FuncExpr`. A function with `VARIADIC "any"` then takes the elements of the array as its arguments. pub variadic: bool, + /// `funcretset`: true when the function gives a set. + pub retset: bool, } /// A call of an aggregate function, as `Aggref`. @@ -417,6 +419,14 @@ impl Expr { self.children().into_iter().find_map(Expr::first_agg) } + /// `expression_returns_set` and `exprLocation` of the first call: the first call of a function that gives a set in the expression, outside its subqueries. + pub fn first_set_call(&self) -> Option<&Expr> { + if matches!(&self.kind, ExprKind::Func(f) if f.retset) { + return Some(self); + } + self.children().into_iter().find_map(Expr::first_set_call) + } + /// `strip_implicit_coercions`: the expression without the casts that the analyzer added. pub fn strip_implicit(&self) -> &Expr { match &self.kind { diff --git a/crates/rupg-analyze/src/from.rs b/crates/rupg-analyze/src/from.rs index 02b7cf9..00eb064 100644 --- a/crates/rupg-analyze/src/from.rs +++ b/crates/rupg-analyze/src/from.rs @@ -398,7 +398,9 @@ impl Analyzer<'_> { self.outer.push(scope); let kind = std::mem::replace(&mut self.kind, Kind::Other); let has_aggs = std::mem::replace(&mut self.has_aggs, false); + let srfs = self.srfs; let result = self.select(stmt); + self.srfs = srfs; self.has_aggs = has_aggs; self.kind = kind; let mut scope = self.outer.pop().unwrap_or_default(); diff --git a/crates/rupg-analyze/src/lib.rs b/crates/rupg-analyze/src/lib.rs index ee521ce..8850dfb 100644 --- a/crates/rupg-analyze/src/lib.rs +++ b/crates/rupg-analyze/src/lib.rs @@ -106,6 +106,8 @@ pub(crate) struct Analyzer<'a> { views: Vec, /// The base type and the typmod of the domain whose check constraint the analyzer reads now, or `None`. Then the name `value` is [`ExprKind::DomainValue`]. domain_value: Option<(u32, i32)>, + /// The number of the calls of functions that give a set that the analysis of the query has made, and the place of the last one. It takes the place of `p_last_srf`: `CASE` and `COALESCE` compare the number before and after their arguments. + srfs: (usize, Option), } impl<'a> Analyzer<'a> { @@ -128,6 +130,7 @@ impl<'a> Analyzer<'a> { keep_unknowns: false, views: Vec::new(), domain_value: None, + srfs: (0, None), } } diff --git a/crates/rupg-analyze/src/node.rs b/crates/rupg-analyze/src/node.rs index eb26c25..c7f15bf 100644 --- a/crates/rupg-analyze/src/node.rs +++ b/crates/rupg-analyze/src/node.rs @@ -613,6 +613,8 @@ fn expr_of(item: &Item) -> Result { args: f.exprs("args")?, form, variadic: f.flag("variadic")?, + // A stored expression has no call of a function that gives a set. + retset: false, }) } "RELABEL" => ExprKind::Relabel(f.expr("arg")?, cast_form(&f)?), @@ -876,6 +878,7 @@ mod tests { args: vec![var(1), constant(Value::Int4(1), oid::INT4)], form: FuncForm::Operator(551), variadic: false, + retset: false, }), ty: oid::INT4, typmod: -1, @@ -929,6 +932,7 @@ mod tests { args: vec![], form: FuncForm::ImplicitCast, variadic: true, + retset: false, }), ty: oid::VARCHAR, typmod: 14, diff --git a/crates/rupg-analyze/src/resolve.rs b/crates/rupg-analyze/src/resolve.rs index 7faf9d1..3319322 100644 --- a/crates/rupg-analyze/src/resolve.rs +++ b/crates/rupg-analyze/src/resolve.rs @@ -539,13 +539,8 @@ impl Analyzer<'_> { .at_opt(last.place())); } } - // check_srf_call_placement. - if proc.retset && self.kind.is_standalone() { - return Err(Error::new( - SqlState::FEATURE_NOT_SUPPORTED, - format!("set-returning functions are not allowed in {}", self.kind.name()), - ) - .at_opt(at)); + if proc.retset { + self.check_srf_placement(&signature(&name, &inputs), at)?; } let over = parts.as_ref().is_some_and(|p| p.over); if let (Some(row), Some(parts)) = (aggregate, parts) @@ -588,25 +583,65 @@ impl Analyzer<'_> { ) .at_opt(at)); } - // A set-returning function can be at the top of a function in FROM, which `transformRangeFunction` checks. - if proc.retset && self.kind != Kind::FromFunction { - return Err(Error::new( - SqlState::FEATURE_NOT_SUPPORTED, - format!( - "set-returning function {} is not supported yet", - signature(&name, &inputs) - ), - ) - .at_opt(at)); + if proc.retset { + self.srfs = (self.srfs.0 + 1, at); } Ok(Expr { - kind: ExprKind::Func(Func { oid: proc.oid, args, form: FuncForm::Call, variadic }), + kind: ExprKind::Func(Func { + oid: proc.oid, + args, + form: FuncForm::Call, + variadic, + retset: proc.retset, + }), ty: result, typmod: -1, location: at, }) } + /// `check_srf_call_placement`: the error of a call of a function that gives a set in a part of the query where it cannot be. A function in `FROM` can only be at the top, which `transformRangeFunction` checks. + fn check_srf_placement(&self, signature: &str, at: Option) -> Result<()> { + let message = match self.kind { + Kind::Select + | Kind::GroupBy + | Kind::OrderBy + | Kind::DistinctOn + | Kind::FromFunction => return Ok(()), + Kind::JoinOn => "set-returning functions are not allowed in JOIN conditions".to_owned(), + Kind::Where + | Kind::Having + | Kind::Filter + | Kind::Limit + | Kind::Offset + | Kind::Values + | Kind::ColumnDefault + | Kind::Check + | Kind::IndexExpression + | Kind::IndexPredicate + | Kind::ExecuteParameter => { + format!("set-returning functions are not allowed in {}", self.kind.name()) + } + Kind::ValuesSingle | Kind::Other => { + format!("set-returning function {signature} is not supported yet") + } + }; + Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, message).at_opt(at)) + } + + /// The error of a call of a function that gives a set in the arguments of `CASE` or `COALESCE`, when the analysis made such a call after `before`, which is the number of the calls before the arguments. + pub(crate) fn no_srf_since(&self, before: usize, construct: &str) -> Result<()> { + if self.srfs.0 == before { + return Ok(()); + } + Err(Error::new( + SqlState::FEATURE_NOT_SUPPORTED, + format!("set-returning functions are not allowed in {construct}"), + ) + .with_hint(crate::agg::SRF_HINT) + .at_opt(self.srfs.1)) + } + /// The errors of a function that takes or gives `internal`. fn check_internal(&self, declared: &[u32], result: u32, at: Option) -> Result<()> { if declared.contains(&oid::INTERNAL) { @@ -807,6 +842,7 @@ impl Analyzer<'_> { args, form: FuncForm::Operator(op.oid), variadic: false, + retset: false, }), ty: result, typmod: -1, diff --git a/crates/rupg-analyze/src/select.rs b/crates/rupg-analyze/src/select.rs index 8e4eb3a..bc4abab 100644 --- a/crates/rupg-analyze/src/select.rs +++ b/crates/rupg-analyze/src/select.rs @@ -115,6 +115,11 @@ impl Query { out } + /// `hasTargetSRFs`: true when a target calls a function that gives a set. + pub fn has_target_srfs(&self) -> bool { + self.targets.iter().any(|t| t.expr.first_set_call().is_some()) + } + /// The expressions of the query and of its subqueries in `FROM`, each with its depth: `depth` for the query, and one more for each subquery in `FROM` between the expression and the query. The subqueries in `FROM` come after the expressions of the query, as in `query_tree_walker`. pub fn all_exprs(&self, depth: usize) -> Vec<(&Expr, usize)> { let mut out: Vec<(&Expr, usize)> = self.exprs().into_iter().map(|e| (e, depth)).collect(); diff --git a/crates/rupg-analyze/src/sublink.rs b/crates/rupg-analyze/src/sublink.rs index 8acf9a6..628da74 100644 --- a/crates/rupg-analyze/src/sublink.rs +++ b/crates/rupg-analyze/src/sublink.rs @@ -27,7 +27,9 @@ impl Analyzer<'_> { self.outer.push(scope); let kind = std::mem::replace(&mut self.kind, Kind::Other); let has_aggs = std::mem::replace(&mut self.has_aggs, false); + let srfs = self.srfs; let result = self.select(stmt); + self.srfs = srfs; self.has_aggs = has_aggs; self.kind = kind; self.scope = self.outer.pop().unwrap_or_default(); diff --git a/crates/rupg-analyze/src/transform.rs b/crates/rupg-analyze/src/transform.rs index e0d9aba..6e03817 100644 --- a/crates/rupg-analyze/src/transform.rs +++ b/crates/rupg-analyze/src/transform.rs @@ -77,14 +77,21 @@ impl Analyzer<'_> { Node::BoolExpr(b) => self.transform_bool(b), Node::FuncCall(f) => self.transform_func(f), Node::TypeCast(t) => self.transform_cast(t), - Node::CaseExpr(c) => self.transform_case(c), + Node::CaseExpr(c) => { + let before = self.srfs.0; + let expr = self.transform_case(c)?; + self.no_srf_since(before, "CASE")?; + Ok(expr) + } Node::CoalesceExpr(c) => { + let before = self.srfs.0; let args = self.transform_list(&c.args)?; let ty = self.common_type(&args, "COALESCE")?; let args = args .into_iter() .map(|a| self.coerce_to_common(a, ty, "COALESCE")) .collect::>>()?; + self.no_srf_since(before, "COALESCE")?; Ok(Expr { kind: ExprKind::Coalesce(args), ty, diff --git a/crates/rupg-analyze/src/values.rs b/crates/rupg-analyze/src/values.rs index 913d27f..042ddba 100644 --- a/crates/rupg-analyze/src/values.rs +++ b/crates/rupg-analyze/src/values.rs @@ -20,13 +20,14 @@ impl Analyzer<'_> { return Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "WITH is not supported yet")); } let mut columns: Vec> = Vec::new(); + let kind = if s.valuesLists.len() == 1 { Kind::ValuesSingle } else { Kind::Values }; for (row, list) in s.valuesLists.iter().enumerate() { let Some(Node::List(list)) = list else { return Err(Error::internal("a VALUES row that is not a list")); }; let mut exprs = Vec::with_capacity(list.len()); for node in list { - exprs.push(self.with_kind(Kind::Values, |a| a.transform(node.as_ref()))?); + exprs.push(self.with_kind(kind, |a| a.transform(node.as_ref()))?); } if row == 0 { columns = vec![Vec::with_capacity(s.valuesLists.len()); exprs.len()]; diff --git a/crates/rupg-exec/src/agg.rs b/crates/rupg-exec/src/agg.rs index bb2269f..8cfca37 100644 --- a/crates/rupg-exec/src/agg.rs +++ b/crates/rupg-exec/src/agg.rs @@ -413,6 +413,7 @@ pub(crate) fn rows<'a>( }) }) .collect::>()?; + let levels = super::set_levels(&query.targets); let mut rows = Vec::new(); for group in groups(eval, query, tuples)? { let mut states = aggs.iter().map(|a| a.start(eval)).collect::>>()?; @@ -465,7 +466,7 @@ pub(crate) fn rows<'a>( { continue; } - rows.push(query.targets.iter().map(|t| eval.eval(&t.expr)).collect::>>()?); + eval.project(&query.targets, &levels, &mut rows)?; } eval.aggs.clear(); Ok(rows) diff --git a/crates/rupg-exec/src/fold.rs b/crates/rupg-exec/src/fold.rs index 58a45f2..1dd6bf8 100644 --- a/crates/rupg-exec/src/fold.rs +++ b/crates/rupg-exec/src/fold.rs @@ -35,6 +35,7 @@ fn plan( && agg::calls(query).is_empty() && query.having.is_none() && query.offset.is_none() + && !query.has_target_srfs() { simple = match &query.limit { None => true, @@ -94,7 +95,7 @@ fn plan( Ok(out) } -/// `is_simple_subquery` of `prepjointree.c`: true when the planner pulls the subquery in `FROM` up into the query, so that its expressions fold where the query reads them. Such a subquery has no aggregate, `GROUP BY`, `HAVING`, `ORDER BY`, `DISTINCT`, `OFFSET` or `LIMIT`, and no volatile function in its target list. +/// `is_simple_subquery` of `prepjointree.c`: true when the planner pulls the subquery in `FROM` up into the query, so that its expressions fold where the query reads them. Such a subquery has no aggregate, `GROUP BY`, `HAVING`, `ORDER BY`, `DISTINCT`, `OFFSET` or `LIMIT`, and no volatile function and no function that gives a set in its target list. fn simple_subquery(query: &Query) -> bool { query.set_op.is_none() && !query.grouped @@ -104,6 +105,7 @@ fn simple_subquery(query: &Query) -> bool { && query.offset.is_none() && query.limit.is_none() && !query.targets.iter().any(|t| volatile(&t.expr)) + && !query.has_target_srfs() } /// `attrs_used`: for each relation of the query, the columns that the expressions of the query read, also in its subqueries. @@ -133,7 +135,7 @@ fn used(query: &Query) -> Vec> { used } -/// `remove_unused_subquery_outputs`: each column of a subquery in `FROM` that the query does not read becomes a null constant, so that it does not run. With a plain `DISTINCT`, all the columns stay. A column that `ORDER BY`, `GROUP BY` or `DISTINCT` reads, or that calls a volatile function, stays. +/// `remove_unused_subquery_outputs`: each column of a subquery in `FROM` that the query does not read becomes a null constant, so that it does not run. With a plain `DISTINCT`, all the columns stay. A column that `ORDER BY`, `GROUP BY` or `DISTINCT` reads, or that calls a volatile function or a function that gives a set, stays. fn prune(sub: &mut Query, used: &[bool]) { if !sub.distinct.is_empty() && !sub.distinct_on { return; @@ -145,6 +147,7 @@ fn prune(sub: &mut Query, used: &[bool]) { || used.get(i).is_none_or(|u| *u) || keep.contains(&i) || volatile(&target.expr) + || target.expr.first_set_call().is_some() { continue; } @@ -395,7 +398,7 @@ impl<'a> Fold<'a> { } ExprKind::Func(f) => { let args = self.list(&f.args)?; - let func = Func { oid: f.oid, args, form: f.form, variadic: f.variadic }; + let func = Func { args, ..f.clone() }; self.function(with(expr, ExprKind::Func(func))) } ExprKind::Relabel(arg, form) => { diff --git a/crates/rupg-exec/src/lib.rs b/crates/rupg-exec/src/lib.rs index 7becfe7..ea6c813 100644 --- a/crates/rupg-exec/src/lib.rs +++ b/crates/rupg-exec/src/lib.rs @@ -1,6 +1,6 @@ //! The vectorized executor, the point path, compressed execution, spill, the per-distinct result tables of document 14 section 14.6. //! -//! This version reads the tables of the catalog. It joins the parts of `FROM`, tests the `WHERE` clause and computes the target list for each row. Then it applies `DISTINCT`, `ORDER BY`, `OFFSET` and `LIMIT` as the plan of PostgreSQL does with the nodes `Sort`, `Unique` and `Limit`. [`prepare`] checks that the engine has every function and every type of the query, so that a query that the engine cannot run is an error `0A000` before it runs. [`Plan::run`] then computes the row as `ExecInterpExpr` of PostgreSQL does. +//! This version reads the tables of the catalog. It joins the parts of `FROM`, tests the `WHERE` clause and computes the target list for each row, with the values of the functions that give a set as the node `ProjectSet` gives them. Then it applies `DISTINCT`, `ORDER BY`, `OFFSET` and `LIMIT` as the plan of PostgreSQL does with the nodes `Sort`, `Unique` and `Limit`. [`prepare`] checks that the engine has every function and every type of the query, so that a query that the engine cannot run is an error `0A000` before it runs. [`Plan::run`] then computes the row as `ExecInterpExpr` of PostgreSQL does. #![forbid(unsafe_code)] @@ -14,7 +14,7 @@ use rupg_analyze::{ use rupg_common::{Error, Result, SqlState}; use rupg_func::{Call, Kernel, Session, base_type}; use rupg_pgcatalog::builtin; -use rupg_types::{Array, ArrayDim, MAXDIM, Value, format_type, oid}; +use rupg_types::{Array, ArrayDim, MAXDIM, Record, Value, format_type, oid}; /// A query that the engine can run. #[derive(Debug)] @@ -75,6 +75,40 @@ fn strict(func: u32) -> bool { builtin::proc_by_oid(func).is_none_or(|p| p.strict) } +/// The types of the `OUT`, `INOUT` and `TABLE` arguments of a function, which are the fields of the rows that it gives. +fn out_types(func: u32) -> Vec { + let Some(proc) = builtin::proc_by_oid(func) else { return Vec::new() }; + let (Some(types), Some(modes)) = (proc.allargtypes, proc.argmodes) else { return Vec::new() }; + types + .iter() + .zip(modes) + .filter(|(_, mode)| matches!(mode, b'o' | b'b' | b't')) + .map(|(ty, _)| *ty) + .collect() +} + +/// The calls of functions that give a set in the targets, by level, as `split_pathtarget_at_srfs` puts them in `ProjectSet` nodes: a call is one level above the highest call in its arguments. +fn set_levels(targets: &[Target]) -> Vec> { + fn walk<'a>(expr: &'a Expr, levels: &mut Vec>) -> usize { + let below = expr.children().into_iter().map(|c| walk(c, levels)).max().unwrap_or(0); + match &expr.kind { + ExprKind::Func(f) if f.retset => { + if levels.len() == below { + levels.push(Vec::new()); + } + levels[below].push(expr); + below + 1 + } + _ => below, + } + } + let mut levels = Vec::new(); + for target in targets { + walk(&target.expr, &mut levels); + } + levels +} + /// Checks that the engine has every function and every type that an expression uses. fn check(expr: &Expr) -> Result<()> { match &expr.kind { @@ -91,6 +125,19 @@ fn check(expr: &Expr) -> Result<()> { } check_query(&sub.query, false)?; } + ExprKind::Func(f) if f.retset => { + set_kernel(f.oid, expr.location)?; + if expr.ty == oid::RECORD + && out_types(f.oid).into_iter().any(rupg_analyze::types::is_polymorphic) + { + let error = not_yet(format!("the call of {} in a target", signature(f.oid))); + return Err(match expr.location { + Some(at) => error.at(at), + None => error, + }); + } + f.args.iter().try_for_each(check)?; + } ExprKind::Func(f) => { // The engine runs the body of a function in SQL, so such a function needs no kernel. if !builtin::is_system_proc(f.oid) { @@ -187,6 +234,9 @@ fn check_query(query: &Query, output: bool) -> Result<()> { return Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "could not implement DISTINCT") .with_detail(MIXED_KEYS)); } + if query.group.iter().any(|g| query.targets[g.target].expr.first_set_call().is_some()) { + return Err(not_yet("a set-returning function in GROUP BY")); + } for target in &query.targets { if output && !rupg_func::output_supported(target.expr.ty) { return Err(not_yet(format!("output of type {}", format_type(target.expr.ty)))); @@ -550,6 +600,7 @@ fn run_query( && agg::calls(query).is_empty() && query.having.is_none() && query.offset.is_none() + && !query.has_target_srfs() { simple = match &query.limit { None => true, @@ -632,12 +683,14 @@ fn run_query( Some(count) if !ordered => offset.saturating_add(count), _ => usize::MAX, }; + let levels = set_levels(&query.targets); let mut rows = Vec::with_capacity(tuples.len().min(needed)); - for tuple in tuples.iter().take(needed) { + for tuple in &tuples { + if rows.len() >= needed { + break; + } eval.tuple = tuple; - let row = - query.targets.iter().map(|t| eval.eval(&t.expr)).collect::>>()?; - rows.push(row); + eval.project(&query.targets, &levels, &mut rows)?; } rows }; @@ -785,6 +838,8 @@ struct Eval<'a> { sub: Vec>, /// The values that the checks of a domain test now, for `DomainValue`. domain_value: Vec, + /// The value of each call of a function that gives a set in the row that the targets compute now, by the address of the call in the query. + sets: Vec<(*const Expr, Value)>, } impl<'a> Eval<'a> { @@ -807,6 +862,7 @@ impl<'a> Eval<'a> { outer, sub: Vec::new(), domain_value: Vec::new(), + sets: Vec::new(), } } @@ -856,6 +912,51 @@ impl<'a> Eval<'a> { kernel(&c, &args) } + /// `ExecProjectSet`: the rows of the targets for the row of the query. The calls of functions that give a set run level by level, and the calls of a level read the values of the levels below. The calls of one level go side by side, and a call with fewer values gives nulls, as `ExecProjectSRF` does. When no call of a level gives a value, the row gives no rows. + fn project( + &mut self, + targets: &[Target], + levels: &[Vec<&Expr>], + out: &mut Vec>, + ) -> Result<()> { + let Some((calls, above)) = levels.split_first() else { + out.push(targets.iter().map(|t| self.eval(&t.expr)).collect::>()?); + return Ok(()); + }; + let sets = calls.iter().map(|call| self.set_values(call)).collect::>>()?; + let count = sets.iter().map(Vec::len).max().unwrap_or(0); + let base = self.sets.len(); + for i in 0..count { + for (call, set) in calls.iter().zip(&sets) { + let value = set.get(i).cloned().unwrap_or(Value::Null); + self.sets.push((std::ptr::from_ref(*call), value)); + } + let result = self.project(targets, above, out); + self.sets.truncate(base); + result?; + } + Ok(()) + } + + /// `ExecMakeFunctionResultSet`: the values of a call of a function that gives a set. A function that gives rows of more than one column, or of type `record`, gives a row value for each row. + fn set_values(&mut self, call: &Expr) -> Result> { + let ExprKind::Func(f) = &call.kind else { + return Err(Error::internal("a set that is not a function call")); + }; + let rows = self.function_set(call, 1)?; + let types = out_types(f.oid); + Ok(rows + .into_iter() + .map(|mut row| { + if row.len() == 1 && call.ty != oid::RECORD { + row.pop().unwrap_or(Value::Null) + } else { + Value::Record(Box::new(Record { types: types.clone(), values: row })) + } + }) + .collect()) + } + /// A new state for the evaluation of a condition on another tuple of the same query. fn at<'b>(&self, tuple: &'b [usize]) -> Eval<'b> where @@ -872,6 +973,7 @@ impl<'a> Eval<'a> { outer: self.outer, sub: Vec::new(), domain_value: Vec::new(), + sets: Vec::new(), } } @@ -911,6 +1013,16 @@ impl<'a> Eval<'a> { let value = n.checked_sub(1).and_then(|i| self.params.get(i)); value.cloned().ok_or_else(|| Error::internal("a parameter has no value")) } + ExprKind::Func(f) if f.retset => { + let at = std::ptr::from_ref(expr); + let value = self.sets.iter().rev().find(|(call, _)| *call == at); + value.map(|(_, v)| v.clone()).ok_or_else(|| { + Error::new( + SqlState::FEATURE_NOT_SUPPORTED, + "set-valued function called in context that cannot accept a set", + ) + }) + } ExprKind::Func(f) => self.func(f, expr), ExprKind::Relabel(arg, _) => Ok(relabel(self.eval(arg)?, expr.ty)), ExprKind::CoerceViaIo(arg, _) => { @@ -1413,6 +1525,7 @@ fn evaluate(expr: &Expr, session: &dyn Session) -> Result { outer: &[], sub: Vec::new(), domain_value: Vec::new(), + sets: Vec::new(), }; eval.eval(expr) } @@ -1445,6 +1558,7 @@ pub fn check_input(ty: u32, value: &Value, session: &dyn Session) -> Result<()> outer: &[], sub: Vec::new(), domain_value: Vec::new(), + sets: Vec::new(), }; for value in values { eval.domain_check(domain, value.clone())?; diff --git a/crates/rupg-func/src/acl.rs b/crates/rupg-func/src/acl.rs index f529ab6..8d0381e 100644 --- a/crates/rupg-func/src/acl.rs +++ b/crates/rupg-func/src/acl.rs @@ -777,7 +777,18 @@ pub(crate) fn aclexplode(call: &Call<'_>, args: &[Value]) -> Result, args: &[Value]) -> Result { + if let Some(Value::Text(name)) = args.first() { + table_name(name, call.session)?; + } + Ok(Value::Bool(false)) +} + pub(crate) fn by_src(src: &str) -> Option { + if matches!(src, "row_security_active" | "row_security_active_name") { + return Some(row_security_active); + } let family = if src.starts_with("pg_has_role") { "pg_has_role" } else { diff --git a/crates/rupg-func/src/deparse.rs b/crates/rupg-func/src/deparse.rs index 6173895..39f9f52 100644 --- a/crates/rupg-func/src/deparse.rs +++ b/crates/rupg-func/src/deparse.rs @@ -241,8 +241,9 @@ impl<'a> Deparser<'a> { ExprKind::CoerceToDomain(arg, _) => self.coercion(arg, e.ty, e.typmod, e)?, ExprKind::DomainValue => self.buf.push_str("VALUE"), ExprKind::Subscript(sub) => { - // The array needs parentheses unless it is a column, also when it is another subscript. - let parens = !matches!(sub.container.kind, ExprKind::Var(_)); + // The array needs parentheses unless it is a column or a field, also when it is another subscript. + let parens = + !matches!(sub.container.kind, ExprKind::Var(_) | ExprKind::FieldSelect(..)); if parens { self.buf.push('('); } @@ -1089,6 +1090,11 @@ fn looks_like_function(e: &Expr) -> bool { } } +/// `pg_get_statisticsobjdef`, `pg_get_statisticsobjdef_columns` and `pg_get_statisticsobjdef_expressions`: the text of an object of extended statistics. rupg has no such objects, and an OID that is not an object gives null. +fn get_statisticsobjdef(_: &Call<'_>, _: &[Value]) -> Result { + Ok(Value::Null) +} + /// `pg_get_indexdef(oid)` and `pg_get_indexdef(oid, int4, bool)`. fn get_indexdef(call: &Call<'_>, args: &[Value]) -> Result { let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?; @@ -1119,6 +1125,9 @@ pub(crate) fn by_src(src: &str) -> Option { "pg_get_expr" | "pg_get_expr_ext" => get_expr, "pg_get_constraintdef" | "pg_get_constraintdef_ext" => get_constraintdef, "pg_get_indexdef" | "pg_get_indexdef_ext" => get_indexdef, + "pg_get_statisticsobjdef" + | "pg_get_statisticsobjdef_columns" + | "pg_get_statisticsobjdef_expressions" => get_statisticsobjdef, "pg_get_function_arguments" => function::get_function_arguments, "pg_get_function_identity_arguments" => function::get_function_identity_arguments, "pg_get_function_result" => function::get_function_result, diff --git a/crates/rupg-func/src/srf.rs b/crates/rupg-func/src/srf.rs index 71a8945..8eaa177 100644 --- a/crates/rupg-func/src/srf.rs +++ b/crates/rupg-func/src/srf.rs @@ -1,4 +1,4 @@ -//! The set-returning functions that a query can call in `FROM`: `generate_series` of the integer types, `unnest`, `generate_subscripts`, `string_to_table`, `regexp_matches`, `regexp_split_to_table`, `pg_options_to_table`, `aclexplode`, `pg_get_keywords`, `pg_show_all_settings`, `pg_stat_get_activity`, `pg_stat_get_backend_idset`, `pg_prepared_statement`, the functions of the views of replication, of progress and of the prepared transactions, the functions of the statistics of the whole server that give rows, the functions of the views of asynchronous I/O, shared memory, replication origins, recovery, publications and cursors, and the functions of the views of the time zones. +//! The set-returning functions that a query can call in `FROM` or in its target list: `generate_series` of the integer types, `unnest`, `generate_subscripts`, `string_to_table`, `regexp_matches`, `regexp_split_to_table`, `pg_options_to_table`, `aclexplode`, `pg_get_keywords`, `pg_show_all_settings`, `pg_stat_get_activity`, `pg_stat_get_backend_idset`, `pg_prepared_statement`, the functions of the views of replication, of progress and of the prepared transactions, the functions of the statistics of the whole server that give rows, the functions of the views of asynchronous I/O, shared memory, replication origins, recovery, publications and cursors, and the functions of the views of the time zones. //! //! A set kernel gives all the rows of the call at once, as a function of PostgreSQL in the materialize mode gives a tuplestore. Each row has one value for each column of the result: one value for a function of a scalar type, or one value for each `OUT` parameter. diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 77cb255..a3ee792 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1811,7 +1811,7 @@ mod tests { big_stack(|| script(include_str!("connection/views.test"))); } - /// The views that `system_views.sql` makes in `pg_catalog` when the cluster starts: their rows in `pg_class`, `pg_type`, `pg_rewrite` and `pg_attribute`, with the privileges and the options, and the queries that read them. The script also shows the options of `CREATE VIEW` and their errors. `pg_stats_ext_exprs` is not there yet. `connection/system_views.test` is the output of PostgreSQL 19 for the same script in a new database. + /// The views that `system_views.sql` makes in `pg_catalog` when the cluster starts: their rows in `pg_class`, `pg_type`, `pg_rewrite` and `pg_attribute`, with the privileges and the options, and the queries that read them. The script also shows the options of `CREATE VIEW` and their errors, and `row_security_active` and `pg_get_statisticsobjdef_expressions`, which the view `pg_stats_ext_exprs` calls. `connection/system_views.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn system_views() { big_stack(|| script(include_str!("connection/system_views.test"))); @@ -1901,6 +1901,12 @@ mod tests { big_stack(|| script(include_str!("connection/regex_matches.test"))); } + /// The functions that give a set in the target list, as the nodes `ProjectSet` of PostgreSQL run them: calls side by side with nulls for the shorter sets, nested calls, calls inside expressions, the rows with no values, the calls above aggregates and below `ORDER BY`, `DISTINCT` and `LIMIT`, subqueries, row values and each error of a call in a wrong place. `connection/target_srf.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn target_srf() { + big_stack(|| script(include_str!("connection/target_srf.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/system_views.test b/crates/rupg-session/src/connection/system_views.test index fc10539..8c82600 100644 --- a/crates/rupg-session/src/connection/system_views.test +++ b/crates/rupg-session/src/connection/system_views.test @@ -1,4 +1,4 @@ -> SELECT relname, relkind, relnatts, relhasrules, relpersistence, relowner = 10, reltype::regtype, reloptions, relacl FROM pg_class WHERE relnamespace = 'pg_catalog'::regnamespace AND relkind = 'v' AND relname <> 'pg_stats_ext_exprs' ORDER BY relname +> SELECT relname, relkind, relnatts, relhasrules, relpersistence, relowner = 10, reltype::regtype, reloptions, relacl FROM pg_class WHERE relnamespace = 'pg_catalog'::regnamespace AND relkind = 'v' ORDER BY relname fields relname@pg_class.2:19:64:-1,relkind@pg_class.18:18:1:-1,relnatts@pg_class.19:21:2:-1,relhasrules@pg_class.21:16:1:-1,relpersistence@pg_class.17:18:1:-1,?column?:16:1:-1,reltype:2206:4:-1,reloptions@pg_class.33:1009:-1:-1,relacl@pg_class.32:1034:-1:-1 row pg_aios,v,15,t,p,t,pg_aios,NULL,{postgres=arwdDxtm/postgres,pg_read_all_stats=r/postgres} row pg_available_extension_versions,v,10,t,p,t,pg_available_extension_versions,NULL,{postgres=arwdDxtm/postgres,=r/postgres} @@ -78,6 +78,7 @@ row pg_statio_user_sequences,v,6,t,p,t,pg_statio_user_sequences,NULL,{postgres=a row pg_statio_user_tables,v,12,t,p,t,pg_statio_user_tables,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_stats,v,19,t,p,t,pg_stats,{security_barrier=true},{postgres=arwdDxtm/postgres,=r/postgres} row pg_stats_ext,v,17,t,p,t,pg_stats_ext,{security_barrier=true},{postgres=arwdDxtm/postgres,=r/postgres} +row pg_stats_ext_exprs,v,22,t,p,t,pg_stats_ext_exprs,{security_barrier=true},{postgres=arwdDxtm/postgres,=r/postgres} row pg_tables,v,8,t,p,t,pg_tables,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_timezone_abbrevs,v,3,t,p,t,pg_timezone_abbrevs,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_timezone_names,v,4,t,p,t,pg_timezone_names,NULL,{postgres=arwdDxtm/postgres,=r/postgres} @@ -85,8 +86,8 @@ row pg_user,v,9,t,p,t,pg_user,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_user_mappings,v,6,t,p,t,pg_user_mappings,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_views,v,4,t,p,t,pg_views,NULL,{postgres=arwdDxtm/postgres,=r/postgres} row pg_wait_events,v,3,t,p,t,pg_wait_events,NULL,{postgres=arwdDxtm/postgres,=r/postgres} -SELECT 85 -> SELECT t.typname, t.typtype, t.typcategory, t.typarray::regtype, t.typnamespace::regnamespace FROM pg_type t, pg_class c WHERE t.typrelid = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' AND c.relname <> 'pg_stats_ext_exprs' ORDER BY t.typname +SELECT 86 +> SELECT t.typname, t.typtype, t.typcategory, t.typarray::regtype, t.typnamespace::regnamespace FROM pg_type t, pg_class c WHERE t.typrelid = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' ORDER BY t.typname fields typname@pg_type.2:19:64:-1,typtype@pg_type.7:18:1:-1,typcategory@pg_type.8:18:1:-1,typarray:2206:4:-1,typnamespace:4089:4:-1 row pg_aios,c,C,pg_aios[],pg_catalog row pg_available_extension_versions,c,C,pg_available_extension_versions[],pg_catalog @@ -166,6 +167,7 @@ row pg_statio_user_sequences,c,C,pg_statio_user_sequences[],pg_catalog row pg_statio_user_tables,c,C,pg_statio_user_tables[],pg_catalog row pg_stats,c,C,pg_stats[],pg_catalog row pg_stats_ext,c,C,pg_stats_ext[],pg_catalog +row pg_stats_ext_exprs,c,C,pg_stats_ext_exprs[],pg_catalog row pg_tables,c,C,pg_tables[],pg_catalog row pg_timezone_abbrevs,c,C,pg_timezone_abbrevs[],pg_catalog row pg_timezone_names,c,C,pg_timezone_names[],pg_catalog @@ -173,8 +175,8 @@ row pg_user,c,C,pg_user[],pg_catalog row pg_user_mappings,c,C,pg_user_mappings[],pg_catalog row pg_views,c,C,pg_views[],pg_catalog row pg_wait_events,c,C,pg_wait_events[],pg_catalog -SELECT 85 -> SELECT r.rulename, r.ev_class::regclass, r.ev_type, r.is_instead, r.ev_enabled FROM pg_rewrite r, pg_class c WHERE r.ev_class = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' AND c.relname <> 'pg_stats_ext_exprs' AND r.rulename = '_RETURN' ORDER BY c.relname +SELECT 86 +> SELECT r.rulename, r.ev_class::regclass, r.ev_type, r.is_instead, r.ev_enabled FROM pg_rewrite r, pg_class c WHERE r.ev_class = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' AND r.rulename = '_RETURN' ORDER BY c.relname fields rulename@pg_rewrite.2:19:64:-1,ev_class:2205:4:-1,ev_type@pg_rewrite.4:18:1:-1,is_instead@pg_rewrite.6:16:1:-1,ev_enabled@pg_rewrite.5:18:1:-1 row _RETURN,pg_aios,1,t,O row _RETURN,pg_available_extension_versions,1,t,O @@ -254,6 +256,7 @@ row _RETURN,pg_statio_user_sequences,1,t,O row _RETURN,pg_statio_user_tables,1,t,O row _RETURN,pg_stats,1,t,O row _RETURN,pg_stats_ext,1,t,O +row _RETURN,pg_stats_ext_exprs,1,t,O row _RETURN,pg_tables,1,t,O row _RETURN,pg_timezone_abbrevs,1,t,O row _RETURN,pg_timezone_names,1,t,O @@ -261,8 +264,8 @@ row _RETURN,pg_user,1,t,O row _RETURN,pg_user_mappings,1,t,O row _RETURN,pg_views,1,t,O row _RETURN,pg_wait_events,1,t,O -SELECT 85 -> SELECT c.relname, a.attnum, a.attname, a.atttypid::regtype, a.atttypmod, a.attlen, a.attbyval, a.attalign, a.attnotnull, a.attcollation <> 0 FROM pg_attribute a, pg_class c WHERE a.attrelid = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' AND c.relname <> 'pg_stats_ext_exprs' ORDER BY c.relname, a.attnum +SELECT 86 +> SELECT c.relname, a.attnum, a.attname, a.atttypid::regtype, a.atttypmod, a.attlen, a.attbyval, a.attalign, a.attnotnull, a.attcollation <> 0 FROM pg_attribute a, pg_class c WHERE a.attrelid = c.oid AND c.relnamespace = 'pg_catalog'::regnamespace AND c.relkind = 'v' ORDER BY c.relname, a.attnum fields relname@pg_class.2:19:64:-1,attnum@pg_attribute.5:21:2:-1,attname@pg_attribute.2:19:64:-1,atttypid:2206:4:-1,atttypmod@pg_attribute.6:23:4:-1,attlen@pg_attribute.4:21:2:-1,attbyval@pg_attribute.8:16:1:-1,attalign@pg_attribute.9:18:1:-1,attnotnull@pg_attribute.12:16:1:-1,?column?:16:1:-1 row pg_aios,1,pid,integer,-1,4,t,i,f,f row pg_aios,2,io_id,integer,-1,4,t,i,f,f @@ -1112,6 +1115,28 @@ row pg_stats_ext,14,most_common_vals,text[],-1,-1,f,i,f,t row pg_stats_ext,15,most_common_val_nulls,boolean[],-1,-1,f,i,f,f row pg_stats_ext,16,most_common_freqs,double precision[],-1,-1,f,d,f,f row pg_stats_ext,17,most_common_base_freqs,double precision[],-1,-1,f,d,f,f +row pg_stats_ext_exprs,1,schemaname,name,-1,64,f,c,f,t +row pg_stats_ext_exprs,2,tablename,name,-1,64,f,c,f,t +row pg_stats_ext_exprs,3,tableid,oid,-1,4,t,i,f,f +row pg_stats_ext_exprs,4,statistics_schemaname,name,-1,64,f,c,f,t +row pg_stats_ext_exprs,5,statistics_name,name,-1,64,f,c,f,t +row pg_stats_ext_exprs,6,statistics_id,oid,-1,4,t,i,f,f +row pg_stats_ext_exprs,7,statistics_owner,name,-1,64,f,c,f,t +row pg_stats_ext_exprs,8,expr,text,-1,-1,f,i,f,t +row pg_stats_ext_exprs,9,inherited,boolean,-1,1,t,c,f,f +row pg_stats_ext_exprs,10,null_frac,real,-1,4,t,i,f,f +row pg_stats_ext_exprs,11,avg_width,integer,-1,4,t,i,f,f +row pg_stats_ext_exprs,12,n_distinct,real,-1,4,t,i,f,f +row pg_stats_ext_exprs,13,most_common_vals,anyarray,-1,-1,f,d,f,f +row pg_stats_ext_exprs,14,most_common_freqs,real[],-1,-1,f,i,f,f +row pg_stats_ext_exprs,15,histogram_bounds,anyarray,-1,-1,f,d,f,f +row pg_stats_ext_exprs,16,correlation,real,-1,4,t,i,f,f +row pg_stats_ext_exprs,17,most_common_elems,anyarray,-1,-1,f,d,f,f +row pg_stats_ext_exprs,18,most_common_elem_freqs,real[],-1,-1,f,i,f,f +row pg_stats_ext_exprs,19,elem_count_histogram,real[],-1,-1,f,i,f,f +row pg_stats_ext_exprs,20,range_length_histogram,anyarray,-1,-1,f,d,f,f +row pg_stats_ext_exprs,21,range_empty_frac,real,-1,4,t,i,f,f +row pg_stats_ext_exprs,22,range_bounds_histogram,anyarray,-1,-1,f,d,f,f row pg_tables,1,schemaname,name,-1,64,f,c,f,t row pg_tables,2,tablename,name,-1,64,f,c,f,t row pg_tables,3,tableowner,name,-1,64,f,c,f,t @@ -1149,7 +1174,7 @@ row pg_views,4,definition,text,-1,-1,f,i,f,t row pg_wait_events,1,type,text,-1,-1,f,i,f,t row pg_wait_events,2,name,text,-1,-1,f,i,f,t row pg_wait_events,3,description,text,-1,-1,f,i,f,t -SELECT 885 +SELECT 907 > CREATE VIEW v1 WITH (security_barrier) AS SELECT 1 AS a CREATE VIEW > CREATE VIEW v2 WITH (security_invoker = 'TRUE', security_barrier = off) AS SELECT 1 AS a @@ -1202,3 +1227,131 @@ SELECT 1 fields rolname@pg_roles.1:19:64:-1,rolsuper@pg_roles.2:16:1:-1,rolinherit@pg_roles.3:16:1:-1,rolcanlogin@pg_roles.6:16:1:-1 row postgres,t,t,t SELECT 1 +> SELECT pg_get_viewdef('pg_stats_ext_exprs'::regclass, true) +fields pg_get_viewdef:25:-1:-1 +row SELECT cn.nspname AS schemaname, + c.relname AS tablename, + s.stxrelid AS tableid, + sn.nspname AS statistics_schemaname, + s.stxname AS statistics_name, + s.oid AS statistics_id, + pg_get_userbyid(s.stxowner) AS statistics_owner, + stat.expr, + sd.stxdinherit AS inherited, + (stat.a).stanullfrac AS null_frac, + (stat.a).stawidth AS avg_width, + (stat.a).stadistinct AS n_distinct, + CASE + WHEN (stat.a).stakind1 = 1 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 1 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 1 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 1 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 1 THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS most_common_vals, + CASE + WHEN (stat.a).stakind1 = 1 THEN (stat.a).stanumbers1 + WHEN (stat.a).stakind2 = 1 THEN (stat.a).stanumbers2 + WHEN (stat.a).stakind3 = 1 THEN (stat.a).stanumbers3 + WHEN (stat.a).stakind4 = 1 THEN (stat.a).stanumbers4 + WHEN (stat.a).stakind5 = 1 THEN (stat.a).stanumbers5 + ELSE NULL::real[] + END AS most_common_freqs, + CASE + WHEN (stat.a).stakind1 = 2 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 2 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 2 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 2 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 2 THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS histogram_bounds, + CASE + WHEN (stat.a).stakind1 = 3 THEN (stat.a).stanumbers1[1] + WHEN (stat.a).stakind2 = 3 THEN (stat.a).stanumbers2[1] + WHEN (stat.a).stakind3 = 3 THEN (stat.a).stanumbers3[1] + WHEN (stat.a).stakind4 = 3 THEN (stat.a).stanumbers4[1] + WHEN (stat.a).stakind5 = 3 THEN (stat.a).stanumbers5[1] + ELSE NULL::real + END AS correlation, + CASE + WHEN (stat.a).stakind1 = 4 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 4 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 4 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 4 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 4 THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS most_common_elems, + CASE + WHEN (stat.a).stakind1 = 4 THEN (stat.a).stanumbers1 + WHEN (stat.a).stakind2 = 4 THEN (stat.a).stanumbers2 + WHEN (stat.a).stakind3 = 4 THEN (stat.a).stanumbers3 + WHEN (stat.a).stakind4 = 4 THEN (stat.a).stanumbers4 + WHEN (stat.a).stakind5 = 4 THEN (stat.a).stanumbers5 + ELSE NULL::real[] + END AS most_common_elem_freqs, + CASE + WHEN (stat.a).stakind1 = 5 THEN (stat.a).stanumbers1 + WHEN (stat.a).stakind2 = 5 THEN (stat.a).stanumbers2 + WHEN (stat.a).stakind3 = 5 THEN (stat.a).stanumbers3 + WHEN (stat.a).stakind4 = 5 THEN (stat.a).stanumbers4 + WHEN (stat.a).stakind5 = 5 THEN (stat.a).stanumbers5 + ELSE NULL::real[] + END AS elem_count_histogram, + CASE + WHEN (stat.a).stakind1 = 6 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 6 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 6 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 6 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 6 THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS range_length_histogram, + CASE + WHEN (stat.a).stakind1 = 6 THEN (stat.a).stanumbers1[1] + WHEN (stat.a).stakind2 = 6 THEN (stat.a).stanumbers2[1] + WHEN (stat.a).stakind3 = 6 THEN (stat.a).stanumbers3[1] + WHEN (stat.a).stakind4 = 6 THEN (stat.a).stanumbers4[1] + WHEN (stat.a).stakind5 = 6 THEN (stat.a).stanumbers5[1] + ELSE NULL::real + END AS range_empty_frac, + CASE + WHEN (stat.a).stakind1 = 7 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 7 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 7 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 7 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 7 THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS range_bounds_histogram + FROM pg_statistic_ext s + JOIN pg_class c ON c.oid = s.stxrelid + LEFT JOIN pg_statistic_ext_data sd ON s.oid = sd.stxoid + LEFT JOIN pg_namespace cn ON cn.oid = c.relnamespace + LEFT JOIN pg_namespace sn ON sn.oid = s.stxnamespace + JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr, + unnest(sd.stxdexpr) AS a) stat ON stat.expr IS NOT NULL + WHERE pg_has_role(c.relowner, 'USAGE'::text) AND (c.relrowsecurity = false OR NOT row_security_active(c.oid)); +SELECT 1 +> SELECT count(*) FROM pg_stats_ext_exprs +fields count:20:8:-1 +row 0 +SELECT 1 +> SELECT row_security_active('pg_class'), row_security_active('pg_catalog.pg_proc'::regclass), row_security_active(0), row_security_active(NULL::oid) +fields row_security_active:16:1:-1,row_security_active:16:1:-1,row_security_active:16:1:-1,row_security_active:16:1:-1 +row f,f,f,NULL +SELECT 1 +> SELECT row_security_active('no_such_table') +fields row_security_active:16:1:-1 +ERROR 42P01 relation "no_such_table" does not exist +> SELECT row_security_active('pg_catalog.no_such_table') +fields row_security_active:16:1:-1 +ERROR 42P01 relation "pg_catalog.no_such_table" does not exist +> SELECT row_security_active('no_schema.pg_class') +fields row_security_active:16:1:-1 +ERROR 3F000 schema "no_schema" does not exist +> SELECT pg_get_statisticsobjdef(0), pg_get_statisticsobjdef_columns(0), pg_get_statisticsobjdef_expressions(0), pg_get_statisticsobjdef_expressions('pg_class'::regclass) +fields pg_get_statisticsobjdef:25:-1:-1,pg_get_statisticsobjdef_columns:25:-1:-1,pg_get_statisticsobjdef_expressions:1009:-1:-1,pg_get_statisticsobjdef_expressions:1009:-1:-1 +row NULL,NULL,NULL,NULL +SELECT 1 +> SELECT count(*) FROM pg_stats_ext_exprs WHERE expr IS NOT NULL +fields count:20:8:-1 +row 0 +SELECT 1 diff --git a/crates/rupg-session/src/connection/target_srf.test b/crates/rupg-session/src/connection/target_srf.test new file mode 100644 index 0000000..3731585 --- /dev/null +++ b/crates/rupg-session/src/connection/target_srf.test @@ -0,0 +1,367 @@ +> SELECT generate_series(1, 3) AS g +fields g:23:4:-1 +row 1 +row 2 +row 3 +SELECT 3 +> SELECT generate_series(1, 3) AS a, generate_series(1, 2) AS b +fields a:23:4:-1,b:23:4:-1 +row 1,1 +row 2,2 +row 3,NULL +SELECT 3 +> SELECT generate_series(1, 3) AS a, unnest(ARRAY['x', 'y', 'z', 'w']) AS b +fields a:23:4:-1,b:25:-1:-1 +row 1,x +row 2,y +row 3,z +row NULL,w +SELECT 4 +> SELECT generate_series(1, 0) AS g +fields g:23:4:-1 +SELECT 0 +> SELECT 1 AS k, generate_series(1, 0) AS g +fields k:23:4:-1,g:23:4:-1 +SELECT 0 +> SELECT generate_series(1, 2) AS a, generate_series(1, 0) AS b +fields a:23:4:-1,b:23:4:-1 +row 1,NULL +row 2,NULL +SELECT 2 +> SELECT generate_series(1, 3) + 10 AS g +fields g:23:4:-1 +row 11 +row 12 +row 13 +SELECT 3 +> SELECT generate_series(1, generate_series(1, 3)) AS g +fields g:23:4:-1 +row 1 +row 1 +row 2 +row 1 +row 2 +row 3 +SELECT 6 +> SELECT generate_series(1, generate_series(1, 3)) AS g, generate_series(1, 2) AS h +fields g:23:4:-1,h:23:4:-1 +row 1,1 +row 1,2 +row 2,2 +row 1,NULL +row 2,NULL +row 3,NULL +SELECT 6 +> SELECT generate_series(1, 3) AS a, generate_series(2, generate_series(1, 3)) AS b +fields a:23:4:-1,b:23:4:-1 +row 2,2 +row 3,2 +row 3,3 +SELECT 3 +> SELECT unnest(ARRAY[generate_series(1, 2), 10]) AS u +fields u:23:4:-1 +row 1 +row 10 +row 2 +row 10 +SELECT 4 +> SELECT x, generate_series(1, x) AS g FROM (VALUES (1), (2), (3)) v(x) +fields x:23:4:-1,g:23:4:-1 +row 1,1 +row 2,1 +row 2,2 +row 3,1 +row 3,2 +row 3,3 +SELECT 6 +> SELECT x, generate_series(1, x) AS g FROM (VALUES (1), (2), (3)) v(x) ORDER BY 2 DESC, 1 +fields x:23:4:-1,g:23:4:-1 +row 3,3 +row 2,2 +row 3,2 +row 1,1 +row 2,1 +row 3,1 +SELECT 6 +> SELECT DISTINCT generate_series(1, x) AS g FROM (VALUES (1), (2), (3)) v(x) ORDER BY 1 +fields g:23:4:-1 +row 1 +row 2 +row 3 +SELECT 3 +> SELECT x, generate_series(1, x) AS g FROM (VALUES (3), (2)) v(x) LIMIT 4 +fields x:23:4:-1,g:23:4:-1 +row 3,1 +row 3,2 +row 3,3 +row 2,1 +SELECT 4 +> SELECT generate_series(1, 5) AS g LIMIT 2 OFFSET 1 +fields g:23:4:-1 +row 2 +row 3 +SELECT 2 +> SELECT count(*) AS c, generate_series(1, 3) AS g FROM (VALUES (1), (2)) v(x) +fields c:20:8:-1,g:23:4:-1 +row 2,1 +row 2,2 +row 2,3 +SELECT 3 +> SELECT generate_series(1, count(*)::int) AS g FROM (VALUES (1), (2)) v(x) +fields g:23:4:-1 +row 1 +row 2 +SELECT 2 +> SELECT max(x) AS m, generate_series(1, max(x)) AS g FROM (VALUES (2), (1)) v(x) +fields m:23:4:-1,g:23:4:-1 +row 2,1 +row 2,2 +SELECT 2 +> SELECT x, generate_series(1, 2) AS g FROM (VALUES (1), (2)) v(x) GROUP BY x HAVING x > 1 +fields x:23:4:-1,g:23:4:-1 +row 2,1 +row 2,2 +SELECT 2 +> SELECT count(*) AS c, generate_series(1, 2) AS g FROM (VALUES (1)) v(x) WHERE x > 5 +fields c:20:8:-1,g:23:4:-1 +row 0,1 +row 0,2 +SELECT 2 +> SELECT unnest(ARRAY[1, NULL, 3]) AS u +fields u:23:4:-1 +row 1 +row NULL +row 3 +SELECT 3 +> SELECT unnest(NULL::int[]) AS u +fields u:23:4:-1 +SELECT 0 +> SELECT 7 AS k, unnest(NULL::int[]) AS u +fields k:23:4:-1,u:23:4:-1 +SELECT 0 +> SELECT unnest(ARRAY[[1, 2], [3, 4]]) AS u +fields u:23:4:-1 +row 1 +row 2 +row 3 +row 4 +SELECT 4 +> SELECT generate_series(1, x) AS g FROM (VALUES (1), (NULL), (2)) v(x) +fields g:23:4:-1 +row 1 +row 1 +row 2 +SELECT 3 +> SELECT a, unnest(a) AS u FROM (VALUES (ARRAY[1, 2]), (ARRAY[]::int[]), (ARRAY[3])) v(a) +fields a:1007:-1:-1,u:23:4:-1 +row {1,2},1 +row {1,2},2 +row {3},3 +SELECT 3 +> SELECT (SELECT generate_series(1, 1)) AS s +fields s:23:4:-1 +row 1 +SELECT 1 +> SELECT (SELECT generate_series(1, 2)) AS s +fields s:23:4:-1 +ERROR 21000 more than one row returned by a subquery used as an expression +> SELECT (SELECT generate_series(1, 0)) AS s +fields s:23:4:-1 +row NULL +SELECT 1 +> SELECT ARRAY(SELECT generate_series(1, 4)) AS a +fields a:1007:-1:-1 +row {1,2,3,4} +SELECT 1 +> SELECT EXISTS (SELECT generate_series(1, 0)) AS e, EXISTS (SELECT generate_series(1, 2)) AS f +fields e:16:1:-1,f:16:1:-1 +row f,t +SELECT 1 +> SELECT 5 IN (SELECT generate_series(1, 10)) AS i, 50 IN (SELECT generate_series(1, 10)) AS j +fields i:16:1:-1,j:16:1:-1 +row t,f +SELECT 1 +> SELECT generate_series(1, 3) AS g ORDER BY g DESC +fields g:23:4:-1 +row 3 +row 2 +row 1 +SELECT 3 +> SELECT 1 AS one ORDER BY generate_series(1, 3) +fields one:23:4:-1 +row 1 +row 1 +row 1 +SELECT 3 +> SELECT x FROM (VALUES (5)) v(x) ORDER BY generate_series(1, 2) DESC +fields x:23:4:-1 +row 5 +row 5 +SELECT 2 +> SELECT * FROM (SELECT generate_series(1, 4) AS g) s WHERE g > 2 +fields g:23:4:-1 +row 3 +row 4 +SELECT 2 +> SELECT g, count(*) AS c FROM (SELECT generate_series(1, x) AS g FROM (VALUES (1), (2), (3)) v(x)) s GROUP BY g ORDER BY g +fields g:23:4:-1,c:20:8:-1 +row 1,3 +row 2,2 +row 3,1 +SELECT 3 +> SELECT array_agg(g) AS a FROM (SELECT generate_series(1, 4) AS g) s +fields a:1007:-1:-1 +row {1,2,3,4} +SELECT 1 +> SELECT count(*) AS c FROM (SELECT generate_series(1, 1000)) s +fields c:20:8:-1 +row 1000 +SELECT 1 +> SELECT count(*) AS c FROM (SELECT 1 AS one, generate_series(1, 3) AS g) s +fields c:20:8:-1 +row 3 +SELECT 1 +> SELECT generate_series(1, 3) UNION SELECT 5 ORDER BY 1 +fields generate_series:23:4:-1 +row 1 +row 2 +row 3 +row 5 +SELECT 4 +> SELECT (SELECT max(g) FROM (SELECT generate_series(1, x) AS g) s) AS m FROM (VALUES (2), (4)) v(x) +fields m:23:4:-1 +row 2 +row 4 +SELECT 2 +> SELECT DISTINCT ON (x % 2) x, generate_series(1, 2) AS g FROM (VALUES (1), (2), (3)) v(x) ORDER BY x % 2, g DESC +fields x:23:4:-1,g:23:4:-1 +row 2,2 +row 1,2 +SELECT 2 +> SELECT generate_series(1, 2) IS NULL AS n, generate_series(1, 2) = ANY (ARRAY[1]) AS q +fields n:16:1:-1,q:16:1:-1 +row f,t +row f,f +SELECT 2 +> SELECT unnest('{c,a,b}'::text[]) AS u ORDER BY 1 DESC +fields u:25:-1:-1 +row c +row b +row a +SELECT 3 +> SELECT regexp_matches('abcabc', '(b)(c)', 'g') AS m +fields m:1009:-1:-1 +row {b,c} +row {b,c} +SELECT 2 +> SELECT string_to_table('a,b,c', ',') AS t, generate_subscripts(ARRAY[5, 6], 1) AS s +fields t:25:-1:-1,s:23:4:-1 +row a,1 +row b,2 +row c,NULL +SELECT 3 +> SELECT regexp_split_to_table('one two', ' ') AS w +fields w:25:-1:-1 +row one +row two +SELECT 2 +> SELECT x, generate_series(1, 2) AS g FROM (VALUES (1), (2)) v(x) WHERE x = 2 +fields x:23:4:-1,g:23:4:-1 +row 2,1 +row 2,2 +SELECT 2 +> SELECT 1 WHERE generate_series(1, 2) > 1 +ERROR 0A000 set-returning functions are not allowed in WHERE at 16 +> SELECT CASE WHEN true THEN generate_series(1, 2) END +ERROR 0A000 set-returning functions are not allowed in CASE at 28 +> SELECT CASE generate_series(1, 2) WHEN 1 THEN 0 END +ERROR 0A000 set-returning functions are not allowed in CASE at 13 +> SELECT COALESCE(generate_series(1, 2), 0) +ERROR 0A000 set-returning functions are not allowed in COALESCE at 17 +> SELECT sum(generate_series(1, 3)) +ERROR 0A000 aggregate function calls cannot contain set-returning function calls at 12 +> SELECT 1 + sum(1 + generate_series(1, 3)) +ERROR 0A000 aggregate function calls cannot contain set-returning function calls at 20 +> SELECT 1 LIMIT generate_series(1, 2) +ERROR 0A000 set-returning functions are not allowed in LIMIT at 16 +> SELECT 1 OFFSET generate_series(1, 2) +ERROR 0A000 set-returning functions are not allowed in OFFSET at 17 +> SELECT count(*) FILTER (WHERE generate_series(1, 2) > 0) FROM (VALUES (1)) v(x) +ERROR 0A000 set-returning functions are not allowed in FILTER at 31 +> SELECT 1 FROM (VALUES (1)) a(x) JOIN (VALUES (1)) b(y) ON generate_series(1, 2) = 1 +ERROR 0A000 set-returning functions are not allowed in JOIN conditions at 59 +> VALUES (1), (generate_series(1, 2)) +ERROR 0A000 set-returning functions are not allowed in VALUES at 14 +> SELECT count(*) FROM (VALUES (1)) v(x) HAVING generate_series(1, 2) > 0 +ERROR 0A000 set-returning functions are not allowed in HAVING at 47 +> SELECT * FROM generate_series(1, generate_series(1, 2)) +ERROR 0A000 set-returning functions must appear at top level of FROM at 34 +> SELECT sum((SELECT generate_series(1, 1))) AS s +fields s:20:8:-1 +row 1 +SELECT 1 +> SELECT generate_series(1, 2) + NULL AS n +fields n:23:4:-1 +row NULL +SELECT 1 +> SELECT generate_series(1, 2) AS g, NULL::int + 1 AS n +fields g:23:4:-1,n:23:4:-1 +row 1,NULL +row 2,NULL +SELECT 2 +> SELECT pg_options_to_table(ARRAY['fillfactor=70', 'autovacuum_enabled=off']) AS o +fields o:2249:-1:-1 +row (fillfactor,70) +row (autovacuum_enabled,off) +SELECT 2 +> SELECT x, (SELECT count(*) FROM (SELECT generate_series(1, x)) s) AS c FROM (VALUES (1), (3)) v(x) +fields x:23:4:-1,c:20:8:-1 +row 1,1 +row 3,3 +SELECT 2 +> SELECT x FROM (SELECT generate_series(1, 3) AS x) s LIMIT 1 +fields x:23:4:-1 +row 1 +SELECT 1 +> SELECT generate_series(1, 2) AS g FROM (VALUES (2), (1)) v(x) ORDER BY x +fields g:23:4:-1 +row 1 +row 2 +row 1 +row 2 +SELECT 4 +> SELECT DISTINCT ON (generate_series(1, 2)) x FROM (VALUES (5), (6)) v(x) ORDER BY generate_series(1, 2), x DESC +fields x:23:4:-1 +row 6 +row 6 +SELECT 2 +> SELECT generate_series(1, 3) AS g WHERE false +fields g:23:4:-1 +SELECT 0 +> SELECT 1 AS one FROM (VALUES (1)) v(x) WHERE x = 2 ORDER BY generate_series(1, 2) +fields one:23:4:-1 +SELECT 0 +> SELECT unnest(ARRAY[1, 2]) AS u UNION ALL SELECT unnest(ARRAY[3]) ORDER BY 1 DESC +fields u:23:4:-1 +row 3 +row 2 +row 1 +SELECT 3 +> SELECT generate_series(1, 3) * generate_series(1, 2) AS p +fields p:23:4:-1 +row 1 +row 4 +row NULL +SELECT 3 +> SELECT ARRAY[generate_series(1, 2), generate_series(5, 7)] AS a +fields a:1007:-1:-1 +row {1,5} +row {2,6} +row {NULL,7} +SELECT 3 +> SELECT (SELECT generate_series(1, x)) AS s FROM (VALUES (1)) v(x) +fields s:23:4:-1 +row 1 +SELECT 1 +> SELECT generate_series(1, 2) FROM (VALUES (1)) v(x) GROUP BY x HAVING sum(generate_series(1, 2)) > 0 +ERROR 0A000 set-returning functions are not allowed in HAVING at 75 diff --git a/crates/rupg-session/src/system.rs b/crates/rupg-session/src/system.rs index 3349e03..389b99b 100644 --- a/crates/rupg-session/src/system.rs +++ b/crates/rupg-session/src/system.rs @@ -8,7 +8,7 @@ //! //! `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. //! -//! These parts of `system_views.sql` are not done yet: the view `pg_stats_ext_exprs`, which has a set-returning function in its target list, and the rules `pg_settings_u` and `pg_settings_n` on `pg_settings`. Their OIDs stay unused. +//! 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. use std::cell::RefCell; use std::sync::{Arc, OnceLock}; @@ -148,9 +148,6 @@ const FUNCTION_OIDS: [(&str, u32); 9] = [ ("_pg_interval_type", 13362), ]; -/// The views of the script that rupg cannot make yet. -const NOT_YET: [&str; 1] = ["pg_stats_ext_exprs"]; - /// The catalog of a new cluster: the objects that `initdb` makes with SQL. The first call makes it on a thread with a large stack, because the queries of some views are deep. // The thread runs once in the process before any session, as `initdb` runs before the server. It is not an engine task, so it uses std::thread and not the Tasks trait of rupg-platform. #[allow(clippy::disallowed_methods)] @@ -168,7 +165,7 @@ pub(crate) fn catalog() -> Arc { Arc::clone(made) } -/// Runs `system_views.sql` on a catalog of the bootstrap. Also gives each statement that failed, except the views of [`NOT_YET`], with its error. +/// Runs `system_views.sql` on a catalog of the bootstrap. Also gives each statement that failed, with its error. pub(crate) fn make() -> (Catalog, Vec<(String, Error)>) { let mut catalog = Catalog::new(); let mut failed = Vec::new(); @@ -214,7 +211,6 @@ pub(crate) fn make() -> (Catalog, Vec<(String, Error)>) { rupg_analyze::define(stmt, statement, &reader, &mut work, BOOTSTRAP_SUPERUSER); match defined.result { Ok(()) => catalog = work, - Err(_) if NOT_YET.contains(&name) => {} Err(error) => failed.push((name.to_string(), error)), } } @@ -550,8 +546,6 @@ mod tests { .filter(|r| r.kind == RelKind::View) .map(|r| (r.name.as_str(), r.oid)) .collect(); - let expected: Vec<(&str, u32)> = - VIEW_OIDS.iter().copied().filter(|(name, _)| !NOT_YET.contains(name)).collect(); - assert_eq!(views, expected); + assert_eq!(views, VIEW_OIDS); } }