agent_patch
runs/2026-02-28__18-27-22__gpt-5-4/flux-pr-1984/flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/agent-logs/agent.patch
13586 bytes
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/data_type.rs b/app/src/ast/data_type.rs
index 0897f2d..2e8f50b 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/data_type.rs
+++ b/app/src/ast/data_type.rs
@@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "visitor")]
use sqlparser_derive::{Visit, VisitMut};
-use crate::ast::{display_comma_separated, Expr, ObjectName, StructField, UnionField};
+use crate::ast::{display_comma_separated, DateTimeField, Expr, ObjectName, StructField, UnionField};
use super::{value::escape_single_quote_string, ColumnDef};
@@ -345,8 +345,8 @@ pub enum DataType {
///
/// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type
TimestampNtz,
- /// Interval type.
- Interval,
+ /// Interval type with optional PostgreSQL-style field qualifiers and precision.
+ Interval(IntervalFields),
/// JSON type.
JSON,
/// Binary JSON type.
@@ -635,7 +635,7 @@ impl fmt::Display for DataType {
timezone,
)
}
- DataType::Interval => write!(f, "INTERVAL"),
+ DataType::Interval(fields) => write!(f, "INTERVAL{fields}"),
DataType::JSON => write!(f, "JSON"),
DataType::JSONB => write!(f, "JSONB"),
DataType::Regclass => write!(f, "REGCLASS"),
@@ -867,6 +867,31 @@ pub enum TimezoneInfo {
Tz,
}
+/// Optional field qualifiers and precision for the `INTERVAL` data type.
+#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct IntervalFields {
+ pub leading_field: Option<DateTimeField>,
+ pub last_field: Option<DateTimeField>,
+ pub fractional_seconds_precision: Option<u64>,
+}
+
+impl fmt::Display for IntervalFields {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if let Some(leading_field) = &self.leading_field {
+ write!(f, " {leading_field}")?;
+ }
+ if let Some(last_field) = &self.last_field {
+ write!(f, " TO {last_field}")?;
+ }
+ if let Some(fractional_seconds_precision) = self.fractional_seconds_precision {
+ write!(f, "({fractional_seconds_precision})")?;
+ }
+ Ok(())
+ }
+}
+
impl fmt::Display for TimezoneInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/mod.rs b/app/src/ast/mod.rs
index 34112ac..c6212f1 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -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,
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/generic.rs b/app/src/dialect/generic.rs
index de83d50..bfbe24a 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/generic.rs
+++ b/app/src/dialect/generic.rs
@@ -152,6 +152,10 @@ impl Dialect for GenericDialect {
true
}
+ fn supports_interval_type_options(&self) -> bool {
+ true
+ }
+
fn supports_user_host_grantee(&self) -> bool {
true
}
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/mod.rs b/app/src/dialect/mod.rs
index 9003220..735b974 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/mod.rs
+++ b/app/src/dialect/mod.rs
@@ -868,6 +868,12 @@ pub trait Dialect: Debug + Any {
false
}
+ /// Whether the dialect supports PostgreSQL-style `INTERVAL` type qualifiers,
+ /// such as `INTERVAL YEAR TO MONTH` and `INTERVAL HOUR TO SECOND(3)`.
+ fn supports_interval_type_options(&self) -> bool {
+ false
+ }
+
fn supports_explain_with_utility_options(&self) -> bool {
false
}
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/postgresql.rs b/app/src/dialect/postgresql.rs
index b12abaa..a9cc69d 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/postgresql.rs
+++ b/app/src/dialect/postgresql.rs
@@ -239,6 +239,10 @@ impl Dialect for PostgreSqlDialect {
true
}
+ fn supports_interval_type_options(&self) -> bool {
+ true
+ }
+
fn supports_string_escape_constant(&self) -> bool {
true
}
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/parser/mod.rs b/app/src/parser/mod.rs
index 455e0ca..aee803c 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -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),
@@ -3023,6 +3023,76 @@ impl<'a> Parser<'a> {
}
}
+ fn next_token_is_interval_type_field(&mut self) -> bool {
+ if let Token::Word(word) = self.peek_token().token {
+ matches!(
+ word.keyword,
+ Keyword::YEAR
+ | Keyword::MONTH
+ | Keyword::DAY
+ | Keyword::HOUR
+ | Keyword::MINUTE
+ | Keyword::SECOND
+ )
+ } else {
+ false
+ }
+ }
+
+ fn parse_interval_type_field(&mut self) -> Result<DateTimeField, ParserError> {
+ let token = self.next_token();
+ match token.token {
+ Token::Word(w) => match w.keyword {
+ Keyword::YEAR => Ok(DateTimeField::Year),
+ Keyword::MONTH => Ok(DateTimeField::Month),
+ Keyword::DAY => Ok(DateTimeField::Day),
+ Keyword::HOUR => Ok(DateTimeField::Hour),
+ Keyword::MINUTE => Ok(DateTimeField::Minute),
+ Keyword::SECOND => Ok(DateTimeField::Second),
+ _ => self.expected("interval field", token),
+ },
+ _ => self.expected("interval field", token),
+ }
+ }
+
+ fn parse_interval_type_fields(&mut self) -> Result<IntervalFields, ParserError> {
+ if !self.dialect.supports_interval_type_options() {
+ return Ok(IntervalFields::default());
+ }
+
+ let leading_field = if self.next_token_is_interval_type_field() {
+ Some(self.parse_interval_type_field()?)
+ } else {
+ None
+ };
+
+ let last_field = if leading_field.is_some() && leading_field != Some(DateTimeField::Second)
+ {
+ if self.parse_keyword(Keyword::TO) {
+ Some(self.parse_interval_type_field()?)
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ let fractional_seconds_precision = if leading_field.is_none()
+ || leading_field == Some(DateTimeField::Second)
+ || last_field == Some(DateTimeField::Second)
+ {
+ self.parse_optional_precision()?
+ } else {
+ None
+ };
+
+ Ok(IntervalFields {
+ leading_field,
+ last_field,
+ fractional_seconds_precision,
+ })
+ }
+
/// Syntax
/// ```sql
/// -- typed
@@ -10066,10 +10136,7 @@ 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 => Ok(DataType::Interval(self.parse_interval_type_fields()?)),
Keyword::JSON => Ok(DataType::JSON),
Keyword::JSONB => Ok(DataType::JSONB),
Keyword::REGCLASS => Ok(DataType::Regclass),
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_bigquery.rs b/app/tests/sqlparser_bigquery.rs
index 839ea6b..4714024 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_bigquery.rs
+++ b/app/tests/sqlparser_bigquery.rs
@@ -961,7 +961,7 @@ fn parse_typed_struct_syntax_bigquery() {
})],
fields: vec![StructField {
field_name: None,
- field_type: DataType::Interval,
+ field_type: DataType::Interval(IntervalFields::default()),
options: None,
}]
},
@@ -1300,7 +1300,7 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
})],
fields: vec![StructField {
field_name: None,
- field_type: DataType::Interval,
+ field_type: DataType::Interval(IntervalFields::default()),
options: None,
}]
},
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index 5e389ae..7929e73 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_common.rs
+++ b/app/tests/sqlparser_common.rs
@@ -6155,6 +6155,59 @@ fn parse_interval_all() {
);
}
+#[test]
+fn parse_interval_data_type_options() {
+ let dialects = all_dialects_where(|d| d.supports_interval_type_options());
+
+ let create = dialects.verified_stmt("CREATE TABLE t (i INTERVAL YEAR TO MONTH)");
+ let Statement::CreateTable(create) = create else {
+ unreachable!()
+ };
+ assert_eq!(
+ create.columns[0].data_type,
+ DataType::Interval(IntervalFields {
+ leading_field: Some(DateTimeField::Year),
+ last_field: Some(DateTimeField::Month),
+ fractional_seconds_precision: None,
+ })
+ );
+
+ let expr = dialects.verified_expr("'1 second'::INTERVAL HOUR TO SECOND(3)");
+ assert_eq!(
+ expr,
+ Expr::Cast {
+ kind: CastKind::DoubleColon,
+ expr: Box::new(Expr::Value(
+ Value::SingleQuotedString("1 second".to_string()).with_empty_span()
+ )),
+ data_type: DataType::Interval(IntervalFields {
+ leading_field: Some(DateTimeField::Hour),
+ last_field: Some(DateTimeField::Second),
+ fractional_seconds_precision: Some(3),
+ }),
+ format: None,
+ }
+ );
+
+ dialects.verified_expr("'1 second'::INTERVAL(3)");
+ dialects.verified_expr("'1 second'::INTERVAL SECOND(3)");
+}
+
+#[test]
+fn parse_interval_data_type_options_are_dialect_gated() {
+ let dialects = all_dialects_except(|d| d.supports_interval_type_options());
+ assert!(
+ dialects
+ .parse_sql_statements("CREATE TABLE t (i INTERVAL YEAR TO MONTH)")
+ .is_err()
+ );
+ assert!(
+ dialects
+ .parse_sql_statements("SELECT '1 second'::INTERVAL HOUR TO SECOND(3)")
+ .is_err()
+ );
+}
+
#[test]
fn parse_interval_dont_require_unit() {
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
@@ -12955,7 +13008,7 @@ 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(IntervalFields::default()),
format: None,
}),
}
@@ -12980,7 +13033,7 @@ 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(IntervalFields::default()),
format: None,
}),
})],
@@ -13034,7 +13087,7 @@ 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(IntervalFields::default()),
format: None,
}),
}