diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 51dd3493a..d73163f8c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -874,6 +874,9 @@ public Void visit(WithItem withItem, S context) { if (body instanceof Statement) { ((Statement) body).accept(analysis.statements, context); } + if (withItem.getExpression() != null) { + withItem.getExpression().accept(new FeatureExpressionVisitor(analysis), context); + } return null; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 051f3929e..2e476c90d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -313,6 +313,19 @@ public T visit(WithItem withItem, S context) { return ((Statement) body).accept(statementVisitor, context); } + // a ClickHouse expression alias (WITH AS ): a + // parenthesized subquery stays on the select path, any other expression + // goes to the expression visitor + Expression expression = withItem.getExpression(); + if (expression != null) { + if (expression instanceof Select) { + return ((Select) expression).accept(this, context); + } + if (expressionVisitor != null) { + return expression.accept(expressionVisitor, context); + } + } + // no statement visitor available: skip the body rather than fail return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java index 767f96846..601165c3d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java @@ -30,6 +30,7 @@ public class WithItem implements Serializable { private Alias alias; private List> withItemList; private WithFunctionDeclaration withFunctionDeclaration; + private Expression expression; private WithSearchClause searchClause; private WithCycleClause cycleClause; private boolean recursive = false; @@ -45,6 +46,25 @@ public WithItem() { this(null, (Alias) null); } + /** + * The aliased expression of a WITH item like ClickHouse's + * {@code WITH AS }, where the item does not hold a statement. + * + * @return the expression of this WITH item, or null for statement-based (CTE) items + */ + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public WithItem withExpression(Expression expression) { + this.setExpression(expression); + return this; + } + public K getParenthesedStatement() { return statement; } @@ -184,6 +204,12 @@ public String toString() { StringBuilder builder = new StringBuilder(); if (withFunctionDeclaration != null) { builder.append(withFunctionDeclaration); + } else if (expression != null) { + builder.append(expression); + if (alias != null) { + builder.append(" AS ").append(alias.getName()); + } + appendRecursiveClausesTo(builder, expr -> builder.append(expr)); } else { builder.append(recursive ? "RECURSIVE " : ""); if (alias != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index ece42015b..4b5f44b3f 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -354,6 +354,10 @@ public Void visit(WithItem withItem, S context) { } // dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert) withItem.accept((StatementVisitor) this, context); + // an expression alias (WITH expr AS name) may read tables on its own + if (withItem.getExpression() != null) { + withItem.getExpression().accept(this, context); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java index 9231d2b0e..59338fc64 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java @@ -85,6 +85,14 @@ StringBuilder deparseWithItem(WithItem item, S context) { if (item.getWithFunctionDeclaration() != null) { return builder.append(item.getWithFunctionDeclaration()); } + if (item.getExpression() != null) { + item.getExpression().accept(expressions, context); + if (item.getAlias() != null) { + builder.append(" AS ").append(item.getAlias().getName()); + } + return item.appendRecursiveClausesTo(builder, + expression -> expression.accept(expressions, context)); + } if (item.isRecursive()) { builder.append("RECURSIVE "); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index b8347c325..54b9a2b26 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -416,6 +416,9 @@ public Void visit(WithItem withItem, S context) { if (withItem.getCycleClause() != null) { withItem.getCycleClause().accept(getValidator(ExpressionValidator.class), context); } + if (withItem.getExpression() != null) { + withItem.getExpression().accept(getValidator(ExpressionValidator.class), context); + } withItem.accept(getValidator(StatementValidator.class), context); return null; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index b763ea3b6..9d448cc33 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -89,6 +89,7 @@ import net.sf.jsqlparser.statement.export.*; import net.sf.jsqlparser.statement.lock.*; import java.util.*; import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.CancellationException; import net.sf.jsqlparser.statement.select.SetOperationList.SetOperationType; import java.util.logging.Level; @@ -711,6 +712,90 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** The literal tokens that may complete a typed literal in {@link #isImplicitCastAhead()}. */ + private boolean isTypedLiteralFollower(int kind) { + return kind == S_CHAR_LITERAL || kind == S_LONG || kind == S_DOUBLE; + } + + /** + * Whether the token could start a {@link #RelObjectName()}, mirroring the token + * alternatives accepted there (base identifiers, DATA_TYPE, non-reserved keywords + * and the reserved-keyword-as-identifier list). + */ + private static boolean isIdentifierishNameToken(Token t) { + switch (t.kind) { + case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE: + case K_DATETIMELITERAL: case K_DATE_LITERAL: + case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: case K_CREATE: + case K_DEFAULT: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: + case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: case K_LEFT: + case K_LIMIT: case K_NEXTVAL: case K_OFFSET: case K_ON: case K_OPTIMIZE: + case K_ORDER: case K_PROCEDURE: case K_PUBLIC: case K_QUALIFY: case K_RIGHT: + case K_FILE: case K_SET: case K_SOME: case K_START: case K_TABLES: + case K_TOP: case K_VALUE: case K_VALUES: + return true; + default: + return t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD; + } + } + + private Token nextWithItemToken(Token current) { + if (current.next == null) { + current.next = token_source.getNextToken(); + } + return current.next; + } + + private Token skipBalancedBracketGroup(Token current) { + int depth = 0; + while (!interrupted && current.kind != EOF) { + if (current.kind == OPENING_BRACKET) { + depth++; + } else if (current.kind == CLOSING_BRACKET) { + if (--depth == 0) { + return nextWithItemToken(current); + } + } + current = nextWithItemToken(current); + } + if (interrupted) { + throw new CancellationException("Parsing interrupted"); + } + return null; + } + + private boolean isWithExpressionAliasAhead() { + try { + Token t1 = getToken(1); + if (t1.kind == K_FUNCTION || t1.kind == K_RECURSIVE) { + return false; + } + if (!isIdentifierishNameToken(t1)) { + return true; + } + Token next = getToken(2); + if (next.kind == OPENING_BRACKET) { + next = skipBalancedBracketGroup(next); + if (next == null) { + return false; + } + } + if (next.kind != K_AS) { + return true; + } + next = nextWithItemToken(next); + if (next.kind == K_NOT) { + next = nextWithItemToken(next); + } + if (next.kind == K_MATERIALIZED) { + next = nextWithItemToken(next); + } + return next.kind != OPENING_BRACKET; + } catch (TokenMgrException e) { + return false; + } + } + protected boolean isImplicitCastAhead() { try { int k1 = getToken(1).kind; @@ -721,18 +806,33 @@ public class CCJSqlParser extends AbstractJSqlParser { if (k1 == K_JSON) return getToken(2).kind == S_CHAR_LITERAL; if (k1 != DATA_TYPE) return false; int k2 = getToken(2).kind; - if (k2 != OPENING_BRACKET) return true; // DATA_TYPE literal - simple cast - // DATA_TYPE( ... ) - precision cast if content is only S_LONG literals - // function call otherwise (e.g. UUID(), VARCHAR(col)) + if (k2 != OPENING_BRACKET) { + // a bare DATA_TYPE is a typed literal (INT '5') only when a trailing + // literal completes it, possibly after further type-name tokens + // (DOUBLE PRECISION '1'); otherwise it is an identifier, e.g. a + // column named number or a keyword-named lambda alias + if (isTypedLiteralFollower(k2)) { + return true; + } + return k2 == DATA_TYPE && isTypedLiteralFollower(getToken(3).kind); + } + // DATA_TYPE( ... ) - precision cast only when a trailing literal completes + // the typed literal (DATA_TYPE(N) 'lit'); a function call otherwise, + // e.g. double(5), int(5), VARCHAR(col), UUID() int k3 = getToken(3).kind; if (k3 == CLOSING_BRACKET) return false; // DATA_TYPE() - empty call if (k3 != S_LONG) return false; // DATA_TYPE(expr) - function call int k4 = getToken(4).kind; - if (k4 == CLOSING_BRACKET) return true; // DATA_TYPE(N) - precision cast - if (k4 != K_COMMA) return false; // DATA_TYPE(N expr) - function call - int k5 = getToken(5).kind; - if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call - return getToken(6).kind == CLOSING_BRACKET; // DATA_TYPE(N,M) - precision cast + if (k4 == K_COMMA) { + int k5 = getToken(5).kind; + if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call + if (getToken(6).kind != CLOSING_BRACKET) { + return false; // DATA_TYPE(N,M expr) - function call + } + return isTypedLiteralFollower(getToken(7).kind); // DATA_TYPE(N,M) 'lit' + } + if (k4 != CLOSING_BRACKET) return false; // DATA_TYPE(N expr) - function call + return isTypedLiteralFollower(getToken(5).kind); // DATA_TYPE(N) 'lit' } catch (TokenMgrException e) { return false; } @@ -7011,6 +7111,7 @@ WithItem WithItem() #WithItem: List> selectItems = null; WithFunctionDeclaration withFunctionDeclaration = null; ParenthesedStatement statement = null; + Expression expr = null; WithSearchClause withSearchClause = null; WithCycleClause withCycleClause = null; WithItem withItem; @@ -7023,6 +7124,7 @@ WithItem WithItem() #WithItem: withItem = new WithItem().withWithFunctionDeclaration(withFunctionDeclaration); } | + LOOKAHEAD({ !isWithExpressionAliasAhead() }) ( [ LOOKAHEAD(2) { recursive = true; } ] name=RelObjectName() @@ -7044,6 +7146,16 @@ WithItem WithItem() #WithItem: .withWithItemList(selectItems); } ) + | + ( + // ClickHouse style expression alias: WITH AS + expr = WithItemExpression() + + name = RelObjectName() + { + withItem = new WithItem().withExpression(expr).withAlias(new Alias(name, false)); + } + ) ) [ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ] [ withCycleClause = WithCycleClause() { withItem.setCycleClause(withCycleClause); } ] @@ -7052,6 +7164,31 @@ WithItem WithItem() #WithItem: } } +Expression WithItemExpression(): +{ + LambdaExpression lambdaExpression; + Expression expr; +} +{ + ( + // unparenthesized parameter, e.g. x -> x * 2 + LOOKAHEAD( RelObjectName() "->" ) + expr = LambdaExpression() + | + // parenthesized parameter list, e.g. (value) -> value + 1 + LOOKAHEAD( ParenthesedColumnList() "->" ) + expr = LambdaExpression() + | + // whole lambda wrapped in parentheses, e.g. (x -> x * 2) + LOOKAHEAD( "(" ( RelObjectName() | ParenthesedColumnList() ) "->" ) + "(" lambdaExpression = LambdaExpression() ")" + { expr = new ParenthesedExpressionList(lambdaExpression); } + | + expr = Expression() + ) + { return expr; } +} + WithSearchClause WithSearchClause() #WithSearchClause: { Token orderingToken; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java index 696bf88a0..265c9621d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java @@ -11,13 +11,20 @@ import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.ColumnsExpression; import net.sf.jsqlparser.expression.ColumnsTransformer; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -488,4 +495,169 @@ public void testColumnsAliasAstAtEachBoundary(String suffix) throws JSQLParserEx } } } + + @Test + public void testWithLambdaExpressionAliasIssue2632() throws JSQLParserException { + // https://github.com/JSQLParser/JSqlParser/issues/2632 + String sql = "WITH (x -> x * 2) AS double SELECT double(5)"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + WithItem withItem = select.getWithItemsList().get(0); + Assertions.assertTrue(withItem.getExpression() instanceof ParenthesedExpressionList); + LambdaExpression lambda = (LambdaExpression) ((ParenthesedExpressionList) withItem + .getExpression()).get(0); + Assertions.assertEquals(List.of("x"), lambda.getIdentifiers()); + Assertions.assertEquals("double", withItem.getAliasName()); + Assertions.assertEquals("double(5)", + ((PlainSelect) select.getSelectBody()).getSelectItems().get(0).getExpression() + .toString()); + } + + @Test + public void testWithLambdaExpressionAliasDocForms() throws JSQLParserException { + // documented lambda form: (param) -> expr — the AST keeps the parameter name only, + // so the single-parameter parentheses follow the same deparse convention as + // function-argument lambdas and are not preserved + Select select = (Select) CCJSqlParserUtil + .parse("WITH (value) -> value + 1 AS increment SELECT increment(5)"); + LambdaExpression lambda = (LambdaExpression) select.getWithItemsList().get(0) + .getExpression(); + Assertions.assertEquals(List.of("value"), lambda.getIdentifiers()); + Assertions.assertEquals("increment", select.getWithItemsList().get(0).getAliasName()); + assertSqlCanBeParsedAndDeparsed("WITH value -> value + 1 AS increment SELECT increment(5)", + true); + + // multi-parameter lambda with a parenthesized parameter list + assertSqlCanBeParsedAndDeparsed( + "WITH (id, extension) -> concat(lower(id), extension) AS gen_name " + + "SELECT gen_name('A', 'B')", + true); + + // the whole lambda may be wrapped in parentheses (issue #2632 style), multi-param too + assertSqlCanBeParsedAndDeparsed("WITH ((x, y) -> x + y) AS f SELECT f(1, 2)", true); + + // unparenthesized parameter is accepted by ClickHouse as well + assertSqlCanBeParsedAndDeparsed("WITH x -> x * 2 AS double SELECT double(5)", true); + } + + @Test + public void testWithExpressionAlias() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("WITH 1 AS one SELECT one + 1", true); + assertSqlCanBeParsedAndDeparsed("WITH 2 + 2 AS a SELECT a", true); + assertSqlCanBeParsedAndDeparsed( + "WITH '2019-08-01 15:23:00' AS ts_upper_bound SELECT ts_upper_bound", true); + assertSqlCanBeParsedAndDeparsed("WITH sum(number) AS s SELECT s FROM numbers(3)", true); + + // a bare column reference aliased to another name + assertSqlCanBeParsedAndDeparsed("WITH x AS y SELECT y FROM (SELECT 42 AS x)", true); + + // an expression alias may reference an earlier WITH item, and the expression + // may itself be a compound one + assertSqlCanBeParsedAndDeparsed( + "WITH (SELECT sum(number) FROM numbers(10)) AS total, total * 2 AS doubled " + + "SELECT doubled", + true); + assertSqlCanBeParsedAndDeparsed("WITH sum(x) + 1 AS y SELECT y", true); + assertSqlCanBeParsedAndDeparsed( + "WITH CASE WHEN 1 < 2 THEN 'a' ELSE 'b' END AS c SELECT c", true); + assertSqlCanBeParsedAndDeparsed("WITH t AS u, v AS w SELECT u, w", true); + } + + @Test + public void testWithParenthesedSubqueryAlias() throws JSQLParserException { + String sql = "WITH (SELECT a FROM src) AS v SELECT a FROM v, dst"; + assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(Set.of("src", "dst"), TablesNamesFinder.findTables(sql)); + } + + @Test + public void testWithExpressionAliasMixedItems() throws JSQLParserException { + String sql = "WITH 1 AS one, (x -> x * 2) AS double, t AS (SELECT 1 AS a) " + + "SELECT one, double(5), a FROM t"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testWithStandardCteUnaffected() throws JSQLParserException { + // guards: the classic CTE shapes must keep parsing exactly as before + assertSqlCanBeParsedAndDeparsed("WITH t AS (SELECT 1 AS a) SELECT a FROM t", true); + assertSqlCanBeParsedAndDeparsed("WITH t (a, b) AS (SELECT 1 AS a, 2 AS b) SELECT a FROM t", + true); + assertSqlCanBeParsedAndDeparsed( + "WITH RECURSIVE q AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM q WHERE n < 3) " + + "SELECT n FROM q", + true); + assertSqlCanBeParsedAndDeparsed( + "WITH a AS MATERIALIZED (SELECT 1 AS b) SELECT b FROM a", true); + assertSqlCanBeParsedAndDeparsed( + "WITH a AS NOT MATERIALIZED (SELECT 1 AS b) SELECT b FROM a", true); + } + + @Test + public void testDataTypeNumericArgsFunctionCall() throws JSQLParserException { + // ClickHouse names lambda aliases after type-like keywords (issue #2632 uses double), + // and calls them like functions: double(5). DATA_TYPE(N) without a trailing literal + // is a function call, not a typed literal. + Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT double(5)", true); + Assertions.assertTrue(((PlainSelect) select.getSelectBody()).getSelectItems().get(0) + .getExpression() instanceof Function); + assertSqlCanBeParsedAndDeparsed("SELECT int(5), varchar(5), double(5) + 1", true); + + // the same family: a DATA_TYPE keyword used as a plain column name, e.g. the + // documented ClickHouse corpus sum(number) FROM numbers(10) + assertSqlCanBeParsedAndDeparsed("SELECT number FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT sum(number) FROM numbers(3)", true); + + // guards: typed literals with a trailing literal keep their cast reading + assertSqlCanBeParsedAndDeparsed("SELECT INT '5'", true); + assertSqlCanBeParsedAndDeparsed("SELECT DECIMAL(10,2) '1.5'", true); + } + + @Test + public void testWithExpressionAliasTraversableByVisitorAdapter() throws JSQLParserException { + // a generic SelectVisitorAdapter traversal must reach the expression body of a + // WITH AS item, including nested table references, + // exactly as it reaches the body of a classic CTE + List plainSelects = new ArrayList<>(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>() { + @Override + public Void visit(PlainSelect plainSelect, S context) { + plainSelects.add(String.valueOf(plainSelect.getFromItem())); + return super.visit(plainSelect, context); + } + }; + Select select = (Select) CCJSqlParserUtil + .parse("WITH (SELECT 1 FROM src) AS v SELECT a FROM v, dst"); + select.getSelectBody().accept(visitor, null); + Assertions.assertTrue(plainSelects.contains("src"), + "expression body table missing from traversal: " + plainSelects); + + // a lambda alias is handed to the expression visitor + List expressions = new ArrayList<>(); + SelectVisitorAdapter exprVisitor = + new SelectVisitorAdapter<>(new ExpressionVisitorAdapter<>() { + @Override + public Void visit(LambdaExpression lambdaExpression, S context) { + expressions.add(String.valueOf(lambdaExpression)); + return super.visit(lambdaExpression, context); + } + }); + Select lambdaSelect = (Select) CCJSqlParserUtil + .parse("WITH (x -> x * 2) AS double SELECT double(5)"); + Assertions.assertNotNull(lambdaSelect.getWithItemsList().get(0).getExpression()); + lambdaSelect.getSelectBody().accept(exprVisitor, null); + Assertions.assertEquals(List.of("x -> x * 2"), expressions); + } + + @Test + public void testWithExpressionAliasRecursiveClausesSerialize() throws JSQLParserException { + // the tolerant-grammar SEARCH/CYCLE suffix must survive serialization on the + // expression branch instead of being dropped + String sql = "WITH 1 AS one SEARCH DEPTH FIRST BY one SET o SELECT one"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getWithItemsList().get(0).getSearchClause()); + + String sql2 = "WITH 1 AS one CYCLE one SET c USING p SELECT one"; + Select select2 = (Select) assertSqlCanBeParsedAndDeparsed(sql2, true); + Assertions.assertNotNull(select2.getWithItemsList().get(0).getCycleClause()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java b/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java index f84bfa4b7..ecc405a97 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/WithItemTest.java @@ -10,16 +10,117 @@ package net.sf.jsqlparser.statement.select; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserConstants; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.ParseException; +import net.sf.jsqlparser.parser.Token; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; class WithItemTest { + @ParameterizedTest + @CsvSource({"2047, 1", "2047, -1", "2048, 1", "4096, 1"}) + void testLargeExpressionAlias(int arguments, String firstArgument) throws JSQLParserException { + String expression = "coalesce(" + firstArgument + ", 1".repeat(arguments - 1) + ")"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + expression + " AS v, t AS (SELECT v) SELECT v FROM t", true); + WithItem item = select.getWithItemsList().get(0); + Function function = assertInstanceOf(Function.class, item.getExpression()); + assertEquals("v", item.getAliasName()); + assertNull(item.getParenthesedStatement()); + assertEquals("coalesce", function.getName()); + assertEquals(arguments, function.getParameters().size()); + assertEquals(firstArgument, function.getParameters().get(0).toString()); + assertEquals("1", function.getParameters().get(arguments - 1).toString()); + assertNotNull(select.getWithItemsList().get(1).getParenthesedStatement()); + } + + @ParameterizedTest + @ValueSource(ints = {2047, 2048}) + void testLargeCteColumnList(int columns) throws JSQLParserException { + String names = IntStream.range(0, columns).mapToObj(i -> "c" + i) + .collect(Collectors.joining(", ")); + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH t(" + names + ") AS (SELECT 1" + ", 1".repeat(columns - 1) + + ") SELECT * FROM t", + true); + WithItem item = select.getWithItemsList().get(0); + assertEquals("t", item.getAliasName()); + assertNull(item.getExpression()); + assertNotNull(item.getParenthesedStatement()); + assertEquals(columns, item.getWithItemList().size()); + assertEquals("c" + (columns - 1), item.getWithItemList().get(columns - 1).toString()); + } + + @Test + void testLargeNestedExpressionAlias() throws JSQLParserException { + String expression = "concat(concat('(', ')')" + ", 'x'".repeat(2047) + ")"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + expression + " AS v SELECT v", true); + Function function = assertInstanceOf(Function.class, + select.getWithItemsList().get(0).getExpression()); + assertEquals(2048, function.getParameters().size()); + assertEquals("concat('(', ')')", function.getParameters().get(0).toString()); + } + + @ParameterizedTest + @ValueSource(ints = {1, 64, 65}) + void testQualifiedExpressionAlias(int qualifiers) throws JSQLParserException { + String name = "s.".repeat(qualifiers) + "coalesce"; + Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH " + name + "(1, 2) AS v SELECT v", true); + Function function = assertInstanceOf(Function.class, + select.getWithItemsList().get(0).getExpression()); + assertEquals(name, function.getName()); + assertEquals(2, function.getParameters().size()); + } + + @ParameterizedTest + @ValueSource(ints = {2, 2048}) + @Timeout(5) + void testUnclosedExpressionAlias(int arguments) { + String sql = "WITH coalesce(1" + ", 1".repeat(arguments - 1); + assertThrows(ParseException.class, () -> CCJSqlParserUtil.newParser(sql).Statement()); + } + + @Test + void testInterruptedWithItemLookaheadStopsReading() { + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "WITH coalesce(1" + ", 1".repeat(512) + ") AS v SELECT v"); + AtomicBoolean reachedAlias = new AtomicBoolean(); + parser.token_source = Mockito.spy(parser.token_source); + Mockito.doAnswer(invocation -> { + Token token = (Token) invocation.callRealMethod(); + if (token.kind == CCJSqlParserConstants.S_LONG) { + parser.interrupted = true; + } else if (token.kind == CCJSqlParserConstants.K_AS) { + reachedAlias.set(true); + } + return token; + }).when(parser.token_source).getNextToken(); + assertThrows(CancellationException.class, parser::Statement); + assertFalse(reachedAlias.get()); + } + @Test void testNotMaterializedIssue2251() throws JSQLParserException { String sqlStr = "WITH devices AS NOT MATERIALIZED (\n"