Skip to content

Commit 0a31ed0

Browse files
committed
Postgres: Support INTERVAL data type options
[Postgres] allows extra options for the `INTERVAL` data type; namely fields and subsecond precision. For example `'3 years 1 second'::interval year to month` casts the interval and strips the seconds, and `'1.3333 seconds'::interval(1)` returns `1.3` seconds. This is supported by adding two optional fields to `DataType::Interval`, along with a new `enum` for the allowed fields. Note that [MSSQL] also supports similar options, but with more complicated precision syntax, e.g. `INTERVAL HOUR(p) TO SECOND(q)`. This is not implemented in this commit because I don't have a way to test it. [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html [MSSQL]: https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/sql-data-types?view=sql-server-ver17
1 parent dd650b8 commit 0a31ed0

File tree

9 files changed

+239
-13
lines changed

9 files changed

+239
-13
lines changed

src/ast/data_type.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,16 @@ pub enum DataType {
346346
/// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type
347347
TimestampNtz,
348348
/// Interval type.
349-
Interval,
349+
Interval {
350+
/// [PostgreSQL] fields specification like `INTERVAL YEAR TO MONTH`.
351+
///
352+
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html
353+
fields: Option<IntervalFields>,
354+
/// [PostgreSQL] subsecond precision like `INTERVAL HOUR TO SECOND(3)`
355+
///
356+
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-datetime.html
357+
precision: Option<u64>,
358+
},
350359
/// JSON type.
351360
JSON,
352361
/// Binary JSON type.
@@ -635,7 +644,16 @@ impl fmt::Display for DataType {
635644
timezone,
636645
)
637646
}
638-
DataType::Interval => write!(f, "INTERVAL"),
647+
DataType::Interval { fields, precision } => {
648+
write!(f, "INTERVAL")?;
649+
if let Some(fields) = fields {
650+
write!(f, " {fields}")?;
651+
}
652+
if let Some(precision) = precision {
653+
write!(f, "({precision})")?;
654+
}
655+
Ok(())
656+
}
639657
DataType::JSON => write!(f, "JSON"),
640658
DataType::JSONB => write!(f, "JSONB"),
641659
DataType::Regclass => write!(f, "REGCLASS"),
@@ -889,6 +907,48 @@ impl fmt::Display for TimezoneInfo {
889907
}
890908
}
891909

