Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 81 additions & 9 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ use crate::ast::{
UniqueConstraint,
},
ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody,
CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr,
FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDefinitionSetParam, FunctionDesc,
FunctionDeterminismSpecifier, FunctionParallel, FunctionSecurity, HiveDistributionStyle,
HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind,
MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg,
OrderByExpr, ProjectionSelect, Query, RefreshModeKind, ResetConfig, RowAccessPolicy,
SequenceOptions, Spanned, SqlOption, StorageLifecyclePolicy, StorageSerializationPolicy,
TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod,
TriggerReferencing, Value, ValueWithSpan, WrappedCollection,
CreateFunctionUsing, CreateServerOption, CreateTableLikeKind, CreateTableOptions,
CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull,
FunctionDefinitionSetParam, FunctionDesc, FunctionDeterminismSpecifier, FunctionParallel,
FunctionSecurity, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat,
HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit,
OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind,
ResetConfig, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageLifecyclePolicy,
StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject,
TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection,
};
use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline};
use crate::keywords::Keyword;
Expand Down Expand Up @@ -5995,3 +5995,75 @@ impl From<AlterPolicy> for crate::ast::Statement {
crate::ast::Statement::AlterPolicy(v)
}
}

/// A `CREATE FOREIGN TABLE` statement.
///
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CreateForeignTable {
/// The foreign table name.
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
pub name: ObjectName,
/// Whether `IF NOT EXISTS` was specified.
pub if_not_exists: bool,
/// Column definitions.
pub columns: Vec<ColumnDef>,
/// Table-level constraints (e.g. `CHECK (...)`, composite `FOREIGN KEY`).
/// PostgreSQL's grammar accepts these here, but rejects primary key, unique,
/// foreign key and exclusion constraints on a foreign table at execution.
pub constraints: Vec<TableConstraint>,
/// The `SERVER server_name` clause.
pub server_name: Ident,
/// Optional `OPTIONS (key 'value', ...)` clause at the table level.
pub options: Option<Vec<CreateServerOption>>,
}

impl fmt::Display for CreateForeignTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}",
if_not_exists = if self.if_not_exists {
"IF NOT EXISTS "
} else {
""
},
name = self.name,
columns = display_comma_separated(&self.columns),
)?;
if !self.columns.is_empty() && !self.constraints.is_empty() {
write!(f, ", ")?;
}
write!(f, "{}", display_comma_separated(&self.constraints))?;
write!(f, ") SERVER {}", self.server_name)?;
if let Some(options) = &self.options {
write!(f, " OPTIONS ({})", display_comma_separated(options))?;
}
Ok(())
}
}

impl From<CreateForeignTable> for crate::ast::Statement {
fn from(v: CreateForeignTable) -> Self {
crate::ast::Statement::CreateForeignTable(v)
}
}

impl Spanned for CreateForeignTable {
fn span(&self) -> Span {
Span::union_iter(
core::iter::once(self.name.span())
.chain(self.columns.iter().map(|column| column.span()))
.chain(self.constraints.iter().map(|constraint| constraint.span()))
.chain(core::iter::once(self.server_name.span))
.chain(
self.options
.iter()
.flatten()
.flat_map(|option| [option.key.span, option.value.span]),
),
)
}
}
10 changes: 8 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub use self::ddl::{
AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef,
ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty,
ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector,
CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator,
CreateDomain, CreateExtension, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator,
CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType,
CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial,
DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass,
Expand Down Expand Up @@ -3773,6 +3773,11 @@ pub enum Statement {
/// A `CREATE SERVER` statement.
CreateServer(CreateServerStatement),
/// ```sql
/// CREATE FOREIGN TABLE
/// ```
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html)
CreateForeignTable(CreateForeignTable),
/// ```sql
/// CREATE POLICY
/// ```
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html)
Expand Down Expand Up @@ -5632,6 +5637,7 @@ impl fmt::Display for Statement {
Statement::CreateServer(stmt) => {
write!(f, "{stmt}")
}
Statement::CreateForeignTable(stmt) => write!(f, "{stmt}"),
Statement::CreatePolicy(policy) => write!(f, "{policy}"),
Statement::CreateConnector(create_connector) => create_connector.fmt(f),
Statement::CreateOperator(create_operator) => create_operator.fmt(f),
Expand Down Expand Up @@ -9189,7 +9195,7 @@ impl fmt::Display for CreateServerStatement {
}
}

