use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin;
use rupg_types::{Value, oid};
use crate::Analyzer;
use crate::expr::{CastForm, Expr, ExprKind, Func, FuncForm};
use crate::types;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Context {
Implicit,
Assignment,
Plpgsql,
Explicit,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Path {
None,
Func(u32),
Relabel,
ArrayCoerce,
ViaIo,
}
pub fn find_path(target: u32, source: u32, context: Context) -> Path {
let (target, source) = (types::base(target), types::base(source));
if source == target {
return Path::Relabel;
}
if source == oid::INTERNAL || target == oid::INTERNAL {
return Path::None;
}
if let Some(cast) = builtin::cast(source, target) {
let allowed = match cast.context {
b'i' => Context::Implicit,
b'a' => Context::Assignment,
_ => Context::Explicit,
};
if context < allowed {
return Path::None;
}
return match cast.method {
b'f' => Path::Func(cast.func),
b'i' => Path::ViaIo,
_ => Path::Relabel,
};
}
if target != oid::OIDVECTOR && target != oid::INT2VECTOR {
let (target_element, source_element) = (types::element(target), types::element(source));
if target_element != 0
&& source_element != 0
&& find_path(target_element, source_element, context) != Path::None
{
return Path::ArrayCoerce;
}
}
if (context >= Context::Assignment && types::category(target) == types::STRING)
|| (context >= Context::Explicit && types::category(source) == types::STRING)
|| context == Context::Plpgsql
{
Path::ViaIo
} else {
Path::None
}
}
pub fn explicit_cast(expr: Expr, target: u32) -> Option<Expr> {
if expr.ty == target {
return Some(expr);
}
let location = expr.location;
match find_path(target, expr.ty, Context::Explicit) {
Path::None | Path::ArrayCoerce => None,
Path::Relabel => Some(Expr {
kind: ExprKind::Relabel(Box::new(expr), CastForm::Explicit),
ty: target,
typmod: -1,
location,
}),
Path::ViaIo => Some(Expr {
kind: ExprKind::CoerceViaIo(Box::new(expr), CastForm::Explicit),
ty: target,
typmod: -1,
location,
}),
Path::Func(func) => Some(build_cast(
expr,
func,
target,
-1,
Context::Explicit,
FuncForm::ExplicitCast,
location,
)),
}
}
pub fn can_coerce(inputs: &[u32], targets: &[u32], context: Context) -> bool {
let mut generic = false;
for (&input, &target) in inputs.iter().zip(targets) {
if input == target {
continue;
}
if input == oid::INTERNAL || target == oid::INTERNAL {
return false;
}
if target == oid::ANY {
continue;
}
if types::is_polymorphic(target) {
generic = true;
continue;
}
if input == oid::UNKNOWN {
continue;
}
if (target == oid::RECORD && types::is_complex(input))
|| (target == oid::RECORD_ARRAY && types::is_complex(types::element(input)))
{
continue;
}
if input == oid::RECORD && types::is_complex(types::base(target)) {
continue;
}
if find_path(target, input, context) != Path::None {
continue;
}
return false;
}
!generic || crate::poly::consistent(inputs, targets)
}
pub(crate) fn common_of(tys: &[u32]) -> std::result::Result<u32, (u32, u32, usize)> {
let Some(&first) = tys.first() else { return Ok(oid::TEXT) };
if first != oid::UNKNOWN && tys.iter().all(|&t| t == first) {
return Ok(first);
}
let mut ptype = types::base(first);
let (mut pcategory, mut ppreferred) = types::category_preferred(ptype);
for (i, &next) in tys.iter().enumerate().skip(1) {
let ntype = types::base(next);
if ntype == oid::UNKNOWN || ntype == ptype {
continue;
}
let (ncategory, npreferred) = types::category_preferred(ntype);
if ptype == oid::UNKNOWN {
(ptype, pcategory, ppreferred) = (ntype, ncategory, npreferred);
} else if ncategory != pcategory {
return Err((ptype, ntype, i));
} else if !ppreferred
&& can_coerce(&[ptype], &[ntype], Context::Implicit)
&& !can_coerce(&[ntype], &[ptype], Context::Implicit)
{
(ptype, pcategory, ppreferred) = (ntype, ncategory, npreferred);
}
}
Ok(if ptype == oid::UNKNOWN { oid::TEXT } else { ptype })
}
fn typmod_function(ty: u32) -> Option<(u32, bool)> {
let (ty, array) = match types::element(ty) {
0 => (ty, false),
element => (element, true),
};
builtin::cast(ty, ty).filter(|cast| cast.func != 0).map(|cast| (cast.func, array))
}
impl Analyzer<'_> {
pub(crate) fn coerce_to_target(
&mut self,
expr: Expr,
target: u32,
typmod: i32,
context: Context,
location: Option<usize>,
) -> Result<Option<Expr>> {
if !can_coerce(&[expr.ty], &[target], context) {
return Ok(None);
}
let input = (expr.ty, expr.typmod);
let expr = self.coerce(expr, target, typmod, context, location)?;
let length = typmod >= 0 && (expr.typmod != typmod || expr.ty != target);
let hide =
length && (expr.ty, expr.typmod) != input && !matches!(expr.kind, ExprKind::Const(_));
let mut expr = self.coerce_typmod(expr, target, typmod, context, location);
if hide
&& let ExprKind::Func(Func { args, .. }) = &mut expr.kind
&& let Some(arg) = args.first_mut()
{
hide_cast(arg);
}
Ok(Some(expr))
}
pub(crate) fn coerce(
&mut self,
expr: Expr,
target: u32,
typmod: i32,
context: Context,
location: Option<usize>,
) -> Result<Expr> {
let input = expr.ty;
if target == input {
return Ok(expr);
}
if matches!(
target,
oid::ANY
| oid::ANYELEMENT
| oid::ANYNONARRAY
| oid::ANYCOMPATIBLE
| oid::ANYCOMPATIBLENONARRAY
) {
return Ok(expr);
}
if types::is_polymorphic(target) && input != oid::UNKNOWN {
return Ok(expr);
}
if (target == oid::RECORD && types::is_complex(input))
|| (target == oid::RECORD_ARRAY && types::is_complex(types::element(input)))
{
return Ok(expr);
}
if input == oid::RECORD && types::is_complex(types::base(target)) {
return self.coerce_record_to_complex(expr, target, context, location);
}
if input == oid::UNKNOWN {
if let ExprKind::Const(value) = &expr.kind {
let (base, base_typmod) = types::base_typmod(target, typmod);
let input_typmod = if base == oid::INTERVAL { base_typmod } else { -1 };
let value = match value {
Value::Text(text) => {
let text = text.clone();
self.env.input_at(base, &text, input_typmod, expr.location).map_err(
|e| match expr.location {
Some(at) => e.with_position(Some(at)),
None => e,
},
)?
}
_ => Value::Null,
};
let constant = Expr::constant(value, base, input_typmod, expr.location);
return Ok(coerce_to_domain(constant, target, context, location, false));
}
if let ExprKind::Param(n) = expr.kind
&& let Some(ty) = self.param_type(n, target, expr.location)?
{
return Ok(Expr {
kind: ExprKind::Param(n),
ty,
typmod: -1,
location: expr.location,
});
}
}
let (form, cast) = if context == Context::Explicit {
(FuncForm::ExplicitCast, CastForm::Explicit)
} else {
(FuncForm::ImplicitCast, CastForm::Implicit)
};
let (base, base_typmod) = types::base_typmod(target, typmod);
match find_path(target, input, context) {
Path::None => Err(Error::internal(format!(
"failed to find conversion function from {} to {}",
types::name(input),
types::name(target)
))),
Path::Relabel if base != target => {
Ok(coerce_to_domain(expr, target, context, location, false))
}
Path::Relabel => Ok(Expr {
kind: ExprKind::Relabel(Box::new(expr), cast),
ty: target,
typmod: -1,
location,
}),
Path::ViaIo => {
let expr = Expr {
kind: ExprKind::CoerceViaIo(Box::new(expr), cast),
ty: base,
typmod: -1,
location,
};
Ok(coerce_to_domain(expr, target, context, location, true))
}
Path::Func(func) => {
let expr = build_cast(expr, func, base, base_typmod, context, form, location);
Ok(coerce_to_domain(expr, target, context, location, true))
}
Path::ArrayCoerce
if find_path(types::element(target), types::element(input), context)
== Path::Relabel =>
{
Ok(Expr {
kind: ExprKind::Relabel(Box::new(expr), cast),
ty: target,
typmod: -1,
location,
})
}
Path::ArrayCoerce => {
let test = Expr {
kind: ExprKind::CaseTest,
ty: types::element(input),
typmod: expr.typmod,
location: None,
};
let element = self
.coerce_to_target(test, types::element(target), typmod, context, location)?
.ok_or_else(|| {
Error::internal("failed to coerce array element type as expected")
})?;
let typmod = element.typmod;
Ok(Expr {
kind: ExprKind::ArrayCoerce {
arg: Box::new(expr),
element: Box::new(element),
form: cast,
},
ty: target,
typmod,
location,
})
}
}
}
fn coerce_record_to_complex(
&mut self,
expr: Expr,
target: u32,
context: Context,
location: Option<usize>,
) -> Result<Expr> {
let at = location.or(expr.location);
let base = types::base(target);
let cannot = |detail: Option<String>| {
let error = Error::new(
SqlState::CANNOT_COERCE,
format!("cannot cast type record to {}", types::name(target)),
);
match detail {
Some(detail) => error.with_detail(detail),
None => error,
}
.at_opt(at)
};
let ExprKind::Row(args, _) = expr.kind else { return Err(cannot(None)) };
let columns = self.row_columns(base).ok_or_else(|| cannot(None))?;
if args.len() < columns.len() {
return Err(cannot(Some("Input has too few columns.".into())));
}
if args.len() > columns.len() {
return Err(cannot(Some("Input has too many columns.".into())));
}
let mut fields = Vec::with_capacity(args.len());
for (i, (arg, column)) in args.into_iter().zip(&columns).enumerate() {
let (from, arg_at) = (arg.ty, arg.location);
let field = self.coerce_to_target(arg, column.ty, column.typmod, context, None)?;
let Some(field) = field else {
return Err(Error::new(
SqlState::CANNOT_COERCE,
format!("cannot cast type {} to {}", types::name(from), types::name(column.ty)),
)
.with_detail(format!(
"Cannot cast type record to {} in column {}.",
types::name(target),
i + 1
))
.at_opt(location.or(arg_at)));
};
fields.push(field);
}
let form =
if context == Context::Explicit { CastForm::Explicit } else { CastForm::Implicit };
let row = Expr { kind: ExprKind::Row(fields, form), ty: base, typmod: -1, location };
Ok(if base == target {
row
} else {
coerce_to_domain(row, target, context, location, false)
})
}
pub(crate) fn coerce_typmod(
&mut self,
expr: Expr,
target: u32,
typmod: i32,
context: Context,
location: Option<usize>,
) -> Expr {
let explicit = context == Context::Explicit;
typmod_cast(expr, target, typmod, context, explicit, location)
}
}
fn typmod_cast(
expr: Expr,
target: u32,
typmod: i32,
context: Context,
explicit: bool,
location: Option<usize>,
) -> Expr {
if typmod < 0 || expr.typmod == typmod && expr.ty == target {
return expr;
}
let (form, cast) = if explicit {
(FuncForm::ExplicitCast, CastForm::Explicit)
} else {
(FuncForm::ImplicitCast, CastForm::Implicit)
};
match typmod_function(target) {
Some((func, false)) => build_cast(expr, func, target, typmod, context, form, location),
Some((func, true)) => {
let ty = types::element(target);
let test = Expr { kind: ExprKind::CaseTest, ty, typmod: expr.typmod, location: None };
let element = build_cast(test, func, ty, typmod, context, form, location);
Expr {
kind: ExprKind::ArrayCoerce {
arg: Box::new(expr),
element: Box::new(element),
form: cast,
},
ty: target,
typmod,
location,
}
}
None => {
let mut expr = expr;
if let ExprKind::Const(_) = expr.kind {
expr.typmod = typmod;
expr
} else {
Expr { kind: ExprKind::Relabel(Box::new(expr), cast), ty: target, typmod, location }
}
}
}
}
impl Analyzer<'_> {
pub(crate) fn common_type(&self, exprs: &[Expr], context: &str) -> Result<u32> {
let tys: Vec<u32> = exprs.iter().map(|e| e.ty).collect();
common_of(&tys).map_err(|(p, n, i)| {
Error::new(
SqlState::DATATYPE_MISMATCH,
format!(
"{context} types {} and {} cannot be matched",
types::name(p),
types::name(n)
),
)
.at_opt(exprs[i].place())
})
}
pub(crate) fn coerce_to_common(
&mut self,
expr: Expr,
target: u32,
context: &str,
) -> Result<Expr> {
if expr.ty == target {
return Ok(expr);
}
if !can_coerce(&[expr.ty], &[target], Context::Implicit) {
return Err(Error::new(
SqlState::CANNOT_COERCE,
format!(
"{context} could not convert type {} to {}",
types::name(expr.ty),
types::name(target)
),
)
.at_opt(expr.place()));
}
self.coerce(expr, target, -1, Context::Implicit, None)
}
pub(crate) fn coerce_to_boolean(&mut self, expr: Expr, construct: &str) -> Result<Expr> {
self.coerce_to_specific(expr, oid::BOOL, construct)
}
pub(crate) fn coerce_to_specific(
&mut self,
expr: Expr,
target: u32,
construct: &str,
) -> Result<Expr> {
let ty = expr.ty;
let place = expr.place();
let coerced = if ty == target {
Some(expr)
} else {
self.coerce_to_target(expr, target, -1, Context::Assignment, None)?
};
match coerced {
Some(expr) if expr.first_set_call().is_some() => Err(Error::new(
SqlState::DATATYPE_MISMATCH,
format!("argument of {construct} must not return a set"),
)
.at_opt(expr.place())),
Some(expr) => Ok(expr),
None => Err(Error::new(
SqlState::DATATYPE_MISMATCH,
format!(
"argument of {construct} must be type {}, not type {}",
types::name(target),
types::name(ty)
),
)
.at_opt(place)),
}
}
}
fn coerce_to_domain(
mut expr: Expr,
target: u32,
context: Context,
location: Option<usize>,
hide: bool,
) -> Expr {
let (base, typmod) = types::base_typmod(target, -1);
if base == target {
return expr;
}
if hide {
hide_cast(&mut expr);
}
let expr = typmod_cast(expr, base, typmod, context, false, location);
let form = if context == Context::Explicit { CastForm::Explicit } else { CastForm::Implicit };
Expr { kind: ExprKind::CoerceToDomain(Box::new(expr), form), ty: target, typmod: -1, location }
}
pub(crate) fn hide_cast(expr: &mut Expr) {
match &mut expr.kind {
ExprKind::Func(Func { form, .. })
if matches!(form, FuncForm::ExplicitCast | FuncForm::ImplicitCast) =>
{
*form = FuncForm::ImplicitCast;
}
ExprKind::Relabel(_, form)
| ExprKind::CoerceViaIo(_, form)
| ExprKind::ArrayCoerce { form, .. }
| ExprKind::Row(_, form)
| ExprKind::CoerceToDomain(_, form) => *form = CastForm::Implicit,
_ => {}
}
}
fn build_cast(
expr: Expr,
func: u32,
target: u32,
typmod: i32,
context: Context,
form: FuncForm,
location: Option<usize>,
) -> Expr {
let nargs = builtin::proc_by_oid(func).map_or(1, |p| p.nargs);
let mut args = vec![expr];
if nargs >= 2 {
args.push(Expr::constant(Value::Int4(typmod), oid::INT4, -1, None));
}
if nargs >= 3 {
args.push(Expr::constant(Value::Bool(context == Context::Explicit), oid::BOOL, -1, None));
}
let typmod = if nargs >= 2 && typmod_function(target).is_some_and(|(f, _)| f == func) {
typmod
} else {
-1
};
Expr {
kind: ExprKind::Func(Func { oid: func, args, form, variadic: false, retset: false }),
ty: target,
typmod,
location,
}
}
pub(crate) trait AtOpt {
fn at_opt(self, location: Option<usize>) -> Self;
}
impl AtOpt for Error {
fn at_opt(self, location: Option<usize>) -> Error {
match location {
Some(at) => self.at(at),
None => self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths() {
assert_eq!(find_path(oid::INT8, oid::INT4, Context::Implicit), Path::Func(481));
assert_eq!(find_path(oid::INT4, oid::INT8, Context::Implicit), Path::None);
assert_eq!(find_path(oid::INT4, oid::INT8, Context::Assignment), Path::Func(480));
assert_eq!(find_path(oid::TEXT, oid::VARCHAR, Context::Implicit), Path::Relabel);
assert_eq!(find_path(oid::TEXT, oid::INT4, Context::Implicit), Path::None);
assert_eq!(find_path(oid::TEXT, oid::INT4, Context::Assignment), Path::ViaIo);
assert_eq!(find_path(oid::INT4, oid::TEXT, Context::Explicit), Path::ViaIo);
assert_eq!(
find_path(oid::INT8_ARRAY, oid::INT4_ARRAY, Context::Implicit),
Path::ArrayCoerce
);
assert_eq!(find_path(oid::OID_ARRAY, oid::OIDVECTOR, Context::Implicit), Path::ArrayCoerce);
assert_eq!(find_path(oid::OIDVECTOR, oid::OID_ARRAY, Context::Explicit), Path::None);
assert!(can_coerce(&[oid::UNKNOWN, oid::INT2], &[oid::INT4, oid::INT4], Context::Implicit));
assert!(!can_coerce(&[oid::TEXT], &[oid::INT4], Context::Implicit));
assert_eq!(typmod_function(oid::VARCHAR).map(|(_, a)| a), Some(false));
assert_eq!(typmod_function(oid::INT4), None);
}
}