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