1use pg_query::NodeEnum;
7use pg_query::protobuf;
8use pg_query::protobuf::ObjectType;
9
10use crate::parse::error::{ParseError, SourceLocation};
11
12#[derive(Debug, Clone)]
15pub enum Statement {
16 CreateSchema(protobuf::CreateSchemaStmt),
18 CreateTable(protobuf::CreateStmt),
20 CreateSequence(protobuf::CreateSeqStmt),
22 CreateIndex(protobuf::IndexStmt),
24 AlterTable(protobuf::AlterTableStmt),
26 Comment(protobuf::CommentStmt),
28 CreateView(protobuf::ViewStmt),
30 CreateMaterializedView(protobuf::CreateTableAsStmt),
32 CreateEnum(protobuf::CreateEnumStmt),
34 CreateDomain(protobuf::CreateDomainStmt),
36 CreateCompositeType(protobuf::CompositeTypeStmt),
38 CreateFunction(protobuf::CreateFunctionStmt),
40 CreateProcedure(protobuf::CreateFunctionStmt),
42 CreateExtension(protobuf::CreateExtensionStmt),
44 CreateTrigger(protobuf::CreateTrigStmt),
46 AlterTableAttachPartition(protobuf::AlterTableStmt),
48 Grant(protobuf::GrantStmt),
50 AlterOwner(protobuf::AlterOwnerStmt),
52 AlterDefaultPrivileges(protobuf::AlterDefaultPrivilegesStmt),
54 CreatePolicy(protobuf::CreatePolicyStmt),
56 CreatePublication(protobuf::CreatePublicationStmt),
58 AlterPublication(protobuf::AlterPublicationStmt),
60}
61
62impl Statement {
63 pub fn classify(node: NodeEnum, location: SourceLocation) -> Result<Self, ParseError> {
66 match node {
67 NodeEnum::CreateSchemaStmt(s) => Ok(Self::CreateSchema(s)),
68 NodeEnum::CreateStmt(s) => Ok(Self::CreateTable(s)),
69 NodeEnum::CreateSeqStmt(s) => Ok(Self::CreateSequence(s)),
70 NodeEnum::IndexStmt(s) => Ok(Self::CreateIndex(*s)),
71 NodeEnum::AlterTableStmt(s) => {
72 if is_attach_partition_stmt(&s) {
73 Ok(Self::AlterTableAttachPartition(s))
74 } else {
75 Ok(Self::AlterTable(s))
76 }
77 }
78 NodeEnum::CommentStmt(s) => Ok(Self::Comment(*s)),
79 NodeEnum::ViewStmt(s) => Ok(Self::CreateView(*s)),
80 NodeEnum::CreateEnumStmt(s) => Ok(Self::CreateEnum(s)),
81 NodeEnum::CreateDomainStmt(s) => Ok(Self::CreateDomain(*s)),
82 NodeEnum::CompositeTypeStmt(s) => Ok(Self::CreateCompositeType(s)),
83 NodeEnum::CreateFunctionStmt(s) => {
84 if s.is_procedure {
85 Ok(Self::CreateProcedure(*s))
86 } else {
87 Ok(Self::CreateFunction(*s))
88 }
89 }
90 NodeEnum::CreateExtensionStmt(s) => Ok(Self::CreateExtension(s)),
91 NodeEnum::CreateTrigStmt(s) => Ok(Self::CreateTrigger(*s)),
92 NodeEnum::GrantStmt(s) => Ok(Self::Grant(s)),
93 NodeEnum::AlterOwnerStmt(s) => Ok(Self::AlterOwner(*s)),
94 NodeEnum::AlterDefaultPrivilegesStmt(s) => Ok(Self::AlterDefaultPrivileges(s)),
95 NodeEnum::CreatePolicyStmt(s) => Ok(Self::CreatePolicy(*s)),
96 NodeEnum::CreatePublicationStmt(s) => Ok(Self::CreatePublication(s)),
97 NodeEnum::AlterPublicationStmt(s) => Ok(Self::AlterPublication(s)),
98 NodeEnum::RenameStmt(s) => {
99 use pg_query::protobuf::ObjectType;
100 let rename_type =
101 ObjectType::try_from(s.rename_type).unwrap_or(ObjectType::Undefined);
102 if matches!(rename_type, ObjectType::ObjectPublication) {
103 return Err(ParseError::Structural {
104 location,
105 message: "ALTER PUBLICATION … RENAME TO is not supported in pgevolve \
106 (pgevolve never models renames)"
107 .into(),
108 });
109 }
110 Err(unsupported(location, "RENAME"))
111 }
112 NodeEnum::AlterPolicyStmt(_) => Err(ParseError::Structural {
113 location,
114 message: "ALTER POLICY in source is not supported — policy modifications \
115 happen via diff; use CREATE POLICY in source"
116 .into(),
117 }),
118 NodeEnum::DropStmt(s) => {
119 let kind = ObjectType::try_from(s.remove_type).unwrap_or(ObjectType::Undefined);
120 if matches!(kind, ObjectType::ObjectPolicy) {
121 return Err(ParseError::Structural {
122 location,
123 message: "DROP POLICY in source is not supported — drops happen \
124 via diff"
125 .into(),
126 });
127 }
128 Err(unsupported(location, "DROP"))
129 }
130 NodeEnum::AlterExtensionStmt(_) => Err(ParseError::Structural {
131 location,
132 message: "ALTER EXTENSION is not supported in source files — \
133 declare the desired state via CREATE EXTENSION"
134 .into(),
135 }),
136 NodeEnum::CreateTableAsStmt(s) => {
137 match ObjectType::try_from(s.objtype) {
140 Ok(ObjectType::ObjectMatview) => Ok(Self::CreateMaterializedView(*s)),
141 _ => Err(unsupported(location, "CREATE TABLE AS SELECT")),
142 }
143 }
144 other => Err(unsupported(location, friendly_kind(&other))),
145 }
146 }
147}
148
149fn is_attach_partition_stmt(stmt: &protobuf::AlterTableStmt) -> bool {
152 use pg_query::protobuf::AlterTableType;
153 if stmt.cmds.len() != 1 {
154 return false;
155 }
156 match &stmt.cmds[0].node {
157 Some(NodeEnum::AlterTableCmd(c)) => AlterTableType::try_from(c.subtype)
158 .ok()
159 .is_some_and(|t| matches!(t, AlterTableType::AtAttachPartition)),
160 _ => false,
161 }
162}
163
164const fn unsupported(location: SourceLocation, kind: &'static str) -> ParseError {
165 ParseError::UnsupportedObjectKind { location, kind }
166}
167
168#[allow(clippy::match_same_arms)]
170const fn friendly_kind(node: &NodeEnum) -> &'static str {
171 match node {
172 NodeEnum::ViewStmt(_) => "CREATE VIEW", NodeEnum::CreateEnumStmt(_) => "CREATE TYPE ... AS ENUM", NodeEnum::CreateFunctionStmt(_) => "CREATE FUNCTION/PROCEDURE", NodeEnum::CreateTrigStmt(_) => "CREATE TRIGGER", NodeEnum::CreateRangeStmt(_) => "CREATE TYPE ... AS RANGE",
177 NodeEnum::CreateExtensionStmt(_) => "CREATE EXTENSION", NodeEnum::CreatePolicyStmt(_) => "CREATE POLICY", NodeEnum::CreateForeignTableStmt(_) => "CREATE FOREIGN TABLE",
180 NodeEnum::CreateFdwStmt(_) => "CREATE FOREIGN DATA WRAPPER",
181 NodeEnum::CreateForeignServerStmt(_) => "CREATE FOREIGN SERVER",
182 NodeEnum::CreateRoleStmt(_) => "CREATE ROLE",
183 NodeEnum::GrantStmt(_) => "GRANT/REVOKE", NodeEnum::GrantRoleStmt(_) => "GRANT/REVOKE ROLE",
185 NodeEnum::CreateTableAsStmt(_) => "CREATE TABLE AS",
186 NodeEnum::CreateAmStmt(_) => "CREATE ACCESS METHOD",
187 NodeEnum::CreateEventTrigStmt(_) => "CREATE EVENT TRIGGER",
188 NodeEnum::CreateOpClassStmt(_) => "CREATE OPERATOR CLASS",
189 NodeEnum::CreateOpFamilyStmt(_) => "CREATE OPERATOR FAMILY",
190 NodeEnum::CreatePlangStmt(_) => "CREATE LANGUAGE",
191 NodeEnum::CreateStatsStmt(_) => "CREATE STATISTICS",
192 NodeEnum::CreateUserMappingStmt(_) => "CREATE USER MAPPING",
193 NodeEnum::CreateTableSpaceStmt(_) => "CREATE TABLESPACE",
194 NodeEnum::CreatedbStmt(_) => "CREATE DATABASE",
195 NodeEnum::DefineStmt(_) => "CREATE AGGREGATE/OPERATOR/etc.",
196 NodeEnum::RuleStmt(_) => "CREATE RULE",
197 NodeEnum::DropStmt(_) => "DROP",
198 NodeEnum::TruncateStmt(_) => "TRUNCATE",
199 NodeEnum::SelectStmt(_) => "SELECT",
200 NodeEnum::InsertStmt(_) => "INSERT",
201 NodeEnum::UpdateStmt(_) => "UPDATE",
202 NodeEnum::DeleteStmt(_) => "DELETE",
203 NodeEnum::CopyStmt(_) => "COPY",
204 NodeEnum::TransactionStmt(_) => "BEGIN/COMMIT/ROLLBACK",
205 _ => "this statement kind",
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use std::path::PathBuf;
213
214 fn loc() -> SourceLocation {
215 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
216 }
217
218 fn first_node(sql: &str) -> NodeEnum {
219 let parsed = pg_query::parse(sql).expect("pg_query parses");
220 parsed
221 .protobuf
222 .stmts
223 .into_iter()
224 .next()
225 .and_then(|raw| raw.stmt)
226 .and_then(|n| n.node)
227 .expect("at least one statement")
228 }
229
230 #[test]
231 fn create_table_classifies() {
232 let node = first_node("CREATE TABLE app.users (id integer);");
233 let stmt = Statement::classify(node, loc()).unwrap();
234 assert!(matches!(stmt, Statement::CreateTable(_)));
235 }
236
237 #[test]
238 fn create_schema_classifies() {
239 let node = first_node("CREATE SCHEMA app;");
240 let stmt = Statement::classify(node, loc()).unwrap();
241 assert!(matches!(stmt, Statement::CreateSchema(_)));
242 }
243
244 #[test]
245 fn create_index_classifies() {
246 let node = first_node("CREATE INDEX users_email_idx ON app.users (email);");
247 let stmt = Statement::classify(node, loc()).unwrap();
248 assert!(matches!(stmt, Statement::CreateIndex(_)));
249 }
250
251 #[test]
252 fn create_sequence_classifies() {
253 let node = first_node("CREATE SEQUENCE app.seq1;");
254 let stmt = Statement::classify(node, loc()).unwrap();
255 assert!(matches!(stmt, Statement::CreateSequence(_)));
256 }
257
258 #[test]
259 fn alter_table_classifies() {
260 let node = first_node(
261 "ALTER TABLE app.invoices ADD CONSTRAINT fk1 FOREIGN KEY (cid) REFERENCES app.c(id);",
262 );
263 let stmt = Statement::classify(node, loc()).unwrap();
264 assert!(matches!(stmt, Statement::AlterTable(_)));
265 }
266
267 #[test]
268 fn comment_classifies() {
269 let node = first_node("COMMENT ON TABLE app.users IS 'people';");
270 let stmt = Statement::classify(node, loc()).unwrap();
271 assert!(matches!(stmt, Statement::Comment(_)));
272 }
273
274 #[test]
275 fn create_view_classifies() {
276 let node = first_node("CREATE VIEW app.v AS SELECT 1;");
277 let stmt = Statement::classify(node, loc()).unwrap();
278 assert!(matches!(stmt, Statement::CreateView(_)));
279 }
280
281 #[test]
282 fn create_materialized_view_classifies() {
283 let node = first_node(
284 "CREATE MATERIALIZED VIEW app.mv AS SELECT count(*) FROM app.t WITH NO DATA;",
285 );
286 let stmt = Statement::classify(node, loc()).unwrap();
287 assert!(matches!(stmt, Statement::CreateMaterializedView(_)));
288 }
289
290 #[test]
291 fn create_table_as_select_unsupported() {
292 let node = first_node("CREATE TABLE app.t2 AS SELECT 1;");
293 let err = Statement::classify(node, loc()).unwrap_err();
294 assert!(matches!(err, ParseError::UnsupportedObjectKind { .. }));
295 }
296
297 #[test]
298 fn create_function_classifies() {
299 let node =
300 first_node("CREATE FUNCTION app.f() RETURNS integer AS $$ SELECT 1; $$ LANGUAGE sql;");
301 let stmt = Statement::classify(node, loc()).unwrap();
302 assert!(matches!(stmt, Statement::CreateFunction(_)));
303 }
304
305 #[test]
306 fn create_procedure_classifies() {
307 let node =
308 first_node("CREATE PROCEDURE app.p() LANGUAGE plpgsql AS $$ BEGIN NULL; END $$;");
309 let stmt = Statement::classify(node, loc()).unwrap();
310 assert!(matches!(stmt, Statement::CreateProcedure(_)));
311 }
312
313 #[test]
314 fn create_extension_classifies() {
315 let node = first_node("CREATE EXTENSION pgcrypto;");
316 let stmt = Statement::classify(node, loc()).unwrap();
317 assert!(matches!(stmt, Statement::CreateExtension(_)));
318 }
319
320 #[test]
321 fn alter_extension_rejected() {
322 let node = first_node("ALTER EXTENSION pgcrypto UPDATE;");
323 let err = Statement::classify(node, loc()).unwrap_err();
324 assert!(matches!(err, ParseError::Structural { .. }));
325 }
326
327 #[test]
328 fn select_unsupported() {
329 let node = first_node("SELECT 1;");
330 let err = Statement::classify(node, loc()).unwrap_err();
331 assert!(matches!(err, ParseError::UnsupportedObjectKind { .. }));
332 }
333}