use super::{
compile_expr, compile_from_node, compile_named_windows, extract_strings, right_is_lateral,
Expr, FromClause, JoinKind, Node, NodeEnum, OrderBy, Projection, Result, SQLError, SelectStmt,
SetOp, SetOpKind, Value, CTE,
};
use crate::ast::{CteCycleClause, CteMaterialization, CteSearchClause};
#[expect(clippy::too_many_lines, reason = "preserves PostgreSQL lowering order")]
pub(in crate::compiler) fn compile_select(
stmt: &pg_query::protobuf::SelectStmt,
) -> Result<SelectStmt> {
let locking = super::locking::compile_locking_clauses(&stmt.locking_clause)?;
if stmt.into_clause.is_some() {
return Err(SQLError::Unsupported("SELECT INTO is not supported".into()));
}
let named_windows = compile_named_windows(&stmt.window_clause)?;
let with_ties = stmt.limit_option() == pg_query::protobuf::LimitOption::WithTies;
if with_ties && stmt.sort_clause.is_empty() {
return Err(SQLError::Routine {
sqlstate: "42601".into(),
message: "WITH TIES cannot be specified without ORDER BY clause".into(),
});
}
if stmt.op != pg_query::protobuf::SetOperation::SetopNone as i32 && !locking.is_empty() {
return Err(SQLError::Unsupported(format!(
"{} is not allowed with UNION/INTERSECT/EXCEPT",
locking[0].strength.sql_name()
)));
}
if stmt.op != pg_query::protobuf::SetOperation::SetopNone as i32 {
for operand in [stmt.larg.as_deref(), stmt.rarg.as_deref()]
.into_iter()
.flatten()
{
let operand_locking = super::locking::compile_locking_clauses(&operand.locking_clause)?;
if let Some(clause) = operand_locking.first() {
return Err(SQLError::Unsupported(format!(
"{} is not allowed with UNION/INTERSECT/EXCEPT",
clause.strength.sql_name()
)));
}
}
}
let mut from = compile_from_list(&stmt.from_clause)?;
let mut projections = compile_projections(&stmt.target_list)?;
let mut values = compile_values_lists(&stmt.values_lists)?;
let r#where = stmt
.where_clause
.as_ref()
.map(|w| compile_expr(w))
.transpose()?;
let order_by = compile_order_by(&stmt.sort_clause)?;
let limit = compile_limit_offset_expr(stmt.limit_count.as_deref(), !with_ties)?;
let offset = compile_limit_offset_expr(stmt.limit_offset.as_deref(), true)?;
let (group_by, grouping_sets) = compile_group_clause(&stmt.group_clause)?;
let group_by = resolve_group_by_aliases(group_by, &projections);
let grouping_sets: Vec<Vec<Expr>> = grouping_sets
.into_iter()
.map(|s| resolve_group_by_aliases(s, &projections))
.collect();
let having = stmt
.having_clause
.as_ref()
.map(|h| compile_expr(h))
.transpose()?;
let with = match stmt.with_clause.as_ref() {
Some(wc) => compile_with_clause(wc)?,
None => Vec::new(),
};
if !values.is_empty() && (!order_by.is_empty() || limit.is_some() || offset.is_some()) {
from = Some(FromClause::Values {
rows: std::mem::take(&mut values),
alias: None,
column_aliases: Vec::new(),
internal_relation: None,
internal_column_types: Vec::new(),
});
projections = vec![Projection {
expr: Expr::Star,
alias: None,
}];
}
let mut set_op = compile_set_op(stmt)?;
let (projections, values, mut from, r#where, group_by, order_by, limit, with_ties, offset) =
if set_op.is_some() {
if let Some(so) = set_op.as_mut() {
so.combined_order_by = order_by;
so.combined_limit = limit;
so.combined_with_ties = with_ties;
so.combined_offset = offset;
}
let lhs_node = stmt
.larg
.as_deref()
.ok_or_else(|| SQLError::Internal("set op missing left".into()))?;
let lhs = compile_select(lhs_node)?;
if let Some(so) = set_op.as_mut() {
so.left = Some(Box::new(lhs.clone()));
}
(
lhs.projections,
lhs.values,
lhs.from,
lhs.r#where,
lhs.group_by,
lhs.order_by,
lhs.limit,
lhs.with_ties,
lhs.offset,
)
} else {
(
projections,
values,
from,
r#where,
group_by,
order_by,
limit,
with_ties,
offset,
)
};
if let Some(from) = from.as_mut() {
reduce_null_rejected_outer_joins_to_fixpoint(from, r#where.as_ref());
}
let (distinct, distinct_on) = compile_distinct_clause(&stmt.distinct_clause)?;
let mut compiled = SelectStmt {
projections,
values,
from,
r#where,
group_by,
grouping_sets,
group_distinct: stmt.group_distinct,
having,
order_by,
limit,
with_ties,
offset,
with,
set_op,
distinct,
distinct_on,
locking,
};
super::resolve_named_windows_in_select(&mut compiled, &named_windows)?;
super::locking::validate_select_locking(&compiled)?;
super::locking::propagate_select_locking(&mut compiled)?;
Ok(compiled)
}
fn reduce_null_rejected_outer_joins_to_fixpoint(from: &mut FromClause, predicate: Option<&Expr>) {
loop {
let mut quals: Vec<Expr> = predicate.iter().map(|expr| (*expr).clone()).collect();
collect_inner_join_quals(from, &mut quals);
let mut changed = false;
for qual in &quals {
changed |= reduce_null_rejected_outer_joins(from, qual);
}
if !changed {
break;
}
}
}
fn collect_inner_join_quals(from: &FromClause, quals: &mut Vec<Expr>) {
let FromClause::Join {
left,
right,
kind,
on,
..
} = from
else {
return;
};
if !matches!(kind, JoinKind::Inner) {
return;
}
if let Some(on) = on.as_ref() {
quals.push(on.clone());
}
collect_inner_join_quals(left, quals);
collect_inner_join_quals(right, quals);
}
fn reduce_null_rejected_outer_joins(from: &mut FromClause, predicate: &Expr) -> bool {
let FromClause::Join {
left, right, kind, ..
} = from
else {
return false;
};
let mut left_names = std::collections::BTreeSet::new();
let mut right_names = std::collections::BTreeSet::new();
collect_visible_qualifiers(left, &mut left_names);
collect_visible_qualifiers(right, &mut right_names);
let rejects_left = predicate_rejects_null_extended_side(predicate, &left_names);
let rejects_right = predicate_rejects_null_extended_side(predicate, &right_names);
let reduced = match (*kind, rejects_left, rejects_right) {
(JoinKind::Left, _, true) | (JoinKind::Right, true, _) => JoinKind::Inner,
(JoinKind::Full, true, true) => JoinKind::Inner,
(JoinKind::Full, true, false) => JoinKind::Left,
(JoinKind::Full, false, true) => JoinKind::Right,
(kind, _, _) => kind,
};
let mut changed = reduced != *kind;
*kind = reduced;
changed |= reduce_null_rejected_outer_joins(left, predicate);
changed |= reduce_null_rejected_outer_joins(right, predicate);
changed
}
fn collect_visible_qualifiers(from: &FromClause, names: &mut std::collections::BTreeSet<String>) {
match from {
FromClause::Table {
name,
qualifier,
alias,
..
} => {
if let Some(alias) = alias {
names.insert(alias.clone());
} else {
names.insert(qualifier.clone());
names.insert(name.clone());
if let Some((_, local)) = name.rsplit_once('.') {
names.insert(local.to_string());
}
}
}
FromClause::Join {
left, right, alias, ..
} => {
if let Some(alias) = alias {
names.insert(alias.clone());
} else {
collect_visible_qualifiers(left, names);
collect_visible_qualifiers(right, names);
}
}
FromClause::Values { alias, .. }
| FromClause::Subquery { alias, .. }
| FromClause::Function { alias, .. }
| FromClause::FunctionGroup { alias, .. } => {
if let Some(alias) = alias {
names.insert(alias.clone());
}
}
}
}
const TRUTH_FALSE: u8 = 1;
const TRUTH_TRUE: u8 = 2;
const TRUTH_NULL: u8 = 4;
const TRUTH_ANY: u8 = TRUTH_FALSE | TRUTH_TRUE | TRUTH_NULL;
fn predicate_rejects_null_extended_side(
expression: &Expr,
qualifiers: &std::collections::BTreeSet<String>,
) -> bool {
!qualifiers.is_empty() && truth_values_with_null_side(expression, qualifiers) & TRUTH_TRUE == 0
}
fn truth_values_with_null_side(
expression: &Expr,
qualifiers: &std::collections::BTreeSet<String>,
) -> u8 {
match expression {
Expr::Literal(Value::Bool(value)) => {
if *value {
TRUTH_TRUE
} else {
TRUTH_FALSE
}
}
Expr::Literal(Value::Null) => TRUTH_NULL,
Expr::IsNull { expr, negated } if expression_is_null_with_side(expr, qualifiers) => {
if *negated {
TRUTH_FALSE
} else {
TRUTH_TRUE
}
}
Expr::Between {
expr, low, high, ..
} => {
let value_is_null = expression_is_null_with_side(expr, qualifiers);
let low_is_null = expression_is_null_with_side(low, qualifiers);
let high_is_null = expression_is_null_with_side(high, qualifiers);
if value_is_null || (low_is_null && high_is_null) {
TRUTH_NULL
} else if low_is_null || high_is_null {
TRUTH_FALSE | TRUTH_NULL
} else {
TRUTH_ANY
}
}
expression if expression_is_null_with_side(expression, qualifiers) => TRUTH_NULL,
Expr::Not(inner) => negate_truth_values(truth_values_with_null_side(inner, qualifiers)),
Expr::And(items) => items.iter().fold(TRUTH_TRUE, |left, right| {
combine_truth_values(left, truth_values_with_null_side(right, qualifiers), true)
}),
Expr::Or(items) => items.iter().fold(TRUTH_FALSE, |left, right| {
combine_truth_values(left, truth_values_with_null_side(right, qualifiers), false)
}),
_ => TRUTH_ANY,
}
}
fn expression_is_null_with_side(
expression: &Expr,
qualifiers: &std::collections::BTreeSet<String>,
) -> bool {
match expression {
Expr::Literal(Value::Null) => true,
Expr::QualifiedColumn { qualifier, .. } => qualifiers.contains(qualifier),
Expr::Binary { lhs, rhs, .. } => {
expression_is_null_with_side(lhs, qualifiers)
|| expression_is_null_with_side(rhs, qualifiers)
}
Expr::UnaryMinus(inner) | Expr::Cast { expr: inner, .. } => {
expression_is_null_with_side(inner, qualifiers)
}
Expr::Between {
expr, low, high, ..
} => {
expression_is_null_with_side(expr, qualifiers)
|| (expression_is_null_with_side(low, qualifiers)
&& expression_is_null_with_side(high, qualifiers))
}
Expr::InList { expr, .. } => expression_is_null_with_side(expr, qualifiers),
Expr::Func {
name,
args,
distinct: _,
order_by: _,
filter: _,
binding,
} if crate::expr::bound_scalar_function_strictness(name, binding.as_ref(), args.len())
== Some(true) =>
{
args.iter()
.map(crate::expr::call_argument_value)
.any(|argument| expression_is_null_with_side(argument, qualifiers))
}
_ => false,
}
}
fn negate_truth_values(values: u8) -> u8 {
(u8::from(values & TRUTH_FALSE != 0) * TRUTH_TRUE)
| (u8::from(values & TRUTH_TRUE != 0) * TRUTH_FALSE)
| (values & TRUTH_NULL)
}
fn combine_truth_values(left: u8, right: u8, and: bool) -> u8 {
let mut output = 0;
for lhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
if left & lhs == 0 {
continue;
}
for rhs in [TRUTH_FALSE, TRUTH_TRUE, TRUTH_NULL] {
if right & rhs == 0 {
continue;
}
output |= if and {
match (lhs, rhs) {
(TRUTH_FALSE, _) | (_, TRUTH_FALSE) => TRUTH_FALSE,
(TRUTH_TRUE, TRUTH_TRUE) => TRUTH_TRUE,
_ => TRUTH_NULL,
}
} else {
match (lhs, rhs) {
(TRUTH_TRUE, _) | (_, TRUTH_TRUE) => TRUTH_TRUE,
(TRUTH_FALSE, TRUTH_FALSE) => TRUTH_FALSE,
_ => TRUTH_NULL,
}
};
}
}
output
}
pub(in crate::compiler) fn compile_values_lists(nodes: &[Node]) -> Result<Vec<Vec<Expr>>> {
nodes
.iter()
.map(|node| {
let Some(NodeEnum::List(list)) = node.node.as_ref() else {
return Err(SQLError::Internal("VALUES contains a malformed row".into()));
};
list.items.iter().map(compile_expr).collect()
})
.collect()
}
pub(in crate::compiler) fn compile_distinct_clause(nodes: &[Node]) -> Result<(bool, Vec<Expr>)> {
if nodes.is_empty() {
return Ok((false, Vec::new()));
}
let mut distinct_on = Vec::new();
for node in nodes {
match node.node.as_ref() {
None => return Ok((true, Vec::new())),
Some(NodeEnum::AConst(c)) if c.isnull || c.val.is_none() => {
return Ok((true, Vec::new()));
}
Some(_) => distinct_on.push(compile_expr(node)?),
}
}
Ok((true, distinct_on))
}
pub(in crate::compiler) fn compile_from_list(nodes: &[Node]) -> Result<Option<FromClause>> {
let Some(first) = nodes.first() else {
return Ok(None);
};
let mut current = compile_from_node(first)?;
for node in nodes.iter().skip(1) {
let lateral = right_is_lateral(node);
current = FromClause::Join {
left: Box::new(current),
right: Box::new(compile_from_node(node)?),
kind: JoinKind::Cross,
on: None,
using: None,
natural: false,
alias: None,
column_aliases: Vec::new(),
lateral,
};
}
Ok(Some(current))
}
pub(in crate::compiler) fn resolve_group_by_aliases(
group_by: Vec<Expr>,
projections: &[Projection],
) -> Vec<Expr> {
group_by
.into_iter()
.map(|g| match &g {
Expr::Literal(Value::Int(n)) if *n >= 1 => match usize::try_from(*n) {
Ok(position) if position <= projections.len() => {
projections[position - 1].expr.clone()
}
_ => g,
},
Expr::Column(name) => {
for p in projections {
if let Some(alias) = &p.alias {
if alias == name {
if let Expr::Column(col_name) = &p.expr {
if col_name == name {
return g;
}
}
return p.expr.clone();
}
}
}
g
}
_ => g,
})
.collect()
}
#[expect(clippy::too_many_lines, reason = "preserves PostgreSQL lowering order")]
pub(in crate::compiler) fn compile_group_clause(
nodes: &[pg_query::protobuf::Node],
) -> Result<(Vec<Expr>, Vec<Vec<Expr>>)> {
use pg_query::protobuf::{CoercionForm, GroupingSetKind};
fn simple_item(node: &pg_query::protobuf::Node) -> Result<Vec<Expr>> {
match node.node.as_ref() {
Some(NodeEnum::GroupingSet(grouping)) => match grouping.kind() {
GroupingSetKind::GroupingSetEmpty => Ok(Vec::new()),
GroupingSetKind::GroupingSetSimple => grouping
.content
.iter()
.map(simple_item)
.collect::<Result<Vec<_>>>()
.map(|items| items.into_iter().flatten().collect()),
other => Err(SQLError::Unsupported(format!(
"nested grouping item {other:?} is not a simple grouping key"
))),
},
Some(NodeEnum::RowExpr(row))
if row.row_format() == CoercionForm::CoerceImplicitCast =>
{
row.args.iter().map(compile_expr).collect()
}
Some(_) => Ok(vec![compile_expr(node)?]),
None => Err(SQLError::Internal(
"GROUP BY contains an empty parse node".into(),
)),
}
}
fn expand(grouping: &pg_query::protobuf::GroupingSet) -> Result<Vec<Vec<Expr>>> {
match grouping.kind() {
GroupingSetKind::GroupingSetEmpty => Ok(vec![Vec::new()]),
GroupingSetKind::GroupingSetSimple => Ok(vec![grouping
.content
.iter()
.map(simple_item)
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect()]),
GroupingSetKind::GroupingSetRollup => {
let items = grouping
.content
.iter()
.map(simple_item)
.collect::<Result<Vec<_>>>()?;
let set_count = items.len().checked_add(1).ok_or_else(|| {
SQLError::Unsupported("ROLLUP has too many grouping items".into())
})?;
let mut sets = Vec::new();
sets.try_reserve(set_count).map_err(|error| {
SQLError::Unsupported(format!(
"ROLLUP expansion of {set_count} grouping sets is too large: {error}"
))
})?;
for prefix in (0..=items.len()).rev() {
sets.push(items[..prefix].iter().flatten().cloned().collect());
}
Ok(sets)
}
GroupingSetKind::GroupingSetCube => {
let items = grouping
.content
.iter()
.map(simple_item)
.collect::<Result<Vec<_>>>()?;
let shift = u32::try_from(items.len()).map_err(|_| {
SQLError::Unsupported(format!(
"CUBE has too many grouping items: {}",
items.len()
))
})?;
let set_count = 1_usize.checked_shl(shift).ok_or_else(|| {
SQLError::Unsupported(format!(
"CUBE has too many grouping items: {}",
items.len()
))
})?;
let mut sets = Vec::new();
sets.try_reserve(set_count).map_err(|error| {
SQLError::Unsupported(format!(
"CUBE expansion of {set_count} grouping sets is too large: {error}"
))
})?;
for mask in 0..set_count {
let mut set = Vec::new();
for (index, item) in items.iter().enumerate() {
if mask & (1_usize << index) != 0 {
set.extend(item.iter().cloned());
}
}
sets.push(set);
}
Ok(sets)
}
GroupingSetKind::GroupingSetSets => {
let mut sets = Vec::new();
for child in &grouping.content {
match child.node.as_ref() {
Some(NodeEnum::GroupingSet(nested)) => sets.extend(expand(nested)?),
Some(_) => sets.push(simple_item(child)?),
None => {
return Err(SQLError::Internal(
"GROUPING SETS contains an empty parse node".into(),
))
}
}
}
Ok(sets)
}
other => Err(SQLError::Unsupported(format!(
"GROUP BY grouping-set kind {other:?}"
))),
}
}
let mut plain = Vec::new();
let mut combined_sets = vec![Vec::new()];
let mut has_grouping_set = false;
for node in nodes {
let alternatives = match node.node.as_ref() {
Some(NodeEnum::GroupingSet(grouping)) => {
has_grouping_set = true;
expand(grouping)?
}
Some(_) => {
let expression = compile_expr(node)?;
plain.push(expression.clone());
vec![vec![expression]]
}
None => {
return Err(SQLError::Internal(
"GROUP BY contains an empty parse node".into(),
))
}
};
let mut product = Vec::new();
let product_count = combined_sets
.len()
.checked_mul(alternatives.len())
.ok_or_else(|| {
SQLError::Unsupported("GROUP BY expansion count overflowed usize".into())
})?;
product.try_reserve(product_count).map_err(|error| {
SQLError::Unsupported(format!("GROUP BY expansion is too large: {error}"))
})?;
for prefix in &combined_sets {
for alternative in &alternatives {
let mut set = prefix.clone();
set.extend(alternative.iter().cloned());
product.push(set);
}
}
combined_sets = product;
}
if has_grouping_set {
Ok((Vec::new(), combined_sets))
} else {
Ok((plain, Vec::new()))
}
}
pub(in crate::compiler) fn compile_projections(
targets: &[pg_query::protobuf::Node],
) -> Result<Vec<Projection>> {
let mut out = Vec::with_capacity(targets.len());
for target_node in targets {
let inner = target_node
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("SELECT contains an empty target".into()))?;
let res_target = match inner {
NodeEnum::ResTarget(t) => t,
_ => return Err(SQLError::Internal(format!("unexpected target {inner:?}"))),
};
let alias = if res_target.name.is_empty() {
None
} else {
Some(res_target.name.clone())
};
let expr = match &res_target.val {
Some(node) => compile_expr(node)?,
None => return Err(SQLError::Internal("ResTarget without value".into())),
};
out.push(Projection { expr, alias });
}
Ok(out)
}
pub(in crate::compiler) fn compile_order_by(
sort_clause: &[pg_query::protobuf::Node],
) -> Result<Vec<OrderBy>> {
let mut out = Vec::with_capacity(sort_clause.len());
for sort_node in sort_clause {
let inner = sort_node
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("ORDER BY contains an empty item".into()))?;
let NodeEnum::SortBy(sb) = inner else {
return Err(SQLError::Internal(format!(
"ORDER BY expected SortBy, got {inner:?}"
)));
};
let expr_node = sb
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("SortBy without expr".into()))?;
let expr = compile_expr(expr_node)?;
let (descending, nulls) = compile_sort_options(sb, "ORDER BY")?;
out.push(OrderBy {
expr,
descending,
nulls,
});
}
Ok(out)
}
pub(in crate::compiler) fn compile_sort_options(
sort: &pg_query::protobuf::SortBy,
context: &str,
) -> Result<(bool, Option<crate::ast::NullsOrder>)> {
use pg_query::protobuf::{SortByDir, SortByNulls};
let direction = SortByDir::try_from(sort.sortby_dir).map_err(|_| {
SQLError::Internal(format!(
"{context} has invalid sort direction {}",
sort.sortby_dir
))
})?;
let descending = match direction {
SortByDir::SortbyDefault | SortByDir::SortbyAsc => false,
SortByDir::SortbyDesc => true,
SortByDir::SortbyUsing => {
return Err(SQLError::Unsupported(format!(
"{context} USING operators are not represented by OrderBy"
)));
}
SortByDir::Undefined => {
return Err(SQLError::Internal(format!(
"{context} has an undefined sort direction"
)));
}
};
let null_order = SortByNulls::try_from(sort.sortby_nulls).map_err(|_| {
SQLError::Internal(format!(
"{context} has invalid NULLS ordering {}",
sort.sortby_nulls
))
})?;
let nulls = match null_order {
SortByNulls::SortbyNullsDefault => None,
SortByNulls::SortbyNullsFirst => Some(crate::ast::NullsOrder::First),
SortByNulls::SortbyNullsLast => Some(crate::ast::NullsOrder::Last),
SortByNulls::Undefined => {
return Err(SQLError::Internal(format!(
"{context} has an undefined NULLS ordering"
)));
}
};
Ok((descending, nulls))
}
pub(in crate::compiler) fn compile_set_op(
stmt: &pg_query::protobuf::SelectStmt,
) -> Result<Option<Box<SetOp>>> {
let kind = match stmt.op() {
pg_query::protobuf::SetOperation::SetopNone => return Ok(None),
pg_query::protobuf::SetOperation::SetopUnion => SetOpKind::Union,
pg_query::protobuf::SetOperation::SetopIntersect => SetOpKind::Intersect,
pg_query::protobuf::SetOperation::SetopExcept => SetOpKind::Except,
other => return Err(SQLError::Unsupported(format!("set op {other:?}"))),
};
if stmt.larg.is_none() {
return Err(SQLError::Internal("set op missing left".into()));
}
let right_node = stmt
.rarg
.as_deref()
.ok_or_else(|| SQLError::Internal("set op missing right".into()))?;
let right = compile_select(right_node)?;
Ok(Some(Box::new(SetOp {
kind,
all: stmt.all,
left: None,
right,
combined_order_by: Vec::new(),
combined_limit: None,
combined_with_ties: false,
combined_offset: None,
})))
}
#[expect(clippy::too_many_lines, reason = "preserves PostgreSQL lowering order")]
pub(in crate::compiler) fn compile_with_clause(
wc: &pg_query::protobuf::WithClause,
) -> Result<Vec<CTE>> {
let mut out = Vec::with_capacity(wc.ctes.len());
for cte_node in &wc.ctes {
let inner = cte_node
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("WITH contains an empty CTE".into()))?;
let cte = match inner {
NodeEnum::CommonTableExpr(c) => c,
_ => return Err(SQLError::Internal("expected CommonTableExpr".into())),
};
if cte.ctename.is_empty() {
return Err(SQLError::Internal("CTE name is empty".into()));
}
let materialization = match cte.ctematerialized() {
pg_query::protobuf::CteMaterialize::CtematerializeUndefined
| pg_query::protobuf::CteMaterialize::Default => CteMaterialization::Default,
pg_query::protobuf::CteMaterialize::Always => CteMaterialization::Materialized,
pg_query::protobuf::CteMaterialize::Never => CteMaterialization::NotMaterialized,
};
let select_node = cte
.ctequery
.as_ref()
.ok_or_else(|| SQLError::Internal("CTE without query".into()))?;
let select_inner = select_node
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("CTE query node empty".into()))?;
let select = match select_inner {
NodeEnum::SelectStmt(s) => s,
_ => return Err(SQLError::Unsupported("CTE body must be SELECT".into())),
};
let columns = extract_strings(&cte.aliascolnames)?;
let search = cte
.search_clause
.as_ref()
.map(|clause| -> Result<CteSearchClause> {
Ok(CteSearchClause {
columns: extract_strings(&clause.search_col_list)?,
breadth_first: clause.search_breadth_first,
sequence_column: clause.search_seq_column.clone(),
})
})
.transpose()?;
let cycle = cte
.cycle_clause
.as_deref()
.map(|clause| -> Result<CteCycleClause> {
let mark_value = clause
.cycle_mark_value
.as_deref()
.map(compile_expr)
.transpose()?
.unwrap_or(Expr::Literal(Value::Bool(true)));
let mark_default = clause
.cycle_mark_default
.as_deref()
.map(compile_expr)
.transpose()?
.unwrap_or(Expr::Literal(Value::Bool(false)));
Ok(CteCycleClause {
columns: extract_strings(&clause.cycle_col_list)?,
mark_column: clause.cycle_mark_column.clone(),
mark_value,
mark_default,
path_column: clause.cycle_path_column.clone(),
})
})
.transpose()?;
let query = compile_select(select)?;
let recursive = wc.recursive && select_references_relation(&query, &cte.ctename);
if recursive {
reject_recursive_query_ordering(&query)?;
}
if (search.is_some() || cycle.is_some()) && !recursive {
return Err(SQLError::Routine {
sqlstate: "42601".into(),
message: "WITH query is not recursive".into(),
});
}
if let (Some(search), Some(cycle)) = (&search, &cycle) {
if search.sequence_column == cycle.mark_column {
return Err(SQLError::Routine {
sqlstate: "42601".into(),
message: "search sequence column name and cycle mark column name are the same"
.into(),
});
}
if search.sequence_column == cycle.path_column {
return Err(SQLError::Routine {
sqlstate: "42601".into(),
message: "search sequence column name and cycle path column name are the same"
.into(),
});
}
}
if cycle
.as_ref()
.is_some_and(|cycle| cycle.mark_column == cycle.path_column)
{
return Err(SQLError::Routine {
sqlstate: "42601".into(),
message: "cycle mark column name and cycle path column name are the same".into(),
});
}
out.push(CTE {
name: cte.ctename.clone(),
columns,
recursive: wc.recursive,
materialization,
search,
cycle,
query: Box::new(query),
});
}
Ok(out)
}
fn select_references_relation(statement: &SelectStmt, name: &str) -> bool {
fn from_references_relation(from: &FromClause, name: &str) -> bool {
match from {
FromClause::Table { name: table, .. } => table == name,
FromClause::Join { left, right, .. } => {
from_references_relation(left, name) || from_references_relation(right, name)
}
FromClause::Subquery { body, .. } => select_references_relation(body, name),
FromClause::Values { .. }
| FromClause::Function { .. }
| FromClause::FunctionGroup { .. } => false,
}
}
statement
.from
.as_ref()
.is_some_and(|from| from_references_relation(from, name))
|| statement.set_op.as_ref().is_some_and(|set| {
set.left
.as_deref()
.is_some_and(|left| select_references_relation(left, name))
|| select_references_relation(&set.right, name)
})
}
fn reject_recursive_query_ordering(statement: &SelectStmt) -> Result<()> {
let Some(set_op) = statement.set_op.as_ref() else {
return Ok(());
};
if !set_op.combined_order_by.is_empty() {
return Err(SQLError::Unsupported(
"ORDER BY in a recursive query is not implemented".into(),
));
}
if set_op.combined_offset.is_some() {
return Err(SQLError::Unsupported(
"OFFSET in a recursive query is not implemented".into(),
));
}
if set_op.combined_limit.is_some() {
return Err(SQLError::Unsupported(
"LIMIT in a recursive query is not implemented".into(),
));
}
Ok(())
}
pub(in crate::compiler) fn compile_limit_offset_expr(
node: Option<&Node>,
null_is_absent: bool,
) -> Result<Option<Expr>> {
let Some(node) = node else { return Ok(None) };
let inner = node
.node
.as_ref()
.ok_or_else(|| SQLError::Internal("LIMIT/OFFSET contains an empty expression".into()))?;
if let NodeEnum::AConst(c) = inner {
if c.val.is_none() && null_is_absent {
return Ok(None);
}
}
Ok(Some(compile_expr(node)?))
}