use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_sql::ast::{Expr, Literal, SelectItem, SelectStatement, Statement, UnOp};
use spg_storage::{Catalog, ColumnSchema, DataType, Value};
pub fn describe_prepared(stmt: &Statement, catalog: &Catalog) -> (Vec<u32>, Vec<ColumnSchema>) {
let params = collect_parameter_oids(stmt, catalog);
let columns = describe_output_columns(stmt, catalog);
(params, columns)
}
const MAX_DESCRIBE_DEPTH: usize = 16;
fn describe_output_columns(stmt: &Statement, catalog: &Catalog) -> Vec<ColumnSchema> {
let Statement::Select(s) = stmt else {
return Vec::new();
};
describe_select_columns(s, catalog, &[], 0)
}
pub(crate) fn describe_select_columns(
s: &SelectStatement,
catalog: &Catalog,
outer_ctes: &[&spg_sql::ast::Cte],
depth: usize,
) -> Vec<ColumnSchema> {
if depth > MAX_DESCRIBE_DEPTH {
return Vec::new();
}
let mut ctes: Vec<&spg_sql::ast::Cte> = s.ctes.iter().collect();
ctes.extend(outer_ctes.iter());
let ns = match &s.from {
None => Vec::new(),
Some(from) => {
let Some(mut ns) = relation_columns(&from.primary, catalog, &ctes, depth) else {
return Vec::new();
};
for j in &from.joins {
let Some(cols) = relation_columns(&j.table, catalog, &ctes, depth) else {
return Vec::new();
};
ns.extend(cols);
}
ns
}
};
if s.from.as_ref().is_some_and(|f| !f.joins.is_empty())
&& s.items
.iter()
.any(|i| matches!(i, SelectItem::QualifiedWildcard(_)))
{
return Vec::new();
}
let out = describe_select_items(&s.items, &ns);
if out.is_empty() {
return out;
}
for (_, arm) in &s.unions {
if describe_select_columns(arm, catalog, &ctes, depth + 1).len() != out.len() {
return Vec::new();
}
}
out
}
fn relation_columns(
t: &spg_sql::ast::TableRef,
catalog: &Catalog,
ctes: &[&spg_sql::ast::Cte],
depth: usize,
) -> Option<Vec<ColumnSchema>> {
if let Some(sub) = &t.lateral_subquery {
let cols = describe_select_columns(sub, catalog, &[], depth + 1);
return (!cols.is_empty()).then_some(cols);
}
if let Some(table) = catalog.get(&t.name) {
return Some(table.schema().columns.clone());
}
if let Some(cte) = ctes.iter().find(|c| c.name == t.name) {
let spg_sql::ast::CteBody::Select(body) = &cte.body else {
return None;
};
let mut cols = describe_select_columns(body, catalog, &[], depth + 1);
if cols.is_empty() {
return None;
}
if !cte.column_overrides.is_empty() && cte.column_overrides.len() == cols.len() {
for (slot, name) in cols.iter_mut().zip(cte.column_overrides.iter()) {
slot.name = name.clone();
}
}
return Some(cols);
}
if catalog.has_view(&t.name) {
let cols = describe_view_columns_depth(catalog, &t.name, depth + 1);
return (!cols.is_empty()).then_some(cols);
}
None
}
fn describe_select_items(items: &[SelectItem], schema_cols: &[ColumnSchema]) -> Vec<ColumnSchema> {
let mut out: Vec<ColumnSchema> = Vec::with_capacity(items.len());
for item in items {
match item {
SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
for c in schema_cols {
out.push(c.clone());
}
}
SelectItem::Expr { expr, alias } => {
let Some(desc) = describe_expr(expr, schema_cols) else {
return Vec::new();
};
let name = alias.clone().unwrap_or(desc.name);
out.push(ColumnSchema {
collation_name: None,
user_composite_type: None,
acl: alloc::vec::Vec::new(),
name,
ty: desc.ty,
nullable: desc.nullable,
auto_increment: false,
default: None,
runtime_default: None,
user_enum_type: None,
user_domain_type: None,
on_update_runtime: None,
collation: spg_storage::Collation::Binary,
is_unsigned: false,
inline_enum_variants: None,
inline_set_variants: None,
generated_stored_expr: None,
identity_always: false,
default_text: None,
auto_restart: None,
scalar_row_source: false,
mysql_int_width: None,
mysql_fsp: None,
});
}
}
}
out
}
pub(crate) fn describe_view_columns(catalog: &Catalog, view_name: &str) -> Vec<ColumnSchema> {
describe_view_columns_depth(catalog, view_name, 0)
}
fn describe_view_columns_depth(
catalog: &Catalog,
view_name: &str,
depth: usize,
) -> Vec<ColumnSchema> {
if depth > MAX_DESCRIBE_DEPTH {
return Vec::new();
}
let Some(view) = catalog.view(view_name) else {
return Vec::new();
};
let Ok(Statement::Select(select)) = spg_sql::parser::parse_statement(&view.body) else {
return Vec::new();
};
let mut out = describe_select_columns(&select, catalog, &[], depth);
if !view.columns.is_empty() && view.columns.len() == out.len() {
for (slot, name) in out.iter_mut().zip(view.columns.iter()) {
slot.name = name.clone();
}
}
for c in &mut out {
c.nullable = true;
}
out
}
pub(crate) struct ExprShape {
pub(crate) name: String,
pub(crate) ty: DataType,
pub(crate) nullable: bool,
}
pub(crate) fn numeric_rank(t: DataType) -> Option<u8> {
match t {
DataType::SmallInt => Some(1),
DataType::Int => Some(2),
DataType::BigInt => Some(3),
DataType::Numeric { .. } => Some(4),
DataType::Real => Some(5),
DataType::Float => Some(6),
_ => None,
}
}
pub(crate) fn common_type(types: &[DataType]) -> Option<DataType> {
let mut distinct: Vec<&DataType> = Vec::new();
for t in types {
if !distinct.iter().any(|d| *d == t) {
distinct.push(t);
}
}
if distinct.len() < 2 {
return None;
}
if distinct.iter().all(|t| numeric_rank(**t).is_some()) {
return distinct
.iter()
.max_by_key(|t| numeric_rank(***t).unwrap_or(0))
.map(|t| *(*t));
}
let non_text: Vec<&DataType> = distinct
.iter()
.copied()
.filter(|t| !matches!(t, DataType::Text))
.collect();
if non_text.iter().all(|t| {
matches!(
t,
DataType::Date | DataType::Timestamp | DataType::Timestamptz
)
}) && non_text
.iter()
.any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
{
if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
return Some(DataType::Timestamptz);
}
return Some(DataType::Timestamp);
}
if non_text.len() == 1 {
return Some(*non_text[0]);
}
None
}
fn literal_type(lit: &spg_sql::ast::Literal) -> Option<(DataType, bool)> {
use spg_sql::ast::Literal as L;
let (ty, nullable) = match lit {
L::Null => (DataType::Text, true),
L::TextArray(_) | L::IntArray(_) | L::BigIntArray(_) => (DataType::Text, false),
L::Integer(n) => {
if i32::try_from(*n).is_ok() {
(DataType::Int, false)
} else {
(DataType::BigInt, false)
}
}
L::Float(_) => (DataType::Float, false),
L::Numeric { .. } => (
DataType::Numeric {
precision: 0,
scale: 0,
},
false,
),
L::NumericBig(_) => (
DataType::Numeric {
precision: 0,
scale: 0,
},
false,
),
L::String(_) => (DataType::Text, false),
L::Bool(_) => (DataType::Bool, false),
L::Vector(_) | L::Interval { .. } => return None,
};
Some((ty, nullable))
}
pub(crate) fn describe_expr_type(e: &Expr, schema_cols: &[ColumnSchema]) -> Option<DataType> {
match e {
Expr::Column(c) => {
if let Some(col) = schema_cols.iter().find(|s| s.name == c.name) {
return Some(col.ty);
}
describe_expr(e, schema_cols).map(|s| s.ty)
}
Expr::Literal(lit) => literal_type(lit).map(|(ty, _)| ty),
_ => describe_expr(e, schema_cols).map(|s| s.ty),
}
}
pub(crate) fn describe_expr(e: &Expr, schema_cols: &[ColumnSchema]) -> Option<ExprShape> {
match e {
Expr::Column(c) => {
let bare = schema_cols.iter().find(|s| s.name == c.name);
if let Some(col) = bare {
return Some(ExprShape {
name: c.name.clone(),
ty: col.ty,
nullable: col.nullable,
});
}
let suffix = alloc::format!(".{}", c.name);
let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
let first = matches.next()?;
if matches.next().is_some() {
return None;
}
Some(ExprShape {
name: c.name.clone(),
ty: first.ty,
nullable: first.nullable,
})
}
Expr::Literal(lit) => {
let (ty, nullable) = literal_type(lit)?;
Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable,
})
}
Expr::Cast { target, .. } => {
use spg_sql::ast::CastTarget;
let ty = match target {
CastTarget::Int => DataType::Int,
CastTarget::BigInt => DataType::BigInt,
CastTarget::Float => DataType::Float,
CastTarget::Text => DataType::Text,
CastTarget::Bool => DataType::Bool,
CastTarget::Vector => return None,
CastTarget::Date => DataType::Date,
CastTarget::Timestamp => DataType::Timestamp,
CastTarget::Timestamptz => DataType::Timestamptz,
CastTarget::Interval => DataType::Interval,
CastTarget::Json => DataType::Json,
CastTarget::Jsonb => DataType::Jsonb,
CastTarget::RegType | CastTarget::RegClass => DataType::Text,
CastTarget::TextArray => DataType::TextArray,
CastTarget::IntArray => DataType::IntArray,
CastTarget::BigIntArray => DataType::BigIntArray,
CastTarget::TsVector => DataType::TsVector,
CastTarget::TsQuery => DataType::TsQuery,
CastTarget::Uuid => DataType::Uuid,
CastTarget::Bytea => DataType::Bytes,
CastTarget::Named(name) => crate::conversions::type_name_to_data_type(name)?,
};
Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable: true,
})
}
Expr::Unary {
op: UnOp::Neg,
expr,
} => {
let inner = describe_expr(expr, schema_cols)?;
Some(ExprShape {
name: "?column?".to_string(),
ty: inner.ty,
nullable: inner.nullable,
})
}
Expr::FunctionCall { name, args } => function_return_shape(name, args, schema_cols),
Expr::AggregateOrdered { call, .. } => describe_expr(call, schema_cols),
Expr::WindowFunction { name, args, .. } => {
let lower = name.to_ascii_lowercase();
let fixed = match lower.as_str() {
"row_number" | "rank" | "dense_rank" => Some(DataType::BigInt),
"ntile" => Some(DataType::Int),
"percent_rank" | "cume_dist" => Some(DataType::Float),
_ => None,
};
if let Some(ty) = fixed {
return Some(ExprShape {
name: lower,
ty,
nullable: true,
});
}
if matches!(
lower.as_str(),
"lag" | "lead" | "first_value" | "last_value" | "nth_value"
) {
let inner = describe_expr(args.first()?, schema_cols)?;
return Some(ExprShape {
name: lower,
ty: inner.ty,
nullable: true,
});
}
let inner = function_return_shape(name, args, schema_cols)?;
Some(ExprShape {
name: lower,
ty: inner.ty,
nullable: true,
})
}
Expr::Case {
branches,
else_branch,
..
} => {
let probe = branches
.first()
.map(|(_, t)| t)
.or(else_branch.as_deref())?;
let inner = describe_expr(probe, schema_cols)?;
Some(ExprShape {
name: "case".to_string(),
ty: inner.ty,
nullable: true,
})
}
Expr::Binary { lhs, op, rhs: _ } => {
use spg_sql::ast::BinOp as B;
match op {
B::Eq | B::NotEq | B::Lt | B::LtEq | B::Gt | B::GtEq | B::And | B::Or => {
Some(ExprShape {
name: "?column?".to_string(),
ty: DataType::Bool,
nullable: true,
})
}
_ => {
let inner = describe_expr(lhs, schema_cols)?;
let ty = if matches!(op, B::Concat)
&& matches!(inner.ty, DataType::Bit(_) | DataType::BitVarying(_))
{
DataType::BitVarying(0)
} else {
inner.ty
};
Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable: true,
})
}
}
}
Expr::ArraySubscript { target, .. } => {
let inner = describe_expr(target, schema_cols)?;
let elem = match inner.ty {
DataType::IntArray => DataType::Int,
DataType::BigIntArray => DataType::BigInt,
DataType::TextArray => DataType::Text,
other => other,
};
Some(ExprShape {
name: "?column?".to_string(),
ty: elem,
nullable: true,
})
}
Expr::ArraySlice { target, .. } => describe_expr(target, schema_cols),
Expr::Placeholder(_) => Some(ExprShape {
name: "?column?".to_string(),
ty: DataType::Text,
nullable: true,
}),
_ => None,
}
}
fn function_return_shape(
name: &str,
args: &[Expr],
schema_cols: &[ColumnSchema],
) -> Option<ExprShape> {
let lc = name.to_ascii_lowercase();
let (ty, nullable) = match lc.as_str() {
"now"
| "current_timestamp"
| "localtimestamp"
| "transaction_timestamp"
| "statement_timestamp"
| "clock_timestamp" => (DataType::Timestamptz, false),
"current_date" => (DataType::Date, false),
"timezone" if args.len() == 2 => {
let src_is_tstz = args
.get(1)
.and_then(|a| describe_expr(a, schema_cols))
.is_some_and(|s| matches!(s.ty, DataType::Timestamptz));
(
if src_is_tstz {
DataType::Timestamp
} else {
DataType::Timestamptz
},
true,
)
}
"current_time" => (DataType::TimeTz, false),
"localtime" => (DataType::Time, false),
"concat"
| "concat_ws"
| "format"
| "lower"
| "upper"
| "trim"
| "ltrim"
| "rtrim"
| "substring"
| "substr"
| "replace"
| "split_part"
| "repeat"
| "lpad"
| "rpad"
| "left"
| "right"
| "translate"
| "regexp_replace"
| "to_char"
| "encode"
| "host"
| "network"
| "version"
| "database"
| "current_database"
| "current_schema"
| "current_user"
| "session_user"
| "user"
| "pg_get_serial_sequence"
| "pg_get_constraintdef"
| "pg_get_indexdef"
| "date_format"
| "pg_typeof" => (DataType::Text, true),
"decode" | "hex" => (DataType::Bytes, true),
"length" | "char_length" | "character_length" | "octet_length" | "bit_length"
| "position" | "strpos" | "ascii" | "masklen" => (DataType::Int, true),
"count" | "count_star" | "nextval" | "currval" | "lastval" | "unix_timestamp" => {
(DataType::BigInt, true)
}
"random" | "ts_rank" | "ts_rank_cd" | "similarity" | "ln" | "log" | "log2" | "exp"
| "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "atan2" | "degrees" | "radians"
| "pi" => (DataType::Float, true),
"starts_with" => (DataType::Bool, true),
"regexp_matches"
| "regexp_split_to_array"
| "show_trgm"
| "string_to_array"
| "array_remove"
| "array_append"
| "array_cat" => (DataType::TextArray, true),
"to_json"
| "to_jsonb"
| "json_build_object"
| "jsonb_build_object"
| "json_build_array"
| "jsonb_build_array"
| "json_object"
| "jsonb_object"
| "jsonb_set"
| "jsonb_insert"
| "jsonb_path_query"
| "jsonb_path_query_first"
| "jsonb_path_query_array"
| "json_path_query" => (DataType::Json, true),
"to_tsvector" => (DataType::TsVector, true),
"to_tsquery" | "plainto_tsquery" | "phraseto_tsquery" | "websearch_to_tsquery" => {
(DataType::TsQuery, true)
}
"gen_random_uuid" | "uuid_generate_v4" => (DataType::Uuid, false),
"age" => (DataType::Interval, true),
"make_timestamp" => (DataType::Timestamp, true),
"date_trunc" | "date_bin" => {
let src = args.get(1)?;
let ty = describe_expr(src, schema_cols).map_or(DataType::Timestamp, |s| match s.ty {
DataType::Timestamptz | DataType::Date => DataType::Timestamptz,
_ => DataType::Timestamp,
});
(ty, true)
}
"date_add" | "date_subtract" => {
let src = args.first()?;
if !matches!(
describe_expr(src, schema_cols).map(|s| s.ty),
Some(DataType::Timestamptz)
) {
return None;
}
(DataType::Timestamptz, true)
}
"from_unixtime" => {
if args.len() >= 2 {
(DataType::Text, true)
} else {
(DataType::Timestamp, true)
}
}
"make_date" | "to_date" => (DataType::Date, true),
"to_timestamp" => (DataType::Timestamptz, true),
"make_timestamptz" => (DataType::Timestamptz, true),
"uuid_extract_timestamp" => (DataType::Timestamptz, true),
"date_part" | "extract" => (DataType::Float, true),
"bool_and" | "bool_or" | "every" => (DataType::Bool, true),
"string_agg" => (DataType::Text, true),
"array_agg" => {
let elem = args
.first()
.and_then(|a| describe_expr(a, schema_cols))
.map(|s| s.ty);
let ty = match elem {
Some(DataType::Int | DataType::SmallInt) => DataType::IntArray,
Some(DataType::BigInt) => DataType::BigIntArray,
_ => DataType::TextArray,
};
(ty, true)
}
"coalesce" | "greatest" | "least" | "ifnull" | "isnull" | "nullif" => {
let shapes: Vec<ExprShape> = args
.iter()
.filter(|a| !matches!(a, Expr::Literal(Literal::Null)))
.filter_map(|a| describe_expr(a, schema_cols))
.collect();
let first = shapes.first()?;
let types: Vec<DataType> = shapes.iter().map(|s| s.ty).collect();
return Some(ExprShape {
name: "?column?".to_string(),
ty: common_type(&types).unwrap_or(first.ty),
nullable: true,
});
}
"sum" => {
let inner = describe_expr(args.first()?, schema_cols)?;
let ty = match inner.ty {
DataType::SmallInt | DataType::Int => DataType::BigInt,
DataType::BigInt => DataType::Numeric {
precision: 0,
scale: 0,
},
other => other,
};
return Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable: true,
});
}
"avg" => {
let inner = describe_expr(args.first()?, schema_cols)?;
let ty = match inner.ty {
DataType::SmallInt | DataType::Int | DataType::BigInt => DataType::Numeric {
precision: 0,
scale: 0,
},
DataType::Real => DataType::Float,
other => other,
};
return Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable: true,
});
}
"max" | "min" | "abs" | "floor" | "ceil" | "ceiling" | "round" | "trunc" | "mod"
| "power" | "pow" | "sqrt" | "sign" => {
let first = args.first()?;
let inner = describe_expr(first, schema_cols)?;
return Some(ExprShape {
name: "?column?".to_string(),
ty: inner.ty,
nullable: true, });
}
_ => return None,
};
Some(ExprShape {
name: "?column?".to_string(),
ty,
nullable,
})
}
fn collect_parameter_oids(stmt: &Statement, catalog: &Catalog) -> Vec<u32> {
let max = max_placeholder(stmt);
if max == 0 {
return Vec::new();
}
let mut oids = alloc::vec![25u32; max as usize];
infer_placeholder_oids(stmt, catalog, &mut oids);
oids
}
fn wire_oid_for(ty: DataType) -> u32 {
match ty {
DataType::Bool => 16,
DataType::SmallInt => 21,
DataType::Int => 23,
DataType::BigInt => 20,
DataType::Real => 700,
DataType::Float => 701,
DataType::Numeric { .. } => 1700,
DataType::Date => 1082,
DataType::Time => 1083,
DataType::Timestamp => 1114,
DataType::Timestamptz => 1184,
DataType::Uuid => 2950,
DataType::Bytes => 17,
DataType::Json => 114,
DataType::Jsonb => 3802,
_ => 25,
}
}
fn infer_placeholder_oids(stmt: &Statement, catalog: &Catalog, oids: &mut [u32]) {
let col_oid = |schema: &[ColumnSchema], name: &spg_sql::ast::ColumnName| -> Option<u32> {
schema
.iter()
.find(|c| c.name.eq_ignore_ascii_case(&name.name))
.map(|c| wire_oid_for(c.ty))
};
let mut mark = |n: u16, oid: Option<u32>| {
if let Some(oid) = oid
&& let Some(slot) = oids.get_mut((n as usize).saturating_sub(1))
{
*slot = oid;
}
};
match stmt {
Statement::Select(s) => {
let Some(from) = &s.from else { return };
if !from.joins.is_empty() {
return;
}
let Some(t) = catalog.get(&from.primary.name) else {
return;
};
let schema = t.schema().columns.clone();
if let Some(w) = &s.where_ {
walk_expr(w, &mut |e| {
if let Expr::Binary { lhs, rhs, .. } = e {
match (lhs.as_ref(), rhs.as_ref()) {
(Expr::Column(c), Expr::Placeholder(n))
| (Expr::Placeholder(n), Expr::Column(c)) => {
mark(*n, col_oid(&schema, c));
}
_ => {}
}
}
});
}
}
Statement::Insert(ins) => {
let Some(t) = catalog.get(&ins.table) else {
return;
};
let schema = t.schema().columns.clone();
let order: alloc::vec::Vec<usize> = match &ins.columns {
Some(cols) if !cols.is_empty() => cols
.iter()
.map(|name| {
schema
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.unwrap_or(usize::MAX)
})
.collect(),
_ => (0..schema.len()).collect(),
};
for row in &ins.rows {
for (i, e) in row.iter().enumerate() {
if let Expr::Placeholder(n) = e
&& let Some(&pos) = order.get(i)
&& let Some(c) = schema.get(pos)
{
mark(*n, Some(wire_oid_for(c.ty)));
}
}
}
}
Statement::Update(u) => {
let Some(t) = catalog.get(&u.table) else {
return;
};
let schema = t.schema().columns.clone();
for (col, e) in &u.assignments {
if let Expr::Placeholder(n) = e
&& let Some(c) = schema.iter().find(|c| c.name.eq_ignore_ascii_case(col))
{
mark(*n, Some(wire_oid_for(c.ty)));
}
}
if let Some(w) = &u.where_ {
walk_expr(w, &mut |e| {
if let Expr::Binary { lhs, rhs, .. } = e {
match (lhs.as_ref(), rhs.as_ref()) {
(Expr::Column(c), Expr::Placeholder(n))
| (Expr::Placeholder(n), Expr::Column(c)) => {
mark(*n, col_oid(&schema, c));
}
_ => {}
}
}
});
}
}
_ => {}
}
}
fn max_placeholder(stmt: &Statement) -> u16 {
let mut max: u16 = 0;
walk_statement(stmt, &mut |e| {
if let Expr::Placeholder(n) = e {
max = max.max(*n);
}
});
max
}
fn walk_statement(stmt: &Statement, f: &mut impl FnMut(&Expr)) {
match stmt {
Statement::Select(s) => walk_select(s, f),
Statement::Insert(s) => {
for row in &s.rows {
for e in row {
walk_expr(e, f);
}
}
}
Statement::Update(s) => {
for (_, e) in &s.assignments {
walk_expr(e, f);
}
if let Some(w) = &s.where_ {
walk_expr(w, f);
}
}
Statement::Delete(s) => {
if let Some(w) = &s.where_ {
walk_expr(w, f);
}
}
Statement::Explain(inner) => {
if let Statement::Select(sel) = &*inner.inner {
walk_select(sel, f);
}
}
_ => {}
}
}
fn walk_select(s: &SelectStatement, f: &mut impl FnMut(&Expr)) {
for item in &s.items {
if let SelectItem::Expr { expr, .. } = item {
walk_expr(expr, f);
}
}
if let Some(w) = &s.where_ {
walk_expr(w, f);
}
if let Some(h) = &s.having {
walk_expr(h, f);
}
if let Some(gb) = &s.group_by {
for e in gb {
walk_expr(e, f);
}
}
for (_, peer) in &s.unions {
walk_select(peer, f);
}
}
fn walk_expr(e: &Expr, f: &mut impl FnMut(&Expr)) {
f(e);
match e {
Expr::NamedArg { expr, .. } => walk_expr(expr, f),
Expr::Variadic(expr) => walk_expr(expr, f),
Expr::AggregateOrdered { call, order_by, .. } => {
walk_expr(call, f);
for o in order_by {
walk_expr(&o.expr, f);
}
}
Expr::Binary { lhs, rhs, .. } => {
walk_expr(lhs, f);
walk_expr(rhs, f);
}
Expr::Unary { expr, .. } => walk_expr(expr, f),
Expr::Cast { expr, .. } | Expr::FieldAccess { base: expr, .. } => walk_expr(expr, f),
Expr::IsNull { expr, .. } | Expr::BoolTest { expr, .. } => walk_expr(expr, f),
Expr::Like { expr, pattern, .. } => {
walk_expr(expr, f);
walk_expr(pattern, f);
}
Expr::FunctionCall { args, .. } => {
for a in args {
walk_expr(a, f);
}
}
Expr::WindowFunction {
args,
partition_by,
order_by,
..
} => {
for a in args {
walk_expr(a, f);
}
for p in partition_by {
walk_expr(p, f);
}
for (o, _, _) in order_by {
walk_expr(o, f);
}
}
Expr::ScalarSubquery(s) => walk_select(s, f),
Expr::Exists { subquery, .. } => walk_select(subquery, f),
Expr::InSubquery { expr, subquery, .. } => {
walk_expr(expr, f);
walk_select(subquery, f);
}
Expr::RowInSubquery { row, subquery, .. } => {
for el in row {
walk_expr(el, f);
}
walk_select(subquery, f);
}
Expr::RowCmpSubquery { row, subquery, .. } => {
for el in row {
walk_expr(el, f);
}
walk_select(subquery, f);
}
Expr::Extract { source, .. } => walk_expr(source, f),
Expr::Array(items) => {
for elem in items {
walk_expr(elem, f);
}
}
Expr::ArraySubscript { target, index } => {
walk_expr(target, f);
walk_expr(index, f);
}
Expr::ArraySlice { target, lo, hi } => {
walk_expr(target, f);
if let Some(l) = lo {
walk_expr(l, f);
}
if let Some(h) = hi {
walk_expr(h, f);
}
}
Expr::AnyAll { expr, array, .. } => {
walk_expr(expr, f);
walk_expr(array, f);
}
Expr::InList { expr, list, .. } => {
walk_expr(expr, f);
for item in list {
walk_expr(item, f);
}
}
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
walk_expr(o, f);
}
for (w, t) in branches {
walk_expr(w, f);
walk_expr(t, f);
}
if let Some(e) = else_branch {
walk_expr(e, f);
}
}
Expr::Literal(_) | Expr::Column(_) | Expr::Placeholder(_) => {}
}
}
pub(crate) fn upgrade_timestamptz_array(
v: Value<'static>,
items: &[Expr],
columns: &[ColumnSchema],
) -> Value<'static> {
let Value::TimestampArray(elems) = v else {
return v;
};
if items.is_empty()
|| !items
.iter()
.all(|e| describe_expr(e, columns).is_some_and(|s| s.ty == DataType::Timestamptz))
{
return Value::TimestampArray(elems);
}
Value::TimestamptzArray(elems)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Engine;
use spg_sql::parser::parse_statement;
fn parse(sql: &str) -> Statement {
parse_statement(sql).expect("parses")
}
#[test]
fn describe_returns_columns_for_wildcard_select() {
let mut eng = Engine::new();
eng.execute("CREATE TABLE t (a INT, b TEXT)").unwrap();
let stmt = eng.prepare("SELECT * FROM t").unwrap();
let (params, cols) = describe_prepared(&stmt, eng_catalog(&eng));
assert_eq!(params, Vec::<u32>::new());
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].name, "a");
assert_eq!(cols[0].ty, DataType::Int);
assert_eq!(cols[1].name, "b");
assert_eq!(cols[1].ty, DataType::Text);
}
#[test]
fn describe_returns_columns_for_projection_select() {
let mut eng = Engine::new();
eng.execute("CREATE TABLE t (a INT, b TEXT)").unwrap();
let stmt = eng.prepare("SELECT b, a FROM t").unwrap();
let (_, cols) = describe_prepared(&stmt, eng_catalog(&eng));
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].name, "b");
assert_eq!(cols[0].ty, DataType::Text);
assert_eq!(cols[1].name, "a");
assert_eq!(cols[1].ty, DataType::Int);
}
#[test]
fn describe_counts_placeholders() {
let stmt = parse("SELECT * FROM t WHERE id = $1 AND name = $2");
let (params, _) = describe_prepared(&stmt, &Catalog::new());
assert_eq!(params, alloc::vec![25u32, 25u32]);
}
#[test]
fn describe_resolves_a_join_namespace() {
let mut eng = Engine::new();
eng.execute("CREATE TABLE a (id INT)").unwrap();
eng.execute("CREATE TABLE b (id INT)").unwrap();
let stmt = eng
.prepare("SELECT * FROM a JOIN b ON a.id = b.id")
.unwrap();
let (_, cols) = describe_prepared(&stmt, eng_catalog(&eng));
let names: Vec<&str> = cols.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, alloc::vec!["id", "id"]);
}
#[test]
fn describe_emits_empty_columns_for_non_select() {
let stmt = parse("INSERT INTO t VALUES (1)");
let (params, cols) = describe_prepared(&stmt, &Catalog::new());
assert_eq!(params, Vec::<u32>::new());
assert!(cols.is_empty());
}
fn eng_catalog(eng: &Engine) -> &Catalog {
eng.catalog()
}
}