mod explain;
mod function;
mod partqual;
mod query;
pub use explain::explain;
pub use query::view_statement;
use std::ptr;
use std::rc::Rc;
use rupg_analyze::node::{StoredBound, StoredDatum};
use rupg_analyze::{
Aggref, BoolOp, BoolTest, Case, CastForm, Expr, ExprKind, Func, FuncForm, JsonBehavior,
JsonBehaviorKind, JsonCtor, JsonCtorKind, JsonEncoding, JsonExpr, JsonExprOp, JsonFormat,
JsonFormatKind, JsonItemType, JsonTable, JsonTablePlan, JsonTableScan, JsonWrapper, RowCompare,
SortGroup, SqlValue, SubLink, SubLinkKind, Var, WindowClause, WindowFunc, XmlExpr, XmlOp,
};
use rupg_analyze::{
MERGE_ACTION, RETURNING_NEW, RETURNING_OLD, default_opclass, node, sort_operators,
};
use rupg_catalog::{ConKind, Constraint, IndexInfo, PartStrategy, PartitionKey, RelKind, Relation};
use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin::{self, Named};
use rupg_plan::TARGET;
use rupg_types::{Value, oid, sql_name};
use crate::io::to_text;
use crate::reg;
use crate::text::quote_identifier;
use crate::{Call, Kernel, Session, bad_value, not_yet};
const INDENT_VAR: i32 = 4;
const INDENT_STD: i32 = 8;
const INDENT_LIMIT: i32 = 40;
const BTREE: u32 = 403;
const HASH: u32 = 405;
const DESC: i16 = 1;
const NULLS_FIRST: i16 = 2;
const FIRST_USER_OID: u32 = 16384;
#[derive(Clone, Debug, Default)]
struct Namespace {
names: Vec<String>,
columns: Vec<Vec<String>>,
print_aliases: Vec<bool>,
merged: Vec<(Expr, String)>,
relations: Vec<rupg_analyze::Relation>,
joins: Vec<JoinNames>,
inside: Vec<usize>,
removed: Vec<(usize, Rc<Removed>)>,
target: Option<(String, Vec<String>)>,
returning: Option<(String, String, Vec<String>)>,
}
#[derive(Debug)]
struct Removed {
namespaces: Vec<Namespace>,
targets: Vec<Expr>,
}
#[derive(Clone, Debug, Default)]
struct JoinNames {
name: Option<String>,
relations: Vec<usize>,
columns: Vec<String>,
using: Vec<String>,
values: Vec<Expr>,
print_aliases: bool,
}
struct Deparser<'a> {
session: &'a dyn Session,
namespaces: Vec<Namespace>,
paren: bool,
indent: i32,
pub(crate) flat: bool,
wrap: i32,
prefix: bool,
names_visible: bool,
in_group_by: bool,
outputs: Vec<(String, Option<Var>)>,
windows: Vec<WindowClause>,
window_targets: Vec<Expr>,
subplans: Vec<SubName>,
subplan: Option<String>,
window_names: Vec<Option<String>>,
outer_prefix: bool,
pub(crate) own: Option<usize>,
function: Option<(String, Vec<String>)>,
buf: String,
}
#[derive(Clone, Debug)]
struct SubName {
link: *const SubLink,
name: String,
init: bool,
hashed: bool,
alternative: bool,
}
impl<'a> Deparser<'a> {
fn for_columns(
session: &'a dyn Session,
name: &str,
columns: Vec<String>,
paren: bool,
) -> Deparser<'a> {
let mut d = Deparser::new(session, paren, 0);
d.namespaces.push(Namespace {
names: vec![name.to_string()],
columns: vec![columns],
print_aliases: vec![false],
merged: Vec::new(),
relations: Vec::new(),
joins: Vec::new(),
inside: Vec::new(),
removed: Vec::new(),
target: None,
returning: None,
});
d
}
fn new(session: &'a dyn Session, paren: bool, wrap: i32) -> Deparser<'a> {
Deparser {
session,
namespaces: Vec::new(),
paren,
indent: 0,
flat: false,
wrap,
prefix: false,
names_visible: true,
in_group_by: false,
outputs: Vec::new(),
windows: Vec::new(),
window_targets: Vec::new(),
subplans: Vec::new(),
subplan: None,
window_names: Vec::new(),
outer_prefix: false,
own: None,
function: None,
buf: String::new(),
}
}
fn parameter(&mut self, n: usize) {
let named = self.function.as_ref().and_then(|(function, names)| {
let name = names.get(n.checked_sub(1)?).filter(|name| !name.is_empty())?;
Some((quote_identifier(function), quote_identifier(name)))
});
let Some((function, name)) = named else {
self.buf.push_str(&format!("${n}"));
return;
};
if self.namespaces.iter().any(|space| !space.names.is_empty()) {
self.buf.push_str(&function);
self.buf.push('.');
}
self.buf.push_str(&name);
}
fn keyword(&mut self, word: &str, before: i32, after: i32, plus: i32) {
if self.flat {
self.buf.push_str(word);
return;
}
self.indent += before;
let kept = self.buf.trim_end_matches(' ').len();
self.buf.truncate(kept);
self.buf.push('\n');
let amount = if self.indent < INDENT_LIMIT {
self.indent.max(0)
} else {
(INDENT_LIMIT + (self.indent - INDENT_LIMIT) / (INDENT_STD / 2)) % INDENT_LIMIT
} + plus;
for _ in 0..amount {
self.buf.push(' ');
}
self.buf.push_str(word);
self.indent = (self.indent + after).max(0);
}
fn space(&mut self) {
if self.flat {
self.buf.push(' ');
}
}
fn variable(&mut self, var: &Var, in_order_by: bool) -> Result<String> {
let bogus = || Error::internal(format!("bogus varlevelsup: {}", var.levels_up));
let depth = self.namespaces.len().checked_sub(var.levels_up + 1).ok_or_else(bogus)?;
if var.relation == MERGE_ACTION {
self.buf.push_str("MERGE_ACTION()");
return Ok("merge_action".to_string());
}
let space = &self.namespaces[depth];
let target = match (&space.target, &space.returning, var.relation) {
(Some((name, columns)), _, TARGET) => Some((name, columns)),
(_, Some((old, _, columns)), RETURNING_OLD) => Some((old, columns)),
(_, Some((_, new, columns)), RETURNING_NEW) => Some((new, columns)),
_ => None,
};
let refname = match target {
Some((name, _)) => name.clone(),
None => space.names.get(var.relation).cloned().unwrap_or_default(),
};
let columns =
target.map(|(_, columns)| columns).or_else(|| space.columns.get(var.relation));
let attname = match usize::try_from(var.attnum) {
Ok(n) if n > 0 => columns.and_then(|c| c.get(n - 1)).cloned(),
_ => system_column(var.attnum).map(str::to_string),
};
let attname =
attname.ok_or_else(|| Error::internal(format!("invalid attnum {}", var.attnum)))?;
let removed = space.removed.iter().find(|(r, _)| *r == var.relation);
if let Some(removed) = removed.map(|(_, removed)| Rc::clone(removed))
&& let Ok(at @ 1..) = usize::try_from(var.attnum)
&& let Some(target) = removed.targets.get(at - 1)
{
let outer = std::mem::replace(&mut self.namespaces, removed.namespaces.clone());
let parens = !matches!(target.kind, ExprKind::Var(_));
if parens {
self.buf.push('(');
}
let done = self.expr(target, true);
if parens {
self.buf.push(')');
}
self.namespaces = outer;
done?;
return Ok(attname);
}
let mut prefix = self.prefix
|| (self.outer_prefix && var.levels_up > 0)
|| self.own.is_some_and(|o| var.levels_up == 0 && var.relation != o)
|| [RETURNING_OLD, RETURNING_NEW].contains(&var.relation);
if in_order_by && !self.in_group_by && !prefix {
prefix = self.outputs.iter().any(|(name, v)| *name == attname && *v != Some(*var));
}
if prefix && !refname.is_empty() {
self.buf.push_str("e_identifier(&refname));
self.buf.push('.');
}
self.buf.push_str("e_identifier(&attname));
Ok(attname)
}
fn whole_row(&mut self, var: &Var, e: &Expr, top: bool) -> Result<()> {
let bogus = || Error::internal(format!("bogus varlevelsup: {}", var.levels_up));
let depth = self.namespaces.len().checked_sub(var.levels_up + 1).ok_or_else(bogus)?;
let space = &self.namespaces[depth];
let refname = match (&space.returning, var.relation) {
(Some((old, ..)), RETURNING_OLD) => old.clone(),
(Some((_, new, _)), RETURNING_NEW) => new.clone(),
_ => space.names.get(var.relation).cloned().unwrap_or_default(),
};
if !refname.is_empty() {
self.buf.push_str("e_identifier(&refname));
self.buf.push('.');
}
self.buf.push('*');
if top {
self.buf.push_str("::");
self.buf.push_str(®::type_text(e.ty, Some(e.typmod), self.session)?);
}
Ok(())
}
fn open(&mut self) {
if !self.paren {
self.buf.push('(');
}
}
fn close(&mut self) {
if !self.paren {
self.buf.push(')');
}
}
fn open_pretty(&mut self) {
if self.paren {
self.buf.push('(');
}
}
fn close_pretty(&mut self) {
if self.paren {
self.buf.push(')');
}
}
fn paren(&mut self, e: &Expr, implicit: bool, parent: &Expr) -> Result<()> {
let need = self.paren && !is_simple(e, parent, self.paren);
if need {
self.buf.push('(');
}
self.expr(e, implicit)?;
if need {
self.buf.push(')');
}
Ok(())
}
fn list(&mut self, items: &[Expr], implicit: bool) -> Result<()> {
for (i, item) in items.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.expr(item, implicit)?;
}
Ok(())
}
fn expr(&mut self, e: &Expr, implicit: bool) -> Result<()> {
if let Some((join, column)) = self.join_column(e) {
if self.prefix {
self.buf.push_str("e_identifier(&join));
self.buf.push('.');
}
self.buf.push_str("e_identifier(&column));
return Ok(());
}
if let Some(name) = self.merged_name(e) {
self.buf.push_str("e_identifier(&name));
return Ok(());
}
match &e.kind {
ExprKind::Const(value) => self.constant(e, value, 0)?,
ExprKind::Param(n) => self.parameter(*n),
ExprKind::Var(var) if var.attnum == 0 => self.whole_row(var, e, false)?,
ExprKind::Var(var) => {
self.variable(var, false)?;
}
ExprKind::FieldSelect(arg, field) => {
let parens =
!matches!(arg.kind, ExprKind::Subscript(_) | ExprKind::FieldSelect(..));
if parens {
self.buf.push('(');
}
self.expr(arg, true)?;
if parens {
self.buf.push(')');
}
let name = self.field_name(arg, *field)?;
self.buf.push('.');
self.buf.push_str("e_identifier(&name));
}
ExprKind::Func(f) => self.func(e, f, implicit)?,
ExprKind::NamedArg(na) => {
self.buf.push_str("e_identifier(&na.name));
self.buf.push_str(" => ");
self.expr(&na.arg, implicit)?;
}
ExprKind::Relabel(arg, form) => self.cast_node(e, arg, *form, e.typmod, implicit)?,
ExprKind::CoerceViaIo(arg, form) => self.cast_node(e, arg, *form, -1, implicit)?,
ExprKind::ArrayCoerce { arg, form, .. } => {
self.cast_node(e, arg, *form, e.typmod, implicit)?;
}
ExprKind::CoerceToDomain(arg, CastForm::Implicit) if !implicit => {
self.expr(arg, false)?;
}
ExprKind::CoerceToDomain(arg, _) => self.coercion(arg, e.ty, e.typmod, e)?,
ExprKind::DomainValue => self.buf.push_str("VALUE"),
ExprKind::FieldStore(_) => return Err(Error::internal("unexpected FieldStore")),
ExprKind::Subscript(sub) => {
let parens =
!matches!(sub.container.kind, ExprKind::Var(_) | ExprKind::FieldSelect(..));
if parens {
self.buf.push('(');
}
self.expr(&sub.container, implicit)?;
if parens {
self.buf.push(')');
}
for (i, upper) in sub.upper.iter().enumerate() {
self.buf.push('[');
if let Some(lower) = &sub.lower {
if let Some(Some(lower)) = lower.get(i) {
self.expr(lower, false)?;
}
self.buf.push(':');
}
if let Some(upper) = upper {
self.expr(upper, false)?;
}
self.buf.push(']');
}
if let Some(assign) = &sub.assign {
self.buf.push_str(" := ");
self.expr(assign, false)?;
}
}
ExprKind::Bool(op, args) => {
self.open();
if *op == BoolOp::Not {
self.buf.push_str("NOT ");
}
let word = if *op == BoolOp::And { " AND " } else { " OR " };
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.buf.push_str(word);
}
self.paren(arg, false, e)?;
}
self.close();
}
ExprKind::NullTest(arg, is_null) => {
self.open();
self.paren(arg, true, e)?;
self.buf.push_str(if *is_null { " IS NULL" } else { " IS NOT NULL" });
self.close();
}
ExprKind::BooleanTest(arg, test) => {
self.open();
self.paren(arg, false, e)?;
self.buf.push_str(match test {
BoolTest::IsTrue => " IS TRUE",
BoolTest::IsNotTrue => " IS NOT TRUE",
BoolTest::IsFalse => " IS FALSE",
BoolTest::IsNotFalse => " IS NOT FALSE",
BoolTest::IsUnknown => " IS UNKNOWN",
BoolTest::IsNotUnknown => " IS NOT UNKNOWN",
});
self.close();
}
ExprKind::Case(case) => self.case(case)?,
ExprKind::CaseTest => self.buf.push_str("CASE_TEST_EXPR"),
ExprKind::Coalesce(args) => {
self.buf.push_str("COALESCE(");
self.list(args, true)?;
self.buf.push(')');
}
ExprKind::Row(args, form) => {
self.buf.push_str("ROW(");
self.list(args, true)?;
self.buf.push(')');
if *form == CastForm::Explicit {
let ty = reg::type_text(e.ty, Some(-1), self.session)?;
self.buf.push_str(&format!("::{ty}"));
}
}
ExprKind::JoinRow(row) => match &row.alias {
Some(_) => {
let name = self.join_row_name(row);
self.buf.push_str("e_identifier(&name));
self.buf.push_str(".*");
}
None => {
self.buf.push_str("ROW(");
self.list(&row.fields, true)?;
self.buf.push(')');
}
},
ExprKind::RowCompare(rc) => self.row_compare(rc)?,
ExprKind::MinMax { greatest, args, .. } => {
self.buf.push_str(if *greatest { "GREATEST(" } else { "LEAST(" });
self.list(args, true)?;
self.buf.push(')');
}
ExprKind::NullIf { args, .. } => {
self.buf.push_str("NULLIF(");
self.list(args, true)?;
self.buf.push(')');
}
ExprKind::Distinct { not, args, .. } => {
if *not {
self.open();
self.buf.push_str("NOT ");
}
self.open();
self.paren(&args[0], true, e)?;
self.buf.push_str(" IS DISTINCT FROM ");
self.paren(&args[1], true, e)?;
self.close();
if *not {
self.close();
}
}
ExprKind::ScalarArrayOp { func, any, args } => {
self.open();
self.paren(&args[0], true, e)?;
let element = builtin::type_by_oid(args[1].ty).map_or(0, |t| t.elem);
let op = builtin::operators()
.iter()
.filter(|op| op.code == *func)
.max_by_key(|op| (op.left == args[0].ty, op.right == element))
.ok_or_else(|| Error::internal(format!("no operator for function {func}")))?;
self.buf.push_str(&format!(" {} {} (", op.name, if *any { "ANY" } else { "ALL" }));
self.paren(&args[1], true, e)?;
self.buf.push(')');
self.close();
}
ExprKind::Array { elements, .. } => {
self.buf.push_str("ARRAY[");
self.list(elements, true)?;
self.buf.push(']');
if elements.is_empty() {
let ty = reg::type_text(e.ty, Some(-1), self.session)?;
self.buf.push_str(&format!("::{ty}"));
}
}
ExprKind::SqlValue(value) => self.buf.push_str(&sql_value(*value)),
ExprKind::Agg(agg) => self.aggregate(agg)?,
ExprKind::Grouping(g) => {
self.buf.push_str("GROUPING(");
self.list(&g.args, true)?;
self.buf.push(')');
}
ExprKind::PlaceHolder(p) => {
self.buf.push('(');
self.expr(&p.expr, true)?;
self.buf.push(')');
}
ExprKind::Window(w) => self.window_func(w)?,
ExprKind::SubLink(sub) => {
let link = ptr::from_ref(sub.as_ref());
match self.subplans.iter().find(|s| s.link == link).cloned() {
Some(name) => self.subplan_link(sub, &name)?,
None => self.sublink(sub)?,
}
}
ExprKind::SubColumn(column) => {
let Some(subplan) = &self.subplan else {
return Err(Error::internal("a column of a subquery outside its test"));
};
self.buf.push_str(&format!("{subplan}.col{}", column + 1));
}
ExprKind::JsonValue(v) => {
self.expr(&v.raw, false)?;
json_format(v.format, &mut self.buf);
}
ExprKind::JsonCtor(c) => self.json_ctor(c)?,
ExprKind::Xml(x) => self.xml_expr(x)?,
ExprKind::JsonExpr(j) => self.json_expr(j, implicit)?,
ExprKind::JsonTable(_) => return Err(Error::internal("JSON_TABLE out of FROM")),
ExprKind::CurrentOf(c) => match c.args.first().map(|a| &a.kind) {
Some(ExprKind::Param(n)) => self.buf.push_str(&format!("CURRENT OF ${n}")),
Some(ExprKind::Const(name)) => {
let name = name.as_str().unwrap_or_default();
self.buf.push_str(&format!("CURRENT OF {}", quote_identifier(name)));
}
_ => return Err(Error::internal("CURRENT OF with no cursor")),
},
ExprKind::IsJson(p) => {
self.open();
self.paren(&p.expr, true, e)?;
self.buf.push_str(" IS JSON");
self.buf.push_str(match p.item {
JsonItemType::Any => "",
JsonItemType::Object => " OBJECT",
JsonItemType::Array => " ARRAY",
JsonItemType::Scalar => " SCALAR",
});
if p.unique {
self.buf.push_str(" WITH UNIQUE KEYS");
}
self.close();
}
}
Ok(())
}
fn xml_expr(&mut self, x: &XmlExpr) -> Result<()> {
self.buf.push_str(match x.op {
XmlOp::Concat => "XMLCONCAT(",
XmlOp::Element => "XMLELEMENT(",
XmlOp::Forest => "XMLFOREST(",
XmlOp::Parse => "XMLPARSE(",
XmlOp::Pi => "XMLPI(",
XmlOp::Root => "XMLROOT(",
XmlOp::Serialize => "XMLSERIALIZE(",
XmlOp::Document => "",
});
if matches!(x.op, XmlOp::Parse | XmlOp::Serialize) {
self.buf.push_str(if x.document { "DOCUMENT " } else { "CONTENT " });
}
let mut comma = false;
if let Some(name) = &x.name {
self.buf.push_str("NAME ");
self.buf.push_str("e_identifier(&sql_name(name)));
comma = true;
}
if !x.named.is_empty() {
let forest = x.op == XmlOp::Forest;
if !forest {
if comma {
self.buf.push_str(", ");
}
self.buf.push_str("XMLATTRIBUTES(");
comma = false;
}
for (name, e) in &x.named {
if comma {
self.buf.push_str(", ");
}
self.expr(e, true)?;
self.buf.push_str(" AS ");
self.buf.push_str("e_identifier(&sql_name(name)));
comma = true;
}
if !forest {
self.buf.push(')');
}
}
if !x.args.is_empty() {
if comma {
self.buf.push_str(", ");
}
match x.op {
XmlOp::Parse => {
self.expr(&x.args[0], true)?;
let preserve = matches!(
x.args.get(1).map(|e| &e.kind),
Some(ExprKind::Const(Value::Bool(true)))
);
self.buf.push_str(if preserve {
" PRESERVE WHITESPACE"
} else {
" STRIP WHITESPACE"
});
}
XmlOp::Root => {
self.expr(&x.args[0], true)?;
self.buf.push_str(", VERSION ");
match x.args.get(1) {
Some(Expr { kind: ExprKind::Const(Value::Null), .. }) | None => {
self.buf.push_str("NO VALUE");
}
Some(version) => self.expr(version, false)?,
}
self.buf.push_str(match x.args.get(2).map(|e| &e.kind) {
Some(ExprKind::Const(Value::Int4(0))) => ", STANDALONE YES",
Some(ExprKind::Const(Value::Int4(1))) => ", STANDALONE NO",
Some(ExprKind::Const(Value::Int4(2))) => ", STANDALONE NO VALUE",
_ => "",
});
}
XmlOp::Document => {
self.open_pretty();
self.list(&x.args, false)?;
self.close_pretty();
}
_ => self.list(&x.args, true)?,
}
}
if x.op == XmlOp::Serialize {
self.buf.push_str(" AS ");
self.buf.push_str(®::type_text(x.target.0, Some(x.target.1), self.session)?);
self.buf.push_str(if x.indent { " INDENT" } else { " NO INDENT" });
}
self.buf.push_str(if x.op == XmlOp::Document { " IS DOCUMENT" } else { ")" });
Ok(())
}
fn json_expr(&mut self, j: &JsonExpr, showimplicit: bool) -> Result<()> {
self.buf.push_str(match j.op {
JsonExprOp::Exists => "JSON_EXISTS(",
JsonExprOp::Query => "JSON_QUERY(",
JsonExprOp::Value => "JSON_VALUE(",
});
self.expr(&j.formatted, showimplicit)?;
self.buf.push_str(", ");
match &j.path.kind {
ExprKind::Const(value) => self.constant(&j.path, value, -1)?,
_ => self.expr(&j.path, showimplicit)?,
}
for (i, (name, value)) in j.passing.iter().enumerate() {
self.buf.push_str(if i == 0 { " PASSING " } else { ", " });
self.expr(value, showimplicit)?;
self.buf.push_str(" AS ");
self.buf.push_str("e_identifier(name));
}
let returning = j.returning;
if j.op != JsonExprOp::Exists || returning.ty != oid::BOOL {
let ty = reg::type_text(returning.ty, Some(returning.typmod), self.session)?;
self.buf.push_str(&format!(" RETURNING {ty}"));
let default = if returning.ty == oid::JSONB {
JsonFormatKind::Jsonb
} else {
JsonFormatKind::Json
};
if j.op != JsonExprOp::Query || returning.format.kind != default {
json_format(returning.format, &mut self.buf);
}
}
if j.op == JsonExprOp::Query {
self.buf.push_str(match j.wrapper {
JsonWrapper::Conditional => " WITH CONDITIONAL WRAPPER",
JsonWrapper::Unconditional => " WITH UNCONDITIONAL WRAPPER",
JsonWrapper::None | JsonWrapper::Unspec => " WITHOUT WRAPPER",
});
self.buf.push_str(if j.omit_quotes { " OMIT QUOTES" } else { " KEEP QUOTES" });
}
let default = if j.op == JsonExprOp::Exists {
JsonBehaviorKind::False
} else {
JsonBehaviorKind::Null
};
if let Some(on_empty) = &j.on_empty
&& on_empty.kind != default
{
self.json_behavior(on_empty, "EMPTY")?;
}
if j.on_error.kind != default {
self.json_behavior(&j.on_error, "ERROR")?;
}
self.buf.push(')');
Ok(())
}
pub(crate) fn json_table(&mut self, t: &JsonTable) -> Result<()> {
let JsonTablePlan::Scan(root) = &t.plan else {
return Err(Error::internal("a JSON_TABLE plan with no scan at the top"));
};
self.buf.push_str("JSON_TABLE(");
self.indent += INDENT_VAR;
self.keyword("", 0, 0, 0);
self.expr(&t.context, false)?;
self.buf.push_str(", ");
self.json_table_path(root)?;
if !t.passing.is_empty() {
self.buf.push(' ');
self.keyword("PASSING ", 0, 0, 0);
self.indent += INDENT_VAR;
for (i, (name, value)) in t.passing.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.keyword("", 0, 0, 0);
self.expr(value, false)?;
self.buf.push_str(" AS ");
self.buf.push_str("e_identifier(name));
}
self.indent -= INDENT_VAR;
}
self.json_table_columns(t, root)?;
match t.on_error {
JsonBehaviorKind::EmptyArray => {}
kind => {
self.buf.push(' ');
self.buf.push_str(kind.name());
self.buf.push_str(" ON ERROR");
}
}
self.indent -= INDENT_VAR;
self.keyword(")", 0, 0, 0);
Ok(())
}
fn json_table_path(&mut self, scan: &JsonTableScan) -> Result<()> {
let path = Expr::new(ExprKind::Const(scan.path.clone()), oid::JSONPATH);
self.constant(&path, &scan.path, -1)?;
self.buf.push_str(" AS ");
self.buf.push_str("e_identifier(&scan.name));
Ok(())
}
fn json_table_columns(&mut self, t: &JsonTable, scan: &JsonTableScan) -> Result<()> {
self.buf.push(' ');
self.keyword("COLUMNS (", 0, 0, 0);
self.indent += INDENT_VAR;
for at in scan.columns.clone() {
let column = &t.columns[at];
if at > scan.columns.start {
self.buf.push_str(", ");
}
self.keyword("", 0, 0, 0);
self.buf.push_str("e_identifier(&column.name));
self.buf.push(' ');
let Some(ExprKind::JsonExpr(j)) = column.expr.as_ref().map(|e| &e.kind) else {
self.buf.push_str("FOR ORDINALITY");
continue;
};
self.buf.push_str(®::type_text(column.ty, Some(column.typmod), self.session)?);
let default = if j.op == JsonExprOp::Exists {
self.buf.push_str(" EXISTS");
JsonBehaviorKind::False
} else {
if j.op == JsonExprOp::Query
&& builtin::type_by_oid(column.ty).is_some_and(|row| row.category == b'S')
{
self.buf.push_str(" FORMAT JSON");
}
JsonBehaviorKind::Null
};
self.buf.push_str(" PATH ");
match &j.path.kind {
ExprKind::Const(value) => self.constant(&j.path, value, -1)?,
_ => self.expr(&j.path, false)?,
}
if j.op == JsonExprOp::Query {
self.buf.push_str(match j.wrapper {
JsonWrapper::Conditional => " WITH CONDITIONAL WRAPPER",
JsonWrapper::Unconditional => " WITH UNCONDITIONAL WRAPPER",
JsonWrapper::None | JsonWrapper::Unspec => " WITHOUT WRAPPER",
});
self.buf.push_str(if j.omit_quotes { " OMIT QUOTES" } else { " KEEP QUOTES" });
}
if let Some(on_empty) = &j.on_empty
&& on_empty.kind != default
{
self.json_behavior(on_empty, "EMPTY")?;
}
if j.on_error.kind != default {
self.json_behavior(&j.on_error, "ERROR")?;
}
}
if let Some(child) = &scan.child {
self.json_table_nested(t, child, !scan.columns.is_empty())?;
}
self.indent -= INDENT_VAR;
self.keyword(")", 0, 0, 0);
Ok(())
}
fn json_table_nested(
&mut self,
t: &JsonTable,
plan: &JsonTablePlan,
comma: bool,
) -> Result<()> {
match plan {
JsonTablePlan::Scan(scan) => {
if comma {
self.buf.push(',');
}
self.buf.push(' ');
self.keyword("NESTED PATH ", 0, 0, 0);
self.json_table_path(scan)?;
self.json_table_columns(t, scan)
}
JsonTablePlan::Sibling(left, right) => {
self.json_table_nested(t, left, comma)?;
self.json_table_nested(t, right, true)
}
}
}
fn json_behavior(&mut self, behavior: &JsonBehavior, on: &str) -> Result<()> {
match (&behavior.expr, behavior.kind) {
(Some(expr), JsonBehaviorKind::Default) => {
self.buf.push_str(" DEFAULT ");
self.expr(expr, false)?;
}
(_, kind) => {
self.buf.push(' ');
self.buf.push_str(kind.name());
}
}
self.buf.push_str(" ON ");
self.buf.push_str(on);
Ok(())
}
fn json_ctor(&mut self, c: &JsonCtor) -> Result<()> {
self.buf.push_str(match c.kind {
JsonCtorKind::Object => "JSON_OBJECT(",
JsonCtorKind::Array => "JSON_ARRAY(",
JsonCtorKind::Parse => "JSON(",
JsonCtorKind::Scalar => "JSON_SCALAR(",
JsonCtorKind::Serialize => "JSON_SERIALIZE(",
JsonCtorKind::ObjectAgg | JsonCtorKind::ArrayAgg => return self.json_agg_ctor(c),
JsonCtorKind::ArrayQuery => return self.json_array_query(c),
});
for (i, arg) in c.args.iter().enumerate() {
if i > 0 {
let object = c.kind == JsonCtorKind::Object && i % 2 != 0;
self.buf.push_str(if object { " : " } else { ", " });
}
self.expr(arg, true)?;
}
self.json_ctor_options(c)?;
self.buf.push(')');
Ok(())
}
fn json_agg_ctor(&mut self, c: &JsonCtor) -> Result<()> {
let saved = std::mem::take(&mut self.buf);
let done = self.json_ctor_options(c);
let options = std::mem::replace(&mut self.buf, saved);
done?;
let object = c.kind == JsonCtorKind::ObjectAgg;
let json =
Some((if object { "JSON_OBJECTAGG" } else { "JSON_ARRAYAGG" }, &*options, object));
match c.func.as_ref().map(|f| &f.kind) {
Some(ExprKind::Agg(agg)) => self.aggregate_as(agg, json),
Some(ExprKind::Window(w)) => self.window_func_as(w, json),
_ => Err(Error::internal("invalid JsonConstructorExpr underlying node type")),
}
}
fn json_ctor_options(&mut self, c: &JsonCtor) -> Result<()> {
if c.absent_on_null {
if matches!(c.kind, JsonCtorKind::Object | JsonCtorKind::ObjectAgg) {
self.buf.push_str(" ABSENT ON NULL");
}
} else if matches!(c.kind, JsonCtorKind::Array | JsonCtorKind::ArrayAgg) {
self.buf.push_str(" NULL ON NULL");
}
if c.unique {
self.buf.push_str(" WITH UNIQUE KEYS");
}
if !matches!(c.kind, JsonCtorKind::Parse | JsonCtorKind::Scalar) {
let returning = c.returning;
let ty = reg::type_text(returning.ty, Some(returning.typmod), self.session)?;
self.buf.push_str(&format!(" RETURNING {ty}"));
let default = if returning.ty == oid::JSONB {
JsonFormatKind::Jsonb
} else {
JsonFormatKind::Json
};
if returning.format.kind != default {
json_format(returning.format, &mut self.buf);
}
}
Ok(())
}
fn constant(&mut self, e: &Expr, value: &Value, showtype: i32) -> Result<()> {
let label = |this: &Self| reg::type_text(e.ty, Some(e.typmod), this.session);
if value.is_null() {
self.buf.push_str("NULL");
if showtype >= 0 {
let ty = label(self)?;
self.buf.push_str(&format!("::{ty}"));
}
return Ok(());
}
let text = to_text(e.ty, value, self.session)?;
let mut need = false;
match e.ty {
oid::INT4 if !text.starts_with('-') => self.buf.push_str(&text),
oid::NUMERIC
if text.starts_with(|c: char| c.is_ascii_digit())
&& text.contains(['e', 'E', '.']) =>
{
self.buf.push_str(&text);
}
oid::INT4 | oid::NUMERIC => {
self.buf.push_str(&format!("'{text}'"));
need = true;
}
oid::BOOL => {
self.buf.push_str(if value.as_bool() == Some(true) { "true" } else { "false" })
}
_ => {
self.buf.push('\'');
self.buf.push_str(&text.replace('\'', "''"));
self.buf.push('\'');
}
}
if showtype < 0 {
return Ok(());
}
let need = match e.ty {
oid::BOOL | oid::UNKNOWN => false,
oid::INT4 => need,
oid::NUMERIC => need || e.typmod >= 0,
_ => true,
};
if need || showtype > 0 {
let ty = label(self)?;
self.buf.push_str(&format!("::{ty}"));
}
Ok(())
}
fn cast_node(
&mut self,
e: &Expr,
arg: &Expr,
form: CastForm,
typmod: i32,
implicit: bool,
) -> Result<()> {
if form == CastForm::Implicit && !implicit {
self.paren(arg, false, e)
} else {
self.coercion(arg, e.ty, typmod, e)
}
}
fn coercion(&mut self, arg: &Expr, ty: u32, typmod: i32, parent: &Expr) -> Result<()> {
match &arg.kind {
ExprKind::Const(value) if arg.ty == ty && arg.typmod == -1 => {
self.constant(arg, value, -1)?;
}
_ => {
self.open();
self.paren(arg, false, parent)?;
self.close();
}
}
let ty = reg::type_text(ty, Some(typmod), self.session)?;
self.buf.push_str(&format!("::{ty}"));
Ok(())
}
fn func(&mut self, e: &Expr, f: &Func, implicit: bool) -> Result<()> {
match f.form {
FuncForm::Operator(opno) => {
let op = builtin::operator_by_oid(opno).ok_or_else(|| {
Error::internal(format!("cache lookup failed for operator {opno}"))
})?;
self.open();
if let [left, right] = f.args.as_slice() {
self.paren(left, true, e)?;
self.buf.push_str(&format!(" {} ", op.name));
self.paren(right, true, e)?;
} else {
self.buf.push_str(&format!("{} ", op.name));
self.paren(&f.args[0], true, e)?;
}
self.close();
}
FuncForm::ImplicitCast if !implicit => self.paren(&f.args[0], false, e)?,
FuncForm::ExplicitCast | FuncForm::ImplicitCast => {
self.coercion(&f.args[0], e.ty, e.typmod, e)?;
}
FuncForm::SqlSyntax => {
if !self.sql_syntax(e, f)? {
self.call(f)?;
}
}
FuncForm::Call => self.call(f)?,
}
Ok(())
}
fn row_compare(&mut self, rc: &RowCompare) -> Result<()> {
let mut pairs = Vec::with_capacity(rc.ops.len());
let mut name = "";
for op in &rc.ops {
let ExprKind::Func(Func { form: FuncForm::Operator(opno), args, .. }) = &op.kind else {
return Err(Error::internal("a row comparison that is not an operator"));
};
let [left, right] = args.as_slice() else {
return Err(Error::internal("a row comparison operator needs two arguments"));
};
if name.is_empty() {
name = builtin::operator_by_oid(*opno).map(|op| op.name).ok_or_else(|| {
Error::internal(format!("cache lookup failed for operator {opno}"))
})?;
}
pairs.push((left, right));
}
self.buf.push_str("(ROW(");
for (i, (left, _)) in pairs.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.expr(left, true)?;
}
self.buf.push_str(&format!(") {name} ROW("));
for (i, (_, right)) in pairs.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
self.expr(right, true)?;
}
self.buf.push_str("))");
Ok(())
}
fn call(&mut self, f: &Func) -> Result<()> {
let proc = builtin::proc_by_oid(f.oid).ok_or_else(|| {
Error::internal(format!("cache lookup failed for function {}", f.oid))
})?;
if reg::function_visible(proc, self.session) {
self.buf.push_str("e_identifier(proc.name));
} else {
let schema = reg::namespace_name(proc.namespace, self.session);
self.buf.push_str(®::qualified(schema, proc.name));
}
self.buf.push('(');
let variadic = f.variadic && proc.variadic != 0;
for (i, arg) in f.args.iter().enumerate() {
if i > 0 {
self.buf.push_str(", ");
}
if variadic && i + 1 == f.args.len() {
self.buf.push_str("VARIADIC ");
}
self.expr(arg, true)?;
}
self.buf.push(')');
Ok(())
}
fn case(&mut self, case: &Case) -> Result<()> {
self.keyword("CASE", 0, INDENT_VAR, 0);
if let Some(arg) = &case.arg {
self.buf.push(' ');
self.expr(arg, true)?;
}
for (when, result) in &case.whens {
let mut shown = when;
if case.arg.is_some()
&& let ExprKind::Func(Func { form: FuncForm::Operator(_), args, .. }) = &when.kind
&& let [left, right] = args.as_slice()
&& matches!(left.strip_implicit().kind, ExprKind::CaseTest)
{
shown = right;
}
self.space();
self.keyword("WHEN ", 0, 0, 0);
self.expr(shown, false)?;
self.buf.push_str(" THEN ");
self.expr(result, true)?;
}
self.space();
self.keyword("ELSE ", 0, 0, 0);
self.expr(&case.default, true)?;
self.space();
self.keyword("END", -INDENT_VAR, 0, 0);
Ok(())
}
}
fn system_column(attnum: i16) -> Option<&'static str> {
Some(match attnum {
-1 => "ctid",
-2 => "xmin",
-3 => "cmin",
-4 => "xmax",
-5 => "cmax",
-6 => "tableoid",
_ => return None,
})
}
fn sql_value(value: SqlValue) -> String {
let with = |name: &str, precision: Option<i32>| match precision {
Some(p) => format!("{name}({p})"),
None => name.to_string(),
};
match value {
SqlValue::CurrentDate => "CURRENT_DATE".to_string(),
SqlValue::CurrentTime(p) => with("CURRENT_TIME", p),
SqlValue::CurrentTimestamp(p) => with("CURRENT_TIMESTAMP", p),
SqlValue::LocalTime(p) => with("LOCALTIME", p),
SqlValue::LocalTimestamp(p) => with("LOCALTIMESTAMP", p),
SqlValue::CurrentRole => "CURRENT_ROLE".to_string(),
SqlValue::CurrentUser => "CURRENT_USER".to_string(),
SqlValue::User => "USER".to_string(),
SqlValue::SessionUser => "SESSION_USER".to_string(),
SqlValue::CurrentCatalog => "CURRENT_CATALOG".to_string(),
SqlValue::CurrentSchema => "CURRENT_SCHEMA".to_string(),
}
}
fn simple_binary_op(e: &Expr) -> Option<u8> {
match &e.kind {
ExprKind::Func(Func { form: FuncForm::Operator(opno), args, .. }) if args.len() == 2 => {
builtin::operator_by_oid(*opno).and_then(|op| match op.name.as_bytes() {
[c] => Some(*c),
_ => None,
})
}
_ => None,
}
}
fn own_syntax(parent: &Expr) -> bool {
match &parent.kind {
ExprKind::Func(f) => f.form == FuncForm::Call,
ExprKind::Bool(..)
| ExprKind::Subscript(_)
| ExprKind::Array { .. }
| ExprKind::Row(..)
| ExprKind::JoinRow(_)
| ExprKind::Coalesce(_)
| ExprKind::MinMax { .. }
| ExprKind::NullIf { .. }
| ExprKind::Agg(_)
| ExprKind::Grouping(_)
| ExprKind::PlaceHolder(_)
| ExprKind::Window(_)
| ExprKind::Xml(_)
| ExprKind::Case(_) => true,
_ => false,
}
}
fn bool_simple(op: BoolOp, parent: &Expr, paren: bool) -> bool {
match &parent.kind {
ExprKind::Bool(parent_op, _) => {
paren
&& match op {
BoolOp::Not | BoolOp::And => matches!(parent_op, BoolOp::And | BoolOp::Or),
BoolOp::Or => *parent_op == BoolOp::Or,
}
}
_ => own_syntax(parent),
}
}
fn json_format(format: JsonFormat, buf: &mut String) {
match format.kind {
JsonFormatKind::Default => return,
JsonFormatKind::Json => buf.push_str(" FORMAT JSON"),
JsonFormatKind::Jsonb => buf.push_str(" FORMAT JSONB"),
}
if format.encoding != JsonEncoding::Default {
buf.push_str(" ENCODING ");
buf.push_str(format.encoding.name());
}
}
fn is_simple(e: &Expr, parent: &Expr, paren: bool) -> bool {
match &e.kind {
ExprKind::Const(_)
| ExprKind::Param(_)
| ExprKind::Var(_)
| ExprKind::SubColumn(_)
| ExprKind::DomainValue => true,
ExprKind::FieldSelect(..) => !matches!(parent.kind, ExprKind::FieldSelect(..)),
ExprKind::Array { .. }
| ExprKind::Row(..)
| ExprKind::JoinRow(_)
| ExprKind::Subscript(_)
| ExprKind::Coalesce(_)
| ExprKind::MinMax { .. }
| ExprKind::SqlValue(_)
| ExprKind::NullIf { .. }
| ExprKind::Agg(_)
| ExprKind::Grouping(_)
| ExprKind::PlaceHolder(_)
| ExprKind::Window(_)
| ExprKind::JsonCtor(_)
| ExprKind::JsonExpr(_)
| ExprKind::JsonTable(_)
| ExprKind::Xml(_)
| ExprKind::CurrentOf(_)
| ExprKind::Case(_) => true,
ExprKind::Func(f) if !matches!(f.form, FuncForm::Operator(_)) => true,
ExprKind::JsonValue(v) => is_simple(&v.raw, e, paren),
ExprKind::Relabel(arg, _)
| ExprKind::CoerceViaIo(arg, _)
| ExprKind::ArrayCoerce { arg, .. }
| ExprKind::CoerceToDomain(arg, _) => is_simple(arg, e, paren),
ExprKind::Func(f) => {
if paren
&& matches!(&parent.kind, ExprKind::Func(p) if matches!(p.form, FuncForm::Operator(_)))
{
let (Some(op), Some(parent_op)) = (simple_binary_op(e), simple_binary_op(parent))
else {
return false;
};
let (low, high) = (b"+-".contains(&op), b"*/%".contains(&op));
let (parent_low, parent_high) =
(b"+-".contains(&parent_op), b"*/%".contains(&parent_op));
if !(low || high) || !(parent_low || parent_high) {
return false;
}
if high && parent_low {
return true;
}
if low && parent_high {
return false;
}
let ExprKind::Func(p) = &parent.kind else { return false };
return p.args.first().is_some_and(|first| ptr::eq(first, e)) && !f.args.is_empty();
}
own_syntax(parent)
}
ExprKind::SubLink(_)
| ExprKind::NullTest(..)
| ExprKind::BooleanTest(..)
| ExprKind::IsJson(_) => own_syntax(parent),
ExprKind::Distinct { not: false, .. } => own_syntax(parent),
ExprKind::Distinct { not: true, .. } => bool_simple(BoolOp::Not, parent, paren),
ExprKind::Bool(op, _) => bool_simple(*op, parent, paren),
ExprKind::ScalarArrayOp { .. }
| ExprKind::RowCompare(_)
| ExprKind::CaseTest
| ExprKind::FieldStore(_)
| ExprKind::NamedArg(_) => false,
}
}
fn has_var(e: &Expr) -> bool {
matches!(e.kind, ExprKind::Var(_)) || e.children().into_iter().any(has_var)
}
impl Deparser<'_> {
fn field_name(&self, arg: &Expr, field: usize) -> Result<String> {
if arg.ty == oid::RECORD {
let levels: Vec<&[rupg_analyze::Relation]> =
self.namespaces.iter().rev().map(|n| &n.relations[..]).collect();
if let Some(columns) = rupg_analyze::record_fields(arg, &levels)?
&& let Some(column) = columns.get(field)
{
return Ok(column.name.clone());
}
}
row_field_name(arg.ty, field, self.session)
}
}
fn row_field_name(ty: u32, field: usize, session: &dyn Session) -> Result<String> {
row_names(ty, session).and_then(|names| names.get(field).cloned()).ok_or_else(|| {
Error::internal(format!("could not identify column {} in a record", field + 1))
})
}
pub(crate) fn row_names(ty: u32, session: &dyn Session) -> Option<Vec<String>> {
let mut ty = ty;
let relid = loop {
match builtin::type_by_oid(ty) {
Some(row) if row.kind == b'd' && row.base != 0 => ty = row.base,
Some(row) => break row.relid,
None => {
break session
.catalog()
.and_then(|c| c.relations().find(|r| r.row_type == ty))
.map_or(0, |r| r.oid);
}
}
};
if let Some(rel) = session.catalog().and_then(|c| c.relation(relid)) {
return Some(rel.columns.iter().filter(|c| !c.dropped).map(|c| c.name.clone()).collect());
}
column_names(relid, session)
}
fn column_names(relid: u32, session: &dyn Session) -> Option<Vec<String>> {
if let Some(rel) = session.catalog().and_then(|c| c.relation(relid)) {
return Some(rel.columns.iter().map(|c| c.name.clone()).collect());
}
builtin::class_by_oid(relid)?;
let mut names = Vec::new();
for attr in builtin::attributes(relid).iter().filter(|a| a.num > 0) {
let at = usize::try_from(attr.num - 1).unwrap_or(0);
if names.len() <= at {
names.resize(at + 1, String::new());
}
names[at] = attr.name.to_string();
}
Some(names)
}
fn deparse(
text: &str,
name: &str,
columns: &[String],
paren: bool,
session: &dyn Session,
) -> Result<String> {
if node::is_bound(text) {
return partition_bound(text, session);
}
let exprs = stored(text)?;
let mut d = Deparser::for_columns(session, name, columns.to_vec(), paren);
d.list(&exprs, false)?;
Ok(d.buf)
}
fn bound_constant(d: &mut Deparser<'_>, e: &Expr) -> Result<()> {
match &e.kind {
ExprKind::Const(value) => d.constant(e, value, -1),
_ => Err(Error::internal("a partition bound that is not a constant")),
}
}
fn partition_bound(text: &str, session: &dyn Session) -> Result<String> {
let mut d = Deparser::for_columns(session, "", Vec::new(), false);
match node::read_bound(text)? {
StoredBound::Default => d.buf.push_str("DEFAULT"),
StoredBound::Hash { modulus, remainder } => {
d.buf.push_str(&format!("FOR VALUES WITH (modulus {modulus}, remainder {remainder})"));
}
StoredBound::List(values) => {
d.buf.push_str("FOR VALUES IN (");
for (i, value) in values.iter().enumerate() {
if i > 0 {
d.buf.push_str(", ");
}
match value {
Some(e) => bound_constant(&mut d, e)?,
None => d.buf.push_str("NULL"),
}
}
d.buf.push(')');
}
StoredBound::Range { lower, upper } => {
d.buf.push_str("FOR VALUES FROM ");
range_datums(&mut d, &lower)?;
d.buf.push_str(" TO ");
range_datums(&mut d, &upper)?;
}
}
Ok(d.buf)
}
fn range_datums(d: &mut Deparser<'_>, datums: &[StoredDatum]) -> Result<()> {
d.buf.push('(');
for (i, datum) in datums.iter().enumerate() {
if i > 0 {
d.buf.push_str(", ");
}
match datum {
StoredDatum::MinValue => d.buf.push_str("MINVALUE"),
StoredDatum::MaxValue => d.buf.push_str("MAXVALUE"),
StoredDatum::Value(e) => bound_constant(d, e)?,
}
}
d.buf.push(')');
Ok(())
}
pub fn deparse_constant(text: &str, session: &dyn Session) -> Result<String> {
let e = node::read(text)?;
let mut d = Deparser::for_columns(session, "", Vec::new(), false);
bound_constant(&mut d, &e)?;
Ok(d.buf)
}
pub fn deparse_expression(text: &str, session: &dyn Session) -> Result<String> {
deparse(text, "", &[], false, session)
}
pub fn deparse_relation_expression(
text: &str,
relid: u32,
session: &dyn Session,
) -> Result<String> {
if relid == 0 {
return deparse_expression(text, session);
}
let columns = column_names(relid, session)
.ok_or_else(|| Error::internal(format!("cache lookup failed for relation {relid}")))?;
let name = reg::class_name(relid, session).map_or("", |(name, _)| name);
deparse(text, name, &columns, false, session)
}
fn stored(text: &str) -> Result<Vec<Expr>> {
if text.trim_start().starts_with('(') {
node::read_list(text)
} else {
Ok(vec![node::read(text)?])
}
}
fn relation_name(relid: u32, session: &dyn Session) -> Result<String> {
let (name, namespace) = reg::class_name(relid, session)
.ok_or_else(|| Error::internal(format!("cache lookup failed for relation {relid}")))?;
if reg::class_visible(relid, session) == Some(true) {
Ok(quote_identifier(name))
} else {
Ok(reg::qualified(reg::namespace_name(namespace, session), name))
}
}
fn qualified_relation_name(relid: u32, session: &dyn Session) -> Result<String> {
let (name, namespace) = reg::class_name(relid, session)
.ok_or_else(|| Error::internal(format!("cache lookup failed for relation {relid}")))?;
Ok(reg::qualified(reg::namespace_name(namespace, session), name))
}
fn pretty_arg(args: &[Value], index: usize) -> Result<bool> {
match args.get(index) {
Some(value) => value.as_bool().ok_or_else(bad_value),
None => Ok(false),
}
}
fn get_expr(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let text = args.first().and_then(Value::as_str).ok_or_else(bad_value)?;
let relid = args.get(1).and_then(Value::as_oid).ok_or_else(bad_value)?;
let paren = pretty_arg(args, 2)?;
let session = call.session;
let columns = if relid == 0 {
if stored(text)?.iter().any(has_var) {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
"expression contains variables",
));
}
Vec::new()
} else {
match column_names(relid, session) {
Some(columns) => columns,
None => return Ok(Value::Null),
}
};
let name = reg::class_name(relid, session).map_or("", |(name, _)| name);
Ok(Value::text(deparse(text, name, &columns, paren, session)?))
}
fn system_object(what: &str) -> Error {
not_yet(format!("{what} of a system catalog"))
}
fn column_list(rel: &Relation, keys: &[i16]) -> Result<String> {
let names: Result<Vec<String>> = keys
.iter()
.map(|&k| {
rel.column(k).map(|c| quote_identifier(&c.name)).ok_or_else(|| {
Error::internal(format!(
"cache lookup failed for attribute {k} of relation {}",
rel.oid
))
})
})
.collect();
Ok(names?.join(", "))
}
fn action(code: char) -> Option<&'static str> {
match code {
'r' => Some("RESTRICT"),
'c' => Some("CASCADE"),
'n' => Some("SET NULL"),
'd' => Some("SET DEFAULT"),
_ => None,
}
}
fn constraint_def(con: &Constraint, paren: bool, session: &dyn Session) -> Result<String> {
let catalog = session.catalog().ok_or_else(|| Error::internal("no catalog"))?;
let relation = || {
catalog.relation(con.relation).ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {}", con.relation))
})
};
let mut out = String::new();
match con.kind {
ConKind::Foreign => {
let fk = con
.foreign
.as_ref()
.ok_or_else(|| Error::internal("a foreign key without its facts"))?;
let target = catalog.relation(fk.table).ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {}", fk.table))
})?;
out.push_str(&format!(
"FOREIGN KEY ({}) REFERENCES {}({})",
column_list(relation()?, &con.keys)?,
relation_name(fk.table, session)?,
column_list(target, &fk.keys)?
));
out.push_str(match fk.match_type {
'f' => " MATCH FULL",
'p' => " MATCH PARTIAL",
_ => "",
});
if let Some(update) = action(fk.update) {
out.push_str(&format!(" ON UPDATE {update}"));
}
if let Some(delete) = action(fk.delete) {
out.push_str(&format!(" ON DELETE {delete}"));
}
}
ConKind::Primary | ConKind::Unique => {
let index = catalog.relation(con.index).and_then(|r| r.index.as_ref());
let index = index.ok_or_else(|| {
Error::internal(format!("cache lookup failed for index {}", con.index))
})?;
out.push_str(if con.kind == ConKind::Primary { "PRIMARY KEY " } else { "UNIQUE " });
if con.kind == ConKind::Unique && index.nulls_not_distinct {
out.push_str("NULLS NOT DISTINCT ");
}
let rel = relation()?;
out.push_str(&format!("({})", column_list(rel, &con.keys)?));
if index.keys.len() > con.keys.len() {
out.push_str(&format!(
" INCLUDE ({})",
column_list(rel, &index.keys[con.keys.len()..])?
));
}
}
ConKind::Check => {
let text = con
.expr
.as_deref()
.ok_or_else(|| Error::internal("a check constraint without its expression"))?;
let (name, columns): (&str, Vec<String>) = if con.domain == 0 {
let rel = relation()?;
(&rel.name, rel.columns.iter().map(|c| c.name.clone()).collect())
} else {
("", Vec::new())
};
out.push_str(&format!("CHECK ({})", deparse(text, name, &columns, paren, session)?));
if con.no_inherit {
out.push_str(" NO INHERIT");
}
}
ConKind::NotNull if con.domain != 0 => out.push_str("NOT NULL"),
ConKind::Trigger => out.push_str("TRIGGER"),
ConKind::NotNull => {
out.push_str(&format!("NOT NULL {}", column_list(relation()?, &con.keys[..1])?));
if con.no_inherit {
out.push_str(" NO INHERIT");
}
}
}
if con.deferrable {
out.push_str(" DEFERRABLE");
}
if con.deferred {
out.push_str(" INITIALLY DEFERRED");
}
if !con.enforced {
out.push_str(" NOT ENFORCED");
} else if !con.validated {
out.push_str(" NOT VALID");
}
Ok(out)
}
fn get_constraintdef(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let paren = pretty_arg(args, 1)?;
match call.session.catalog().and_then(|c| c.constraint(oid)) {
Some(con) => Ok(Value::text(constraint_def(con, paren, call.session)?)),
None if oid < FIRST_USER_OID => {
Ok(builtin_constraint_def(oid)?.map_or(Value::Null, Value::text))
}
None => Ok(Value::Null),
}
}
fn builtin_position(catalog: &str, oid: u32) -> Option<(&'static rupg_pgcatalog::Catalog, usize)> {
let cat = rupg_pgcatalog::catalog(catalog)?;
let column = if catalog == "pg_index" { "indexrelid" } else { "oid" };
match &cat.rows.get(cat.column(column)?)?.values {
rupg_pgcatalog::Values::Oid(v) => Some((cat, v.iter().position(|&o| o == oid)?)),
_ => None,
}
}
fn builtin_field(cat: &rupg_pgcatalog::Catalog, row: usize, column: &str) -> Result<String> {
cat.column(column)
.and_then(|at| cat.rows.get(at)?.text(row))
.ok_or_else(|| Error::internal(format!("no column {column} in {}", cat.name)))
}
fn builtin_columns(relid: u32, keys: &[i16]) -> Result<String> {
let names: Result<Vec<String>> = keys
.iter()
.map(|&k| {
builtin::attributes(relid)
.iter()
.find(|a| a.num == k)
.map(|a| quote_identifier(a.name))
.ok_or_else(|| {
Error::internal(format!(
"cache lookup failed for attribute {k} of relation {relid}"
))
})
})
.collect();
Ok(names?.join(", "))
}
fn int2_list(text: &str) -> Vec<i16> {
text.trim_matches(['{', '}']).split([' ', ',']).filter_map(|n| n.parse().ok()).collect()
}
fn builtin_constraint_def(oid: u32) -> Result<Option<String>> {
let Some((cat, row)) = builtin_position("pg_constraint", oid) else { return Ok(None) };
let relid: u32 = builtin_field(cat, row, "conrelid")?.parse().map_err(|_| bad_value())?;
let keys = int2_list(&builtin_field(cat, row, "conkey")?);
let columns = builtin_columns(relid, &keys)?;
Ok(Some(match builtin_field(cat, row, "contype")?.as_str() {
"p" => format!("PRIMARY KEY ({columns})"),
"u" => format!("UNIQUE ({columns})"),
"n" => format!("NOT NULL {columns}"),
_ => return Err(system_object("pg_get_constraintdef")),
}))
}
fn builtin_index_def(
oid: u32,
colno: i32,
paren: bool,
session: &dyn Session,
) -> Result<Option<String>> {
let Some((cat, row)) = builtin_position("pg_index", oid) else { return Ok(None) };
let relid: u32 = builtin_field(cat, row, "indrelid")?.parse().map_err(|_| bad_value())?;
let keys = int2_list(&builtin_field(cat, row, "indkey")?);
if colno != 0 {
let key = usize::try_from(colno - 1).ok().and_then(|at| keys.get(at));
return Ok(Some(match key {
Some(&key) => builtin_columns(relid, &[key])?,
None => String::new(),
}));
}
let index = builtin::class_by_oid(oid)
.ok_or_else(|| Error::internal(format!("cache lookup failed for relation {oid}")))?;
let table = if paren {
relation_name(relid, session)?
} else {
qualified_relation_name(relid, session)?
};
let unique = builtin_field(cat, row, "indisunique")? == "t";
Ok(Some(format!(
"CREATE {}INDEX {} ON {table} USING btree ({})",
if unique { "UNIQUE " } else { "" },
quote_identifier(index.name),
builtin_columns(relid, &keys)?
)))
}
fn expr_collation(e: &Expr, rel: &Relation) -> u32 {
fn walk(e: &Expr, rel: &Relation, found: &mut Vec<u32>) {
if let ExprKind::Var(var) = &e.kind
&& let Some(column) = rel.column(var.attnum)
&& column.collation != 0
{
found.push(column.collation);
}
for child in e.children() {
walk(child, rel, found);
}
}
if !rupg_analyze::types::collatable(e.ty) {
return 0;
}
let mut found = Vec::new();
walk(e, rel, &mut found);
match found.first() {
Some(&first) if found.iter().all(|&c| c == first) => first,
Some(_) => 0,
None => DEFAULT_COLLATION,
}
}
const DEFAULT_COLLATION: u32 = 100;
fn collation_name(collation: u32, session: &dyn Session) -> Result<String> {
let row = builtin::named_by_oid(Named::Collation, collation)
.ok_or_else(|| Error::internal(format!("cache lookup failed for collation {collation}")))?;
if reg::visible(Named::Collation, row, session) {
Ok(quote_identifier(row.name))
} else {
Ok(reg::qualified(reg::namespace_name(row.namespace, session), row.name))
}
}
fn opclass_name(class: u32, ty: u32, session: &dyn Session) -> Result<String> {
let (name, namespace, method) = reg::method_row(Named::Opclass, class, session)
.ok_or_else(|| Error::internal(format!("cache lookup failed for opclass {class}")))?;
if default_opclass(ty, method).is_some_and(|d| d.oid == class) {
return Ok(String::new());
}
if reg::method_visible(Named::Opclass, class, session) {
Ok(format!(" {}", quote_identifier(&name)))
} else {
Ok(format!(" {}", reg::qualified(reg::namespace_name(namespace, session), &name)))
}
}
fn index_def(
index: &Relation,
info: &IndexInfo,
colno: i32,
paren: bool,
session: &dyn Session,
) -> Result<String> {
let catalog = session.catalog().ok_or_else(|| Error::internal("no catalog"))?;
let rel = catalog.relation(info.table).ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {}", info.table))
})?;
let columns: Vec<String> = rel.columns.iter().map(|c| c.name.clone()).collect();
let exprs = match &info.exprs {
Some(text) => node::read_list(text)?,
None => Vec::new(),
};
let mut exprs = exprs.iter();
let whole = colno == 0;
let mut out = String::new();
if whole {
let method = match info.method {
BTREE => "btree",
HASH => "hash",
other => {
return Err(Error::internal(format!(
"cache lookup failed for access method {other}"
)));
}
};
let table = if paren {
relation_name(info.table, session)?
} else {
qualified_relation_name(info.table, session)?
};
out.push_str(&format!(
"CREATE {}INDEX {} ON {}{table} USING {method} (",
if info.unique { "UNIQUE " } else { "" },
quote_identifier(&index.name),
if index.kind == RelKind::PartitionedIndex { "ONLY " } else { "" }
));
}
let key_count = usize::try_from(info.key_count).unwrap_or(0);
for (keyno, &attnum) in info.keys.iter().enumerate() {
let shown = whole || usize::try_from(colno).is_ok_and(|c| c == keyno + 1);
if whole {
if keyno == key_count {
out.push_str(") INCLUDE (");
} else if keyno > 0 {
out.push_str(", ");
}
}
let (ty, collation) = if attnum != 0 {
let column = rel.column(attnum).ok_or_else(|| {
Error::internal(format!("cache lookup failed for attribute {attnum}"))
})?;
if shown {
out.push_str("e_identifier(&column.name));
}
(column.ty, column.collation)
} else {
let e =
exprs.next().ok_or_else(|| Error::internal("too few entries in indexprs list"))?;
if shown {
let mut d = Deparser::for_columns(session, &rel.name, columns.clone(), paren);
d.expr(e, false)?;
if looks_like_function(e) {
out.push_str(&d.buf);
} else {
out.push_str(&format!("({})", d.buf));
}
}
(e.ty, expr_collation(e, rel))
};
if whole && keyno < key_count {
let collation_used = info.collations.get(keyno).copied().unwrap_or(0);
if collation_used != 0 && collation_used != collation {
out.push_str(&format!(" COLLATE {}", collation_name(collation_used, session)?));
}
if let Some(&class) = info.classes.get(keyno) {
out.push_str(&opclass_name(class, ty, session)?);
}
if info.method == BTREE {
let option = info.options.get(keyno).copied().unwrap_or(0);
if option & DESC != 0 {
out.push_str(" DESC");
if option & NULLS_FIRST == 0 {
out.push_str(" NULLS LAST");
}
} else if option & NULLS_FIRST != 0 {
out.push_str(" NULLS FIRST");
}
}
}
}
if whole {
out.push(')');
if info.nulls_not_distinct {
out.push_str(" NULLS NOT DISTINCT");
}
if let Some(text) = &info.predicate {
out.push_str(&format!(
" WHERE {}",
deparse(text, &rel.name, &columns, paren, session)?
));
}
}
Ok(out)
}
fn looks_like_function(e: &Expr) -> bool {
match &e.kind {
ExprKind::Func(f) => matches!(f.form, FuncForm::Call | FuncForm::SqlSyntax),
ExprKind::NullIf { .. }
| ExprKind::Coalesce(_)
| ExprKind::MinMax { .. }
| ExprKind::SqlValue(_)
| ExprKind::Xml(_) => true,
_ => false,
}
}
fn statisticsobj_def(
oid: u32,
columns_only: bool,
session: &dyn Session,
) -> Result<Option<String>> {
let catalog = session.catalog().ok_or_else(|| Error::internal("no catalog"))?;
let Some(stat) = catalog.stat_ext(oid) else { return Ok(None) };
let rel = catalog.relation(stat.relation).ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {}", stat.relation))
})?;
let exprs = match &stat.exprs {
Some(text) => node::read_list(text)?,
None => Vec::new(),
};
let mut out = String::new();
if !columns_only {
let schema = if reg::is_own_temp(stat.namespace, session) {
Some("pg_temp")
} else {
reg::namespace_name(stat.namespace, session)
};
out.push_str(&format!("CREATE STATISTICS {}", reg::qualified(schema, &stat.name)));
let kinds: Vec<&str> = [(b'd', "ndistinct"), (b'f', "dependencies"), (b'm', "mcv")]
.into_iter()
.filter(|(code, _)| stat.kinds.contains(code))
.map(|(_, word)| word)
.collect();
if kinds.len() < 3 && stat.keys.len() + exprs.len() > 1 {
out.push_str(&format!(" ({})", kinds.join(", ")));
}
out.push_str(" ON ");
}
let mut items = Vec::new();
for &attnum in &stat.keys {
let column = rel.column(attnum).ok_or_else(|| {
Error::internal(format!(
"cache lookup failed for attribute {attnum} of relation {}",
stat.relation
))
})?;
items.push(quote_identifier(&column.name));
}
let columns: Vec<String> = rel.columns.iter().map(|c| c.name.clone()).collect();
for e in &exprs {
let mut d = Deparser::for_columns(session, &rel.name, columns.clone(), true);
d.flat = true;
d.expr(e, false)?;
items.push(if looks_like_function(e) { d.buf } else { format!("({})", d.buf) });
}
out.push_str(&items.join(", "));
if !columns_only {
out.push_str(&format!(" FROM {}", relation_name(stat.relation, session)?));
}
Ok(Some(out))
}
fn get_statisticsobjdef(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
Ok(statisticsobj_def(oid, false, call.session)?.map_or(Value::Null, Value::text))
}
fn get_statisticsobjdef_columns(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
Ok(statisticsobj_def(oid, true, call.session)?.map_or(Value::Null, Value::text))
}
fn get_statisticsobjdef_expressions(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let catalog = call.session.catalog().ok_or_else(|| Error::internal("no catalog"))?;
let Some((stat, Some(text))) = catalog.stat_ext(oid).map(|s| (s, s.exprs.as_ref())) else {
return Ok(Value::Null);
};
let rel = catalog.relation(stat.relation).ok_or_else(|| {
Error::internal(format!("cache lookup failed for relation {}", stat.relation))
})?;
let columns: Vec<String> = rel.columns.iter().map(|c| c.name.clone()).collect();
let mut items = Vec::new();
for e in node::read_list(text)? {
let mut d = Deparser::for_columns(call.session, &rel.name, columns.clone(), false);
d.expr(&e, false)?;
items.push(Some(Value::text(d.buf)));
}
Ok(Value::Array(Box::new(rupg_types::Array::one(items))))
}
fn partkey_def(
rel: &Relation,
key: &PartitionKey,
attrs_only: bool,
session: &dyn Session,
) -> Result<String> {
let columns: Vec<String> = rel.columns.iter().map(|c| c.name.clone()).collect();
let exprs = match &key.exprs {
Some(text) => node::read_list(text)?,
None => Vec::new(),
};
let mut exprs = exprs.iter();
let strategy = match key.strategy {
PartStrategy::Hash => "HASH",
PartStrategy::List => "LIST",
PartStrategy::Range => "RANGE",
};
let mut out = if attrs_only { String::new() } else { format!("{strategy} (") };
for (keyno, &attnum) in key.attrs.iter().enumerate() {
if keyno > 0 {
out.push_str(", ");
}
let (ty, collation) = if attnum != 0 {
let column = rel.column(attnum).ok_or_else(|| {
Error::internal(format!("cache lookup failed for attribute {attnum}"))
})?;
out.push_str("e_identifier(&column.name));
(column.ty, column.collation)
} else {
let e =
exprs.next().ok_or_else(|| Error::internal("too few entries in partexprs list"))?;
let mut d = Deparser::for_columns(session, &rel.name, columns.clone(), attrs_only);
d.expr(e, false)?;
if looks_like_function(e) {
out.push_str(&d.buf);
} else {
out.push_str(&format!("({})", d.buf));
}
(e.ty, expr_collation(e, rel))
};
if attrs_only {
continue;
}
let collation_used = key.collations.get(keyno).copied().unwrap_or(0);
if collation_used != 0 && collation_used != collation {
out.push_str(&format!(" COLLATE {}", collation_name(collation_used, session)?));
}
if let Some(&class) = key.classes.get(keyno) {
out.push_str(&opclass_name(class, ty, session)?);
}
}
if !attrs_only {
out.push(')');
}
Ok(out)
}
pub fn partition_key_columns(rel: &Relation, session: &dyn Session) -> Result<String> {
match &rel.partition_key {
Some(key) => partkey_def(rel, key, true, session),
None => Err(Error::internal(format!("relation {} is not partitioned", rel.oid))),
}
}
pub fn index_key_columns(index: u32, session: &dyn Session) -> Result<String> {
let catalog = session.catalog().ok_or_else(|| Error::internal("no catalog"))?;
let found = catalog.relation(index).and_then(|r| Some((r, r.index.as_ref()?)));
let Some((rel, info)) = found else {
return Err(Error::internal(format!("cache lookup failed for index {index}")));
};
let mut out = String::new();
for colno in 1..=info.key_count {
if colno > 1 {
out.push_str(", ");
}
out.push_str(&index_def(rel, info, i32::from(colno), true, session)?);
}
Ok(out)
}
fn get_partkeydef(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let found = call
.session
.catalog()
.and_then(|c| c.relation(oid))
.and_then(|r| Some((r, r.partition_key.as_ref()?)));
match found {
Some((rel, key)) => Ok(Value::text(partkey_def(rel, key, false, call.session)?)),
None => Ok(Value::Null),
}
}
fn get_partition_constraintdef(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let Some(catalog) = call.session.catalog() else { return Ok(Value::Null) };
let Some(rel) = catalog.relation(oid) else { return Ok(Value::Null) };
let mut qual = partqual::partition_qual(catalog, rel)?;
let expr = match qual.len() {
0 => return Ok(Value::Null),
1 => qual.remove(0),
_ => Expr {
kind: ExprKind::Bool(BoolOp::And, qual),
ty: oid::BOOL,
typmod: -1,
location: None,
},
};
let columns = rel.columns.iter().map(|c| c.name.clone()).collect();
let mut d = Deparser::for_columns(call.session, &rel.name, columns, false);
d.expr(&expr, false)?;
Ok(Value::text(d.buf))
}
fn get_indexdef(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let oid = args.first().and_then(Value::as_oid).ok_or_else(bad_value)?;
let colno = match args.get(1) {
Some(value) => {
i32::try_from(value.as_i64().ok_or_else(bad_value)?).map_err(|_| bad_value())?
}
None => 0,
};
let paren = pretty_arg(args, 2)?;
let found = call
.session
.catalog()
.and_then(|c| c.relation(oid))
.and_then(|r| Some((r, r.index.as_ref()?)));
match found {
Some((index, info)) => Ok(Value::text(index_def(index, info, colno, paren, call.session)?)),
None if oid < FIRST_USER_OID => {
Ok(builtin_index_def(oid, colno, paren, call.session)?.map_or(Value::Null, Value::text))
}
None => Ok(Value::Null),
}
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"pg_get_expr" | "pg_get_expr_ext" => get_expr,
"pg_get_constraintdef" | "pg_get_constraintdef_ext" => get_constraintdef,
"pg_get_indexdef" | "pg_get_indexdef_ext" => get_indexdef,
"pg_get_statisticsobjdef" => get_statisticsobjdef,
"pg_get_statisticsobjdef_columns" => get_statisticsobjdef_columns,
"pg_get_statisticsobjdef_expressions" => get_statisticsobjdef_expressions,
"pg_get_function_arguments" => function::get_function_arguments,
"pg_get_function_identity_arguments" => function::get_function_identity_arguments,
"pg_get_function_result" => function::get_function_result,
"pg_get_function_arg_default" => function::get_function_arg_default,
"pg_get_function_sqlbody" => function::get_function_sqlbody,
"pg_get_functiondef" => function::get_functiondef,
"pg_get_viewdef" | "pg_get_viewdef_ext" | "pg_get_viewdef_wrap" => query::get_viewdef,
"pg_get_viewdef_name" | "pg_get_viewdef_name_ext" => query::get_viewdef_name,
"pg_get_ruledef" | "pg_get_ruledef_ext" => query::get_ruledef,
"pg_get_triggerdef" | "pg_get_triggerdef_ext" => query::get_triggerdef,
"pg_get_partkeydef" => get_partkeydef,
"pg_get_partition_constraintdef" => get_partition_constraintdef,
_ => return None,
})
}