From 2e5f3562f063ce35c31d81511c433878f36fb200 Mon Sep 17 00:00:00 2001 From: Sergey Olontsev Date: Mon, 30 Jun 2025 08:14:00 +0300 Subject: [PATCH] Support for Postgres's CREATE SERVER --- src/ast/mod.rs | 56 +++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 2 ++ src/parser/mod.rs | 54 +++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 72 +++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 19966d21..123a0683 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3316,6 +3316,18 @@ pub enum Statement { options: Vec, }, /// ```sql + /// CREATE SERVER + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createserver.html) + CreateServer { + name: ObjectName, + if_not_exists: bool, + server_type: Option, + version: Option, + fdw_name: ObjectName, + options: Option>, + }, + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -5175,6 +5187,36 @@ impl fmt::Display for Statement { write!(f, " )")?; Ok(()) } + Statement::CreateServer { + name, + if_not_exists, + server_type, + version, + fdw_name, + options + } => { + write!( + f, + "CREATE SERVER {if_not_exists}{name} ", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + + if let Some(st) = server_type { + write!(f, "TYPE {st} ")?; + } + + if let Some(v) = version { + write!(f, "VERSION {v} ")?; + } + + write!(f, "FOREIGN DATA WRAPPER {fdw_name}")?; + + if let Some(o) = options { + write!(f, " OPTIONS ({o})", o = display_comma_separated(o))?; + } + + Ok(()) + } Statement::CreatePolicy { name, table_name, @@ -7973,6 +8015,20 @@ impl fmt::Display for SecretOption { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ServerOption { + pub key: Ident, + pub value: Ident, +} + +impl fmt::Display for ServerOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.key, self.value) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 78ed772b..6b7b4738 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -423,6 +423,7 @@ impl Spanned for Statement { Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole { .. } => Span::empty(), Statement::CreateSecret { .. } => Span::empty(), + Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::AlterTable { name, diff --git a/src/keywords.rs b/src/keywords.rs index a8bbca3d..81ad68b6 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -816,6 +816,7 @@ define_keywords!( SERDE, SERDEPROPERTIES, SERIALIZABLE, + SERVER, SERVICE, SESSION, SESSION_USER, @@ -1016,6 +1017,7 @@ define_keywords!( WITHOUT, WITHOUT_ARRAY_WRAPPER, WORK, + WRAPPER, WRITE, XML, XMLNAMESPACES, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index adf50a8f..2c6afc71 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -33,7 +33,7 @@ use IsLateral::*; use IsOptional::*; use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; -use crate::ast::Statement::CreatePolicy; +use crate::ast::Statement::{CreatePolicy, CreateServer}; use crate::ast::*; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; @@ -4662,6 +4662,8 @@ impl<'a> Parser<'a> { self.parse_create_procedure(or_alter) } else if self.parse_keyword(Keyword::CONNECTOR) { self.parse_create_connector() + } else if self.parse_keyword(Keyword::SERVER) && dialect_of!(self is PostgreSqlDialect | GenericDialect) { + self.parse_pg_create_server() } else { self.expected("an object type after CREATE", self.peek_token()) } @@ -15806,6 +15808,56 @@ impl<'a> Parser<'a> { Ok(sequence_options) } + /// ```sql + /// CREATE SERVER [ IF NOT EXISTS ] server_name [ TYPE 'server_type' ] [ VERSION 'server_version' ] + /// FOREIGN DATA WRAPPER fdw_name + /// [ OPTIONS ( option 'value' [, ... ] ) ] + /// ``` + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html) + pub fn parse_pg_create_server(&mut self) -> Result { + let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let server_type = if self.parse_keyword(Keyword::TYPE) { + Some(self.parse_identifier()?) + } else { + None + }; + + let version = if self.parse_keyword(Keyword::VERSION) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::FOREIGN)?; + self.expect_keyword_is(Keyword::DATA)?; + self.expect_keyword_is(Keyword::WRAPPER)?; + + let fdw_name = 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(ServerOption { key, value }) + })?); + self.expect_token(&Token::RParen)?; + } + + Ok(CreateServer { + name, + if_not_exists: ine, + server_type, + version, + fdw_name, + options, + }) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7b0a8c5d..897ae9e7 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6231,3 +6231,75 @@ fn parse_ts_datatypes() { _ => unreachable!(), } } + +#[test] +fn parse_create_server() { + assert_eq!( + pg_and_generic().verified_stmt("CREATE SERVER myserver FOREIGN DATA WRAPPER postgres_fdw"), + Statement::CreateServer { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: false, + server_type: None, + version: None, + fdw_name: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("CREATE SERVER IF NOT EXISTS myserver TYPE 'server_type' VERSION 'server_version' FOREIGN DATA WRAPPER postgres_fdw"), + Statement::CreateServer { + name: ObjectName::from(vec!["myserver".into()]), + if_not_exists: true, + server_type: Some(Ident { + value: "server_type".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + version: Some(Ident { + value: "server_version".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + fdw_name: ObjectName::from(vec!["postgres_fdw".into()]), + options: None, + } + ); + + assert_eq!( + pg_and_generic().verified_stmt("CREATE SERVER myserver2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foo', dbname 'foodb', port '5432')"), + Statement::CreateServer { + name: ObjectName::from(vec!["myserver2".into()]), + if_not_exists: false, + server_type: None, + version: None, + fdw_name: ObjectName::from(vec!["postgres_fdw".into()]), + options: Some(vec![ + ServerOption { + key: "host".into(), + value: Ident { + value: "foo".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ServerOption { + key: "dbname".into(), + value: Ident { + value: "foodb".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ServerOption { + key: "port".into(), + value: Ident { + value: "5432".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }, + ]), + } + ) +}