Postgres: Support INTERVAL data type options (#1984)
Some checks failed
Rust / compile (push) Has been cancelled
license / Release Audit Tool (RAT) (push) Has been cancelled
Rust / codestyle (push) Has been cancelled
Rust / lint (push) Has been cancelled
Rust / benchmark-lint (push) Has been cancelled
Rust / docs (push) Has been cancelled
Rust / compile-no-std (push) Has been cancelled
Rust / test (beta) (push) Has been cancelled
Rust / test (nightly) (push) Has been cancelled
Rust / test (stable) (push) Has been cancelled

This commit is contained in:
Michael Victor Zink 2025-08-01 23:05:13 -07:00 committed by GitHub
parent dd650b88f3
commit c1648e79fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 239 additions and 13 deletions

View file

@ -346,7 +346,16 @@ pub enum DataType {
/// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type
TimestampNtz,
/// Interval type.
Interval,
Interval {
/// [PostgreSQL] fields specification like `INTERVAL YEAR TO MONTH`.
///
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html
fields: Option<IntervalFields>,
/// [PostgreSQL] subsecond precision like `INTERVAL HOUR TO SECOND(3)`
///
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html
precision: Option<u64>,
},
/// JSON type.
JSON,
/// Binary JSON type.
@ -635,7 +644,16 @@ impl fmt::Display for DataType {
timezone,
)
}
DataType::Interval => write!(f, "INTERVAL"),
DataType::Interval { fields, precision } => {
write!(f, "INTERVAL")?;
if let Some(fields) = fields {
write!(f, " {fields}")?;
}
if let Some(precision) = precision {
write!(f, "({precision})")?;
}
Ok(())
}
DataType::JSON => write!(f, "JSON"),
DataType::JSONB => write!(f, "JSONB"),
DataType::Regclass => write!(f, "REGCLASS"),
@ -889,6 +907,48 @@ impl fmt::Display for TimezoneInfo {
}
}
/// Fields for [Postgres] `INTERVAL` type.
///
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum IntervalFields {
Year,
Month,
Day,
Hour,
Minute,
Second,
YearToMonth,
DayToHour,
DayToMinute,
DayToSecond,
HourToMinute,
HourToSecond,
MinuteToSecond,
}
impl fmt::Display for IntervalFields {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IntervalFields::Year => write!(f, "YEAR"),
IntervalFields::Month => write!(f, "MONTH"),
IntervalFields::Day => write!(f, "DAY"),
IntervalFields::Hour => write!(f, "HOUR"),
IntervalFields::Minute => write!(f, "MINUTE"),
IntervalFields::Second => write!(f, "SECOND"),
IntervalFields::YearToMonth => write!(f, "YEAR TO MONTH"),
IntervalFields::DayToHour => write!(f, "DAY TO HOUR"),
IntervalFields::DayToMinute => write!(f, "DAY TO MINUTE"),
IntervalFields::DayToSecond => write!(f, "DAY TO SECOND"),
IntervalFields::HourToMinute => write!(f, "HOUR TO MINUTE"),
IntervalFields::HourToSecond => write!(f, "HOUR TO SECOND"),
IntervalFields::MinuteToSecond => write!(f, "MINUTE TO SECOND"),
}
}
}
/// Additional information for `NUMERIC`, `DECIMAL`, and `DEC` data types
/// following the 2016 [SQL Standard].
///

View file