910+
/// Fields for [Postgres] `INTERVAL` type.
911+
///
912+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
913+
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
914+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
915+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
916+
pub enum IntervalFields {
917+
Year,
918+
Month,
919+
Day,
920+
Hour,
921+
Minute,
922+
Second,
923+
YearToMonth,
924+
DayToHour,
925+
DayToMinute,
926+
DayToSecond,
927+
HourToMinute,
928+
HourToSecond,
929+
MinuteToSecond,
930+
}
931+
932+
impl fmt::Display for IntervalFields {
933+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934+
match self {
935+
IntervalFields::Year => write!(f, "YEAR"),
936+
IntervalFields::Month => write!(f, "MONTH"),
937+
IntervalFields::Day => write!(f, "DAY"),
938+
IntervalFields::Hour => write!(f, "HOUR"),
939+
IntervalFields::Minute => write!(f, "MINUTE"),
940+
IntervalFields::Second => write!(f, "SECOND"),
941+
IntervalFields::YearToMonth => write!(f, "YEAR TO MONTH"),
942+
IntervalFields::DayToHour => write!(f, "DAY TO HOUR"),
943+
IntervalFields::DayToMinute => write!(f, "DAY TO MINUTE"),
944+
IntervalFields::DayToSecond => write!(f, "DAY TO SECOND"),
945+
IntervalFields::HourToMinute => write!(f, "HOUR TO MINUTE"),
946+
IntervalFields::HourToSecond => write!(f, "HOUR TO SECOND"),
947+
IntervalFields::MinuteToSecond => write!(f, "MINUTE TO SECOND"),
948+
}
949+
}
950+
}
951+
892952
/// Additional information for `NUMERIC`, `DECIMAL`, and `DEC` data types
893953
/// following the 2016 [SQL Standard].
894954
///

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use crate::{
5252

5353
pub use self::data_type::{
5454
ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember,
55-
ExactNumberInfo, StructBracketKind, TimezoneInfo,
55+
ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo,
5656
};
5757
pub use self::dcl::{
5858
AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use,

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,8 @@ impl Dialect for GenericDialect {
187187
fn supports_data_type_signed_suffix(&self) -> bool {
188188
true
189189
}
190+
191+
fn supports_interval_options(&self) -> bool {
192+
true
193+
}
190194
}

src/dialect/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,21 @@ pub trait Dialect: Debug + Any {
11481148
fn supports_data_type_signed_suffix(&self) -> bool {
11491149
false
11501150
}
1151+
1152+
/// Returns true if the dialect supports the `INTERVAL` data type with [Postgres]-style options.
1153+
///
1154+
/// Examples:
1155+
/// ```sql
1156+
/// CREATE TABLE t (i INTERVAL YEAR TO MONTH);
1157+
/// SELECT '1 second'::INTERVAL HOUR TO SECOND(3);
1158+
/// ```
1159+
///
1160+
/// See [`crate::ast::DataType::Interval`] and [`crate::ast::IntervalFields`].
1161+
///
1162+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
1163+
fn supports_interval_options(&self) -> bool {
1164+
false
1165+
}
11511166
}
11521167

11531168
/// This represents the operators for which precedence must be defined

src/dialect/postgresql.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,11 @@ impl Dialect for PostgreSqlDialect {
269269
fn supports_notnull_operator(&self) -> bool {
270270
true
271271
}
272+
273+
/// [Postgres] supports optional field and precision options for `INTERVAL` data type.
274+
///
275+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
276+
fn supports_interval_options(&self) -> bool {
277+
true
278+
}
272279
}

src/parser/mod.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,7 +1534,7 @@ impl<'a> Parser<'a> {
15341534
let loc = self.peek_token_ref().span.start;
15351535
let opt_expr = self.maybe_parse(|parser| {
15361536
match parser.parse_data_type()? {
1537-
DataType::Interval => parser.parse_interval(),
1537+
DataType::Interval { .. } => parser.parse_interval(),
15381538
// PostgreSQL allows almost any identifier to be used as custom data type name,
15391539
// and we support that in `parse_data_type()`. But unlike Postgres we don't
15401540
// have a list of globally reserved keywords (since they vary across dialects),
@@ -10066,10 +10066,18 @@ impl<'a> Parser<'a> {
1006610066
self.parse_optional_precision()?,
1006710067
TimezoneInfo::Tz,
1006810068
)),
10069-
// Interval types can be followed by a complicated interval
10070-
// qualifier that we don't currently support. See
10071-
// parse_interval for a taste.
10072-
Keyword::INTERVAL => Ok(DataType::Interval),
10069+
Keyword::INTERVAL => {
10070+
if self.dialect.supports_interval_options() {
10071+
let fields = self.maybe_parse_optional_interval_fields()?;
10072+
let precision = self.parse_optional_precision()?;
10073+
Ok(DataType::Interval { fields, precision })
10074+
} else {
10075+
Ok(DataType::Interval {
10076+
fields: None,
10077+
precision: None,
10078+
})
10079+
}
10080+
}
1007310081
Keyword::JSON => Ok(DataType::JSON),
1007410082
Keyword::JSONB => Ok(DataType::JSONB),
1007510083
Keyword::REGCLASS => Ok(DataType::Regclass),
@@ -11038,6 +11046,85 @@ impl<'a> Parser<'a> {
1103811046
}
1103911047
}
1104011048

11049+
fn maybe_parse_optional_interval_fields(
11050+
&mut self,
11051+
) -> Result<Option<IntervalFields>, ParserError> {
11052+
match self.parse_one_of_keywords(&[
11053+
// Can be followed by `TO` option
11054+
Keyword::YEAR,
11055+
Keyword::DAY,
11056+
Keyword::HOUR,
11057+
Keyword::MINUTE,
11058+
// No `TO` option
11059+
Keyword::MONTH,
11060+
Keyword::SECOND,
11061+
]) {
11062+
Some(Keyword::YEAR) => {
11063+
if self.peek_keyword(Keyword::TO) {
11064+
self.expect_keyword(Keyword::TO)?;
11065+
self.expect_keyword(Keyword::MONTH)?;
11066+
Ok(Some(IntervalFields::YearToMonth))
11067+
} else {
11068+
Ok(Some(IntervalFields::Year))
11069+
}
11070+
}
11071+
Some(Keyword::DAY) => {
11072+
if self.peek_keyword(Keyword::TO) {
11073+
self.expect_keyword(Keyword::TO)?;
11074+
match self.expect_one_of_keywords(&[
11075+
Keyword::HOUR,
11076+
Keyword::MINUTE,
11077+
Keyword::SECOND,
11078+
])? {
11079+
Keyword::HOUR => Ok(Some(IntervalFields::DayToHour)),
11080+
Keyword::MINUTE => Ok(Some(IntervalFields::DayToMinute)),
11081+
Keyword::SECOND => Ok(Some(IntervalFields::DayToSecond)),
11082+
_ => {
11083+
self.prev_token();
11084+
self.expected("HOUR, MINUTE, or SECOND", self.peek_token())
11085+
}
11086+
}
11087+
} else {
11088+
Ok(Some(IntervalFields::Day))
11089+
}
11090+
}
11091+
Some(Keyword::HOUR) => {
11092+
if self.peek_keyword(Keyword::TO) {
11093+
self.expect_keyword(Keyword::TO)?;
11094+
match self.expect_one_of_keywords(&[Keyword::MINUTE, Keyword::SECOND])? {
11095+
Keyword::MINUTE => Ok(Some(IntervalFields::HourToMinute)),
11096+
Keyword::SECOND => Ok(Some(IntervalFields::HourToSecond)),
11097+
_ => {
11098+
self.prev_token();
11099+
self.expected("MINUTE or SECOND", self.peek_token())
11100+
}
11101+
}
11102+
} else {
11103+
Ok(Some(IntervalFields::Hour))
11104+
}
11105+
}
11106+
Some(Keyword::MINUTE) => {
11107+
if self.peek_keyword(Keyword::TO) {
11108+
self.expect_keyword(Keyword::TO)?;
11109+
self.expect_keyword(Keyword::SECOND)?;
11110+
Ok(Some(IntervalFields::MinuteToSecond))
11111+
} else {
11112+
Ok(Some(IntervalFields::Minute))
11113+
}
11114+
}
11115+
Some(Keyword::MONTH) => Ok(Some(IntervalFields::Month)),
11116+
Some(Keyword::SECOND) => Ok(Some(IntervalFields::Second)),
11117+
Some(_) => {
11118+
self.prev_token();
11119+
self.expected(
11120+
"YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND",
11121+
self.peek_token(),
11122+
)
11123+
}
11124+
None => Ok(None),
11125+
}
11126+
}
11127+
1104111128
/// Parse datetime64 [1]
1104211129
/// Syntax
1104311130
/// ```sql

tests/sqlparser_bigquery.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,10 @@ fn parse_typed_struct_syntax_bigquery() {
961961
})],
962962
fields: vec![StructField {
963963
field_name: None,
964-
field_type: DataType::Interval,
964+
field_type: DataType::Interval {
965+
fields: None,
966+
precision: None
967+
},
965968
options: None,
966969
}]
967970
},
@@ -1300,7 +1303,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
13001303
})],
13011304
fields: vec![StructField {
13021305
field_name: None,
1303-
field_type: DataType::Interval,
1306+
field_type: DataType::Interval {
1307+
fields: None,
1308+
precision: None
1309+
},
13041310
options: None,
13051311
}]
13061312
},

