1use pg_query::NodeEnum;
19use pg_query::protobuf::{
20 AlterTableCmd, AlterTableStmt, AlterTableType, ConstrType, Constraint as PgConstraint,
21 ObjectType, RoleSpecType,
22};
23
24use crate::identifier::{Identifier, QualifiedName};
25use crate::ir::catalog::Catalog;
26use crate::ir::column::{Compression, StorageKind};
27use crate::ir::constraint::Constraint;
28use crate::ir::reloptions::TableStorageOptions;
29use crate::parse::builder::create_stmt;
30use crate::parse::builder::shared;
31use crate::parse::error::{ParseError, SourceLocation};
32
33#[derive(Debug, Clone)]
36pub struct PendingFk {
37 pub target: QualifiedName,
39 pub constraint: Constraint,
41}
42
43#[derive(Debug, Clone)]
46pub struct PendingColumnAttr {
47 pub target: QualifiedName,
49 pub column: Identifier,
51 pub kind: PendingColumnAttrKind,
53}
54
55#[derive(Debug, Clone)]
57pub enum PendingColumnAttrKind {
58 Storage(StorageKind),
60 Compression(Option<Compression>),
63}
64
65#[derive(Debug, Clone)]
67pub struct PendingOwner {
68 pub target: QualifiedName,
70 pub new_owner: Identifier,
72}
73
74#[derive(Debug, Clone)]
77pub struct PendingRlsToggle {
78 pub target: QualifiedName,
80 pub subtype: AlterTableType,
82}
83
84#[derive(Debug, Clone)]
87pub struct PendingRelOptions {
88 pub target: QualifiedName,
90 pub options: TableStorageOptions,
92}
93
94#[derive(Debug, Default)]
96pub struct AlterTableOutput {
97 pub pending_fks: Vec<PendingFk>,
99 pub pending_column_attrs: Vec<PendingColumnAttr>,
101 pub pending_owners: Vec<PendingOwner>,
103 pub pending_rls_toggles: Vec<PendingRlsToggle>,
105 pub pending_rel_options: Vec<PendingRelOptions>,
107}
108
109pub fn build_alter_table(
115 stmt: &AlterTableStmt,
116 default_schema: Option<&Identifier>,
117 location: &SourceLocation,
118) -> Result<AlterTableOutput, ParseError> {
119 let relation = stmt
120 .relation
121 .as_ref()
122 .ok_or_else(|| ParseError::Structural {
123 location: location.clone(),
124 message: "ALTER TABLE missing relation".into(),
125 })?;
126 let target = shared::resolve_qname(relation, default_schema, location)?;
127
128 let mut out = AlterTableOutput::default();
129 for cmd_node in &stmt.cmds {
130 let Some(NodeEnum::AlterTableCmd(cmd)) = cmd_node.node.as_ref() else {
131 return Err(unsupported_alter(location));
132 };
133 process_cmd(cmd, &target, default_schema, location, &mut out)?;
134 }
135 Ok(out)
136}
137
138fn process_cmd(
139 cmd: &AlterTableCmd,
140 target: &QualifiedName,
141 default_schema: Option<&Identifier>,
142 location: &SourceLocation,
143 out: &mut AlterTableOutput,
144) -> Result<(), ParseError> {
145 let subtype = AlterTableType::try_from(cmd.subtype).unwrap_or(AlterTableType::Undefined);
146 match subtype {
147 AlterTableType::AtAddConstraint => {
148 let pending = process_add_constraint_cmd(cmd, target, default_schema, location)?;
149 out.pending_fks.push(pending);
150 }
151 AlterTableType::AtSetStorage => {
152 let pending = process_set_storage_cmd(cmd, target, location)?;
153 out.pending_column_attrs.push(pending);
154 }
155 AlterTableType::AtSetCompression => {
156 let pending = process_set_compression_cmd(cmd, target, location)?;
157 out.pending_column_attrs.push(pending);
158 }
159 AlterTableType::AtChangeOwner => {
160 let pending = process_change_owner_cmd(cmd, target, location)?;
161 out.pending_owners.push(pending);
162 }
163 AlterTableType::AtEnableRowSecurity
164 | AlterTableType::AtDisableRowSecurity
165 | AlterTableType::AtForceRowSecurity
166 | AlterTableType::AtNoForceRowSecurity => {
167 out.pending_rls_toggles.push(PendingRlsToggle {
168 target: target.clone(),
169 subtype,
170 });
171 }
172 AlterTableType::AtSetRelOptions => {
173 let pending = process_set_rel_options_cmd(cmd, target, location)?;
174 out.pending_rel_options.push(pending);
175 }
176 AlterTableType::AtResetRelOptions | AlterTableType::AtReplaceRelOptions => {
177 return Err(ParseError::Structural {
178 location: location.clone(),
179 message: "ALTER TABLE ... RESET (...) in source is not supported — \
180 clear options out-of-band, then remove from source"
181 .into(),
182 });
183 }
184 _ => return Err(unsupported_alter(location)),
185 }
186 Ok(())
187}
188
189fn process_add_constraint_cmd(
190 cmd: &AlterTableCmd,
191 target: &QualifiedName,
192 default_schema: Option<&Identifier>,
193 location: &SourceLocation,
194) -> Result<PendingFk, ParseError> {
195 let con = cmd
196 .def
197 .as_ref()
198 .and_then(|d| d.node.as_ref())
199 .and_then(|n| match n {
200 NodeEnum::Constraint(c) => Some(c.as_ref()),
201 _ => None,
202 })
203 .ok_or_else(|| ParseError::Structural {
204 location: location.clone(),
205 message: "ALTER TABLE ADD CONSTRAINT missing constraint definition".into(),
206 })?;
207
208 let kind = ConstrType::try_from(con.contype).unwrap_or(ConstrType::Undefined);
209 if !matches!(kind, ConstrType::ConstrForeign) {
210 return Err(ParseError::Structural {
211 location: location.clone(),
212 message: "ALTER TABLE may only ADD CONSTRAINT FOREIGN KEY in source DDL — \
213 other constraint kinds belong inline in the CREATE TABLE that \
214 declares them"
215 .into(),
216 });
217 }
218
219 let constraint = build_fk_constraint(con, target, default_schema, location)?;
220 Ok(PendingFk {
221 target: target.clone(),
222 constraint,
223 })
224}
225
226fn process_set_storage_cmd(
227 cmd: &AlterTableCmd,
228 target: &QualifiedName,
229 location: &SourceLocation,
230) -> Result<PendingColumnAttr, ParseError> {
231 let column = shared::ident(&cmd.name, location)?;
233 let keyword = def_as_string(cmd, location)?;
234 let storage = match keyword.to_ascii_lowercase().as_str() {
235 "plain" => StorageKind::Plain,
236 "external" => StorageKind::External,
237 "extended" => StorageKind::Extended,
238 "main" => StorageKind::Main,
239 other => {
240 return Err(ParseError::Structural {
241 location: location.clone(),
242 message: format!("unknown STORAGE attribute '{other}'"),
243 });
244 }
245 };
246 Ok(PendingColumnAttr {
247 target: target.clone(),
248 column,
249 kind: PendingColumnAttrKind::Storage(storage),
250 })
251}
252
253fn process_set_compression_cmd(
254 cmd: &AlterTableCmd,
255 target: &QualifiedName,
256 location: &SourceLocation,
257) -> Result<PendingColumnAttr, ParseError> {
258 let column = shared::ident(&cmd.name, location)?;
260 let keyword = def_as_string(cmd, location)?;
261 let compression = match keyword.to_ascii_lowercase().as_str() {
262 "default" => None,
263 "pglz" => Some(Compression::Pglz),
264 "lz4" => Some(Compression::Lz4),
265 other => {
266 return Err(ParseError::Structural {
267 location: location.clone(),
268 message: format!("unknown COMPRESSION codec '{other}'"),
269 });
270 }
271 };
272 Ok(PendingColumnAttr {
273 target: target.clone(),
274 column,
275 kind: PendingColumnAttrKind::Compression(compression),
276 })
277}
278
279fn process_change_owner_cmd(
284 cmd: &AlterTableCmd,
285 target: &QualifiedName,
286 location: &SourceLocation,
287) -> Result<PendingOwner, ParseError> {
288 let rs = cmd
289 .newowner
290 .as_ref()
291 .ok_or_else(|| ParseError::Structural {
292 location: location.clone(),
293 message: "ALTER TABLE OWNER TO missing role specification".into(),
294 })?;
295 let roletype = RoleSpecType::try_from(rs.roletype).unwrap_or(RoleSpecType::Undefined);
296 if roletype == RoleSpecType::RolespecPublic {
297 return Err(ParseError::Structural {
298 location: location.clone(),
299 message: "ALTER TABLE OWNER TO PUBLIC is not valid — PUBLIC is not a role name".into(),
300 });
301 }
302 let new_owner = shared::ident(&rs.rolename, location)?;
303 Ok(PendingOwner {
304 target: target.clone(),
305 new_owner,
306 })
307}
308
309fn process_set_rel_options_cmd(
310 cmd: &AlterTableCmd,
311 target: &QualifiedName,
312 location: &SourceLocation,
313) -> Result<PendingRelOptions, ParseError> {
314 let items = crate::parse::builder::reloptions::extract_def_list(cmd.def.as_deref(), location)?;
315 let options = crate::parse::builder::reloptions::decode_table_options(&items, location)?;
316 Ok(PendingRelOptions {
317 target: target.clone(),
318 options,
319 })
320}
321
322pub fn apply_pending_rel_options(
327 catalog: &mut Catalog,
328 pending: Vec<PendingRelOptions>,
329 location: &SourceLocation,
330) -> Result<(), ParseError> {
331 for p in pending {
332 if let Some(table) = catalog.tables.iter_mut().find(|t| t.qname == p.target) {
334 merge_table_options(&mut table.storage, p.options);
335 continue;
336 }
337 if let Some(mv) = catalog
339 .materialized_views
340 .iter_mut()
341 .find(|m| m.qname == p.target)
342 {
343 merge_table_options(&mut mv.storage, p.options);
344 continue;
345 }
346 return Err(ParseError::Structural {
347 location: location.clone(),
348 message: format!(
349 "ALTER ... SET (...) referenced unknown relation {}",
350 p.target
351 ),
352 });
353 }
354 Ok(())
355}
356
357fn merge_table_options(
360 dst: &mut crate::ir::reloptions::TableStorageOptions,
361 src: crate::ir::reloptions::TableStorageOptions,
362) {
363 if src.fillfactor.is_some() {
364 dst.fillfactor = src.fillfactor;
365 }
366 if src.parallel_workers.is_some() {
367 dst.parallel_workers = src.parallel_workers;
368 }
369 if src.toast_tuple_target.is_some() {
370 dst.toast_tuple_target = src.toast_tuple_target;
371 }
372 if src.user_catalog_table.is_some() {
373 dst.user_catalog_table = src.user_catalog_table;
374 }
375 if src.vacuum_truncate.is_some() {
376 dst.vacuum_truncate = src.vacuum_truncate;
377 }
378 merge_autovacuum(&mut dst.autovacuum, &src.autovacuum);
379 for (k, v) in src.extra {
380 dst.extra.insert(k, v);
381 }
382}
383
384const fn merge_autovacuum(
385 dst: &mut crate::ir::reloptions::AutovacuumOptions,
386 src: &crate::ir::reloptions::AutovacuumOptions,
387) {
388 if src.enabled.is_some() {
389 dst.enabled = src.enabled;
390 }
391 if src.vacuum_threshold.is_some() {
392 dst.vacuum_threshold = src.vacuum_threshold;
393 }
394 if src.vacuum_scale_factor.is_some() {
395 dst.vacuum_scale_factor = src.vacuum_scale_factor;
396 }
397 if src.vacuum_cost_delay.is_some() {
398 dst.vacuum_cost_delay = src.vacuum_cost_delay;
399 }
400 if src.vacuum_cost_limit.is_some() {
401 dst.vacuum_cost_limit = src.vacuum_cost_limit;
402 }
403 if src.analyze_threshold.is_some() {
404 dst.analyze_threshold = src.analyze_threshold;
405 }
406 if src.analyze_scale_factor.is_some() {
407 dst.analyze_scale_factor = src.analyze_scale_factor;
408 }
409 if src.freeze_max_age.is_some() {
410 dst.freeze_max_age = src.freeze_max_age;
411 }
412 if src.freeze_min_age.is_some() {
413 dst.freeze_min_age = src.freeze_min_age;
414 }
415 if src.freeze_table_age.is_some() {
416 dst.freeze_table_age = src.freeze_table_age;
417 }
418 if src.multixact_freeze_max_age.is_some() {
419 dst.multixact_freeze_max_age = src.multixact_freeze_max_age;
420 }
421 if src.multixact_freeze_min_age.is_some() {
422 dst.multixact_freeze_min_age = src.multixact_freeze_min_age;
423 }
424 if src.multixact_freeze_table_age.is_some() {
425 dst.multixact_freeze_table_age = src.multixact_freeze_table_age;
426 }
427 if src.vacuum_insert_threshold.is_some() {
428 dst.vacuum_insert_threshold = src.vacuum_insert_threshold;
429 }
430 if src.vacuum_insert_scale_factor.is_some() {
431 dst.vacuum_insert_scale_factor = src.vacuum_insert_scale_factor;
432 }
433 if src.log_min_duration.is_some() {
434 dst.log_min_duration = src.log_min_duration;
435 }
436}
437
438pub fn apply_pending_owners(
442 catalog: &mut Catalog,
443 pending: Vec<PendingOwner>,
444 location: &SourceLocation,
445) -> Result<(), ParseError> {
446 for po in pending {
447 super::owner_stmt::set_owner_for_relation(
448 catalog,
449 &po.target,
450 ObjectType::ObjectTable, po.new_owner,
452 location,
453 )?;
454 }
455 Ok(())
456}
457
458pub fn apply_pending_rls_toggles(
462 catalog: &mut Catalog,
463 pending: Vec<PendingRlsToggle>,
464 location: &SourceLocation,
465) -> Result<(), ParseError> {
466 for toggle in pending {
467 let table = catalog
468 .tables
469 .iter_mut()
470 .find(|t| t.qname == toggle.target)
471 .ok_or_else(|| ParseError::Structural {
472 location: location.clone(),
473 message: format!(
474 "ALTER TABLE … ROW LEVEL SECURITY referenced unknown table {}",
475 toggle.target
476 ),
477 })?;
478 match toggle.subtype {
479 AlterTableType::AtEnableRowSecurity => {
480 table.rls_enabled = true;
481 }
482 AlterTableType::AtDisableRowSecurity => {
483 table.rls_enabled = false;
484 }
485 AlterTableType::AtForceRowSecurity => {
486 table.rls_forced = true;
487 }
488 AlterTableType::AtNoForceRowSecurity => {
489 table.rls_forced = false;
490 }
491 _ => {
492 return Err(ParseError::Structural {
493 location: location.clone(),
494 message: "unexpected subtype in PendingRlsToggle".into(),
495 });
496 }
497 }
498 }
499 Ok(())
500}
501
502fn def_as_string(cmd: &AlterTableCmd, location: &SourceLocation) -> Result<String, ParseError> {
504 cmd.def
505 .as_ref()
506 .and_then(|d| d.node.as_ref())
507 .and_then(|n| match n {
508 NodeEnum::String(s) => Some(s.sval.clone()),
509 _ => None,
510 })
511 .ok_or_else(|| ParseError::Structural {
512 location: location.clone(),
513 message: "ALTER COLUMN SET STORAGE/COMPRESSION missing keyword node".into(),
514 })
515}
516
517fn build_fk_constraint(
520 con: &PgConstraint,
521 target_table: &QualifiedName,
522 default_schema: Option<&Identifier>,
523 location: &SourceLocation,
524) -> Result<Constraint, ParseError> {
525 create_stmt::build_fk_for_alter(con, target_table, default_schema, location)
528}
529
530fn unsupported_alter(location: &SourceLocation) -> ParseError {
531 ParseError::Structural {
532 location: location.clone(),
533 message: "ALTER TABLE in source DDL is restricted to ADD CONSTRAINT FOREIGN KEY, \
534 ALTER COLUMN SET STORAGE/COMPRESSION, and SET (reloptions); \
535 pgevolve treats source SQL as declarative — express the desired schema \
536 state via CREATE statements"
537 .into(),
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use crate::ir::constraint::ConstraintKind;
545 use std::path::PathBuf;
546
547 fn loc() -> SourceLocation {
548 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
549 }
550
551 fn build(sql: &str) -> Result<AlterTableOutput, ParseError> {
552 let parsed = pg_query::parse(sql).expect("parses");
553 let stmt = parsed
554 .protobuf
555 .stmts
556 .into_iter()
557 .next()
558 .and_then(|raw| raw.stmt)
559 .and_then(|n| n.node)
560 .expect("stmt");
561 let NodeEnum::AlterTableStmt(s) = stmt else {
562 panic!("not AlterTableStmt")
563 };
564 build_alter_table(&s, None, &loc())
565 }
566
567 #[test]
568 fn allowed_add_fk() {
569 let out = build(
570 "ALTER TABLE app.invoices ADD CONSTRAINT invoices_customer_fk \
571 FOREIGN KEY (customer_id) REFERENCES app.customers (id);",
572 )
573 .expect("builds");
574 assert_eq!(out.pending_fks.len(), 1);
575 let p = &out.pending_fks[0];
576 assert_eq!(p.target.to_string(), "app.invoices");
577 assert!(matches!(p.constraint.kind, ConstraintKind::ForeignKey(_)));
578 }
579
580 #[test]
581 fn alter_column_set_storage_external() {
582 let out =
583 build("ALTER TABLE app.t ALTER COLUMN doc SET STORAGE EXTERNAL;").expect("builds");
584 assert_eq!(out.pending_column_attrs.len(), 1);
585 let attr = &out.pending_column_attrs[0];
586 assert_eq!(attr.target.to_string(), "app.t");
587 assert_eq!(attr.column.as_str(), "doc");
588 assert!(matches!(
589 attr.kind,
590 PendingColumnAttrKind::Storage(StorageKind::External)
591 ));
592 }
593
594 #[test]
595 fn alter_column_set_storage_plain() {
596 let out = build("ALTER TABLE app.t ALTER COLUMN n SET STORAGE PLAIN;").expect("builds");
597 let attr = &out.pending_column_attrs[0];
598 assert!(matches!(
599 attr.kind,
600 PendingColumnAttrKind::Storage(StorageKind::Plain)
601 ));
602 }
603
604 #[test]
605 fn alter_column_set_storage_main() {
606 let out = build("ALTER TABLE app.t ALTER COLUMN n SET STORAGE MAIN;").expect("builds");
607 let attr = &out.pending_column_attrs[0];
608 assert!(matches!(
609 attr.kind,
610 PendingColumnAttrKind::Storage(StorageKind::Main)
611 ));
612 }
613
614 #[test]
615 fn alter_column_set_compression_lz4() {
616 let out = build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION lz4;").expect("builds");
617 assert_eq!(out.pending_column_attrs.len(), 1);
618 let attr = &out.pending_column_attrs[0];
619 assert!(matches!(
620 attr.kind,
621 PendingColumnAttrKind::Compression(Some(Compression::Lz4))
622 ));
623 }
624
625 #[test]
626 fn alter_column_set_compression_pglz() {
627 let out =
628 build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION pglz;").expect("builds");
629 let attr = &out.pending_column_attrs[0];
630 assert!(matches!(
631 attr.kind,
632 PendingColumnAttrKind::Compression(Some(Compression::Pglz))
633 ));
634 }
635
636 #[test]
637 fn alter_column_set_compression_default() {
638 let out =
639 build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION DEFAULT;").expect("builds");
640 let attr = &out.pending_column_attrs[0];
641 assert!(matches!(
642 attr.kind,
643 PendingColumnAttrKind::Compression(None)
644 ));
645 }
646
647 #[test]
648 fn rejects_drop_column() {
649 let err = build("ALTER TABLE app.users DROP COLUMN email;").unwrap_err();
650 match err {
651 ParseError::Structural { message, .. } => {
652 assert!(
653 message.contains("declarative"),
654 "expected declarative message, got: {message}"
655 );
656 }
657 other => panic!("expected Structural, got {other:?}"),
658 }
659 }
660
661 #[test]
662 fn rejects_add_column() {
663 let err = build("ALTER TABLE app.users ADD COLUMN email text;").unwrap_err();
664 match err {
665 ParseError::Structural { message, .. } => {
666 assert!(
667 message.contains("declarative") || message.contains("FOREIGN KEY"),
668 "got: {message}"
669 );
670 }
671 other => panic!("expected Structural, got {other:?}"),
672 }
673 }
674
675 #[test]
683 fn alter_column_set_storage_unknown_errors() {
684 let sql = "ALTER TABLE app.t ALTER COLUMN doc SET STORAGE BOGUS;";
685 match pg_query::parse(sql) {
688 Err(_pg_err) => {
689 }
692 Ok(parsed) => {
693 let stmt = parsed
695 .protobuf
696 .stmts
697 .into_iter()
698 .next()
699 .and_then(|raw| raw.stmt)
700 .and_then(|n| n.node)
701 .expect("stmt");
702 let NodeEnum::AlterTableStmt(s) = stmt else {
703 panic!("expected AlterTableStmt");
704 };
705 let err = build_alter_table(&s, None, &loc())
706 .expect_err("BOGUS storage keyword must be rejected by our decoder");
707 match err {
708 ParseError::Structural { ref message, .. } => {
709 assert!(
710 message.contains("STORAGE"),
711 "expected error to mention STORAGE, got: {message}"
712 );
713 }
714 other => panic!("expected Structural error, got {other:?}"),
715 }
716 }
717 }
718 }
719
720 #[test]
721 fn rejects_add_check_via_alter() {
722 let err = build("ALTER TABLE app.t ADD CONSTRAINT c1 CHECK (n > 0);").unwrap_err();
723 assert!(matches!(err, ParseError::Structural { .. }));
724 }
725
726 #[test]
730 fn alter_table_owner_to_role_name() {
731 let out = build("ALTER TABLE app.t OWNER TO app_owner;").expect("builds");
732 assert_eq!(out.pending_owners.len(), 1);
733 let po = &out.pending_owners[0];
734 assert_eq!(po.target.to_string(), "app.t");
735 assert_eq!(po.new_owner.as_str(), "app_owner");
736 }
737
738 #[test]
741 fn enable_row_security_produces_toggle() {
742 let out = build("ALTER TABLE app.docs ENABLE ROW LEVEL SECURITY;").expect("builds");
743 assert_eq!(out.pending_rls_toggles.len(), 1);
744 let t = &out.pending_rls_toggles[0];
745 assert_eq!(t.target.to_string(), "app.docs");
746 assert!(matches!(t.subtype, AlterTableType::AtEnableRowSecurity));
747 }
748
749 #[test]
750 fn disable_row_security_produces_toggle() {
751 let out = build("ALTER TABLE app.docs DISABLE ROW LEVEL SECURITY;").expect("builds");
752 assert_eq!(out.pending_rls_toggles.len(), 1);
753 let t = &out.pending_rls_toggles[0];
754 assert!(matches!(t.subtype, AlterTableType::AtDisableRowSecurity));
755 }
756
757 #[test]
758 fn force_row_security_produces_toggle() {
759 let out = build("ALTER TABLE app.docs FORCE ROW LEVEL SECURITY;").expect("builds");
760 assert_eq!(out.pending_rls_toggles.len(), 1);
761 let t = &out.pending_rls_toggles[0];
762 assert!(matches!(t.subtype, AlterTableType::AtForceRowSecurity));
763 }
764
765 #[test]
766 fn no_force_row_security_produces_toggle() {
767 let out = build("ALTER TABLE app.docs NO FORCE ROW LEVEL SECURITY;").expect("builds");
768 assert_eq!(out.pending_rls_toggles.len(), 1);
769 let t = &out.pending_rls_toggles[0];
770 assert!(matches!(t.subtype, AlterTableType::AtNoForceRowSecurity));
771 }
772
773 #[test]
776 fn alter_table_set_reloption_fillfactor() {
777 let out = build("ALTER TABLE app.t SET (fillfactor = 80);").expect("builds");
778 assert_eq!(out.pending_rel_options.len(), 1);
779 let p = &out.pending_rel_options[0];
780 assert_eq!(p.target.to_string(), "app.t");
781 assert_eq!(p.options.fillfactor, Some(80));
782 }
783
784 #[test]
785 fn alter_table_set_reloption_autovacuum_enabled() {
786 let out = build("ALTER TABLE app.t SET (autovacuum_enabled = false);").expect("builds");
787 assert_eq!(out.pending_rel_options.len(), 1);
788 let p = &out.pending_rel_options[0];
789 assert_eq!(p.options.autovacuum.enabled, Some(false));
790 }
791
792 #[test]
793 fn alter_table_set_reloption_multiple_options() {
794 let out = build("ALTER TABLE app.t SET (fillfactor = 70, parallel_workers = 2);")
795 .expect("builds");
796 let p = &out.pending_rel_options[0];
797 assert_eq!(p.options.fillfactor, Some(70));
798 assert_eq!(p.options.parallel_workers, Some(2));
799 }
800
801 #[test]
802 fn alter_table_reset_reloption_errors() {
803 let err = build("ALTER TABLE app.t RESET (fillfactor);").unwrap_err();
804 assert!(
805 matches!(err, ParseError::Structural { ref message, .. }
806 if message.contains("RESET") || message.contains("not supported")),
807 "unexpected error: {err:?}"
808 );
809 }
810
811 #[test]
812 fn alter_table_set_fillfactor_out_of_range_errors() {
813 let err = build("ALTER TABLE app.t SET (fillfactor = 5);").unwrap_err();
814 assert!(
815 matches!(err, ParseError::Structural { ref message, .. } if message.contains("out of range")),
816 "unexpected error: {err:?}"
817 );
818 }
819}