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