Skip to content

Commit 9d25c20

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 9127370 commit 9d25c20

File tree

9 files changed

+208
-10
lines changed

9 files changed

+208
-10
lines changed

src/ast/data_type.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,11 @@ 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+
///
350+
/// Optional [Postgres] field list and precision: `interval [ fields ] [ (p) ]`
351+
///
352+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
353+
Interval(Option<IntervalFields>, Option<u64>),
350354
/// JSON type.
351355
JSON,
352356
/// Binary JSON type.
@@ -635,7 +639,16 @@ impl fmt::Display for DataType {
635639
timezone,
636640
)
637641
}
638-
DataType::Interval => write!(f, "INTERVAL"),
642+
DataType::Interval(fields, precision) => {
643+
write!(f, "INTERVAL")?;
644+
if let Some(fields) = fields {
645+
write!(f, " {fields}")?;
646+
}
647+
if let Some(precision) = precision {
648+
write!(f, "({precision})")?;
649+
}
650+
Ok(())
651+
}
639652
DataType::JSON => write!(f, "JSON"),
640653
DataType::JSONB => write!(f, "JSONB"),
641654
DataType::Regclass => write!(f, "REGCLASS"),
@@ -889,6 +902,48 @@ impl fmt::Display for TimezoneInfo {
889902
}
890903
}
891904

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

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
@@ -183,4 +183,8 @@ impl Dialect for GenericDialect {
183183
fn supports_select_wildcard_exclude(&self) -> bool {
184184
true
185185
}
186+
187+
fn supports_interval_options(&self) -> bool {
188+
true
189+
}
186190
}

src/dialect/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,13 @@ pub trait Dialect: Debug + Any {
11361136
fn supports_notnull_operator(&self) -> bool {
11371137
false
11381138
}
1139+
1140+
/// Returns true if the dialect supports the `INTERVAL` data type with [Postgres]-style options.
1141+
///
1142+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
1143+
fn supports_interval_options(&self) -> bool {
1144+
false
1145+
}
11391146
}
11401147

11411148
/// 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: 89 additions & 2 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),
@@ -10026,7 +10026,15 @@ impl<'a> Parser<'a> {
1002610026
// Interval types can be followed by a complicated interval
1002710027
// qualifier that we don't currently support. See
1002810028
// parse_interval for a taste.
10029-
Keyword::INTERVAL => Ok(DataType::Interval),
10029+
Keyword::INTERVAL => {
10030+
if self.dialect.supports_interval_options() {
10031+
let fields = self.parse_optional_interval_fields()?;
10032+
let precision = self.parse_optional_precision()?;
10033+
Ok(DataType::Interval(fields, precision))
10034+
} else {
10035+
Ok(DataType::Interval(None, None))
10036+
}
10037+
}
1003010038
Keyword::JSON => Ok(DataType::JSON),
1003110039
Keyword::JSONB => Ok(DataType::JSONB),
1003210040
Keyword::REGCLASS => Ok(DataType::Regclass),
@@ -10995,6 +11003,85 @@ impl<'a> Parser<'a> {
1099511003
}
1099611004
}
1099711005

