Skip to content

Commit 30cd573

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/alter-set
# Conflicts: # src/ast/ddl.rs # tests/sqlparser_postgres.rs
2 parents ccf2756 + 145ef5f commit 30cd573

6 files changed

Lines changed: 292 additions & 2 deletions

File tree

src/ast/ddl.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6280,6 +6280,39 @@ impl fmt::Display for AlterDomainOperation {
62806280
}
62816281
}
62826282

6283+
/// The target of a `CREATE PUBLICATION` statement: which rows to publish.
6284+
///
6285+
/// See <https://www.postgresql.org/docs/current/sql-createpublication.html>
6286+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6287+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6288+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6289+
pub enum PublicationTarget {
6290+
/// `FOR ALL TABLES`
6291+
AllTables,
6292+
/// `FOR TABLE table [, ...]`
6293+
Tables(Vec<ObjectName>),
6294+
/// `FOR TABLES IN SCHEMA schema [, ...]`
6295+
TablesInSchema(Vec<Ident>),
6296+
}
6297+
6298+
impl fmt::Display for PublicationTarget {
6299+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6300+
match self {
6301+
PublicationTarget::AllTables => write!(f, "FOR ALL TABLES"),
6302+
PublicationTarget::Tables(tables) => {
6303+
write!(f, "FOR TABLE {}", display_comma_separated(tables))
6304+
}
6305+
PublicationTarget::TablesInSchema(schemas) => {
6306+
write!(
6307+
f,
6308+
"FOR TABLES IN SCHEMA {}",
6309+
display_comma_separated(schemas)
6310+
)
6311+
}
6312+
}
6313+
}
6314+
}
6315+
62836316
impl From<AlterDomain> for crate::ast::Statement {
62846317
fn from(a: AlterDomain) -> Self {
62856318
crate::ast::Statement::AlterDomain(a)
@@ -6404,3 +6437,78 @@ impl From<AlterExtension> for crate::ast::Statement {
64046437
crate::ast::Statement::AlterExtension(a)
64056438
}
64066439
}
6440+
6441+
/// A `CREATE PUBLICATION` statement.
6442+
///
6443+
/// Note: this is a PostgreSQL-specific statement.
6444+
/// <https://www.postgresql.org/docs/current/sql-createpublication.html>
6445+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6446+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6447+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6448+
pub struct CreatePublication {
6449+
/// The publication name.
6450+
pub name: Ident,
6451+
/// Optional target specification (`FOR ALL TABLES`, `FOR TABLE ...`, or `FOR TABLES IN SCHEMA ...`).
6452+
pub target: Option<PublicationTarget>,
6453+
/// Optional `WITH (key = value, ...)` clause.
6454+
pub with_options: Vec<SqlOption>,
6455+
}
6456+
6457+
impl fmt::Display for CreatePublication {
6458+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6459+
write!(f, "CREATE PUBLICATION {}", self.name)?;
6460+
if let Some(target) = &self.target {
6461+
write!(f, " {target}")?;
6462+
}
6463+
if !self.with_options.is_empty() {
6464+
write!(f, " WITH ({})", display_comma_separated(&self.with_options))?;
6465+
}
6466+
Ok(())
6467+
}
6468+
}
6469+
6470+
impl From<CreatePublication> for crate::ast::Statement {
6471+
fn from(v: CreatePublication) -> Self {
6472+
crate::ast::Statement::CreatePublication(v)
6473+
}
6474+
}
6475+
6476+
/// A `CREATE SUBSCRIPTION` statement.
6477+
///
6478+
/// Note: this is a PostgreSQL-specific statement.
6479+
/// <https://www.postgresql.org/docs/current/sql-createsubscription.html>
6480+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6481+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6482+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
6483+
pub struct CreateSubscription {
6484+
/// The subscription name.
6485+
pub name: Ident,
6486+
/// The `CONNECTION 'conninfo'` string.
6487+
pub connection: Value,
6488+
/// The `PUBLICATION publication_name [, ...]` list.
6489+
pub publications: Vec<Ident>,
6490+
/// Optional `WITH (key = value, ...)` clause.
6491+
pub with_options: Vec<SqlOption>,
6492+
}
6493+
6494+
impl fmt::Display for CreateSubscription {
6495+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6496+
write!(
6497+
f,
6498+
"CREATE SUBSCRIPTION {name} CONNECTION {connection} PUBLICATION {publications}",
6499+
name = self.name,
6500+
connection = self.connection,
6501+
publications = display_comma_separated(&self.publications),
6502+
)?;
6503+
if !self.with_options.is_empty() {
6504+
write!(f, " WITH ({})", display_comma_separated(&self.with_options))?;
6505+
}
6506+
Ok(())
6507+
}
6508+
}
6509+
6510+
impl From<CreateSubscription> for crate::ast::Statement {
6511+
fn from(v: CreateSubscription) -> Self {
6512+
crate::ast::Statement::CreateSubscription(v)
6513+
}
6514+
}

