agent_patch
runs/2026-02-21__03-36-16__gpt-5-1-codex-mini/flux-pr-1534/flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/agent-logs/agent.patch
28030 bytes
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/ast/mod.rs b/app/src/ast/mod.rs
index 9185c9d..14c5b58 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -447,16 +447,33 @@ pub enum JsonPathElem {
pub struct JsonPath {
pub path: Vec<JsonPathElem>,
}
-
-impl fmt::Display for JsonPath {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+/// The style used to format a [`JsonPath`].
+#[derive(Debug, Copy, Clone)]
+pub enum JsonPathStyle {
+ /// Snowflake-style path with a leading colon on the first identifier.
+ Snowflake,
+ /// PartiQL-style path with dot notation for all identifiers.
+ Partiql,
+}
+
+impl JsonPath {
+ pub fn fmt_with_style(
+ &self,
+ f: &mut fmt::Formatter<'_>,
+ style: JsonPathStyle,
+ ) -> fmt::Result {
for (i, elem) in self.path.iter().enumerate() {
match elem {
JsonPathElem::Dot { key, quoted } => {
- if i == 0 {
- write!(f, ":")?;
- } else {
- write!(f, ".")?;
+ match style {
+ JsonPathStyle::Snowflake => {
+ if i == 0 {
+ write!(f, ":")?;
+ } else {
+ write!(f, ".")?;
+ }
+ }
+ JsonPathStyle::Partiql => write!(f, ".")?,
}
if *quoted {
@@ -474,6 +491,14 @@ impl fmt::Display for JsonPath {
}
}
+impl fmt::Display for JsonPath {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.fmt_with_style(f, JsonPathStyle::Snowflake)
+ }
+ Ok(())
+ }
+}
+
/// The syntax used for in a cast expression.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/ast/query.rs b/app/src/ast/query.rs
index 078bbc8..5f5b677 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -975,6 +975,20 @@ pub enum TableFactor {
/// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL.
partitions: Vec<Ident>,
},
+ /// Represents the PartiQL syntax for iterating over a JSON path expression.
+ ///
+ /// For example, `c.c_orders o` iterates over the `c_orders` array exposed via the
+ /// `c` alias from a previous table reference.
+ Partiql {
+ /// The alias or identifier preceding the JSON path.
+ base: Ident,
+ /// The JSON path expressions accessed via dot/bracket notation.
+ path: JsonPath,
+ /// Optional alias for the derived table expression.
+ alias: Option<TableAlias>,
+ /// Optional `AT <alias>` clause that exposes the current index.
+ at: Option<Ident>,
+ },
Derived {
lateral: bool,
subquery: Box<Query>,
@@ -1405,6 +1419,17 @@ impl fmt::Display for TableFactor {
}
Ok(())
}
+ TableFactor::Partiql { base, path, alias, at } => {
+ write!(f, "{base}")?;
+ path.fmt_with_style(f, JsonPathStyle::Partiql)?;
+ if let Some(alias) = alias {
+ write!(f, " AS {alias}")?;
+ }
+ if let Some(at) = at {
+ write!(f, " AT {at}")?;
+ }
+ Ok(())
+ }
TableFactor::Derived {
lateral,
subquery,
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/dialect/mod.rs b/app/src/dialect/mod.rs
index 985cad7..caab7f9 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/dialect/mod.rs
+++ b/app/src/dialect/mod.rs
@@ -221,6 +221,11 @@ pub trait Dialect: Debug + Any {
false
}
+ /// Returns true if the dialect supports PartiQL-style JSON path navigation.
+ fn supports_partiql_json_path(&self) -> bool {
+ false
+ }
+
/// Returns true if the dialect supports `(NOT) IN ()` expressions
fn supports_in_empty_list(&self) -> bool {
false
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/dialect/redshift.rs b/app/src/dialect/redshift.rs
index 4d07738..f1da496 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/dialect/redshift.rs
+++ b/app/src/dialect/redshift.rs
@@ -69,6 +69,10 @@ impl Dialect for RedshiftSqlDialect {
true
}
+ fn supports_partiql_json_path(&self) -> bool {
+ true
+ }
+
/// Redshift expects the `TOP` option before the `ALL/DISTINCT` option:
/// <https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_list.html#r_SELECT_list-parameters>
fn supports_top_before_distinct(&self) -> bool {
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/parser/mod.rs b/app/src/parser/mod.rs
index c835876..5d722c1 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -277,6 +277,7 @@ pub struct Parser<'a> {
options: ParserOptions,
/// Ensure the stack does not overflow by limiting recursion depth.
recursion_counter: RecursionCounter,
+ partiql_alias_stack: Vec<Vec<Ident>>,
}
impl<'a> Parser<'a> {
@@ -303,6 +304,7 @@ impl<'a> Parser<'a> {
dialect,
recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH),
options: ParserOptions::new().with_trailing_commas(dialect.supports_trailing_commas()),
+ partiql_alias_stack: Vec::new(),
}
}
@@ -2934,7 +2936,10 @@ impl<'a> Parser<'a> {
expr: Box::new(expr),
})
} else if Token::LBracket == tok {
- if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) {
+ if self.dialect.supports_partiql_json_path() {
+ self.prev_token();
+ self.parse_json_access(expr)
+ } else if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect) {
self.parse_subscript(expr)
} else if dialect_of!(self is SnowflakeDialect) {
self.prev_token();
@@ -9967,145 +9972,264 @@ impl<'a> Parser<'a> {
}
pub fn parse_table_and_joins(&mut self) -> Result<TableWithJoins, ParserError> {
- let relation = self.parse_table_factor()?;
- // Note that for keywords to be properly handled here, they need to be
- // added to `RESERVED_FOR_TABLE_ALIAS`, otherwise they may be parsed as
- // a table alias.
- let mut joins = vec![];
- loop {
- let global = self.parse_keyword(Keyword::GLOBAL);
- let join = if self.parse_keyword(Keyword::CROSS) {
- let join_operator = if self.parse_keyword(Keyword::JOIN) {
- JoinOperator::CrossJoin
- } else if self.parse_keyword(Keyword::APPLY) {
- // MSSQL extension, similar to CROSS JOIN LATERAL
- JoinOperator::CrossApply
- } else {
- return self.expected("JOIN or APPLY after CROSS", self.peek_token());
- };
- Join {
- relation: self.parse_table_factor()?,
- global,
- join_operator,
- }
- } else if self.parse_keyword(Keyword::OUTER) {
- // MSSQL extension, similar to LEFT JOIN LATERAL .. ON 1=1
- self.expect_keyword(Keyword::APPLY)?;
- Join {
- relation: self.parse_table_factor()?,
- global,
- join_operator: JoinOperator::OuterApply,
- }
- } else if self.parse_keyword(Keyword::ASOF) {
- self.expect_keyword(Keyword::JOIN)?;
- let relation = self.parse_table_factor()?;
- self.expect_keyword(Keyword::MATCH_CONDITION)?;
- let match_condition = self.parse_parenthesized(Self::parse_expr)?;
- Join {
- relation,
- global,
- join_operator: JoinOperator::AsOf {
- match_condition,
- constraint: self.parse_join_constraint(false)?,
- },
- }
- } else {
- let natural = self.parse_keyword(Keyword::NATURAL);
- let peek_keyword = if let Token::Word(w) = self.peek_token().token {
- w.keyword
+ self.push_partiql_alias_context();
+ let result = (|| {
+ let relation = self.parse_table_factor()?;
+ self.register_partiql_alias_from_table_factor(&relation);
+ // Note that for keywords to be properly handled here, they need to be
+ // added to `RESERVED_FOR_TABLE_ALIAS`, otherwise they may be parsed as
+ // a table alias.
+ let mut joins = vec![];
+ loop {
+ let global = self.parse_keyword(Keyword::GLOBAL);
+ let join = if self.parse_keyword(Keyword::CROSS) {
+ let join_operator = if self.parse_keyword(Keyword::JOIN) {
+ JoinOperator::CrossJoin
+ } else if self.parse_keyword(Keyword::APPLY) {
+ // MSSQL extension, similar to CROSS JOIN LATERAL
+ JoinOperator::CrossApply
+ } else {
+ return self.expected("JOIN or APPLY after CROSS", self.peek_token());
+ };
+ let relation = self.parse_table_factor()?;
+ self.register_partiql_alias_from_table_factor(&relation);
+ Join {
+ relation,
+ global,
+ join_operator,
+ }
+ } else if self.parse_keyword(Keyword::OUTER) {
+ // MSSQL extension, similar to LEFT JOIN LATERAL .. ON 1=1
+ self.expect_keyword(Keyword::APPLY)?;
+ let relation = self.parse_table_factor()?;
+ self.register_partiql_alias_from_table_factor(&relation);
+ Join {
+ relation,
+ global,
+ join_operator: JoinOperator::OuterApply,
+ }
+ } else if self.parse_keyword(Keyword::ASOF) {
+ self.expect_keyword(Keyword::JOIN)?;
+ let relation = self.parse_table_factor()?;
+ self.register_partiql_alias_from_table_factor(&relation);
+ self.expect_keyword(Keyword::MATCH_CONDITION)?;
+ let match_condition = self.parse_parenthesized(Self::parse_expr)?;
+ Join {
+ relation,
+ global,
+ join_operator: JoinOperator::AsOf {
+ match_condition,
+ constraint: self.parse_join_constraint(false)?,
+ },
+ }
} else {
- Keyword::NoKeyword
- };
+ let natural = self.parse_keyword(Keyword::NATURAL);
+ let peek_keyword = if let Token::Word(w) = self.peek_token().token {
+ w.keyword
+ } else {
+ Keyword::NoKeyword
+ };
- let join_operator_type = match peek_keyword {
- Keyword::INNER | Keyword::JOIN => {
- let _ = self.parse_keyword(Keyword::INNER); // [ INNER ]
- self.expect_keyword(Keyword::JOIN)?;
- JoinOperator::Inner
- }
- kw @ Keyword::LEFT | kw @ Keyword::RIGHT => {
- let _ = self.next_token(); // consume LEFT/RIGHT
- let is_left = kw == Keyword::LEFT;
- let join_type = self.parse_one_of_keywords(&[
- Keyword::OUTER,
- Keyword::SEMI,
- Keyword::ANTI,
- Keyword::JOIN,
- ]);
- match join_type {
- Some(Keyword::OUTER) => {
- self.expect_keyword(Keyword::JOIN)?;
- if is_left {
- JoinOperator::LeftOuter
- } else {
- JoinOperator::RightOuter
+ let join_operator_type = match peek_keyword {
+ Keyword::INNER | Keyword::JOIN => {
+ let _ = self.parse_keyword(Keyword::INNER); // [ INNER ]
+ self.expect_keyword(Keyword::JOIN)?;
+ JoinOperator::Inner
+ }
+ kw @ Keyword::LEFT | kw @ Keyword::RIGHT => {
+ let _ = self.next_token(); // consume LEFT/RIGHT
+ let is_left = kw == Keyword::LEFT;
+ let join_type = self.parse_one_of_keywords(&[
+ Keyword::OUTER,
+ Keyword::SEMI,
+ Keyword::ANTI,
+ Keyword::JOIN,
+ ]);
+ match join_type {
+ Some(Keyword::OUTER) => {
+ self.expect_keyword(Keyword::JOIN)?;
+ if is_left {
+ JoinOperator::LeftOuter
+ } else {
+ JoinOperator::RightOuter
+ }
}
- }
- Some(Keyword::SEMI) => {
- self.expect_keyword(Keyword::JOIN)?;
- if is_left {
- JoinOperator::LeftSemi
- } else {
- JoinOperator::RightSemi
+ Some(Keyword::SEMI) => {
+ self.expect_keyword(Keyword::JOIN)?;
+ if is_left {
+ JoinOperator::LeftSemi
+ } else {
+ JoinOperator::RightSemi
+ }
}
- }
- Some(Keyword::ANTI) => {
- self.expect_keyword(Keyword::JOIN)?;
- if is_left {
- JoinOperator::LeftAnti
- } else {
- JoinOperator::RightAnti
+ Some(Keyword::ANTI) => {
+ self.expect_keyword(Keyword::JOIN)?;
+ if is_left {
+ JoinOperator::LeftAnti
+ } else {
+ JoinOperator::RightAnti
+ }
}
- }
- Some(Keyword::JOIN) => {
- if is_left {
- JoinOperator::LeftOuter
- } else {
- JoinOperator::RightOuter
+ Some(Keyword::JOIN) => {
+ if is_left {
+ JoinOperator::LeftOuter
+ } else {
+ JoinOperator::RightOuter
+ }
+ }
+ _ => {
+ return Err(ParserError::ParserError(format!(
+ "expected OUTER, SEMI, ANTI or JOIN after {kw:?}"
+ )))
}
- }
- _ => {
- return Err(ParserError::ParserError(format!(
- "expected OUTER, SEMI, ANTI or JOIN after {kw:?}"
- )))
}
}
+ Keyword::ANTI => {
+ let _ = self.next_token(); // consume ANTI
+ self.expect_keyword(Keyword::JOIN)?;
+ JoinOperator::Anti
+ }
+ Keyword::SEMI => {
+ let _ = self.next_token(); // consume SEMI
+ self.expect_keyword(Keyword::JOIN)?;
+ JoinOperator::Semi
+ }
+ Keyword::FULL => {
+ let _ = self.next_token(); // consume FULL
+ let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ]
+ self.expect_keyword(Keyword::JOIN)?;
+ JoinOperator::FullOuter
+ }
+ Keyword::OUTER => {
+ return self.expected("LEFT, RIGHT, or FULL", self.peek_token());
+ }
+ _ if natural => {
+ return self.expected("a join type after NATURAL", self.peek_token());
+ }
+ _ => break,
+ };
+ let relation = self.parse_table_factor()?;
+ self.register_partiql_alias_from_table_factor(&relation);
+ let join_constraint = self.parse_join_constraint(natural)?;
+ Join {
+ relation,
+ global,
+ join_operator: join_operator_type(join_constraint),
}
- Keyword::ANTI => {
- let _ = self.next_token(); // consume ANTI
- self.expect_keyword(Keyword::JOIN)?;
- JoinOperator::Anti
- }
- Keyword::SEMI => {
- let _ = self.next_token(); // consume SEMI
- self.expect_keyword(Keyword::JOIN)?;
- JoinOperator::Semi
- }
- Keyword::FULL => {
- let _ = self.next_token(); // consume FULL
- let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ]
- self.expect_keyword(Keyword::JOIN)?;
- JoinOperator::FullOuter
- }
- Keyword::OUTER => {
- return self.expected("LEFT, RIGHT, or FULL", self.peek_token());
- }
- _ if natural => {
- return self.expected("a join type after NATURAL", self.peek_token());
- }
- _ => break,
};
- let relation = self.parse_table_factor()?;
- let join_constraint = self.parse_join_constraint(natural)?;
- Join {
- relation,
- global,
- join_operator: join_operator_type(join_constraint),
+ joins.push(join);
+ }
+ Ok(TableWithJoins { relation, joins })
+ })();
+ self.pop_partiql_alias_context();
+ result
+ }
+
+ fn push_partiql_alias_context(&mut self) {
+ if self.dialect.supports_partiql_json_path() {
+ self.partiql_alias_stack.push(Vec::new());
+ }
+ }
+
+ fn pop_partiql_alias_context(&mut self) {
+ if self.dialect.supports_partiql_json_path() {
+ self.partiql_alias_stack.pop();
+ }
+ }
+
+ fn register_partiql_alias_name(&mut self, alias: Ident) {
+ if let Some(stack) = self.partiql_alias_stack.last_mut() {
+ stack.push(alias);
+ }
+ }
+
+ fn register_partiql_alias_from_table_factor(&mut self, table_factor: &TableFactor) {
+ if !self.dialect.supports_partiql_json_path() {
+ return;
+ }
+ let alias = match table_factor {
+ TableFactor::Table { alias, .. } => alias.as_ref(),
+ TableFactor::Derived { alias, .. } => alias.as_ref(),
+ TableFactor::TableFunction { alias, .. } => alias.as_ref(),
+ TableFactor::Function { alias, .. } => alias.as_ref(),
+ TableFactor::UNNEST { alias, .. } => alias.as_ref(),
+ TableFactor::JsonTable { alias, .. } => alias.as_ref(),
+ TableFactor::OpenJsonTable { alias, .. } => alias.as_ref(),
+ TableFactor::NestedJoin { alias, .. } => alias.as_ref(),
+ TableFactor::Pivot { alias, .. } => alias.as_ref(),
+ TableFactor::Unpivot { alias, .. } => alias.as_ref(),
+ TableFactor::MatchRecognize { alias, .. } => alias.as_ref(),
+ TableFactor::Partiql { alias, .. } => alias.as_ref(),
+ };
+ if let Some(alias) = alias {
+ self.register_partiql_alias_name(alias.name.clone());
+ }
+ }
+
+ fn is_partiql_alias(&self, ident: &Ident) -> bool {
+ self.partiql_alias_stack
+ .last()
+ .map(|aliases| aliases.iter().any(|alias| Self::ident_eq(alias, ident)))
+ .unwrap_or(false)
+ }
+
+ fn ident_eq(a: &Ident, b: &Ident) -> bool {
+ match (a.quote_style, b.quote_style) {
+ (None, None) => a.value.eq_ignore_ascii_case(&b.value),
+ _ => a.value == b.value,
+ }
+ }
+
+ fn try_parse_partiql_table_reference(
+ &mut self,
+ name: &ObjectName,
+ ) -> Result<Option<(Ident, JsonPath)>, ParserError> {
+ if !self.dialect.supports_partiql_json_path() {
+ return Ok(None);
+ }
+ let base_ident = match name.0.first() {
+ Some(ident) => ident.clone(),
+ None => return Ok(None),
+ };
+ if !self.is_partiql_alias(&base_ident) {
+ return Ok(None);
+ }
+
+ let mut path = Vec::new();
+ for ident in name.0.iter().skip(1) {
+ path.push(JsonPathElem::Dot {
+ key: ident.value.clone(),
+ quoted: ident.quote_style.is_some(),
+ });
+ }
+ self.parse_partiql_table_json_path(&mut path)?;
+ if path.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some((base_ident, JsonPath { path })))
+ }
+ }
+
+ fn parse_partiql_table_json_path(
+ &mut self,
+ path: &mut Vec<JsonPathElem>,
+ ) -> Result<(), ParserError> {
+ loop {
+ match self.peek_token().token {
+ Token::Period => {
+ self.next_token();
+ path.push(self.parse_json_path_object_key()?);
}
- };
- joins.push(join);
+ Token::LBracket => {
+ self.next_token();
+ let key = self.parse_expr()?;
+ self.expect_token(&Token::RBracket)?;
+ path.push(JsonPathElem::Bracket { key });
+ }
+ _ => break,
+ }
}
- Ok(TableWithJoins { relation, joins })
+ Ok(())
}
/// A table name or a parenthesized subquery, followed by optional `[AS] alias`
@@ -10337,6 +10461,20 @@ impl<'a> Parser<'a> {
self.parse_open_json_table_factor()
} else {
let name = self.parse_object_name(true)?;
+ if let Some((base, path)) = self.try_parse_partiql_table_reference(&name)? {
+ let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?;
+ let at = if self.parse_keyword(Keyword::AT) {
+ Some(self.parse_identifier(false)?)
+ } else {
+ None
+ };
+ return Ok(TableFactor::Partiql {
+ base,
+ path,
+ alias,
+ at,
+ });
+ }
let partitions: Vec<Ident> = if dialect_of!(self is MySqlDialect | GenericDialect)
&& self.parse_keyword(Keyword::PARTITION)
diff --git a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/tests/sqlparser_redshift.rs b/app/tests/sqlparser_redshift.rs
index a25d506..36edce8 100644
--- a/tmp/agent-patch-flux-pr-1534.1-of-1.2026-03-05__17-48-34__gpt-5-1-codex-mini/app/tests/sqlparser_redshift.rs
+++ b/app/tests/sqlparser_redshift.rs
@@ -170,6 +170,60 @@ fn parse_delimited_identifiers() {
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
}
+#[test]
+fn parse_partiql_json_path_expression() {
+ let select = redshift()
+ .verified_only_select("SELECT c.c_orders[0].o_orderkey FROM customer_orders_lineitem c");
+
+ let expected = Expr::JsonAccess {
+ value: Box::new(Expr::CompoundIdentifier(vec![
+ Ident::new("c"),
+ Ident::new("c_orders"),
+ ])),
+ path: JsonPath {
+ path: vec![
+ JsonPathElem::Bracket {
+ key: Expr::Value(Value::Number("0".to_string(), false)),
+ },
+ JsonPathElem::Dot {
+ key: "o_orderkey".to_string(),
+ quoted: false,
+ },
+ ],
+ },
+ };
+
+ assert_eq!(expected, expr_from_projection(&select.projection[0]));
+}
+
+#[test]
+fn parse_partiql_table_reference() {
+ let select = redshift().verified_only_select(
+ "SELECT * FROM customer_orders_lineitem c, c.c_orders o",
+ );
+
+ match &select.from[1].relation {
+ TableFactor::Partiql {
+ base,
+ path,
+ alias,
+ at,
+ } => {
+ assert_eq!(&Ident::new("c"), base);
+ assert_eq!(
+ vec![JsonPathElem::Dot {
+ key: "c_orders".to_string(),
+ quoted: false,
+ }],
+ path.path
+ );
+ assert_eq!(Some(Ident::new("o")), alias.as_ref().map(|n| n.name.clone()));
+ assert!(at.is_none());
+ }
+ other => panic!("expected Partiql table factor, got {other:?}"),
+ }
+}
+
fn redshift() -> TestedDialects {
TestedDialects::new(vec![Box::new(RedshiftSqlDialect {})])
}