diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8caefdb5c..1eaaaaab8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -8535,9 +8535,24 @@ Expression ArrayExpression(Expression obj): { { return expr; } } +Expression NavigationStep(Expression base): +{ + Expression step = null; + String nm = null; +} +{ + ( + "." nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); } + | + step = ArrayExpression(base) + ) + { return step; } +} + Expression PrimaryExpression() #PrimaryExpression: { Expression retval = null; + Expression nxt = null; Expression expression = null; CastExpression castExpr = null; TimezoneExpression timezoneExpr = null; @@ -8713,6 +8728,12 @@ Expression PrimaryExpression() #PrimaryExpression: [ LOOKAHEAD(2) retval = ArrayExpression(retval) ] + // navigation chains, e.g. Redshift SUPER col[0].field[1].sub: field accesses + // and subscripts may alternate freely after the primary expression + ( + LOOKAHEAD(2) nxt = NavigationStep(retval) { retval = nxt; } + )* + ( LOOKAHEAD(2) "::" type=ColDataType() { castExpr = new CastExpression(); castExpr.setUseCastKeyword(false); diff --git a/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java index dd9644100..4e114c90f 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java @@ -30,4 +30,116 @@ void testColumnArrayExpression() throws JSQLParserException { assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); } + @Test + void testNavigationAfterSubscript() throws JSQLParserException { + // Redshift SUPER navigation: subscript then field access + String sqlStr = "SELECT recommendations[0].language_id FROM recs"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SelectItem selectItem = select.getSelectItem(0); + + RowGetExpression rowGet = selectItem.getExpression(RowGetExpression.class); + assertNotNull(rowGet); + assertEquals("language_id", rowGet.getColumnName()); + Column column = assertInstanceOf(Column.class, rowGet.getExpression()); + assertEquals("recommendations", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testAlternatingNavigationChain() throws JSQLParserException { + // field accesses and subscripts may alternate freely after the primary expression + String sqlStr = "SELECT a.b[0].c[1].d FROM t"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SelectItem selectItem = select.getSelectItem(0); + + RowGetExpression outer = selectItem.getExpression(RowGetExpression.class); + assertNotNull(outer); + assertEquals("d", outer.getColumnName()); + ArrayExpression subscript = assertInstanceOf(ArrayExpression.class, outer.getExpression()); + assertInstanceOf(LongValue.class, subscript.getIndexExpression()); + RowGetExpression inner = + assertInstanceOf(RowGetExpression.class, subscript.getObjExpression()); + assertEquals("c", inner.getColumnName()); + Column column = assertInstanceOf(Column.class, inner.getExpression()); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testNavigationFieldNames() throws JSQLParserException { + // dotted continuations accept FROM/SELECT/CURRENT and quoted names, + // same as ColumnIdentifier's dotted continuation (RelObjectNameExt) + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].from FROM t", true); + RowGetExpression rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class); + assertEquals("from", rowGet.getColumnName()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].\"quoted field\" FROM t", true); + rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class); + assertEquals("\"quoted field\"", rowGet.getColumnName()); + } + + @Test + void testNavigationInClauses() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a[0].b = 1", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t ORDER BY a[0].b", true); + } + + @Test + void testNavigationThenCastAndJson() throws JSQLParserException { + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b::text FROM t", true); + CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class); + assertInstanceOf(RowGetExpression.class, cast.getLeftExpression()); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b -> 'x' FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b[1 : 2].c FROM t", true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0].b[1][2].c FROM t", true); + } + + @Test + void testNavigationDoesNotSwallowQualifiedColumns() throws JSQLParserException { + // the navigation loop must not change how plain qualified columns parse + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b FROM t", true); + Column column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertNull(column.getArrayConstructor()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b.c FROM t", true); + column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a.b.c", column.toString()); + + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a.b[0] FROM t", true); + column = select.getSelectItem(0).getExpression(Column.class); + assertEquals("a", column.getTable().getName()); + assertEquals("b", column.getColumnName()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + + // a bracket group after a subscript stays on the ArrayExpression slot, + // not on the navigation loop + select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a[0]['k'] FROM t", true); + ArrayExpression array = select.getSelectItem(0).getExpression(ArrayExpression.class); + assertNotNull(array); + column = assertInstanceOf(Column.class, array.getObjExpression()); + assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor()); + } + + @Test + void testMalformedNavigationRejected() { + assertThrows(JSQLParserException.class, + () -> TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a[0].* FROM t", true)); + } + }