STET

agent_patch

runs/2026-02-28__18-27-22__gpt-5-3-codex/flux-pr-1984/flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/agent-logs/agent.patch

15041 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/data_type.rs b/app/src/ast/data_type.rs
index 0897f2d..7fa2ee2 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/data_type.rs
+++ b/app/src/ast/data_type.rs
@@ -25,7 +25,9 @@ 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::{
+    DateTimeField, Expr, ObjectName, StructField, UnionField, display_comma_separated,
+};
 
 use super::{value::escape_single_quote_string, ColumnDef};
 
@@ -40,6 +42,52 @@ pub enum EnumMember {
     NamedValue(String, Expr),
 }
 
+/// Optional interval qualifiers for the `INTERVAL` data type.
+#[derive(Debug, Clone, Default, 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 leading_precision: Option<u64>,
+    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 {
+        match (
+            &self.leading_field,
+            self.leading_precision,
+            self.fractional_seconds_precision,
+        ) {
+            (
+                Some(DateTimeField::Second),
+                Some(leading_precision),
+                Some(fractional_seconds_precision),
+            ) => {
+                // SECOND(p, s) cannot be combined with a trailing TO clause.
+                assert!(self.last_field.is_none());
+                write!(f, " SECOND ({leading_precision}, {fractional_seconds_precision})")
+            }
+            _ => {
+                if let Some(leading_field) = &self.leading_field {
+                    write!(f, " {leading_field}")?;
+                }
+                if let Some(leading_precision) = self.leading_precision {
+                    write!(f, " ({leading_precision})")?;
+                }
+                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(())
+            }
+        }
+    }
+}
+
 /// SQL data types
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -346,7 +394,7 @@ pub enum DataType {
     /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type
     TimestampNtz,
     /// Interval type.
-    Interval,
+    Interval(IntervalFields),
     /// JSON type.
     JSON,
     /// Binary JSON type.
@@ -635,7 +683,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"),
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/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-3-codex/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-3-codex/app/src/dialect/generic.rs b/app/src/dialect/generic.rs
index de83d50..5d206f3 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/dialect/generic.rs
+++ b/app/src/dialect/generic.rs
@@ -187,4 +187,8 @@ impl Dialect for GenericDialect {
     fn supports_data_type_signed_suffix(&self) -> bool {
         true
     }
+
+    fn supports_interval_datatype_qualifier(&self) -> bool {
+        true
+    }
 }
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/dialect/mod.rs b/app/src/dialect/mod.rs
index 9003220..278c3f2 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/dialect/mod.rs
+++ b/app/src/dialect/mod.rs
@@ -868,6 +868,12 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Whether this dialect supports PostgreSQL-style `INTERVAL` data type qualifiers,
+    /// such as `INTERVAL YEAR TO MONTH` or `INTERVAL HOUR TO SECOND(3)`.
+    fn supports_interval_datatype_qualifier(&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-3-codex/app/src/dialect/postgresql.rs b/app/src/dialect/postgresql.rs
index b12abaa..58cdabb 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/dialect/postgresql.rs
+++ b/app/src/dialect/postgresql.rs
@@ -215,6 +215,10 @@ impl Dialect for PostgreSqlDialect {
         true
     }
 
+    fn supports_interval_datatype_qualifier(&self) -> bool {
+        true
+    }
+
     /// See <https://www.postgresql.org/docs/current/functions-json.html>
     ///
     /// Required to support the label in:
diff --git a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/parser/mod.rs b/app/src/parser/mod.rs
index 455e0ca..3c6d3ef 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/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),
@@ -2943,16 +2943,39 @@ impl<'a> Parser<'a> {
             None
         };
 
-        let (leading_precision, last_field, fsec_precision) =
+        let fields = self.parse_interval_qualifier(leading_field)?;
+
+        Ok(Expr::Interval(Interval {
+            value: Box::new(value),
+            leading_field: fields.leading_field,
+            leading_precision: fields.leading_precision,
+            last_field: fields.last_field,
+            fractional_seconds_precision: fields.fractional_seconds_precision,
+        }))
+    }
+
+    fn parse_optional_interval_qualifier(&mut self) -> Result<IntervalFields, ParserError> {
+        let leading_field = if self.next_token_is_temporal_unit() {
+            Some(self.parse_date_time_field()?)
+        } else {
+            None
+        };
+        self.parse_interval_qualifier(leading_field)
+    }
+
+    fn parse_interval_qualifier(
+        &mut self,
+        leading_field: Option<DateTimeField>,
+    ) -> Result<IntervalFields, ParserError> {
+        let (leading_precision, last_field, fractional_seconds_precision) =
             if leading_field == Some(DateTimeField::Second) {
                 // SQL mandates special syntax for `SECOND TO SECOND` literals.
                 // Instead of
                 //     `SECOND [(<leading precision>)] TO SECOND[(<fractional seconds precision>)]`
                 // one must use the special format:
                 //     `SECOND [( <leading precision> [ , <fractional seconds precision>] )]`
-                let last_field = None;
                 let (leading_precision, fsec_precision) = self.parse_optional_precision_scale()?;
-                (leading_precision, last_field, fsec_precision)
+                (leading_precision, None, fsec_precision)
             } else {
                 let leading_precision = self.parse_optional_precision()?;
                 if self.parse_keyword(Keyword::TO) {
@@ -2968,13 +2991,12 @@ impl<'a> Parser<'a> {
                 }
             };
 
-        Ok(Expr::Interval(Interval {
-            value: Box::new(value),
+        Ok(IntervalFields {
             leading_field,
             leading_precision,
             last_field,
-            fractional_seconds_precision: fsec_precision,
-        }))
+            fractional_seconds_precision,
+        })
     }
 
     /// Peek at the next token and determine if it is a temporal unit
@@ -10066,10 +10088,14 @@ 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 => {
+                    let fields = if dialect.supports_interval_datatype_qualifier() {
+                        self.parse_optional_interval_qualifier()?
+                    } else {
+                        IntervalFields::default()
+                    };
+                    Ok(DataType::Interval(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-3-codex/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-3-codex/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-3-codex/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index 5e389ae..b088429 100644
--- a/tmp/agent-patch-flux-pr-1984.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/tests/sqlparser_common.rs
+++ b/app/tests/sqlparser_common.rs
@@ -6155,6 +6155,63 @@ fn parse_interval_all() {
     );
 }
 
+#[test]
+fn parse_interval_data_type_with_qualifier() {
+    let dialects = all_dialects_where(|d| d.supports_interval_datatype_qualifier());
+
+    let sql = "CREATE TABLE t (i INTERVAL YEAR TO MONTH, j INTERVAL HOUR TO SECOND(3), k INTERVAL SECOND(4))";
+    let canonical = "CREATE TABLE t (i INTERVAL YEAR TO MONTH, j INTERVAL HOUR TO SECOND (3), k INTERVAL SECOND (4))";
+    let stmt = dialects.one_statement_parses_to(sql, canonical);
+    let Statement::CreateTable(create_table) = stmt else {
+        panic!("Expected CREATE TABLE");
+    };
+
+    assert_eq!(
+        create_table.columns[0].data_type,
+        DataType::Interval(IntervalFields {
+            leading_field: Some(DateTimeField::Year),
+            leading_precision: None,
+            last_field: Some(DateTimeField::Month),
+            fractional_seconds_precision: None,
+        })
+    );
+    assert_eq!(
+        create_table.columns[1].data_type,
+        DataType::Interval(IntervalFields {
+            leading_field: Some(DateTimeField::Hour),
+            leading_precision: None,
+            last_field: Some(DateTimeField::Second),
+            fractional_seconds_precision: Some(3),
+        })
+    );
+    assert_eq!(
+        create_table.columns[2].data_type,
+        DataType::Interval(IntervalFields {
+            leading_field: Some(DateTimeField::Second),
+            leading_precision: Some(4),
+            last_field: None,
+            fractional_seconds_precision: None,
+        })
+    );
+
+    dialects.one_statement_parses_to(
+        "SELECT '1 second'::INTERVAL HOUR TO SECOND(3)",
+        "SELECT '1 second'::INTERVAL HOUR TO SECOND (3)",
+    );
+}
+
+#[test]
+fn parse_interval_data_type_qualifier_not_supported_in_mysql() {
+    let err = Parser::parse_sql(
+        &MySqlDialect {},
+        "CREATE TABLE t (i INTERVAL YEAR TO MONTH)",
+    )
+    .unwrap_err();
+    let err = err.to_string();
+    assert!(err.contains("after column definition"), "{err}");
+    assert!(err.contains("YEAR"), "{err}");
+}
+
 #[test]
 fn parse_interval_dont_require_unit() {
     let dialects = all_dialects_except(|d| d.require_interval_qualifier());
@@ -12955,7 +13012,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 +13037,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 +13091,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,
             }),
         }