/// A key/value option for `CREATE SERVER`.
/// A key/value entry in a Postgres `OPTIONS ( ... )` clause.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
Expand Down
16 changes: 16 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ impl Spanned for Statement {
Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(),
Statement::CreateSecret { .. } => Span::empty(),
Statement::CreateServer { .. } => Span::empty(),
Statement::CreateForeignTable(stmt) => stmt.span(),
Statement::CreateConnector { .. } => Span::empty(),
Statement::CreateOperator(create_operator) => create_operator.span(),
Statement::CreateOperatorFamily(create_operator_family) => {
Expand Down Expand Up @@ -3163,4 +3164,19 @@ WHERE id = 1
Span::new(Location::new(2, 8), Location::new(4, 52))
);
}

#[test]
fn test_create_foreign_table_span_includes_option_keys() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public')";
let mut test = SpanTest::new(dialect, sql);

// Ends at the option key, not the statement: a quoted option value is an
// Ident with an empty span, so it contributes nothing to the union.
let stmt = test.0.parse_statement().unwrap();
assert_eq!(
test.get_source(stmt.span()),
"ft (a INT) SERVER s OPTIONS (schema_name"
);
}
}
76 changes: 66 additions & 10 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5257,6 +5257,7 @@ impl<'a> Parser<'a> {

/// Parse a SQL CREATE statement
pub fn parse_create(&mut self) -> Result<Statement, ParserError> {
let modifier_loc = self.peek_token_ref().span.start;
let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]);
let multiset = self.maybe_parse_multiset();
Expand Down Expand Up @@ -5359,6 +5360,25 @@ impl<'a> Parser<'a> {
}
} else if self.parse_keyword(Keyword::SERVER) {
self.parse_pg_create_server()
} else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::TABLE]) {
// `or_replace` cannot reach here today, since the arm above catches it.
// It stays so that reordering the arms cannot make it fall through.
if or_replace
|| or_alter
|| temporary
|| global.is_some()
|| transient
|| volatile
|| multiset.is_some()
|| persistent
|| create_view_params.is_some()
{
return parser_err!(
"CREATE FOREIGN TABLE does not accept this modifier",
modifier_loc
);
}
self.parse_create_foreign_table().map(Into::into)
} else {
self.expected_ref("an object type after CREATE", self.peek_token_ref())
}
Expand Down Expand Up @@ -20431,16 +20451,7 @@ impl<'a> Parser<'a> {
self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?;
let foreign_data_wrapper = self.parse_object_name(false)?;

let mut options = None;
if self.parse_keyword(Keyword::OPTIONS) {
self.expect_token(&Token::LParen)?;
options = Some(self.parse_comma_separated(|p| {
let key = p.parse_identifier()?;
let value = p.parse_identifier()?;
Ok(CreateServerOption { key, value })
})?);
self.expect_token(&Token::RParen)?;
}
let options = self.parse_pg_options_clause()?;

Ok(Statement::CreateServer(CreateServerStatement {
name,
Expand All @@ -20452,6 +20463,51 @@ impl<'a> Parser<'a> {
}))
}

/// Parse an optional Postgres `OPTIONS ( key value [, ...] )` clause.
fn parse_pg_options_clause(&mut self) -> Result<Option<Vec<CreateServerOption>>, ParserError> {
if !self.parse_keyword(Keyword::OPTIONS) {
return Ok(None);
}
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 })
})?;
self.expect_token(&Token::RParen)?;
Ok(Some(options))
}

/// Parse a `CREATE FOREIGN TABLE` statement.
///
/// Per-column `OPTIONS ( ... )`, `INHERITS`, and the `PARTITION OF` form are
/// not parsed yet.
///
/// See <https://www.postgresql.org/docs/current/sql-createforeigntable.html>
pub fn parse_create_foreign_table(&mut self) -> Result<CreateForeignTable, ParserError> {
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let name = self.parse_object_name(false)?;
if self.peek_token_ref().token != Token::LParen {
return self.expected_ref(
"'(' before the column list of CREATE FOREIGN TABLE",
self.peek_token_ref(),
);
}
let (columns, constraints) = self.parse_columns()?;
self.expect_keyword_is(Keyword::SERVER)?;
let server_name = self.parse_identifier()?;
let options = self.parse_pg_options_clause()?;

Ok(CreateForeignTable {
name,
if_not_exists,
columns,
constraints,
server_name,
options,
})
}

