Skip to content

Commit baea69a

Browse files
committed
feat(parser): parse navigation chains after array subscripts
Redshift SUPER navigation like recommendations[0].language_id failed to parse: the dot after the subscript was unreachable, because field access was only wired inside the parenthesized branch of PrimaryExpression. Add a postfix navigation loop that alternates field accesses and subscripts after any primary expression, reusing the existing RowGetExpression and ArrayExpression nodes. Chains like a.b[0].c[1].d now parse and round-trip as well. The javacc warning count stays at 11. Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent ff2bc8d commit baea69a

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8535,9 +8535,24 @@ Expression ArrayExpression(Expression obj): {
85358535
{ return expr; }
85368536
}
85378537

8538+
Expression NavigationStep(Expression base):
8539+
{
8540+
Expression step = null;
8541+
String nm = null;
8542+
}
8543+
{
8544+
(
8545+
"." nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); }
8546+
|
8547+
step = ArrayExpression(base)
8548+
)
8549+
{ return step; }
8550+
}
8551+
85388552
Expression PrimaryExpression() #PrimaryExpression:
85398553
{
85408554
Expression retval = null;
8555+
Expression nxt = null;
85418556
Expression expression = null;
85428557
CastExpression castExpr = null;
85438558
TimezoneExpression timezoneExpr = null;
@@ -8713,6 +8728,12 @@ Expression PrimaryExpression() #PrimaryExpression:
87138728

87148729
[ LOOKAHEAD(2) retval = ArrayExpression(retval) ]
87158730

8731+
// navigation chains, e.g. Redshift SUPER col[0].field[1].sub: field accesses
8732+
// and subscripts may alternate freely after the primary expression
8733+
(
8734+
LOOKAHEAD(2) nxt = NavigationStep(retval) { retval = nxt; }
8735+
)*
8736+
87168737
( LOOKAHEAD(2) "::" type=ColDataType() {
87178738
castExpr = new CastExpression();
87188739
castExpr.setUseCastKeyword(false);

src/test/java/net/sf/jsqlparser/expression/ArrayExpressionTest.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,116 @@ void testColumnArrayExpression() throws JSQLParserException {
3030
assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor());
3131
}
3232

33+
@Test
34+
void testNavigationAfterSubscript() throws JSQLParserException {
35+
// Redshift SUPER navigation: subscript then field access
36+
String sqlStr = "SELECT recommendations[0].language_id FROM recs";
37+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
38+
SelectItem<?> selectItem = select.getSelectItem(0);
39+
40+
RowGetExpression rowGet = selectItem.getExpression(RowGetExpression.class);
41+
assertNotNull(rowGet);
42+
assertEquals("language_id", rowGet.getColumnName());
43+
Column column = assertInstanceOf(Column.class, rowGet.getExpression());
44+
assertEquals("recommendations", column.getColumnName());
45+
assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor());
46+
}
47+
48+
@Test
49+
void testAlternatingNavigationChain() throws JSQLParserException {
50+
// field accesses and subscripts may alternate freely after the primary expression
51+
String sqlStr = "SELECT a.b[0].c[1].d FROM t";
52+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
53+
SelectItem<?> selectItem = select.getSelectItem(0);
54+
55+
RowGetExpression outer = selectItem.getExpression(RowGetExpression.class);
56+
assertNotNull(outer);
57+
assertEquals("d", outer.getColumnName());
58+
ArrayExpression subscript = assertInstanceOf(ArrayExpression.class, outer.getExpression());
59+
assertInstanceOf(LongValue.class, subscript.getIndexExpression());
60+
RowGetExpression inner =
61+
assertInstanceOf(RowGetExpression.class, subscript.getObjExpression());
62+
assertEquals("c", inner.getColumnName());
63+
Column column = assertInstanceOf(Column.class, inner.getExpression());
64+
assertEquals("a", column.getTable().getName());
65+
assertEquals("b", column.getColumnName());
66+
assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor());
67+
}
68+
69+
@Test
70+
void testNavigationFieldNames() throws JSQLParserException {
71+
// dotted continuations accept FROM/SELECT/CURRENT and quoted names,
72+
// same as ColumnIdentifier's dotted continuation (RelObjectNameExt)
73+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
74+
"SELECT a[0].from FROM t", true);
75+
RowGetExpression rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class);
76+
assertEquals("from", rowGet.getColumnName());
77+
78+
select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
79+
"SELECT a[0].\"quoted field\" FROM t", true);
80+
rowGet = select.getSelectItem(0).getExpression(RowGetExpression.class);
81+
assertEquals("\"quoted field\"", rowGet.getColumnName());
82+
}
83+
84+
@Test
85+
void testNavigationInClauses() throws JSQLParserException {
86+
TestUtils.assertSqlCanBeParsedAndDeparsed(
87+
"SELECT * FROM t WHERE a[0].b = 1", true);
88+
TestUtils.assertSqlCanBeParsedAndDeparsed(
89+
"SELECT * FROM t ORDER BY a[0].b", true);
90+
}
91+
92+
@Test
93+
void testNavigationThenCastAndJson() throws JSQLParserException {
94+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
95+
"SELECT a[0].b::text FROM t", true);
96+
CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class);
97+
assertInstanceOf(RowGetExpression.class, cast.getLeftExpression());
98+
99+
TestUtils.assertSqlCanBeParsedAndDeparsed(
100+
"SELECT a[0].b -> 'x' FROM t", true);
101+
TestUtils.assertSqlCanBeParsedAndDeparsed(
102+
"SELECT a.b[1 : 2].c FROM t", true);
103+
TestUtils.assertSqlCanBeParsedAndDeparsed(
104+
"SELECT a[0].b[1][2].c FROM t", true);
105+
}
106+
107+
@Test
108+
void testNavigationDoesNotSwallowQualifiedColumns() throws JSQLParserException {
109+
// the navigation loop must not change how plain qualified columns parse
110+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
111+
"SELECT a.b FROM t", true);
112+
Column column = select.getSelectItem(0).getExpression(Column.class);
113+
assertEquals("a", column.getTable().getName());
114+
assertEquals("b", column.getColumnName());
115+
assertNull(column.getArrayConstructor());
116+
117+
select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
118+
"SELECT a.b.c FROM t", true);
119+
column = select.getSelectItem(0).getExpression(Column.class);
120+
assertEquals("a.b.c", column.toString());
121+
122+
select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
123+
"SELECT a.b[0] FROM t", true);
124+
column = select.getSelectItem(0).getExpression(Column.class);
125+
assertEquals("a", column.getTable().getName());
126+
assertEquals("b", column.getColumnName());
127+
assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor());
128+
129+
// a bracket group after a subscript stays on the ArrayExpression slot,
130+
// not on the navigation loop
131+
select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
132+
"SELECT a[0]['k'] FROM t", true);
133+
ArrayExpression array = select.getSelectItem(0).getExpression(ArrayExpression.class);
134+
assertNotNull(array);
135+
column = assertInstanceOf(Column.class, array.getObjExpression());
136+
assertInstanceOf(ArrayConstructor.class, column.getArrayConstructor());
137+
}
138+
139+
@Test
140+
void testMalformedNavigationRejected() {
141+
assertThrows(JSQLParserException.class,
142+
() -> TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a[0].* FROM t", true));
143+
}
144+
33145
}

0 commit comments

Comments
 (0)