diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6fd8d0375..8df19a2c0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -432,7 +432,7 @@ public class CCJSqlParser extends AbstractJSqlParser { // True when a fresh evaluation started at this token would pass all // first-token guards and run the same chain walk. private boolean isFunctionAheadChainStartEligible(Token t) { - return !t.image.equals("{") && t.kind != K_APPROXIMATE && !isNonFunctionKeyword(t) + return !t.image.equals("{") && !t.image.equals("[") && t.kind != K_APPROXIMATE && !isNonFunctionKeyword(t) && t.kind != S_LONG && t.kind != S_DOUBLE && t.kind != S_HEX && t.kind != S_CHAR_LITERAL && t.kind != OPENING_BRACKET && t.kind != CLOSING_BRACKET && t.kind != EOF; @@ -465,7 +465,7 @@ public class CCJSqlParser extends AbstractJSqlParser { // First token must not be a literal, bracket, or EOF if (t.kind == S_LONG || t.kind == S_DOUBLE || t.kind == S_HEX || t.kind == S_CHAR_LITERAL || t.kind == OPENING_BRACKET - || t.kind == CLOSING_BRACKET || t.kind == EOF) { + || t.kind == CLOSING_BRACKET || t.kind == EOF || "[".equals(t.image)) { return 0; } i++; @@ -10939,7 +10939,7 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(3, {!interrupted}) [ "<" type=ColDataType() ">" ] retval=ArrayConstructor(true) { if (type!=null) ((ArrayConstructor) retval).setDataType(type); } - | LOOKAHEAD(3, {!interrupted}) retval=ArrayConstructor(false) + | LOOKAHEAD(1, {!interrupted}) retval=ArrayConstructor(false) | LOOKAHEAD(2, {!interrupted}) retval = NextValExpression() @@ -11242,29 +11242,25 @@ RangeExpression RangeExpression(Expression startExpression): } ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { - ExpressionList expList = new ExpressionList(); - ArrayConstructor array = new ArrayConstructor(expList, arrayKeyword); + ExpressionList expList = new ExpressionList(); Expression exp; } { "[" [ - ( - LOOKAHEAD(3) exp = Expression() [ exp=RangeExpression(exp) ] - | - exp = ArrayConstructor(false) - ) { expList.add(exp); } - - ( - "," - ( - LOOKAHEAD(3) exp = Expression() [ exp=RangeExpression(exp) ] - | - exp = ArrayConstructor(false) - ){ expList.add(exp); } - )* + exp=ArrayElement() { expList.add(exp); } + ( "," exp=ArrayElement() { expList.add(exp); } )* ] "]" - { return array; } + { return new ArrayConstructor(expList, arrayKeyword); } +} + +Expression ArrayElement(): +{ + Expression expression; +} +{ + expression=Expression() [ expression=RangeExpression(expression) ] + { return expression; } } MapExpression MapExpression() #MapExpression: diff --git a/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java b/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java new file mode 100644 index 000000000..10e83537a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/ArrayConstructorTest.java @@ -0,0 +1,110 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ArrayConstructorTest { + @ParameterizedTest + @ValueSource(strings = {"ARRAY['a', (1 + 0)::text]", "ARRAY[(1 + 2), 3]", + "ARRAY[1, (2 + 3), 4]", "ARRAY[1, ((2 * 3) + 4)]", + "ARRAY[(SELECT 1), 2]", "ARRAY[NULL, COALESCE(NULL, (2 + 3))]", + "ARRAY[CASE WHEN 1 < 2 THEN (3 + 4) ELSE 5 END, 6]", + "ARRAY[ARRAY[(1 + 2), 3], ARRAY[4, (5 + 6)]]", + "ARRAY[[(1 + 2), 3], [4, (5 + 6)]]", "ARRAY[]::integer[]", + "ARRAY[ROW(1, 2), ROW(3, 4)]", "ARRAY[(1 < 2), (3 > 4)]"}) + void parsesGeneralExpressionsAtEveryArrayPosition(String expression) throws Exception { + for (boolean complex : List.of(false, true)) { + Statement statement = CCJSqlParserUtil.parse("SELECT " + expression, + p -> p.withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex)); + roundTrip(statement); + } + } + + @Test + void retainsCastAndParenthesesInTheElementAst() throws Exception { + PlainSelect select = (PlainSelect) parse("SELECT ARRAY['a', (1 + 0)::text]"); + ArrayConstructor array = assertInstanceOf(ArrayConstructor.class, + select.getSelectItem(0).getExpression()); + assertEquals(2, array.getExpressions().size()); + CastExpression cast = assertInstanceOf(CastExpression.class, array.getExpressions().get(1)); + assertInstanceOf(ParenthesedExpressionList.class, cast.getLeftExpression()); + cast.setLeftExpression(new LongValue(42)); + assertEquals("SELECT ARRAY['a', 42::text]", select.toString()); + roundTrip(select); + } + + @Test + void visitsElementsAndFindsTablesInScalarSubqueries() throws Exception { + Statement statement = parse("SELECT ARRAY[1, (SELECT value FROM hidden), (2 + 3)]"); + assertThat(new TablesNamesFinder().getTables(statement)).containsExactly("hidden"); + List seen = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + seen.add(value.getValue()); + return getBuilder().append(value.getValue() + 10); + } + }; + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of(1L, 2L, 3L), seen); + assertEquals("SELECT ARRAY[11, (SELECT value FROM hidden), (12 + 13)]", output.toString()); + roundTrip(parse(output.toString())); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT ARRAY[1,]", "SELECT ARRAY[,1]", "SELECT ARRAY[(1 +)]", + "SELECT ARRAY[1, (2 + 3]", "SELECT ARRAY[[1,2],]"}) + void rejectsIncompleteElements(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT [1, (2 + 3)]", "SELECT [(1 + 2), 3]", + "SELECT ARRAY[1, (2 + 3)]", + "SELECT ARRAY[1:3]", "SELECT a[(1 + 2)] FROM t", + "SELECT a.b[1:2].c FROM t", "SELECT ARRAY[ARRAY[], ARRAY[]]"}) + void preservesExistingArrayAndSubscriptForms(String sql) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws Exception { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } +}