Skip to content

Commit 54ef1f7

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 3d2db8c commit 54ef1f7

File tree

9 files changed

+237
-13
lines changed

9 files changed

+237
-13
lines changed

src/ast/data_type.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,14 @@ 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 {
354+
fields: Option<IntervalFields>,
355+
precision: Option<u64>,
356+
},
350357
/// JSON type.
351358
JSON,
352359
/// Binary JSON type.
@@ -635,7 +642,16 @@ impl fmt::Display for DataType {
635642
timezone,
636643
)
637644
}
638-
DataType::Interval => write!(f, "INTERVAL"),
645+
DataType::Interval { fields, precision } => {
646+
write!(f, "INTERVAL")?;
647+
if let Some(fields) = fields {
648+
write!(f, " {fields}")?;
649+
}
650+
if let Some(precision) = precision {
651+
write!(f, "({precision})")?;
652+
}
653+
Ok(())
654+
}
639655
DataType::JSON => write!(f, "JSON"),
640656
DataType::JSONB => write!(f, "JSONB"),
641657
DataType::Regclass => write!(f, "REGCLASS"),
@@ -889,6 +905,48 @@ impl fmt::Display for TimezoneInfo {
889905
}
890906
}
891907

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

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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,21 @@ 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+
/// Examples:
1143+
/// ```sql
1144+
/// CREATE TABLE t (i INTERVAL YEAR TO MONTH);
1145+
/// SELECT '1 second'::INTERVAL HOUR TO SECOND(3);
1146+
/// ```
1147+
///
1148+
/// See [`crate::ast::DataType::Interval`] and [`crate::ast::IntervalFields`].
1149+
///
1150+
/// [Postgres]: https://www.postgresql.org/docs/17/datatype-datetime.html
1151+
fn supports_interval_options(&self) -> bool {
1152+
false
1153+
}
11391154
}
11401155

11411156
/// 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),
@@ -10019,10 +10019,18 @@ impl<'a> Parser<'a> {
1001910019
self.parse_optional_precision()?,
1002010020
TimezoneInfo::Tz,
1002110021
)),
10022-
// Interval types can be followed by a complicated interval
10023-
// qualifier that we don't currently support. See
10024-
// parse_interval for a taste.
10025-
Keyword::INTERVAL => Ok(DataType::Interval),
10022+
Keyword::INTERVAL => {
10023+
if self.dialect.supports_interval_options() {
10024+
let fields = self.maybe_parse_optional_interval_fields()?;
10025+
let precision = self.parse_optional_precision()?;
10026+
Ok(DataType::Interval { fields, precision })
10027+
} else {
10028+
Ok(DataType::Interval {
10029+
fields: None,
10030+
precision: None,
10031+
})
10032+
}
10033+
}
1002610034
Keyword::JSON => Ok(DataType::JSON),
1002710035
Keyword::JSONB => Ok(DataType::JSONB),
1002810036
Keyword::REGCLASS => Ok(DataType::Regclass),
@@ -10991,6 +10999,85 @@ impl<'a> Parser<'a> {
1099110999
}
1099211000
}
1099311001

11002+
fn maybe_parse_optional_interval_fields(
11003+
&mut self,
11004+
) -> Result<Option<IntervalFields>, ParserError> {
11005+
match self.parse_one_of_keywords(&[
11006+
// Can be followed by `TO` option
11007+
Keyword::YEAR,
11008+
Keyword::DAY,
11009+
Keyword::HOUR,
11010+
Keyword::MINUTE,
11011+
// No `TO` option
11012+
Keyword::MONTH,
11013+
Keyword::SECOND,
11014+
]) {
11015+
Some(Keyword::YEAR) => {
11016+
if self.peek_keyword(Keyword::TO) {
11017+
self.expect_keyword(Keyword::TO)?;
11018+
self.expect_keyword(Keyword::MONTH)?;
11019+
Ok(Some(IntervalFields::YearToMonth))
11020+
} else {
11021+
Ok(Some(IntervalFields::Year))
11022+
}
11023+
}
11024+
Some(Keyword::DAY) => {
11025+
if self.peek_keyword(Keyword::TO) {
11026+
self.expect_keyword(Keyword::TO)?;
11027+
match self.expect_one_of_keywords(&[
11028+
Keyword::HOUR,
11029+
Keyword::MINUTE,
11030+
Keyword::SECOND,
11031+
])? {
11032+
Keyword::HOUR => Ok(Some(IntervalFields::DayToHour)),
11033+
Keyword::MINUTE => Ok(Some(IntervalFields::DayToMinute)),
11034+
Keyword::SECOND => Ok(Some(IntervalFields::DayToSecond)),
11035+
_ => {
11036+
self.prev_token();
11037+
self.expected("HOUR, MINUTE, or SECOND", self.peek_token())
11038+
}
11039+
}
11040+
} else {
11041+
Ok(Some(IntervalFields::Day))
11042+
}
11043+
}
11044+
Some(Keyword::HOUR) => {
11045+
if self.peek_keyword(Keyword::TO) {
11046+
self.expect_keyword(Keyword::TO)?;
11047+
match self.expect_one_of_keywords(&[Keyword::MINUTE, Keyword::SECOND])? {
11048+
Keyword::MINUTE => Ok(Some(IntervalFields::HourToMinute)),
11049+
Keyword::SECOND => Ok(Some(IntervalFields::HourToSecond)),
11050+
_ => {
11051+
self.prev_token();
11052+
self.expected("MINUTE or SECOND", self.peek_token())
11053+
}
11054+
}
11055+
} else {
11056+
Ok(Some(IntervalFields::Hour))
11057+
}
11058+
}
11059+
Some(Keyword::MINUTE) => {
11060+
if self.peek_keyword(Keyword::TO) {
11061+
self.expect_keyword(Keyword::TO)?;
11062+
self.expect_keyword(Keyword::SECOND)?;
11063+
Ok(Some(IntervalFields::MinuteToSecond))
11064+
} else {
11065+
Ok(Some(IntervalFields::Minute))
11066+
}
11067+
}
11068+
Some(Keyword::MONTH) => Ok(Some(IntervalFields::Month)),
11069+
Some(Keyword::SECOND) => Ok(Some(IntervalFields::Second)),
11070+
Some(_) => {
11071+
self.prev_token();
11072+
self.expected(
11073+
"YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND",
11074+
self.peek_token(),
11075+
)
11076+
}
11077+
None => Ok(None),
11078+
}
11079+
}
11080+
1099411081
/// Parse datetime64 [1]
1099511082
/// Syntax
1099611083
/// ```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
@@ -12841,7 +12841,10 @@ 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 {
12845+
fields: None,
12846+
precision: None
12847+
},
1284512848
format: None,
1284612849
}),
1284712850
}
@@ -12866,7 +12869,10 @@ fn test_extract_seconds_ok() {
1286612869
expr: Box::new(Expr::Value(
1286712870
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span(),
1286812871
)),
12869-
data_type: DataType::Interval,
12872+
data_type: DataType::Interval {
12873+
fields: None,
12874+
precision: None,
12875+
},
1287012876
format: None,
1287112877
}),
1287212878
})],
@@ -12920,7 +12926,10 @@ fn test_extract_seconds_single_quote_ok() {
1292012926
expr: Box::new(Expr::Value(
1292112927
(Value::SingleQuotedString("2 seconds".to_string())).with_empty_span()
1292212928
)),
12923-
data_type: DataType::Interval,
12929+
data_type: DataType::Interval {
12930+
fields: None,
12931+
precision: None
12932+
},
1292412933
format: None,
1292512934
}),
1292612935
}

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)