@ -52,7 +52,7 @@ use crate::{
pub use self::data_type::{
ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember,
ExactNumberInfo, StructBracketKind, TimezoneInfo,
ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo,
};
pub use self::dcl::{
AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use,

View file

@ -187,4 +187,8 @@ impl Dialect for GenericDialect {
fn supports_data_type_signed_suffix(&self) -> bool {
true
}
fn supports_interval_options(&self) -> bool {
true
}
}

View file

@ -1148,6 +1148,21 @@ pub trait Dialect: Debug + Any {
fn supports_data_type_signed_suffix(&self) -> bool {
false
}
/// Returns true if the dialect supports the `INTERVAL` data type with [Postgres]-style options.
///
/// Examples:
/// ```sql
/// CREATE TABLE t (i INTERVAL YEAR TO MONTH);
/// SELECT '1 second'::INTERVAL HOUR TO SECOND(3);
/// ```
///
/// See [`crate::ast::DataType::Interval`] and [`crate::ast::IntervalFields`].
///
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
fn supports_interval_options(&self) -> bool {
false
}
}
/// This represents the operators for which precedence must be defined

View file

@ -269,4 +269,11 @@ impl Dialect for PostgreSqlDialect {
fn supports_notnull_operator(&self) -> bool {
true
}
/// [Postgres] supports optional field and precision options for `INTERVAL` data type.
///
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
fn supports_interval_options(&self) -> bool {
true
}
}

View file

@ -1534,7 +1534,7 @@ impl<'a> Parser<'a> {
let loc = self.peek_token_ref().span.start;
let opt_expr = self.maybe_parse(|parser| {
match parser.parse_data_type()? {
DataType::Interval => parser.parse_interval(),
DataType::Interval { .. } => parser.parse_interval(),
// PostgreSQL allows almost any identifier to be used as custom data type name,
// and we support that in `parse_data_type()`. But unlike Postgres we don't
// have a list of globally reserved keywords (since they vary across dialects),
@ -10066,10 +10066,18 @@ impl<'a> Parser<'a> {
self.parse_optional_precision()?,
TimezoneInfo::Tz,
)),
// Interval types can be followed by a complicated interval
// qualifier that we don't currently support. See
// parse_interval for a taste.
Keyword::INTERVAL => Ok(DataType::Interval),
Keyword::INTERVAL => {
if self.dialect.supports_interval_options() {
let fields = self.maybe_parse_optional_interval_fields()?;
let precision = self.parse_optional_precision()?;
Ok(DataType::Interval { fields, precision })
} else {
Ok(DataType::Interval {
fields: None,
precision: None,
})
}
}
Keyword::JSON => Ok(DataType::JSON),
Keyword::JSONB => Ok(DataType::JSONB),
Keyword::REGCLASS => Ok(DataType::Regclass),
@ -11038,6 +11046,85 @@ impl<'a> Parser<'a> {
}
}
fn maybe_parse_optional_interval_fields(
&mut self,
) -> Result<Option<IntervalFields>, ParserError> {
match self.parse_one_of_keywords(&[
// Can be followed by `TO` option
Keyword::YEAR,
Keyword::DAY,
Keyword::HOUR,
Keyword::MINUTE,
// No `TO` option
Keyword::MONTH,
Keyword::SECOND,
]) {
Some(Keyword::YEAR) => {
if self.peek_keyword(Keyword::TO) {
self.expect_keyword(Keyword::TO)?;
self.expect_keyword(Keyword::MONTH)?;
Ok(Some(IntervalFields::YearToMonth))
} else {
Ok(Some(IntervalFields::Year))
}
}
Some(Keyword::DAY) => {
if self.peek_keyword(Keyword::TO) {
self.expect_keyword(Keyword::TO)?;
match self.expect_one_of_keywords(&[
Keyword::HOUR,
Keyword::MINUTE,
Keyword::SECOND,
])? {
Keyword::HOUR => Ok(Some(IntervalFields::DayToHour)),
Keyword::MINUTE => Ok(Some(IntervalFields::DayToMinute)),
Keyword::SECOND => Ok(Some(IntervalFields::DayToSecond)),
_ => {
self.prev_token();
self.expected("HOUR, MINUTE, or SECOND", self.peek_token())
}
}
} else {
Ok(Some(IntervalFields::Day))
}
}
Some(Keyword::HOUR) => {
if self.peek_keyword(Keyword::TO) {
self.expect_keyword(Keyword::TO)?;
match self.expect_one_of_keywords(&[Keyword::MINUTE, Keyword::SECOND])? {
Keyword::MINUTE => Ok(Some(IntervalFields::HourToMinute)),
Keyword::SECOND => Ok(Some(IntervalFields::HourToSecond)),
_ => {
self.prev_token();
self.expected("MINUTE or SECOND", self.peek_token())
}
}
} else {
Ok(Some(IntervalFields::Hour))
}
}
Some(Keyword::MINUTE) => {
if self.peek_keyword(Keyword::TO) {
self.expect_keyword(Keyword::TO)?;
self.expect_keyword(Keyword::SECOND)?;
Ok(Some(IntervalFields::MinuteToSecond))
} else {
Ok(Some(IntervalFields::Minute))
}
}
Some(Keyword::MONTH) => Ok(Some(IntervalFields::Month)),
Some(Keyword::SECOND) => Ok(Some(IntervalFields::Second)),
Some(_) => {
self.prev_token();
self.expected(
"YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND",
self.peek_token(),
)
}
None => Ok(None),
}
}
/// Parse datetime64 [1]
/// Syntax
/// ```sql

View file

@ -961,7 +961,10 @@ fn parse_typed_struct_syntax_bigquery() {
})],
fields: vec![StructField {
field_name: None,
field_type: DataType::Interval,
field_type: DataType::Interval {
fields: None,
precision: None
},
options: None,
}]
},
@ -1300,7 +1303,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
})],
fields: vec![StructField {
field_name: None,
field_type: DataType::Interval,
field_type: DataType::Interval {
fields: None,
precision: None
},
options: None,
}]
},

View file

@ -12955,7 +12955,10 @@ fn test_extract_seconds_ok() {
expr: Box::new(Expr::Value(
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
)),
data_type: DataType::Interval,
data_type: DataType::Interval {
fields: None,
precision: None
},
format: None,
}),
}
@ -12980,7 +12983,10 @@ fn test_extract_seconds_ok() {
expr: Box::new(Expr::Value(
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(),
)),
data_type: DataType::Interval,
data_type: DataType::Interval {
fields: None,
precision: None,
},
format: None,
}),
})],
@ -13034,7 +13040,10 @@ fn test_extract_seconds_single_quote_ok() {
expr: Box::new(Expr::Value(
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
)),
data_type: DataType::Interval,
data_type: DataType::Interval {
fields: None,
precision: None
},
format: None,
}),
}

View file

@ -5332,6 +5332,44 @@ fn parse_at_time_zone() {
);
}
#[test]
fn parse_interval_data_type() {
pg_and_generic().verified_stmt("CREATE TABLE t (i INTERVAL)");
for p in 0..=6 {
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL({p}))"));
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL({p})"));
pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL({p}))"));
}
let fields = [
"YEAR",
"MONTH",
"DAY",
"HOUR",
"MINUTE",
"SECOND",
"YEAR TO MONTH",
"DAY TO HOUR",
"DAY TO MINUTE",
"DAY TO SECOND",
"HOUR TO MINUTE",
"HOUR TO SECOND",
"MINUTE TO SECOND",
];
for field in fields {
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field})"));
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}"));
pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field})"));
}
for p in 0..=6 {
for field in fields {
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field}({p}))"));
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}({p})"));
pg_and_generic()
.verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field}({p}))"));
}
}
}
#[test]
fn parse_create_table_with_options() {
let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)";