use pg_query::NodeEnum;
use pg_query::protobuf::{AConst, ColumnRef, DefElem, Node, RangeVar, TypeName, a_const};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::column_type::ColumnType;
use crate::ir::default_expr::{DefaultExpr, LiteralValue};
use crate::parse::error::{ParseError, SourceLocation};
use crate::parse::normalize_expr;
pub fn resolve_qname(
range_var: &RangeVar,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<QualifiedName, ParseError> {
let name = ident(&range_var.relname, location)?;
if !range_var.schemaname.is_empty() {
let schema = ident(&range_var.schemaname, location)?;
return Ok(QualifiedName::new(schema, name));
}
default_schema.map_or_else(
|| {
Err(ParseError::UnqualifiedName {
location: location.clone(),
})
},
|s| Ok(QualifiedName::new(s.clone(), name)),
)
}
pub fn qname_from_string_list(
nodes: &[pg_query::protobuf::Node],
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<QualifiedName, ParseError> {
let strings: Vec<&str> = nodes
.iter()
.map(|n| match n.node.as_ref() {
Some(NodeEnum::String(s)) => Ok(s.sval.as_str()),
other => Err(ParseError::Structural {
location: location.clone(),
message: format!(
"expected String node in type-name list, got {:?}",
other.map(std::mem::discriminant),
),
}),
})
.collect::<Result<Vec<_>, _>>()?;
match strings.as_slice() {
[name] => {
let name_id = ident(name, location)?;
let schema = default_schema
.cloned()
.ok_or_else(|| ParseError::UnqualifiedName {
location: location.clone(),
})?;
Ok(QualifiedName::new(schema, name_id))
}
[schema, name] => {
let schema_id = ident(schema, location)?;
let name_id = ident(name, location)?;
Ok(QualifiedName::new(schema_id, name_id))
}
_ => Err(ParseError::Structural {
location: location.clone(),
message: format!(
"unexpected type-name list length {} (expected 1 or 2 String nodes)",
nodes.len()
),
}),
}
}
pub fn node_string_value(node: &Node) -> Option<String> {
match node.node.as_ref()? {
NodeEnum::String(s) => Some(s.sval.clone()),
_ => None,
}
}
pub fn def_elem_string(de: &DefElem) -> Option<String> {
let arg = de.arg.as_ref()?;
match arg.node.as_ref()? {
NodeEnum::String(s) => Some(s.sval.clone()),
NodeEnum::Integer(i) => Some(i.ival.to_string()),
NodeEnum::Float(f) => Some(f.fval.clone()),
NodeEnum::List(list) => list
.items
.first()
.and_then(|n| n.node.as_ref())
.and_then(|n| {
if let NodeEnum::String(s) = n {
Some(s.sval.clone())
} else {
None
}
}),
_ => None,
}
}
pub fn ident(s: &str, location: &SourceLocation) -> Result<Identifier, ParseError> {
Identifier::from_unquoted(s).map_err(|e| ParseError::Ir {
location: location.clone(),
source: crate::ir::IrError::InvalidIdentifier(e.to_string()),
})
}
pub fn render_type_name_to_string(type_name: &TypeName) -> Option<String> {
let last = type_name.names.last()?.node.as_ref()?;
let NodeEnum::String(s) = last else {
return None;
};
let bare = s.sval.clone();
let mut out = bare;
if !type_name.typmods.is_empty() {
let mut args: Vec<String> = Vec::with_capacity(type_name.typmods.len());
for n in &type_name.typmods {
args.push(typmod_arg_to_string(n.node.as_ref()?)?);
}
out = format!("{out}({})", args.join(","));
}
for _ in 0..type_name.array_bounds.len() {
out.push_str("[]");
}
Some(out)
}
fn typmod_arg_to_string(node: &NodeEnum) -> Option<String> {
match node {
NodeEnum::AConst(c) => literal_arg_to_string(c),
NodeEnum::ColumnRef(cref) => columnref_ident(cref),
_ => None,
}
}
fn literal_arg_to_string(c: &AConst) -> Option<String> {
match c.val.as_ref()? {
a_const::Val::Ival(i) => Some(i.ival.to_string()),
a_const::Val::Sval(s) => Some(s.sval.clone()),
_ => None,
}
}
fn columnref_ident(cref: &ColumnRef) -> Option<String> {
let [field] = cref.fields.as_slice() else {
return None;
};
match field.node.as_ref()? {
NodeEnum::String(s) => Some(s.sval.clone()),
_ => None,
}
}
pub fn type_name_to_column_type(
type_name: &TypeName,
location: &SourceLocation,
) -> Result<ColumnType, ParseError> {
let name_strings: Vec<&str> = type_name
.names
.iter()
.map(|n| match n.node.as_ref() {
Some(NodeEnum::String(s)) => Ok(s.sval.as_str()),
other => Err(ParseError::Structural {
location: location.clone(),
message: format!(
"expected String node in type-name list, got {:?}",
other.map(std::mem::discriminant),
),
}),
})
.collect::<Result<Vec<_>, _>>()?;
if let [schema, name] = name_strings.as_slice()
&& *schema != "pg_catalog"
{
let schema_id = ident(schema, location)?;
let name_id = ident(name, location)?;
return Ok(ColumnType::UserDefined(QualifiedName::new(
schema_id, name_id,
)));
}
let s = render_type_name_to_string(type_name).ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: "could not stringify type name".into(),
})?;
ColumnType::parse_from_pg_type_string(&s).map_err(|e| ParseError::Structural {
location: location.clone(),
message: format!("invalid column type {s:?}: {e}"),
})
}
pub fn build_default_expr(
node: &NodeEnum,
target_type: Option<&ColumnType>,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<DefaultExpr, ParseError> {
if let Some(lit) = literal_from_node(node) {
return Ok(DefaultExpr::Literal(lit));
}
if let Some(seq) = nextval_target(node, default_schema, location)? {
return Ok(DefaultExpr::Sequence(seq));
}
let normalized = normalize_expr::from_pg_node(node, target_type, location)?;
Ok(DefaultExpr::Expr(normalized))
}
fn literal_from_node(node: &NodeEnum) -> Option<LiteralValue> {
let inner = unwrap_typecast(node);
match inner {
NodeEnum::AConst(c) => aconst_to_literal(c),
_ => None,
}
}
fn unwrap_typecast(node: &NodeEnum) -> &NodeEnum {
if let NodeEnum::TypeCast(cast) = node
&& let Some(arg) = cast.arg.as_ref()
&& let Some(inner) = arg.node.as_ref()
{
return inner;
}
node
}
fn aconst_to_literal(c: &AConst) -> Option<LiteralValue> {
if c.isnull {
return Some(LiteralValue::Null);
}
match c.val.as_ref()? {
a_const::Val::Ival(i) => Some(LiteralValue::Integer(i64::from(i.ival))),
a_const::Val::Fval(f) => f.fval.parse::<f64>().ok().map(LiteralValue::Float),
a_const::Val::Boolval(b) => Some(LiteralValue::Bool(b.boolval)),
a_const::Val::Sval(s) => Some(LiteralValue::Text(s.sval.clone())),
a_const::Val::Bsval(_) => None,
}
}
fn nextval_target(
node: &NodeEnum,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<Option<QualifiedName>, ParseError> {
let inner = unwrap_typecast(node);
let NodeEnum::FuncCall(fc) = inner else {
return Ok(None);
};
let func = match fc.funcname.last().and_then(|n| n.node.as_ref()) {
Some(NodeEnum::String(s)) => s.sval.as_str(),
_ => return Ok(None),
};
if !func.eq_ignore_ascii_case("nextval") {
return Ok(None);
}
let arg = fc.args.first().and_then(|n| n.node.as_ref());
let Some(arg_node) = arg else {
return Ok(None);
};
let inner_arg = unwrap_typecast(arg_node);
let NodeEnum::AConst(c) = inner_arg else {
return Ok(None);
};
let Some(a_const::Val::Sval(s)) = c.val.as_ref() else {
return Ok(None);
};
Ok(Some(parse_qualified_seq_name(
&s.sval,
default_schema,
location,
)?))
}
fn parse_qualified_seq_name(
s: &str,
default_schema: Option<&Identifier>,
location: &SourceLocation,
) -> Result<QualifiedName, ParseError> {
let parts: Vec<&str> = s.split('.').collect();
match parts.len() {
1 => {
let name = ident(parts[0], location)?;
let schema = default_schema
.cloned()
.ok_or_else(|| ParseError::UnqualifiedName {
location: location.clone(),
})?;
Ok(QualifiedName::new(schema, name))
}
2 => {
let schema = ident(parts[0], location)?;
let name = ident(parts[1], location)?;
Ok(QualifiedName::new(schema, name))
}
_ => Err(ParseError::Structural {
location: location.clone(),
message: format!("unsupported qualified sequence reference {s:?}"),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
}
fn parse_first(sql: &str) -> NodeEnum {
let parsed = pg_query::parse(sql).expect("parses");
parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|raw| raw.stmt)
.and_then(|n| n.node)
.expect("at least one statement")
}
fn parse_select_expr(expr: &str) -> NodeEnum {
let n = parse_first(&format!("SELECT {expr};"));
let NodeEnum::SelectStmt(s) = n else { panic!() };
let target = s.target_list.into_iter().next().unwrap().node.unwrap();
let NodeEnum::ResTarget(rt) = target else {
panic!()
};
rt.val.unwrap().node.unwrap()
}
#[test]
fn literal_integer_default() {
let n = parse_select_expr("42");
let d = build_default_expr(&n, Some(&ColumnType::Integer), None, &loc()).unwrap();
assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Integer(42))));
}
#[test]
fn literal_text_default() {
let n = parse_select_expr("'hello'");
let d = build_default_expr(&n, Some(&ColumnType::Text), None, &loc()).unwrap();
assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Text(s)) if s == "hello"));
}
#[test]
fn null_default() {
let n = parse_select_expr("NULL");
let d = build_default_expr(&n, None, None, &loc()).unwrap();
assert!(matches!(d, DefaultExpr::Literal(LiteralValue::Null)));
}
#[test]
fn bool_default() {
let n = parse_select_expr("true");
let d = build_default_expr(&n, Some(&ColumnType::Boolean), None, &loc()).unwrap();
match d {
DefaultExpr::Literal(LiteralValue::Bool(true)) => {}
DefaultExpr::Expr(e) => assert!(e.canonical_text.contains("true")),
other => panic!("unexpected default: {other:?}"),
}
}
#[test]
fn nextval_qualified_default() {
let n = parse_select_expr("nextval('app.seq1'::regclass)");
let d = build_default_expr(&n, None, None, &loc()).unwrap();
match d {
DefaultExpr::Sequence(q) => assert_eq!(q.to_string(), "app.seq1"),
other => panic!("expected Sequence, got {other:?}"),
}
}
#[test]
fn nextval_unqualified_uses_default_schema() {
let n = parse_select_expr("nextval('seq1'::regclass)");
let app = Identifier::from_unquoted("app").unwrap();
let d = build_default_expr(&n, None, Some(&app), &loc()).unwrap();
match d {
DefaultExpr::Sequence(q) => assert_eq!(q.to_string(), "app.seq1"),
other => panic!("expected Sequence, got {other:?}"),
}
}
#[test]
fn func_call_other_than_nextval_is_expr() {
let n = parse_select_expr("now()");
let d = build_default_expr(&n, None, None, &loc()).unwrap();
assert!(matches!(d, DefaultExpr::Expr(_)));
}
#[test]
fn cast_to_target_strips_in_expr_arm() {
let n = parse_select_expr("'a' || 'b'");
let d = build_default_expr(&n, Some(&ColumnType::Text), None, &loc()).unwrap();
assert!(matches!(d, DefaultExpr::Expr(_)));
}
#[test]
fn type_name_renders_with_typmod() {
let stmt = parse_first("CREATE TABLE t (c varchar(50));");
let NodeEnum::CreateStmt(create) = stmt else {
panic!()
};
let elt = create.table_elts.into_iter().next().unwrap();
let NodeEnum::ColumnDef(col) = elt.node.unwrap() else {
panic!()
};
let tn = col.type_name.unwrap();
let s = render_type_name_to_string(&tn).unwrap();
let ct = ColumnType::parse_from_pg_type_string(&s).unwrap();
assert_eq!(ct, ColumnType::Varchar { len: Some(50) });
}
fn first_column_type_name(sql: &str) -> TypeName {
let NodeEnum::CreateStmt(create) = parse_first(sql) else {
panic!("expected CREATE TABLE")
};
let elt = create.table_elts.into_iter().next().unwrap();
let NodeEnum::ColumnDef(col) = elt.node.unwrap() else {
panic!("expected ColumnDef")
};
col.type_name.unwrap()
}
#[test]
fn parameterized_postgis_types_parse() {
let cases = [
("geometry(Point,4326)", "geometry(point,4326)"),
(
"geography(MultiPolygon,4326)",
"geography(multipolygon,4326)",
),
("geometry(geometry,4326)", "geometry(geometry,4326)"),
];
for (decl, expected_raw) in cases {
let tn = first_column_type_name(&format!("CREATE TABLE t (c {decl});"));
let ct = type_name_to_column_type(&tn, &loc())
.unwrap_or_else(|e| panic!("{decl} should parse, got {e:?}"));
assert!(
matches!(&ct, ColumnType::Other { raw } if raw == expected_raw),
"{decl} -> {ct:?}"
);
}
}
#[test]
fn heterogeneous_typmod_args_render() {
let tn = first_column_type_name("CREATE TABLE t (c mytype('foo',1,bar));");
let ct = type_name_to_column_type(&tn, &loc()).expect("mixed typmods should parse");
assert!(
matches!(&ct, ColumnType::Other { raw } if raw == "mytype(foo,1,bar)"),
"got {ct:?}"
);
}
}