Skip to content

Commit fae2648

Browse files
committed
Add PostgreSQL PARTITION OF syntax support (#2042).
1 parent 0cf85d3 commit fae2648

9 files changed

Lines changed: 390 additions & 9 deletions

File tree

src/ast/ddl.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2697,6 +2697,14 @@ pub struct CreateTable {
26972697
/// <https://www.postgresql.org/docs/current/ddl-inherit.html>
26982698
/// <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-INHERITS>
26992699
pub inherits: Option<Vec<ObjectName>>,
2700+
/// PostgreSQL `PARTITION OF` clause to create a partition of a parent table.
2701+
/// Contains the parent table name.
2702+
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
2703+
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
2704+
pub partition_of: Option<ObjectName>,
2705+
/// PostgreSQL partition bound specification for PARTITION OF.
2706+
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
2707+
pub for_values: Option<ForValues>,
27002708
/// SQLite "STRICT" clause.
27012709
/// if the "STRICT" table-option keyword is added to the end, after the closing ")",
27022710
/// then strict typing rules apply to that table.
@@ -2792,6 +2800,9 @@ impl fmt::Display for CreateTable {
27922800
dynamic = if self.dynamic { "DYNAMIC " } else { "" },
27932801
name = self.name,
27942802
)?;
2803+
if let Some(partition_of) = &self.partition_of {
2804+
write!(f, " PARTITION OF {partition_of}")?;
2805+
}
27952806
if let Some(on_cluster) = &self.on_cluster {
27962807
write!(f, " ON CLUSTER {on_cluster}")?;
27972808
}
@@ -2806,12 +2817,19 @@ impl fmt::Display for CreateTable {
28062817
Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?;
28072818
NewLine.fmt(f)?;
28082819
f.write_str(")")?;
2809-
} else if self.query.is_none() && self.like.is_none() && self.clone.is_none() {
2820+
} else if self.query.is_none()
2821+
&& self.like.is_none()
2822+
&& self.clone.is_none()
2823+
&& self.partition_of.is_none()
2824+
{
28102825
// PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens
28112826
f.write_str(" ()")?;
28122827
} else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like {
28132828
write!(f, " ({like_in_columns_list})")?;
28142829
}
2830+
if let Some(for_values) = &self.for_values {
2831+
write!(f, " {for_values}")?;
2832+
}
28152833

28162834
// Hive table comment should be after column definitions, please refer to:
28172835
// [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable)
@@ -3053,6 +3071,76 @@ impl fmt::Display for CreateTable {
30533071
}
30543072
}
30553073

3074+
/// PostgreSQL partition bound specification for PARTITION OF.
3075+
///
3076+
/// Specifies partition bounds for a child partition table.
3077+
///
3078+
/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html)
3079+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3080+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3081+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3082+
pub enum ForValues {
3083+
/// `FOR VALUES IN (expr, ...)`
3084+
In(Vec<Expr>),
3085+
/// `FOR VALUES FROM (expr|MINVALUE|MAXVALUE, ...) TO (expr|MINVALUE|MAXVALUE, ...)`
3086+
From {
3087+
from: Vec<PartitionBoundValue>,
3088+
to: Vec<PartitionBoundValue>,
3089+
},
3090+
/// `FOR VALUES WITH (MODULUS n, REMAINDER r)`
3091+
With { modulus: u64, remainder: u64 },
3092+
/// `DEFAULT`
3093+
Default,
3094+
}
3095+
3096+
impl fmt::Display for ForValues {
3097+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3098+
match self {
3099+
ForValues::In(values) => {
3100+
write!(f, "FOR VALUES IN ({})", display_comma_separated(values))
3101+
}
3102+
ForValues::From { from, to } => {
3103+
write!(
3104+
f,
3105+
"FOR VALUES FROM ({}) TO ({})",
3106+
display_comma_separated(from),
3107+
display_comma_separated(to)
3108+
)
3109+
}
3110+
ForValues::With { modulus, remainder } => {
3111+
write!(
3112+
f,
3113+
"FOR VALUES WITH (MODULUS {modulus}, REMAINDER {remainder})"
3114+
)
3115+
}
3116+
ForValues::Default => write!(f, "DEFAULT"),
3117+
}
3118+
}
3119+
}
3120+
3121+
/// A value in a partition bound specification.
3122+
///
3123+
/// Used in RANGE partition bounds where values can be expressions,
3124+
/// MINVALUE (negative infinity), or MAXVALUE (positive infinity).
3125+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3126+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3127+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3128+
pub enum PartitionBoundValue {
3129+
Expr(Expr),
3130+
MinValue,
3131+
MaxValue,
3132+
}
3133+
3134+
impl fmt::Display for PartitionBoundValue {
3135+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3136+
match self {
3137+
PartitionBoundValue::Expr(expr) => write!(f, "{expr}"),
3138+
PartitionBoundValue::MinValue => write!(f, "MINVALUE"),
3139+
PartitionBoundValue::MaxValue => write!(f, "MAXVALUE"),
3140+
}
3141+
}
3142+
}
3143+
30563144
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
30573145
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30583146
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/helpers/stmt_create_table.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use sqlparser_derive::{Visit, VisitMut};
2626

