Add PostgreSQL PARTITION OF syntax support (#2042).

This commit is contained in:
Filipe Guerreiro 2025-12-08 15:02:57 +09:00
parent 355a3bfd90
commit 8369e81ea4
9 changed files with 390 additions and 9 deletions

View file

@ -2697,6 +2697,14 @@ pub struct CreateTable {
/// <https://www.postgresql.org/docs/current/ddl-inherit.html>
/// <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-INHERITS>
pub inherits: Option<Vec<ObjectName>>,
/// PostgreSQL `PARTITION OF` clause to create a partition of a parent table.
/// Contains the parent table name.
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
pub partition_of: Option<ObjectName>,
/// PostgreSQL partition bound specification for PARTITION OF.
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
pub for_values: Option<ForValues>,
/// SQLite "STRICT" clause.
/// if the "STRICT" table-option keyword is added to the end, after the closing ")",
/// then strict typing rules apply to that table.
@ -2792,6 +2800,9 @@ impl fmt::Display for CreateTable {
dynamic = if self.dynamic { "DYNAMIC " } else { "" },
name = self.name,
)?;
if let Some(partition_of) = &self.partition_of {
write!(f, " PARTITION OF {partition_of}")?;
}
if let Some(on_cluster) = &self.on_cluster {
write!(f, " ON CLUSTER {on_cluster}")?;
}
@ -2806,12 +2817,19 @@ impl fmt::Display for CreateTable {
Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?;
NewLine.fmt(f)?;
f.write_str(")")?;
} else if self.query.is_none() && self.like.is_none() && self.clone.is_none() {
} else if self.query.is_none()
&& self.like.is_none()
&& self.clone.is_none()
&& self.partition_of.is_none()
{
// PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens
f.write_str(" ()")?;
} else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like {
write!(f, " ({like_in_columns_list})")?;
}
if let Some(for_values) = &self.for_values {
write!(f, " {for_values}")?;
}
// Hive table comment should be after column definitions, please refer to:
// [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable)
@ -3053,6 +3071,76 @@ impl fmt::Display for CreateTable {
}
}
/// PostgreSQL partition bound specification for PARTITION OF.
///
/// Specifies partition bounds for a child partition table.
///
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.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 ForValues {
/// `FOR VALUES IN (expr, ...)`
In(Vec<Expr>),
/// `FOR VALUES FROM (expr|MINVALUE|MAXVALUE, ...) TO (expr|MINVALUE|MAXVALUE, ...)`
From {
from: Vec<PartitionBoundValue>,
to: Vec<PartitionBoundValue>,
},
/// `FOR VALUES WITH (MODULUS n, REMAINDER r)`
With { modulus: u64, remainder: u64 },
/// `DEFAULT`
Default,
}
impl fmt::Display for ForValues {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ForValues::In(values) => {
write!(f, "FOR VALUES IN ({})", display_comma_separated(values))
}
ForValues::From { from, to } => {
write!(
f,
"FOR VALUES FROM ({}) TO ({})",
display_comma_separated(from),
display_comma_separated(to)
)
}
ForValues::With { modulus, remainder } => {
write!(
f,
"FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})"
)
}
ForValues::Default => write!(f, "DEFAULT"),
}
}
}
/// A value in a partition bound specification.
///
/// Used in RANGE partition bounds where values can be expressions,
/// MINVALUE (negative infinity), or MAXVALUE (positive infinity).
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum PartitionBoundValue {
Expr(Expr),
MinValue,
MaxValue,
}
impl fmt::Display for PartitionBoundValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PartitionBoundValue::Expr(expr) => write!(f, "{expr}"),
PartitionBoundValue::MinValue => write!(f, "MINVALUE"),
PartitionBoundValue::MaxValue => write!(f, "MAXVALUE"),
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

View file

