use rupg_common::{Error, Result, SqlState};
use rupg_sql::nodes::{ColumnRef, List, Node, RangeVar};
use rupg_types::oid;
use crate::coerce::{AtOpt, Context};
use crate::expr::{Expr, ExprKind};
use crate::select::{Query, Target};
use crate::{Analyzer, Env, Params, types};
pub(crate) struct Assign {
pub(crate) ty: u32,
pub(crate) typmod: i32,
pub(crate) name: String,
pub(crate) indirect: Option<(Expr, List)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Variable {
pub param: usize,
pub ty: u32,
pub typmod: i32,
}
pub trait Variables {
fn column(&self, names: &[&str], at: Option<usize>) -> Result<Option<Variable>>;
fn param(&self, number: usize) -> Option<Variable>;
}
pub fn analyze_with(
stmt: &Node,
env: &dyn Env,
params: &Params,
variables: &dyn Variables,
) -> Result<Query> {
crate::analyze_hooked(stmt, env, params, Some(variables))
}
pub fn analyze_assign(
stmt: &Node,
env: &dyn Env,
variables: &dyn Variables,
(ty, typmod): (u32, i32),
name: &str,
) -> Result<Query> {
let assign = Assign { ty, typmod, name: name.to_owned(), indirect: None };
crate::analyze_assigned(Analyzer::new(env, &Params::default()), stmt, variables, assign)
}
pub fn analyze_assign_part(stmt: &Node, env: &dyn Env, variables: &dyn Variables) -> Result<Query> {
let Node::PLAssignStmt(s) = stmt else {
return Err(Error::internal("an assignment is not a PLAssignStmt"));
};
let Some(val) = &s.val else { return Err(Error::internal("an assignment has no value")) };
let name = s.name.as_deref().unwrap_or_default();
let split = usize::try_from(s.nnames).unwrap_or(1).saturating_sub(1).min(s.indirection.len());
let (names, indirection) = s.indirection.split_at(split);
let mut fields = vec![Some(Node::String(name.into()))];
fields.extend(names.iter().cloned());
let cref = Node::ColumnRef(Box::new(ColumnRef { fields, location: s.location }));
let mut analyzer = Analyzer::new(env, &Params::default());
analyzer.variables = Some(variables);
let target = analyzer.transform(Some(&cref))?;
let (ty, typmod) = (target.ty, target.typmod);
let indirect = Some((target, indirection.to_vec()));
let assign = Assign { ty, typmod, name: name.to_owned(), indirect };
crate::analyze_assigned(analyzer, &Node::SelectStmt(val.clone()), variables, assign)
}
impl Analyzer<'_> {
pub(crate) fn assign_target(&mut self, targets: &mut [Target], assign: Assign) -> Result<()> {
let Assign { ty, typmod, name, indirect } = assign;
let [target] = targets else {
let n = targets.len();
let word = if n == 1 { "column" } else { "columns" };
return Err(Error::new(
SqlState::SYNTAX_ERROR,
format!("assignment source returned {n} {word}"),
));
};
if let Some((base, indirection)) = indirect {
let rhs = std::mem::replace(&mut target.expr, Expr::new(ExprKind::CaseTest, 0));
let at = base.place();
let parts = (base, &name[..], (ty, typmod));
target.expr =
self.assignment_indirection(parts, &indirection, rhs, Context::Plpgsql, at)?;
return Ok(());
}
let source = target.expr.ty;
let composite = |t: u32| t == oid::RECORD || types::is_complex(t);
if ty != source && composite(ty) && composite(source) {
return Ok(());
}
let expr = std::mem::replace(&mut target.expr, Expr::new(ExprKind::CaseTest, 0));
let at = expr.location;
let Some(expr) = self.coerce_to_target(expr, ty, typmod, Context::Plpgsql, None)? else {
return Err(Error::new(
SqlState::DATATYPE_MISMATCH,
format!(
"variable \"{name}\" is of type {} but expression is of type {}",
types::name(ty),
types::name(source)
),
)
.with_hint("You will need to rewrite or cast the expression.")
.at_opt(at));
};
target.expr = expr;
Ok(())
}
}
pub fn plpgsql_cast(env: &dyn Env, source: (u32, i32), target: u32, typmod: i32) -> Result<Query> {
let mut analyzer = Analyzer::new(env, &Params { types: vec![source.0], variable: false });
let param = Expr { kind: ExprKind::Param(1), ty: source.0, typmod: source.1, location: None };
let Some(expr) = analyzer.coerce_to_target(param, target, typmod, Context::Plpgsql, None)?
else {
return Err(Error::new(
SqlState::CANNOT_COERCE,
format!("cannot cast type {} to {}", types::name(source.0), types::name(target)),
));
};
let target = Target { name: "?column?".into(), expr, origin: None, junk: false };
Ok(Query { targets: vec![target], params: vec![source.0], ..Query::default() })
}
pub fn row_type_fields(env: &dyn Env, ty: u32) -> Option<Vec<(String, u32, i32)>> {
let analyzer = Analyzer::new(env, &Params::default());
let columns = analyzer.row_columns(ty)?;
Some(columns.into_iter().map(|c| (c.name, c.ty, c.typmod)).collect())
}
pub fn relation_by_names(env: &dyn Env, names: &[&str]) -> Result<Option<u32>> {
let mut rv = RangeVar::default();
match names {
[rel] => rv.relname = Some((*rel).into()),
[schema, rel] => {
rv.schemaname = Some((*schema).into());
rv.relname = Some((*rel).into());
}
[database, schema, rel] => {
rv.catalogname = Some((*database).into());
rv.schemaname = Some((*schema).into());
rv.relname = Some((*rel).into());
}
_ => return Ok(None),
}
Analyzer::new(env, &Params::default()).lookup_relation(&rv)
}
pub fn column_type(env: &dyn Env, names: &[&str], column: &str) -> Result<Option<(u32, i32)>> {
let Some(relid) = relation_by_names(env, names)? else { return Ok(None) };
let analyzer = Analyzer::new(env, &Params::default());
let Ok(columns) = analyzer.open_relation(relid, "", None) else { return Ok(None) };
Ok(columns.into_iter().find(|c| c.name == column).map(|c| (c.ty, c.typmod)))
}
pub fn relation_row_type(env: &dyn Env, names: &[&str]) -> Result<Option<u32>> {
let Some(relid) = relation_by_names(env, names)? else { return Ok(None) };
if let Some(rel) = env.catalog().and_then(|c| c.relation(relid)) {
return Ok(Some(rel.row_type).filter(|&ty| ty != 0));
}
Ok(rupg_pgcatalog::builtin::types().iter().find(|t| t.relid == relid).map(|t| t.oid))
}
impl Analyzer<'_> {
pub(crate) fn column_or_variable(
&mut self,
c: &ColumnRef,
variables: &dyn Variables,
) -> Result<Expr> {
let at = crate::typename::place(c.location);
let names: Option<Vec<&str>> = c
.fields
.iter()
.map(|f| match f {
Some(Node::String(s)) => Some(&**s),
_ => None,
})
.collect();
let core = self.column_ref(c);
let missing =
|e: &Error| matches!(e.state(), SqlState::UNDEFINED_COLUMN | SqlState::UNDEFINED_TABLE);
let Some(names) = names else {
if let Some((Some(Node::A_Star(_)), prefix)) = c.fields.split_last()
&& let Err(error) = &core
&& missing(error)
{
let prefix: Option<Vec<&str>> = prefix
.iter()
.map(|f| match f {
Some(Node::String(s)) => Some(&**s),
_ => None,
})
.collect();
if let Some(prefix) = prefix.filter(|p| !p.is_empty())
&& let Some(variable) = self.composite_variable(&prefix, at)?
{
return Ok(self.variable_param(variable, at));
}
}
return core;
};
match core {
Ok(expr) => match variables.column(&names, at)? {
Some(_) => Err(Error::new(
SqlState::AMBIGUOUS_COLUMN,
format!("column reference \"{}\" is ambiguous", names.join(".")),
)
.with_detail("It could refer to either a PL/pgSQL variable or a table column.")
.at_opt(at)),
None => Ok(expr),
},
Err(error) if missing(&error) => match variables.column(&names, at)? {
Some(variable) => Ok(self.variable_param(variable, at)),
None => Err(error),
},
Err(error) => Err(error),
}
}
pub(crate) fn composite_variable(
&self,
names: &[&str],
at: Option<usize>,
) -> Result<Option<Variable>> {
let Some(variables) = self.variables else { return Ok(None) };
let Some(variable) = variables.column(names, at)? else { return Ok(None) };
let composite =
variable.ty == oid::RECORD || self.row_columns(types::base(variable.ty)).is_some();
Ok(composite.then_some(variable))
}
pub(crate) fn variable_param(&mut self, variable: Variable, at: Option<usize>) -> Expr {
if self.params.len() < variable.param {
self.params.resize(variable.param, 0);
}
self.params[variable.param - 1] = variable.ty;
Expr {
kind: ExprKind::Param(variable.param),
ty: variable.ty,
typmod: variable.typmod,
location: at,
}
}
}