2727
use crate::ast::{
2828
ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr,
29-
FileFormat, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit,
30-
OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement,
29+
FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName,
30+
OnCommit, OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement,
3131
StorageSerializationPolicy, TableConstraint, TableVersion, Tag, WrappedCollection,
3232
};
3333

@@ -94,6 +94,8 @@ pub struct CreateTableBuilder {
9494
pub cluster_by: Option<WrappedCollection<Vec<Expr>>>,
9595
pub clustered_by: Option<ClusteredBy>,
9696
pub inherits: Option<Vec<ObjectName>>,
97+
pub partition_of: Option<ObjectName>,
98+
pub for_values: Option<ForValues>,
9799
pub strict: bool,
98100
pub copy_grants: bool,
99101
pub enable_schema_evolution: Option<bool>,
@@ -150,6 +152,8 @@ impl CreateTableBuilder {
150152
cluster_by: None,
151153
clustered_by: None,
152154
inherits: None,
155+
partition_of: None,
156+
for_values: None,
153157
strict: false,
154158
copy_grants: false,
155159
enable_schema_evolution: None,
@@ -317,6 +321,16 @@ impl CreateTableBuilder {
317321
self
318322
}
319323

324+
pub fn partition_of(mut self, partition_of: Option<ObjectName>) -> Self {
325+
self.partition_of = partition_of;
326+
self
327+
}
328+
329+
pub fn for_values(mut self, for_values: Option<ForValues>) -> Self {
330+
self.for_values = for_values;
331+
self
332+
}
333+
320334
pub fn strict(mut self, strict: bool) -> Self {
321335
self.strict = strict;
322336
self
@@ -463,6 +477,8 @@ impl CreateTableBuilder {
463477
cluster_by: self.cluster_by,
464478
clustered_by: self.clustered_by,
465479
inherits: self.inherits,
480+
partition_of: self.partition_of,
481+
for_values: self.for_values,
466482
strict: self.strict,
467483
copy_grants: self.copy_grants,
468484
enable_schema_evolution: self.enable_schema_evolution,
@@ -527,6 +543,8 @@ impl TryFrom<Statement> for CreateTableBuilder {
527543
cluster_by,
528544
clustered_by,
529545
inherits,
546+
partition_of,
547+
for_values,
530548
strict,
531549
copy_grants,
532550
enable_schema_evolution,
@@ -577,6 +595,8 @@ impl TryFrom<Statement> for CreateTableBuilder {
577595
cluster_by,
578596
clustered_by,
579597
inherits,
598+
partition_of,
599+
for_values,
580600
strict,
581601
iceberg,
582602
copy_grants,

src/ast/mod.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,13 @@ pub use self::ddl::{
6969
CreateExtension, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass,
7070
CreateOperatorFamily, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial,
7171
DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily,
72-
DropOperatorSignature, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters,
73-
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
74-
IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption,
75-
OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem,
76-
OperatorOption, OperatorPurpose, Owner, Partition, ProcedureParam, ReferentialAction,
77-
RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate,
72+
DropOperatorSignature, DropTrigger, ForValues, GeneratedAs, GeneratedExpressionMode,
73+
IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
74+
IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck,
75+
NullsDistinctOption, OperatorArgTypes, OperatorClassItem, OperatorFamilyDropItem,
76+
OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, PartitionBoundValue,
77+
ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption,
78+
TriggerObjectKind, Truncate,
7879
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
7980
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
8081
UserDefinedTypeStorage, ViewColumnDef,

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,8 @@ impl Spanned for CreateTable {
554554
cluster_by: _, // todo, BigQuery specific
555555
clustered_by: _, // todo, Hive specific
556556
inherits: _, // todo, PostgreSQL specific
557+
partition_of: _, // todo, PostgreSQL specific
558+
for_values: _, // todo, PostgreSQL specific
557559
strict: _, // bool
558560
copy_grants: _, // bool
559561
enable_schema_evolution: _, // bool

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ define_keywords!(
637637
MODIFIES,
638638
MODIFY,
639639
MODULE,
640+
MODULUS,
640641
MONITOR,
641642
MONTH,
642643
MONTHS,
@@ -837,6 +838,7 @@ define_keywords!(
837838
RELAY,
838839
RELEASE,
839840
RELEASES,
841+
REMAINDER,
840842
REMOTE,
841843
REMOVE,
842844
REMOVEQUOTES,

src/parser/mod.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7887,6 +7887,15 @@ impl<'a> Parser<'a> {
78877887
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
78887888
let table_name = self.parse_object_name(allow_unquoted_hyphen)?;
78897889

7890+
// PostgreSQL PARTITION OF for child partition tables
7891+
let partition_of = if dialect_of!(self is PostgreSqlDialect | GenericDialect)
7892+
&& self.parse_keywords(&[Keyword::PARTITION, Keyword::OF])
7893+
{
7894+
Some(self.parse_object_name(allow_unquoted_hyphen)?)
7895+
} else {
7896+
None
7897+
};
7898+
78907899
// Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs
78917900
let on_cluster = self.parse_optional_on_cluster()?;
78927901

@@ -7911,6 +7920,13 @@ impl<'a> Parser<'a> {
79117920
None
79127921
};
79137922

7923+
// PostgreSQL PARTITION OF: partition bound specification
7924+
let for_values = if partition_of.is_some() {
7925+
Some(self.parse_partition_for_values()?)
7926+
} else {
7927+
None
7928+
};
7929+
79147930
// SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE`
79157931
let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]);
79167932

@@ -7988,6 +8004,8 @@ impl<'a> Parser<'a> {
79888004
.partition_by(create_table_config.partition_by)
79898005
.cluster_by(create_table_config.cluster_by)
79908006
.inherits(create_table_config.inherits)
8007+
.partition_of(partition_of)
8008+
.for_values(for_values)
79918009
.table_options(create_table_config.table_options)
79928010
.primary_key(primary_key)
79938011
.strict(strict)
@@ -8047,6 +8065,60 @@ impl<'a> Parser<'a> {
80478065
}
80488066
}
80498067

8068+
/// Parse PostgreSQL partition bound specification for PARTITION OF.
8069+
///
8070+
/// Parses: `FOR VALUES partition_bound_spec | DEFAULT`
8071+
///
8072+
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html)
8073+
fn parse_partition_for_values(&mut self) -> Result<ForValues, ParserError> {
8074+
if self.parse_keyword(Keyword::DEFAULT) {
8075+
return Ok(ForValues::Default);
8076+
}
8077+
8078+
self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?;
8079+
8080+
if self.parse_keyword(Keyword::IN) {
8081+
// FOR VALUES IN (expr, ...)
8082+
self.expect_token(&Token::LParen)?;
8083+
let values = self.parse_comma_separated(Parser::parse_expr)?;
8084+
self.expect_token(&Token::RParen)?;
8085+
Ok(ForValues::In(values))
8086+
} else if self.parse_keyword(Keyword::FROM) {
8087+
// FOR VALUES FROM (...) TO (...)
8088+
self.expect_token(&Token::LParen)?;
8089+
let from = self.parse_comma_separated(Parser::parse_partition_bound_value)?;
8090+
self.expect_token(&Token::RParen)?;
8091+
self.expect_keyword(Keyword::TO)?;
8092+
self.expect_token(&Token::LParen)?;
8093+
let to = self.parse_comma_separated(Parser::parse_partition_bound_value)?;
8094+
self.expect_token(&Token::RParen)?;
8095+
Ok(ForValues::From { from, to })
8096+
} else if self.parse_keyword(Keyword::WITH) {
8097+
// FOR VALUES WITH (MODULUS n, REMAINDER r)
8098+
self.expect_token(&Token::LParen)?;
8099+
self.expect_keyword(Keyword::MODULUS)?;
8100+
let modulus = self.parse_literal_uint()?;
8101+
self.expect_token(&Token::Comma)?;
8102+
self.expect_keyword(Keyword::REMAINDER)?;
8103+
let remainder = self.parse_literal_uint()?;
8104+
self.expect_token(&Token::RParen)?;
8105+
Ok(ForValues::With { modulus, remainder })
8106+
} else {
8107+
self.expected("IN, FROM, or WITH after FOR VALUES", self.peek_token())
8108+
}
8109+
}
8110+
8111+
/// Parse a single partition bound value (MINVALUE, MAXVALUE, or expression).
8112+
fn parse_partition_bound_value(&mut self) -> Result<PartitionBoundValue, ParserError> {
8113+
if self.parse_keyword(Keyword::MINVALUE) {
8114+
Ok(PartitionBoundValue::MinValue)
8115+
} else if self.parse_keyword(Keyword::MAXVALUE) {
8116+
Ok(PartitionBoundValue::MaxValue)
8117+
} else {
8118+
Ok(PartitionBoundValue::Expr(self.parse_expr()?))
8119+
}
8120+
}
8121+
80508122
/// Parse configuration like inheritance, partitioning, clustering information during the table creation.
80518123
///
80528124
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#syntax_2)

tests/sqlparser_duckdb.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,8 @@ fn test_duckdb_union_datatype() {
755755
cluster_by: Default::default(),
756756
clustered_by: Default::default(),
757757
inherits: Default::default(),
758+
partition_of: Default::default(),
759+
for_values: Default::default(),
758760
strict: Default::default(),
759761
copy_grants: Default::default(),
760762
enable_schema_evolution: Default::default(),

tests/sqlparser_mssql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,8 @@ fn parse_create_table_with_valid_options() {
18971897
cluster_by: None,
18981898
clustered_by: None,
18991899
inherits: None,
1900+
partition_of: None,
1901+
for_values: None,
19001902
strict: false,
19011903
iceberg: false,
19021904
copy_grants: false,
@@ -2064,6 +2066,8 @@ fn parse_create_table_with_identity_column() {
20642066
cluster_by: None,
20652067
clustered_by: None,
20662068
inherits: None,
2069+
partition_of: None,
2070+
for_values: None,
20672071
strict: false,
20682072
copy_grants: false,
20692073
enable_schema_evolution: None,

0 commit comments

Comments
 (0)