@ -26,8 +26,8 @@ use sqlparser_derive::{Visit, VisitMut};
use crate::ast::{
ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr,
FileFormat, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit,
OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement,
FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName,
OnCommit, OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement,
StorageSerializationPolicy, TableConstraint, TableVersion, Tag, WrappedCollection,
};
@ -94,6 +94,8 @@ pub struct CreateTableBuilder {
pub cluster_by: Option<WrappedCollection<Vec<Expr>>>,
pub clustered_by: Option<ClusteredBy>,
pub inherits: Option<Vec<ObjectName>>,
pub partition_of: Option<ObjectName>,
pub for_values: Option<ForValues>,
pub strict: bool,
pub copy_grants: bool,
pub enable_schema_evolution: Option<bool>,
@ -150,6 +152,8 @@ impl CreateTableBuilder {
cluster_by: None,
clustered_by: None,
inherits: None,
partition_of: None,
for_values: None,
strict: false,
copy_grants: false,
enable_schema_evolution: None,
@ -317,6 +321,16 @@ impl CreateTableBuilder {
self
}
pub fn partition_of(mut self, partition_of: Option<ObjectName>) -> Self {
self.partition_of = partition_of;
self
}
pub fn for_values(mut self, for_values: Option<ForValues>) -> Self {
self.for_values = for_values;
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
@ -463,6 +477,8 @@ impl CreateTableBuilder {
cluster_by: self.cluster_by,
clustered_by: self.clustered_by,
inherits: self.inherits,
partition_of: self.partition_of,
for_values: self.for_values,
strict: self.strict,
copy_grants: self.copy_grants,
enable_schema_evolution: self.enable_schema_evolution,
@ -527,6 +543,8 @@ impl TryFrom<Statement> for CreateTableBuilder {
cluster_by,
clustered_by,
inherits,
partition_of,
for_values,
strict,
copy_grants,
enable_schema_evolution,
@ -577,6 +595,8 @@ impl TryFrom<Statement> for CreateTableBuilder {
cluster_by,
clustered_by,
inherits,
partition_of,
for_values,
strict,
iceberg,
copy_grants,

View file

@ -69,12 +69,13 @@ pub use self::ddl::{
CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass,
CreateOperatorFamily, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial,
DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily,
DropOperatorSignature, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters,
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption,
OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem,
OperatorOption, OperatorPurpose, Owner, Partition, ProcedureParam, ReferentialAction,
RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate,
DropOperatorSignature, DropTrigger, ForValues, 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, TagsColumnOption,
TriggerObjectKind, Truncate,
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
UserDefinedTypeStorage, ViewColumnDef,

View file

@ -554,6 +554,8 @@ impl Spanned for CreateTable {
cluster_by: _, // todo, BigQuery specific
clustered_by: _, // todo, Hive specific
inherits: _, // todo, PostgreSQL specific
partition_of: _, // todo, PostgreSQL specific
for_values: _, // todo, PostgreSQL specific
strict: _, // bool
copy_grants: _, // bool
enable_schema_evolution: _, // bool

View file

@ -637,6 +637,7 @@ define_keywords!(
MODIFIES,
MODIFY,
MODULE,
MODULUS,
MONITOR,
MONTH,
MONTHS,
@ -837,6 +838,7 @@ define_keywords!(
RELAY,
RELEASE,
RELEASES,
REMAINDER,
REMOTE,
REMOVE,
REMOVEQUOTES,

View file

@ -7887,6 +7887,15 @@ impl<'a> Parser<'a> {
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let table_name = self.parse_object_name(allow_unquoted_hyphen)?;
// PostgreSQL PARTITION OF for child partition tables
let partition_of = if dialect_of!(self is PostgreSqlDialect | GenericDialect)
&& self.parse_keywords(&[Keyword::PARTITION, Keyword::OF])
{
Some(self.parse_object_name(allow_unquoted_hyphen)?)
} else {
None
};
// Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs
let on_cluster = self.parse_optional_on_cluster()?;
@ -7911,6 +7920,13 @@ impl<'a> Parser<'a> {
None
};
// PostgreSQL PARTITION OF: partition bound specification
let for_values = if partition_of.is_some() {
Some(self.parse_partition_for_values()?)
} else {
None
};
// SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE`
let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]);
@ -7988,6 +8004,8 @@ impl<'a> Parser<'a> {
.partition_by(create_table_config.partition_by)
.cluster_by(create_table_config.cluster_by)
.inherits(create_table_config.inherits)
.partition_of(partition_of)
.for_values(for_values)
.table_options(create_table_config.table_options)
.primary_key(primary_key)
.strict(strict)
@ -8047,6 +8065,60 @@ impl<'a> Parser<'a> {
}
}
/// Parse PostgreSQL partition bound specification for PARTITION OF.
///
/// Parses: `FOR VALUES partition_bound_spec | DEFAULT`
///
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html)
fn parse_partition_for_values(&mut self) -> Result<ForValues, ParserError> {
if self.parse_keyword(Keyword::DEFAULT) {
return Ok(ForValues::Default);
}
self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?;
if self.parse_keyword(Keyword::IN) {
// FOR VALUES IN (expr, ...)
self.expect_token(&Token::LParen)?;
let values = self.parse_comma_separated(Parser::parse_expr)?;
self.expect_token(&Token::RParen)?;
Ok(ForValues::In(values))
} else if self.parse_keyword(Keyword::FROM) {
// FOR VALUES FROM (...) TO (...)
self.expect_token(&Token::LParen)?;
let from = self.parse_comma_separated(Parser::parse_partition_bound_value)?;
self.expect_token(&Token::RParen)?;
self.expect_keyword(Keyword::TO)?;
self.expect_token(&Token::LParen)?;
let to = self.parse_comma_separated(Parser::parse_partition_bound_value)?;
self.expect_token(&Token::RParen)?;
Ok(ForValues::From { from, to })
} else if self.parse_keyword(Keyword::WITH) {
// FOR VALUES WITH (MODULUS n, REMAINDER r)
self.expect_token(&Token::LParen)?;
self.expect_keyword(Keyword::MODULUS)?;
let modulus = self.parse_literal_uint()?;
self.expect_token(&Token::Comma)?;
self.expect_keyword(Keyword::REMAINDER)?;
let remainder = self.parse_literal_uint()?;
self.expect_token(&Token::RParen)?;
Ok(ForValues::With { modulus, remainder })
} else {
self.expected("IN, FROM, or WITH after FOR VALUES", self.peek_token())
}
}
/// Parse a single partition bound value (MINVALUE, MAXVALUE, or expression).
fn parse_partition_bound_value(&mut self) -> Result<PartitionBoundValue, ParserError> {
if self.parse_keyword(Keyword::MINVALUE) {
Ok(PartitionBoundValue::MinValue)
} else if self.parse_keyword(Keyword::MAXVALUE) {
Ok(PartitionBoundValue::MaxValue)
} else {
Ok(PartitionBoundValue::Expr(self.parse_expr()?))
}
}
/// Parse configuration like inheritance, partitioning, clustering information during the table creation.
///
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2)

View file

@ -755,6 +755,8 @@ fn test_duckdb_union_datatype() {
cluster_by: Default::default(),
clustered_by: Default::default(),
inherits: Default::default(),
partition_of: Default::default(),
for_values: Default::default(),
strict: Default::default(),
copy_grants: Default::default(),
enable_schema_evolution: Default::default(),

View file

@ -1897,6 +1897,8 @@ fn parse_create_table_with_valid_options() {
cluster_by: None,
clustered_by: None,
inherits: None,
partition_of: None,
for_values: None,
strict: false,
iceberg: false,
copy_grants: false,
@ -2064,6 +2066,8 @@ fn parse_create_table_with_identity_column() {
cluster_by: None,
clustered_by: None,
inherits: None,
partition_of: None,
for_values: None,
strict: false,
copy_grants: false,
enable_schema_evolution: None,

View file

@ -6130,6 +6130,8 @@ fn parse_trigger_related_functions() {
cluster_by: None,
clustered_by: None,
inherits: None,
partition_of: None,
for_values: None,
strict: false,
copy_grants: false,
enable_schema_evolution: None,
@ -7914,3 +7916,191 @@ fn parse_create_operator_class() {
)
.is_err());
}
#[test]
fn parse_create_table_partition_of_range() {
// RANGE partition with FROM ... TO
let sql = "CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("measurement_y2006m02", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("measurement")])),
create_table.partition_of
);
match create_table.for_values {
Some(ForValues::From { from, to }) => {
assert_eq!(1, from.len());
assert_eq!(1, to.len());
match &from[0] {
PartitionBoundValue::Expr(Expr::Value(v)) => {
assert_eq!("'2006-02-01'", v.to_string());
}
_ => panic!("Expected Expr value in from"),
}
match &to[0] {
PartitionBoundValue::Expr(Expr::Value(v)) => {
assert_eq!("'2006-03-01'", v.to_string());
}
_ => panic!("Expected Expr value in to"),
}
}
_ => panic!("Expected ForValues::From"),
}
}
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_range_with_minvalue_maxvalue() {
// RANGE partition with MINVALUE/MAXVALUE
let sql =
"CREATE TABLE orders_old PARTITION OF orders FOR VALUES FROM (MINVALUE) TO ('2020-01-01')";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("orders_old", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("orders")])),
create_table.partition_of
);
match create_table.for_values {
Some(ForValues::From { from, to }) => {
assert_eq!(PartitionBoundValue::MinValue, from[0]);
match &to[0] {
PartitionBoundValue::Expr(Expr::Value(v)) => {
assert_eq!("'2020-01-01'", v.to_string());
}
_ => panic!("Expected Expr value in to"),
}
}
_ => panic!("Expected ForValues::From"),
}
}
_ => panic!("Expected CreateTable"),
}
// With MAXVALUE
let sql =
"CREATE TABLE orders_new PARTITION OF orders FOR VALUES FROM ('2024-01-01') TO (MAXVALUE)";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => match create_table.for_values {
Some(ForValues::From { from, to }) => {
match &from[0] {
PartitionBoundValue::Expr(Expr::Value(v)) => {
assert_eq!("'2024-01-01'", v.to_string());
}
_ => panic!("Expected Expr value in from"),
}
assert_eq!(PartitionBoundValue::MaxValue, to[0]);
}
_ => panic!("Expected ForValues::From"),
},
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_list() {
// LIST partition
let sql = "CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US', 'CA', 'MX')";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("orders_us", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("orders")])),
create_table.partition_of
);
match create_table.for_values {
Some(ForValues::In(values)) => {
assert_eq!(3, values.len());
}
_ => panic!("Expected ForValues::In"),
}
}
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_hash() {
// HASH partition
let sql = "CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0)";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("orders_p0", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("orders")])),
create_table.partition_of
);
match create_table.for_values {
Some(ForValues::With { modulus, remainder }) => {
assert_eq!(4, modulus);
assert_eq!(0, remainder);
}
_ => panic!("Expected ForValues::With"),
}
}
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_default() {
// DEFAULT partition
let sql = "CREATE TABLE orders_default PARTITION OF orders DEFAULT";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("orders_default", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("orders")])),
create_table.partition_of
);
assert_eq!(Some(ForValues::Default), create_table.for_values);
}
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_multicolumn_range() {
// Multi-column RANGE partition
let sql = "CREATE TABLE sales_2023_q1 PARTITION OF sales FOR VALUES FROM ('2023-01-01', 1) TO ('2023-04-01', 1)";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("sales_2023_q1", create_table.name.to_string());
match create_table.for_values {
Some(ForValues::From { from, to }) => {
assert_eq!(2, from.len());
assert_eq!(2, to.len());
}
_ => panic!("Expected ForValues::From"),
}
}
_ => panic!("Expected CreateTable"),
}
}
#[test]
fn parse_create_table_partition_of_with_constraint() {
// With table constraint (not column constraint which has different syntax in PARTITION OF)
let sql = "CREATE TABLE orders_2023 PARTITION OF orders (\
CONSTRAINT check_date CHECK (order_date >= '2023-01-01')\
) FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')";
match pg_and_generic().verified_stmt(sql) {
Statement::CreateTable(create_table) => {
assert_eq!("orders_2023", create_table.name.to_string());
assert_eq!(
Some(ObjectName::from(vec![Ident::new("orders")])),
create_table.partition_of
);
// Check that table constraint was parsed
assert_eq!(1, create_table.constraints.len());
match create_table.for_values {
Some(ForValues::From { .. }) => {}
_ => panic!("Expected ForValues::From"),
}
}
_ => panic!("Expected CreateTable"),
}
}