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