use rupg_common::{Error, Result, SqlState};
use rupg_sql::nodes::{LockClauseStrength, LockingClause, Node};
use crate::coerce::AtOpt;
use crate::from::{FromItem, Join, Relation};
use crate::setop::strength_name;
use crate::typename::place;
use crate::{JoinKind, Query};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RowMark {
pub relation: usize,
pub strength: LockClauseStrength,
}
enum Entry {
Relation(usize),
Join(Option<String>),
}
fn entries(query: &Query) -> Vec<Entry> {
fn walk(item: &FromItem, out: &mut Vec<Entry>) {
match item {
FromItem::Relation(index) => out.push(Entry::Relation(*index)),
FromItem::Join(join) => {
walk(&join.left, out);
walk(&join.right, out);
out.push(Entry::Join(join_name(join)));
}
}
}
let mut out = Vec::new();
for item in &query.from {
walk(item, &mut out);
}
out
}
fn join_name(join: &Join) -> Option<String> {
join.alias.clone().or_else(|| join.using_alias.clone())
}
fn relation_entries(query: &Query) -> Vec<usize> {
entries(query)
.into_iter()
.filter_map(|entry| match entry {
Entry::Relation(index) => Some(index),
Entry::Join(_) => None,
})
.collect()
}
fn is_relation(relation: &Relation) -> bool {
relation.oid != 0 && relation.function.is_none() && relation.cte.is_none()
}
fn is_subquery(relation: &Relation) -> bool {
relation.oid == 0 && relation.subquery.is_some() && relation.cte.is_none()
}
pub(crate) fn check_select_locking(query: &Query, strength: LockClauseStrength) -> Result<()> {
let what = if query.set_op.is_some() {
"UNION/INTERSECT/EXCEPT"
} else if !query.distinct.is_empty() {
"DISTINCT clause"
} else if !query.group.is_empty()
|| (query.grouped && !has_aggs(query) && query.having.is_none())
{
"GROUP BY clause"
} else if query.having.is_some() {
"HAVING clause"
} else if has_aggs(query) {
"aggregate functions"
} else if query.targets.iter().any(|t| t.expr.first_window().is_some()) {
"window functions"
} else if query.has_target_srfs() {
"set-returning functions in the target list"
} else {
return Ok(());
};
Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
format!("{} is not allowed with {what}", strength_name(strength)),
))
}
fn has_aggs(query: &Query) -> bool {
query.targets.iter().any(|t| t.expr.first_agg().is_some())
|| query.having.as_ref().is_some_and(|h| h.first_agg().is_some())
}
fn apply(query: &mut Query, relation: usize, strength: LockClauseStrength) {
match query.row_marks.iter_mut().find(|mark| mark.relation == relation) {
Some(mark) => {
if strength.0 > mark.strength.0 {
mark.strength = strength;
}
}
None => query.row_marks.push(RowMark { relation, strength }),
}
}
fn apply_relation(query: &mut Query, relation: usize, strength: LockClauseStrength) -> Result<()> {
apply(query, relation, strength);
if let Some(view) = query.relations[relation].subquery.as_deref_mut() {
mark_for_locking(view, strength)?;
}
Ok(())
}
fn mark_for_locking(query: &mut Query, strength: LockClauseStrength) -> Result<()> {
for index in relation_entries(query) {
let relation = &query.relations[index];
if is_relation(relation) {
apply_relation(query, index, strength)?;
} else if is_subquery(relation) {
apply(query, index, strength);
if let Some(sub) = query.relations[index].subquery.as_deref_mut() {
mark_for_locking(sub, strength)?;
}
}
}
if let Some(first) = query.row_marks.first() {
check_select_locking(query, first.strength)?;
}
Ok(())
}
pub(crate) fn transform_locking_clause(query: &mut Query, clause: &LockingClause) -> Result<()> {
let strength = clause.strength;
check_select_locking(query, strength)?;
if clause.lockedRels.is_empty() {
for index in relation_entries(query) {
lock_relation(query, index, strength)?;
}
return Ok(());
}
let order = entries(query);
for node in clause.lockedRels.iter().flatten() {
let Node::RangeVar(rv) = node else { continue };
let at = place(rv.location);
if rv.catalogname.is_some() || rv.schemaname.is_some() {
return Err(Error::new(
SqlState::SYNTAX_ERROR,
format!("{} must specify unqualified relation names", strength_name(strength)),
)
.at_opt(at));
}
let name = rv.relname.as_deref().unwrap_or_default();
let found = order.iter().find_map(|entry| match entry {
Entry::Join(alias) => {
(alias.as_deref() == Some(name)).then_some(Found::Error("a join"))
}
Entry::Relation(index) => {
let relation = &query.relations[*index];
let hidden = relation.alias.is_none()
&& (is_subquery(relation) || relation.values.is_some());
if hidden || relation.name != name {
return None;
}
Some(if relation.cte.is_some() {
Found::Error("a WITH query")
} else if relation.json_table().is_some() {
Found::Error("a table function")
} else if relation.function.is_some() {
Found::Error("a function")
} else if relation.values.is_some() {
Found::Error("VALUES")
} else {
Found::Relation(*index)
})
}
});
match found {
Some(Found::Relation(index)) => lock_relation(query, index, strength)?,
Some(Found::Error(what)) => {
return Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
format!("{} cannot be applied to {what}", strength_name(strength)),
)
.at_opt(at));
}
None => {
return Err(Error::new(
SqlState::UNDEFINED_TABLE,
format!(
"relation \"{name}\" in {} clause not found in FROM clause",
strength_name(strength)
),
)
.at_opt(at));
}
}
}
Ok(())
}
enum Found {
Relation(usize),
Error(&'static str),
}
fn lock_relation(query: &mut Query, index: usize, strength: LockClauseStrength) -> Result<()> {
let relation = &query.relations[index];
if is_relation(relation) {
apply_relation(query, index, strength)
} else if is_subquery(relation) {
apply(query, index, strength);
let all = LockingClause { lockedRels: Vec::new(), strength, ..LockingClause::default() };
match query.relations[index].subquery.as_deref_mut() {
Some(sub) => transform_locking_clause(sub, &all),
None => Ok(()),
}
} else {
Ok(())
}
}
pub fn check_outer_joins(query: &Query) -> Result<()> {
fn relations(item: &FromItem, out: &mut Vec<usize>) {
match item {
FromItem::Relation(index) => out.push(*index),
FromItem::Join(join) => {
relations(&join.left, out);
relations(&join.right, out);
}
}
}
fn check(query: &Query, item: &FromItem) -> Result<()> {
let FromItem::Join(join) = item else { return Ok(()) };
check(query, &join.left)?;
check(query, &join.right)?;
let (mut left, mut right) = (Vec::new(), Vec::new());
relations(&join.left, &mut left);
relations(&join.right, &mut right);
let nullable: Vec<usize> = match join.kind {
JoinKind::Left => right,
JoinKind::Right => left,
JoinKind::Full => left.into_iter().chain(right).collect(),
_ => Vec::new(),
};
match query.row_marks.iter().find(|mark| nullable.contains(&mark.relation)) {
Some(mark) => Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
format!(
"{} cannot be applied to the nullable side of an outer join",
strength_name(mark.strength)
),
)),
None => Ok(()),
}
}
for item in &query.from {
check(query, item)?;
}
for relation in &query.relations {
if let Some(sub) = &relation.subquery {
check_outer_joins(sub)?;
}
}
Ok(())
}
pub fn first_row_mark(query: &Query) -> Option<LockClauseStrength> {
let own = query.row_marks.iter().find(|mark| query.relations[mark.relation].subquery.is_none());
match own {
Some(mark) => Some(mark.strength),
None => {
query.relations.iter().filter_map(|r| r.subquery.as_deref()).find_map(first_row_mark)
}
}
}
pub fn locked_tables(query: &Query, out: &mut Vec<u32>) {
for mark in &query.row_marks {
let relation = &query.relations[mark.relation];
if relation.oid != 0 && relation.subquery.is_none() {
out.push(relation.oid);
}
}
for sub in query.relations.iter().filter_map(|r| r.subquery.as_deref()) {
locked_tables(sub, out);
}
}