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 CreateRange(protobuf::CreateRangeStmt),
40 CreateFunction(protobuf::CreateFunctionStmt),
42 CreateProcedure(protobuf::CreateFunctionStmt),
44 CreateExtension(protobuf::CreateExtensionStmt),
46 CreateTrigger(protobuf::CreateTrigStmt),
48 AlterTableAttachPartition(protobuf::AlterTableStmt),
50 Grant(protobuf::GrantStmt),
52 AlterOwner(protobuf::AlterOwnerStmt),
54 AlterDefaultPrivileges(protobuf::AlterDefaultPrivilegesStmt),
56 CreatePolicy(protobuf::CreatePolicyStmt),
58 CreatePublication(protobuf::CreatePublicationStmt),
60 AlterPublication(protobuf::AlterPublicationStmt),
62 CreateSubscription(protobuf::CreateSubscriptionStmt),
64 AlterSubscription(protobuf::AlterSubscriptionStmt),
66 CreateStatistics(protobuf::CreateStatsStmt),
68 AlterStatistics(protobuf::AlterStatsStmt),
70 CreateCollation(protobuf::DefineStmt),
72 CreateEventTrigger(protobuf::CreateEventTrigStmt),
74 AlterEventTrigger(protobuf::AlterEventTrigStmt),
76}
77
78impl Statement {
79 #[allow(clippy::too_many_lines)] pub fn classify(node: NodeEnum, location: SourceLocation) -> Result<Self, ParseError> {
83 match node {
84 NodeEnum::CreateSchemaStmt(s) => Ok(Self::CreateSchema(s)),
85 NodeEnum::CreateStmt(s) => Ok(Self::CreateTable(s)),
86 NodeEnum::CreateSeqStmt(s) => Ok(Self::CreateSequence(s)),
87 NodeEnum::IndexStmt(s) => Ok(Self::CreateIndex(*s)),
88 NodeEnum::AlterTableStmt(s) => {
89 if is_attach_partition_stmt(&s) {
90 Ok(Self::AlterTableAttachPartition(s))
91 } else {
92 Ok(Self::AlterTable(s))
93 }
94 }
95 NodeEnum::CommentStmt(s) => Ok(Self::Comment(*s)),
96 NodeEnum::ViewStmt(s) => Ok(Self::CreateView(*s)),
97 NodeEnum::CreateEnumStmt(s) => Ok(Self::CreateEnum(s)),
98 NodeEnum::CreateDomainStmt(s) => Ok(Self::CreateDomain(*s)),
99 NodeEnum::CompositeTypeStmt(s) => Ok(Self::CreateCompositeType(s)),
100 NodeEnum::CreateRangeStmt(s) => Ok(Self::CreateRange(s)),
101 NodeEnum::CreateFunctionStmt(s) => {
102 if s.is_procedure {
103 Ok(Self::CreateProcedure(*s))
104 } else {
105 Ok(Self::CreateFunction(*s))
106 }
107 }
108 NodeEnum::CreateExtensionStmt(s) => Ok(Self::CreateExtension(s)),
109 NodeEnum::CreateTrigStmt(s) => Ok(Self::CreateTrigger(*s)),
110 NodeEnum::GrantStmt(s) => Ok(Self::Grant(s)),
111 NodeEnum::AlterOwnerStmt(s) => Ok(Self::AlterOwner(*s)),
112 NodeEnum::AlterDefaultPrivilegesStmt(s) => Ok(Self::AlterDefaultPrivileges(s)),
113 NodeEnum::CreatePolicyStmt(s) => Ok(Self::CreatePolicy(*s)),
114 NodeEnum::CreatePublicationStmt(s) => Ok(Self::CreatePublication(s)),
115 NodeEnum::AlterPublicationStmt(s) => Ok(Self::AlterPublication(s)),
116 NodeEnum::CreateSubscriptionStmt(s) => Ok(Self::CreateSubscription(s)),
117 NodeEnum::AlterSubscriptionStmt(s) => Ok(Self::AlterSubscription(s)),
118 NodeEnum::CreateStatsStmt(s) => Ok(Self::CreateStatistics(s)),
119 NodeEnum::AlterStatsStmt(s) => Ok(Self::AlterStatistics(*s)),
120 NodeEnum::CreateEventTrigStmt(s) => Ok(Self::CreateEventTrigger(s)),
121 NodeEnum::AlterEventTrigStmt(s) => Ok(Self::AlterEventTrigger(s)),
122 NodeEnum::DefineStmt(s) => {
123 let kind = ObjectType::try_from(s.kind).unwrap_or(ObjectType::Undefined);
124 if matches!(kind, ObjectType::ObjectCollation) {
125 Ok(Self::CreateCollation(s))
126 } else {
127 Err(unsupported(location, "CREATE AGGREGATE/OPERATOR/etc."))
128 }
129 }
130 NodeEnum::RenameStmt(s) => {
131 use pg_query::protobuf::ObjectType;
132 let rename_type =
133 ObjectType::try_from(s.rename_type).unwrap_or(ObjectType::Undefined);
134 if matches!(rename_type, ObjectType::ObjectPublication) {
135 return Err(ParseError::Structural {
136 location,
137 message: "ALTER PUBLICATION … RENAME TO is not supported in pgevolve \
138 (pgevolve never models renames)"
139 .into(),
140 });
141 }
142 if matches!(rename_type, ObjectType::ObjectSubscription) {
143 let sub_name = s
146 .relation
147 .as_ref()
148 .map_or_else(|| String::from("<unknown>"), |r| r.relname.clone());
149 let id = crate::identifier::Identifier::from_unquoted(&sub_name)
150 .unwrap_or_else(|_| {
151 crate::identifier::Identifier::from_unquoted("subscription")
152 .expect("static identifier")
153 });
154 return Err(ParseError::SubscriptionRenameNotSupported(id, location));
155 }
156 if matches!(rename_type, ObjectType::ObjectEventTrigger) {
157 let name = s
160 .object
161 .as_ref()
162 .and_then(|o| o.node.as_ref())
163 .and_then(|n| match n {
164 NodeEnum::String(st) => Some(st.sval.clone()),
165 _ => None,
166 })
167 .unwrap_or_else(|| String::from("<unknown>"));
168 let id =
169 crate::identifier::Identifier::from_unquoted(&name).unwrap_or_else(|_| {
170 crate::identifier::Identifier::from_unquoted("event_trigger")
171 .expect("static identifier")
172 });
173 return Err(ParseError::EventTriggerRenameNotSupported(id, location));
174 }
175 if matches!(rename_type, ObjectType::ObjectStatisticExt) {
176 let schema = s
179 .relation
180 .as_ref()
181 .map_or_else(String::new, |r| r.schemaname.clone());
182 let name = s
183 .relation
184 .as_ref()
185 .map_or_else(|| String::from("<unknown>"), |r| r.relname.clone());
186 let schema_id = crate::identifier::Identifier::from_unquoted(&schema)
187 .unwrap_or_else(|_| {
188 crate::identifier::Identifier::from_unquoted("unknown")
189 .expect("static identifier")
190 });
191 let name_id = crate::identifier::Identifier::from_unquoted(&name)
192 .unwrap_or_else(|_| {
193 crate::identifier::Identifier::from_unquoted("statistic")
194 .expect("static identifier")
195 });
196 let qname = crate::identifier::QualifiedName::new(schema_id, name_id);
197 return Err(ParseError::StatisticRenameNotSupported(qname, location));
198 }
199 Err(unsupported(location, "RENAME"))
200 }
201 NodeEnum::AlterPolicyStmt(_) => Err(ParseError::Structural {
202 location,
203 message: "ALTER POLICY in source is not supported — policy modifications \
204 happen via diff; use CREATE POLICY in source"
205 .into(),
206 }),
207 NodeEnum::DropStmt(s) => {
208 let kind = ObjectType::try_from(s.remove_type).unwrap_or(ObjectType::Undefined);
209 if matches!(kind, ObjectType::ObjectPolicy) {
210 return Err(ParseError::Structural {
211 location,
212 message: "DROP POLICY in source is not supported — drops happen \
213 via diff"
214 .into(),
215 });
216 }
217 Err(unsupported(location, "DROP"))
218 }
219 NodeEnum::AlterExtensionStmt(_) => Err(ParseError::Structural {
220 location,
221 message: "ALTER EXTENSION is not supported in source files — \
222 declare the desired state via CREATE EXTENSION"
223 .into(),
224 }),
225 NodeEnum::CreateTableAsStmt(s) => {
226 match ObjectType::try_from(s.objtype) {
229 Ok(ObjectType::ObjectMatview) => Ok(Self::CreateMaterializedView(*s)),
230 _ => Err(unsupported(location, "CREATE TABLE AS SELECT")),
231 }
232 }
233 other => Err(unsupported(location, friendly_kind(&other))),
234 }
235 }
236}
237
238fn is_attach_partition_stmt(stmt: &protobuf::AlterTableStmt) -> bool {
241 use pg_query::protobuf::AlterTableType;
242 if stmt.cmds.len() != 1 {
243 return false;
244 }
245 match &stmt.cmds[0].node {
246 Some(NodeEnum::AlterTableCmd(c)) => AlterTableType::try_from(c.subtype)
247 .ok()
248 .is_some_and(|t| matches!(t, AlterTableType::AtAttachPartition)),
249 _ => false,
250 }
251}
252
253const fn unsupported(location: SourceLocation, kind: &'static str) -> ParseError {
254 ParseError::UnsupportedObjectKind { location, kind }
255}
256
257#[allow(clippy::match_same_arms)]
259const fn friendly_kind(node: &NodeEnum) -> &'static str {
260 match node {
261 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", NodeEnum::CreateExtensionStmt(_) => "CREATE EXTENSION", NodeEnum::CreatePolicyStmt(_) => "CREATE POLICY", NodeEnum::CreateForeignTableStmt(_) => "CREATE FOREIGN TABLE",
269 NodeEnum::CreateFdwStmt(_) => "CREATE FOREIGN DATA WRAPPER",
270 NodeEnum::CreateForeignServerStmt(_) => "CREATE FOREIGN SERVER",
271 NodeEnum::CreateRoleStmt(_) => "CREATE ROLE",
272 NodeEnum::GrantStmt(_) => "GRANT/REVOKE", NodeEnum::GrantRoleStmt(_) => "GRANT/REVOKE ROLE",
274 NodeEnum::CreateTableAsStmt(_) => "CREATE TABLE AS",
275 NodeEnum::CreateAmStmt(_) => "CREATE ACCESS METHOD",
276 NodeEnum::CreateEventTrigStmt(_) => "CREATE EVENT TRIGGER",
277 NodeEnum::CreateOpClassStmt(_) => "CREATE OPERATOR CLASS",
278 NodeEnum::CreateOpFamilyStmt(_) => "CREATE OPERATOR FAMILY",
279 NodeEnum::CreatePlangStmt(_) => "CREATE LANGUAGE",
280 NodeEnum::CreateStatsStmt(_) => "CREATE STATISTICS",
281 NodeEnum::CreateUserMappingStmt(_) => "CREATE USER MAPPING",
282 NodeEnum::CreateTableSpaceStmt(_) => "CREATE TABLESPACE",
283 NodeEnum::CreatedbStmt(_) => "CREATE DATABASE",
284 NodeEnum::DefineStmt(_) => "CREATE AGGREGATE/OPERATOR/etc.",
285 NodeEnum::RuleStmt(_) => "CREATE RULE",
286 NodeEnum::DropStmt(_) => "DROP",
287 NodeEnum::TruncateStmt(_) => "TRUNCATE",
288 NodeEnum::SelectStmt(_) => "SELECT",
289 NodeEnum::InsertStmt(_) => "INSERT",
290 NodeEnum::UpdateStmt(_) => "UPDATE",
291 NodeEnum::DeleteStmt(_) => "DELETE",
292 NodeEnum::CopyStmt(_) => "COPY",
293 NodeEnum::TransactionStmt(_) => "BEGIN/COMMIT/ROLLBACK",
294 _ => "this statement kind",
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use std::path::PathBuf;
302
303 fn loc() -> SourceLocation {
304 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
305 }
306
307 fn first_node(sql: &str) -> NodeEnum {
308 let parsed = pg_query::parse(sql).expect("pg_query parses");
309 parsed
310 .protobuf
311 .stmts
312 .into_iter()
313 .next()
314 .and_then(|raw| raw.stmt)
315 .and_then(|n| n.node)
316 .expect("at least one statement")
317 }
318
319 #[test]
320 fn create_table_classifies() {
321 let node = first_node("CREATE TABLE app.users (id integer);");
322 let stmt = Statement::classify(node, loc()).unwrap();
323 assert!(matches!(stmt, Statement::CreateTable(_)));
324 }
325
326 #[test]
327 fn create_schema_classifies() {
328 let node = first_node("CREATE SCHEMA app;");
329 let stmt = Statement::classify(node, loc()).unwrap();
330 assert!(matches!(stmt, Statement::CreateSchema(_)));
331 }
332
333 #[test]
334 fn create_index_classifies() {
335 let node = first_node("CREATE INDEX users_email_idx ON app.users (email);");
336 let stmt = Statement::classify(node, loc()).unwrap();
337 assert!(matches!(stmt, Statement::CreateIndex(_)));
338 }
339
340 #[test]
341 fn create_sequence_classifies() {
342 let node = first_node("CREATE SEQUENCE app.seq1;");
343 let stmt = Statement::classify(node, loc()).unwrap();
344 assert!(matches!(stmt, Statement::CreateSequence(_)));
345 }
346
347 #[test]
348 fn alter_table_classifies() {
349 let node = first_node(
350 "ALTER TABLE app.invoices ADD CONSTRAINT fk1 FOREIGN KEY (cid) REFERENCES app.c(id);",
351 );
352 let stmt = Statement::classify(node, loc()).unwrap();
353 assert!(matches!(stmt, Statement::AlterTable(_)));
354 }
355
356 #[test]
357 fn comment_classifies() {
358 let node = first_node("COMMENT ON TABLE app.users IS 'people';");
359 let stmt = Statement::classify(node, loc()).unwrap();
360 assert!(matches!(stmt, Statement::Comment(_)));
361 }
362
363 #[test]
364 fn create_view_classifies() {
365 let node = first_node("CREATE VIEW app.v AS SELECT 1;");
366 let stmt = Statement::classify(node, loc()).unwrap();
367 assert!(matches!(stmt, Statement::CreateView(_)));
368 }
369
370 #[test]
371 fn create_materialized_view_classifies() {
372 let node = first_node(
373 "CREATE MATERIALIZED VIEW app.mv AS SELECT count(*) FROM app.t WITH NO DATA;",
374 );
375 let stmt = Statement::classify(node, loc()).unwrap();
376 assert!(matches!(stmt, Statement::CreateMaterializedView(_)));
377 }
378
379 #[test]
380 fn create_table_as_select_unsupported() {
381 let node = first_node("CREATE TABLE app.t2 AS SELECT 1;");
382 let err = Statement::classify(node, loc()).unwrap_err();
383 assert!(matches!(err, ParseError::UnsupportedObjectKind { .. }));
384 }
385
386 #[test]
387 fn create_function_classifies() {
388 let node =
389 first_node("CREATE FUNCTION app.f() RETURNS integer AS $$ SELECT 1; $$ LANGUAGE sql;");
390 let stmt = Statement::classify(node, loc()).unwrap();
391 assert!(matches!(stmt, Statement::CreateFunction(_)));
392 }
393
394 #[test]
395 fn create_procedure_classifies() {
396 let node =
397 first_node("CREATE PROCEDURE app.p() LANGUAGE plpgsql AS $$ BEGIN NULL; END $$;");
398 let stmt = Statement::classify(node, loc()).unwrap();
399 assert!(matches!(stmt, Statement::CreateProcedure(_)));
400 }
401
402 #[test]
403 fn create_extension_classifies() {
404 let node = first_node("CREATE EXTENSION pgcrypto;");
405 let stmt = Statement::classify(node, loc()).unwrap();
406 assert!(matches!(stmt, Statement::CreateExtension(_)));
407 }
408
409 #[test]
410 fn alter_extension_rejected() {
411 let node = first_node("ALTER EXTENSION pgcrypto UPDATE;");
412 let err = Statement::classify(node, loc()).unwrap_err();
413 assert!(matches!(err, ParseError::Structural { .. }));
414 }
415
416 #[test]
417 fn select_unsupported() {
418 let node = first_node("SELECT 1;");
419 let err = Statement::classify(node, loc()).unwrap_err();
420 assert!(matches!(err, ParseError::UnsupportedObjectKind { .. }));
421 }
422}