diff --git a/fuzz/fuzz_seeds/postgres_alter_foreign_table.sql b/fuzz/fuzz_seeds/postgres_alter_foreign_table.sql new file mode 100644 index 0000000000..53ff81da23 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_alter_foreign_table.sql @@ -0,0 +1 @@ +ALTER FOREIGN TABLE IF EXISTS ONLY public.ft OWNER TO app, OPTIONS (ADD fetch_size '100', SET batch_size '10', DROP updatable), ALTER COLUMN id OPTIONS (column_name 'remote_id') \ No newline at end of file diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e434c58994..8dd843860d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -560,6 +560,14 @@ pub enum AlterTableOperation { /// Parenthesized options supplied to `SET (...)`. options: Vec, }, + /// `OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ...] )`, changing the + /// options of a foreign table. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterforeigntable.html) + GenericOptions { + /// The option changes, in source order. + options: Vec, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -1122,6 +1130,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { write!(f, "SET ({})", display_comma_separated(options)) } + AlterTableOperation::GenericOptions { options } => { + write!(f, "OPTIONS ({})", display_comma_separated(options)) + } } } } @@ -1375,6 +1386,14 @@ pub enum AlterColumnOperation { /// Optional sequence options for identity generation. sequence_options: Option>, }, + /// `OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ...] )`, changing the + /// options of a foreign table column. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterforeigntable.html) + GenericOptions { + /// The option changes, in source order. + options: Vec, + }, } impl fmt::Display for AlterColumnOperation { @@ -1424,6 +1443,48 @@ impl fmt::Display for AlterColumnOperation { } Ok(()) } + AlterColumnOperation::GenericOptions { options } => { + write!(f, "OPTIONS ({})", display_comma_separated(options)) + } + } + } +} + +/// An element of a PostgreSQL `OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ...] )` +/// list in an `ALTER` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterforeigntable.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterGenericOption { + /// `[ ADD ] option 'value'`, which fails if the option is already set. + Add { + /// The option to add. + option: CreateServerOption, + /// Whether the `ADD` keyword was written. + add_keyword: bool, + }, + /// `SET option 'value'`, which fails if the option is not set. + Set(CreateServerOption), + /// `DROP option` + Drop(Ident), +} + +impl fmt::Display for AlterGenericOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterGenericOption::Add { + option, + add_keyword, + } => { + if *add_keyword { + f.write_str("ADD ")?; + } + write!(f, "{option}") + } + AlterGenericOption::Set(option) => write!(f, "SET {option}"), + AlterGenericOption::Drop(key) => write!(f, "DROP {key}"), } } } @@ -4850,6 +4911,9 @@ pub enum AlterTableType { /// External table type /// External, + /// Foreign table type + /// + Foreign, } /// ALTER TABLE statement @@ -4884,6 +4948,7 @@ impl fmt::Display for AlterTable { Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, Some(AlterTableType::External) => write!(f, "ALTER EXTERNAL TABLE ")?, + Some(AlterTableType::Foreign) => write!(f, "ALTER FOREIGN TABLE ")?, None => write!(f, "ALTER TABLE ")?, } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 257ba6ac77..d47afbe665 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,28 +65,29 @@ pub use self::dcl::{ pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, - AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, - AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, - AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, - AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, - AlterTextSearch, AlterTextSearchOperation, AlterTextSearchOption, AlterTrigger, - AlterTriggerOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, CreateAggregateOption, - CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, - CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, - CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, - CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, - DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, - FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, - IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, - OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, - Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, - ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, + AlterFunctionOperation, AlterGenericOption, AlterIndexOperation, AlterOperator, + AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, + AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, + AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, + AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, + AlterTextSearchOption, AlterTrigger, AlterTriggerOperation, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, + CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, + CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, CreateFunction, + CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, + CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, CreateRuleDoKind, + CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, + DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, + DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, + ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, + GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, + OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, + PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, + SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 704b583b8b..b032489a03 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -29,26 +29,27 @@ use crate::tokenizer::Span; use super::{ comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, AlterTrigger, AlterTriggerOperation, Analyze, Array, - Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, - ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, - ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, - CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, - DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, - Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, - FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, - IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, - JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, - MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, OrderByKind, OutputClause, - Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, - RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, - Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, - TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, - ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, + AlterGenericOption, AlterIndexOperation, AlterTableOperation, AlterTrigger, + AlterTriggerOperation, Analyze, Array, Assignment, AssignmentTarget, AttachedToken, + BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, + ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, + ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, + CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, + ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, + Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, + FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, + IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, + JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, + MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, + NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, + OnConflictAction, OnInsert, OpenStatement, OperateFunctionArg, OrderBy, OrderByExpr, + OrderByKind, OutputClause, Parens, Partition, PartitionBoundValue, PivotValueSource, + ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, + RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, + SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, + TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, + TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -966,6 +967,20 @@ impl Spanned for AlterColumnOperation { had_set: _, } => using.as_ref().map_or(Span::empty(), |u| u.span()), AlterColumnOperation::AddGenerated { .. } => Span::empty(), + AlterColumnOperation::GenericOptions { options } => { + union_spans(options.iter().map(|option| option.span())) + } + } + } +} + +impl Spanned for AlterGenericOption { + fn span(&self) -> Span { + match self { + AlterGenericOption::Add { option, .. } | AlterGenericOption::Set(option) => { + option.key.span.union(&option.value.span) + } + AlterGenericOption::Drop(key) => key.span, } } } @@ -1312,6 +1327,9 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { union_spans(options.iter().map(|i| i.span())) } + AlterTableOperation::GenericOptions { options } => { + union_spans(options.iter().map(|option| option.span())) + } } } } @@ -3377,6 +3395,23 @@ SETTINGS update_parallel_mode = 'auto'"#; assert_eq!(test.get_source(options[0].value.span), "'public'"); } + #[test] + fn test_alter_foreign_table_option_spans() { + let dialect = &crate::dialect::PostgreSqlDialect {}; + let sql = "ALTER FOREIGN TABLE ft OPTIONS (ADD a '1', DROP b), ALTER COLUMN c OPTIONS (SET d '2')"; + let mut test = SpanTest::new(dialect, sql); + + let operations = match test.0.parse_statement().unwrap() { + Statement::AlterTable(alter) => alter.operations, + stmt => panic!("expected ALTER FOREIGN TABLE, got {stmt:?}"), + }; + assert_eq!(test.get_source(operations[0].span()), "a '1', DROP b"); + assert_eq!( + test.get_source(operations[1].span()), + "c OPTIONS (SET d '2'" + ); + } + #[test] fn test_alter_table_modify_order_by_span() { let dialect = &crate::dialect::ClickHouseDialect {}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 58ab4ca112..c70b73721f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11596,11 +11596,15 @@ impl<'a> Parser<'a> { generated_as, sequence_options, } + } else if self.parse_keyword(Keyword::OPTIONS) { + AlterColumnOperation::GenericOptions { + options: self.parse_alter_generic_options()?, + } } else { let message = if is_postgresql { - "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, or ADD GENERATED after ALTER COLUMN" + "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, ADD GENERATED, or OPTIONS after ALTER COLUMN" } else { - "SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN" + "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, or OPTIONS after ALTER COLUMN" }; return self.expected_ref(message, self.peek_token_ref()); @@ -11735,6 +11739,10 @@ impl<'a> Parser<'a> { AlterTableOperation::SetLogged } else if self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) { AlterTableOperation::SetUnlogged + } else if self.parse_keyword(Keyword::OPTIONS) { + AlterTableOperation::GenericOptions { + options: self.parse_alter_generic_options()?, + } } else { let mut options = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; @@ -11806,10 +11814,14 @@ impl<'a> Parser<'a> { Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), Keyword::COLLATION => self.parse_alter_collation().map(Into::into), - Keyword::TABLE => self.parse_alter_table(false), + Keyword::TABLE => self.parse_alter_table(None), Keyword::ICEBERG => { self.expect_keyword(Keyword::TABLE)?; - self.parse_alter_table(true) + self.parse_alter_table(Some(AlterTableType::Iceberg)) + } + Keyword::FOREIGN => { + self.expect_keyword(Keyword::TABLE)?; + self.parse_alter_table(Some(AlterTableType::Foreign)) } Keyword::INDEX => { let index_name = self.parse_object_name(false)?; @@ -11851,7 +11863,7 @@ impl<'a> Parser<'a> { _ => { self.index = index; self.expected_ref( - "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR", + "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR", self.peek_token_ref(), ) } @@ -12072,13 +12084,25 @@ impl<'a> Parser<'a> { } /// Parse a [Statement::AlterTable] - pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { + pub fn parse_alter_table( + &mut self, + table_type: Option, + ) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] let table_name = self.parse_object_name(false)?; + let on_cluster_loc = self.peek_token_ref().span.start; let on_cluster = self.parse_optional_on_cluster()?; + let foreign = table_type == Some(AlterTableType::Foreign); + if foreign && on_cluster.is_some() { + return parser_err!( + "Cannot specify ON CLUSTER in ALTER FOREIGN TABLE", + on_cluster_loc + ); + } let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + let location_loc = self.peek_token_ref().span.start; let mut location = None; if self.parse_keyword(Keyword::LOCATION) { location = Some(HiveSetLocation { @@ -12091,6 +12115,12 @@ impl<'a> Parser<'a> { location: self.parse_identifier()?, }); } + if foreign && location.is_some() { + return parser_err!( + "Cannot specify LOCATION in ALTER FOREIGN TABLE", + location_loc + ); + } let end_token = if self.peek_token_ref().token == Token::SemiColon { self.peek_token_ref().clone() @@ -12105,11 +12135,7 @@ impl<'a> Parser<'a> { operations, location, on_cluster, - table_type: if iceberg { - Some(AlterTableType::Iceberg) - } else { - None - }, + table_type, end_token: AttachedToken(end_token), } .into()) @@ -21346,14 +21372,36 @@ impl<'a> Parser<'a> { if !self.parse_keyword(Keyword::OPTIONS) { return Ok(None); } + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_pg_option)?; + self.expect_token(&Token::RParen)?; + Ok(Some(options)) + } + + fn parse_pg_option(&mut self) -> Result { + let key = self.parse_identifier()?; + let value = self.parse_identifier()?; + Ok(CreateServerOption { key, value }) + } + + /// Parse the parenthesized list following `OPTIONS` in an `ALTER` statement. + fn parse_alter_generic_options(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let options = self.parse_comma_separated(|p| { - let key = p.parse_identifier()?; - let value = p.parse_identifier()?; - Ok(CreateServerOption { key, value }) + if p.parse_keyword(Keyword::SET) { + Ok(AlterGenericOption::Set(p.parse_pg_option()?)) + } else if p.parse_keyword(Keyword::DROP) { + Ok(AlterGenericOption::Drop(p.parse_identifier()?)) + } else { + let add_keyword = p.parse_keyword(Keyword::ADD); + Ok(AlterGenericOption::Add { + option: p.parse_pg_option()?, + add_keyword, + }) + } })?; self.expect_token(&Token::RParen)?; - Ok(Some(options)) + Ok(options) } /// Parse a `CREATE FOREIGN TABLE` statement. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 379e37a660..6b3b6c8899 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15856,7 +15856,7 @@ fn parse_alter_trigger_spans() { #[test] fn parse_alter_rejects_missing_or_unknown_object_type() { - let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or SCHEMA or USER or OPERATOR"; + let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR"; for (sql, found) in [ ("ALTER", "EOF"), ("ALTER 1", "1"), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7fb4bb23fe..d6a799999e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11671,3 +11671,216 @@ fn parse_drop_group_is_dialect_gated() { )) ); } + +#[test] +fn parse_alter_foreign_table() { + for sql in [ + "ALTER FOREIGN TABLE ft ADD COLUMN note TEXT", + "ALTER FOREIGN TABLE ft DROP COLUMN IF EXISTS note CASCADE", + "ALTER FOREIGN TABLE ft RENAME COLUMN note TO memo", + "ALTER FOREIGN TABLE ft ALTER COLUMN id TYPE BIGINT", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET DEFAULT 0", + "ALTER FOREIGN TABLE ft ALTER COLUMN id SET NOT NULL", + "ALTER FOREIGN TABLE ft ADD CONSTRAINT positive CHECK (id > 0) NOT VALID", + "ALTER FOREIGN TABLE ft VALIDATE CONSTRAINT positive", + "ALTER FOREIGN TABLE ft DROP CONSTRAINT positive", + "ALTER FOREIGN TABLE ft OWNER TO app", + "ALTER FOREIGN TABLE ft RENAME TO archive", + "ALTER FOREIGN TABLE IF EXISTS ONLY public.ft ADD COLUMN a INT, DROP COLUMN b", + ] { + let Statement::AlterTable(alter) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(alter.table_type, Some(AlterTableType::Foreign)); + } + + let option = |key: &str, value: &str| CreateServerOption { + key: Ident::new(key), + value: Ident::with_quote('\'', value), + }; + let sql = "ALTER FOREIGN TABLE ft OPTIONS (ADD fetch_size '100', SET batch_size '10', DROP updatable, use_remote_estimate 'true')"; + let Statement::AlterTable(alter) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + alter.operations, + vec![AlterTableOperation::GenericOptions { + options: vec![ + AlterGenericOption::Add { + option: option("fetch_size", "100"), + add_keyword: true, + }, + AlterGenericOption::Set(option("batch_size", "10")), + AlterGenericOption::Drop(Ident::new("updatable")), + AlterGenericOption::Add { + option: option("use_remote_estimate", "true"), + add_keyword: false, + }, + ], + }] + ); + + // As written by pg_dump. + let stmt = pg_and_generic().one_statement_parses_to( + "ALTER FOREIGN TABLE ONLY public.colopts ALTER COLUMN id OPTIONS (\n column_name 'remote_id'\n)", + "ALTER FOREIGN TABLE ONLY public.colopts ALTER COLUMN id OPTIONS (column_name 'remote_id')", + ); + let Statement::AlterTable(alter) = stmt else { + unreachable!() + }; + assert_eq!( + alter.operations, + vec![AlterTableOperation::AlterColumn { + column_name: Ident::new("id"), + op: AlterColumnOperation::GenericOptions { + options: vec![AlterGenericOption::Add { + option: option("column_name", "remote_id"), + add_keyword: false, + }], + }, + }] + ); + pg_and_generic().verified_stmt( + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS (SET column_name 'remote_id', DROP other)", + ); + + for (sql, expected) in [ + ( + "ALTER FOREIGN TABLE ft OPTIONS (DROP fetch_size '100')", + "Expected: ), found: '100'", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS (SET fetch_size)", + "Expected: identifier, found: )", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS ()", + "Expected: identifier, found: )", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS (ADD fetch_size)", + "Expected: identifier, found: )", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS (DROP)", + "Expected: identifier, found: )", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS (ADD fetch_size '100'", + "Expected: ), found: EOF", + ), + ( + "ALTER FOREIGN TABLE ft OPTIONS ADD fetch_size '100'", + "Expected: (, found: ADD", + ), + ( + "ALTER FOREIGN TABLE ft ALTER COLUMN id OPTIONS column_name 'remote_id'", + "Expected: (, found: column_name", + ), + ( + "ALTER FOREIGN ft OWNER TO app", + "Expected: TABLE, found: ft", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } + + assert_eq!( + pg().parse_sql_statements("ALTER FOREIGN TABLE ft ALTER COLUMN id RESET") + .unwrap_err() + .to_string(), + "sql parser error: Expected: SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, ADD GENERATED, or OPTIONS after ALTER COLUMN, found: RESET" + ); + assert_eq!( + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("ALTER FOREIGN TABLE ft ALTER COLUMN id RESET") + .unwrap_err() + .to_string(), + "sql parser error: Expected: SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, or OPTIONS after ALTER COLUMN, found: RESET" + ); +} + +#[test] +fn display_alter_foreign_table_propagates_formatter_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let stmt = + pg_and_generic().verified_stmt("ALTER FOREIGN TABLE ft OPTIONS (ADD fetch_size '100')"); + for rejected in ["ALTER FOREIGN TABLE ", "ADD "] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); + } +} + +#[test] +fn parse_create_foreign_table_options_errors() { + for (sql, expected) in [ + ( + "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS schema_name 'public'", + "Expected: (, found: schema_name", + ), + ( + "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS ()", + "Expected: identifier, found: )", + ), + ( + "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public'", + "Expected: ), found: EOF", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} + +#[test] +fn parse_alter_foreign_table_rejects_clauses() { + for (sql, expected) in [ + ( + "ALTER FOREIGN TABLE ft ON CLUSTER c ADD COLUMN x INT", + "Cannot specify ON CLUSTER in ALTER FOREIGN TABLE", + ), + ( + "ALTER FOREIGN TABLE ft ADD COLUMN x INT LOCATION 's3://x'", + "Cannot specify LOCATION in ALTER FOREIGN TABLE", + ), + ( + "ALTER FOREIGN TABLE ft ADD COLUMN x INT SET LOCATION 's3://x'", + "Cannot specify LOCATION in ALTER FOREIGN TABLE", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +}