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 for (k, v) in src.extra {
417 dst.extra.insert(k, v);
418 }
419}
420
421pub fn apply_pending_owners(
425 catalog: &mut Catalog,
426 pending: Vec<PendingOwner>,
427 location: &SourceLocation,
428) -> Result<(), ParseError> {
429 for po in pending {
430 super::owner_stmt::set_owner_for_relation(
431 catalog,
432 &po.target,
433 ObjectType::ObjectTable, po.new_owner,
435 location,
436 )?;
437 }
438 Ok(())
439}
440
441pub fn apply_pending_rls_toggles(
445 catalog: &mut Catalog,
446 pending: Vec<PendingRlsToggle>,
447 location: &SourceLocation,
448) -> Result<(), ParseError> {
449 for toggle in pending {
450 let table = catalog
451 .tables
452 .iter_mut()
453 .find(|t| t.qname == toggle.target)
454 .ok_or_else(|| ParseError::Structural {
455 location: location.clone(),
456 message: format!(
457 "ALTER TABLE … ROW LEVEL SECURITY referenced unknown table {}",
458 toggle.target
459 ),
460 })?;
461 match toggle.subtype {
462 AlterTableType::AtEnableRowSecurity => {
463 table.rls_enabled = true;
464 }
465 AlterTableType::AtDisableRowSecurity => {
466 table.rls_enabled = false;
467 }
468 AlterTableType::AtForceRowSecurity => {
469 table.rls_forced = true;
470 }
471 AlterTableType::AtNoForceRowSecurity => {
472 table.rls_forced = false;
473 }
474 _ => {
475 return Err(ParseError::Structural {
476 location: location.clone(),
477 message: "unexpected subtype in PendingRlsToggle".into(),
478 });
479 }
480 }
481 }
482 Ok(())
483}
484
485pub fn apply_pending_tablespaces(
489 catalog: &mut crate::ir::catalog::Catalog,
490 pending: Vec<PendingTablespace>,
491 location: &SourceLocation,
492) -> Result<(), ParseError> {
493 for p in pending {
494 let table = catalog
495 .tables
496 .iter_mut()
497 .find(|t| t.qname == p.target)
498 .ok_or_else(|| ParseError::Structural {
499 location: location.clone(),
500 message: format!(
501 "ALTER TABLE … SET TABLESPACE referenced unknown table {}",
502 p.target
503 ),
504 })?;
505 table.tablespace = Some(p.tablespace);
506 }
507 Ok(())
508}
509
510fn def_as_string(cmd: &AlterTableCmd, location: &SourceLocation) -> Result<String, ParseError> {
512 cmd.def
513 .as_ref()
514 .and_then(|d| d.node.as_ref())
515 .and_then(|n| match n {
516 NodeEnum::String(s) => Some(s.sval.clone()),
517 _ => None,
518 })
519 .ok_or_else(|| ParseError::Structural {
520 location: location.clone(),
521 message: "ALTER COLUMN SET STORAGE/COMPRESSION missing keyword node".into(),
522 })
523}
524
525fn build_fk_constraint(
528 con: &PgConstraint,
529 target_table: &QualifiedName,
530 default_schema: Option<&Identifier>,
531 location: &SourceLocation,
532) -> Result<Constraint, ParseError> {
533 create_stmt::build_fk_for_alter(con, target_table, default_schema, location)
536}
537
538fn unsupported_alter(location: &SourceLocation) -> ParseError {
539 ParseError::Structural {
540 location: location.clone(),
541 message: "ALTER TABLE in source DDL is restricted to ADD CONSTRAINT FOREIGN KEY, \
542 ALTER COLUMN SET STORAGE/COMPRESSION, SET (reloptions), and SET TABLESPACE; \
543 pgevolve treats source SQL as declarative — express the desired schema \
544 state via CREATE statements"
545 .into(),
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::ir::constraint::ConstraintKind;
553 use std::path::PathBuf;
554
555 fn loc() -> SourceLocation {
556 SourceLocation::new(PathBuf::from("test.sql"), 1, 1)
557 }
558
559 fn build(sql: &str) -> Result<AlterTableOutput, ParseError> {
560 let parsed = pg_query::parse(sql).expect("parses");
561 let stmt = parsed
562 .protobuf
563 .stmts
564 .into_iter()
565 .next()
566 .and_then(|raw| raw.stmt)
567 .and_then(|n| n.node)
568 .expect("stmt");
569 let NodeEnum::AlterTableStmt(s) = stmt else {
570 panic!("not AlterTableStmt")
571 };
572 build_alter_table(&s, None, &loc())
573 }
574
575 #[test]
576 fn allowed_add_fk() {
577 let out = build(
578 "ALTER TABLE app.invoices ADD CONSTRAINT invoices_customer_fk \
579 FOREIGN KEY (customer_id) REFERENCES app.customers (id);",
580 )
581 .expect("builds");
582 assert_eq!(out.pending_fks.len(), 1);
583 let p = &out.pending_fks[0];
584 assert_eq!(p.target.to_string(), "app.invoices");
585 assert!(matches!(p.constraint.kind, ConstraintKind::ForeignKey(_)));
586 }
587
588 #[test]
589 fn alter_column_set_storage_external() {
590 let out =
591 build("ALTER TABLE app.t ALTER COLUMN doc SET STORAGE EXTERNAL;").expect("builds");
592 assert_eq!(out.pending_column_attrs.len(), 1);
593 let attr = &out.pending_column_attrs[0];
594 assert_eq!(attr.target.to_string(), "app.t");
595 assert_eq!(attr.column.as_str(), "doc");
596 assert!(matches!(
597 attr.kind,
598 PendingColumnAttrKind::Storage(StorageKind::External)
599 ));
600 }
601
602 #[test]
603 fn alter_column_set_storage_plain() {
604 let out = build("ALTER TABLE app.t ALTER COLUMN n SET STORAGE PLAIN;").expect("builds");
605 let attr = &out.pending_column_attrs[0];
606 assert!(matches!(
607 attr.kind,
608 PendingColumnAttrKind::Storage(StorageKind::Plain)
609 ));
610 }
611
612 #[test]
613 fn alter_column_set_storage_main() {
614 let out = build("ALTER TABLE app.t ALTER COLUMN n SET STORAGE MAIN;").expect("builds");
615 let attr = &out.pending_column_attrs[0];
616 assert!(matches!(
617 attr.kind,
618 PendingColumnAttrKind::Storage(StorageKind::Main)
619 ));
620 }
621
622 #[test]
623 fn alter_column_set_compression_lz4() {
624 let out = build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION lz4;").expect("builds");
625 assert_eq!(out.pending_column_attrs.len(), 1);
626 let attr = &out.pending_column_attrs[0];
627 assert!(matches!(
628 attr.kind,
629 PendingColumnAttrKind::Compression(Some(Compression::Lz4))
630 ));
631 }
632
633 #[test]
634 fn alter_column_set_compression_pglz() {
635 let out =
636 build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION pglz;").expect("builds");
637 let attr = &out.pending_column_attrs[0];
638 assert!(matches!(
639 attr.kind,
640 PendingColumnAttrKind::Compression(Some(Compression::Pglz))
641 ));
642 }
643
644 #[test]
645 fn alter_column_set_compression_default() {
646 let out =
647 build("ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION DEFAULT;").expect("builds");
648 let attr = &out.pending_column_attrs[0];
649 assert!(matches!(
650 attr.kind,
651 PendingColumnAttrKind::Compression(None)
652 ));
653 }
654
655 #[test]
656 fn rejects_drop_column() {
657 let err = build("ALTER TABLE app.users DROP COLUMN email;").unwrap_err();
658 match err {
659 ParseError::Structural { message, .. } => {
660 assert!(
661 message.contains("declarative"),
662 "expected declarative message, got: {message}"
663 );
664 }
665 other => panic!("expected Structural, got {other:?}"),
666 }
667 }
668
669 #[test]
670 fn rejects_add_column() {
671 let err = build("ALTER TABLE app.users ADD COLUMN email text;").unwrap_err();
672 match err {
673 ParseError::Structural { message, .. } => {
674 assert!(
675 message.contains("declarative") || message.contains("FOREIGN KEY"),
676 "got: {message}"
677 );
678 }
679 other => panic!("expected Structural, got {other:?}"),
680 }
681 }
682
683 #[test]
691 fn alter_column_set_storage_unknown_errors() {
692 let sql = "ALTER TABLE app.t ALTER COLUMN doc SET STORAGE BOGUS;";
693 match pg_query::parse(sql) {
696 Err(_pg_err) => {
697 }
700 Ok(parsed) => {
701 let stmt = parsed
703 .protobuf
704 .stmts
705 .into_iter()
706 .next()
707 .and_then(|raw| raw.stmt)
708 .and_then(|n| n.node)
709 .expect("stmt");
710 let NodeEnum::AlterTableStmt(s) = stmt else {
711 panic!("expected AlterTableStmt");
712 };
713 let err = build_alter_table(&s, None, &loc())
714 .expect_err("BOGUS storage keyword must be rejected by our decoder");
715 match err {
716 ParseError::Structural { ref message, .. } => {
717 assert!(
718 message.contains("STORAGE"),
719 "expected error to mention STORAGE, got: {message}"
720 );
721 }
722 other => panic!("expected Structural error, got {other:?}"),
723 }
724 }
725 }
726 }
727
728 #[test]
729 fn rejects_add_check_via_alter() {
730 let err = build("ALTER TABLE app.t ADD CONSTRAINT c1 CHECK (n > 0);").unwrap_err();
731 assert!(matches!(err, ParseError::Structural { .. }));
732 }
733
734 #[test]
738 fn alter_table_owner_to_role_name() {
739 let out = build("ALTER TABLE app.t OWNER TO app_owner;").expect("builds");
740 assert_eq!(out.pending_owners.len(), 1);
741 let po = &out.pending_owners[0];
742 assert_eq!(po.target.to_string(), "app.t");
743 assert_eq!(po.new_owner.as_str(), "app_owner");
744 }
745
746 #[test]
749 fn enable_row_security_produces_toggle() {
750 let out = build("ALTER TABLE app.docs ENABLE ROW LEVEL SECURITY;").expect("builds");
751 assert_eq!(out.pending_rls_toggles.len(), 1);
752 let t = &out.pending_rls_toggles[0];
753 assert_eq!(t.target.to_string(), "app.docs");
754 assert!(matches!(t.subtype, AlterTableType::AtEnableRowSecurity));
755 }
756
757 #[test]
758 fn disable_row_security_produces_toggle() {
759 let out = build("ALTER TABLE app.docs DISABLE 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::AtDisableRowSecurity));
763 }
764
765 #[test]
766 fn force_row_security_produces_toggle() {
767 let out = build("ALTER TABLE app.docs 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::AtForceRowSecurity));
771 }
772
773 #[test]
774 fn no_force_row_security_produces_toggle() {
775 let out = build("ALTER TABLE app.docs NO FORCE ROW LEVEL SECURITY;").expect("builds");
776 assert_eq!(out.pending_rls_toggles.len(), 1);
777 let t = &out.pending_rls_toggles[0];
778 assert!(matches!(t.subtype, AlterTableType::AtNoForceRowSecurity));
779 }
780
781 #[test]
784 fn alter_table_set_reloption_fillfactor() {
785 let out = build("ALTER TABLE app.t SET (fillfactor = 80);").expect("builds");
786 assert_eq!(out.pending_rel_options.len(), 1);
787 let p = &out.pending_rel_options[0];
788 assert_eq!(p.target.to_string(), "app.t");
789 assert_eq!(p.options.fillfactor, Some(80));
790 }
791
792 #[test]
793 fn alter_table_set_reloption_autovacuum_enabled() {
794 let out = build("ALTER TABLE app.t SET (autovacuum_enabled = false);").expect("builds");
795 assert_eq!(out.pending_rel_options.len(), 1);
796 let p = &out.pending_rel_options[0];
797 assert_eq!(
798 p.options
799 .extra
800 .get("autovacuum_enabled")
801 .map(String::as_str),
802 Some("false")
803 );
804 }
805
806 #[test]
807 fn alter_table_set_reloption_multiple_options() {
808 let out = build("ALTER TABLE app.t SET (fillfactor = 70, parallel_workers = 2);")
809 .expect("builds");
810 let p = &out.pending_rel_options[0];
811 assert_eq!(p.options.fillfactor, Some(70));
812 assert_eq!(p.options.parallel_workers, Some(2));
813 }
814
815 #[test]
816 fn alter_table_reset_reloption_errors() {
817 let err = build("ALTER TABLE app.t RESET (fillfactor);").unwrap_err();
818 assert!(
819 matches!(err, ParseError::Structural { ref message, .. }
820 if message.contains("RESET") || message.contains("not supported")),
821 "unexpected error: {err:?}"
822 );
823 }
824
825 #[test]
826 fn alter_table_set_fillfactor_out_of_range_errors() {
827 let err = build("ALTER TABLE app.t SET (fillfactor = 5);").unwrap_err();
828 assert!(
829 matches!(err, ParseError::Structural { ref message, .. } if message.contains("out of range")),
830 "unexpected error: {err:?}"
831 );
832 }
833
834 #[test]
837 fn alter_table_set_tablespace_produces_pending() {
838 let out = build("ALTER TABLE app.t SET TABLESPACE ts;").expect("builds");
839 assert_eq!(out.pending_tablespaces.len(), 1);
840 let p = &out.pending_tablespaces[0];
841 assert_eq!(p.target.to_string(), "app.t");
842 assert_eq!(p.tablespace.as_str(), "ts");
843 }
844}