11006+
pub fn parse_optional_interval_fields(
11007+
&mut self,
11008+
) -> Result<Option<IntervalFields>, ParserError> {
11009+
match self.parse_one_of_keywords(&[
11010+
// Can be followed by `TO` option
11011+
Keyword::YEAR,
11012+
Keyword::DAY,
11013+
Keyword::HOUR,
11014+
Keyword::MINUTE,
11015+
// No `TO` option
11016+
Keyword::MONTH,
11017+
Keyword::SECOND,
11018+
]) {
11019+
Some(Keyword::YEAR) => {
11020+
if self.peek_keyword(Keyword::TO) {
11021+
self.expect_keyword(Keyword::TO)?;
11022+
self.expect_keyword(Keyword::MONTH)?;
11023+
Ok(Some(IntervalFields::YearToMonth))
11024+
} else {
11025+
Ok(Some(IntervalFields::Year))
11026+
}
11027+
}
11028+
Some(Keyword::DAY) => {
11029+
if self.peek_keyword(Keyword::TO) {
11030+
self.expect_keyword(Keyword::TO)?;
11031+
match self.expect_one_of_keywords(&[
11032+
Keyword::HOUR,
11033+
Keyword::MINUTE,
11034+
Keyword::SECOND,
11035+
])? {
11036+
Keyword::HOUR => Ok(Some(IntervalFields::DayToHour)),
11037+
Keyword::MINUTE => Ok(Some(IntervalFields::DayToMinute)),
11038+
Keyword::SECOND => Ok(Some(IntervalFields::DayToSecond)),
11039+
_ => {
11040+
self.prev_token();
11041+
self.expected("HOUR, MINUTE, or SECOND", self.peek_token())
11042+
}
11043+
}
11044+
} else {
11045+
Ok(Some(IntervalFields::Day))
11046+
}
11047+
}
11048+
Some(Keyword::HOUR) => {
11049+
if self.peek_keyword(Keyword::TO) {
11050+
self.expect_keyword(Keyword::TO)?;
11051+
match self.expect_one_of_keywords(&[Keyword::MINUTE, Keyword::SECOND])? {
11052+
Keyword::MINUTE => Ok(Some(IntervalFields::HourToMinute)),
11053+
Keyword::SECOND => Ok(Some(IntervalFields::HourToSecond)),
11054+
_ => {
11055+
self.prev_token();
11056+
self.expected("MINUTE or SECOND", self.peek_token())
11057+
}
11058+
}
11059+
} else {
11060+
Ok(Some(IntervalFields::Hour))
11061+
}
11062+
}
11063+
Some(Keyword::MINUTE) => {
11064+
if self.peek_keyword(Keyword::TO) {
11065+
self.expect_keyword(Keyword::TO)?;
11066+
self.expect_keyword(Keyword::SECOND)?;
11067+
Ok(Some(IntervalFields::MinuteToSecond))
11068+
} else {
11069+
Ok(Some(IntervalFields::Minute))
11070+
}
11071+
}
11072+
Some(Keyword::MONTH) => Ok(Some(IntervalFields::Month)),
11073+
Some(Keyword::SECOND) => Ok(Some(IntervalFields::Second)),
11074+
Some(_) => {
11075+
self.prev_token();
11076+
self.expected(
11077+
"YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND",
11078+
self.peek_token(),
11079+
)
11080+
}
11081+
None => Ok(None),
11082+
}
11083+
}
11084+
1099811085
/// Parse datetime64 [1]
1099911086
/// Syntax
1100011087
/// ```sql

tests/sqlparser_bigquery.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ 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(None, None),
965965
options: None,
966966
}]
967967
},
@@ -1300,7 +1300,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
13001300
})],
13011301
fields: vec![StructField {
13021302
field_name: None,
1303-
field_type: DataType::Interval,
1303+
field_type: DataType::Interval(None, None),
13041304
options: None,
13051305
}]
13061306
},

tests/sqlparser_common.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12841,7 +12841,7 @@ fn test_extract_seconds_ok() {
1284112841
expr: Box::new(Expr::Value(
1284212842
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
1284312843
)),
12844-
data_type: DataType::Interval,
12844+
data_type: DataType::Interval(None, None),
1284512845
format: None,
1284612846
}),
1284712847
}
@@ -12866,7 +12866,7 @@ fn test_extract_seconds_ok() {
1286612866
expr: Box::new(Expr::Value(
1286712867
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(),
1286812868
)),
12869-
data_type: DataType::Interval,
12869+
data_type: DataType::Interval(None, None),
1287012870
format: None,
1287112871
}),
1287212872
})],
@@ -12920,7 +12920,7 @@ fn test_extract_seconds_single_quote_ok() {
1292012920
expr: Box::new(Expr::Value(
1292112921
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
1292212922
)),
12923-
data_type: DataType::Interval,
12923+
data_type: DataType::Interval(None, None),
1292412924
format: None,
1292512925
}),
1292612926
}

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)