src/ast/mod.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ pub use self::ddl::{
7474
CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector,
7575
CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, CreateFunction,
7676
CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy,
77-
CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTextSearchConfiguration,
78-
CreateTextSearchDictionary, CreateTextSearchParser, CreateTextSearchTemplate, CreateTrigger,
77+
CreatePolicyCommand, CreatePolicyType, CreatePublication, CreateSubscription, CreateTable,
78+
CreateTextSearchConfiguration, CreateTextSearchDictionary, CreateTextSearchParser,
79+
CreateTextSearchTemplate, CreateTrigger, PublicationTarget,
7980
CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension,
8081
DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature,
8182
DropPolicy, DropTrigger, FdwRoutineClause, ForValues, FunctionReturnType, GeneratedAs,
@@ -4036,6 +4037,18 @@ pub enum Statement {
40364037
/// <https://www.postgresql.org/docs/current/sql-createtstemplate.html>
40374038
CreateTextSearchTemplate(CreateTextSearchTemplate),
40384039
/// ```sql
4040+
/// CREATE PUBLICATION name [ FOR ALL TABLES | FOR TABLE table [, ...] | FOR TABLES IN SCHEMA schema [, ...] ] [ WITH ( option = value [, ...] ) ]
4041+
/// ```
4042+
/// Note: this is a PostgreSQL-specific statement.
4043+
/// <https://www.postgresql.org/docs/current/sql-createpublication.html>
4044+
CreatePublication(CreatePublication),
4045+
/// ```sql
4046+
/// CREATE SUBSCRIPTION name CONNECTION 'conninfo' PUBLICATION publication_name [, ...] [ WITH ( option = value [, ...] ) ]
4047+
/// ```
4048+
/// Note: this is a PostgreSQL-specific statement.
4049+
/// <https://www.postgresql.org/docs/current/sql-createsubscription.html>
4050+
CreateSubscription(CreateSubscription),
4051+
/// ```sql
40394052
/// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ]
40404053
/// ```
40414054
/// Note: this is a PostgreSQL-specific statement.
@@ -5522,6 +5535,8 @@ impl fmt::Display for Statement {
55225535
Statement::CreateTextSearchDictionary(v) => write!(f, "{v}"),
55235536
Statement::CreateTextSearchParser(v) => write!(f, "{v}"),
55245537
Statement::CreateTextSearchTemplate(v) => write!(f, "{v}"),
5538+
Statement::CreatePublication(v) => write!(f, "{v}"),
5539+
Statement::CreateSubscription(v) => write!(f, "{v}"),
55255540
Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"),
55265541
Statement::DropOperator(drop_operator) => write!(f, "{drop_operator}"),
55275542
Statement::DropOperatorFamily(drop_operator_family) => {

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ impl Spanned for Statement {
388388
Statement::CreateTextSearchDictionary(_) => Span::empty(),
389389
Statement::CreateTextSearchParser(_) => Span::empty(),
390390
Statement::CreateTextSearchTemplate(_) => Span::empty(),
391+
Statement::CreatePublication(_) => Span::empty(),
392+
Statement::CreateSubscription(_) => Span::empty(),
391393
Statement::DropExtension(drop_extension) => drop_extension.span(),
392394
Statement::DropOperator(drop_operator) => drop_operator.span(),
393395
Statement::DropOperatorFamily(drop_operator_family) => drop_operator_family.span(),

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ define_keywords!(
822822
PROGRAM,
823823
PROJECTION,
824824
PUBLIC,
825+
PUBLICATION,
825826
PURCHASE,
826827
PURGE,
827828
QUALIFY,
@@ -1010,6 +1011,7 @@ define_keywords!(
10101011
STRUCT,
10111012
SUBMULTISET,
10121013
SUBSCRIPT,
1014+
SUBSCRIPTION,
10131015
SUBSTR,
10141016
SUBSTRING,
10151017
SUBSTRING_REGEX,

src/parser/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5228,6 +5228,10 @@ impl<'a> Parser<'a> {
52285228
}
52295229
} else if self.parse_keywords(&[Keyword::TEXT, Keyword::SEARCH]) {
52305230
self.parse_create_text_search()
5231+
} else if self.parse_keyword(Keyword::PUBLICATION) {
5232+
self.parse_create_publication().map(Into::into)
5233+
} else if self.parse_keyword(Keyword::SUBSCRIPTION) {
5234+
self.parse_create_subscription().map(Into::into)
52315235
} else {
52325236
self.expected_ref("an object type after CREATE", self.peek_token_ref())
52335237
}
@@ -20211,6 +20215,59 @@ impl<'a> Parser<'a> {
2021120215
})
2021220216
}
2021320217

20218+
/// Parse a `CREATE PUBLICATION` statement.
20219+
///
20220+
/// See <https://www.postgresql.org/docs/current/sql-createpublication.html>
20221+
pub fn parse_create_publication(&mut self) -> Result<CreatePublication, ParserError> {
20222+
let name = self.parse_identifier()?;
20223+
20224+
let target = if self.parse_keyword(Keyword::FOR) {
20225+
if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES]) {
20226+
Some(PublicationTarget::AllTables)
20227+
} else if self.parse_keyword(Keyword::TABLE) {
20228+
let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?;
20229+
Some(PublicationTarget::Tables(tables))
20230+
} else if self.parse_keywords(&[Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) {
20231+
let schemas = self.parse_comma_separated(|p| p.parse_identifier())?;
20232+
Some(PublicationTarget::TablesInSchema(schemas))
20233+
} else {
20234+
return self.expected_ref(
20235+
"ALL TABLES, TABLE, or TABLES IN SCHEMA after FOR",
20236+
self.peek_token_ref(),
20237+
);
20238+
}
20239+
} else {
20240+
None
20241+
};
20242+
20243+
let with_options = self.parse_options(Keyword::WITH)?;
20244+
20245+
Ok(CreatePublication {
20246+
name,
20247+
target,
20248+
with_options,
20249+
})
20250+
}
20251+
20252+
/// Parse a `CREATE SUBSCRIPTION` statement.
20253+
///
20254+
/// See <https://www.postgresql.org/docs/current/sql-createsubscription.html>
20255+
pub fn parse_create_subscription(&mut self) -> Result<CreateSubscription, ParserError> {
20256+
let name = self.parse_identifier()?;
20257+
self.expect_keyword_is(Keyword::CONNECTION)?;
20258+
let connection = self.parse_value()?.value;
20259+
self.expect_keyword_is(Keyword::PUBLICATION)?;
20260+
let publications = self.parse_comma_separated(|p| p.parse_identifier())?;
20261+
let with_options = self.parse_options(Keyword::WITH)?;
20262+
20263+
Ok(CreateSubscription {
20264+
name,
20265+
connection,
20266+
publications,
20267+
with_options,
20268+
})
20269+
}
20270+
2021420271
/// The index of the first unprocessed token.
2021520272
pub fn index(&self) -> usize {
2021620273
self.index

tests/sqlparser_postgres.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9761,3 +9761,109 @@ fn alter_procedure_rename() {
97619761
}
97629762
);
97639763
}
9764+
9765+
#[test]
9766+
fn parse_create_publication_basic() {
9767+
let sql = "CREATE PUBLICATION mypub FOR TABLE public.t";
9768+
let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else {
9769+
unreachable!()
9770+
};
9771+
assert_eq!(stmt.name.value, "mypub");
9772+
assert!(stmt.with_options.is_empty());
9773+
match stmt.target.unwrap() {
9774+
PublicationTarget::Tables(tables) => {
9775+
assert_eq!(tables.len(), 1);
9776+
assert_eq!(tables[0].to_string(), "public.t");
9777+
}
9778+
other => panic!("unexpected target: {other:?}"),
9779+
}
9780+
}
9781+
9782+
#[test]
9783+
fn parse_create_publication_for_all_tables() {
9784+
let sql = "CREATE PUBLICATION mypub FOR ALL TABLES";
9785+
let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else {
9786+
unreachable!()
9787+
};
9788+
assert_eq!(stmt.name.value, "mypub");
9789+
assert!(matches!(stmt.target, Some(PublicationTarget::AllTables)));
9790+
assert!(stmt.with_options.is_empty());
9791+
}
9792+
9793+
#[test]
9794+
fn parse_create_publication_for_tables_in_schema() {
9795+
let sql = "CREATE PUBLICATION mypub FOR TABLES IN SCHEMA myschema";
9796+
let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else {
9797+
unreachable!()
9798+
};
9799+
assert_eq!(stmt.name.value, "mypub");
9800+
match stmt.target.unwrap() {
9801+
PublicationTarget::TablesInSchema(schemas) => {
9802+
assert_eq!(schemas.len(), 1);
9803+
assert_eq!(schemas[0].value, "myschema");
9804+
}
9805+
other => panic!("unexpected target: {other:?}"),
9806+
}
9807+
}
9808+
9809+
#[test]
9810+
fn parse_create_publication_with_options() {
9811+
let sql = "CREATE PUBLICATION mypub FOR ALL TABLES WITH (publish = 'insert, update')";
9812+
let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else {
9813+
unreachable!()
9814+
};
9815+
assert_eq!(stmt.name.value, "mypub");
9816+
assert!(matches!(stmt.target, Some(PublicationTarget::AllTables)));
9817+
assert_eq!(stmt.with_options.len(), 1);
9818+
match &stmt.with_options[0] {
9819+
SqlOption::KeyValue { key, value } => {
9820+
assert_eq!(key.value, "publish");
9821+
assert_eq!(value.to_string(), "'insert, update'");
9822+
}
9823+
other => panic!("unexpected option: {other:?}"),
9824+
}
9825+
}
9826+
9827+
#[test]
9828+
fn parse_create_subscription_basic() {
9829+
let sql = "CREATE SUBSCRIPTION mysub CONNECTION 'host=localhost' PUBLICATION mypub";
9830+
let Statement::CreateSubscription(stmt) = pg().verified_stmt(sql) else {
9831+
unreachable!()
9832+
};
9833+
assert_eq!(stmt.name.value, "mysub");
9834+
assert_eq!(stmt.connection.to_string(), "'host=localhost'");
9835+
assert_eq!(stmt.publications.len(), 1);
9836+
assert_eq!(stmt.publications[0].value, "mypub");
9837+
assert!(stmt.with_options.is_empty());
9838+
}
9839+
9840+
#[test]
9841+
fn parse_create_subscription_with_options() {
9842+
let sql = "CREATE SUBSCRIPTION mysub CONNECTION 'host=localhost dbname=mydb' PUBLICATION mypub, otherpub WITH (copy_data = true, slot_name = 'myslot')";
9843+
let Statement::CreateSubscription(stmt) = pg().verified_stmt(sql) else {
9844+
unreachable!()
9845+
};
9846+
assert_eq!(stmt.name.value, "mysub");
9847+
assert_eq!(
9848+
stmt.connection.to_string(),
9849+
"'host=localhost dbname=mydb'"
9850+
);
9851+
assert_eq!(stmt.publications.len(), 2);
9852+
assert_eq!(stmt.publications[0].value, "mypub");
9853+
assert_eq!(stmt.publications[1].value, "otherpub");
9854+
assert_eq!(stmt.with_options.len(), 2);
9855+
match &stmt.with_options[0] {
9856+
SqlOption::KeyValue { key, value } => {
9857+
assert_eq!(key.value, "copy_data");
9858+
assert_eq!(value.to_string(), "true");
9859+
}
9860+
other => panic!("unexpected option: {other:?}"),
9861+
}
9862+
match &stmt.with_options[1] {
9863+
SqlOption::KeyValue { key, value } => {
9864+
assert_eq!(key.value, "slot_name");
9865+
assert_eq!(value.to_string(), "'myslot'");
9866+
}
9867+
other => panic!("unexpected option: {other:?}"),
9868+
}
9869+
}

0 commit comments

Comments
 (0)