Skip to content

Commit 1071d75

Browse files
PostgreSQL: Support PARTITION OF in CREATE FOREIGN TABLE (#2753)
1 parent 9976fd0 commit 1071d75

5 files changed

Lines changed: 223 additions & 12 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE FOREIGN TABLE part PARTITION OF root (id NOT NULL) FOR VALUES FROM (0) TO (10) SERVER s;
2+
CREATE FOREIGN TABLE part2 PARTITION OF root DEFAULT SERVER s

‎src/ast/ddl.rs‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6584,6 +6584,11 @@ pub struct CreateForeignTable {
65846584
pub constraints: Vec<TableConstraint>,
65856585
/// `INHERITS ( parent_table [, ...] )`
65866586
pub inherits: Option<Vec<ObjectName>>,
6587+
/// The parent table of `PARTITION OF parent_table`.
6588+
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
6589+
pub partition_of: Option<ObjectName>,
6590+
/// The partition bound following `PARTITION OF`.
6591+
pub for_values: Option<ForValues>,
65876592
/// The `SERVER server_name` clause.
65886593
pub server_name: Ident,
65896594
/// Optional `OPTIONS (key 'value', ...)` clause at the table level.
@@ -6594,23 +6599,31 @@ impl fmt::Display for CreateForeignTable {
65946599
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65956600
write!(
65966601
f,
6597-
"CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}",
6602+
"CREATE FOREIGN TABLE {if_not_exists}{name}",
65986603
if_not_exists = if self.if_not_exists {
65996604
"IF NOT EXISTS "
66006605
} else {
66016606
""
66026607
},
66036608
name = self.name,
6604-
columns = display_comma_separated(&self.columns),
66056609
)?;
6606-
if !self.columns.is_empty() && !self.constraints.is_empty() {
6607-
write!(f, ", ")?;
6610+
if let Some(partition_of) = &self.partition_of {
6611+
write!(f, " PARTITION OF {partition_of}")?;
6612+
}
6613+
// The column list is optional only in the `PARTITION OF` form.
6614+
if self.partition_of.is_none() || !self.columns.is_empty() || !self.constraints.is_empty() {
6615+
write!(f, " ({}", display_comma_separated(&self.columns))?;
6616+
if !self.columns.is_empty() && !self.constraints.is_empty() {
6617+
write!(f, ", ")?;
6618+
}
6619+
write!(f, "{})", display_comma_separated(&self.constraints))?;
66086620
}
6609-
write!(f, "{}", display_comma_separated(&self.constraints))?;
6610-
f.write_str(")")?;
66116621
if let Some(inherits) = &self.inherits {
66126622
write!(f, " INHERITS ({})", display_comma_separated(inherits))?;
66136623
}
6624+
if let Some(for_values) = &self.for_values {
6625+
write!(f, " {for_values}")?;
6626+
}
66146627
write!(f, " SERVER {}", self.server_name)?;
66156628
if let Some(options) = &self.options {
66166629
write!(f, " OPTIONS ({})", display_comma_separated(options))?;
@@ -6694,6 +6707,8 @@ impl Spanned for CreateForeignTable {
66946707
.chain(self.columns.iter().map(|column| column.span()))
66956708
.chain(self.constraints.iter().map(|constraint| constraint.span()))
66966709
.chain(self.inherits.iter().flatten().map(|parent| parent.span()))
6710+
.chain(self.partition_of.iter().map(|parent| parent.span()))
6711+
.chain(self.for_values.iter().map(|for_values| for_values.span()))
66976712
.chain(core::iter::once(self.server_name.span))
66986713
.chain(
66996714
self.options

‎src/ast/spans.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3489,6 +3489,19 @@ SETTINGS update_parallel_mode = 'auto'"#;
34893489
);
34903490
}
34913491

3492+
#[test]
3493+
fn test_create_foreign_table_partition_of_span() {
3494+
let dialect = &crate::dialect::PostgreSqlDialect {};
3495+
let sql =
3496+
"CREATE FOREIGN TABLE part PARTITION OF root (id NOT NULL) FOR VALUES IN (1) SERVER s";
3497+
let test = SpanTest::new(dialect, sql);
3498+
let r = Parser::parse_sql(dialect, sql).unwrap();
3499+
assert_eq!(
3500+
test.get_source(r[0].span()),
3501+
"part PARTITION OF root (id NOT NULL) FOR VALUES IN (1) SERVER s"
3502+
);
3503+
}
3504+
34923505
#[test]
34933506
fn test_alter_table_modify_order_by_span() {
34943507
let dialect = &crate::dialect::ClickHouseDialect {};

‎src/parser/mod.rs‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21707,24 +21707,46 @@ impl<'a> Parser<'a> {
2170721707

2170821708
/// Parse a `CREATE FOREIGN TABLE` statement.
2170921709
///
21710-
/// The `PARTITION OF` form is not parsed yet.
21711-
///
2171221710
/// See <https://www.postgresql.org/docs/current/sql-createforeigntable.html>
2171321711
pub fn parse_create_foreign_table(&mut self) -> Result<CreateForeignTable, ParserError> {
2171421712
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
2171521713
let name = self.parse_object_name(false)?;
21716-
if self.peek_token_ref().token != Token::LParen {
21714+
let partition_of = if self.parse_keywords(&[Keyword::PARTITION, Keyword::OF]) {
21715+
Some(self.parse_object_name(false)?)
21716+
} else {
21717+
None
21718+
};
21719+
let (columns, constraints) = if partition_of.is_some() {
21720+
self.parse_partition_of_columns()?
21721+
} else if self.peek_token_ref().token == Token::LParen {
21722+
self.parse_columns()?
21723+
} else {
2171721724
return self.expected_ref(
2171821725
"'(' before the column list of CREATE FOREIGN TABLE",
2171921726
self.peek_token_ref(),
2172021727
);
21721-
}
21722-
let (columns, constraints) = self.parse_columns()?;
21723-
let inherits = if self.parse_keyword(Keyword::INHERITS) {
21728+
};
21729+
// `INHERITS` belongs to the column-list form only.
21730+
let inherits = if partition_of.is_none() && self.parse_keyword(Keyword::INHERITS) {
2172421731
Some(self.parse_parenthesized_qualified_column_list(IsOptional::Mandatory, false)?)
2172521732
} else {
2172621733
None
2172721734
};
21735+
21736+
let for_values = if partition_of.is_some() {
21737+
if self
21738+
.peek_one_of_keywords(&[Keyword::FOR, Keyword::DEFAULT])
21739+
.is_none()
21740+
{
21741+
return self.expected_ref(
21742+
"FOR VALUES or DEFAULT after PARTITION OF",
21743+
self.peek_token_ref(),
21744+
);
21745+
}
21746+
Some(self.parse_partition_for_values()?)
21747+
} else {
21748+
None
21749+
};
2172821750
self.expect_keyword_is(Keyword::SERVER)?;
2172921751
let server_name = self.parse_identifier()?;
2173021752
let options = self.parse_pg_options_clause()?;
@@ -21735,6 +21757,8 @@ impl<'a> Parser<'a> {
2173521757
columns,
2173621758
constraints,
2173721759
inherits,
21760+
partition_of,
21761+
for_values,
2173821762
server_name,
2173921763
options,
2174021764
})

‎tests/sqlparser_postgres.rs‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12857,3 +12857,160 @@ fn display_alter_materialized_view_propagates_write_errors() {
1285712857
);
1285812858
}
1285912859
}
12860+
12861+
#[test]
12862+
fn parse_create_foreign_table_partition_of() {
12863+
let sql = "CREATE FOREIGN TABLE part PARTITION OF root FOR VALUES FROM (0) TO (10) SERVER s";
12864+
let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {
12865+
unreachable!()
12866+
};
12867+
assert_eq!(
12868+
stmt.partition_of,
12869+
Some(ObjectName::from(vec![Ident::new("root")]))
12870+
);
12871+
assert_eq!(
12872+
stmt.for_values,
12873+
Some(ForValues::From {
12874+
from: vec![PartitionBoundValue::Expr(Expr::value(number("0")))],
12875+
to: vec![PartitionBoundValue::Expr(Expr::value(number("10")))],
12876+
})
12877+
);
12878+
assert!(stmt.columns.is_empty());
12879+
12880+
let sql = "CREATE FOREIGN TABLE part PARTITION OF public.root (id NOT NULL, CONSTRAINT positive CHECK (id > 0)) FOR VALUES FROM (MINVALUE) TO (MAXVALUE) SERVER s";
12881+
let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {
12882+
unreachable!()
12883+
};
12884+
assert_eq!(
12885+
stmt.columns,
12886+
vec![ColumnDef {
12887+
name: Ident::new("id"),
12888+
data_type: DataType::Unspecified,
12889+
options: vec![ColumnOptionDef {
12890+
name: None,
12891+
option: ColumnOption::NotNull,
12892+
}],
12893+
}]
12894+
);
12895+
assert_eq!(stmt.constraints.len(), 1);
12896+
assert_eq!(
12897+
stmt.for_values,
12898+
Some(ForValues::From {
12899+
from: vec![PartitionBoundValue::MinValue],
12900+
to: vec![PartitionBoundValue::MaxValue],
12901+
})
12902+
);
12903+
12904+
for sql in [
12905+
"CREATE FOREIGN TABLE IF NOT EXISTS part PARTITION OF root DEFAULT SERVER s OPTIONS (table_name 'remote')",
12906+
"CREATE FOREIGN TABLE part PARTITION OF root (CONSTRAINT positive CHECK (id > 0)) FOR VALUES IN (1, 2) SERVER s",
12907+
"CREATE FOREIGN TABLE part PARTITION OF root FOR VALUES WITH (MODULUS 4, REMAINDER 0) SERVER s",
12908+
"CREATE FOREIGN TABLE part PARTITION OF root (id NOT NULL) FOR VALUES IN (3) SERVER s",
12909+
] {
12910+
pg_and_generic().verified_stmt(sql);
12911+
}
12912+
12913+
pg_and_generic().one_statement_parses_to(
12914+
"CREATE FOREIGN TABLE part PARTITION OF root (id WITH OPTIONS NOT NULL) DEFAULT SERVER s",
12915+
"CREATE FOREIGN TABLE part PARTITION OF root (id NOT NULL) DEFAULT SERVER s",
12916+
);
12917+
12918+
for (sql, expected) in [
12919+
(
12920+
"CREATE FOREIGN TABLE part PARTITION OF root SERVER s",
12921+
"Expected: FOR VALUES or DEFAULT after PARTITION OF, found: SERVER",
12922+
),
12923+
(
12924+
"CREATE FOREIGN TABLE part PARTITION OF root DEFAULT INHERITS (other) SERVER s",
12925+
"Expected: SERVER, found: INHERITS",
12926+
),
12927+
(
12928+
"CREATE FOREIGN TABLE part PARTITION OF root INHERITS (other) FOR VALUES IN (1) SERVER s",
12929+
"Expected: FOR VALUES or DEFAULT after PARTITION OF, found: INHERITS",
12930+
),
12931+
(
12932+
"CREATE FOREIGN TABLE part PARTITION OF root FOR VALUES IN (1)",
12933+
"Expected: SERVER, found: EOF",
12934+
),
12935+
(
12936+
"CREATE FOREIGN TABLE part PARTITION OF",
12937+
"Expected: identifier, found: EOF",
12938+
),
12939+
(
12940+
"CREATE FOREIGN TABLE part PARTITION OF root FOR VALUES IN () SERVER s",
12941+
"Expected: at least one value, found: )",
12942+
),
12943+
(
12944+
"CREATE FOREIGN TABLE part PARTITION OF root FOR VALUES SERVER s",
12945+
"Expected: IN, FROM, or WITH after FOR VALUES, found: SERVER",
12946+
),
12947+
(
12948+
"CREATE FOREIGN TABLE part (id INT) PARTITION OF root DEFAULT SERVER s",
12949+
"Expected: SERVER, found: PARTITION",
12950+
),
12951+
(
12952+
"CREATE FOREIGN TABLE part PARTITION OF root (id INT) DEFAULT SERVER s",
12953+
"Expected: ',' or ')' after column definition, found: INT",
12954+
),
12955+
(
12956+
"CREATE FOREIGN TABLE part (id) SERVER s",
12957+
"Expected: a data type name, found: )",
12958+
),
12959+
] {
12960+
assert_eq!(
12961+
pg_and_generic()
12962+
.parse_sql_statements(sql)
12963+
.unwrap_err()
12964+
.to_string(),
12965+
format!("sql parser error: {expected}"),
12966+
"{sql}"
12967+
);
12968+
}
12969+
}
12970+
12971+
#[test]
12972+
#[cfg(feature = "visitor")]
12973+
fn visit_relations_create_foreign_table_partition_of() {
12974+
let stmts = pg()
12975+
.parse_sql_statements("CREATE FOREIGN TABLE part PARTITION OF public.root DEFAULT SERVER s")
12976+
.unwrap();
12977+
let mut relations = vec![];
12978+
let _ = sqlparser::ast::visit_relations(&stmts, |relation| {
12979+
relations.push(relation.to_string());
12980+
core::ops::ControlFlow::<()>::Continue(())
12981+
});
12982+
assert_eq!(relations, ["part", "public.root"]);
12983+
}
12984+
12985+
#[test]
12986+
fn display_create_foreign_table_partition_of_propagates_formatter_errors() {
12987+
use core::fmt::{self, Write};
12988+
12989+
struct RejectingWriter(&'static str);
12990+
12991+
impl Write for RejectingWriter {
12992+
fn write_str(&mut self, value: &str) -> fmt::Result {
12993+
if value == self.0 {
12994+
Err(fmt::Error)
12995+
} else {
12996+
Ok(())
12997+
}
12998+
}
12999+
}
13000+
13001+
let stmt = pg_and_generic().verified_stmt(
13002+
"CREATE FOREIGN TABLE part PARTITION OF root (id NOT NULL, CONSTRAINT positive CHECK (id > 0)) FOR VALUES IN (1) SERVER s",
13003+
);
13004+
for rejected in [
13005+
"CREATE FOREIGN TABLE ",
13006+
" PARTITION OF ",
13007+
" (",
13008+
", ",
13009+
")",
13010+
"FOR VALUES IN (",
13011+
" SERVER ",
13012+
] {
13013+
let mut writer = RejectingWriter(rejected);
13014+
assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}");
13015+
}
13016+
}

0 commit comments

Comments
 (0)