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