use pg_query::NodeEnum;
use pg_query::protobuf::CreateExtensionStmt;
use crate::ir::extension::Extension;
use crate::parse::builder::shared;
use crate::parse::error::{ParseError, SourceLocation};
pub(crate) fn build_extension(
stmt: &CreateExtensionStmt,
location: &SourceLocation,
) -> Result<Extension, ParseError> {
let name = shared::ident(&stmt.extname, location)?;
let mut schema: Option<crate::identifier::Identifier> = None;
let mut version: Option<String> = None;
for opt_node in &stmt.options {
let Some(NodeEnum::DefElem(de)) = opt_node.node.as_ref() else {
return Err(ParseError::Structural {
location: location.clone(),
message: format!("CREATE EXTENSION {name}: unexpected option node"),
});
};
match de.defname.as_str() {
"schema" => {
let s = string_from_def_elem(de).ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: format!("CREATE EXTENSION {name}: SCHEMA option missing value"),
})?;
schema = Some(shared::ident(&s, location)?);
}
"new_version" => {
let v = string_from_def_elem(de).ok_or_else(|| ParseError::Structural {
location: location.clone(),
message: format!("CREATE EXTENSION {name}: VERSION option missing value"),
})?;
version = Some(v);
}
"cascade" => {
return Err(ParseError::Structural {
location: location.clone(),
message: format!(
"CREATE EXTENSION {name}: CASCADE is not supported — \
declare every required extension explicitly so that \
pgevolve can manage it"
),
});
}
"old_version" => {
return Err(ParseError::Structural {
location: location.clone(),
message: format!(
"CREATE EXTENSION {name}: FROM old_version is not supported in \
source files — declare the desired state with VERSION"
),
});
}
other => {
return Err(ParseError::Structural {
location: location.clone(),
message: format!("CREATE EXTENSION {name}: unknown option {other:?}"),
});
}
}
}
Ok(Extension {
name,
schema,
version,
comment: None,
})
}
fn string_from_def_elem(de: &pg_query::protobuf::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()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn loc() -> SourceLocation {
SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
}
fn parse_extension(sql: &str) -> CreateExtensionStmt {
let parsed = pg_query::parse(sql).expect("parses");
let node = parsed
.protobuf
.stmts
.into_iter()
.next()
.and_then(|r| r.stmt)
.and_then(|n| n.node)
.expect("stmt");
let NodeEnum::CreateExtensionStmt(s) = node else {
panic!("expected CreateExtensionStmt, got: {node:?}");
};
s
}
fn build(sql: &str) -> Extension {
let stmt = parse_extension(sql);
build_extension(&stmt, &loc()).expect("build")
}
#[test]
fn parses_bare() {
let ext = build("CREATE EXTENSION pgcrypto;");
assert_eq!(ext.name.as_str(), "pgcrypto");
assert!(ext.schema.is_none());
assert!(ext.version.is_none());
assert!(ext.comment.is_none());
}
#[test]
fn parses_with_schema() {
let ext = build("CREATE EXTENSION pgcrypto WITH SCHEMA app;");
assert_eq!(ext.name.as_str(), "pgcrypto");
assert_eq!(
ext.schema
.as_ref()
.map(crate::identifier::Identifier::as_str),
Some("app")
);
assert!(ext.version.is_none());
}
#[test]
fn parses_with_version() {
let ext = build("CREATE EXTENSION pgcrypto VERSION '1.3';");
assert_eq!(ext.name.as_str(), "pgcrypto");
assert!(ext.schema.is_none());
assert_eq!(ext.version.as_deref(), Some("1.3"));
}
#[test]
fn parses_if_not_exists_with_schema_and_version() {
let ext = build("CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public VERSION '1.6';");
assert_eq!(ext.name.as_str(), "pg_trgm");
assert_eq!(
ext.schema
.as_ref()
.map(crate::identifier::Identifier::as_str),
Some("public")
);
assert_eq!(ext.version.as_deref(), Some("1.6"));
assert!(ext.comment.is_none());
}
#[test]
fn rejects_cascade() {
let stmt = parse_extension("CREATE EXTENSION pgcrypto CASCADE;");
let err = build_extension(&stmt, &loc()).unwrap_err();
let msg = match &err {
ParseError::Structural { message, .. } => message.clone(),
other => panic!("expected Structural, got {other:?}"),
};
assert!(
msg.to_lowercase().contains("cascade"),
"should mention CASCADE: {msg}"
);
}
#[test]
fn rejects_from_clause() {
use pg_query::protobuf::{CreateExtensionStmt, DefElem, Node};
let old_version_opt = Node {
node: Some(NodeEnum::DefElem(Box::new(DefElem {
defnamespace: String::new(),
defname: "old_version".into(),
arg: Some(Box::new(Node {
node: Some(NodeEnum::String(pg_query::protobuf::String {
sval: "1.0".into(),
})),
})),
defaction: 0,
location: 0,
}))),
};
let stmt = CreateExtensionStmt {
extname: "pgcrypto".into(),
if_not_exists: false,
options: vec![old_version_opt],
};
let err = build_extension(&stmt, &loc()).unwrap_err();
let msg = match &err {
ParseError::Structural { message, .. } => message.clone(),
other => panic!("expected Structural, got {other:?}"),
};
assert!(
msg.contains("old_version") || msg.to_lowercase().contains("from"),
"should mention old_version/FROM: {msg}"
);
}
}