pgevolve_core/parse/builder/
create_enum_stmt.rs1use std::collections::BTreeSet;
8
9use pg_query::NodeEnum;
10use pg_query::protobuf::CreateEnumStmt;
11
12use crate::identifier::Identifier;
13use crate::ir::user_type::{EnumValue, UserType, UserTypeKind};
14use crate::parse::builder::shared;
15use crate::parse::error::{ParseError, SourceLocation};
16
17pub(crate) fn build_enum(
25 stmt: &CreateEnumStmt,
26 default_schema: Option<&Identifier>,
27 location: &SourceLocation,
28) -> Result<UserType, ParseError> {
29 let qname = shared::qname_from_string_list(&stmt.type_name, default_schema, location)?;
30
31 if stmt.vals.is_empty() {
32 return Err(ParseError::Structural {
33 location: location.clone(),
34 message: format!("CREATE TYPE {qname} AS ENUM requires at least one value"),
35 });
36 }
37
38 let mut values: Vec<EnumValue> = Vec::with_capacity(stmt.vals.len());
39 let mut seen: BTreeSet<String> = BTreeSet::new();
40
41 for (idx, node) in stmt.vals.iter().enumerate() {
42 let label = string_val_from_node(node, location)?;
43 if !seen.insert(label.clone()) {
44 return Err(ParseError::Structural {
45 location: location.clone(),
46 message: format!("CREATE TYPE {qname} AS ENUM: duplicate label {label:?}"),
47 });
48 }
49 #[allow(clippy::cast_precision_loss)]
50 let sort_order = (idx as f32) + 1.0;
51 values.push(EnumValue {
52 name: label,
53 sort_order,
54 });
55 }
56
57 Ok(UserType {
58 qname,
59 kind: UserTypeKind::Enum { values },
60 comment: None,
61 owner: None,
62 grants: vec![],
63 })
64}
65
66fn string_val_from_node(
68 node: &pg_query::protobuf::Node,
69 location: &SourceLocation,
70) -> Result<String, ParseError> {
71 match node.node.as_ref() {
72 Some(NodeEnum::String(s)) if !s.sval.is_empty() => Ok(s.sval.clone()),
73 Some(NodeEnum::AConst(c)) => {
74 use pg_query::protobuf::a_const;
76 match c.val.as_ref() {
77 Some(a_const::Val::Sval(s)) if !s.sval.is_empty() => Ok(s.sval.clone()),
78 _ => Err(ParseError::Structural {
79 location: location.clone(),
80 message: "expected non-empty string AConst for enum label".into(),
81 }),
82 }
83 }
84 _ => Err(ParseError::Structural {
85 location: location.clone(),
86 message: "expected string node for enum label".into(),
87 }),
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use std::path::PathBuf;
95
96 fn loc() -> SourceLocation {
97 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
98 }
99
100 fn parse_enum(sql: &str) -> CreateEnumStmt {
101 let parsed = pg_query::parse(sql).expect("parses");
102 let node = parsed
103 .protobuf
104 .stmts
105 .into_iter()
106 .next()
107 .and_then(|r| r.stmt)
108 .and_then(|n| n.node)
109 .expect("stmt");
110 let NodeEnum::CreateEnumStmt(s) = node else {
111 panic!("expected CreateEnumStmt, got: {node:?}");
112 };
113 s
114 }
115
116 fn build(sql: &str) -> UserType {
117 let stmt = parse_enum(sql);
118 build_enum(&stmt, None, &loc()).expect("build_enum")
119 }
120
121 #[test]
122 fn qualified_enum_three_values() {
123 let ut = build("CREATE TYPE app.order_status AS ENUM ('pending', 'shipped', 'delivered');");
124 assert_eq!(ut.qname.to_string(), "app.order_status");
125 let crate::ir::user_type::UserTypeKind::Enum { values } = ut.kind else {
126 panic!("not enum kind");
127 };
128 assert_eq!(values.len(), 3);
129 assert_eq!(values[0].name, "pending");
130 assert_eq!(values[0].sort_order.to_bits(), 1.0_f32.to_bits());
131 assert_eq!(values[1].name, "shipped");
132 assert_eq!(values[1].sort_order.to_bits(), 2.0_f32.to_bits());
133 assert_eq!(values[2].name, "delivered");
134 assert_eq!(values[2].sort_order.to_bits(), 3.0_f32.to_bits());
135 assert!(ut.comment.is_none());
136 }
137
138 #[test]
139 fn unqualified_enum_uses_default_schema() {
140 let stmt = parse_enum("CREATE TYPE order_status AS ENUM ('a');");
141 let app = Identifier::from_unquoted("app").unwrap();
142 let ut = build_enum(&stmt, Some(&app), &loc()).unwrap();
143 assert_eq!(ut.qname.to_string(), "app.order_status");
144 }
145
146 #[test]
147 fn unqualified_without_schema_errors() {
148 let stmt = parse_enum("CREATE TYPE order_status AS ENUM ('a');");
149 let err = build_enum(&stmt, None, &loc()).unwrap_err();
150 assert!(matches!(err, ParseError::UnqualifiedName { .. }));
151 }
152
153 #[test]
154 fn single_value_enum() {
155 let ut = build("CREATE TYPE app.singleton AS ENUM ('only');");
156 let crate::ir::user_type::UserTypeKind::Enum { values } = ut.kind else {
157 panic!("not enum kind");
158 };
159 assert_eq!(values.len(), 1);
160 assert_eq!(values[0].name, "only");
161 assert_eq!(values[0].sort_order.to_bits(), 1.0_f32.to_bits());
162 }
163
164 #[test]
165 fn duplicate_label_rejected() {
166 let stmt = parse_enum("CREATE TYPE app.bad AS ENUM ('x', 'y', 'x');");
167 let err = build_enum(&stmt, None, &loc()).unwrap_err();
168 let msg = match &err {
169 ParseError::Structural { message, .. } => message.clone(),
170 other => panic!("expected Structural, got {other:?}"),
171 };
172 assert!(msg.contains("duplicate"), "expected duplicate in: {msg}");
173 assert!(msg.contains("\"x\""), "expected label in: {msg}");
174 }
175
176 #[test]
177 fn empty_enum_rejected() {
178 let synthetic = CreateEnumStmt {
182 type_name: {
183 use pg_query::protobuf::{Node, String as PgString};
184 vec![
185 Node {
186 node: Some(NodeEnum::String(PgString { sval: "app".into() })),
187 },
188 Node {
189 node: Some(NodeEnum::String(PgString {
190 sval: "myenum".into(),
191 })),
192 },
193 ]
194 },
195 vals: vec![],
196 };
197 let err = build_enum(&synthetic, None, &loc()).unwrap_err();
198 let msg = match &err {
199 ParseError::Structural { message, .. } => message.clone(),
200 other => panic!("expected Structural, got {other:?}"),
201 };
202 assert!(msg.contains("at least one value"), "unexpected: {msg}");
203 }
204}