tests/sqlparser_common.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12955,7 +12955,10 @@ fn test_extract_seconds_ok() {
1295512955
expr: Box::new(Expr::Value(
1295612956
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
1295712957
)),
12958-
data_type: DataType::Interval,
12958+
data_type: DataType::Interval {
12959+
fields: None,
12960+
precision: None
12961+
},
1295912962
format: None,
1296012963
}),
1296112964
}
@@ -12980,7 +12983,10 @@ fn test_extract_seconds_ok() {
1298012983
expr: Box::new(Expr::Value(
1298112984
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(),
1298212985
)),
12983-
data_type: DataType::Interval,
12986+
data_type: DataType::Interval {
12987+
fields: None,
12988+
precision: None,
12989+
},
1298412990
format: None,
1298512991
}),
1298612992
})],
@@ -13034,7 +13040,10 @@ fn test_extract_seconds_single_quote_ok() {
1303413040
expr: Box::new(Expr::Value(
1303513041
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
1303613042
)),
13037-
data_type: DataType::Interval,
13043+
data_type: DataType::Interval {
13044+
fields: None,
13045+
precision: None
13046+
},
1303813047
format: None,
1303913048
}),
1304013049
}

tests/sqlparser_postgres.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5332,6 +5332,44 @@ fn parse_at_time_zone() {
53325332
);
53335333
}
53345334

5335+
#[test]
5336+
fn parse_interval_data_type() {
5337+
pg_and_generic().verified_stmt("CREATE TABLE t (i INTERVAL)");
5338+
for p in 0..=6 {
5339+
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL({p}))"));
5340+
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL({p})"));
5341+
pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL({p}))"));
5342+
}
5343+
let fields = [
5344+
"YEAR",
5345+
"MONTH",
5346+
"DAY",
5347+
"HOUR",
5348+
"MINUTE",
5349+
"SECOND",
5350+
"YEAR TO MONTH",
5351+
"DAY TO HOUR",
5352+
"DAY TO MINUTE",
5353+
"DAY TO SECOND",
5354+
"HOUR TO MINUTE",
5355+
"HOUR TO SECOND",
5356+
"MINUTE TO SECOND",
5357+
];
5358+
for field in fields {
5359+
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field})"));
5360+
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}"));
5361+
pg_and_generic().verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field})"));
5362+
}
5363+
for p in 0..=6 {
5364+
for field in fields {
5365+
pg_and_generic().verified_stmt(&format!("CREATE TABLE t (i INTERVAL {field}({p}))"));
5366+
pg_and_generic().verified_stmt(&format!("SELECT '1 second'::INTERVAL {field}({p})"));
5367+
pg_and_generic()
5368+
.verified_stmt(&format!("SELECT CAST('1 second' AS INTERVAL {field}({p}))"));
5369+
}
5370+
}
5371+
}
5372+
53355373
#[test]
53365374
fn parse_create_table_with_options() {
53375375
let sql = "CREATE TABLE t (c INT) WITH (foo = 'bar', a = 123)";

0 commit comments

Comments
 (0)