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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All changes that a user can see are in this file. The version is 0.M.patch, wher

## Unreleased

### Added

- Subqueries in expressions (spec/23 section 23.5): `EXISTS`, `IN`, `NOT IN`, `op ANY`, `op ALL`, `ARRAY(SELECT ...)` and a scalar subquery, in the target list, `WHERE`, `JOIN ... ON`, `HAVING`, `GROUP BY`, `ORDER BY`, `LIMIT` and `OFFSET`. A subquery can read the columns of the queries outside it at any depth. The analyzer finds a name as `parse_relation.c` does, from the subquery up to the outer queries, so a bad name gives the same error and hint as PostgreSQL, also for a close name of an outer relation. It gives the errors of `transformSubLink` for a wrong number of columns, and `subquery uses ungrouped column` for a column of a grouped outer query. The executor runs a subquery that reads no outer column once, as an `InitPlan` does, and runs the other subqueries again for each row. It reads the tables of a subquery once. `EXISTS` skips the target list when `simplify_EXISTS_query` can drop it, so `EXISTS (SELECT 1/0 FROM t)` gives no error, and a scalar subquery with more than one row gives `21000`. A row on the left of `IN` or `ANY`, `ARRAY()` of an array, an aggregate of an outer query, and a subquery in `FROM` give `0A000`.

## 0.0.17 (2026-10-08)

