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