/// The index of the first unprocessed token.
pub fn index(&self) -> usize {
self.index
Expand Down
96 changes: 96 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9639,6 +9639,102 @@ fn parse_lock_table() {
}
}

#[test]
fn parse_create_foreign_table() {
// Each of these round-trips through Display, so verified_stmt already pins
// the name, columns, server and IF NOT EXISTS. Only the parsed shape that
// Display cannot show is asserted below.
for sql in [
"CREATE FOREIGN TABLE ft1 (id INTEGER, name TEXT) SERVER myserver",
"CREATE FOREIGN TABLE IF NOT EXISTS ft2 (col INTEGER) SERVER remoteserver",
] {
assert!(matches!(
pg_and_generic().verified_stmt(sql),
Statement::CreateForeignTable(_)
));
}

let sql =
"CREATE FOREIGN TABLE ft3 (col INTEGER) SERVER remoteserver OPTIONS (schema_name 'public')";
let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {
unreachable!()
};
assert_eq!(
stmt.options,
Some(vec![CreateServerOption {
key: "schema_name".into(),
value: Ident {
value: "public".to_string(),
quote_style: Some('\''),
span: Span::empty(),
},
}])
);
}

#[test]
fn parse_create_foreign_table_requires_column_list() {
// Without the parens Display would invent a `()` the input never had.
assert!(matches!(
pg_and_generic().parse_sql_statements("CREATE FOREIGN TABLE ft SERVER s"),
Err(ParserError::ParserError(_))
));

// An empty list is still legal PostgreSQL.
pg_and_generic().verified_stmt("CREATE FOREIGN TABLE ft () SERVER s");
}

#[test]
fn parse_create_foreign_table_rejects_modifiers() {
// None of these has a field on CreateForeignTable, so accepting one would
// drop it silently on the way back out through Display.
for sql in [
"CREATE TEMPORARY FOREIGN TABLE ft (a INT) SERVER s",
"CREATE GLOBAL FOREIGN TABLE ft (a INT) SERVER s",
"CREATE LOCAL FOREIGN TABLE ft (a INT) SERVER s",
"CREATE TRANSIENT FOREIGN TABLE ft (a INT) SERVER s",
"CREATE VOLATILE FOREIGN TABLE ft (a INT) SERVER s",
"CREATE OR ALTER FOREIGN TABLE ft (a INT) SERVER s",
"CREATE MULTISET FOREIGN TABLE ft (a INT) SERVER s",
"CREATE SET FOREIGN TABLE ft (a INT) SERVER s",
"CREATE ALGORITHM = UNDEFINED FOREIGN TABLE ft (a INT) SERVER s",
] {
let err = pg_and_generic().parse_sql_statements(sql).unwrap_err();
assert!(
err.to_string()
.contains("CREATE FOREIGN TABLE does not accept this modifier"),
"unexpected error for {sql}: {err}"
);
}

// OR REPLACE is caught by an earlier arm, so it never reaches the guard.
assert!(matches!(
pg_and_generic()
.parse_sql_statements("CREATE OR REPLACE FOREIGN TABLE ft (a INT) SERVER s"),
Err(ParserError::ParserError(_))
));
}

#[test]
fn parse_create_foreign_table_with_check_constraint() {
// PostgreSQL accepts table-level CHECK constraints in CREATE FOREIGN TABLE.
let sql =
"CREATE FOREIGN TABLE ft (id INTEGER, CONSTRAINT id_positive CHECK (id > 0)) SERVER s";
let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {
unreachable!()
};
assert_eq!(stmt.columns.len(), 1);
assert_eq!(stmt.constraints.len(), 1);

// Zero columns with only a table-level constraint must not emit `(, CONSTRAINT ...)`.
let sql = "CREATE FOREIGN TABLE ft (CONSTRAINT c CHECK (id > 0)) SERVER s";
let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {
unreachable!()
};
assert_eq!(stmt.columns.len(), 0);
assert_eq!(stmt.constraints.len(), 1);
}

#[test]
fn exclude_as_column_name() {
// `EXCLUDE` is a non-reserved keyword, so it stays usable as a column name
Expand Down
Loading