The seventeenth patch release on the 0.0 line. `SELECT` now has aggregate functions, `GROUP BY` and `HAVING`, and two new fuzz targets check the input, output, receive and send functions of 52 types against PostgreSQL. No crate is published.
Expand Down
99 changes: 83 additions & 16 deletions crates/rupg-analyze/src/agg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,31 @@ impl Analyzer<'_> {
inner.location,
));
}
// check_agg_arguments: an aggregate whose arguments read only the columns of outer queries belongs to the outer query.
let level = args
.iter()
.chain(filter.as_deref())
.filter_map(|e| {
let mut min: Option<usize> = None;
e.find(0, &mut |e, depth| {
if let ExprKind::Var(var) = e.kind
&& var.levels_up >= depth
{
let up = var.levels_up - depth;
min = Some(min.map_or(up, |m| m.min(up)));
}
None::<()>
});
min
})
.min();
if level.is_some_and(|l| l > 0) {
return Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
"an aggregate of an outer query is not supported yet",
)
.at_opt(at));
}
match self.kind {
Kind::JoinOn => {
return Err(grouping_error(
Expand Down Expand Up @@ -160,17 +185,23 @@ impl Analyzer<'_> {
let vars: Vec<Var> = grouped
.iter()
.filter_map(|e| match e.kind {
ExprKind::Var(var) => Some(var),
ExprKind::Var(var) if var.levels_up == 0 => Some(var),
_ => None,
})
.collect();
let mut check =
Check { analyzer: self, grouped: &grouped, non_var, vars: &vars, proven: Vec::new() };
let mut check = Check {
analyzer: self,
grouped: &grouped,
raised: Vec::new(),
non_var,
vars: &vars,
proven: Vec::new(),
};
for target in targets {
check.expr(&target.expr)?;
check.expr(&target.expr, 0)?;
}
if let Some(having) = having {
check.expr(having)?;
check.expr(having, 0)?;
}
Ok(())
}
Expand All @@ -181,6 +212,8 @@ struct Check<'a, 'b> {
analyzer: &'a Analyzer<'b>,
/// The expressions of `GROUP BY`.
grouped: &'a [&'a Expr],
/// `groupClauseSubLevels`: the expressions of `GROUP BY` as a subquery sees them, with the index 0 for a subquery of the query.
raised: Vec<Vec<Expr>>,
/// `have_non_var_grouping`: true when an item of `GROUP BY` is not a column.
non_var: bool,
/// The items of `GROUP BY` that are columns.
Expand All @@ -190,26 +223,58 @@ struct Check<'a, 'b> {
}

impl Check<'_, '_> {
fn expr(&mut self, expr: &Expr) -> Result<()> {
if let ExprKind::Agg(_) = expr.kind {
/// Checks an expression of the query, or of a subquery `depth` levels down.
fn expr(&mut self, expr: &Expr, depth: usize) -> Result<()> {
if let ExprKind::Agg(_) = expr.kind
&& depth == 0
{
return Ok(());
}
if self.non_var && self.grouped.iter().any(|g| g.same(expr)) {
if self.non_var && self.is_grouped(expr, depth) {
return Ok(());
}
match &expr.kind {
ExprKind::Const(_) | ExprKind::Param(_) => Ok(()),
ExprKind::Var(var) => self.var(*var, expr.location),
ExprKind::Var(var) if var.levels_up == depth => {
self.var(Var { levels_up: 0, ..*var }, depth, expr.location)
}
ExprKind::Var(_) => Ok(()),
_ => {
for child in expr.children() {
self.expr(child)?;
self.expr(child, depth)?;
}
if let ExprKind::SubLink(sub) = &expr.kind {
for e in sub.query.exprs() {
self.expr(e, depth + 1)?;
}
}
Ok(())
}
}
}

fn var(&mut self, var: Var, at: Option<usize>) -> Result<()> {
/// True when the expression is an item of `GROUP BY`, as a subquery `depth` levels down sees it.
fn is_grouped(&mut self, expr: &Expr, depth: usize) -> bool {
if depth == 0 {
return self.grouped.iter().any(|g| g.same(expr));
}
while self.raised.len() < depth {
let levels = self.raised.len() + 1;
let raised = self
.grouped
.iter()
.map(|g| {
let mut g = (*g).clone();
g.raise(levels);
g
})
.collect();
self.raised.push(raised);
}
self.raised[depth - 1].iter().any(|g| g.same(expr))
}

fn var(&mut self, var: Var, depth: usize, at: Option<usize>) -> Result<()> {
if !self.non_var && self.vars.contains(&var) {
return Ok(());
}
Expand All @@ -219,17 +284,19 @@ impl Check<'_, '_> {
// check_functional_grouping: all the columns of the primary key are in GROUP BY.
let relid = self.analyzer.scope.relations[var.relation].oid;
if let Some(key) = builtin::primary_key(relid)
&& key.iter().all(|&attnum| self.vars.contains(&Var { relation: var.relation, attnum }))
&& key.iter().all(|&attnum| self.vars.contains(&Var { attnum, ..var }))
{
self.proven.push(var.relation);
return Ok(());
}
let (table, column) = self.analyzer.column_names(var);
Err(grouping_error(
let message = if depth == 0 {
format!(
"column \"{table}.{column}\" must appear in the GROUP BY clause or be used in an aggregate function"
),
at,
))
)
} else {
format!("subquery uses ungrouped column \"{table}.{column}\" from outer query")
};
Err(grouping_error(message, at))
}
}
95 changes: 89 additions & 6 deletions crates/rupg-analyze/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use rupg_types::Value;

use crate::select::Query;
use crate::sort::SortGroup;

/// An expression with its type and its typmod. A typmod of -1 means that the expression has no typmod.
Expand Down Expand Up @@ -56,6 +57,34 @@ pub enum ExprKind {
Var(Var),
/// `Aggref`: a call of an aggregate function, which reads all the rows of a group.
Agg(Box<Aggref>),
/// `SubLink`: a subquery in an expression.
SubLink(Box<SubLink>),
/// `Param` of the kind `PARAM_SUBLINK`: the value of the column with this index, from 0, of a row of the subquery, in the test of `ANY` or `ALL`.
SubColumn(usize),
}

/// A subquery in an expression, as `SubLink`.
#[derive(Clone, Debug, PartialEq)]
pub struct SubLink {
pub kind: SubLinkKind,
/// `testexpr`: for `ANY` and `ALL`, the test of each row of the subquery. It compares the left side with [`ExprKind::SubColumn`].
pub test: Option<Expr>,
pub query: Query,
}

/// The kinds of `SubLink` that the analyzer takes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubLinkKind {
/// `EXISTS (SELECT ...)`.
Exists,
/// `x op ALL (SELECT ...)`.
All,
/// `x op ANY (SELECT ...)` and `x IN (SELECT ...)`.
Any,
/// `(SELECT ...)`, which gives the value of the one column of the one row.
Expr,
/// `ARRAY(SELECT ...)`, which gives the values of the one column as an array.
Array,
}

/// A column of a relation of `FROM`.
Expand All @@ -65,6 +94,8 @@ pub struct Var {
pub relation: usize,
/// The attribute number: from 1 for a column of the relation, or a negative number for a system column.
pub attnum: i16,
/// `varlevelsup`: 0 for a relation of the query that has the expression, 1 for a relation of the query outside it, and so on.
pub levels_up: usize,
}

/// A call of a function.
Expand Down Expand Up @@ -193,7 +224,9 @@ impl Expr {
| ExprKind::Param(_)
| ExprKind::CaseTest
| ExprKind::SqlValue(_)
| ExprKind::Var(_) => Vec::new(),
| ExprKind::Var(_)
| ExprKind::SubColumn(_) => Vec::new(),
ExprKind::SubLink(sub) => sub.test.iter().collect(),
ExprKind::Func(f) => f.args.iter().collect(),
ExprKind::Agg(agg) => agg.args.iter().chain(agg.filter.as_deref()).collect(),
ExprKind::Relabel(arg)
Expand Down Expand Up @@ -225,7 +258,9 @@ impl Expr {
| ExprKind::Param(_)
| ExprKind::CaseTest
| ExprKind::SqlValue(_)
| ExprKind::Var(_) => Vec::new(),
| ExprKind::Var(_)
| ExprKind::SubColumn(_) => Vec::new(),
ExprKind::SubLink(sub) => sub.test.iter_mut().collect(),
ExprKind::Func(f) => f.args.iter_mut().collect(),
ExprKind::Agg(agg) => {
let Aggref { args, filter, .. } = &mut **agg;
Expand Down Expand Up @@ -261,19 +296,66 @@ impl Expr {
for child in expr.children_mut() {
strip(child);
}
if let ExprKind::SubLink(sub) = &mut expr.kind {
sub.query.exprs_mut().into_iter().for_each(strip);
}
}
let (mut a, mut b) = (self.clone(), other.clone());
strip(&mut a);
strip(&mut b);
a == b
}

/// The first column that the expression reads, as `locate_var_of_level` finds it.
/// Calls `f` for the expression and for each expression in it, also in its subqueries, with the number of subqueries between the expression and this expression. The search stops at the first `Some` that `f` gives.
pub fn find<'a, T>(
&'a self,
depth: usize,
f: &mut impl FnMut(&'a Expr, usize) -> Option<T>,
) -> Option<T> {
if let Some(found) = f(self, depth) {
return Some(found);
}
for child in self.children() {
if let Some(found) = child.find(depth, f) {
return Some(found);
}
}
if let ExprKind::SubLink(sub) = &self.kind {
for expr in sub.query.exprs() {
if let Some(found) = expr.find(depth + 1, f) {
return Some(found);
}
}
}
None
}

/// The first column of the query of the expression that the expression reads, also in a subquery, as `locate_var_of_level` finds it with level 0.
pub fn first_var(&self) -> Option<&Expr> {
if matches!(self.kind, ExprKind::Var(_)) {
return Some(self);
self.find(0, &mut |e, depth| match e.kind {
ExprKind::Var(var) if var.levels_up == depth => Some(e),
_ => None,
})
}

/// `IncrementVarSublevelsUp`: the columns of the outer queries in the expression, also in its subqueries, are `levels` more levels up. A column of a query in the expression does not change.
pub fn raise(&mut self, levels: usize) {
fn raise_at(expr: &mut Expr, levels: usize, depth: usize) {
if let ExprKind::Var(var) = &mut expr.kind
&& var.levels_up >= depth
{
var.levels_up += levels;
}
for child in expr.children_mut() {
raise_at(child, levels, depth);
}
if let ExprKind::SubLink(sub) = &mut expr.kind {
for e in sub.query.exprs_mut() {
raise_at(e, levels, depth + 1);
}
}
}
self.children().into_iter().find_map(Expr::first_var)
raise_at(self, levels, 0);
}

/// `contain_aggs_of_level` and `locate_agg_of_level`: the first aggregate call in the expression.
Expand Down Expand Up @@ -309,6 +391,7 @@ impl Expr {
| ExprKind::CoerceViaIo(arg)
| ExprKind::NullTest(arg, _)
| ExprKind::BooleanTest(arg, _) => Some(&**arg),
ExprKind::SubLink(sub) => sub.test.as_ref(),
_ => None,
};
match (self.location, first.and_then(Expr::place)) {
Expand Down
Loading
Loading