From b5adb60628ea504661c8a3b10f3dd2d5a414c52c Mon Sep 17 00:00:00 2001 From: Chen Jianjin <161543912+jianjindream@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:00:02 +0800 Subject: [PATCH 001/387] fix: support SQL Server CREATE INDEX INCLUDE columns (#2462) - fixes #2462 --- src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 4 ++++ .../net/sf/jsqlparser/statement/create/CreateIndexTest.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f1711daaff..5da3aa1819 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -10507,6 +10507,7 @@ CreateIndex CreateIndex(): CreateIndex createIndex = new CreateIndex(); Table table = null; List colNames; + List includeColumns; String indexType; Index index = null; List parameter = new ArrayList(); @@ -10535,6 +10536,9 @@ CreateIndex CreateIndex(): ) ) colNames = IndexColumnsWithParamsList() + [ includeColumns=ColumnsNamesList() + { tailParameters.add("INCLUDE (" + String.join(", ", includeColumns) + ")"); } + ] ( LOOKAHEAD(2) parameter=CreateParameter() { tailParameters.addAll(parameter); } )* { index.setColumns(colNames); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index 2096b97301..40b7504f75 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -174,4 +174,9 @@ public void testCreateIndexVisibility() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INVISIBLE", true); assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) VISIBLE", true); } + + @Test + public void testCreateIndexIncludeIssue2459() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INCLUDE (b, c)"); + } } From 7ced34d2bdee53699e6a413f694aa5a815c10d94 Mon Sep 17 00:00:00 2001 From: fudian Date: Thu, 13 Aug 2026 22:23:55 +0800 Subject: [PATCH 002/387] fix: support PostgreSQL composite row expansion (function()).* (#2463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL expands a composite-returning function call into its columns with (function_call).*, e.g. SELECT (json_populate_record(NULL::users, data)).* FROM staging_users. JSQLParser rejected the trailing .* . Composite row expansion was implemented in #2207 but afterwards disabled, because the speculative syntactic LOOKAHEAD(FunctionAllColumns()) at the PrimaryExpression entry caused a severe regression (393 ms/op vs ~86 ms/op). The AST node, deparser, validator and all visitors stayed in place; only the grammar call site was commented out. Re-enable the feature without the speculative lookahead: after a ParenthesedExpressionList wrapping a single Function is parsed, a bounded semantic follower check (isFunctionAllColumnsAhead) peeks .* and wraps the result into FunctionAllColumns. The check first compares the next two tokens and only then unwraps the already-parsed expression, so the common path (a parenthesised expression not followed by .*) bails out in two comparisons without any speculative production or backtracking. TablesNamesFinder now descends into the wrapped function so column/table references inside the expansion are not lost. Scope: only (function_call).* is supported; arbitrary (non-function expression).* remains unsupported and fails cleanly as before. Redundant surrounding parentheses are unwrapped to the inner function. Performance (gradle jmh, parseSQLStatements on performance.sql, version=latest, 10 forks x 10 iterations, 100 samples, dedicated 32-core host): master (disabled): 3.632 +/- 0.020 ms/op this change: 3.639 +/- 0.023 ms/op The +0.19% delta lies within the confidence intervals and is far from the 393 ms/op toll that motivated disabling the feature. Testing: CompositeRowExpansionTest covers the issue case, simple and no-arg functions, the INSERT...SELECT use case, multiple surrounding parentheses, and negative cases (no trailing .*, RowGet expression, non-function expression). The positive tests fail on master and pass with this change. Fixes #2412 Signed-off-by: 付典 --- .../sf/jsqlparser/util/TablesNamesFinder.java | 2 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 58 +++++++++++ .../select/CompositeRowExpansionTest.java | 97 +++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 6eea66a574..4b643742a6 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -925,7 +925,7 @@ public Void visit(AllTableColumns allTableColumns, S context) { @Override public Void visit(FunctionAllColumns functionAllColumns, S context) { - + functionAllColumns.getFunction().accept(this, 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 5da3aa1819..0cf4530edc 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -594,6 +594,55 @@ public class CCJSqlParser extends AbstractJSqlParser { return getToken(i - 1).image.equals("*"); } + /** + * Detects PostgreSQL composite row expansion {@code (function()).*} (and any + * number of surrounding parentheses), where a parenthesised expression wrapping + * a single {@link Function} is immediately followed by {@code .*}. + * + *

This is a constant-time follower check (unwraps the already-parsed + * {@code retval} and peeks the next two tokens). It deliberately avoids the + * speculative syntactic lookahead that previously degraded performance, see + * issue #2207. + * + * @param retval the expression parsed so far within the + * {@code ParenthesedExpressionList} branch of {@code PrimaryExpression} + */ + protected boolean isFunctionAllColumnsAhead(Expression retval) { + // Fast follower gate: the overwhelming majority of parenthesised + // expressions are not followed by ".*", so reject on the token stream + // before ever touching the already-parsed expression. + if (!getToken(1).image.equals(".") || !getToken(2).image.equals("*")) { + return false; + } + if (retval == null) { + return false; + } + + Expression inner = retval; + while (inner instanceof ParenthesedExpressionList) { + ParenthesedExpressionList parenthesed = (ParenthesedExpressionList) inner; + if (parenthesed.size() != 1) { + return false; + } + inner = parenthesed.get(0); + } + + return inner instanceof Function; + } + + /** + * Unwraps any number of surrounding parentheses from a parenthesised + * {@link Function} and returns the inner function. Only call this after + * {@link #isFunctionAllColumnsAhead(Expression)} has confirmed the shape. + */ + protected Function unwrapParenthesedFunction(Expression retval) { + Expression inner = retval; + while (inner instanceof ParenthesedExpressionList) { + inner = ((ParenthesedExpressionList) inner).get(0); + } + return (Function) inner; + } + /** * Follower-based disambiguation for reserved keywords in ambiguous * positions (implicit alias, clause boundary, after parenthesised @@ -7903,6 +7952,15 @@ Expression PrimaryExpression() #PrimaryExpression: } ) + // PostgreSQL composite row expansion: (function()).* + // Re-enables FunctionAllColumns, which was disabled in #2207 due to the + // performance toll of a speculative syntactic lookahead. A bounded + // semantic follower check instead reads the already-parsed retval and + // peeks the next two tokens, avoiding any speculative production. + [ LOOKAHEAD( { isFunctionAllColumnsAhead(retval) } ) + "." "*" + { retval = new FunctionAllColumns(unwrapParenthesedFunction(retval)); } ] + // RowGet Expressions ( LOOKAHEAD(2) "." tmp=RelObjectName() { retval = new RowGetExpression(retval, tmp); } )* ) diff --git a/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java b/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java new file mode 100644 index 0000000000..c2e2a09f8d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/CompositeRowExpansionTest.java @@ -0,0 +1,97 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * PostgreSQL composite row expansion {@code (function_returning_composite).*}. + * + *

+ * This feature was merged via #2207 and afterwards disabled, because the speculative syntactic + * lookahead used back then caused a severe performance regression. It is re-enabled here through a + * bounded semantic follower check, see {@code FunctionAllColumns} in the grammar. + */ +public class CompositeRowExpansionTest { + + private static FunctionAllColumns assertFunctionAllColumns(String sql) + throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Expression expression = select.getSelectItems().get(0).getExpression(); + Assertions.assertTrue(expression instanceof FunctionAllColumns, + "Expected a FunctionAllColumns select item but got " + expression.getClass()); + return (FunctionAllColumns) expression; + } + + @Test + public void testIssue2412JsonPopulateRecord() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns( + "SELECT (json_populate_record(NULL::users, data)).* FROM staging_users"); + Assertions.assertEquals("json_populate_record", result.getFunction().getName()); + } + + @Test + public void testSimpleFunctionAllColumns() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns("SELECT (foo(a, b)).* FROM t"); + Assertions.assertEquals("foo", result.getFunction().getName()); + } + + @Test + public void testPgStatFileExampleFrom2207() throws JSQLParserException { + FunctionAllColumns result = assertFunctionAllColumns( + "SELECT (pg_stat_file('postgresql.conf')).*"); + Assertions.assertEquals("pg_stat_file", result.getFunction().getName()); + } + + @Test + public void testIssue2412InsertSelectUseCase() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "INSERT INTO users SELECT (json_populate_record(NULL::users, data)).* FROM staging_users", + true); + } + + @Test + public void testMultipleSurroundingParensAreUnwrapped() throws JSQLParserException { + // Redundant parentheses around a single value are semantically transparent in + // PostgreSQL, so they are unwrapped to the inner function. The round-trip + // therefore normalises to a single surrounding pair. + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT ((((foo(a))))).* FROM t"); + Expression expression = select.getSelectItems().get(0).getExpression(); + Assertions.assertTrue(expression instanceof FunctionAllColumns); + Assertions.assertEquals("foo", ((FunctionAllColumns) expression).getFunction().getName()); + Assertions.assertEquals("(foo(a)).*", expression.toString()); + } + + @Test + public void testParenthesedFunctionWithoutExpansionUnchanged() throws JSQLParserException { + // Without the trailing .* a parenthesised function stays a plain expression. + assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)) FROM t", true); + } + + @Test + public void testRowGetExpressionAfterParenthesedFunctionUnchanged() throws JSQLParserException { + // (function()).name must keep parsing as a RowGetExpression, not be swallowed. + assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)).colname FROM t", true); + } + + @Test + public void testNonFunctionCompositeExpansionStillUnsupported() { + // Expanding an arbitrary (non-function) expression is out of scope and must + // keep failing cleanly instead of producing a wrong AST. + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT (a + b).* FROM t")); + } +} From 2989cb1d9c76774a68b67a9e7e890ad627902975 Mon Sep 17 00:00:00 2001 From: fudian Date: Fri, 14 Aug 2026 09:01:07 +0800 Subject: [PATCH 003/387] fix: allow scalar subquery as LIMIT row count (#2359) (#2464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL allows any expression, including a scalar subquery, as the LIMIT row count, e.g. LIMIT (SELECT COUNT(*) FROM t WHERE ...) or LIMIT GREATEST(0, (SELECT ...)). JSQLParser rejected these. PlainSelect disambiguated the ClickHouse "LIMIT ... BY ..." branch from a plain limit with a numeric LOOKAHEAD(7) on LimitBy(). A numeric lookahead cannot see past a long parenthesized subquery, so for LIMIT (subquery) it wrongly committed to the LIMIT BY branch and then failed at the missing BY keyword. (Short subqueries such as LIMIT (SELECT 1) happened to stay under the lookahead window and worked, which is why the bug only surfaced for longer ones.) The disambiguation is moved to where it belongs: parse the LIMIT row count once via LimitWithOffset() (which already accepts a parenthesized subquery), then check the immediately following token for BY. A token scan for BY would not work, because a subquery may contain ORDER BY (a K_BY). The now-unused LimitBy() production is removed; LimitWithOffset already carries byExpressions, so no AST or public API change. All LIMIT shapes keep working: LIMIT n, LIMIT n, m, LIMIT n OFFSET m, OFFSET m LIMIT n, LIMIT ALL, and ClickHouse LIMIT n BY ... / LIMIT n, m BY ... Fixes #2359 Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 33 ++++++++----------- .../expression/LimitExpressionTest.java | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0cf4530edc..0884929310 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -5383,8 +5383,19 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] - [ LOOKAHEAD(7) limit = LimitBy() { plainSelect.setLimitBy(limit); } ] - [ LOOKAHEAD() limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] + // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then + // optionally attach ClickHouse's `LIMIT ... BY ...`. Checking BY right after the limit + // expression avoids a numeric LOOKAHEAD, which cannot see past a long parenthesized + // subquery and would wrongly commit to LIMIT BY (issue #2359). + [ LOOKAHEAD() limit = LimitWithOffset() + [ LOOKAHEAD() expressionList = ExpressionList() { limit.setByExpressions(expressionList); } ] + { + if (limit.getByExpressions() != null) { + plainSelect.setLimitBy(limit); + } else { + plainSelect.setLimit(limit); + } + } ] [ LOOKAHEAD() offset = Offset() { plainSelect.setOffset(offset); } ] [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] [ LOOKAHEAD() fetch = Fetch() { plainSelect.setFetch(fetch); } ] @@ -6773,24 +6784,6 @@ Limit PlainLimit() #PlainLimit: } } -/** - * Clickhouse LIMIT BY - * @see SELECT Query - */ -Limit LimitBy(): -{ - Limit limit; - ExpressionList byExpressions; -} -{ - limit = LimitWithOffset() - byExpressions = ExpressionList() - { - limit.setByExpressions(byExpressions); - return limit; - } -} - Offset Offset(): { Offset offset = new Offset(); diff --git a/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java index 70dbd21038..3139530c6d 100644 --- a/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/LimitExpressionTest.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -31,6 +32,38 @@ public void testIssue933() throws JSQLParserException { "SELECT * FROM tmp3 LIMIT (SELECT 2)", true); } + @Test + public void testIssue2359() throws JSQLParserException { + // PostgreSQL allows any expression, including a scalar subquery, as the LIMIT row + // count. A long parenthesized subquery used to fail because the ClickHouse + // "LIMIT ... BY ..." branch was chosen by a numeric LOOKAHEAD that cannot see past + // the subquery. + String sql = "WITH some_table AS (SELECT 1 AS some_column), " + + "another_table AS (SELECT 'some_value' AS condition_column) " + + "SELECT some_column FROM some_table ORDER BY some_column " + + "LIMIT (SELECT COUNT(*) FROM another_table WHERE condition_column = 'some_value')"; + + PlainSelect plainSelect = (PlainSelect) CCJSqlParserUtil.parse(sql); + Assertions.assertTrue( + plainSelect.getLimit().getRowCount() instanceof ParenthesedSelect); + Assertions.assertNull(plainSelect.getLimitBy()); + + TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + + // A function wrapping a scalar subquery must work as the row count too. + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT a FROM t LIMIT GREATEST(0, (SELECT COUNT(*) FROM u WHERE c = 'x'))", + true); + } + + @Test + public void testLimitByClickHouseUnchanged() throws JSQLParserException { + // ClickHouse "LIMIT ... BY ..." must keep parsing and round-tripping after the LIMIT + // row-count disambiguation was rewritten (issue #2359). + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT id FROM t LIMIT 5 BY id", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT id FROM t LIMIT 2, 5 BY id", true); + } + @Test public void testIssue1373() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed( From 60b0875abc1b2a59b10264bfd6031ae46cce8aac Mon Sep 17 00:00:00 2001 From: fudian Date: Fri, 14 Aug 2026 21:39:32 +0800 Subject: [PATCH 004/387] fix: support ClickHouse C-style ternary operator (? :) in expressions (#2436) (#2466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse cond ? then : else as a TernaryExpression at the boolean-operator level in prattExpressionRest, keeping C-style precedence (binds looser than AND/OR) and right-associativity. A pending ? is disambiguated between this ternary and the PostgreSQL JSON operator via isTernaryAhead, and inside a then-branch a top-level : closes the ternary instead of starting a JSON path, so both dialects keep working. Signed-off-by: 付典 --- .../expression/ExpressionVisitor.java | 6 + .../expression/ExpressionVisitorAdapter.java | 6 + .../expression/TernaryExpression.java | 68 +++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 8 ++ .../util/deparser/ExpressionDeParser.java | 15 ++ .../validator/ExpressionValidator.java | 13 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 87 +++++++++++- .../expression/TernaryExpressionTest.java | 132 ++++++++++++++++++ 8 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java create mode 100644 src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 08afe5129f..d07d9179aa 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -737,6 +737,12 @@ default void visit(RangeExpression rangeExpression) { this.visit(rangeExpression, null); } + T visit(TernaryExpression ternaryExpression, S context); + + default void visit(TernaryExpression ternaryExpression) { + this.visit(ternaryExpression, null); + } + T visit(TSQLLeftJoin tsqlLeftJoin, S context); default void visit(TSQLLeftJoin tsqlLeftJoin) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 432c16354d..4727e93e85 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -822,6 +822,12 @@ public T visit(RangeExpression rangeExpression, S context) { rangeExpression.getEndExpression()); } + @Override + public T visit(TernaryExpression ternaryExpression, S context) { + return visitExpressions(ternaryExpression, context, ternaryExpression.getCondition(), + ternaryExpression.getThenExpression(), ternaryExpression.getElseExpression()); + } + @Override public T visit(TSQLLeftJoin tsqlLeftJoin, S context) { return visitBinaryExpression(tsqlLeftJoin, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java b/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java new file mode 100644 index 0000000000..3d67d4ea38 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java @@ -0,0 +1,68 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * The C-style ternary conditional operator {@code condition ? thenExpression : elseExpression}, + * supported for instance by ClickHouse as an alias for {@code if(condition, then, else)}. + */ +public class TernaryExpression extends ASTNodeAccessImpl implements Expression { + private Expression condition; + private Expression thenExpression; + private Expression elseExpression; + + public TernaryExpression() {} + + public TernaryExpression(Expression condition, Expression thenExpression, + Expression elseExpression) { + this.condition = condition; + this.thenExpression = thenExpression; + this.elseExpression = elseExpression; + } + + public Expression getCondition() { + return condition; + } + + public TernaryExpression setCondition(Expression condition) { + this.condition = condition; + return this; + } + + public Expression getThenExpression() { + return thenExpression; + } + + public TernaryExpression setThenExpression(Expression thenExpression) { + this.thenExpression = thenExpression; + return this; + } + + public Expression getElseExpression() { + return elseExpression; + } + + public TernaryExpression setElseExpression(Expression elseExpression) { + this.elseExpression = elseExpression; + return this; + } + + @Override + public String toString() { + return condition + " ? " + thenExpression + " : " + elseExpression; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 4b643742a6..99ee4eaccb 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -249,6 +249,14 @@ public Void visit(RangeExpression rangeExpression, S context) { return null; } + @Override + public Void visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + ternaryExpression.getThenExpression().accept(this, context); + ternaryExpression.getElseExpression().accept(this, context); + return null; + } + /** * Main entry for this Tool class. A list of found tables is returned. */ diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index e50ccad6a6..2767f5e23e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -62,6 +62,7 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; @@ -818,6 +819,16 @@ public StringBuilder visit(RangeExpression rangeExpression, S context) { return builder; } + @Override + public StringBuilder visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + builder.append(" ? "); + ternaryExpression.getThenExpression().accept(this, context); + builder.append(" : "); + ternaryExpression.getElseExpression().accept(this, context); + return builder; + } + @Override public StringBuilder visit(Column tableColumn, S context) { final Table table = tableColumn.getTable(); @@ -1045,6 +1056,10 @@ public void visit(RangeExpression rangeExpression) { visit(rangeExpression, null); } + public void visit(TernaryExpression ternaryExpression) { + visit(ternaryExpression, null); + } + public void visit(Column tableColumn) { visit(tableColumn, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index a18a21ab79..128fc7d92d 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -56,6 +56,7 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; @@ -1153,6 +1154,14 @@ public Void visit(RangeExpression rangeExpression, S context) { return null; } + @Override + public Void visit(TernaryExpression ternaryExpression, S context) { + ternaryExpression.getCondition().accept(this, context); + ternaryExpression.getThenExpression().accept(this, context); + ternaryExpression.getElseExpression().accept(this, context); + return null; + } + @Override public Void visit(TSQLLeftJoin tsqlLeftJoin, S context) { tsqlLeftJoin.getLeftExpression().accept(this, context); @@ -1310,6 +1319,10 @@ public void visit(RangeExpression rangeExpression) { visit(rangeExpression, null); } + public void visit(TernaryExpression ternaryExpression) { + visit(ternaryExpression, null); + } + public void visit(TSQLLeftJoin tsqlLeftJoin) { visit(tsqlLeftJoin, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0884929310..99e62720f1 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -101,6 +101,10 @@ public class CCJSqlParser extends AbstractJSqlParser { return this; } + // depth of open ClickHouse-style ternary then-branches: a ":" directly after the + // then-branch closes the ternary and must not be taken as the JSON path operator + private int ternaryThenBranchDepth = 0; + private void linkAST(ASTNodeAccess access, Node node) { access.setASTNode(node); node.jjtSetValue(access); @@ -182,6 +186,11 @@ public class CCJSqlParser extends AbstractJSqlParser { // comparison operator follows after "(" "+" ")" return isComparisonOperator(getToken(4)); } + if ("?".equals(token.image) && isTernaryAhead()) { + // ClickHouse ternary `cond ? then : else` — the `?` must not be taken + // as the PostgreSQL JSON operator here, it is consumed by prattExpressionRest + return false; + } return isComparisonOperator(token); } catch (Exception e) { return false; @@ -212,6 +221,52 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * True when the pending "?" starts a ClickHouse-style ternary conditional + * {@code cond ? then : else} rather than the PostgreSQL JSON operator: + * a standalone ":" closes the then-branch at the same nesting depth before + * any expression boundary (",", ";", EOF, an unbalanced closing bracket or a + * clause keyword such as FROM/WHERE). + */ + protected boolean isTernaryAhead() { + try { + int depth = 0; + for (int i = 2; ; i++) { + Token t = getToken(i); + if (t == null || t.kind == EOF) { + return false; + } + String image = t.image; + if ("(".equals(image) || "[".equals(image)) { + depth++; + } else if (")".equals(image) || "]".equals(image)) { + if (depth == 0) { + return false; + } + depth--; + } else if (depth == 0) { + if (":".equals(image)) { + return true; + } + if (",".equals(image) || ";".equals(image)) { + return false; + } + switch (t.kind) { + case K_SELECT: case K_FROM: case K_WHERE: case K_GROUP: + case K_HAVING: case K_ORDER: case K_LIMIT: case K_UNION: + case K_INTERSECT: case K_EXCEPT: case K_MINUS: case K_INTO: + case K_VALUES: case K_FETCH: case K_OFFSET: + return false; + default: + break; + } + } + } + } catch (Exception e) { + return false; + } + } + /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. @@ -328,12 +383,35 @@ public class CCJSqlParser extends AbstractJSqlParser { */ protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException { while (!interrupted) { - int op = getToken(1).kind; + Token t = getToken(1); + int op = t.kind; + boolean ternary = "?".equals(t.image) && isTernaryAhead(); int prec; if (op == K_AND || op == OP_DOUBLEAND) prec = 4; else if (op == K_XOR || op == K_OR) prec = 2; + else if (ternary) prec = 2; else break; if (prec < minPrec) break; + if (ternary) { + // ClickHouse-style ternary conditional: cond ? then : else + // Right-associative: both branches parse at the full expression + // level (prec 2), nested ternaries are absorbed by the else-branch. + jj_consume_token(op, t.image); + Expression thenExpression; + ternaryThenBranchDepth++; + try { + thenExpression = prattExpressionRest(Condition(), 2); + } finally { + ternaryThenBranchDepth--; + } + if (!":".equals(getToken(1).image)) { + throw new ParseException("Expected ':' closing the ternary conditional operator"); + } + jj_consume_token(getToken(1).kind, getToken(1).image); + Expression elseExpression = prattExpressionRest(Condition(), 2); + left = new TernaryExpression(left, thenExpression, elseExpression); + continue; + } jj_consume_token(op, getToken(1).image); // +1 makes OR/AND/XOR left-associative Expression right = prattExpressionRest(Condition(), prec + 1); @@ -920,6 +998,9 @@ public class CCJSqlParser extends AbstractJSqlParser { if (t.kind == OPENING_BRACKET && getToken(2).image.equals("+")) { return isComparisonOperator(getToken(4)); } + // ClickHouse ternary `cond ? then : else` — the `?` must not be taken as the + // PostgreSQL JSON operator, it is consumed by prattExpressionRest instead + if ("?".equals(t.image) && isTernaryAhead()) return false; if (isComparisonOperator(t)) return true; switch (t.kind) { // Each suffix's start token: @@ -7980,7 +8061,7 @@ Expression PrimaryExpression() #PrimaryExpression: // Check for JSON operands [ - LOOKAHEAD(2) ( + LOOKAHEAD(2, { ternaryThenBranchDepth == 0 || !":".equals(getToken(1).image) }) ( LOOKAHEAD(2) ( token="->" | @@ -8310,7 +8391,7 @@ JsonExpression JsonExpression(Expression expr, List 0 ? 'y' : 'n' FROM t", true); + + Select select = (Select) CCJSqlParserUtil.parse("SELECT x > 0 ? 'y' : 'n' FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getCondition() instanceof ComparisonOperator); + Assertions.assertTrue(ternary.getThenExpression() instanceof StringValue); + Assertions.assertTrue(ternary.getElseExpression() instanceof StringValue); + } + + @ParameterizedTest + @ValueSource(strings = { + // select items, where, order by, group by, having + "SELECT a ? b : c FROM t", + "SELECT * FROM t WHERE a ? b : c", + "SELECT * FROM t WHERE a ? b : c ORDER BY x ? y : z", + "SELECT id FROM t GROUP BY v ? 1 : 0 HAVING count(*) > 1", + // function arguments, nested in parentheses and subqueries + "SELECT abs(a ? b : c) FROM t", + "SELECT (a ? b : c) FROM t", + "SELECT * FROM (SELECT a ? b : c AS v FROM t) x", + // dml statements + "UPDATE t SET v = a ? b : c", + "INSERT INTO t VALUES (a ? b : c)", + "DELETE FROM t WHERE a ? b : c", + // joins + "SELECT * FROM t1 JOIN t2 ON t1.a ? t1.b : t2.c", + // branches with arithmetic or boolean operators + "SELECT x > 0 ? 1 + 2 : 3 * 4 FROM t", + "SELECT a ? b OR c : d AND e FROM t", + "SELECT a IS NULL ? 'x' : y FROM t", + // jdbc parameters as branches + "SELECT a ? ? : c FROM t", + "SELECT a ? b : ? FROM t", + "SELECT * FROM t WHERE x = ? AND y ? z : w", + // case expression inside a branch + "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t" + }) + void testTernaryInVariousContexts(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrecedenceBindsLooserThanBooleanOperators() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("SELECT * FROM t WHERE a OR b ? c : d"); + Expression where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue(((TernaryExpression) where).getCondition() instanceof OrExpression); + + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a AND b ? c : d"); + where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue(((TernaryExpression) where).getCondition() instanceof AndExpression); + + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b : c OR d"); + where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof TernaryExpression); + Assertions.assertTrue( + ((TernaryExpression) where).getElseExpression() instanceof OrExpression); + } + + @Test + void testRightAssociativeNesting() throws JSQLParserException { + // a ? b : c ? d : e ==> a ? b : (c ? d : e) + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b : c ? d : e"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getWhere(); + Assertions.assertTrue(ternary.getThenExpression() instanceof Column); + Assertions.assertTrue(ternary.getElseExpression() instanceof TernaryExpression); + + // a ? b ? c : d : e ==> a ? (b ? c : d) : e + select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE a ? b ? c : d : e"); + ternary = (TernaryExpression) ((PlainSelect) select).getWhere(); + Assertions.assertTrue(ternary.getThenExpression() instanceof TernaryExpression); + Assertions.assertTrue(ternary.getElseExpression() instanceof Column); + } + + @ParameterizedTest + @ValueSource(strings = { + // PostgreSQL JSON operators must keep working + "SELECT col ? 'key' FROM t", + "SELECT col ?| 'key' FROM t", + "SELECT col ?& 'key' FROM t", + "SELECT * FROM t WHERE col ? 'key' AND x = 1", + // JSON path operator must keep working + "SELECT col -> 'key' FROM t", + // JDBC parameters must keep working + "SELECT * FROM t WHERE x = ?", + "SELECT * FROM t WHERE x = ?5", + "SELECT * FROM t WHERE x = ? AND y = ?", + "SELECT * FROM t LIMIT ?", + // array ranges and casts are unaffected + "SELECT ARRAY[1:3]", + "SELECT CAST(a AS CHAR)" + }) + void testUnrelatedSyntaxUnaffected(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } +} From 406a4d41341d30fe45a6c5724bf2a81bfc73d12c Mon Sep 17 00:00:00 2001 From: fudian Date: Sat, 15 Aug 2026 13:11:33 +0800 Subject: [PATCH 005/387] fix: support ClickHouse ORDER BY WITH FILL and INTERPOLATE (#2467) (#2469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse the ClickHouse WITH FILL modifier on ORDER BY elements together with the query-level INTERPOLATE clause, matching the upstream grammar: per element [WITH FILL] [FROM e] [TO e] [STEP e] [STALENESS e] (the step may be an INTERVAL literal), and after the ORDER BY list an optional INTERPOLATE [(col [AS expr], ...)] where the bare form fills all allowed columns. OrderByElement carries a new WithFill holder and Select (shared by PlainSelect and SetOperationList, with union hoisting) carries the interpolate column list. Rendering goes through both toString and the deparser, and the validators walk the new expressions. The keywords FILL, STEP, STALENESS and INTERPOLATE are declared non-reserved, so they remain usable as plain column names. Signed-off-by: 付典 --- .../statement/select/InterpolateElement.java | 73 +++++++++++++ .../statement/select/OrderByElement.java | 18 ++++ .../jsqlparser/statement/select/Select.java | 26 +++++ .../jsqlparser/statement/select/WithFill.java | 102 ++++++++++++++++++ .../util/deparser/OrderByDeParser.java | 25 +++++ .../util/deparser/SelectDeParser.java | 27 +++++ .../validator/OrderByValidator.java | 6 ++ .../validation/validator/SelectValidator.java | 12 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 59 +++++++++- .../statement/select/ClickHouseTest.java | 99 +++++++++++++++++ 10 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/WithFill.java diff --git a/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java new file mode 100644 index 0000000000..a571c4cf5c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java @@ -0,0 +1,73 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** + * Models one column entry of the ClickHouse {@code INTERPOLATE} clause, which computes the values + * of non-sorted columns for the rows generated by {@code ORDER BY ... WITH FILL} (see + * ClickHouse: + * ORDER BY). + *

+ * {@code col AS expr} evaluates {@code expr} for the generated rows, while a bare {@code col} (a + * {@code null} expression) repeats the previous value of the column. + */ +public class InterpolateElement implements Serializable { + + private Column column; + private Expression expression; + + public InterpolateElement() { + super(); + } + + public InterpolateElement(Column column, Expression expression) { + this.column = column; + this.expression = expression; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public InterpolateElement withColumn(Column column) { + setColumn(column); + return this; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public InterpolateElement withExpression(Expression expression) { + setExpression(expression); + return this; + } + + @Override + public String toString() { + if (expression != null) { + return column + " AS " + expression; + } + return column.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java index 38b78384da..4662150fa5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java @@ -22,6 +22,7 @@ public class OrderByElement implements Serializable { private boolean asc = true; private boolean ascDescPresent = false; private NullOrdering nullOrdering; + private WithFill withFill; public boolean isAsc() { return asc; @@ -39,6 +40,20 @@ public void setNullOrdering(NullOrdering nullOrdering) { this.nullOrdering = nullOrdering; } + public WithFill getWithFill() { + return withFill; + } + + public OrderByElement setWithFill(WithFill withFill) { + this.withFill = withFill; + return this; + } + + public OrderByElement withWithFill(WithFill withFill) { + setWithFill(withFill); + return this; + } + public boolean isAscDescPresent() { return ascDescPresent; } @@ -74,6 +89,9 @@ public String toString() { b.append(' '); b.append(nullOrdering == NullOrdering.NULLS_FIRST ? "NULLS FIRST" : "NULLS LAST"); } + if (withFill != null) { + b.append(' ').append(withFill); + } if (isMysqlWithRollup()) { b.append(" WITH ROLLUP"); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 737608020f..0dc57e32a1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -36,6 +36,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp ForClause forClause = null; List orderByElements; + List interpolate; ForMode forMode = null; private boolean skipLocked; private Wait wait; @@ -54,6 +55,16 @@ public static String orderByToString(boolean oracleSiblings, return getFormattedList(orderByElements, oracleSiblings ? "ORDER SIBLINGS BY" : "ORDER BY"); } + public static String interpolateToString(List interpolate) { + if (interpolate == null) { + return ""; + } + if (interpolate.isEmpty()) { + return " INTERPOLATE"; + } + return getFormattedList(interpolate, "INTERPOLATE", true, true); + } + public static String getFormattedList(List list, String expression) { return getFormattedList(list, expression, true, false); } @@ -192,6 +203,19 @@ public void setOrderByElements(List orderByElements) { this.orderByElements = orderByElements; } + public List getInterpolate() { + return interpolate; + } + + public void setInterpolate(List interpolate) { + this.interpolate = interpolate; + } + + public Select withInterpolate(List interpolate) { + setInterpolate(interpolate); + return this; + } + public Select withOrderByElements(List orderByElements) { this.setOrderByElements(orderByElements); return this; @@ -463,6 +487,7 @@ public StringBuilder appendTo(StringBuilder builder) { if (!forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } if (forClause != null) { @@ -512,6 +537,7 @@ public StringBuilder appendTo(StringBuilder builder) { if (forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } return builder; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java new file mode 100644 index 0000000000..9da5672512 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java @@ -0,0 +1,102 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; + +/** + * Models the ClickHouse {@code WITH FILL} modifier of an {@link OrderByElement}, which fills gaps + * in the sorted sequence of the order by expression (see + * ClickHouse: + * ORDER BY). + *

+ * The modifier takes optional {@code FROM}, {@code TO}, {@code STEP} and {@code STALENESS} bounds, + * where the step may be an {@code INTERVAL} literal. Its presence on an {@link OrderByElement} + * marks the element as filled. + */ +public class WithFill implements Serializable { + + private Expression from; + private Expression to; + private Expression step; + private Expression staleness; + + public Expression getFrom() { + return from; + } + + public void setFrom(Expression from) { + this.from = from; + } + + public WithFill withFrom(Expression from) { + setFrom(from); + return this; + } + + public Expression getTo() { + return to; + } + + public void setTo(Expression to) { + this.to = to; + } + + public WithFill withTo(Expression to) { + setTo(to); + return this; + } + + public Expression getStep() { + return step; + } + + public void setStep(Expression step) { + this.step = step; + } + + public WithFill withStep(Expression step) { + setStep(step); + return this; + } + + public Expression getStaleness() { + return staleness; + } + + public void setStaleness(Expression staleness) { + this.staleness = staleness; + } + + public WithFill withStaleness(Expression staleness) { + setStaleness(staleness); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder("WITH FILL"); + if (from != null) { + b.append(" FROM ").append(from); + } + if (to != null) { + b.append(" TO ").append(to); + } + if (step != null) { + b.append(" STEP ").append(step); + } + if (staleness != null) { + b.append(" STALENESS ").append(staleness); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java index 605c54dc28..66e20bebb7 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.WithFill; public class OrderByDeParser extends AbstractDeParser> { @@ -64,11 +65,35 @@ public void deParseElement(OrderByElement orderBy) { ? "NULLS FIRST" : "NULLS LAST"); } + if (orderBy.getWithFill() != null) { + builder.append(' '); + deParseWithFill(orderBy.getWithFill()); + } if (orderBy.isMysqlWithRollup()) { builder.append(" WITH ROLLUP"); } } + private void deParseWithFill(WithFill withFill) { + builder.append("WITH FILL"); + if (withFill.getFrom() != null) { + builder.append(" FROM "); + withFill.getFrom().accept(expressionVisitor, null); + } + if (withFill.getTo() != null) { + builder.append(" TO "); + withFill.getTo().accept(expressionVisitor, null); + } + if (withFill.getStep() != null) { + builder.append(" STEP "); + withFill.getStep().accept(expressionVisitor, null); + } + if (withFill.getStaleness() != null) { + builder.append(" STALENESS "); + withFill.getStaleness().accept(expressionVisitor, null); + } + } + void setExpressionVisitor(ExpressionVisitor expressionVisitor) { this.expressionVisitor = expressionVisitor; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 50edf3b70a..46421c1c8f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -51,6 +51,7 @@ import net.sf.jsqlparser.statement.select.First; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.LateralView; @@ -136,6 +137,7 @@ public StringBuilder visit(ParenthesedSelect select, S context) { if (select.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(select.isOracleSiblings(), select.getOrderByElements()); + deParseInterpolate(select.getInterpolate()); } Alias alias = select.getAlias(); @@ -471,9 +473,33 @@ protected void deparseOrderByElementsClause(PlainSelect plainSelect, if (orderByElements != null) { new OrderByDeParser(expressionVisitor, builder).deParse(plainSelect.isOracleSiblings(), orderByElements); + deParseInterpolate(plainSelect.getInterpolate()); } } + private void deParseInterpolate(List interpolate) { + if (interpolate == null) { + return; + } + if (interpolate.isEmpty()) { + builder.append(" INTERPOLATE"); + return; + } + builder.append(" INTERPOLATE ("); + for (Iterator iter = interpolate.iterator(); iter.hasNext();) { + InterpolateElement element = iter.next(); + element.getColumn().accept(expressionVisitor, null); + if (element.getExpression() != null) { + builder.append(" AS "); + element.getExpression().accept(expressionVisitor, null); + } + if (iter.hasNext()) { + builder.append(", "); + } + } + builder.append(")"); + } + @Override public StringBuilder visit(SelectItem selectItem, S context) { selectItem.getExpression().accept(expressionVisitor, context); @@ -729,6 +755,7 @@ public StringBuilder visit(SetOperationList list, S context) { } if (list.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(list.getOrderByElements()); + deParseInterpolate(list.getInterpolate()); } if (list.getLimit() != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java index 858702ddae..8e04d7df95 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java @@ -32,6 +32,12 @@ public Void visit(OrderByElement orderBy, S context) { validateOptionalFeature(c, orderBy.getNullOrdering(), Feature.orderByNullOrdering); } getValidator(ExpressionValidator.class).validate(orderBy.getExpression()); + if (orderBy.getWithFill() != null) { + validateOptionalExpression(orderBy.getWithFill().getFrom()); + validateOptionalExpression(orderBy.getWithFill().getTo()); + validateOptionalExpression(orderBy.getWithFill().getStep()); + validateOptionalExpression(orderBy.getWithFill().getStaleness()); + } return null; } 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 5cab04030e..92334db529 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 @@ -21,6 +21,7 @@ import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.ForMode; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.IntersectOp; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; @@ -136,6 +137,7 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalExpression(plainSelect.getHaving()); validateOptionalOrderByElements(plainSelect.getOrderByElements()); + validateOptionalInterpolate(plainSelect.getInterpolate()); if (plainSelect.getLimit() != null) { getValidator(LimitValidator.class).validate(plainSelect.getLimit()); @@ -163,6 +165,15 @@ private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectInto validateOptionalExpression(mySqlSelectIntoClause.getLinesTerminatedBy()); } + private void validateOptionalInterpolate(List interpolate) { + if (interpolate != null) { + for (InterpolateElement element : interpolate) { + validateOptionalExpression(element.getColumn()); + validateOptionalExpression(element.getExpression()); + } + } + } + @Override public Void visit(SelectItem selectExpressionItem, S context) { selectExpressionItem.getExpression().accept(getValidator(ExpressionValidator.class), @@ -308,6 +319,7 @@ public Void visit(SetOperationList setOperation, S context) { } validateOptionalOrderByElements(setOperation.getOrderByElements()); + validateOptionalInterpolate(setOperation.getInterpolate()); if (setOperation.getLimit() != null) { getValidator(LimitValidator.class).validate(setOperation.getLimit()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 99e62720f1..5f8e91d117 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1279,6 +1279,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1303,6 +1304,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1453,7 +1455,9 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= @@ -4686,6 +4690,7 @@ Select Select() #Select: Select select = null; List> with = null; List orderByElements = null; + List interpolateElements = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -4693,7 +4698,6 @@ Select Select() #Select: Alias alias = null; } { - [ with=WithList() ] ( LOOKAHEAD(3) select = FromQuery() @@ -4710,6 +4714,7 @@ Select Select() #Select: [ LOOKAHEAD(2) select = SetOperationList(select) ] [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -5334,6 +5339,7 @@ PlainSelect PlainSelect() #PlainSelect: Expression where = null; ForClause forClause = null; List orderByElements; + List interpolateElements = null; GroupByElement groupBy = null; Expression having = null; Expression qualify; @@ -5462,6 +5468,7 @@ PlainSelect PlainSelect() #PlainSelect: { plainSelect.setWindowDefinitions(winDefs); } ] [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then @@ -5502,6 +5509,7 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] ] [ LOOKAHEAD( ( | )) mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING) @@ -5528,6 +5536,7 @@ PlainSelect PlainSelect() #PlainSelect: Select SetOperationList(Select select) #SetOperationList: { SetOperationList list = new SetOperationList(); List orderByElements = null; + List interpolateElements = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -5575,6 +5584,7 @@ Select SetOperationList(Select select) #SetOperationList: { )+ [ LOOKAHEAD(2) orderByElements=OrderByElements() {list.setOrderByElements(orderByElements);} ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { list.setInterpolate(interpolateElements); } ] [ LOOKAHEAD() limit = LimitWithOffset() { list.setLimit(limit); } ] [ LOOKAHEAD() offset = Offset() { list.setOffset(offset); } ] [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { list.setLimit(limit); } ] @@ -5592,6 +5602,10 @@ Select SetOperationList(Select select) #SetOperationList: { ps.setLimit(null); ps.setOffset(null); } + if (ps.getInterpolate() != null) { + list.setInterpolate(ps.getInterpolate()); + ps.setInterpolate(null); + } if (ps.getFetch() != null) { list.setFetch(ps.getFetch()); ps.setFetch(null); @@ -6778,6 +6792,7 @@ OrderByElement OrderByElement(): OrderByElement orderByElement = new OrderByElement(); Expression columnReference = null; Token collateToken = null; + WithFill withFill = null; } { columnReference = Expression() @@ -6791,6 +6806,7 @@ OrderByElement OrderByElement(): ) ] ] + [ LOOKAHEAD(2) withFill = WithFill() { orderByElement.setWithFill(withFill); } ] [ LOOKAHEAD(2) { orderByElement.setMysqlWithRollup(true); } ] { orderByElement.setExpression(columnReference); @@ -6798,6 +6814,47 @@ OrderByElement OrderByElement(): } } +WithFill WithFill(): +{ + WithFill withFill = new WithFill(); + Expression fillExpression = null; +} +{ + + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setFrom(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setTo(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStep(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStaleness(fillExpression); } ] + { return withFill; } +} + +List InterpolateClause(): +{ + List interpolateElements = new ArrayList(); + InterpolateElement interpolateElement = null; +} +{ + + [ "(" + [ interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } + ( "," interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } )* + ] + ")" ] + { return interpolateElements; } +} + +InterpolateElement InterpolateElement(): +{ + InterpolateElement interpolateElement = new InterpolateElement(); + Column column = null; + Expression expression = null; +} +{ + column = Column() { interpolateElement.setColumn(column); } + [ expression = Expression() { interpolateElement.setExpression(expression); } ] + { return interpolateElement; } +} + JdbcParameter JdbcParameter() : { Token tk; JdbcParameter retval; 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 90f69ffd43..cfdca4e776 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java @@ -147,6 +147,105 @@ public void testMultipleSettingsClauseIssue2362() throws JSQLParserException { Assertions.assertEquals(2, select.getSettings().size()); } + @Test + public void testOrderByWithFillIssue2467() throws JSQLParserException { + String sql = + "SELECT d, v FROM t ORDER BY d WITH FILL FROM toDate('2024-01-01') TO toDate('2024-02-01') STEP INTERVAL 1 DAY INTERPOLATE (v AS v + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertNotNull(withFill); + Assertions.assertEquals("toDate('2024-01-01')", withFill.getFrom().toString()); + Assertions.assertEquals("toDate('2024-02-01')", withFill.getTo().toString()); + Assertions.assertEquals("INTERVAL 1 DAY", withFill.getStep().toString()); + Assertions.assertNull(withFill.getStaleness()); + + Assertions.assertEquals(1, select.getInterpolate().size()); + Assertions.assertEquals("v", select.getInterpolate().get(0).getColumn().getColumnName()); + Assertions.assertEquals("v + 1", select.getInterpolate().get(0).getExpression().toString()); + } + + @Test + public void testOrderByWithFillOnly() throws JSQLParserException { + String sql = "SELECT key, value FROM t ORDER BY key WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getOrderByElements().get(0).getWithFill()); + Assertions.assertNull(select.getOrderByElements().get(0).getWithFill().getFrom()); + } + + @Test + public void testOrderByWithFillFromToStep() throws JSQLParserException { + String sql = "SELECT n FROM t ORDER BY n WITH FILL FROM 0 TO 5.51 STEP 0.5"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("0", withFill.getFrom().toString()); + Assertions.assertEquals("5.51", withFill.getTo().toString()); + Assertions.assertEquals("0.5", withFill.getStep().toString()); + } + + @Test + public void testOrderByWithFillStepIntervalAndStaleness() throws JSQLParserException { + String sql = + "SELECT d1, d2 FROM t ORDER BY d1 WITH FILL STEP INTERVAL 1 DAY STALENESS 3, d2 WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getOrderByElements().size()); + WithFill first = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("INTERVAL 1 DAY", first.getStep().toString()); + Assertions.assertEquals("3", first.getStaleness().toString()); + Assertions.assertNotNull(select.getOrderByElements().get(1).getWithFill()); + } + + @Test + public void testOrderByWithFillDescNullsLast() throws JSQLParserException { + String sql = "SELECT d FROM t ORDER BY d DESC NULLS LAST WITH FILL TO 10"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OrderByElement element = select.getOrderByElements().get(0); + Assertions.assertFalse(element.isAsc()); + Assertions.assertEquals(OrderByElement.NullOrdering.NULLS_LAST, element.getNullOrdering()); + Assertions.assertEquals("10", element.getWithFill().getTo().toString()); + } + + @Test + public void testOrderByWithFillInterpolateVariants() throws JSQLParserException { + // multiple items, with and without AS + String sql = "SELECT a, b, c FROM t ORDER BY a WITH FILL INTERPOLATE (b, c AS c + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getInterpolate().size()); + Assertions.assertNull(select.getInterpolate().get(0).getExpression()); + Assertions.assertEquals("c + 1", select.getInterpolate().get(1).getExpression().toString()); + + // bare INTERPOLATE fills all allowed columns + sql = "SELECT a, b FROM t ORDER BY a WITH FILL INTERPOLATE"; + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getInterpolate()); + Assertions.assertTrue(select.getInterpolate().isEmpty()); + } + + @Test + public void testOrderByWithFillUnionInterpolate() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a WITH FILL INTERPOLATE (a AS a + 1) LIMIT 10"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = (SetOperationList) select.getSelectBody(); + Assertions.assertNotNull(setOperationList.getOrderByElements().get(0).getWithFill()); + Assertions.assertEquals(1, setOperationList.getInterpolate().size()); + } + + @Test + public void testOrderByWithFillSubquery() throws JSQLParserException { + String sql = "SELECT * FROM (SELECT d, v FROM t ORDER BY d WITH FILL FROM 0 TO 9) AS sub"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testFillStepInterpolateStalenessAsColumnNames() throws JSQLParserException { + // the new keywords stay non-reserved and must remain usable as plain column names + assertSqlCanBeParsedAndDeparsed("SELECT fill, step, staleness FROM t ORDER BY interpolate", + true); + assertSqlCanBeParsedAndDeparsed("SELECT interpolate FROM t WHERE fill = 1 AND step = 2", + true); + } + @Test public void testCastToNestedParametricTypeIssue2441() throws JSQLParserException { // ClickHouse allows parametric (constructor-style) data types as a CAST target, From edfbb0ac050afa070b9aefd16a0c00cc2785dfb6 Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 16 Aug 2026 08:53:03 +0800 Subject: [PATCH 006/387] fix: allow GROUPS as identifier by making it a non-reserved keyword (#2473) (#2474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 +- .../net/sf/jsqlparser/statement/KeywordsTest.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5f8e91d117..59723124c5 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1287,6 +1287,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1577,7 +1578,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | diff --git a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java index b595ab096f..9e2c14ce7e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java @@ -51,4 +51,15 @@ public void testCombinedTokenKeywords() throws JSQLParserException { String sqlStr = "SELECT current_date(3)"; assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + // #2473: GROUPS is a non-reserved keyword in PostgreSQL and must stay usable as an + // identifier, although it also introduces the GROUPS window frame unit. + @Test + public void testGroupsAsIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM groups", true); + assertSqlCanBeParsedAndDeparsed("SELECT a AS groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT a groups FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true); + } } From 138947d6b1d0d28fbd269bd3e45b8204889cc8c2 Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 16 Aug 2026 08:56:32 +0800 Subject: [PATCH 007/387] feat: support SQL Server OPTION query hints (#161) (#2472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse the T-SQL OPTION (...) clause at the end of SELECT (including after set operations and FOR XML), UPDATE and DELETE statements, following the Query Hints documentation: multi-word keyword hints (RECOMPILE, HASH JOIN, FORCE ORDER, OPTIMIZE FOR UNKNOWN, ...), single value arguments (FAST 100, MAXDOP 4, MAX_GRANT_PERCENT = 25) and parenthesized argument lists (USE HINT ('...'), OPTIMIZE FOR (@p = 1), TABLE HINT (t, INDEX (i))). Hints are kept in a generic OptionHint (keyword name + optional value or ExpressionList parameters) attached via a new OptionClause, mirroring how SQL Server keeps adding new hints without enum churn. OPTION is a non-reserved keyword and K_OPTION followed by ( is excluded from alias positions, so option remains usable as a column and table name. The union hoisting lifts the clause from the last PlainSelect onto the SetOperationList like ORDER BY and LIMIT. Signed-off-by: 付典 --- .../jsqlparser/statement/delete/Delete.java | 19 +++ .../statement/select/OptionClause.java | 59 +++++++ .../statement/select/OptionHint.java | 113 +++++++++++++ .../jsqlparser/statement/select/Select.java | 18 ++ .../jsqlparser/statement/update/Update.java | 19 +++ .../util/deparser/DeleteDeParser.java | 4 + .../util/deparser/SelectDeParser.java | 12 ++ .../util/deparser/UpdateDeParser.java | 4 + .../validation/validator/DeleteValidator.java | 10 ++ .../validation/validator/SelectValidator.java | 15 ++ .../validation/validator/UpdateValidator.java | 10 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 115 +++++++++++++ .../statement/select/OptionClauseTest.java | 158 ++++++++++++++++++ 13 files changed, 556 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java index 4e0f45ffeb..65301efa83 100644 --- a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java +++ b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java @@ -20,6 +20,7 @@ import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.WithItem; @@ -45,6 +46,7 @@ public class Delete implements Statement { private Expression where; private PreferringClause preferringClause; private Limit limit; + private OptionClause option; private List orderByElements; private boolean hasFrom = true; private DeleteModifierPriority modifierPriority; @@ -147,6 +149,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Delete setOption(OptionClause option) { + this.option = option; + return this; + } + + public Delete withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -280,6 +295,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java new file mode 100644 index 0000000000..cb941819ce --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java @@ -0,0 +1,59 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Models the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, which attaches a list of + * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE} or {@code DELETE} statement, + * see Hints + * (Transact-SQL) - Query Hints. + */ +public class OptionClause implements Serializable { + + private List optionHints; + + public OptionClause() { + super(); + } + + public OptionClause(List optionHints) { + this.optionHints = optionHints; + } + + public List getOptionHints() { + return optionHints; + } + + public void setOptionHints(List optionHints) { + this.optionHints = optionHints; + } + + public OptionClause withOptionHints(List optionHints) { + setOptionHints(optionHints); + return this; + } + + public OptionClause addOptionHint(OptionHint optionHint) { + if (optionHints == null) { + optionHints = new ArrayList<>(); + } + optionHints.add(optionHint); + return this; + } + + @Override + public String toString() { + return " OPTION (" + Select.getStringList(optionHints, true, false) + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java new file mode 100644 index 0000000000..85dc0c12dd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionHint.java @@ -0,0 +1,113 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * Models one hint of the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, see + * Hints + * (Transact-SQL) - Query Hints. + *

+ * A hint is a (possibly multi-word) keyword such as {@code RECOMPILE}, {@code HASH JOIN} or + * {@code OPTIMIZE FOR UNKNOWN}, optionally followed by a single value argument ({@code FAST 100}, + * {@code MAXDOP 4}, {@code MAX_GRANT_PERCENT = 25}) or by a parenthesized argument list + * ({@code USE HINT ('...')}, {@code OPTIMIZE FOR (@p = 1)}, {@code TABLE HINT (t, INDEX (i))}). + */ +public class OptionHint implements Serializable { + + private String name; + private Expression value; + private ExpressionList parameters; + private boolean useEquals = false; + + public OptionHint() { + super(); + } + + public OptionHint(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OptionHint withName(String name) { + setName(name); + return this; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public OptionHint withValue(Expression value) { + setValue(value); + return this; + } + + public ExpressionList getParameters() { + return parameters; + } + + public void setParameters(ExpressionList parameters) { + this.parameters = parameters; + } + + public OptionHint withParameters(ExpressionList parameters) { + setParameters(parameters); + return this; + } + + public OptionHint addParameter(Expression parameter) { + if (parameters == null) { + parameters = new ExpressionList<>(); + } + parameters.add(parameter); + return this; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public OptionHint withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(name); + if (value != null) { + b.append(useEquals ? " = " : " ").append(value); + } + if (parameters != null) { + b.append(" (").append(Select.getStringList(parameters, true, false)).append(')'); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 0dc57e32a1..501e21728c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -34,6 +34,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp boolean oracleSiblings = false; ForClause forClause = null; + OptionClause option = null; List orderByElements; List interpolate; @@ -195,6 +196,19 @@ public Select setForClause(ForClause forClause) { return this; } + public OptionClause getOption() { + return option; + } + + public Select setOption(OptionClause option) { + this.option = option; + return this; + } + + public Select withOption(OptionClause option) { + return setOption(option); + } + public List getOrderByElements() { return orderByElements; } @@ -494,6 +508,10 @@ public StringBuilder appendTo(StringBuilder builder) { forClause.appendTo(builder); } + if (option != null) { + builder.append(option); + } + if (limitBy != null) { builder.append(limitBy); } diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index b09854fe69..03d9d25800 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -21,6 +21,7 @@ import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -48,6 +49,7 @@ public class Update implements Statement { private OracleHint oracleHint = null; private List orderByElements; private Limit limit; + private OptionClause option; private ReturningClause returningClause; private UpdateModifierPriority modifierPriority; private boolean modifierIgnore; @@ -255,6 +257,19 @@ public Limit getLimit() { return limit; } + public OptionClause getOption() { + return option; + } + + public Update setOption(OptionClause option) { + this.option = option; + return this; + } + + public Update withOption(OptionClause option) { + return setOption(option); + } + public void setLimit(Limit limit) { this.limit = limit; } @@ -356,6 +371,10 @@ public String toString() { b.append(limit); } + if (option != null) { + b.append(option); + } + if (returningClause != null) { returningClause.appendTo(b); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java index 2ab59ea148..7472c968ff 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java @@ -105,6 +105,10 @@ public void deParse(Delete delete) { new LimitDeparser(expressionVisitor, builder).deParse(delete.getLimit()); } + if (delete.getOption() != null) { + builder.append(delete.getOption()); + } + if (delete.getReturningClause() != null) { delete.getReturningClause().appendTo(builder); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 46421c1c8f..c061ff45f4 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -140,6 +140,10 @@ public StringBuilder visit(ParenthesedSelect select, S context) { deParseInterpolate(select.getInterpolate()); } + if (select.getOption() != null) { + builder.append(select.getOption()); + } + Alias alias = select.getAlias(); if (alias != null) { builder.append(alias); @@ -345,6 +349,10 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { plainSelect.getForClause().appendTo(builder); } + if (plainSelect.getOption() != null) { + builder.append(plainSelect.getOption()); + } + if (plainSelect.isEmitChanges()) { builder.append(" EMIT CHANGES"); } @@ -758,6 +766,10 @@ public StringBuilder visit(SetOperationList list, S context) { deParseInterpolate(list.getInterpolate()); } + if (list.getOption() != null) { + builder.append(list.getOption()); + } + if (list.getLimit() != null) { new LimitDeparser(expressionVisitor, builder).deParse(list.getLimit()); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index d5dcac7f10..b18d58090b 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -103,6 +103,10 @@ public void deParse(Update update) { new LimitDeparser(expressionVisitor, builder).deParse(update.getLimit()); } + if (update.getOption() != null) { + builder.append(update.getOption()); + } + if (update.getReturningClause() != null) { update.getReturningClause().appendTo(builder); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java index 19588e3b7b..4accb3df4e 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/DeleteValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.util.validation.ValidationCapability; /** @@ -48,6 +49,15 @@ public void validate(Delete delete) { getValidator(LimitValidator.class).validate(delete.getLimit()); } + if (delete.getOption() != null) { + for (OptionHint optionHint : delete.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (delete.getReturningClause() != null) { delete.getReturningClause().forEach(c -> c.accept(v, null)); } 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 92334db529..a0511d4808 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 @@ -37,6 +37,8 @@ import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectItemVisitor; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.OptionClause; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; @@ -138,6 +140,7 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalExpression(plainSelect.getHaving()); validateOptionalOrderByElements(plainSelect.getOrderByElements()); validateOptionalInterpolate(plainSelect.getInterpolate()); + validateOptionalOption(plainSelect.getOption()); if (plainSelect.getLimit() != null) { getValidator(LimitValidator.class).validate(plainSelect.getLimit()); @@ -156,6 +159,17 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + private void validateOptionalOption(OptionClause option) { + if (option != null) { + for (OptionHint optionHint : option.getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + } + private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectIntoClause) { validateOptionalExpression(mySqlSelectIntoClause.getFileName()); validateOptionalExpression(mySqlSelectIntoClause.getFieldsTerminatedBy()); @@ -320,6 +334,7 @@ public Void visit(SetOperationList setOperation, S context) { validateOptionalOrderByElements(setOperation.getOrderByElements()); validateOptionalInterpolate(setOperation.getInterpolate()); + validateOptionalOption(setOperation.getOption()); if (setOperation.getLimit() != null) { getValidator(LimitValidator.class).validate(setOperation.getLimit()); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java index 058b6aa9fc..5701b4b7c4 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/UpdateValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -60,6 +61,15 @@ public void validate(Update update) { getValidator(LimitValidator.class).validate(update.getLimit()); } + if (update.getOption() != null) { + for (OptionHint optionHint : update.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } + if (update.getReturningClause() != null) { SelectValidator v = getValidator(SelectValidator.class); update.getReturningClause().forEach(c -> c.accept(v, null)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 59723124c5..e571bb1b5d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -721,6 +721,45 @@ public class CCJSqlParser extends AbstractJSqlParser { return (Function) inner; } + /** + * Whether the next token can continue the (possibly multi-word) keyword of + * an OPTION query hint, e.g. any word of {@code HASH JOIN}, {@code KEEP PLAN}, + * {@code OPTIMIZE FOR UNKNOWN} or {@code IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX}. + * + * Reserved words usable inside hint keywords are enumerated here because + * they cannot be matched by {@code RelObjectName()}. + */ + private boolean isOptionHintWordAhead() { + int kind = getToken(1).kind; + if (kind == S_IDENTIFIER + || (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD)) { + return true; + } + switch (kind) { + case K_JOIN: case K_FOR: case K_ORDER: + case K_UNION: case K_EXCEPT: case K_INTERSECT: + case K_USE: case K_OPTIMIZE: case K_FORCE: case K_UNKNOWN: + return true; + default: + return false; + } + } + + /** + * Whether the next token can start the single value argument of an OPTION + * query hint, e.g. the {@code 100} of {@code FAST 100} or the {@code 25} + * of {@code MAX_GRANT_PERCENT = 25}. + */ + private boolean isOptionHintValueAhead() { + switch (getToken(1).kind) { + case S_LONG: case S_DOUBLE: case S_CHAR_LITERAL: + case S_PARAMETER: + return true; + default: + return false; + } + } + /** * Follower-based disambiguation for reserved keywords in ambiguous * positions (implicit alias, clause boundary, after parenthesised @@ -927,6 +966,11 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; } + // OPTION (...) introduces a query hint clause, not an alias + if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) { + return false; + } + // Non-reserved keywords if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { return true; @@ -1374,6 +1418,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3724,6 +3769,7 @@ Update Update(): FromItem fromItem = null; List joins = null; Limit limit = null; + OptionClause optionClause = null; List orderByElements; boolean useColumnsBrackets = false; ReturningClause returningClause; @@ -3751,6 +3797,7 @@ Update Update(): [ orderByElements = OrderByElements() { update.setOrderByElements(orderByElements); } ] [ limit = PlainLimit() { update.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { update.setOption(optionClause); } ] [ returningClause = ReturningClause() { update.setReturningClause(returningClause); } ] { @@ -4250,6 +4297,7 @@ Delete Delete(): Expression where = null; PreferringClause preferringClause = null; Limit limit = null; + OptionClause optionClause = null; List orderByElements; boolean hasFrom = false; Token tk = null; @@ -4277,6 +4325,7 @@ Delete Delete(): [preferringClause=PreferringClause() { delete.setPreferringClause(preferringClause);} ] [orderByElements = OrderByElements() { delete.setOrderByElements(orderByElements); } ] [limit=PlainLimit() {delete.setLimit(limit); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { delete.setOption(optionClause); } ] [ returningClause = ReturningClause() { delete.setReturningClause(returningClause); } ] { @@ -4691,6 +4740,7 @@ Select Select() #Select: List> with = null; List orderByElements = null; List interpolateElements = null; + OptionClause optionClause = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -4715,6 +4765,7 @@ Select Select() #Select: [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { select.setOption(optionClause); } ] [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -5338,6 +5389,7 @@ PlainSelect PlainSelect() #PlainSelect: Expression preWhere = null; Expression where = null; ForClause forClause = null; + OptionClause optionClause = null; List orderByElements; List interpolateElements = null; GroupByElement groupBy = null; @@ -5470,6 +5522,7 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] + [ LOOKAHEAD(2) optionClause = OptionClause() { plainSelect.setOption(optionClause); } ] [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then // optionally attach ClickHouse's `LIMIT ... BY ...`. Checking BY right after the limit @@ -5606,6 +5659,10 @@ Select SetOperationList(Select select) #SetOperationList: { list.setInterpolate(ps.getInterpolate()); ps.setInterpolate(null); } + if (ps.getOption() != null) { + list.setOption(ps.getOption()); + ps.setOption(null); + } if (ps.getFetch() != null) { list.setFetch(ps.getFetch()); ps.setFetch(null); @@ -6855,6 +6912,64 @@ InterpolateElement InterpolateElement(): { return interpolateElement; } } +OptionClause OptionClause(): +{ + OptionClause optionClause = new OptionClause(); + OptionHint optionHint = null; +} +{ + "(" optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } + ( "," optionHint = OptionHint() { optionClause.addOptionHint(optionHint); } )* + ")" + { return optionClause; } +} + +OptionHint OptionHint(): +{ + OptionHint optionHint = new OptionHint(); + String name = null; + Expression value = null; + Expression parameter = null; +} +{ + name = OptionHintName() { optionHint.setName(name); } + [ + ( + LOOKAHEAD("=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); } + | + LOOKAHEAD("(") "(" parameter = Expression() { optionHint.addParameter(parameter); } + ( "," parameter = Expression() { optionHint.addParameter(parameter); } )* + ")" + | + LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); } + ) + ] + { return optionHint; } +} + +String OptionHintName(): +{ + Token tk = null; + String word = null; + StringBuilder name = new StringBuilder(); +} +{ + ( + LOOKAHEAD({ isOptionHintWordAhead() }) + ( tk= | word = NonReservedWord() + | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= ) + { + if (name.length() > 0) { name.append(' '); } + name.append(tk != null ? tk.image : word); + tk = null; + word = null; + } + )+ + { return name.toString(); } +} + JdbcParameter JdbcParameter() : { Token tk; JdbcParameter retval; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java new file mode 100644 index 0000000000..bfeba4be24 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -0,0 +1,158 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OptionClauseTest { + + @Test + public void testOptionHintIssue161() throws JSQLParserException { + String sql = + "SELECT CustomerID, PersonID, StoreID FROM cte OPTION (MAXRECURSION 2)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + PlainSelect plainSelect = (PlainSelect) ((Select) statement).getPlainSelect(); + Assertions.assertNotNull(plainSelect.getOption()); + Assertions.assertEquals(1, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("MAXRECURSION", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("2", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionWithoutArguments() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE x = 1 OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionMultiWordHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (KEEP PLAN, KEEPFIXED PLAN, FORCE ORDER, EXPAND VIEWS, NO_PERFORMANCE_SPOOL, IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(6, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("KEEP PLAN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("FORCE ORDER", + plainSelect.getOption().getOptionHints().get(2).getName()); + } + + @Test + public void testOptionJoinAndUnionHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (HASH JOIN, LOOP JOIN, MERGE JOIN, CONCAT UNION)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(4, plainSelect.getOption().getOptionHints().size()); + Assertions.assertEquals("HASH JOIN", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("CONCAT UNION", + plainSelect.getOption().getOptionHints().get(3).getName()); + } + + @Test + public void testOptionNumericValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (FAST 100, MAXDOP 4, MAXRECURSION 0)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("FAST", plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertEquals("100", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + Assertions.assertFalse(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + } + + @Test + public void testOptionEqualsValueHints() throws JSQLParserException { + String sql = "SELECT * FROM t OPTION (MAX_GRANT_PERCENT = 25.5, MIN_GRANT_PERCENT = 10)"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("MAX_GRANT_PERCENT", + plainSelect.getOption().getOptionHints().get(0).getName()); + Assertions.assertTrue(plainSelect.getOption().getOptionHints().get(0).isUseEquals()); + Assertions.assertEquals("25.5", + plainSelect.getOption().getOptionHints().get(0).getValue().toString()); + } + + @Test + public void testOptionParameterizedHints() throws JSQLParserException { + String sql = + "SELECT * FROM t OPTION (OPTIMIZE FOR UNKNOWN, USE HINT ('ASSUME_JOIN_PREDICATE_DEPENDS_ON_COLUMNS', 'DISABLE_PARAMETER_SNIFFING'))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals("OPTIMIZE FOR UNKNOWN", + plainSelect.getOption().getOptionHints().get(0).getName()); + OptionHint useHint = plainSelect.getOption().getOptionHints().get(1); + Assertions.assertEquals("USE HINT", useHint.getName()); + Assertions.assertEquals(2, useHint.getParameters().size()); + } + + @Test + public void testOptionOptimizeForAssignments() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("OPTIMIZE FOR", optimizeFor.getName()); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + } + + @Test + public void testOptionTableHint() throws JSQLParserException { + String sql = "SELECT * FROM t1 JOIN t2 ON t1.a = t2.a OPTION (TABLE HINT (t2, INDEX(ix2)))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint tableHint = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals("TABLE HINT", tableHint.getName()); + Assertions.assertEquals("t2", tableHint.getParameters().get(0).toString()); + Assertions.assertEquals("INDEX(ix2)", tableHint.getParameters().get(1).toString()); + } + + @Test + public void testOptionAfterUnion() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a OPTION (RECOMPILE, MAXDOP 1) LIMIT 10"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = + (SetOperationList) ((Select) statement).getSelectBody(); + Assertions.assertNotNull(setOperationList.getOption()); + Assertions.assertEquals(2, setOperationList.getOption().getOptionHints().size()); + } + + @Test + public void testOptionAfterForXml() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t FOR XML PATH('row') OPTION (RECOMPILE)", true); + } + + @Test + public void testOptionInUpdateAndDelete() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("UPDATE t SET a = 1 WHERE b = 2 OPTION (RECOMPILE)", true); + assertSqlCanBeParsedAndDeparsed("DELETE FROM t WHERE a = 1 OPTION (LOOP JOIN)", true); + + Statement update = CCJSqlParserUtil.parse("UPDATE t SET a = 1 OPTION (RECOMPILE)"); + Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption()); + } + + @Test + public void testOptionAsIdentifierStillWorks() throws JSQLParserException { + // OPTION stays a non-reserved keyword usable as column and table name + assertSqlCanBeParsedAndDeparsed("SELECT option FROM t", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM option", true); + assertSqlCanBeParsedAndDeparsed( + "SELECT a, option AS o FROM t WHERE option = 1 ORDER BY option", true); + } +} From 7b648250618299e97ee715c60f769ca760d85f63 Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 16 Aug 2026 08:57:41 +0800 Subject: [PATCH 008/387] feat: support CREATE DATABASE statement (#2070) (#2471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse CREATE DATABASE [IF NOT EXISTS] into a typed CreateDatabase statement instead of the generic UnsupportedStatement fallback. Vendor-specific options after the name (e.g. MySQL's DEFAULT CHARACTER SET / COLLATE) are captured verbatim and round-trip unchanged. Signed-off-by: 付典 --- .../sf/jsqlparser/parser/feature/Feature.java | 7 ++ .../statement/StatementVisitor.java | 7 ++ .../statement/StatementVisitorAdapter.java | 6 + .../create/database/CreateDatabase.java | 113 ++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 ++ .../util/deparser/StatementDeParser.java | 7 ++ .../validator/StatementValidator.java | 8 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 19 +++ .../create/database/CreateDatabaseTest.java | 76 ++++++++++++ 9 files changed, 255 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index d786f5170a..4c1b6dd512 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -30,6 +30,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; @@ -475,6 +476,12 @@ public enum Feature { */ drop, dropTable, dropIndex, dropView, dropSchema, dropSequence, dropTableIfExists, dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, dropSequenceIfExists, + /** + * SQL "CREATE DATABASE" statement is allowed + * + * @see CreateDatabase + */ + createDatabase, /** * SQL "CREATE SCHEMA" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 9ebab53a8b..24a5ed6abc 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -16,6 +16,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -118,6 +119,12 @@ default void visit(CreateSchema createSchema) { this.visit(createSchema, null); } + T visit(CreateDatabase createDatabase, S context); + + default void visit(CreateDatabase createDatabase) { + this.visit(createDatabase, null); + } + T visit(CreateTable createTable, S context); default void visit(CreateTable createTable) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 3b12c01c0b..38a32ea1f5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -20,6 +20,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -261,6 +262,11 @@ public T visit(CreateSchema createSchema, S context) { return null; } + @Override + public T visit(CreateDatabase createDatabase, S context) { + return null; + } + @Override public T visit(CreateTable createTable, S context) { return createTable.getTable().accept(fromItemVisitor, context); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java new file mode 100644 index 0000000000..9444d6bad3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/database/CreateDatabase.java @@ -0,0 +1,113 @@ +/*- + * #%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.statement.create.database; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * A {@code CREATE DATABASE} statement. Modelled after MySQL, where the statement takes an optional + * {@code IF NOT EXISTS} and vendor-specific options, which are captured verbatim. + */ +public class CreateDatabase implements Statement { + + private String databaseName; + private boolean hasIfNotExists = false; + private List databaseOptions = null; + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + /** + * The name of the database. + * + * @return the database name + */ + public String getDatabaseName() { + return databaseName; + } + + public CreateDatabase setDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + public boolean hasIfNotExists() { + return hasIfNotExists; + } + + public CreateDatabase setIfNotExists(boolean hasIfNotExists) { + this.hasIfNotExists = hasIfNotExists; + return this; + } + + /** + * The vendor-specific options of the database, captured as raw tokens. + * + * @return the list of option tokens, {@code null} when no options were given + */ + public List getDatabaseOptions() { + return databaseOptions; + } + + public CreateDatabase setDatabaseOptions(List databaseOptions) { + this.databaseOptions = databaseOptions; + return this; + } + + public CreateDatabase withDatabaseName(String databaseName) { + return this.setDatabaseName(databaseName); + } + + public CreateDatabase withIfNotExists(boolean hasIfNotExists) { + return this.setIfNotExists(hasIfNotExists); + } + + public CreateDatabase withDatabaseOptions(List databaseOptions) { + return this.setDatabaseOptions(databaseOptions); + } + + public CreateDatabase addDatabaseOptions(String... databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, databaseOptions); + return this.withDatabaseOptions(collection); + } + + public CreateDatabase addDatabaseOptions(Collection databaseOptions) { + List collection = + Optional.ofNullable(getDatabaseOptions()).orElseGet(ArrayList::new); + collection.addAll(databaseOptions); + return this.withDatabaseOptions(collection); + } + + @Override + public String toString() { + String sql = "CREATE DATABASE"; + if (hasIfNotExists) { + sql += " IF NOT EXISTS"; + } + if (databaseName != null) { + sql += " " + databaseName; + } + if (databaseOptions != null && !databaseOptions.isEmpty()) { + sql += " " + String.join(" ", databaseOptions); + } + return sql; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 99ee4eaccb..720e162541 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -94,6 +94,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -1166,6 +1167,17 @@ public void visit(CreateSchema createSchema) { StatementVisitor.super.visit(createSchema); } + @Override + public Void visit(CreateDatabase createDatabase, S context) { + throwUnsupported(createDatabase); + return null; + } + + @Override + public void visit(CreateDatabase createDatabase) { + StatementVisitor.super.visit(createDatabase); + } + @Override public Void visit(CreateTable create, S context) { visit(create.getTable(), null); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 751c4bf64d..51d06c7f1a 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -41,6 +41,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; import net.sf.jsqlparser.statement.create.schema.CreateSchema; @@ -431,6 +432,12 @@ public StringBuilder visit(CreateSchema aThis, S context) { return builder; } + @Override + public StringBuilder visit(CreateDatabase aThis, S context) { + builder.append(aThis.toString()); + return builder; + } + @Override public StringBuilder visit(CreateSequence createSequence, S context) { new CreateSequenceDeParser(builder).deParse(createSequence); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 9e073a227c..a3debcba3e 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -37,6 +37,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; @@ -299,6 +300,13 @@ public Void visit(CreateSchema aThis, S context) { return null; } + @Override + public Void visit(CreateDatabase aThis, S context) { + validateFeatureAndName(Feature.createDatabase, NamedObject.database, + aThis.getDatabaseName()); + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { getValidator(CreateSequenceValidator.class).validate(createSequence); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e571bb1b5d..2b71488f18 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -47,6 +47,7 @@ import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; import net.sf.jsqlparser.statement.alter.sequence.*; import net.sf.jsqlparser.statement.comment.*; +import net.sf.jsqlparser.statement.create.database.*; import net.sf.jsqlparser.statement.create.function.*; import net.sf.jsqlparser.statement.create.index.*; import net.sf.jsqlparser.statement.create.policy.*; @@ -10935,6 +10936,22 @@ List PathSpecification(): } } +CreateDatabase CreateDatabase(): +{ + CreateDatabase createDatabase = new CreateDatabase(); + String databaseName = null; + List databaseOptions = null; +} +{ + + [ LOOKAHEAD(2) { createDatabase.setIfNotExists(true); } ] + databaseName = RelObjectName() { createDatabase.setDatabaseName(databaseName); } + [ databaseOptions = captureRest() { createDatabase.setDatabaseOptions(databaseOptions); } ] + { + return createDatabase; + } +} + /** * Parses a single table-level constraint inside CREATE TABLE (...). * Handles INDEX, PRIMARY KEY, UNIQUE, KEY, FOREIGN KEY, CHECK, EXCLUDE. @@ -13382,6 +13399,8 @@ Statement Create(): | statement = CreateSchema() | + statement = CreateDatabase() + | statement = CreateSequence() | statement = CreateSynonym(isUsingOrReplace) diff --git a/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java new file mode 100644 index 0000000000..724b5ac059 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/database/CreateDatabaseTest.java @@ -0,0 +1,76 @@ +/*- + * #%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.statement.create.database; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +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.assertTrue; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class CreateDatabaseTest { + + @Test + void testCreateDatabaseIssue2070() throws JSQLParserException { + String statement = "CREATE DATABASE USERS"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("USERS"), statement); + } + + @Test + void testCreateDatabaseIfNotExists() throws JSQLParserException { + String statement = "CREATE DATABASE IF NOT EXISTS mydb"; + assertSqlCanBeParsedAndDeparsed(statement); + assertDeparse(new CreateDatabase().withDatabaseName("mydb").withIfNotExists(true), + statement); + } + + @Test + void testCreateDatabaseQuotedName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE `my db`"); + assertSqlCanBeParsedAndDeparsed("CREATE DATABASE \"my db\""); + } + + @Test + void testCreateDatabaseWithOptions() throws JSQLParserException { + String statement = + "CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"; + assertSqlCanBeParsedAndDeparsed(statement); + + Statement parsed = CCJSqlParserUtil.parse(statement); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, parsed); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + assertEquals( + Arrays.asList("DEFAULT", "CHARACTER", "SET", "utf8mb4", "COLLATE", + "utf8mb4_unicode_ci"), + createDatabase.getDatabaseOptions()); + } + + @Test + void testCreateDatabaseAST() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("CREATE DATABASE USERS"); + CreateDatabase createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("USERS", createDatabase.getDatabaseName()); + assertFalse(createDatabase.hasIfNotExists()); + + statement = CCJSqlParserUtil.parse("CREATE DATABASE IF NOT EXISTS mydb"); + createDatabase = assertInstanceOf(CreateDatabase.class, statement); + assertEquals("mydb", createDatabase.getDatabaseName()); + assertTrue(createDatabase.hasIfNotExists()); + } +} From 5080d19fab90463cbb0d513d2ec292110dca94f5 Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 16 Aug 2026 10:04:09 +0800 Subject: [PATCH 009/387] fix: do not treat a named parameter ':' as the ternary else separator (#2476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 34 ++++++++++++++- .../expression/TernaryExpressionTest.java | 43 ++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2b71488f18..973004a6ed 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -227,11 +227,14 @@ public class CCJSqlParser extends AbstractJSqlParser { * {@code cond ? then : else} rather than the PostgreSQL JSON operator: * a standalone ":" closes the then-branch at the same nesting depth before * any expression boundary (",", ";", EOF, an unbalanced closing bracket or a - * clause keyword such as FROM/WHERE). + * clause keyword such as FROM/WHERE). A ":" in operand position (directly + * after an operator) starts a JDBC named parameter instead and cannot close + * the then-branch. */ protected boolean isTernaryAhead() { try { int depth = 0; + Token prev = null; for (int i = 2; ; i++) { Token t = getToken(i); if (t == null || t.kind == EOF) { @@ -246,7 +249,7 @@ public class CCJSqlParser extends AbstractJSqlParser { } depth--; } else if (depth == 0) { - if (":".equals(image)) { + if (":".equals(image) && canEndExpression(prev)) { return true; } if (",".equals(image) || ";".equals(image)) { @@ -262,12 +265,39 @@ public class CCJSqlParser extends AbstractJSqlParser { break; } } + prev = t; } } catch (Exception e) { return false; } } + /** + * True when the token can be the LAST token of an expression: an + * identifier, a literal, a JDBC parameter, or a balanced closing bracket. + * Only such a token may directly precede the ":" closing the ternary + * then-branch, since the then-branch must be a complete expression. A ":" + * in operand position instead (directly after an operator, e.g. `x = :name` + * or directly after the leading "?") starts a JDBC named parameter. + * DATA_TYPE tokens end expressions as well, because a cast's target type + * (`b :: int`) or a bare type keyword closes the then-branch. + */ + private boolean canEndExpression(Token t) { + if (t == null) { + return false; + } + switch (t.kind) { + case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case S_PARAMETER: + case S_LONG: case S_DOUBLE: case S_HEX: case S_CHAR_LITERAL: + case K_DATE_LITERAL: case K_DATETIMELITERAL: case DATA_TYPE: + case K_NULL: case K_TRUE: case K_FALSE: + return true; + default: + return (t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD) + || ")".equals(t.image) || "]".equals(t.image); + } + } + /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. diff --git a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java index 3f3270176a..03f535db31 100644 --- a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator; +import net.sf.jsqlparser.expression.operators.relational.JsonOperator; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -67,7 +68,11 @@ void testIssue2436() throws JSQLParserException { "SELECT a ? b : ? FROM t", "SELECT * FROM t WHERE x = ? AND y ? z : w", // case expression inside a branch - "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t" + "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t", + // cast target type ends the then-branch (a bare ":" after a type + // keyword is the ternary separator, not a named parameter) + "SELECT a ? b :: int : c FROM t", + "SELECT x > 0 ? CAST(b AS int) : c FROM t" }) void testTernaryInVariousContexts(String sqlStr) throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); @@ -129,4 +134,40 @@ void testRightAssociativeNesting() throws JSQLParserException { void testUnrelatedSyntaxUnaffected(String sqlStr) throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testJsonbOperatorWithNamedParameter() throws JSQLParserException { + // regression guard (ternary support, #2466): a ":" in operand position + // starts a JDBC named parameter and must not close a ternary then-branch + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE j ? :key"); + Expression where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof JsonOperator); + Assertions.assertTrue( + ((JsonOperator) where).getRightExpression() instanceof JdbcNamedParameter); + } + + @Test + void testTernaryWithCastThenBranch() throws JSQLParserException { + // regression guard: a DATA_TYPE token can end the then-branch, so the + // ":" after the cast target type must close the ternary + Select select = (Select) CCJSqlParserUtil.parse("SELECT a ? b :: int : c FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof CastExpression); + } + + @ParameterizedTest + @ValueSource(strings = { + // PostgreSQL jsonb operator combined with JDBC named parameters + "SELECT * FROM t WHERE j ? :key", + "SELECT * FROM t WHERE j ? 'k' AND x = :p", + "SELECT * FROM t WHERE j ? 'k' OR x = :p", + "SELECT * FROM t WHERE j ? :key AND x = 1", + // named parameters keep working inside ternary branches, too + "SELECT a ? :t : :e FROM t", + "SELECT * FROM t WHERE c = :c AND a ? :t : :e" + }) + void testTernaryAndJsonbWithJdbcNamedParameters(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } From f41c0b8aa29cd22f7ab77da46763f2e26b6b1475 Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 16 Aug 2026 18:54:39 +0800 Subject: [PATCH 010/387] fix: treat a positional parameter '?' as a valid end of the ternary then-branch (#2477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare '?' (anonymous token, no kind constant) was missing from canEndExpression(), so a ternary whose then-branch ends with a positional parameter was silently misread as a jsonb operator: SELECT a ? ? : c FROM t parsed as JsonOperator(a, JsonExpression(?:c)) instead of TernaryExpression(a, JdbcParameter, c). Recognize it by image, like the closing brackets, and pin the AST shape with a regression test. Follow-up to #2476. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 7 +++++-- .../expression/TernaryExpressionTest.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 973004a6ed..d86e227d7c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -280,7 +280,9 @@ public class CCJSqlParser extends AbstractJSqlParser { * in operand position instead (directly after an operator, e.g. `x = :name` * or directly after the leading "?") starts a JDBC named parameter. * DATA_TYPE tokens end expressions as well, because a cast's target type - * (`b :: int`) or a bare type keyword closes the then-branch. + * (`b :: int`) or a bare type keyword closes the then-branch. The bare "?" + * positional parameter has no token-kind constant (anonymous token), so it + * is recognized by its image, like the closing brackets. */ private boolean canEndExpression(Token t) { if (t == null) { @@ -294,7 +296,8 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; default: return (t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD) - || ")".equals(t.image) || "]".equals(t.image); + || ")".equals(t.image) || "]".equals(t.image) + || "?".equals(t.image); } } diff --git a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java index 03f535db31..38f20111df 100644 --- a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; @@ -65,6 +66,8 @@ void testIssue2436() throws JSQLParserException { "SELECT a IS NULL ? 'x' : y FROM t", // jdbc parameters as branches "SELECT a ? ? : c FROM t", + "SELECT a ? b + ? : c FROM t", + "SELECT a ? ? : ? FROM t", "SELECT a ? b : ? FROM t", "SELECT * FROM t WHERE x = ? AND y ? z : w", // case expression inside a branch @@ -156,6 +159,23 @@ void testTernaryWithCastThenBranch() throws JSQLParserException { Assertions.assertTrue(ternary.getThenExpression() instanceof CastExpression); } + @Test + void testTernaryWithPositionalParameterThenBranch() throws JSQLParserException { + // regression guard: a bare "?" positional parameter is a complete + // expression, so the ":" after it closes the ternary instead of being + // skipped in favor of the jsonb reading + Select select = (Select) CCJSqlParserUtil.parse("SELECT a ? ? : c FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof JdbcParameter); + Assertions.assertTrue(ternary.getElseExpression() instanceof Column); + + // a positional parameter as the LAST token of the then-branch, too + select = (Select) CCJSqlParserUtil.parse("SELECT a ? b + ? : c FROM t"); + ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0).getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof Addition); + } + @ParameterizedTest @ValueSource(strings = { // PostgreSQL jsonb operator combined with JDBC named parameters From a8cb410ad118e789630e9d43a7a15f38ce7e5bdd Mon Sep 17 00:00:00 2001 From: fudian Date: Tue, 18 Aug 2026 07:47:50 +0800 Subject: [PATCH 011/387] fix: reject illegal MERGE WHEN NOT MATCHED clause pairings (#2480) (#2481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHEN NOT MATCHED accepted any clause regardless of the BY qualifier, and the AST collapsed the illegal pairings into the opposite semantics on deparse: WHEN NOT MATCHED [BY TARGET] THEN UPDATE/DELETE printed as WHEN MATCHED, WHEN NOT MATCHED BY SOURCE THEN INSERT printed as WHEN NOT MATCHED. SQL Server and BigQuery reject these pairings at parse time. Validate the pairing via MergeSide.validatePairing, mirroring the existing MergeSide.fromImage validation. Signed-off-by: 付典 --- .../jsqlparser/statement/merge/MergeSide.java | 23 +++++++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 +- .../jsqlparser/statement/merge/MergeTest.java | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java index d871021f7a..6dc7815fb7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java @@ -29,6 +29,29 @@ public enum MergeSide { * @throws IllegalArgumentException if {@code image} is neither {@code TARGET} nor * {@code SOURCE} */ + /** + * Validates the clause pairing of a {@code WHEN NOT MATCHED} branch: {@code BY TARGET} (the + * default) only allows an {@code INSERT} clause, {@code BY SOURCE} only allows {@code UPDATE} + * or {@code DELETE}. + * + * @param side the parsed {@code BY TARGET}/{@code BY SOURCE} qualifier, null when absent + * @param operation the parsed clause + * @return the passed {@code operation} + * @throws IllegalArgumentException when the pairing is not legal in any dialect + */ + public static MergeOperation validatePairing(MergeSide side, MergeOperation operation) { + if (side == MergeSide.SOURCE) { + if (operation instanceof MergeInsert) { + throw new IllegalArgumentException( + "WHEN NOT MATCHED BY SOURCE cannot take an INSERT clause"); + } + } else if (!(operation instanceof MergeInsert)) { + throw new IllegalArgumentException( + "WHEN NOT MATCHED [BY TARGET] cannot take an UPDATE or DELETE clause"); + } + return operation; + } + public static MergeSide fromImage(String image) { for (MergeSide value : values()) { if (value.name().equalsIgnoreCase(image)) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d86e227d7c..9d03142049 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4489,7 +4489,7 @@ MergeOperation MergeWhenNotMatched() : { | operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); } ) - { return operation; } + { return MergeSide.validatePairing(side, operation); } } // table names seem to allow ":" delimiters, e.g. for Informix see #1134 diff --git a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java index 27c8c4da0c..87d1d14489 100644 --- a/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/merge/MergeTest.java @@ -25,6 +25,7 @@ import static net.sf.jsqlparser.test.TestUtils.assertOracleHintExists; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; /** @@ -363,4 +364,36 @@ private static Stream deriveStandardClausesFromOperationsCases() { Arguments.of(Arrays.asList(insert1, insert2, update1, update2, delete1), update1, insert1, true)); } + + @Test + void testMergeRejectsInvalidWhenNotMatchedClausePairings() { + // WHEN MATCHED rows exist in the target: INSERT is not legal + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN INSERT (a) VALUES (1)")); + + // WHEN NOT MATCHED [BY TARGET] rows do not exist in the target: only INSERT is legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DELETE"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN UPDATE SET a = 1"); + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY TARGET THEN DELETE"); + + // WHEN NOT MATCHED BY SOURCE rows exist in the target: INSERT is not legal + assertInvalidMergePairing( + "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED BY SOURCE THEN INSERT (a) VALUES (1)"); + } + + private static void assertInvalidMergePairing(String sql) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql)); + Throwable cause = exception; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + assertThat(cause).isInstanceOf(IllegalArgumentException.class); + assertThat(cause.getMessage()).contains("WHEN NOT MATCHED"); + } } From ad69eccc9928b92131a399b4608ea7ac10911a9f Mon Sep 17 00:00:00 2001 From: fudian Date: Tue, 18 Aug 2026 07:49:34 +0800 Subject: [PATCH 012/387] fix: complete TablesNamesFinder traversal for piped queries, DML side clauses and analytic functions (#2478) (#2479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Silently missed tables or threw for: piped queries (visit(FromQuery) was empty, now implements PipeOperatorVisitor), DELETE WITH lists (CTE tables lost, CTE alias leaked as phantom table), MERGE ON condition and WHEN operations (now implements MergeOperationVisitor), INSERT SET / ON DUPLICATE KEY UPDATE / ON CONFLICT actions and OUTPUT / RETURNING clauses of INSERT / UPDATE / DELETE, data modifying CTEs (ClassCastException in WithItem.getSelect(), dispatch any ParenthesedStatement instead) and analytic functions with function level ORDER BY (NPE), window ORDER BY or FILTER clause. Signed-off-by: 付典 --- .../sf/jsqlparser/util/TablesNamesFinder.java | 277 +++++++++++++++++- .../util/TablesNamesFinderTest.java | 130 +++++++- 2 files changed, 402 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 720e162541..3e15be9a77 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -76,6 +76,7 @@ import net.sf.jsqlparser.statement.PurgeObjectType; import net.sf.jsqlparser.statement.PurgeStatement; import net.sf.jsqlparser.statement.ResetStatement; +import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.statement.RollbackStatement; import net.sf.jsqlparser.statement.SavepointStatement; import net.sf.jsqlparser.statement.SessionStatement; @@ -92,6 +93,7 @@ import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; +import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; import net.sf.jsqlparser.statement.create.database.CreateDatabase; @@ -111,12 +113,38 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertConflictAction; +import net.sf.jsqlparser.statement.insert.InsertDuplicateAction; import net.sf.jsqlparser.statement.insert.OracleMultiInsertBranch; import net.sf.jsqlparser.statement.insert.OracleMultiInsertClause; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.merge.MergeDelete; +import net.sf.jsqlparser.statement.merge.MergeInsert; +import net.sf.jsqlparser.statement.merge.MergeOperation; +import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; +import net.sf.jsqlparser.statement.merge.MergeUpdate; +import net.sf.jsqlparser.statement.piped.AggregatePipeOperator; +import net.sf.jsqlparser.statement.piped.AsPipeOperator; +import net.sf.jsqlparser.statement.piped.CallPipeOperator; +import net.sf.jsqlparser.statement.piped.DropPipeOperator; +import net.sf.jsqlparser.statement.piped.ExtendPipeOperator; import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.JoinPipeOperator; +import net.sf.jsqlparser.statement.piped.LimitPipeOperator; +import net.sf.jsqlparser.statement.piped.OrderByPipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperatorVisitor; +import net.sf.jsqlparser.statement.piped.PivotPipeOperator; +import net.sf.jsqlparser.statement.piped.RenamePipeOperator; +import net.sf.jsqlparser.statement.piped.SelectPipeOperator; +import net.sf.jsqlparser.statement.piped.SetOperationPipeOperator; +import net.sf.jsqlparser.statement.piped.SetPipeOperator; +import net.sf.jsqlparser.statement.piped.TableSamplePipeOperator; +import net.sf.jsqlparser.statement.piped.UnPivotPipeOperator; +import net.sf.jsqlparser.statement.piped.WherePipeOperator; +import net.sf.jsqlparser.statement.piped.WindowPipeOperator; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.select.AllColumns; import net.sf.jsqlparser.statement.select.AllTableColumns; @@ -156,7 +184,8 @@ @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.UncommentedEmptyMethodBody"}) public class TablesNamesFinder implements SelectVisitor, FromItemVisitor, ExpressionVisitor, - SelectItemVisitor, StatementVisitor { + SelectItemVisitor, StatementVisitor, MergeOperationVisitor, + PipeOperatorVisitor { private Set tables; private boolean allowColumnProcessing = false; @@ -277,9 +306,8 @@ public Void visit(WithItem withItem, S context) { if (withItem.getAlias() != null) { otherItemNames.add(withItem.getAlias().getName()); } - if (withItem.getSelect() != null) { - withItem.getSelect().accept((SelectVisitor) this, context); - } + // dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert) + withItem.accept((StatementVisitor) this, context); return null; } @@ -768,7 +796,15 @@ public Void visit(AnalyticExpression analytic, S context) { if (analytic.getKeep() != null) { analytic.getKeep().accept(this, context); } + if (analytic.getFilterExpression() != null) { + analytic.getFilterExpression().accept(this, context); + } if (analytic.getFuncOrderBy() != null) { + for (OrderByElement element : analytic.getFuncOrderBy()) { + element.getExpression().accept(this, context); + } + } + if (analytic.getOrderByElements() != null) { for (OrderByElement element : analytic.getOrderByElements()) { element.getExpression().accept(this, context); } @@ -852,9 +888,143 @@ public void visit(TableStatement tableStatement) { @Override public Void visit(FromQuery fromQuery, S context) { + List> withItemsList = fromQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + for (WithItem withItem : withItemsList) { + withItem.accept((SelectVisitor) this, context); + } + } + if (fromQuery.getFromItem() != null) { + fromQuery.getFromItem().accept(this, context); + } + for (PipeOperator pipeOperator : fromQuery.getPipeOperators()) { + pipeOperator.accept(this, null); + } + return null; + } + + @Override + public Void visit(AggregatePipeOperator aggregate, Void context) { + for (SelectItem selectItem : aggregate.getSelectItems()) { + selectItem.accept(this, context); + } + for (SelectItem groupItem : aggregate.getGroupItems()) { + groupItem.accept(this, context); + } return null; } + @Override + public Void visit(AsPipeOperator as, Void context) { + if (as.getAlias() != null) { + otherItemNames.add(as.getAlias().getName()); + } + return null; + } + + @Override + public Void visit(CallPipeOperator call, Void context) { + visit(call.getTableFunction(), context); + return null; + } + + @Override + public Void visit(DropPipeOperator drop, Void context) { + drop.getColumns().accept(this, context); + return null; + } + + @Override + public Void visit(ExtendPipeOperator extend, Void context) { + return visit((SelectPipeOperator) extend, context); + } + + @Override + public Void visit(JoinPipeOperator joinPipeOperator, Void context) { + visitJoins(List.of(joinPipeOperator.getJoin()), context); + return null; + } + + @Override + public Void visit(LimitPipeOperator limit, Void context) { + limit.getLimitExpression().accept(this, context); + if (limit.getOffsetExpression() != null) { + limit.getOffsetExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(OrderByPipeOperator orderBy, Void context) { + for (OrderByElement element : orderBy.getOrderByElements()) { + element.getExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(PivotPipeOperator pivot, Void context) { + pivot.getAggregateExpression().accept(this, context); + for (SelectItem pivotColumn : pivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(RenamePipeOperator rename, Void context) { + return visit((SelectPipeOperator) rename, context); + } + + @Override + public Void visit(SelectPipeOperator select, Void context) { + for (SelectItem selectItem : select.getSelectItems()) { + selectItem.accept(this, context); + } + return null; + } + + @Override + public Void visit(SetPipeOperator set, Void context) { + for (UpdateSet updateSet : set.getUpdateSets()) { + updateSet.getColumns().accept(this, context); + updateSet.getValues().accept(this, context); + } + return null; + } + + @Override + public Void visit(TableSamplePipeOperator tableSample, Void context) { + return null; + } + + @Override + public Void visit(SetOperationPipeOperator setOperation, Void context) { + for (ParenthesedSelect select : setOperation.getSelects()) { + select.accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public Void visit(UnPivotPipeOperator unPivot, Void context) { + for (SelectItem pivotColumn : unPivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(WherePipeOperator where, Void context) { + where.getExpression().accept(this, context); + return null; + } + + @Override + public Void visit(WindowPipeOperator window, Void context) { + return visit((SelectPipeOperator) window, context); + } + @Override public Void visit(DateUnitExpression dateUnitExpression, S context) { return null; @@ -988,6 +1158,11 @@ public Void visit(MySQLGroupConcat groupConcat, S context) { @Override public Void visit(Delete delete, S context) { + if (delete.getWithItemsList() != null) { + for (WithItem withItem : delete.getWithItemsList()) { + withItem.accept((SelectVisitor) this, context); + } + } visit(delete.getTable(), context); if (delete.getUsingFromItemList() != null) { @@ -1001,6 +1176,8 @@ public Void visit(Delete delete, S context) { if (delete.getWhere() != null) { delete.getWhere().accept(this, context); } + visitOutputClause(delete.getOutputClause(), context); + visitReturningClause(delete.getReturningClause(), context); return null; } @@ -1058,6 +1235,8 @@ public Void visit(Update update, S context) { if (update.getWhere() != null) { update.getWhere().accept(this, context); } + visitOutputClause(update.getOutputClause(), context); + visitReturningClause(update.getReturningClause(), context); return null; } @@ -1096,12 +1275,55 @@ public Void visit(Insert insert, S context) { withItem.accept((SelectVisitor) this, context); } } + if (insert.getSetUpdateSets() != null) { + visitUpdateSets(insert.getSetUpdateSets(), context); + } + if (insert.getDuplicateAction() != null) { + visitInsertAction(insert.getDuplicateAction(), context); + } + if (insert.getConflictAction() != null) { + visitInsertAction(insert.getConflictAction(), context); + } + visitOutputClause(insert.getOutputClause(), context); + visitReturningClause(insert.getReturningClause(), context); if (insert.getSelect() != null) { visit(insert.getSelect(), context); } return null; } + private void visitInsertAction(InsertDuplicateAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } + } + + private void visitInsertAction(InsertConflictAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } + } + + @Override + public Void visitOutputClause(OutputClause outputClause, S context) { + if (outputClause != null && outputClause.getSelectItemList() != null) { + for (SelectItem selectItem : outputClause.getSelectItemList()) { + selectItem.accept(this, context); + } + } + return null; + } + + private void visitReturningClause(ReturningClause returningClause, S context) { + if (returningClause != null) { + for (SelectItem selectItem : returningClause) { + selectItem.accept(this, context); + } + } + } + @Override public Void visit(ParenthesedInsert insert, S context) { return visit(insert.getInsert(), context); @@ -1315,6 +1537,53 @@ public Void visit(Merge merge, S context) { if (merge.getFromItem() != null) { merge.getFromItem().accept(this, context); } + + if (merge.getOnCondition() != null) { + merge.getOnCondition().accept(this, context); + } + + if (merge.getOperations() != null) { + for (MergeOperation operation : merge.getOperations()) { + operation.accept(this, context); + } + } + return null; + } + + @Override + public Void visit(MergeDelete mergeDelete, S context) { + if (mergeDelete.getAndPredicate() != null) { + mergeDelete.getAndPredicate().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeInsert mergeInsert, S context) { + if (mergeInsert.getAndPredicate() != null) { + mergeInsert.getAndPredicate().accept(this, context); + } + if (mergeInsert.getValues() != null) { + mergeInsert.getValues().accept(this, context); + } + if (mergeInsert.getWhereCondition() != null) { + mergeInsert.getWhereCondition().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeUpdate mergeUpdate, S context) { + if (mergeUpdate.getAndPredicate() != null) { + mergeUpdate.getAndPredicate().accept(this, context); + } + visitUpdateSets(mergeUpdate.getUpdateSets(), context); + if (mergeUpdate.getWhereCondition() != null) { + mergeUpdate.getWhereCondition().accept(this, context); + } + if (mergeUpdate.getDeleteWhereCondition() != null) { + mergeUpdate.getDeleteWhereCondition().accept(this, context); + } return null; } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 8581a0e59b..53a33e9882 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -773,10 +773,138 @@ void testJsonTable() throws JSQLParserException { @Test void testWindowExpressionWithNoRangeAndNoOffsetDoesNotThrowException() { - String sqlStr = "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; + String sqlStr = + "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; assertThatCode(() -> TablesNamesFinder.findTables(sqlStr)) .doesNotThrowAnyException(); } + @Test + void testPipedQuery() throws JSQLParserException { + String sqlStr = "FROM MY_TABLE1\n" + + "|> WHERE id IN (SELECT id FROM MY_TABLE2)\n" + + "|> LEFT JOIN (SELECT item, id FROM MY_TABLE3) AS t3 ON t3.item = item\n" + + "|> UNION ALL (SELECT * FROM MY_TABLE4)\n" + + "|> SELECT item"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4"); + } + + @Test + void testDeleteWithWithItemList() throws JSQLParserException { + String sqlStr = + "WITH cte AS (SELECT * FROM MY_TABLE2) DELETE FROM MY_TABLE1 WHERE id IN (SELECT id FROM cte)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testMergeOnConditionAndOperations() throws JSQLParserException { + String sqlStr = "MERGE INTO MY_TABLE1 t USING MY_TABLE2 s " + + "ON t.id IN (SELECT id FROM MY_TABLE3) " + + "WHEN MATCHED AND t.v > (SELECT MIN(v) FROM MY_TABLE4) THEN UPDATE SET t.v = (SELECT MAX(v) FROM MY_TABLE5) " + + "WHEN NOT MATCHED THEN INSERT (v) VALUES (1)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4", "MY_TABLE5"); + } + + @Test + void testInsertWithSetUpdateSets() throws JSQLParserException { + String sqlStr = "INSERT INTO MY_TABLE1 SET a = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnDuplicateKeyUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON DUPLICATE KEY UPDATE b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnConflictDoUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testWithDataModifyingCte() throws JSQLParserException { + String sqlStr = + "WITH del AS (DELETE FROM MY_TABLE2) INSERT INTO MY_TABLE1 SELECT * FROM del"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithOutputClause() throws JSQLParserException { + String sqlStr = + "UPDATE MY_TABLE1 SET a = 1 OUTPUT (SELECT x FROM MY_TABLE2) INTO @tv"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithReturningClause() throws JSQLParserException { + String sqlStr = "UPDATE MY_TABLE1 SET a = 1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithReturningClause() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testDeleteWithReturningClause() throws JSQLParserException { + String sqlStr = "DELETE FROM MY_TABLE1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFunctionOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT string_agg(name, ',' ORDER BY id) OVER (PARTITION BY grp) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1"); + } + + @Test + void testAnalyticFunctionsWithWindowOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) OVER (ORDER BY (SELECT MAX(k) FROM MY_TABLE2)) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFilterClause() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) FILTER (WHERE id IN (SELECT id FROM MY_TABLE2)) OVER () FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + } From 5c86877dfa8f3d3732dd62a79cf8e17e43d6212d Mon Sep 17 00:00:00 2001 From: Watashi00 <94692745+watashi-00@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:45:47 -0300 Subject: [PATCH 013/387] feat(parser): add support for ClickHouse [LEFT] ARRAY JOIN syntax (#2482) * feat(Join): add array join support to Join class * feat(parser): add support for ClickHouse ARRAY JOIN syntax and deparsing * fix(parser): correct array literal parsing in JSqlParserCC.jjt * style: apply spotless code formatting --- .../sf/jsqlparser/statement/select/Join.java | 23 ++++++++++- .../util/deparser/SelectDeParser.java | 4 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 38 +++++++++++-------- .../statement/select/SelectTest.java | 29 ++++++++++++++ 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Join.java b/src/main/java/net/sf/jsqlparser/statement/select/Join.java index 191465e27f..c8d675cecb 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Join.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Join.java @@ -40,6 +40,7 @@ public class Join extends ASTNodeAccessImpl { private boolean straight = false; private boolean apply = false; private boolean fetch = false; + private boolean array = false; private FromItem fromItem; private KSQLJoinWindow joinWindow; @@ -73,7 +74,10 @@ public boolean isInnerJoin() { || cross /* Natural Join */ - || natural); + || natural + + /* Array Join */ + || array); } /** @@ -337,6 +341,19 @@ public Join withCross(boolean cross) { return this; } + public boolean isArray() { + return array; + } + + public void setArray(boolean array) { + this.array = array; + } + + public Join withArray(boolean array) { + this.setArray(array); + return this; + } + /** * Returns the right item of the join */ @@ -489,6 +506,10 @@ public String toString() { builder.append("SEMI "); } + if (isArray()) { + builder.append("ARRAY "); + } + if (isStraight()) { builder.append("STRAIGHT_JOIN "); } else if (isApply()) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index c061ff45f4..066d40d069 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -683,6 +683,10 @@ public void deparseJoin(Join join) { builder.append(" SEMI"); } + if (join.isArray()) { + builder.append(" ARRAY"); + } + if (join.isStraight()) { builder.append(" STRAIGHT_JOIN "); } else if (join.isApply()) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9d03142049..da45e58870 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -829,9 +829,11 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_CONNECT: return nextKind != K_BY; case K_START: return nextKind != K_WITH; case K_LEFT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI; + && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; + case K_ARRAY_LITERAL: + return nextKind != K_JOIN; case K_ALL: return nextKind != K_JOIN; case K_ANY: return nextKind != OPENING_BRACKET; case K_SOME: return nextKind != OPENING_BRACKET; @@ -993,29 +995,17 @@ public class CCJSqlParser extends AbstractJSqlParser { // String-literal alias: SELECT col 'myAlias' if (kind == S_CHAR_LITERAL) return true; - // Base identifier tokens - if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER - || kind == DATA_TYPE || kind == K_DATETIMELITERAL - || kind == K_DATE_LITERAL) { - return true; - } - // OPTION (...) introduces a query hint clause, not an alias if (kind == K_OPTION && getToken(2).kind == OPENING_BRACKET) { return false; } - // Non-reserved keywords - if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { - return true; - } - // For reserved keywords in alias position, skip the structural- // keyword whitelist and go directly to follower disambiguation. // Only check keywords that are actually in RelObjectName's token // alternatives — don't fire for brackets, operators, literals, etc. switch (kind) { - case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: + case K_ALL: case K_ARRAY_LITERAL: 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: @@ -1026,8 +1016,22 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_TOP: case K_VALUE: case K_VALUES: return isReservedKeywordSafeByFollower(); default: - return false; + break; } + + // Base identifier tokens + if (kind == S_IDENTIFIER || kind == S_QUOTED_IDENTIFIER + || kind == DATA_TYPE || kind == K_DATETIMELITERAL + || kind == K_DATE_LITERAL) { + return true; + } + + // Non-reserved keywords + if (kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD) { + return true; + } + + return false; } /** @@ -6492,6 +6496,8 @@ Join JoinerExpression() #JoinerExpression: { join.setOuter(true); } ] + [ { join.setArray(true); } ] + ( ( [ joinHint=JoinHint() {join.setJoinHint(joinHint); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index d0000c1389..c664acd8af 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -6527,4 +6527,33 @@ void testIssue2445FunctionWithBracketParameters() throws JSQLParserException { " , cast(ex.value_date - f.appraisal_date AS DECIMAL) / 365 )"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testClickHouseArrayJoin() throws Exception { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, x FROM t ARRAY JOIN arr AS x", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t ARRAY JOIN arr", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT s, arr FROM t LEFT ARRAY JOIN arr", true); + + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT s, x, o.name FROM t LEFT ARRAY JOIN arr AS x INNER JOIN other_table o ON t.id = o.t_id", + true); + TestUtils.assertSqlCanBeParsedAndDeparsed( + "WITH exploded AS (SELECT s, x FROM t LEFT ARRAY JOIN arr AS x) SELECT * FROM exploded", + true); + + PlainSelect selectLeft = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t LEFT ARRAY JOIN arr AS x"); + Join joinLeft = selectLeft.getJoins().get(0); + Assertions.assertTrue(joinLeft.isArray(), "Should be an array join"); + Assertions.assertTrue(joinLeft.isLeft(), "Should be a left join"); + + PlainSelect selectInner = + (PlainSelect) CCJSqlParserUtil.parse("SELECT s, x FROM t ARRAY JOIN arr AS x"); + Join joinInner = selectInner.getJoins().get(0); + Assertions.assertTrue(joinInner.isArray(), "Should be an array join"); + Assertions.assertFalse(joinInner.isLeft(), "Should not be a left join"); + } + } From 70139091d9d78f2ddf84aa487004921d78eef936 Mon Sep 17 00:00:00 2001 From: fudian Date: Tue, 18 Aug 2026 16:12:45 +0800 Subject: [PATCH 014/387] fix: keep all clauses when deparsing SELECT without FROM (#2483) (#2484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlainSelect.appendSelectBodyTo printed the post-FROM clause chain (oracle hierarchical, PREFERRING, GROUP BY, HAVING, QUALIFY, WINDOW, EMIT CHANGES) only when a FROM item was present; the "without from" branch rendered WHERE and PREWHERE alone. Move the shared clause chain out of the fromItem branch, mirroring SelectDeParser, so a FROM-less SELECT renders every clause stored on the AST. Output for SELECTs with FROM is unchanged (same checks in the same order). Signed-off-by: 付典 --- .../statement/select/PlainSelect.java | 66 ++++++++----------- .../statement/select/SelectTest.java | 23 +++++++ 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java index 1d76255e05..1f289effe8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java @@ -607,43 +607,35 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (ksqlWindow != null) { builder.append(" WINDOW ").append(ksqlWindow); } - if (preWhere != null) { - builder.append(" PREWHERE ").append(preWhere); - } - if (where != null) { - builder.append(" WHERE ").append(where); - } - if (oracleHierarchical != null) { - builder.append(oracleHierarchical); - } - if (preferringClause != null) { - builder.append(" ").append(preferringClause); - } - if (groupBy != null) { - builder.append(" ").append(groupBy); - } - if (having != null) { - builder.append(" HAVING ").append(having); - } - if (qualify != null) { - builder.append(" QUALIFY ").append(qualify); - } - if (windowDefinitions != null) { - builder.append(" WINDOW "); - builder.append(windowDefinitions.stream().map(WindowDefinition::toString) - .collect(joining(", "))); - } - if (emitChanges) { - builder.append(" EMIT CHANGES"); - } - } else { - // without from - if (preWhere != null) { - builder.append(" PREWHERE ").append(preWhere); - } - if (where != null) { - builder.append(" WHERE ").append(where); - } + } + if (preWhere != null) { + builder.append(" PREWHERE ").append(preWhere); + } + if (where != null) { + builder.append(" WHERE ").append(where); + } + if (oracleHierarchical != null) { + builder.append(oracleHierarchical); + } + if (preferringClause != null) { + builder.append(" ").append(preferringClause); + } + if (groupBy != null) { + builder.append(" ").append(groupBy); + } + if (having != null) { + builder.append(" HAVING ").append(having); + } + if (qualify != null) { + builder.append(" QUALIFY ").append(qualify); + } + if (windowDefinitions != null) { + builder.append(" WINDOW "); + builder.append(windowDefinitions.stream().map(WindowDefinition::toString) + .collect(joining(", "))); + } + if (emitChanges) { + builder.append(" EMIT CHANGES"); } if (intoTempTable != null) { builder.append(" INTO TEMP ").append(intoTempTable); diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index c664acd8af..a0f7489377 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -1907,6 +1907,29 @@ public void testSelectFunction() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(statement); } + @Test + public void testSelectWithoutFromRetainsWhereGroupByHaving() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT 1 WHERE 1 = 1"); + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT 1 GROUP BY 1 HAVING 1 = 1"); + assertNotNull(plainSelect.getGroupBy()); + assertNotNull(plainSelect.getHaving()); + } + + @Test + public void testSelectWithoutFromRetainsWindowAndQualify() throws JSQLParserException { + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT 1 WINDOW w AS (ORDER BY 1)"); + assertEquals(1, plainSelect.getWindowDefinitions().size()); + assertSqlCanBeParsedAndDeparsed("SELECT 1 QUALIFY 1 = 1"); + } + + @Test + public void testSelectWithoutFromRetainsHierarchicalAndPreferring() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT 1 START WITH 1 = 1 CONNECT BY LEVEL <= 1"); + assertSqlCanBeParsedAndDeparsed("SELECT 1 PREFERRING HIGH 1"); + } + @Test public void testWeirdSelect() throws JSQLParserException { String sql = From 55b23637604ea249e673140a4a7331e389d2d0ad Mon Sep 17 00:00:00 2001 From: Denis Gregor Date: Tue, 18 Aug 2026 22:25:49 -0500 Subject: [PATCH 015/387] Separate JSON operators with spaces so #> deparses correctly (#2487) JsonExpression.toString glued each operator directly onto its operand, so a #> ... deparsed as a#>... . Because # is a legal identifier character, that re-parses as a# > ... -- a GreaterThan, a different tree from the original JsonExpression. Emit a space on each side of the operator, matching the JsonOperator visitor. --- .../sf/jsqlparser/expression/JsonExpression.java | 5 ++++- .../expression/JsonExpressionTest.java | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java index f258e855cb..7cd34b53d7 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java @@ -87,7 +87,10 @@ public String toString() { StringBuilder b = new StringBuilder(); b.append(expr.toString()); for (Map.Entry ident : idents) { - b.append(ident.getValue()).append(ident.getKey()); + // Separate the operator with spaces: without them `#>`/`#>>` glue onto + // the operand (e.g. `a#>'{b}'`), and since `#` is a legal identifier + // character the result re-parses as `a#` `>` `...` -- a different tree. + b.append(' ').append(ident.getValue()).append(' ').append(ident.getKey()); } return b.toString(); } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java index 5fc72bea8e..b31efae37c 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -39,6 +40,21 @@ void testIssue1792() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void testHashArrowOperatorsRoundTrip() throws JSQLParserException { + // #> and #>> must survive deparse+reparse: without spaces the operator glues + // onto the object (a#>'{b}') and re-parses as the comparison a# > '{b}'. + for (String sqlStr : new String[] {"SELECT a #> '{b}' FROM t", + "SELECT a #>> '{b}' FROM t"}) { + PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr); + Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression()); + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(st.toString()); + Assertions.assertInstanceOf(JsonExpression.class, + reparsed.getSelectItem(0).getExpression(), + sqlStr + " deparsed to " + st); + } + } + @Test void testSnowflakeGetOperator() throws JSQLParserException { // https://docs.snowflake.com/en/user-guide/querying-semistructured From 4c6a4fb141db7dbe40a9cf4c5a5d12e21bde5ff8 Mon Sep 17 00:00:00 2001 From: fudian Date: Wed, 19 Aug 2026 15:54:32 +0800 Subject: [PATCH 016/387] feat: complete SQL Server OPTION query hint support (#2486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attach OPTION to MERGE statements and validate and deparse its parameters. Parse OPTIMIZE FOR (@variable UNKNOWN) as a table-less Column, reusing the existing expression visitor contract. Reject OPTION after standalone VALUES, including INSERT ... VALUES, because SQL Server allows query hints on INSERT only in a nested SELECT. Preserve exact spacing when MERGE combines OUTPUT and OPTION. Signed-off-by: 付典 --- .../sf/jsqlparser/statement/merge/Merge.java | 22 +++++++ .../statement/select/OptionClause.java | 5 +- .../util/deparser/MergeDeParser.java | 7 +++ .../validation/validator/MergeValidator.java | 9 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 26 +++++++- .../statement/select/OptionClauseTest.java | 61 +++++++++++++++++++ 6 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java index 7a3baf7a47..948a7d648c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java @@ -17,6 +17,7 @@ import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; import net.sf.jsqlparser.statement.select.FromItem; +import net.sf.jsqlparser.statement.select.OptionClause; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.WithItem; @@ -41,6 +42,7 @@ public class Merge implements Statement { private List operations; private OutputClause outputClause; + private OptionClause option; private void deriveOperationsFromStandardClauses() { List operations = new ArrayList<>(); @@ -214,6 +216,19 @@ public Merge setOutputClause(OutputClause outputClause) { return this; } + public OptionClause getOption() { + return option; + } + + public Merge setOption(OptionClause option) { + this.option = option; + return this; + } + + public Merge withOption(OptionClause option) { + return setOption(option); + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public String toString() { @@ -248,6 +263,13 @@ public String toString() { b.append(outputClause); } + if (option != null) { + if (outputClause != null) { + b.setLength(b.length() - 1); + } + b.append(option); + } + return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java index cb941819ce..185166798d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/OptionClause.java @@ -15,8 +15,9 @@ /** * Models the SQL Server (T-SQL) {@code OPTION (...)} query hint clause, which attaches a list of - * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE} or {@code DELETE} statement, - * see Hints + * {@link OptionHint}s to the end of a {@code SELECT}, {@code UPDATE}, {@code DELETE} or + * {@code MERGE} statement, see + * Hints * (Transact-SQL) - Query Hints. */ public class OptionClause implements Serializable { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java index 003527770f..249f9c134b 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java @@ -63,6 +63,13 @@ public void deParse(Merge merge) { if (merge.getOutputClause() != null) { merge.getOutputClause().appendTo(builder); } + + if (merge.getOption() != null) { + if (merge.getOutputClause() != null) { + builder.setLength(builder.length() - 1); + } + builder.append(merge.getOption()); + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java index e82ebc91dc..79d84c6c64 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.merge.*; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.update.UpdateSet; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -31,6 +32,14 @@ public void validate(Merge merge) { merge.getOperations().forEach(operation -> operation.accept(this, null)); } validateOptionalFromItems(merge.getFromItem()); + if (merge.getOption() != null) { + for (OptionHint optionHint : merge.getOption().getOptionHints()) { + validateOptionalExpression(optionHint.getValue()); + if (optionHint.getParameters() != null) { + optionHint.getParameters().forEach(this::validateOptionalExpression); + } + } + } } @Override diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index da45e58870..f6c400a458 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4388,6 +4388,7 @@ Statement Merge( List> with ) : { Expression condition; List operations; OutputClause outputClause; + OptionClause optionClause = null; } { { merge.setOracleHint(getOracleHint()); } table=TableWithAlias() { merge.setTable(table); } @@ -4398,6 +4399,8 @@ Statement Merge( List> with ) : { [ outputClause = OutputClause() { merge.setOutputClause(outputClause); } ] + [ LOOKAHEAD(2) optionClause = OptionClause() { merge.setOption(optionClause); } ] + { return merge.withWithItemsList(with); } } @@ -4803,7 +4806,7 @@ Select Select() #Select: [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] - [ LOOKAHEAD(2) optionClause = OptionClause() { select.setOption(optionClause); } ] + [ LOOKAHEAD(2, { !(select instanceof Values) }) optionClause = OptionClause() { select.setOption(optionClause); } ] [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -6952,6 +6955,23 @@ InterpolateElement InterpolateElement(): { return interpolateElement; } } +Expression OptionHintParameter(): +{ + UserVariable userVariable = null; + Expression parameter = null; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && getToken(2).kind == K_UNKNOWN }) + userVariable = UserVariable() + { + return new Column(userVariable + " UNKNOWN"); + } + | + parameter = Expression() { return parameter; } + ) +} + OptionClause OptionClause(): { OptionClause optionClause = new OptionClause(); @@ -6977,8 +6997,8 @@ OptionHint OptionHint(): ( LOOKAHEAD("=") "=" value = Expression() { optionHint.setValue(value); optionHint.setUseEquals(true); } | - LOOKAHEAD("(") "(" parameter = Expression() { optionHint.addParameter(parameter); } - ( "," parameter = Expression() { optionHint.addParameter(parameter); } )* + LOOKAHEAD("(") "(" parameter = OptionHintParameter() { optionHint.addParameter(parameter); } + ( "," parameter = OptionHintParameter() { optionHint.addParameter(parameter); } )* ")" | LOOKAHEAD({ isOptionHintValueAhead() }) value = Expression() { optionHint.setValue(value); } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java index bfeba4be24..113d9235f2 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/OptionClauseTest.java @@ -13,7 +13,10 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -147,6 +150,64 @@ public void testOptionInUpdateAndDelete() throws JSQLParserException { Assertions.assertNotNull(((net.sf.jsqlparser.statement.update.Update) update).getOption()); } + @Test + public void testOptionOptimizeForUnknownParameter() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(1, optimizeFor.getParameters().size()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(0)); + Assertions.assertEquals("@p UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionOptimizeForMixedParameters() throws JSQLParserException { + String sql = "SELECT * FROM t WHERE c = @p OPTION (OPTIMIZE FOR (@p = 1, @q UNKNOWN))"; + PlainSelect plainSelect = + (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OptionHint optimizeFor = plainSelect.getOption().getOptionHints().get(0); + Assertions.assertEquals(2, optimizeFor.getParameters().size()); + Assertions.assertEquals("@p = 1", optimizeFor.getParameters().get(0).toString()); + Column parameter = + Assertions.assertInstanceOf(Column.class, optimizeFor.getParameters().get(1)); + Assertions.assertEquals("@q UNKNOWN", parameter.getColumnName()); + } + + @Test + public void testOptionAfterInsertValuesIsRejected() { + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse( + "INSERT INTO t (a) VALUES (1) OPTION (RECOMPILE)")); + } + + @Test + public void testOptionAfterInsertSelect() throws JSQLParserException { + String sql = "INSERT INTO t (a) SELECT a FROM s OPTION (RECOMPILE)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Insert insert = (Insert) statement; + Assertions.assertNotNull(insert.getSelect().getOption()); + } + + @Test + public void testOptionAfterMergeStatement() throws JSQLParserException { + String sql = + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a OPTION (HASH JOIN)"; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + Merge merge = (Merge) statement; + Assertions.assertNotNull(merge.getOption()); + Assertions.assertEquals("HASH JOIN", merge.getOption().getOptionHints().get(0).getName()); + } + + @Test + public void testOptionAfterMergeOutputClause() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a " + + "OUTPUT deleted.a OPTION (HASH JOIN)", + false); + } + @Test public void testOptionAsIdentifierStillWorks() throws JSQLParserException { // OPTION stays a non-reserved keyword usable as column and table name From f5ecba7dc51fab7cb3c86cb02e5374f89db4e89f Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sat, 22 Aug 2026 09:00:06 +0800 Subject: [PATCH 017/387] fix: support Oracle inline constraint states in CREATE TABLE (#2491) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 21 ++++++++++++- .../statement/create/CreateTableTest.java | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f6c400a458..0c73b40357 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -10913,6 +10913,25 @@ CreateIndex CreateIndex(): } } +List ColumnDefinitionParameter(): { + Token tk = null; + List parameter = new ArrayList(); +} { + ( + parameter=CreateParameter() + | + ( + tk= + | tk= + | tk= + | tk= + | tk= + ) + { parameter.add(tk.image); } + ) + { return parameter; } +} + ColumnDefinition ColumnDefinition(): { ColumnDefinition coldef; String columnName; @@ -10922,7 +10941,7 @@ ColumnDefinition ColumnDefinition(): { } { columnName=RelObjectName() colDataType=ColDataType() - ( LOOKAHEAD(2) parameter=CreateParameter() { columnSpecs.addAll(parameter); } )* + ( LOOKAHEAD(2) parameter=ColumnDefinitionParameter() { columnSpecs.addAll(parameter); } )* { coldef = new ColumnDefinition(); coldef.setColumnName(columnName); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index aa7574c0aa..a58caea808 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -1191,6 +1191,36 @@ void testCreateTableColumnVisibility() throws JSQLParserException { createTable.getColumnDefinitions().get(1).getColumnSpecs()); } + @Test + void testOracleInlineConstraintState() throws JSQLParserException { + String sqlStr = + "CREATE TABLE DIAM.IAM_DENEME1 (" + + "ID VARCHAR2(36 BYTE) NOT NULL ENABLE," + + "REQUEST_ID VARCHAR2(50 BYTE)," + + "CREATE_DATE TIMESTAMP(9)," + + "USER_ID VARCHAR2(50 BYTE)," + + "CODE VARCHAR2(100 BYTE) NOT NULL ENABLE," + + "IS_OUTSOURCE_ENABLED CHAR(1 BYTE) DEFAULT 1 NOT NULL ENABLE," + + "IS_PASSWORD_NUMERIC CHAR(1 BYTE) DEFAULT 0 NOT NULL ENABLE" + + ")"; + + CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlStr); + + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE test (" + + "a NUMBER NOT NULL ENABLE," + + "b NUMBER NOT NULL DISABLE," + + "c NUMBER NOT NULL ENABLE NOVALIDATE," + + "d NUMBER CONSTRAINT d_nn NOT NULL DEFERRABLE VALIDATE" + + ")", + true); + + assertEquals(Arrays.asList("NOT", "NULL", "ENABLE"), + createTable.getColumnDefinitions().get(0).getColumnSpecs()); + assertEquals(Arrays.asList("DEFAULT", "1", "NOT", "NULL", "ENABLE"), + createTable.getColumnDefinitions().get(5).getColumnSpecs()); + } + @Test void testCreateTableIndexVisibilityWithOtherIndexOptions() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( From edd5bfc1a513f01f63c420b8a917cf3fc3b64564 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Sat, 22 Aug 2026 08:11:56 +0530 Subject: [PATCH 018/387] avoid quadratic backtracking in Server name pattern (#2489) --- .../java/net/sf/jsqlparser/schema/Server.java | 2 +- .../net/sf/jsqlparser/schema/ServerTest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/schema/Server.java b/src/main/java/net/sf/jsqlparser/schema/Server.java index 9ac9bd2d2b..7e4eaa51ca 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Server.java +++ b/src/main/java/net/sf/jsqlparser/schema/Server.java @@ -14,7 +14,7 @@ public final class Server implements MultiPartName { public static final Pattern SERVER_PATTERN = - Pattern.compile("\\[([^\\]]+?)(?:\\\\([^\\]]+))?\\]"); + Pattern.compile("\\[([^\\]\\\\]+)(?:\\\\([^\\]]+))?\\]"); private String serverName; diff --git a/src/test/java/net/sf/jsqlparser/schema/ServerTest.java b/src/test/java/net/sf/jsqlparser/schema/ServerTest.java index 216248be90..ed24ea52d4 100644 --- a/src/test/java/net/sf/jsqlparser/schema/ServerTest.java +++ b/src/test/java/net/sf/jsqlparser/schema/ServerTest.java @@ -10,10 +10,27 @@ package net.sf.jsqlparser.schema; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; import org.junit.jupiter.api.Test; public class ServerTest { + @Test + public void testCraftedServerNameDoesNotBacktrack() throws Exception { + final StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < 40000; i++) { + sb.append("a\\"); + } + final String crafted = sb.toString(); + + // an unterminated bracketed name full of backslashes used to backtrack quadratically + final Server server = assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> new Server(crafted)); + assertEquals(crafted, server.getFullyQualifiedName()); + } + @Test public void testServerNameParsing() throws Exception { final String serverName = "LOCALHOST"; From 98bd01de38de073d8efae0ecfc4fc22ea3fd5632 Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sat, 22 Aug 2026 10:45:06 +0800 Subject: [PATCH 019/387] feat(parser): support MySQL PROCEDURE ANALYSE clause (#2492) - add ANALYSE as a non-reserved keyword - parse zero, one, or two integer parameters - prevent PROCEDURE ANALYSE from being consumed as an alias - add AST, deparser, and visitor support - add parsing and regression tests --- .../select/MySqlProcedureAnalyse.java | 75 +++++++++++++++++++ .../jsqlparser/statement/select/Select.java | 17 +++++ .../select/SelectVisitorAdapter.java | 6 ++ .../util/deparser/SelectDeParser.java | 3 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 33 +++++++- .../select/MySqlProcedureAnalyseTest.java | 72 ++++++++++++++++++ 6 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java new file mode 100644 index 0000000000..b70f2fdc4b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java @@ -0,0 +1,75 @@ +/*- + * #%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.statement.select; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * Models the legacy MySQL {@code PROCEDURE ANALYSE()} clause. + * + * @see MySQL 5.7 Reference + * Manual + */ +public class MySqlProcedureAnalyse extends ASTNodeAccessImpl implements Serializable { + + private LongValue maxElements; + private LongValue maxMemory; + + public MySqlProcedureAnalyse() {} + + public MySqlProcedureAnalyse(LongValue maxElements, LongValue maxMemory) { + this.maxElements = maxElements; + this.maxMemory = maxMemory; + } + + public LongValue getMaxElements() { + return maxElements; + } + + public void setMaxElements(LongValue maxElements) { + this.maxElements = maxElements; + } + + public MySqlProcedureAnalyse withMaxElements(LongValue maxElements) { + setMaxElements(maxElements); + return this; + } + + public LongValue getMaxMemory() { + return maxMemory; + } + + public void setMaxMemory(LongValue maxMemory) { + this.maxMemory = maxMemory; + } + + public MySqlProcedureAnalyse withMaxMemory(LongValue maxMemory) { + setMaxMemory(maxMemory); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(" PROCEDURE ANALYSE("); + if (maxElements != null) { + builder.append(maxElements); + if (maxMemory != null) { + builder.append(", ").append(maxMemory); + } + } + return builder.append(")"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 501e21728c..d3750c98ef 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -31,6 +31,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp Offset offset; Fetch fetch; WithIsolation isolation; + MySqlProcedureAnalyse mySqlProcedureAnalyse; boolean oracleSiblings = false; ForClause forClause = null; @@ -322,6 +323,19 @@ public Select withIsolation(WithIsolation isolation) { return this; } + public MySqlProcedureAnalyse getMySqlProcedureAnalyse() { + return mySqlProcedureAnalyse; + } + + public void setMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + this.mySqlProcedureAnalyse = mySqlProcedureAnalyse; + } + + public Select withMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + setMySqlProcedureAnalyse(mySqlProcedureAnalyse); + return this; + } + public ForMode getForMode() { return this.forMode; } @@ -521,6 +535,9 @@ public StringBuilder appendTo(StringBuilder builder) { if (offset != null) { builder.append(offset); } + if (mySqlProcedureAnalyse != null) { + mySqlProcedureAnalyse.appendTo(builder); + } if (fetch != null) { builder.append(fetch); } 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 9b0eb33a65..962e73719f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -200,6 +200,12 @@ public T visit(PlainSelect plainSelect, S context) { if (plainSelect.getOffset() != null) { expressionVisitor.visitExpression(plainSelect.getOffset().getOffset(), context); } + if (plainSelect.getMySqlProcedureAnalyse() != null) { + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxElements(), context); + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxMemory(), context); + } if (plainSelect.getFetch() != null) { expressionVisitor.visitExpression(plainSelect.getFetch().getExpression(), context); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 066d40d069..24fea616a5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -365,6 +365,9 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { if (plainSelect.getOffset() != null) { visit(plainSelect.getOffset()); } + if (plainSelect.getMySqlProcedureAnalyse() != null) { + plainSelect.getMySqlProcedureAnalyse().appendTo(builder); + } if (plainSelect.getFetch() != null) { visit(plainSelect.getFetch()); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0c73b40357..974c93baf6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -819,11 +819,12 @@ public class CCJSqlParser extends AbstractJSqlParser { switch (kind) { // Safe as implicit identifiers (no clause collision) - case K_TABLES: case K_OPTIMIZE: case K_PROCEDURE: case K_PUBLIC: + case K_TABLES: case K_OPTIMIZE: case K_PUBLIC: case K_CASEWHEN: case K_IIF: return true; // Safe when the follower doesn't form a clause + case K_PROCEDURE: return nextKind != K_ANALYSE; case K_GROUP: return nextKind != K_BY; case K_ORDER: return nextKind != K_BY && nextKind != K_SIBLINGS; case K_CONNECT: return nextKind != K_BY; @@ -1250,6 +1251,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -5451,6 +5453,7 @@ PlainSelect PlainSelect() #PlainSelect: boolean partitionByBrackets = false; List intoTables = null; MySqlSelectIntoClause mySqlSelectIntoClause = null; + MySqlProcedureAnalyse mySqlProcedureAnalyse = null; Table updateTable = null; List
updateTables = new ArrayList
(); Wait wait = null; @@ -5580,6 +5583,10 @@ PlainSelect PlainSelect() #PlainSelect: } ] [ LOOKAHEAD() offset = Offset() { plainSelect.setOffset(offset); } ] [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] + [ LOOKAHEAD( ) + mySqlProcedureAnalyse = MySqlProcedureAnalyse() + { plainSelect.setMySqlProcedureAnalyse(mySqlProcedureAnalyse); } + ] [ LOOKAHEAD() fetch = Fetch() { plainSelect.setFetch(fetch); } ] [ LOOKAHEAD( ) withIsolation = WithIsolation() { plainSelect.setIsolation(withIsolation); } ] [ LOOKAHEAD(2) @@ -6226,6 +6233,30 @@ MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position posit } } +MySqlProcedureAnalyse MySqlProcedureAnalyse(): +{ + MySqlProcedureAnalyse procedureAnalyse = new MySqlProcedureAnalyse(); + Token maxElements = null; + Token maxMemory = null; +} +{ + "(" + [ + maxElements= + [ "," maxMemory= ] + ] + ")" + { + if (maxElements != null) { + procedureAnalyse.setMaxElements(new LongValue(maxElements.image)); + } + if (maxMemory != null) { + procedureAnalyse.setMaxMemory(new LongValue(maxMemory.image)); + } + return procedureAnalyse; + } +} + void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause): { Token token; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java new file mode 100644 index 0000000000..7d90bdeb9e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java @@ -0,0 +1,72 @@ +/*- + * #%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.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; + +class MySqlProcedureAnalyseTest { + + @Test + void parseWithoutParameters() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1, col2 FROM heavy_table PROCEDURE ANALYSE()"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertNotNull(procedureAnalyse); + assertNull(procedureAnalyse.getMaxElements()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseOneParameter() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table PROCEDURE ANALYSE(10)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseTwoParametersAfterLimit() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table LIMIT 20 PROCEDURE ANALYSE(10, 256)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertEquals("256", procedureAnalyse.getMaxMemory().getStringValue()); + } + + @Test + void keepAnalyseAndProcedureAvailableAsIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT analyse FROM heavy_table"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM heavy_table procedure"); + } + + @Test + void rejectInvalidForms() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(-1)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(1, 2, 3)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYZE()")); + } +} From 158b5800b4fd370ed697334de7d51b1d08535174 Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sat, 22 Aug 2026 13:54:22 +0800 Subject: [PATCH 020/387] fix: support character set introduced string literals (#2493) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 ++++++++++++++++ .../statement/select/SelectTest.java | 33 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 974c93baf6..5c431829ca 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -301,6 +301,19 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Whether the next two tokens form a character-set-introduced string literal, + * for example {@code _utf8mb4'text'} or {@code _ISO8859_1 'text'}. + */ + private boolean isCharacterSetIntroducedStringAhead() { + Token introducer = getToken(1); + Token literal = getToken(2); + return introducer.kind == S_IDENTIFIER + && introducer.image != null + && introducer.image.length() > 1 + && introducer.image.charAt(0) == '_' + && literal.kind == S_CHAR_LITERAL; + } /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. @@ -8067,6 +8080,18 @@ Expression SimpleExpression(): } } +StringValue CharacterSetIntroducedString(): +{ + Token introducer; + Token literal; +} +{ + introducer= literal= + { + return new StringValue(literal.image).withPrefix(introducer.image); + } +} + /** Multiplicative/bitwise-shift — higher precedence */ Expression MulChain(): { @@ -8243,6 +8268,9 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) { retval = new AllValue(); } + | LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() }) + retval=CharacterSetIntroducedString() + | LOOKAHEAD(2, {!interrupted}) retval=Column() [ LOOKAHEAD( "(" "+" ")" ("," | ")") ) diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index a0f7489377..a385e17bb2 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -36,6 +36,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.CollateExpression; import net.sf.jsqlparser.expression.DoubleValue; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; @@ -4183,6 +4184,38 @@ public void testEscaped() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT _utf8'testvalue'"); } + @ParameterizedTest + @ValueSource(strings = {"_utf8mb4'testvalue'", "_utf8mb4 'testvalue'", + "_utf8 'testvalue'", "_latin1'testvalue'", "_binary'testvalue'", + "_ISO8859_1 'testvalue'"}) + public void testCharacterSetIntroducedString(String expression) throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT " + expression); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + StringValue stringValue = assertInstanceOf(StringValue.class, selectItem.getExpression()); + + assertEquals(expression.substring(0, expression.indexOf('\'')).trim(), + stringValue.getPrefix()); + assertEquals("testvalue", stringValue.getValue()); + assertNull(selectItem.getAlias()); + } + + @Test + public void testCharacterSetIntroducedStringWithCollate() throws JSQLParserException { + String sql = "SELECT _utf8mb4'some text' COLLATE utf8mb4_unicode_ci AS custom_string"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + CollateExpression collateExpression = + assertInstanceOf(CollateExpression.class, selectItem.getExpression()); + StringValue stringValue = assertInstanceOf(StringValue.class, + collateExpression.getLeftExpression()); + + assertEquals("_utf8mb4", stringValue.getPrefix()); + assertEquals("some text", stringValue.getValue()); + assertEquals("utf8mb4_unicode_ci", collateExpression.getCollate()); + assertEquals("custom_string", selectItem.getAlias().getName()); + assertEquals(sql, select.toString()); + } + @Test public void testIssue563MultiSubJoin() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( From 44c0fe50747fbe6ef2b6a7825eb1fa09343ed434 Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sat, 22 Aug 2026 13:55:15 +0800 Subject: [PATCH 021/387] fix(parser): support IF EXISTS in ALTER TABLE MODIFY COLUMN (#2494) - accept both IF EXISTS and IF NOT EXISTS with optional COLUMN - preserve existence clauses when deparsing - add round-trip and AST regression tests --- .../statement/alter/AlterExpression.java | 5 +++-- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 9 ++++++++- .../jsqlparser/statement/alter/AlterTest.java | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 43615abdb6..dd4e07f52a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -1118,8 +1118,9 @@ protected void toStringGeneral(StringBuilder b) { } else if (hasColumns) { b.append("COLUMNS "); } - if (useIfNotExists - && operation == AlterOperation.ADD) { + if (usingIfExists) { + b.append("IF EXISTS "); + } else if (useIfNotExists) { b.append("IF NOT EXISTS "); } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5c431829ca..1ed5192ebe 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -12661,7 +12661,14 @@ AlterExpression AlterExpressionAddAlterModify(): { alterExp.hasColumns(true); } ) )? - [ LOOKAHEAD(2) { alterExp.setUseIfNotExists(true); } ] + [ + LOOKAHEAD(2) + ( + { alterExp.setUseIfNotExists(true); } + | + { alterExp.setUsingIfExists(true); } + ) + ] ( LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) | diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 5ba1a87625..35481fb7e6 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -1109,6 +1109,25 @@ public void testIssue1875() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(stmt); } + @ParameterizedTest + @MethodSource("provideModifyColumnExistenceClauses") + public void testModifyColumnExistenceClauses(String sql, boolean usingIfExists, + boolean useIfNotExists) throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(sql); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertEquals(usingIfExists, expression.isUsingIfExists()); + assertEquals(useIfNotExists, expression.isUseIfNotExists()); + } + + private static Stream provideModifyColumnExistenceClauses() { + return Stream.of( + Arguments.of("ALTER TABLE t MODIFY COLUMN IF EXISTS c INT", true, false), + Arguments.of("ALTER TABLE t MODIFY IF EXISTS c INT", true, false), + Arguments.of("ALTER TABLE t MODIFY COLUMN IF NOT EXISTS c INT", false, true), + Arguments.of("ALTER TABLE t MODIFY IF NOT EXISTS c INT", false, true)); + } + @Test public void testIssue2027() throws JSQLParserException { String sql = "ALTER TABLE `foo_bar` ADD COLUMN `baz` text"; From e13019a393d9229a416f4b137ffe17cc6cf54925 Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sat, 22 Aug 2026 20:43:18 +0800 Subject: [PATCH 022/387] feat(parser): support DuckDB MAP literals and PIVOT queries (#2496) * feat(parser): support DuckDB MAP literals and PIVOT queries * refactor: reduce PivotQuery visitor complexity --- .../expression/ExpressionVisitor.java | 8 + .../expression/ExpressionVisitorAdapter.java | 11 ++ .../jsqlparser/expression/MapExpression.java | 79 +++++++++ .../statement/select/FromItemVisitor.java | 13 +- .../select/FromItemVisitorAdapter.java | 10 +- .../statement/select/PivotQuery.java | 160 ++++++++++++++++++ .../statement/select/SelectVisitor.java | 11 +- .../select/SelectVisitorAdapter.java | 27 +++ .../sf/jsqlparser/util/TablesNamesFinder.java | 48 +++++- .../util/deparser/ExpressionDeParser.java | 21 ++- .../util/deparser/SelectDeParser.java | 81 +++++++++ .../validator/ExpressionValidator.java | 19 ++- .../validation/validator/SelectValidator.java | 39 ++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 88 +++++++++- .../select/DuckDBCompatibilityTest.java | 70 ++++++++ .../statement/select/PivotQueryTest.java | 94 ++++++++++ 16 files changed, 759 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/MapExpression.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/DuckDBCompatibilityTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/PivotQueryTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index d07d9179aa..2eb909a989 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -621,6 +621,14 @@ default void visit(ArrayConstructor arrayConstructor) { this.visit(arrayConstructor, null); } + default T visit(MapExpression mapExpression, S context) { + return null; + } + + default void visit(MapExpression mapExpression) { + this.visit(mapExpression, null); + } + T visit(VariableAssignment variableAssignment, S context); default void visit(VariableAssignment variableAssignment) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 4727e93e85..274d4e00a3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Map; import java.util.Optional; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; @@ -693,6 +694,16 @@ public T visit(ArrayConstructor arrayConstructor, S context) { return visitExpressions(arrayConstructor, context, arrayConstructor.getExpressions()); } + @Override + public T visit(MapExpression mapExpression, S context) { + ArrayList subExpressions = new ArrayList<>(); + for (Map.Entry entry : mapExpression.getEntries()) { + subExpressions.add(entry.getKey()); + subExpressions.add(entry.getValue()); + } + return visitExpressions(mapExpression, context, subExpressions); + } + @Override public T visit(VariableAssignment variableAssignment, S context) { return visitExpressions(variableAssignment, context, variableAssignment.getVariable(), diff --git a/src/main/java/net/sf/jsqlparser/expression/MapExpression.java b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java new file mode 100644 index 0000000000..1e64a7db84 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java @@ -0,0 +1,79 @@ +/*- + * #%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 java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * DuckDB's {@code MAP {key: value, ...}} literal expression. + * + * @see DuckDB MAP type + */ +public class MapExpression extends ASTNodeAccessImpl implements Expression { + + private List> entries = new ArrayList<>(); + + public MapExpression() {} + + public MapExpression(Collection> entries) { + this.entries.addAll(entries); + } + + public List> getEntries() { + return entries; + } + + public void setEntries(List> entries) { + this.entries = entries; + } + + public MapExpression withEntries(List> entries) { + setEntries(entries); + return this; + } + + public MapExpression addEntries( + Collection> entries) { + this.entries.addAll(entries); + return this; + } + + public MapExpression addEntry(Expression key, Expression value) { + entries.add(new AbstractMap.SimpleEntry<>(key, value)); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("MAP {"); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = entries.get(i); + builder.append(entry.getKey()).append(": ").append(entry.getValue()); + } + return builder.append("}"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java index 6b10480319..69af24f551 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -9,13 +9,12 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.Collection; +import java.util.List; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.Collection; -import java.util.List; - public interface FromItemVisitor { default T visitFromItem(FromItem fromItem, S context) { @@ -85,6 +84,14 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + T visit(SetOperationList setOperationList, S context); default void visit(SetOperationList setOperationList) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 23bd480b82..3c88d63c41 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.ArrayList; +import java.util.Collection; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; @@ -17,9 +19,6 @@ import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.ArrayList; -import java.util.Collection; - @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class FromItemVisitorAdapter implements FromItemVisitor { private SelectVisitor selectVisitor; @@ -125,6 +124,11 @@ public T visit(PlainSelect plainSelect, S context) { return plainSelect.accept(selectVisitor, context); } + @Override + public T visit(PivotQuery pivotQuery, S context) { + return pivotQuery.accept(selectVisitor, context); + } + @Override public T visit(SetOperationList setOperationList, S context) { ArrayList results = new ArrayList<>(); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java new file mode 100644 index 0000000000..24f298907b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/PivotQuery.java @@ -0,0 +1,160 @@ +/*- + * #%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.statement.select; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * DuckDB's simplified {@code PIVOT} query. + * + *

+ * Unlike {@link Pivot}, which is a clause attached to a {@link FromItem}, this construct is a query + * in its own right and can therefore be used as a statement, CTE, or parenthesized subquery. + * + * @see DuckDB PIVOT + */ +public class PivotQuery extends Select { + + private FromItem fromItem; + private ExpressionList onExpressions; + private List> usingItems; + private ExpressionList groupByExpressions; + + public FromItem getFromItem() { + return fromItem; + } + + public void setFromItem(FromItem fromItem) { + this.fromItem = fromItem; + } + + public PivotQuery withFromItem(FromItem fromItem) { + setFromItem(fromItem); + return this; + } + + public ExpressionList getOnExpressions() { + return onExpressions; + } + + public void setOnExpressions(ExpressionList onExpressions) { + this.onExpressions = onExpressions; + } + + public PivotQuery withOnExpressions(ExpressionList onExpressions) { + setOnExpressions(onExpressions); + return this; + } + + public PivotQuery addOnExpressions(Expression... expressions) { + return addOnExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addOnExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getOnExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withOnExpressions(collection); + } + + public List> getUsingItems() { + return usingItems; + } + + public void setUsingItems(List> usingItems) { + this.usingItems = usingItems; + } + + public PivotQuery withUsingItems(List> usingItems) { + setUsingItems(usingItems); + return this; + } + + public PivotQuery addUsingItems(SelectItem... usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + Collections.addAll(collection, usingItems); + return withUsingItems(collection); + } + + public PivotQuery addUsingItems(Collection> usingItems) { + List> collection = + Optional.ofNullable(getUsingItems()).orElseGet(ArrayList::new); + collection.addAll(usingItems); + return withUsingItems(collection); + } + + public ExpressionList getGroupByExpressions() { + return groupByExpressions; + } + + public void setGroupByExpressions(ExpressionList groupByExpressions) { + this.groupByExpressions = groupByExpressions; + } + + public PivotQuery withGroupByExpressions(ExpressionList groupByExpressions) { + setGroupByExpressions(groupByExpressions); + return this; + } + + public PivotQuery addGroupByExpressions(Expression... expressions) { + return addGroupByExpressions(Arrays.asList(expressions)); + } + + public PivotQuery addGroupByExpressions(Collection expressions) { + ExpressionList collection = + Optional.ofNullable(getGroupByExpressions()).orElseGet(ExpressionList::new); + collection.addAll(expressions); + return withGroupByExpressions(collection); + } + + @Override + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + builder.append("PIVOT ").append(fromItem); + if (onExpressions != null) { + builder.append(" ON ").append(onExpressions); + } + if (usingItems != null) { + builder.append(" USING ").append(Select.getStringList(usingItems)); + } + if (groupByExpressions != null) { + builder.append(" GROUP BY ").append(groupByExpressions); + } + return builder; + } + + @Override + public T accept(SelectVisitor selectVisitor, S context) { + return selectVisitor.visit(this, context); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } + + @Override + public SampleClause getSampleClause() { + return null; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java index db5ce31752..f5b88bd357 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java @@ -9,11 +9,10 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.piped.FromQuery; -import java.util.List; - public interface SelectVisitor { default T visitWithItems(List> withItemsList, S context) { if (withItemsList != null) { @@ -40,6 +39,14 @@ default void visit(PlainSelect plainSelect) { this.visit(plainSelect, null); } + default T visit(PivotQuery pivotQuery, S context) { + return null; + } + + default void visit(PivotQuery pivotQuery) { + this.visit(pivotQuery, null); + } + T visit(FromQuery fromQuery, S context); T visit(SetOperationList setOpList, S context); 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 962e73719f..7180dfab58 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.piped.FromQuery; @@ -217,6 +218,32 @@ public T visit(PlainSelect plainSelect, S context) { return null; } + @Override + public T visit(PivotQuery pivotQuery, S context) { + visitWithItems(pivotQuery.getWithItemsList(), context); + + fromItemVisitor.visitFromItem(pivotQuery.getFromItem(), context); + expressionVisitor.visitExpressions(pivotQuery.getOnExpressions(), context); + + if (pivotQuery.getUsingItems() != null) { + for (SelectItem item : pivotQuery.getUsingItems()) { + item.accept(selectItemVisitor, context); + } + } + + expressionVisitor.visitExpressions(pivotQuery.getGroupByExpressions(), context); + expressionVisitor.visitOrderBy(pivotQuery.getOrderByElements(), context); + expressionVisitor.visitLimit(pivotQuery.getLimit(), context); + + if (pivotQuery.getOffset() != null) { + expressionVisitor.visitExpression(pivotQuery.getOffset().getOffset(), context); + } + if (pivotQuery.getFetch() != null) { + expressionVisitor.visitExpression(pivotQuery.getFetch().getExpression(), context); + } + return null; + } + @Override public T visit(FromQuery fromQuery, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 3e15be9a77..471a971a42 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -73,6 +73,7 @@ import net.sf.jsqlparser.statement.DescribeStatement; import net.sf.jsqlparser.statement.ExplainStatement; import net.sf.jsqlparser.statement.IfElseStatement; +import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.PurgeObjectType; import net.sf.jsqlparser.statement.PurgeStatement; import net.sf.jsqlparser.statement.ResetStatement; @@ -93,7 +94,6 @@ import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; -import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; import net.sf.jsqlparser.statement.create.database.CreateDatabase; @@ -156,6 +156,7 @@ import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; @@ -372,11 +373,47 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(PivotQuery pivotQuery, S context) { + visitWithItems(pivotQuery.getWithItemsList(), context); + visitFromItem(pivotQuery.getFromItem(), context); + visitExpressions(pivotQuery.getOnExpressions(), context); + visitSelectItems(pivotQuery.getUsingItems(), context); + visitExpressions(pivotQuery.getGroupByExpressions(), context); + visitOrderBy(pivotQuery.getOrderByElements(), context); + visitLimit(pivotQuery.getLimit(), context); + + visitPivotPagination(pivotQuery, context); + return null; + } + + private void visitSelectItems(List> selectItems, S context) { + if (selectItems != null) { + for (SelectItem item : selectItems) { + item.accept(this, context); + } + } + } + + private void visitPivotPagination(PivotQuery pivotQuery, S context) { + if (pivotQuery.getOffset() != null) { + pivotQuery.getOffset().getOffset().accept(this, context); + } + if (pivotQuery.getFetch() != null && pivotQuery.getFetch().getExpression() != null) { + pivotQuery.getFetch().getExpression().accept(this, context); + } + } + @Override public void visit(PlainSelect plainSelect) { SelectVisitor.super.visit(plainSelect); } + @Override + public void visit(PivotQuery pivotQuery) { + SelectVisitor.super.visit(pivotQuery); + } + /** * Override to adapt the tableName generation (e.g. with / without schema). * @@ -1848,6 +1885,15 @@ public Void visit(ArrayConstructor array, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { throwUnsupported(createSequence); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 2767f5e23e..8328dc7997 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -45,12 +45,12 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -62,12 +62,12 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; -import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -79,6 +79,7 @@ import net.sf.jsqlparser.expression.WhenClause; import net.sf.jsqlparser.expression.WindowElement; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -1620,6 +1621,22 @@ public StringBuilder visit(ArrayConstructor arrayConstructor, S context) { return builder; } + @Override + public StringBuilder visit(MapExpression mapExpression, S context) { + builder.append("MAP {"); + for (int i = 0; i < mapExpression.getEntries().size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = mapExpression.getEntries().get(i); + entry.getKey().accept(this, context); + builder.append(": "); + entry.getValue().accept(this, context); + } + builder.append("}"); + return builder; + } + @Override void deParse(Expression statement) { statement.accept(this, null); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 24fea616a5..b506b0b27c 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -62,6 +62,7 @@ import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -429,6 +430,81 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { return builder; } + @Override + public StringBuilder visit(PivotQuery pivotQuery, S context) { + List> withItemsList = pivotQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + builder.append("WITH "); + for (Iterator> iter = withItemsList.iterator(); iter.hasNext();) { + iter.next().accept((SelectVisitor) this, context); + if (iter.hasNext()) { + builder.append(","); + } + builder.append(" "); + } + } + + builder.append("PIVOT "); + pivotQuery.getFromItem().accept(this, context); + + if (pivotQuery.getOnExpressions() != null) { + builder.append(" ON "); + pivotQuery.getOnExpressions().accept(expressionVisitor, context); + } + if (pivotQuery.getUsingItems() != null) { + builder.append(" USING "); + for (Iterator> iter = + pivotQuery.getUsingItems().iterator(); iter.hasNext();) { + iter.next().accept(this, context); + if (iter.hasNext()) { + builder.append(", "); + } + } + } + if (pivotQuery.getGroupByExpressions() != null) { + builder.append(" GROUP BY "); + pivotQuery.getGroupByExpressions().accept(expressionVisitor, context); + } + + Alias alias = pivotQuery.getAlias(); + if (alias != null) { + builder.append(alias); + } + Pivot pivot = pivotQuery.getPivot(); + if (pivot != null) { + pivot.accept(this, context); + } + UnPivot unpivot = pivotQuery.getUnPivot(); + if (unpivot != null) { + unpivot.accept(this, context); + } + + if (pivotQuery.getOrderByElements() != null) { + new OrderByDeParser(expressionVisitor, builder).deParse(pivotQuery.isOracleSiblings(), + pivotQuery.getOrderByElements()); + deParseInterpolate(pivotQuery.getInterpolate()); + } + if (pivotQuery.getOption() != null) { + builder.append(pivotQuery.getOption()); + } + if (pivotQuery.getLimitBy() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimitBy()); + } + if (pivotQuery.getLimit() != null) { + new LimitDeparser(expressionVisitor, builder).deParse(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + visit(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + visit(pivotQuery.getFetch()); + } + if (pivotQuery.getIsolation() != null) { + builder.append(pivotQuery.getIsolation()); + } + return builder; + } + protected void deparseWhereClause(PlainSelect plainSelect) { if (plainSelect.getWhere() != null) { builder.append(" WHERE "); @@ -911,6 +987,11 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + @Override + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 128fc7d92d..e44fee2413 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.validation.validator; +import java.util.Map; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.AnalyticExpression; import net.sf.jsqlparser.expression.AnyComparisonExpression; @@ -39,12 +40,12 @@ import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.JsonTableFunction; -import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; +import net.sf.jsqlparser.expression.MapExpression; import net.sf.jsqlparser.expression.MySQLGroupConcat; import net.sf.jsqlparser.expression.NextValExpression; import net.sf.jsqlparser.expression.NotExpression; @@ -56,12 +57,12 @@ import net.sf.jsqlparser.expression.OverlapsCondition; import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; -import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; import net.sf.jsqlparser.expression.SignedExpression; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StructType; +import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.TimeKeyExpression; import net.sf.jsqlparser.expression.TimeValue; import net.sf.jsqlparser.expression.TimestampValue; @@ -75,6 +76,7 @@ import net.sf.jsqlparser.expression.WindowOffset; import net.sf.jsqlparser.expression.WindowRange; import net.sf.jsqlparser.expression.XMLSerializeExpr; +import net.sf.jsqlparser.expression.XmlTableFunction; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -1008,6 +1010,15 @@ public Void visit(ArrayConstructor aThis, S context) { return null; } + @Override + public Void visit(MapExpression mapExpression, S context) { + for (Map.Entry entry : mapExpression.getEntries()) { + entry.getKey().accept(this, context); + entry.getValue().accept(this, context); + } + return null; + } + @Override public void validate(Expression expression) { expression.accept(this, null); @@ -1250,6 +1261,10 @@ public void visit(ArrayConstructor aThis) { visit(aThis, null); } + public void visit(MapExpression mapExpression) { + visit(mapExpression, null); + } + public void visit(VariableAssignment a) { visit(a, null); 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 a0511d4808..b9f22aada4 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 @@ -28,17 +28,18 @@ import net.sf.jsqlparser.statement.select.MinusOp; import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause; import net.sf.jsqlparser.statement.select.Offset; +import net.sf.jsqlparser.statement.select.OptionClause; +import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; +import net.sf.jsqlparser.statement.select.PivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectItemVisitor; import net.sf.jsqlparser.statement.select.SelectVisitor; -import net.sf.jsqlparser.statement.select.OptionClause; -import net.sf.jsqlparser.statement.select.OptionHint; import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; @@ -159,6 +160,36 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(PivotQuery pivotQuery, S context) { + if (isNotEmpty(pivotQuery.getWithItemsList())) { + pivotQuery.getWithItemsList() + .forEach(withItem -> withItem.accept((SelectVisitor) this, context)); + } + + validateFeature(Feature.pivot); + validateOptionalFromItem(pivotQuery.getFromItem()); + validateOptionalExpressions(pivotQuery.getOnExpressions()); + if (isNotEmpty(pivotQuery.getUsingItems())) { + pivotQuery.getUsingItems().forEach(item -> item.accept(this, context)); + } + validateOptionalExpressions(pivotQuery.getGroupByExpressions()); + validateOptionalOrderByElements(pivotQuery.getOrderByElements()); + validateOptionalInterpolate(pivotQuery.getInterpolate()); + validateOptionalOption(pivotQuery.getOption()); + + if (pivotQuery.getLimit() != null) { + getValidator(LimitValidator.class).validate(pivotQuery.getLimit()); + } + if (pivotQuery.getOffset() != null) { + validateOffset(pivotQuery.getOffset()); + } + if (pivotQuery.getFetch() != null) { + validateFetch(pivotQuery.getFetch()); + } + return null; + } + private void validateOptionalOption(OptionClause option) { if (option != null) { for (OptionHint optionHint : option.getOptionHints()) { @@ -420,6 +451,10 @@ public void visit(PlainSelect plainSelect) { visit(plainSelect, null); } + public void visit(PivotQuery pivotQuery) { + visit(pivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1ed5192ebe..f56523b4fc 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -106,6 +106,10 @@ public class CCJSqlParser extends AbstractJSqlParser { // then-branch closes the ternary and must not be taken as the JSON path operator private int ternaryThenBranchDepth = 0; + // depth of DuckDB MAP keys: the following ":" separates a key from its value + // and must not be taken as the JSON path operator + private int mapKeyDepth = 0; + private void linkAST(ASTNodeAccess access, Node node) { access.setASTNode(node); node.jjtSetValue(access); @@ -536,10 +540,10 @@ public class CCJSqlParser extends AbstractJSqlParser { try { if (getToken(1).kind != OPENING_BRACKET) return false; int k2 = getToken(2).kind; - // Direct subquery: ( SELECT/WITH ) → ParenthesedSelect handles it + // Direct subquery: ( SELECT/WITH/PIVOT ) → ParenthesedSelect handles it // ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect; // ParenthesedFromItem is checked first so Values-as-FROM-item works - if (k2 == K_SELECT || k2 == K_WITH) return false; + if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT) return false; // ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it // Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem if (k2 == K_LATERAL) return false; // handled by LateralSubSelect @@ -4810,6 +4814,8 @@ Select Select() #Select: | ( ( + LOOKAHEAD(2) select = PivotQuery() + | LOOKAHEAD(3) select = PlainSelect() | LOOKAHEAD(3) select = Values() @@ -6101,10 +6107,39 @@ List> PivotFunctionItems(): } { item = FunctionItem() {functionItems.add(item);} - ( "," item = FunctionItem() {functionItems.add(item);} )* + ( LOOKAHEAD(2) "," item = FunctionItem() {functionItems.add(item);} )* { return functionItems; } } +PivotQuery PivotQuery(): +{ + PivotQuery pivotQuery = new PivotQuery(); + FromItem fromItem; + ExpressionList onExpressions = null; + List> usingItems = null; + ExpressionList groupByExpressions = null; +} +{ + fromItem = FromItem() + ( + onExpressions = ExpressionList() + [ LOOKAHEAD(2) usingItems = PivotFunctionItems() ] + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + usingItems = PivotFunctionItems() + [ LOOKAHEAD(2) groupByExpressions = ExpressionList() ] + | + groupByExpressions = ExpressionList() + ) + { + pivotQuery.setFromItem(fromItem); + pivotQuery.setOnExpressions(onExpressions); + pivotQuery.setUsingItems(usingItems); + pivotQuery.setGroupByExpressions(groupByExpressions); + return pivotQuery; + } +} + SelectItem> ExpressionListItem(): { ExpressionList expressionList; @@ -8203,6 +8238,10 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) retval=CharacterPrimary() + | LOOKAHEAD({ !interrupted && getToken(1).kind == DATA_TYPE + && "MAP".equalsIgnoreCase(getToken(1).image) + && getToken(2).kind == OPENING_CURLY_BRACKET }) retval=MapExpression() + | LOOKAHEAD({!interrupted && isImplicitCastAhead()}) retval=ImplicitCast() | retval = JdbcParameter() @@ -8352,7 +8391,8 @@ Expression PrimaryExpression() #PrimaryExpression: // Check for JSON operands [ - LOOKAHEAD(2, { ternaryThenBranchDepth == 0 || !":".equals(getToken(1).image) }) ( + LOOKAHEAD(2, { (ternaryThenBranchDepth == 0 && mapKeyDepth == 0) + || !":".equals(getToken(1).image) }) ( LOOKAHEAD(2) ( token="->" | @@ -8576,6 +8616,43 @@ ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { { return array; } } +MapExpression MapExpression() #MapExpression: +{ + MapExpression mapExpression = new MapExpression(); + Expression key; + Expression value; +} +{ + + [ + key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + ( + "," key = MapKeyExpression() value = Expression() + { mapExpression.addEntry(key, value); } + )* + ] + + { + linkAST(mapExpression, jjtThis); + return mapExpression; + } +} + +Expression MapKeyExpression(): +{ + Expression key; +} +{ + try { + { mapKeyDepth++; } + key = Expression() + } finally { + mapKeyDepth--; + } + { return key; } +} + List> StructParameters(): { String parameterName = ""; @@ -8682,7 +8759,8 @@ JsonExpression JsonExpression(Expression expr, List Date: Sat, 22 Aug 2026 18:15:11 +0530 Subject: [PATCH 023/387] anchor oracle hint pattern to avoid quadratic backtracking (#2495) --- .../sf/jsqlparser/expression/OracleHint.java | 2 +- .../jsqlparser/expression/OracleHintTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/OracleHint.java b/src/main/java/net/sf/jsqlparser/expression/OracleHint.java index 4ab164f987..166e19e08d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/OracleHint.java +++ b/src/main/java/net/sf/jsqlparser/expression/OracleHint.java @@ -24,7 +24,7 @@ public class OracleHint extends ASTNodeAccessImpl implements Expression { private static final Pattern SINGLE_LINE = Pattern.compile("--\\+ *([^ ].*[^ ])"); private static final Pattern MULTI_LINE = - Pattern.compile("/\\*\\+ *([^ ].*[^ ]) *\\*+/", Pattern.MULTILINE | Pattern.DOTALL); + Pattern.compile("\\A/\\*\\+ *([^ ].*[^ ]) *\\*+/", Pattern.MULTILINE | Pattern.DOTALL); private String value; private boolean singleLine = false; diff --git a/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java b/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java index 2da69721c2..687c29ad57 100644 --- a/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/OracleHintTest.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.expression; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -46,4 +50,18 @@ void testMerge() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); } + @Test + void testCraftedHintCommentDoesNotBacktrack() { + final StringBuilder sb = new StringBuilder("-- /*+ "); + for (int i = 0; i < 100000; i++) { + sb.append('*'); + } + final String crafted = sb.toString(); + + // a line comment carrying an unterminated /*+ marker (no closing */) used to make the + // block hint pattern backtrack quadratically, and it is not an oracle hint anyway + assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> assertFalse(OracleHint.isHintMatch(crafted))); + } + } From aa7df62608b6a881ee4861dab4ea392fd7c07a23 Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sun, 23 Aug 2026 06:03:39 +0800 Subject: [PATCH 024/387] fix(parser): support Oracle USING INDEX TABLESPACE in ALTER TABLE ADD CONSTRAINT (#2498) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 11 ++++- .../jsqlparser/statement/alter/AlterTest.java | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f56523b4fc..28c070db40 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11717,7 +11717,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName): } /** - * Parses USING [INDEX] name clause in ALTER TABLE constraint tails. + * Parses USING [INDEX] [name] [TABLESPACE name] clause in ALTER TABLE constraint tails. */ void AlterExpressionUsingIndex(AlterExpression alterExp): { @@ -11726,7 +11726,14 @@ void AlterExpressionUsingIndex(AlterExpression alterExp): { { alterExp.addParameters("USING"); } [ LOOKAHEAD(2) { alterExp.addParameters("INDEX"); } ] - sk4=RelObjectName() { alterExp.addParameters(sk4); } + ( + LOOKAHEAD(2, ) sk4=RelObjectName() + { alterExp.addParameters("TABLESPACE"); alterExp.addParameters(sk4); } + | + sk4=RelObjectName() { alterExp.addParameters(sk4); } + [ LOOKAHEAD(2) sk4=RelObjectName() + { alterExp.addParameters("TABLESPACE"); alterExp.addParameters(sk4); } ] + ) } /** diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 35481fb7e6..f9a18ec4ce 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -2364,4 +2364,46 @@ public void testAlterTableAddConstraintPrimaryKeyUsingIndexName() throws JSQLPar assertSqlCanBeParsedAndDeparsed(sql); } + + @Test + public void testAlterTableAddConstraintPrimaryKeyUsingIndexTablespace() + throws JSQLParserException { + String sql = + "ALTER TABLE bfmcs.your_table ADD CONSTRAINT your_table_pk PRIMARY KEY (ID) USING INDEX TABLESPACE your_tablespace"; + Statement stmt = CCJSqlParserUtil.parse(sql); + assertInstanceOf(Alter.class, stmt); + + Alter alter = (Alter) stmt; + List alterExpressions = alter.getAlterExpressions(); + assertNotNull(alterExpressions); + assertEquals(1, alterExpressions.size()); + + AlterExpression alterExp = alterExpressions.get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + assertEquals(Arrays.asList("USING", "INDEX", "TABLESPACE", "your_tablespace"), + alterExp.getParameters()); + + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + public void testAlterTableAddConstraintPrimaryKeyUsingIndexNameAndTablespace() + throws JSQLParserException { + String sql = + "ALTER TABLE your_table ADD CONSTRAINT pk_test PRIMARY KEY (ID) USING INDEX idx_test TABLESPACE ts_test"; + Statement stmt = CCJSqlParserUtil.parse(sql); + assertInstanceOf(Alter.class, stmt); + + Alter alter = (Alter) stmt; + List alterExpressions = alter.getAlterExpressions(); + assertNotNull(alterExpressions); + assertEquals(1, alterExpressions.size()); + + AlterExpression alterExp = alterExpressions.get(0); + assertEquals(AlterOperation.ADD, alterExp.getOperation()); + assertEquals(Arrays.asList("USING", "INDEX", "idx_test", "TABLESPACE", "ts_test"), + alterExp.getParameters()); + + assertSqlCanBeParsedAndDeparsed(sql); + } } From 1e4e92bafdfd0fa9f801772afe52ef76b44c3c2e Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sun, 23 Aug 2026 06:04:39 +0800 Subject: [PATCH 025/387] fix(parser): support CHARACTER SET in CAST CHAR types (#2497) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 14 ++++++++------ .../jsqlparser/expression/CastExpressionTest.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 28c070db40..381f3afc1a 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11410,12 +11410,14 @@ ColDataType DataType(): ( tk= | tk= | tk = | tk = | tk = | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= ) { type = tk.image; } - [ - // MySQL seems to allow: INT UNSIGNED - LOOKAHEAD(2) ( LOOKAHEAD(2) ( tk = | tk = | tk = - | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= ) { type += " " + tk.image; } )+ - ] + ( + // MySQL seems to allow: INT UNSIGNED. Do not consume CHARACTER when it starts + // the trailing CHARACTER SET clause of a character type. + LOOKAHEAD(2, { getToken(1).kind != K_CHARACTER || getToken(2).kind != K_SET }) + ( tk = | tk = | tk = + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= ) { type += " " + tk.image; } + )* [ LOOKAHEAD(2) "(" ( tk= { precision = Integer.valueOf(tk.image); } | tk= { precision = Integer.MAX_VALUE; } ) [ | ] diff --git a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java index 81c2ae58af..d0365054ea 100644 --- a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java @@ -68,6 +68,21 @@ void testImplicitCastDoublePrecisionIssue1344() throws JSQLParserException { Assertions.assertTrue(select.getSelectItem(0).getExpression() instanceof CastExpression); } + @Test + void testCastCharWithCharacterSet() throws JSQLParserException { + String sqlStr = "SELECT CAST('abc' AS CHAR CHARACTER SET utf8mb4)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + CastExpression castExpression = Assertions.assertInstanceOf(CastExpression.class, + select.getSelectItem(0).getExpression()); + + Assertions.assertEquals("CHAR", castExpression.getColDataType().getDataType()); + Assertions.assertEquals("utf8mb4", castExpression.getColDataType().getCharacterSet()); + + assertSqlCanBeParsedAndDeparsed( + "SELECT CAST('abc' AS CHAR(10) CHARACTER SET utf8mb4)", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST('abc' AS CHARACTER VARYING)", true); + } + @Test public void testCastToSigned() throws JSQLParserException { From 46584159b957706dfb2c4c852e4a60cf161cff9e Mon Sep 17 00:00:00 2001 From: Liming Deng Date: Sun, 23 Aug 2026 13:09:37 +0800 Subject: [PATCH 026/387] fix(parser): support leading CONTENT/DOCUMENT keyword in XMLPARSE (#2501) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 36 +++++++++- .../statement/select/SelectXMLParseTest.java | 65 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 381f3afc1a..0bdead858d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -657,6 +657,38 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * XML constructor functions take a bare leading keyword before their first argument, + * e.g. XMLPARSE(CONTENT expr), XMLPARSE(DOCUMENT expr), XMLELEMENT(NAME ident, ...). + * Guarded by the function name so that CONTENT/DOCUMENT/NAME stay plain identifiers + * everywhere else, e.g. f(content). + */ + private void consumeXmlLeadingKeyword(Function function) { + String name = function.getName(); + if (name == null) { + return; + } + name = name.toUpperCase(java.util.Locale.ROOT); + if (!"XMLPARSE".equals(name) && !"XMLELEMENT".equals(name) && !"XMLFOREST".equals(name)) { + return; + } + Token token = getToken(1); + if (token.kind != S_IDENTIFIER && token.kind != K_NAME) { + return; + } + String keyword = token.image.toUpperCase(java.util.Locale.ROOT); + if (!"CONTENT".equals(keyword) && !"DOCUMENT".equals(keyword) && !"NAME".equals(keyword)) { + return; + } + // only a bare leading keyword: an expression must follow + Token next = getToken(2); + if (next.kind == EOF || ")".equals(next.image) || ",".equals(next.image) + || ".".equals(next.image) || "(".equals(next.image)) { + return; + } + function.setExtraKeyword(getNextToken().image); + } + private boolean isKeywordArgumentAhead() { Token t = getToken(1); if (t.kind == EOF || t.image.equals(")")) return false; @@ -10154,9 +10186,9 @@ Function InternalFunction(boolean escaped): } { [ LOOKAHEAD(2) prefixToken = ] - funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); } + funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); retval.setName(funcName.getNames()); } - "(" + "(" { consumeXmlLeadingKeyword(retval); } [ LOOKAHEAD(2) [ LOOKAHEAD(2) ( diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java new file mode 100644 index 0000000000..9bb5352558 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java @@ -0,0 +1,65 @@ +/*- + * #%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.statement.select; + +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Test; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +/** + * XML constructor functions taking a bare leading keyword before their first argument, e.g. + * {@code XMLPARSE(CONTENT expr)}. + */ +public class SelectXMLParseTest { + + @Test + public void testXmlParseContent() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content a) FROM mytable"); + } + + @Test + public void testXmlParseDocument() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(document a) FROM mytable"); + } + + @Test + public void testXmlParseUpperCaseKeyword() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(CONTENT a) FROM mytable"); + } + + @Test + public void testXmlAggWithXmlParse() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT xmlagg(xmlparse(content sn.CODE || ',') ORDER BY sn.CODE).getclobval() AS SN FROM GV_SYS_CODEINFO sn"); + } + + @Test + public void testXmlElementName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlelement(name a, b) FROM mytable"); + } + + @Test + public void testXmlForestName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlforest(name a) FROM mytable"); + } + + /** + * CONTENT, DOCUMENT and NAME must stay plain identifiers everywhere else. + */ + @Test + public void testKeywordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT content, document, name FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT content FROM content c WHERE c.document = 1"); + assertSqlCanBeParsedAndDeparsed("SELECT t.name AS document FROM mytable t"); + assertSqlCanBeParsedAndDeparsed("SELECT f(content) FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content) FROM mytable"); + } +} From 9fc38bd13bdaa7bc5bbcad7a8e36eed2cc599c79 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Mon, 24 Aug 2026 20:19:04 +0900 Subject: [PATCH 027/387] fix(parser): support MySQL index options and prefix-length key parts in index DDL (#2505) * fix(parser): support index key parts with a prefix length and a direction MySQL allows a key part to carry both a prefix length and a sort direction, e.g. "CREATE INDEX i ON t (c1(20) DESC)". IndexColumnWithParams() accepted at most one optional CreateParameter() per key part, so the prefix length consumed it and the following ASC/DESC could not be matched. Collect the parameters in a loop instead. IndexColumnsWithParamsList() is shared by the CREATE INDEX and the ALTER TABLE ADD INDEX paths, so both are fixed. Refs #2490 * fix(parser): support MySQL index options in CREATE INDEX and DROP INDEX Several valid MySQL index DDL statements were rejected because their option keywords are tokens of their own and were therefore not reachable from the option lists that CREATE INDEX and DROP INDEX use: CREATE INDEX i ON t (c1) KEY_BLOCK_SIZE = 8 CREATE INDEX i ON t (c1) ALGORITHM = INPLACE LOCK = NONE CREATE FULLTEXT INDEX i ON t (body) WITH PARSER ngram CREATE SPATIAL INDEX i ON t (g) DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE The keywords are added to the existing flat token lists of CreateParameter() and Drop() rather than as new grammar alternatives, so no new choice is introduced and the JavaCC warning count is unchanged. LOCK is the one exception: it also starts a LOCK TABLE statement, so taking it unconditionally as a DROP argument would be ambiguous with the next statement. It is guarded by a semantic lookahead that only accepts it when it is not followed by TABLE. Refs #2490 * refactor(parser): clarify key part option variable names in IndexColumnWithParams Rename the accumulator to columnParams and keep parameter for the result of a single CreateParameter(), matching how the other CreateParameter() loops in the grammar name them. Collect eagerly and pass null only when no option was parsed, because ColumnParams renders a separating space for a non-null list. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 31 +++++++++-- .../jsqlparser/statement/alter/AlterTest.java | 8 +++ .../statement/create/CreateIndexTest.java | 55 +++++++++++++++++++ .../jsqlparser/statement/drop/DropTest.java | 17 ++++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0bdead858d..de05f830d2 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -10979,6 +10979,8 @@ List ColumnNamesWithParamsList() : { Index.ColumnParams IndexColumnWithParams(): { String columnName = null; + // the options collected for this key part, and the result of a single CreateParameter() + List columnParams = new ArrayList(); List parameter = null; Expression expression = null; Index.ColumnParams column = null; @@ -10986,17 +10988,21 @@ Index.ColumnParams IndexColumnWithParams(): { { ( columnName=RelObjectName() - { parameter = null; } - [ parameter = CreateParameter() ] + // MySQL allows a key part to carry a prefix length and a direction, e.g. "c1(20) DESC", + // so more than one parameter has to be collected here. + ( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )* { - column = new Index.ColumnParams(columnName, parameter); + // ColumnParams renders a separating space for a non-null list, so a key part + // without options has to be given null rather than an empty list. + column = new Index.ColumnParams(columnName, + columnParams.isEmpty() ? null : columnParams); } | "(" expression=Expression() ")" - { parameter = null; } - [ LOOKAHEAD(2) parameter = CreateParameter() ] + ( LOOKAHEAD(2) parameter = CreateParameter() { columnParams.addAll(parameter); } )* { - column = new Index.ColumnParams(expression, parameter); + column = new Index.ColumnParams(expression, + columnParams.isEmpty() ? null : columnParams); } ) { @@ -11846,6 +11852,11 @@ List CreateParameter(): | tk= | tk= | tk= | tk= | tk= | tk = | tk = | tk= | tk= | tk= | tk= | tk= | tk= | tk= + // MySQL index_option / algorithm_option / lock_option keywords, e.g. the trailing + // "KEY_BLOCK_SIZE = 8 ALGORITHM = INPLACE LOCK = NONE" of CREATE INDEX, the + // "WITH PARSER" option, and the FULLTEXT / SPATIAL index types of CREATE INDEX. + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk="=" ) { param.add(tk.image); } @@ -12021,11 +12032,19 @@ Drop Drop(): ( ( tk= | tk= | tk= + // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, + // e.g. "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE". + | tk= | tk= | tk="=" ) { dropArgs.add(tk.image); } | ( name = Table() { dropArgs.add("ON"); dropArgs.add(name.toString()); } ) + | + // The lock_option of DROP INDEX. LOCK also starts a LOCK TABLE statement, so it is only + // taken as a DROP argument when it cannot be the beginning of the next statement. + LOOKAHEAD({ getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE }) + tk= { dropArgs.add(tk.image); } )* { if (dropArgs.size() > 0) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index f9a18ec4ce..d9344e1b48 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -2406,4 +2406,12 @@ public void testAlterTableAddConstraintPrimaryKeyUsingIndexNameAndTablespace() assertSqlCanBeParsedAndDeparsed(sql); } + + @Test + public void testAlterTableAddIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i05 (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i33 (c1 (20) ASC)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD UNIQUE INDEX i34 (c1 (10) DESC)"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index 40b7504f75..dea34d1926 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -179,4 +179,59 @@ public void testCreateIndexVisibility() throws JSQLParserException { public void testCreateIndexIncludeIssue2459() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("CREATE INDEX idx_a ON t1 (a) INCLUDE (b, c)"); } + + @Test + public void testCreateIndexKeyPartWithPrefixLengthAndDirectionIssue2490() + throws JSQLParserException { + // MySQL writes the prefix length without a space, JSqlParser deparses it with one. + String statement = "CREATE INDEX i03 ON t (c1(20) DESC)"; + CreateIndex createIndex = (CreateIndex) parserManager.parse(new StringReader(statement)); + + List params = createIndex.getIndex().getColumns().get(0).getParams(); + assertEquals(2, params.size()); + assertEquals("(20)", params.get(0)); + assertEquals("DESC", params.get(1)); + + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i03 ON t (c1 (20) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i04 ON t (c1 (20) ASC, c2 (10) DESC)"); + assertSqlCanBeParsedAndDeparsed("CREATE UNIQUE INDEX i25 ON t (c1 (10) DESC)"); + } + + @Test + public void testCreateIndexKeyBlockSizeIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i08 ON t (c1) KEY_BLOCK_SIZE = 8"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i09 ON t (c1) KEY_BLOCK_SIZE 8"); + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i14 ON t (c1) USING BTREE KEY_BLOCK_SIZE = 8 COMMENT 'combo' INVISIBLE"); + } + + @Test + public void testCreateIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i10 ON t (c1) ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i11 ON t (c1) ALGORITHM INPLACE LOCK NONE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i12 ON t (c1) ALGORITHM = INPLACE"); + assertSqlCanBeParsedAndDeparsed("CREATE INDEX i13 ON t (c1) LOCK = NONE"); + + CreateIndex createIndex = (CreateIndex) parserManager + .parse(new StringReader("CREATE INDEX i10 ON t (c1) ALGORITHM=INPLACE LOCK=NONE")); + assertEquals(List.of("ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"), + createIndex.getTailParameters()); + } + + @Test + public void testCreateFullTextAndSpatialIndexIssue2490() throws JSQLParserException { + // These used to fall back to UnsupportedStatement instead of producing a CreateIndex. + CreateIndex fullText = (CreateIndex) parserManager + .parse(new StringReader("CREATE FULLTEXT INDEX i17 ON t (body)")); + assertEquals("FULLTEXT", fullText.getIndex().getType()); + + CreateIndex spatial = (CreateIndex) parserManager + .parse(new StringReader("CREATE SPATIAL INDEX i19 ON t (g)")); + assertEquals("SPATIAL", spatial.getIndex().getType()); + + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i17 ON t (body)"); + assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i18 ON t (body) WITH PARSER ngram"); + assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i19 ON t (g)"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java index a48b2fe7df..a692273f26 100644 --- a/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/drop/DropTest.java @@ -12,8 +12,10 @@ import java.io.StringReader; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; import static net.sf.jsqlparser.test.TestUtils.*; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -152,4 +154,19 @@ void dropTemporaryTableTestIssue1712() throws JSQLParserException { String sqlStr = "drop temporary table if exists tmp_MwYT8N0z"; assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + public void testDropIndexAlgorithmAndLockOptionsIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("DROP INDEX i15 ON t ALGORITHM = INPLACE LOCK = NONE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i16 ON t ALGORITHM INPLACE"); + assertSqlCanBeParsedAndDeparsed("DROP INDEX i17 ON t LOCK = NONE"); + } + + @Test + public void testDropTableFollowedByLockTableIssue2490() throws JSQLParserException { + // LOCK must not be swallowed as a DROP argument when it starts the next statement. + Statements statements = CCJSqlParserUtil.parseStatements( + "DROP TABLE t1; LOCK TABLE t2 IN SHARE MODE;"); + assertEquals(2, statements.size()); + } } From d8d054aa6038a472a7468df91d9728d2a184e52a Mon Sep 17 00:00:00 2001 From: fudian Date: Tue, 25 Aug 2026 12:34:46 +0800 Subject: [PATCH 028/387] fix(parser): support PostgreSQL # binary operator (bitwise XOR / geometric intersection) (#2507) A lone '#' now lexes as a binary operator instead of a one-character identifier. In PostgreSQL '#' is the integer bitwise exclusive OR (docs Table 9.4) and the geometric intersection of lseg / line / box (Table 9.36), see #2499. Identifier lexing of '#' is untouched by longest match: SQL Server #temp / ##global (#1197) and the JSON operators #> / #>> (#1695) lex as before. A lone '#' can no longer be an identifier: bare column / table / alias names previously parsed silently and now fail loudly; quoted forms ("#", `#`) still parse. The AST node is the dedicated Intersects operator (sibling of GeometryDistance), wired into ExpressionVisitor, the adapter, the deparser, TablesNamesFinder and the validator. Signed-off-by: Fu Dian --- .../expression/ExpressionVisitor.java | 7 ++++ .../expression/ExpressionVisitorAdapter.java | 6 +++ .../operators/relational/Intersects.java | 41 +++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 7 ++++ .../util/deparser/ExpressionDeParser.java | 7 ++++ .../validator/ExpressionValidator.java | 11 +++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 11 ++++- .../statement/select/PostgresTest.java | 41 +++++++++++++++++++ 8 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 2eb909a989..adbe17e8dc 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -40,6 +40,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -725,6 +726,12 @@ default void visit(GeometryDistance geometryDistance) { this.visit(geometryDistance, null); } + T visit(Intersects intersects, S context); + + default void visit(Intersects intersects) { + this.visit(intersects, null); + } + T visit(Select select, S context); T visit(TranscodingFunction transcodingFunction, S context); diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 274d4e00a3..853ffbfe9a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -44,6 +44,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -804,6 +805,11 @@ public T visit(GeometryDistance geometryDistance, S context) { return visitBinaryExpression(geometryDistance, context); } + @Override + public T visit(Intersects intersects, S context) { + return visitBinaryExpression(intersects, context); + } + @Override public T visit(Select select, S context) { if (selectVisitor != null) { diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java new file mode 100644 index 0000000000..f85e5b3385 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java @@ -0,0 +1,41 @@ +/*- + * #%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.operators.relational; + +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; + +/** + * The PostgreSQL # binary operator: the geometric intersection of lseg / line / box + * (documentation Table 9.36) and the integer bitwise exclusive OR (Table 9.4). + */ +public class Intersects extends BinaryExpression { + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String getStringExpression() { + return "#"; + } + + @Override + public Intersects withLeftExpression(Expression expression) { + return (Intersects) super.withLeftExpression(expression); + } + + @Override + public Intersects withRightExpression(Expression expression) { + return (Intersects) super.withRightExpression(expression); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 471a971a42..0e94b9c9e5 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -46,6 +46,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -1127,6 +1128,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects); + return null; + } + @Override public Void visit(AllColumns allColumns, S context) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 8328dc7997..05f251019f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -110,6 +110,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -1773,6 +1774,12 @@ public StringBuilder visit(GeometryDistance geometryDistance, S context) { return builder; } + @Override + public StringBuilder visit(Intersects intersects, S context) { + deparse(intersects, " # ", null); + return builder; + } + @Override public StringBuilder visit(TSQLLeftJoin tsqlLeftJoin, S context) { this.deparse(tsqlLeftJoin, " *= ", null); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index e44fee2413..3f35986ee3 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -107,6 +107,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -815,6 +816,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects, " # "); + return null; + } + @Override public Void visit(UserVariable var, S context) { // nothing to validate @@ -916,6 +923,10 @@ public void visit(JsonOperator jsonExpr) { visit(jsonExpr, null); } + public void visit(Intersects intersects) { + visit(intersects, null); + } + public void visit(UserVariable var) { visit(var, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index de05f830d2..9ea69f37bb 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -487,9 +487,11 @@ public class CCJSqlParser extends AbstractJSqlParser { int prec; // Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||), - // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>) + // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>), + // S_HASH_OPERATOR(#) // String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar) if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; + else if (op == S_HASH_OPERATOR) prec = 5; else if (op == OP_CONCAT || op == OP_PIPE || op == OP_LSHIFT || op == OP_RSHIFT) prec = 5; else { @@ -515,6 +517,7 @@ public class CCJSqlParser extends AbstractJSqlParser { Expression right = prattArithRest(PrimaryExpression(), prec + 1); if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } @@ -1930,6 +1933,12 @@ TOKEN: matchedToken.kind = charLiteralIndex; } | +// Bare `#` as a binary operator (PostgreSQL bitwise XOR / geometric +// intersection). Declared before to win the length tie on a +// lone `#`; `#temp`-style identifiers and `#>` / `#>>` keep their usual +// lexing by longest match (#1197, #1695). + +| ()*) | "$" | ("$" ()*)> | <#LETTER: | | [ "#", "_" ] // Not SQL:2016 compliant! diff --git a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java index 9ded167869..218cb580d1 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java @@ -13,6 +13,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; @@ -138,4 +139,44 @@ void testQuotedIdentifier() throws JSQLParserException { Assertions.assertEquals("`This is a Test Table`", table.getName()); } + + @Test + public void testPostgresHashBinaryOperator() throws JSQLParserException { + // PostgreSQL 18, Table 9.4 (Mathematical Operators): + // integral_type # integral_type -> bitwise exclusive OR + assertSqlCanBeParsedAndDeparsed("SELECT 17 # 5"); + // PostgreSQL 18, Table 9.36 (Geometric Operators): + // geometric_type # geometric_type -> point of intersection + Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT lseg1 # lseg2"); + Intersects intersects = Assertions.assertInstanceOf(Intersects.class, + select.getPlainSelect().getSelectItem(0).getExpression()); + Assertions.assertEquals("#", intersects.getStringExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getLeftExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getRightExpression()); + + assertSqlCanBeParsedAndDeparsed("SELECT a # (b + 1) FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a # b AS x FROM t"); + // same tier as `&`: binds tighter than the comparison that follows + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a # b = 1"); + } + + @Test + public void testPostgresHashOperatorKeepsIdentifiersAndJsonOperators() + throws JSQLParserException { + // `#` stays an identifier character: SQL Server `#temp`, `##global` + // (#1197), Oracle `#$tab1#` and unquoted names containing `#` + assertSqlCanBeParsedAndDeparsed("SELECT #temp FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT ##global FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT #$tab1# FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a#b FROM t"); + // `#>` / `#>>` keep their JSON lexing (#1695) + assertSqlCanBeParsedAndDeparsed("SELECT data #> '{a,b}' FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT data #>> '{0,1}' FROM t"); + // a lone `#` can no longer be an identifier (alias, column or table + // name), all such forms fail loudly now that `#` is an operator + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT 1 #")); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT # FROM t")); + } } From e781b3f27720347161faf9f6c6c76d4ca4e25b6d Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Tue, 25 Aug 2026 14:02:43 +0900 Subject: [PATCH 029/387] fix(parser): support CAST to an array type for MySQL multi-valued indexes (#2506) MySQL defines a multi-valued index key part by casting to an array, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). The trailing ARRAY keyword was not reachable, so both the CREATE INDEX and the ALTER TABLE form failed to parse. ARRAY is accepted as a further word of the cast target type, the way INT UNSIGNED already is, so no model change is needed and the deparsed statement round trips. It is added to the CAST production only, where MySQL allows it, which leaves the ARRAY constructor form untouched. Closes the last open group of #2490. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 8 +++++++- .../jsqlparser/expression/CastExpressionTest.java | 15 +++++++++++++++ .../sf/jsqlparser/statement/alter/AlterTest.java | 6 ++++++ .../statement/create/CreateIndexTest.java | 6 ++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9ea69f37bb..8a66239abe 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -9955,7 +9955,13 @@ CastExpression CastExpression(): ")" ) | - type=ColDataType() { retval.setColDataType(type); } + ( + type=ColDataType() { retval.setColDataType(type); } + // MySQL casts to an array of the given type when a multi-valued index key part is + // defined, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). ARRAY reads as a further + // word of the type, the way INT UNSIGNED already does. + [ { type.setDataType(type.getDataType() + " ARRAY"); } ] + ) ) // BigQuery FORMAT clause diff --git a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java index d0365054ea..38827001cc 100644 --- a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java @@ -13,6 +13,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -155,4 +156,18 @@ void testNestedCompositeTypeCastIssue2341() throws JSQLParserException { Assertions.assertEquals(1, parenthesedCast.size()); Assertions.assertInstanceOf(CastExpression.class, parenthesedCast.get(0)); } + + @Test + public void testCastToArrayIssue2490() throws JSQLParserException { + // MySQL casts to an array of the given type when a multi-valued index key part is + // defined, and accepts the same cast anywhere else an expression is allowed. + assertSqlCanBeParsedAndDeparsed("SELECT CAST(data -> '$.zips' AS UNSIGNED ARRAY) FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(x AS CHAR (10) ARRAY) FROM t"); + + PlainSelect select = (PlainSelect) CCJSqlParserUtil + .parse("SELECT CAST(data -> '$.zips' AS UNSIGNED ARRAY) FROM t"); + CastExpression cast = Assertions.assertInstanceOf(CastExpression.class, + select.getSelectItem(0).getExpression()); + Assertions.assertEquals("UNSIGNED ARRAY", cast.getColDataType().getDataType()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index d9344e1b48..a18215b790 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -2414,4 +2414,10 @@ public void testAlterTableAddIndexKeyPartWithPrefixLengthAndDirectionIssue2490() assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD INDEX i33 (c1 (20) ASC)"); assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD UNIQUE INDEX i34 (c1 (10) DESC)"); } + + @Test + public void testAlterTableAddMultiValuedIndexIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE t ADD INDEX i31 ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java index dea34d1926..a124565914 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateIndexTest.java @@ -234,4 +234,10 @@ public void testCreateFullTextAndSpatialIndexIssue2490() throws JSQLParserExcept assertSqlCanBeParsedAndDeparsed("CREATE FULLTEXT INDEX i18 ON t (body) WITH PARSER ngram"); assertSqlCanBeParsedAndDeparsed("CREATE SPATIAL INDEX i19 ON t (g)"); } + + @Test + public void testCreateMultiValuedIndexIssue2490() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX i20 ON t ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); + } } From dff722b593ed4e4e68b26a2798ad836c3695aa4c Mon Sep 17 00:00:00 2001 From: Chen Jianjin <161543912+jianjindream@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:04:57 +0800 Subject: [PATCH 030/387] fix(parser): parse qualified columns in GROUP BY (#2504) --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 4 +- .../expression/LambdaExpressionTest.java | 17 ++++++- .../statement/select/SelectTest.java | 50 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8a66239abe..a4f1557641 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -7975,7 +7975,7 @@ ExpressionList SimpleExpressionList(): ( LOOKAHEAD(2, {!interrupted} ) "," ( - LOOKAHEAD( 7 ) expr=LambdaExpression() + LOOKAHEAD( RelObjectName() "->" ) expr=LambdaExpression() | expr=SimpleExpression() ) @@ -8038,7 +8038,7 @@ ExpressionList ComplexExpressionList(): | LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() | - LOOKAHEAD(7) expr=LambdaExpression() + LOOKAHEAD( RelObjectName() "->" ) expr=LambdaExpression() | expr=Expression() ) { expressions.add(expr); } diff --git a/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java index 4dcc4c6e16..bd6a0fcd8a 100644 --- a/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/LambdaExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -20,7 +21,10 @@ class LambdaExpressionTest { @Test void testLambdaFunctionSingleParameter() throws JSQLParserException { String sqlStr = "select list_transform( split('test', ''), x -> unicode(x) )"; - TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Select select = assertInstanceOf( + Select.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true)); + Function function = select.getPlainSelect().getSelectItem(0).getExpression(Function.class); + assertInstanceOf(LambdaExpression.class, function.getParameters().get(1)); } @Test @@ -52,4 +56,15 @@ void testLambdaFirstArgumentIssue2195() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void testLambdaFunctionFourParameters() throws JSQLParserException { + String sqlStr = "SELECT list_transform([1], (a, b, c, d) -> a)"; + Select select = assertInstanceOf( + Select.class, TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true)); + Function function = select.getPlainSelect().getSelectItem(0).getExpression(Function.class); + LambdaExpression lambda = + assertInstanceOf(LambdaExpression.class, function.getParameters().get(1)); + assertEquals(4, lambda.getIdentifiers().size()); + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index a385e17bb2..bb3fa06562 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -43,6 +43,7 @@ import net.sf.jsqlparser.expression.IntervalExpression; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.NotExpression; import net.sf.jsqlparser.expression.NullValue; @@ -60,6 +61,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThan; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; @@ -1375,6 +1377,47 @@ public void testGroupBy() throws JSQLParserException { assertStatementCanBeDeparsedAs(select, statement); } + @Test + public void testIssue2485OracleParenthesizedFullyQualifiedGroupByColumn() + throws JSQLParserException { + String sql = "SELECT sys.dual.dummy, count(*)\n" + + "FROM sys.dual\n" + + "GROUP BY (sys.dual.dummy), (sys.dual.dummy)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + Select select = assertInstanceOf(Select.class, statement); + PlainSelect plainSelect = select.getPlainSelect(); + assertNotNull(plainSelect.getGroupBy()); + + ExpressionList groupByExpressions = plainSelect.getGroupBy() + .getGroupByExpressionList(); + assertEquals(2, groupByExpressions.size()); + for (Expression groupByExpression : groupByExpressions) { + ParenthesedExpressionList parenthesedExpression = assertInstanceOf( + ParenthesedExpressionList.class, groupByExpression); + Column column = assertInstanceOf(Column.class, parenthesedExpression.get(0)); + assertEquals("sys.dual.dummy", column.getFullyQualifiedName()); + } + } + + @Test + public void testIssue2485OracleParenthesizedFourPartGroupByColumn() + throws JSQLParserException { + String sql = "SELECT 1\n" + + "FROM sys.dual\n" + + "GROUP BY sys.dual.dummy, (catalog.sys.dual.dummy)"; + + Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true); + Select select = assertInstanceOf(Select.class, statement); + ExpressionList groupByExpressions = select.getPlainSelect().getGroupBy() + .getGroupByExpressionList(); + assertEquals(2, groupByExpressions.size()); + ParenthesedExpressionList parenthesedExpression = assertInstanceOf( + ParenthesedExpressionList.class, groupByExpressions.get(1)); + Column column = assertInstanceOf(Column.class, parenthesedExpression.get(0)); + assertEquals("catalog.sys.dual.dummy", column.getFullyQualifiedName()); + } + @Test public void testHaving() throws JSQLParserException { String statement = @@ -3205,6 +3248,13 @@ public void testJsonExpression() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( "SELECT json_typeof(sale->'items'), json_typeof(sale->'items'->'quantity') FROM sales"); + Select jsonSelect = (Select) CCJSqlParserUtil.parse("SELECT data->'images' FROM instagram"); + assertInstanceOf(JsonExpression.class, + jsonSelect.getPlainSelect().getSelectItem(0).getExpression()); + jsonSelect = (Select) CCJSqlParserUtil.parse("SELECT data->>'images' FROM instagram"); + assertInstanceOf(JsonExpression.class, + jsonSelect.getPlainSelect().getSelectItem(0).getExpression()); + // The following staments can be parsed but not deparsed for (String statement : new String[] { "SELECT doc->'site_name' FROM websites WHERE doc @> '{\"tags\":[{\"term\":\"paris\"}, {\"term\":\"food\"}]}'", From 177dbd716f0a3c377e8b51e0202054c79562c8d9 Mon Sep 17 00:00:00 2001 From: fudian Date: Wed, 26 Aug 2026 09:06:17 +0800 Subject: [PATCH 031/387] feat(parser): support MySQL # line comments behind allowHashLineComments (#2508) * feat(parser): support MySQL # line comments behind allowHashLineComments The second step agreed in #2502: with Feature.allowHashLineComments (default off) a `#` runs to end of line as a comment, unconditional like MySQL itself (no blank needed, `42#24` is a comment too); with the flag off a lone `#` stays the binary operator introduced in #2507, so neither reading silently replaces the other. Mechanics: under the flag SimpleCharStream rewrites a token-start `#` in the buffer to a character no other lexical rule starts with, so the dedicated HASH_LINE_COMMENT production wins the match for every `#` form while identifier and JSON-operator lexing of the default mode stay untouched (rewriting the buffer keeps the matcher's backup / re-read arithmetic intact, and GetImage() restores the `#` in the token image). Unquoted identifiers (and @@variables) end at their first `#` via their token actions, which re-lex the remainder as the comment. Quoted forms ("#", `#`, "a#b") keep their `#` in both modes. Under the flag the statement semantics are MySQL's: `SELECT #temp FROM t` comments out the rest of the line and fails, quoted "#temp" still parses. Closes #2499, supersedes #2502. Signed-off-by: Fu Dian * docs(parser): add regeneration warning to SimpleCharStream header The file is maintained by hand on top of the JavaCC template and carries the in-buffer rewrite of a leading # in BeginToken(), which Feature.allowHashLineComments depends on. Per review on #2508. --------- Signed-off-by: Fu Dian --- .../jsqlparser/parser/AbstractJSqlParser.java | 8 +++ .../jsqlparser/parser/SimpleCharStream.java | 44 ++++++++++++++- .../sf/jsqlparser/parser/feature/Feature.java | 6 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 54 +++++++++++++++++++ .../parser/CCJSqlParserUtilTest.java | 50 +++++++++++++++++ .../statement/select/SelectASTTest.java | 18 +++++++ 6 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 45580cb5eb..b4e63851d7 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -77,6 +77,14 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) { return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter); } + public P withHashLineComments() { + return withFeature(Feature.allowHashLineComments, true); + } + + public P withHashLineComments(boolean allowHashLineComments) { + return withFeature(Feature.allowHashLineComments, allowHashLineComments); + } + public P withUnparenthesizedSubSelects() { return withFeature(Feature.allowUnparenthesizedSubSelects, true); } diff --git a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java index 0e633ebbb5..ca81a8dd11 100644 --- a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java +++ b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java @@ -7,9 +7,18 @@ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 * #L% */ -/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 8.0.0 */ +/* Generated By:JavaCC: Do not edit this line by hand. SimpleCharStream.java Version 8.0.0 */ +/* + * Warning: this file is maintained by hand on top of the JavaCC template. Among other manual + * adjustments it carries the in-buffer '#' rewrite in BeginToken(), which + * Feature.allowHashLineComments depends on. Never overwrite it with a freshly generated + * SimpleCharStream, that would silently drop the rewrite and break allowHashLineComments. + */ package net.sf.jsqlparser.parser; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; + /** * An implementation of interface CharStream, where the stream is assumed to contain only ASCII * characters (without unicode processing). @@ -42,6 +51,17 @@ public class SimpleCharStream { int available; int tokenBegin; + // MySQL `#` line comments (#2499): under the flag a token-start `#` is + // rewritten in the buffer to HASH_SUBSTITUTION, a character no other + // lexical rule starts with, so the dedicated HASH_LINE_COMMENT production + // wins the match for every `#` form (`# c`, `#c`, `#>`, ...). Rewriting + // the buffer itself (instead of synthesizing reads) keeps the matcher's + // backup / re-read arithmetic intact; the production's action restores + // the `#` in the token image. Wired (with the token manager's + // configuration) before parsing, null keeps this inert. + static final char HASH_SUBSTITUTION = '\u0001'; + FeatureConfiguration featureConfiguration; + /** * Constructor. */ @@ -159,6 +179,11 @@ public final char BeginToken() throws java.io.IOException { absoluteTokenBegin = totalCharsRead; + if (c == '#' && featureConfiguration != null + && featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) { + buffer[bufpos] = HASH_SUBSTITUTION; + c = HASH_SUBSTITUTION; + } return c; } @@ -325,6 +350,23 @@ public void ReInit(Provider dstream) { * Get token literal value. */ public String GetImage() { + // restore the `#` rewritten to HASH_SUBSTITUTION: only the comment + // token's own window starts with the sentinel (token-start rewrites + // only), so this is exact and covers every consumer of GetImage. + // The wiring check keeps parses that never opted in on the original + // cost (the stream is only wired through the feature consumers / + // withConfiguration) + if (featureConfiguration == null) { + return doGetImage(); + } + String image = doGetImage(); + if (!image.isEmpty() && image.charAt(0) == HASH_SUBSTITUTION) { + image = "#" + image.substring(1); + } + return image; + } + + private String doGetImage() { if (bufpos >= tokenBegin) { return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); } else { diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 4c1b6dd512..5daa74ecaa 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -795,6 +795,12 @@ public enum Feature { */ allowBackslashEscapeCharacter(false), + /** + * allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary + * operator (#2507: PostgreSQL bitwise XOR / geometric intersection) + */ + allowHashLineComments(false), + /** * allows sub selects without parentheses, e.g. `select * from dual where 1 = select 1` */ diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a4f1557641..9ae702d483 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -91,10 +91,17 @@ public class CCJSqlParser extends AbstractJSqlParser { public CCJSqlParser withConfiguration(FeatureConfiguration configuration) { token_source.configuration = configuration; + jj_input_stream.featureConfiguration = configuration; return this; } public FeatureConfiguration getConfiguration() { + // wire the stream before parsing starts: every feature mutation goes + // through here (the consumers run pre-parse), default-off parsing + // never wires the stream and stays on the zero-cost path + if (jj_input_stream.featureConfiguration != token_source.configuration) { + jj_input_stream.featureConfiguration = token_source.configuration; + } return token_source.configuration; } @@ -1883,6 +1890,19 @@ SPECIAL_TOKEN: < LINE_COMMENT: ("--" | "//") (~["\r","\n"])*> } +// MySQL `#` line comment (#2499), flag-gated. Under +// Feature.allowHashLineComments the stream rewrites a token-start `#` to +// SimpleCharStream.HASH_SUBSTITUTION, a character no other rule starts with, +// so this production wins the match for every `#` form (`# c`, `#c`, `#>`, +// ...) without touching the identifier and JSON-operator lexing of the +// default mode. The action restores the `#` in the token image; unquoted +// identifiers end at their first `#` via the S_IDENTIFIER action, which +// re-lexes the remainder as this comment. +SPECIAL_TOKEN: +{ + < HASH_LINE_COMMENT: "\u0001" (~["\r","\n"])*> +} + // Nested block comments: /* ... /* ... */ ... */ // // Uses a nesting counter (commentNesting in TOKEN_MGR_DECLS) and @@ -1940,12 +1960,46 @@ TOKEN: | ()*) | "$" | ("$" ()*)> +{ + // MySQL `#` line comments (#2499): under the flag an unquoted identifier + // ends at its first `#`, the rest of the line becomes a comment via the + // stream-level substitution (real MySQL reads `42#24` as `42` plus + // comment too). Quoted identifiers and strings keep their `#`. + // the wiring check keeps the flag lookup off the hot path of parses + // that never opted in (the stream is only wired through the feature + // consumers / withConfiguration); getValue avoids the String-based + // getAsBoolean roundtrip + if (input_stream.featureConfiguration != null + && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + int hashIndex = matchedToken.image.indexOf('#'); + if (hashIndex > 0) { + input_stream.backup(matchedToken.image.length() - hashIndex); + matchedToken.image = matchedToken.image.substring(0, hashIndex); + } + } +} | <#LETTER: | | [ "#", "_" ] // Not SQL:2016 compliant! > | <#PART_LETTER_NO_DOLLAR: | | [ "#", "_" , "@" ] > | <#PART_LETTER: | | [ "$" , "#", "_" , "@" ] > | ()? > +{ + // same truncation as S_IDENTIFIER: `@@#name` ends at the `#` under + // allowHashLineComments, the rest of the line becomes the comment + // the wiring check keeps the flag lookup off the hot path of parses + // that never opted in (the stream is only wired through the feature + // consumers / withConfiguration); getValue avoids the String-based + // getAsBoolean roundtrip + if (input_stream.featureConfiguration != null + && Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) { + int hashIndex = matchedToken.image.indexOf('#'); + if (hashIndex > 0) { + input_stream.backup(matchedToken.image.length() - hashIndex); + matchedToken.image = matchedToken.image.substring(0, hashIndex); + } + } +} // Unicode characters and categories are defined here: https://www.unicode.org/Public/UNIDATA/UnicodeData.txt // SQL:2016 states: diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index dd9b557325..4a72587070 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -514,4 +514,54 @@ void testSingleStatementWithEmptyLines() throws JSQLParserException { + "def'\n" + "where id=?", true); } + + @Test + public void testHashLineCommentsFeature() throws Exception { + // with the flag: MySQL line comments, `#` to end of line, + // unconditional like MySQL itself (no blank needed, `42#24` is a + // comment there too); the statement continues on the next line + assertEquals("SELECT 42", CCJSqlParserUtil + .parse("SELECT 42 # 24", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 # comment, 2", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #comment", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #!bang", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 42", CCJSqlParserUtil + .parse("SELECT 42#24", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT a", CCJSqlParserUtil + .parse("SELECT a#b FROM t", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1 FROM t", CCJSqlParserUtil + .parse("SELECT 1 # c\nFROM t", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("# leading\nSELECT 1", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #", p -> p.withHashLineComments(true)).toString()); + assertEquals("SELECT 1", CCJSqlParserUtil + .parse("SELECT 1 #\n", p -> p.withHashLineComments(true)).toString()); + // the `#>` family has no meaning in MySQL and comments out like any + // other `#`; quoted forms keep their `#` + assertEquals("SELECT data", CCJSqlParserUtil + .parse("SELECT data #> '{a}' FROM t", p -> p.withHashLineComments(true)) + .toString()); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT '#' FROM t", true, + p -> p.withHashLineComments(true)); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT \"a#b\" FROM t", true, + p -> p.withHashLineComments(true)); + // default (flag off): the #2507 binary operator, unchanged + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24").toString()); + assertEquals("SELECT 42#24", CCJSqlParserUtil.parse("SELECT 42#24").toString()); + } + + @Test + public void testHashLineCommentsMySQLStatementSemantics() throws Exception { + // `#temp` is a comment under the flag, so `SELECT #temp FROM t` + // loses the rest of the line and fails, exactly like MySQL; with the + // flag off the same input parses as the #2507 operator expression + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT #temp FROM t", p -> p.withHashLineComments(true))); + assertEquals("SELECT 1 # comment", + CCJSqlParserUtil.parse("SELECT 1 # comment").toString()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java index 8c2a36261f..173248762a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor; import net.sf.jsqlparser.parser.CCJSqlParserTreeConstants; +import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.Node; import net.sf.jsqlparser.parser.Token; @@ -212,4 +213,21 @@ public void testSelectASTExtractWithCommentsIssue1580_2() throws JSQLParserExcep assertThat(root.jjtGetFirstToken().specialToken.image) .isEqualTo("/* I want this comment */\n"); } + + @Test + public void testSelectASTHashLineCommentImage() throws Exception { + // a `#` comment under allowHashLineComments is a normal special + // token carrying the original `# ...` text + CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1 # note\nFROM t"); + parser.withHashLineComments(true); + parser.Statement(); + List comments = new ArrayList<>(); + for (Token t = parser.getASTRoot().jjtGetFirstToken(); t.next != null; t = t.next) { + for (Token sp = t.specialToken; sp != null; sp = sp.specialToken) { + comments.add(sp); + } + } + + assertThat(comments).extracting(token -> token.image).containsExactly("# note"); + } } From 94c4508bd3819126398670e5ed7c68a069ebfca4 Mon Sep 17 00:00:00 2001 From: fudian Date: Wed, 26 Aug 2026 10:29:03 +0800 Subject: [PATCH 032/387] feat(parser): turn on the dialect's lexer feature preset in withDialect (#2511) * feat(parser): enable the dialect's lexer feature preset in withDialect Extends Dialect with ANSI_SQL, MYSQL, MARIADB, SQLSERVER, POSTGRESQL and H2 and makes withDialect turn on the dialect's lexer switches on top of the grammar gating: backslash escapes and # line comments for MYSQL and MARIADB, square bracket quotation for SQLSERVER, none for the rest. The preset only turns features on, an explicit switch after it still wins. Implements #2510. * docs(site): document the Dialect preset in the parser features section Per review on #2511: the usage page's parser features section gets the withDialect preset paragraph and example, which also covers withHashLineComments (part of the MySQL and MariaDB presets, previously undocumented). --- .../jsqlparser/parser/AbstractJSqlParser.java | 25 +++++++++- src/site/sphinx/usage.rst | 14 +++++- .../parser/CCJSqlParserUtilTest.java | 47 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index b4e63851d7..8d4e72ea40 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -13,7 +13,10 @@ import net.sf.jsqlparser.parser.feature.FeatureConfiguration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; +import java.util.Set; public abstract class AbstractJSqlParser

{ @@ -22,7 +25,21 @@ public abstract class AbstractJSqlParser

{ protected List parseErrors = new ArrayList<>(); public enum Dialect { - ORACLE, EXASOL + ANSI_SQL, ORACLE, MYSQL(Feature.allowBackslashEscapeCharacter, + Feature.allowHashLineComments), MARIADB(Feature.allowBackslashEscapeCharacter, + Feature.allowHashLineComments), SQLSERVER( + Feature.allowSquareBracketQuotation), POSTGRESQL, H2, EXASOL; + + private final Set lexerFeatures; + + Dialect(Feature... lexerFeatures) { + this.lexerFeatures = lexerFeatures.length == 0 ? EnumSet.noneOf(Feature.class) + : EnumSet.copyOf(Arrays.asList(lexerFeatures)); + } + + public Set getLexerFeatures() { + return lexerFeatures; + } } public P withSquareBracketQuotation() { @@ -62,7 +79,11 @@ public P withTimeOut(long timeOutMillSeconds) { } public P withDialect(Dialect dialect) { - return withFeature(Feature.dialect, dialect.name()); + withFeature(Feature.dialect, dialect.name()); + for (Feature lexerFeature : dialect.getLexerFeatures()) { + withFeature(lexerFeature, true); + } + return me(); } public P withAllowedNestingDepth(int allowedNestingDepth) { diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index c0bcdac88a..d989b010bb 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -317,4 +317,16 @@ Additionally there are Features to control the Parser's effort at the cost of th sqlStr , parser -> parser .withBackslashEscapeCharacter(true) - ); \ No newline at end of file + ); + +Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter`` and ``withHashLineComments`` (both MySQL and MariaDB syntax), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset. + +.. code-block:: java + + // Select the Database Dialect: turns on that dialect's parser features + sqlStr="SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24"; + Statement stmt3 = CCJSqlParserUtil.parse( + sqlStr + , parser -> parser + .withDialect(Dialect.MYSQL) + ); diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 4a72587070..6addd7f830 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -564,4 +564,51 @@ public void testHashLineCommentsMySQLStatementSemantics() throws Exception { assertEquals("SELECT 1 # comment", CCJSqlParserUtil.parse("SELECT 1 # comment").toString()); } + + @Test + public void testDialectPresets() throws Exception { + // MYSQL and MARIADB: backslash escapes and # line comments (MySQL 8 + // manual "Comments" and "String Literals", MariaDB KB "Comment Syntax" + // and "String Literals"); backticks are always on, brackets are not + // MySQL syntax + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB}) { + assertEquals("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42", CCJSqlParserUtil + .parse("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24", + p -> p.withDialect(dialect)) + .toString()); + } + // SQLSERVER: bracket quotation only (Microsoft Learn "Database + // Identifiers"), `#` stays available for identifiers (temp tables) + assertEquals("SELECT [my column] FROM t", + CCJSqlParserUtil.parse("SELECT [my column] FROM t", + p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER)).toString()); + // empty presets keep the defaults: the #2507 operator, no backslash + // escapes + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.ANSI_SQL, AbstractJSqlParser.Dialect.ORACLE, + AbstractJSqlParser.Dialect.POSTGRESQL, AbstractJSqlParser.Dialect.H2, + AbstractJSqlParser.Dialect.EXASOL}) { + assertEquals("SELECT 42 # 24", + CCJSqlParserUtil.parse("SELECT 42 # 24", p -> p.withDialect(dialect)) + .toString()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil + .parse("SELECT 'a\\'b'", p -> p.withDialect(dialect))); + } + } + + @Test + public void testDialectPresetSwitchOverrides() throws Exception { + // an explicit switch after the preset wins: `#` is the #2507 + // operator again + assertEquals("SELECT 1 # comment", CCJSqlParserUtil.parse("SELECT 1 # comment", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL).withHashLineComments(false)) + .toString()); + // the preset never turns a previously enabled switch off + assertEquals("SELECT [my column] FROM mytable", + CCJSqlParserUtil.parse("SELECT [my column] FROM mytable", + p -> p.withSquareBracketQuotation(true) + .withDialect(AbstractJSqlParser.Dialect.MYSQL)) + .toString()); + } } From c86cf6ad2ce210b7536821597d2a18226ccd2179 Mon Sep 17 00:00:00 2001 From: fudian Date: Wed, 26 Aug 2026 13:40:11 +0800 Subject: [PATCH 033/387] feat(parser): support double quoted string literals behind allowDoubleQuotedStrings (#2513) One lexeme, two readings: ANSI/Postgres/Oracle and SQL Server quote identifiers with double quotes, while BigQuery, Spark/Databricks and MySQL default sql_mode read them as string literals. The switch rewrites the token kind in the S_QUOTED_IDENTIFIER action (the square bracket machinery below), StringValue keeps the double quote on round-trip via its quoteStr. MYSQL and MARIADB presets carry the switch. Implements item 1 of #2512. --- .../sf/jsqlparser/expression/StringValue.java | 6 +++ .../jsqlparser/parser/AbstractJSqlParser.java | 14 +++++- .../sf/jsqlparser/parser/feature/Feature.java | 6 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 5 +++ src/site/sphinx/usage.rst | 4 +- .../parser/CCJSqlParserUtilTest.java | 44 +++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/StringValue.java b/src/main/java/net/sf/jsqlparser/expression/StringValue.java index a16536fab5..f02bff523c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/StringValue.java +++ b/src/main/java/net/sf/jsqlparser/expression/StringValue.java @@ -36,6 +36,12 @@ public StringValue(String escapedValue) { && escapedValue.endsWith("'")) { value = escapedValue.substring(1, escapedValue.length() - 1); return; + } else if (escapedValue.length() >= 2 && escapedValue.startsWith("\"") + && escapedValue.endsWith("\"")) { + // double quoted String Literals (Feature.allowDoubleQuotedStrings) + value = escapedValue.substring(1, escapedValue.length() - 1); + quoteStr = "\""; + return; } else if (escapedValue.length() >= 4 && escapedValue.startsWith("$$") && escapedValue.endsWith("$$")) { value = escapedValue.substring(2, escapedValue.length() - 2); diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 8d4e72ea40..2cb8033963 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -26,8 +26,10 @@ public abstract class AbstractJSqlParser

{ public enum Dialect { ANSI_SQL, ORACLE, MYSQL(Feature.allowBackslashEscapeCharacter, - Feature.allowHashLineComments), MARIADB(Feature.allowBackslashEscapeCharacter, - Feature.allowHashLineComments), SQLSERVER( + Feature.allowHashLineComments, + Feature.allowDoubleQuotedStrings), MARIADB(Feature.allowBackslashEscapeCharacter, + Feature.allowHashLineComments, + Feature.allowDoubleQuotedStrings), SQLSERVER( Feature.allowSquareBracketQuotation), POSTGRESQL, H2, EXASOL; private final Set lexerFeatures; @@ -98,6 +100,14 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) { return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter); } + public P withDoubleQuotedStrings() { + return withFeature(Feature.allowDoubleQuotedStrings, true); + } + + public P withDoubleQuotedStrings(boolean allowDoubleQuotedStrings) { + return withFeature(Feature.allowDoubleQuotedStrings, allowDoubleQuotedStrings); + } + public P withHashLineComments() { return withFeature(Feature.allowHashLineComments, true); } diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 5daa74ecaa..de5a492f2e 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -795,6 +795,12 @@ public enum Feature { */ allowBackslashEscapeCharacter(false), + /** + * allows double quoted String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode); + * disabled by default, where double quotes stay quoted identifiers (ANSI SQL) + */ + allowDoubleQuotedStrings(false), + /** * allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary * operator (#2507: PostgreSQL bitwise XOR / geometric intersection) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 9ae702d483..4533078c97 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2077,6 +2077,11 @@ TOKEN: matchedToken.kind = squaredBracketOpenIndex; input_stream.backup(image.length() - 1); } + if ( configuration.getAsBoolean(Feature.allowDoubleQuotedStrings) + && matchedToken.image.charAt(0) == '"' ) { + // `charLiteralIndex` defined in TokenManagerDeclaration above + matchedToken.kind = charLiteralIndex; + } } } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index d989b010bb..4a9696b176 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -279,7 +279,7 @@ Define the Parser Features JSQLParser interprets Squared Brackets ``[..]`` as Arrays, which does not work with MS SQL Server and T-SQL. Please use the Parser Features to instruct JSQLParser to read Squared Brackets as Quotes instead. -JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. +JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature. Additionally there are Features to control the Parser's effort at the cost of the performance. @@ -319,7 +319,7 @@ Additionally there are Features to control the Parser's effort at the cost of th .withBackslashEscapeCharacter(true) ); -Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter`` and ``withHashLineComments`` (both MySQL and MariaDB syntax), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset. +Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset. .. code-block:: java diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 6addd7f830..54c5e44029 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -12,6 +12,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; @@ -20,6 +21,7 @@ import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.TableStatement; import net.sf.jsqlparser.test.MemoryLeakVerifier; import net.sf.jsqlparser.test.TestUtils; @@ -44,6 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class CCJSqlParserUtilTest { @@ -611,4 +614,45 @@ public void testDialectPresetSwitchOverrides() throws Exception { .withDialect(AbstractJSqlParser.Dialect.MYSQL)) .toString()); } + + @Test + public void testDoubleQuotedStringsFeature() throws Exception { + // default (flag off): double quotes are quoted identifiers, unchanged + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil + .parse("SELECT \"not an identifier\"")).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + // on: the BigQuery/Spark/MySQL-default reading, double quotes are + // string literals, the quote kept on round-trip + select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"not an identifier\"", + p -> p.withDoubleQuotedStrings(true))).getSelectBody(); + Expression expression = select.getSelectItems().get(0).getExpression(); + assertTrue(expression instanceof StringValue); + assertEquals("not an identifier", ((StringValue) expression).getValue()); + assertEquals("SELECT \"not an identifier\"", select.toString()); + // empty string and doubled quotes take the single-quote treatment + assertEquals("SELECT \"\"", CCJSqlParserUtil + .parse("SELECT \"\"", p -> p.withDoubleQuotedStrings(true)).toString()); + assertEquals("SELECT \"a\"\"b\"", CCJSqlParserUtil + .parse("SELECT \"a\"\"b\"", p -> p.withDoubleQuotedStrings(true)).toString()); + // identifier positions: a `"..."` token follows the existing + // string-as-table branch (`FROM 'file.csv'`), the same leniency + // single quotes already have; MySQL itself would error here + assertEquals("SELECT * FROM \"t\"", CCJSqlParserUtil + .parse("SELECT * FROM \"t\"", p -> p.withDoubleQuotedStrings(true)).toString()); + } + + @Test + public void testDoubleQuotedStringsPreset() throws Exception { + // MYSQL and MARIADB presets carry the switch (default sql_mode reading) + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil + .parse("SELECT \"abc\"", p -> p.withDialect(dialect))).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof StringValue); + } + // the identifier-default dialects keep the quoted identifier reading + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"abc\"", + p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER))).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + } } From 84d23e7dfae5ac85c1632dac6a1a3074704eaf66 Mon Sep 17 00:00:00 2001 From: fudian Date: Fri, 28 Aug 2026 08:35:07 +0800 Subject: [PATCH 034/387] feat(parser): concatenate adjacent string literals behind adjacentStringLiterals (#2514) * feat(parser): concatenate adjacent string literals behind adjacentStringLiterals Three readings of two neighboring string literals: alias (MySQL, SQL Server, the current behavior), concatenated across a newline (the SQL standard and PostgreSQL), concatenated across any whitespace (GoogleSQL, Spark/Databricks). The mode lives in a String valued Feature like Feature.dialect, the merge is one guarded loop in PrimaryExpression using the token line numbers for the newline rule, producing a single merged StringValue. ANSI_SQL and POSTGRESQL presets carry NEWLINE. Implements item 3 of #2512. * docs(site): document adjacentStringLiterals in the parser features section * hardening: null-safe mode check in isAdjacentStringConcat The FeatureConfiguration seeds every configurable Feature with its declared default, so the mode is never null today; the guard keeps the predicate locally correct regardless. * feat(parser): withAdjacentStringLiterals(boolean) mapping false = OFF, true = NEWLINE stores the AdjacentStringLiterals enum constant itself as the feature value * docs(parser): move isAdjacentStringConcat below isNamedExprListAhead, reattaching its javadoc --- .../jsqlparser/parser/AbstractJSqlParser.java | 45 +++++++++-- .../sf/jsqlparser/parser/feature/Feature.java | 9 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 26 +++++++ src/site/sphinx/usage.rst | 4 +- .../parser/CCJSqlParserUtilTest.java | 78 +++++++++++++++++++ 5 files changed, 155 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 2cb8033963..27880a2c71 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -24,24 +24,41 @@ public abstract class AbstractJSqlParser

{ protected boolean errorRecovery = false; protected List parseErrors = new ArrayList<>(); + public enum AdjacentStringLiterals { + OFF, NEWLINE, WHITESPACE + } + public enum Dialect { - ANSI_SQL, ORACLE, MYSQL(Feature.allowBackslashEscapeCharacter, + ANSI_SQL(AdjacentStringLiterals.NEWLINE), ORACLE, MYSQL(AdjacentStringLiterals.OFF, + Feature.allowBackslashEscapeCharacter, Feature.allowHashLineComments, - Feature.allowDoubleQuotedStrings), MARIADB(Feature.allowBackslashEscapeCharacter, + Feature.allowDoubleQuotedStrings), MARIADB(AdjacentStringLiterals.OFF, + Feature.allowBackslashEscapeCharacter, Feature.allowHashLineComments, - Feature.allowDoubleQuotedStrings), SQLSERVER( - Feature.allowSquareBracketQuotation), POSTGRESQL, H2, EXASOL; + Feature.allowDoubleQuotedStrings), SQLSERVER(AdjacentStringLiterals.OFF, + Feature.allowSquareBracketQuotation), POSTGRESQL( + AdjacentStringLiterals.NEWLINE), H2, EXASOL; private final Set lexerFeatures; + private final AdjacentStringLiterals adjacentStringLiterals; - Dialect(Feature... lexerFeatures) { + Dialect(AdjacentStringLiterals adjacentStringLiterals, Feature... lexerFeatures) { + this.adjacentStringLiterals = adjacentStringLiterals; this.lexerFeatures = lexerFeatures.length == 0 ? EnumSet.noneOf(Feature.class) : EnumSet.copyOf(Arrays.asList(lexerFeatures)); } + Dialect(Feature... lexerFeatures) { + this(AdjacentStringLiterals.OFF, lexerFeatures); + } + public Set getLexerFeatures() { return lexerFeatures; } + + public AdjacentStringLiterals getAdjacentStringLiterals() { + return adjacentStringLiterals; + } } public P withSquareBracketQuotation() { @@ -82,12 +99,30 @@ public P withTimeOut(long timeOutMillSeconds) { public P withDialect(Dialect dialect) { withFeature(Feature.dialect, dialect.name()); + if (dialect.getAdjacentStringLiterals() != AdjacentStringLiterals.OFF) { + withAdjacentStringLiterals(dialect.getAdjacentStringLiterals()); + } for (Feature lexerFeature : dialect.getLexerFeatures()) { withFeature(lexerFeature, true); } return me(); } + public P withAdjacentStringLiterals() { + return withAdjacentStringLiterals(AdjacentStringLiterals.NEWLINE); + } + + public P withAdjacentStringLiterals(boolean adjacentStringLiterals) { + return withAdjacentStringLiterals( + adjacentStringLiterals ? AdjacentStringLiterals.NEWLINE + : AdjacentStringLiterals.OFF); + } + + public P withAdjacentStringLiterals(AdjacentStringLiterals adjacentStringLiterals) { + getConfiguration().setValue(Feature.adjacentStringLiterals, adjacentStringLiterals); + return me(); + } + public P withAllowedNestingDepth(int allowedNestingDepth) { return withFeature(Feature.allowedNestingDepth, allowedNestingDepth); } diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index de5a492f2e..bb68fa387a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.expression.OracleHierarchicalExpression; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.parser.AbstractJSqlParser; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; import net.sf.jsqlparser.statement.CreateFunctionalStatement; @@ -801,6 +802,14 @@ public enum Feature { */ allowDoubleQuotedStrings(false), + /** + * concatenates adjacent String Literals: NEWLINE when separated by whitespace with at least one + * newline (the SQL standard and PostgreSQL), WHITESPACE across any whitespace (GoogleSQL, + * Spark/Databricks); OFF by default, where the second literal stays an alias (MySQL, SQL + * Server) or fails (everywhere else) + */ + adjacentStringLiterals(AbstractJSqlParser.AdjacentStringLiterals.OFF), + /** * allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary * operator (#2507: PostgreSQL bitwise XOR / geometric intersection) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 4533078c97..e617d7e6c7 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1127,6 +1127,24 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Semantic lookahead for the adjacent string literal concatenation in PrimaryExpression: + * true when Feature.adjacentStringLiterals is on and the next token is a string literal + * meeting the mode's separation rule (WHITESPACE: any, NEWLINE: a newline between the + * tokens, detected through the token line numbers). + */ + protected boolean isAdjacentStringConcat() { + String mode = getAsString(Feature.adjacentStringLiterals); + if (mode == null || AdjacentStringLiterals.OFF.name().equals(mode)) { + return false; + } + if (getToken(1).kind != S_CHAR_LITERAL) { + return false; + } + return AdjacentStringLiterals.WHITESPACE.name().equals(mode) + || getToken(1).beginLine > getToken(0).endLine; + } + /** * Checks if the next token can start a condition suffix * (comparison, IN, BETWEEN, LIKE, IS NULL, etc.) @@ -8319,6 +8337,7 @@ Expression PrimaryExpression() #PrimaryExpression: Expression timezoneRightExpr = null; Token token = null; Token sign = null; + Token adjacentToken = null; String tmp = ""; ColDataType type = null; boolean not = false; @@ -8419,6 +8438,13 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new BooleanValue(token.image); } | token= { retval = new StringValue(token.image); linkAST(retval,jjtThis); } + ( LOOKAHEAD({ isAdjacentStringConcat() }) + adjacentToken= + { + ((StringValue) retval) + .setValue(((StringValue) retval).getValue() + new StringValue(adjacentToken.image).getValue()); + } + )* | "{d" token= "}" { retval = new DateValue(token.image); } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 4a9696b176..3ad3190d98 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -279,7 +279,7 @@ Define the Parser Features JSQLParser interprets Squared Brackets ``[..]`` as Arrays, which does not work with MS SQL Server and T-SQL. Please use the Parser Features to instruct JSQLParser to read Squared Brackets as Quotes instead. -JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature. +JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature. Adjacent String Literals concatenate optionally: only across a newline (``NEWLINE``, the SQL standard and PostgreSQL) or across any whitespace (``WHITESPACE``, GoogleSQL and Spark/Databricks); ``withAdjacentStringLiterals(true)`` selects the standard ``NEWLINE`` mode, ``false`` switches it off. Additionally there are Features to control the Parser's effort at the cost of the performance. @@ -319,7 +319,7 @@ Additionally there are Features to control the Parser's effort at the cost of th .withBackslashEscapeCharacter(true) ); -Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset. +Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. ``withDialect(Dialect.POSTGRESQL)`` and ``withDialect(Dialect.ANSI_SQL)`` turn on the newline rule for adjacent String Literals. Features set explicitly after the dialect preset win over the preset. .. code-block:: java diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 54c5e44029..8add3b1bc1 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -655,4 +655,82 @@ public void testDoubleQuotedStringsPreset() throws Exception { p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER))).getSelectBody(); assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); } + + @Test + public void testAdjacentStringLiteralsNewline() throws Exception { + // default (off): the MySQL / SQL Server alias reading, unchanged + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'").toString()); + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'").toString()); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'")); + // newline mode: the standard / Postgres reading, "Two string constants + // that are only separated by whitespace with at least one newline are + // concatenated" + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE))) + .getSelectBody(); + Expression expression = select.getSelectItems().get(0).getExpression(); + assertTrue(expression instanceof StringValue); + assertEquals("ab", ((StringValue) expression).getValue()); + assertEquals("SELECT 'ab'", select.toString()); + // same line keeps the alias reading under newline mode + assertEquals("SELECT 'a' 'b'", + CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // expression positions concatenate too + assertEquals("SELECT * FROM t WHERE x = 'ab'", + CCJSqlParserUtil.parse("SELECT * FROM t WHERE x = 'a'\n'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // three parts merge into one literal + assertEquals("SELECT 'abc'", + CCJSqlParserUtil.parse("SELECT 'a'\n'b'\n'c'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.NEWLINE)) + .toString()); + // the ANSI SQL and Postgres presets carry it, the MySQL preset does not + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.ANSI_SQL, AbstractJSqlParser.Dialect.POSTGRESQL}) { + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withDialect(dialect)).toString()); + } + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withDialect(AbstractJSqlParser.Dialect.MYSQL)).toString()); + } + + @Test + public void testAdjacentStringLiteralsWhitespace() throws Exception { + // whitespace mode: the GoogleSQL / Spark reading, same line + // concatenates (their literal chunking) + assertEquals("SELECT 'ab'", + CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE)) + .toString()); + // combined with allowDoubleQuotedStrings: the BigQuery chunking shape + assertEquals("SELECT \"12\"", + CCJSqlParserUtil.parse("SELECT \"1\" \"2\"", + p -> p.withDoubleQuotedStrings(true).withAdjacentStringLiterals( + AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE)) + .toString()); + } + + @Test + public void testAdjacentStringLiteralsBoolean() throws Exception { + // true: the standard (newline) mode, the same line keeps the alias reading + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals(true)).toString()); + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withAdjacentStringLiterals(true)).toString()); + // false: off, the alias reading also across newlines + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals(false)).toString()); + // the no-arg variant enables the standard mode + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a'\n'b'", + p -> p.withAdjacentStringLiterals()).toString()); + } } From 9c76556d9b5910b1fe7ddb2f2930f8da0f5cdd7c Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Fri, 28 Aug 2026 17:48:00 +0700 Subject: [PATCH 035/387] fix(parser): grammar ambiguities Signed-off-by: Andreas Reichel --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e617d7e6c7..5ea3aab8b6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2070,18 +2070,18 @@ TOKEN: if (!allowEscape) { pos = indexOfSequence(img, "\\'"); if (pos > 0) { - matchedToken.image = "'" + image.substring( 0, image.indexOf("\\'") + 1 ) + "'"; + matchedToken.image = image.substring( 0, image.indexOf("\\'") + 1 ) + "'"; // `charLiteralIndex` defined in TokenManagerDeclaration above matchedToken.kind = charLiteralIndex; - input_stream.backup(image.length() + 1 - matchedToken.image.length()); + input_stream.backup(image.length() - matchedToken.image.length()); } } else { pos = lastIndexOfSequence(img, "\\''"); if (pos > 0) { - matchedToken.image = "'" + image.substring( 0, image.lastIndexOf("\\'") + 3); + matchedToken.image = image.substring( 0, image.lastIndexOf("\\'") + 3); // `charLiteralIndex` defined in TokenManagerDeclaration above matchedToken.kind = charLiteralIndex; - input_stream.backup(image.length() + 1 - matchedToken.image.length() ); + input_stream.backup(image.length() - matchedToken.image.length() ); } } } @@ -7132,7 +7132,7 @@ List InterpolateClause(): } { - [ "(" + [ LOOKAHEAD(2) "(" [ interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } ( "," interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } )* ] @@ -9718,11 +9718,18 @@ IntervalExpression IntervalExpression() : { } } [ - // Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND. - LOOKAHEAD(2, { getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); } - // Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for - // backwards compatibility. - | LOOKAHEAD(2) (token = ) { interval.setIntervalType(token.image); } + // The entry decision for this optional block has to be stated explicitly: + // also starts whatever follows IntervalExpression() at its call sites (an alias, for + // instance), so one token of lookahead cannot decide whether to enter. + LOOKAHEAD({ getToken(1).kind == K_DATE_LITERAL || getToken(1).kind == S_IDENTIFIER }) + ( + // Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND. + LOOKAHEAD({ getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); } + | + // Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for + // backwards compatibility. + token = { interval.setIntervalType(token.image); } + ) ] { return interval; From 4f11c2f81830355bf07919290c126291da62be06 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Fri, 28 Aug 2026 17:48:54 +0700 Subject: [PATCH 036/387] build: upgrade JavaCC (manticore branch) Signed-off-by: Andreas Reichel --- build.gradle | 12 +++++++----- pom.xml | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 319cbdcbb7..508008fb52 100644 --- a/build.gradle +++ b/build.gradle @@ -165,13 +165,13 @@ dependencies { xmlDoclet ('com.manticore-projects.tools:xml-doclet:+'){ changing = true } // enforce latest version of JavaCC - testImplementation('com.manticore-projects.jsqlformatter:javacc-core:+') - testImplementation('com.manticore-projects.jsqlformatter:javacc-java:+') + testImplementation('com.manticore-projects.jsqlformatter:javacc-core:[8.1.2.1941,)') + testImplementation('com.manticore-projects.jsqlformatter:javacc-java:[8.1.2.1941,)') jmh 'org.openjdk.jmh:jmh-core:+' jmh 'org.openjdk.jmh:jmh-generator-annprocess:+' - javacc('com.manticore-projects.jsqlformatter:javacc-core:+') - javacc('com.manticore-projects.jsqlformatter:javacc-java:+') + javacc('com.manticore-projects.jsqlformatter:javacc-core:[8.1.2.1941,)') + javacc('com.manticore-projects.jsqlformatter:javacc-java:[8.1.2.1941,)') } compileJavacc { @@ -447,7 +447,9 @@ tasks.register('renderRR') { def ebnfFile = new File(rrDir, "JSqlParserCC.ebnf") def jjFile = layout.buildDirectory.dir("generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj").get().asFile.absolutePath - def convertProc = new ProcessBuilder('java', '-jar', + def convertProc = new ProcessBuilder('java', + '-Djdk.xml.maxElementDepth=0', + '-jar', new File(rrDir, "convert.war").absolutePath, jjFile) .redirectOutput(ebnfFile) diff --git a/pom.xml b/pom.xml index cd1e28b0b6..212e1fe8f3 100644 --- a/pom.xml +++ b/pom.xml @@ -120,7 +120,7 @@ is expected to track the newest fork build. Pin it (e.g. 8.1.1.1939) before cutting a release so the release is reproducible. --> - [8.1.1.1939,) + [8.1.2.1941,) 5.14.4 5.23.0 3.27.7 From 94bbadcb5a5c3ff9094e5b82e6519fad38d9dd4c Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Fri, 28 Aug 2026 17:49:33 +0700 Subject: [PATCH 037/387] test: add tests for upgraded JavaCC (manticore branch) Signed-off-by: Andreas Reichel --- .../jsqlparser/parser/CCJSqlParserTest.java | 21 +++++++++++++++++++ .../statement/select/SelectASTTest.java | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java index f0093e11f8..d408fc68fb 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserTest.java @@ -10,10 +10,14 @@ package net.sf.jsqlparser.parser; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import net.sf.jsqlparser.JSQLParserException; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.parser.feature.Feature; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; public class CCJSqlParserTest { @Test @@ -24,4 +28,21 @@ public void parserWithTimeout() throws Exception { assertThat(timeOut).isEqualTo(123L); } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "SELECT /* c */1 | SELECT /* c */ 1", + "SELECT 1 /* c */+2 | SELECT 1 /* c */ +2", + "SELECT 1/*x*/+2 | SELECT 1 /*x*/ +2", + "SELECT /* a /* b */ c */1 | SELECT /* a /* b */ c */ 1", + "SELECT * FROM t WHERE/*c*/x=1 | SELECT * FROM t WHERE /*c*/ x=1", + "SELECT/*+ FULL(t) */*FROM t | SELECT /*+ FULL(t) */ * FROM t" + }) + void testBlockCommentFollowedByNonWhitespace(String tight, String spaced) + throws JSQLParserException { + // Issue #2509: the first character after */ was swallowed into the comment token, + // so the tight form either failed to parse or silently lost that character. + assertEquals(CCJSqlParserUtil.parse(spaced).toString(), + CCJSqlParserUtil.parse(tight).toString()); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java index 173248762a..d45052d61a 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java @@ -200,7 +200,7 @@ public Object visit(Node node, Object data) { } }, null); - assertThat(comments).extracting(token -> token.image).containsExactly("/* testcomment */ ", + assertThat(comments).extracting(token -> token.image).containsExactly("/* testcomment */", "-- testcomment2 "); } @@ -211,7 +211,7 @@ public void testSelectASTExtractWithCommentsIssue1580_2() throws JSQLParserExcep Node root = (Node) CCJSqlParserUtil.parseAST(sql); assertThat(root.jjtGetFirstToken().specialToken.image) - .isEqualTo("/* I want this comment */\n"); + .isEqualTo("/* I want this comment */"); } @Test From b9a543cbf469b80d9755047418716746f47cb8a5 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Fri, 28 Aug 2026 17:49:51 +0700 Subject: [PATCH 038/387] doc: updated syntax diagrams Signed-off-by: Andreas Reichel --- src/site/sphinx/syntax_snapshot.rst | 4848 ++++++++++++++++----------- 1 file changed, 2934 insertions(+), 1914 deletions(-) diff --git a/src/site/sphinx/syntax_snapshot.rst b/src/site/sphinx/syntax_snapshot.rst index 76af0c6bf8..342af487d6 100644 --- a/src/site/sphinx/syntax_snapshot.rst +++ b/src/site/sphinx/syntax_snapshot.rst @@ -13,7 +13,7 @@ NonReservedWord .. raw:: html - + @@ -38,682 +38,716 @@ NonReservedWord ALTER ALWAYS - - ANALYZE - - APPEND_ONLY - - APPLY - - APPROXIMATE - - ARCHIVE - - ARRAY - - ASYMMETRIC - - AT - - ASC - - AUTHORIZATION - - AUTO - - AUTO_INCREMENT - - AZURE - - BASE64 + + ANALYSE + + ANALYZE + + APPEND_ONLY + + APPLY + + APPROXIMATE + + ARCHIVE + + ARRAY + + ASYMMETRIC + + AT + + ASC + + AUTHORIZATION + + AUTO + + AUTO_INCREMENT + + AZURE - BEFORE - - BEGIN - - BERNOULLI - - BINARY - - BIT - - BLOBSTORAGE - - BLOCK - - BOOLEAN - - BREADTH - - BRANCH - - BROWSE - - BY - - BYTES - - CACHE - - BUFFERS - - BYTE + BASE64 + + BEFORE + + BEGIN + + BERNOULLI + + BINARY + + BIT + + BLOBSTORAGE + + BLOCK + + BOOLEAN + + BREADTH + + BRANCH + + BROWSE + + BY + + BYTES + + CACHE + + BUFFERS - CALL - - CASCADE - - CASE + BYTE + + CALL + + CASCADE - CAST - - CERTIFICATE - - CHARACTER - - CHANGE - - CHANGES - - CHECKPOINT - - CHAR - - CLOSE - - CLOUD - - COALESCE - - COLLATE + CASE + + CAST + + CERTIFICATE + + CHARACTER + + CHANGE + + CHANGES + + CHECKPOINT + + CHAR + + CLOSE + + CLOUD + + COALESCE - COLUMN - - COLUMNS - - COMMIT - - COMMENT - - COMMENTS - - CONFLICT - - CONSTRAINTS - - CONVERT - - CORRESPONDING - - COSTS - - COUNT - - CREATED - - CYCLE - - DATABASE - - DATA - - DECLARE - - DBA_RECYCLEBIN - - DEFAULTS - - DEPTH - - DEFERRABLE - - DELAYED - - DELETE - - DELIMIT - - DELIMITER - - DESC - - DESCRIBE - - DISABLE - - DISCARD - - DISCONNECT - - DIV - - DDL - - DML - - DO - - DOMAIN - - DRIVER - - DROP - - DUMP - - DUPLICATE + COLLATE + + COLUMN + + COLUMNS + + COMMIT + + COMMENT + + COMMENTS + + CONFLICT + + CONSTRAINTS + + CONVERT + + CORRESPONDING + + COSTS + + COUNT + + CREATED + + CYCLE + + DATABASE + + DATA + + DECLARE + + DBA_RECYCLEBIN + + DEFAULTS + + DEPTH + + DEFERRABLE + + DELAYED + + DELETE + + DELIMIT + + DELIMITER + + DESC + + DESCRIBE + + DISABLE + + DISCARD + + DISCONNECT + + DIV + + DDL + + DML + + DO + + DOMAIN + + DRIVER + + DROP + + DUMP - ELEMENTS - - EMIT - - ENABLE - - ENCODING - - ENCRYPTION - - END - - ENFORCED - - ENGINE - - ERROR - - ESCAPE - - EXA + DUMPFILE + + DUPLICATE + + ELEMENTS + + ENCLOSED + + EMIT + + EMPTY + + ENABLE + + ENCODING + + ENCRYPTION + + END + + ESCAPED - EXCHANGE - - EXCLUDE - - EXCLUDING - - EXCLUSIVE - - EXEC - - EXECUTE - - EXPLAIN - - EXPLICIT - - EXTENDED - - EXTRACT - - EXPORT - - K_ISOLATION - FILTER - - FIRST - - FLUSH - - FOLLOWING - - FORMAT - - FULLTEXT - - FUNCTION - - GRANT - - GROUP_CONCAT - - GUARD - - HASH - - HIGH - - HIGH_PRIORITY - - HISTORY - - HOPPING - - IDENTIFIED - - IDENTITY - - INCLUDE - - INCLUDE_NULL_VALUES - - INCLUDING - - INCREMENT - - INDEX - - INFORMATION - - INSERT - - INTERLEAVE - - INTERPRET - - INVALIDATE - - INVERSE - - INVISIBLE - - ISNULL - - JDBC - - JSON - - JSON_OBJECT - - JSON_OBJECTAGG - - JSON_ARRAY - - JSON_ARRAYAGG - - KEEP - - KEY_BLOCK_SIZE - - KEY + ENFORCED + + ENGINE + + ERROR + + ESCAPE + + EXA + + EXCHANGE + + EXCLUDE + + EXCLUDING + + EXCLUSIVE + + EXEC + + EXECUTE + + EXPLAIN + + EXPLICIT + + EXTENDED + + EXTRACT + + EXPORT + + K_ISOLATION + FILTER + + FIELDS + + FILL + + FIRST + + FLUSH + + FOLLOWING + + FORMAT + + FULLTEXT + + FUNCTION + + GRANT + + GROUPS + + GROUP_CONCAT + + GUARD + + HASH + + HIGH + + HIGH_PRIORITY + + HISTORY + + HOPPING + + IDENTIFIED + + IDENTITY + + INCLUDE + + INCLUDE_NULL_VALUES + + INCLUDING + + INCREMENT + + INDEX + + INFORMATION + + INSERT + + INTERLEAVE + + INTERPOLATE + + INTERPRET + + INVALIDATE + + INVERSE + + INVISIBLE + + ISNULL - KEYS - - KILL - - FN - - LAST - - LEADING - - LESS - - LEVEL - - LOCAL - - LOCK - - LOCKED - - LINK - - LOG - - LOOP - - LOW - - LOW_PRIORITY - - LTRIM - - MATCH - - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - MATCHED - - MATERIALIZED - - MAX - - MAXVALUE - - MEMBER - - MERGE - - MIN - - MINVALUE - - MODE - - MODIFY - - MOVEMENT - - NAMES - - NAME - - NEVER - - NEXT - - K_NEXTVAL - NO - - NOCACHE - - NOKEEP - - NOLOCK - - NOMAXVALUE - - NOMINVALUE + JDBC + + JSON + + JSON_OBJECT + + JSON_OBJECTAGG + + JSON_ARRAY + + JSON_ARRAYAGG + + KEEP + + KEY_BLOCK_SIZE + + KEY + + KEYS + + KILL + + FN + + LAST + + LEADING + + LESS + + LEVEL + + LINES + + LOCAL + + LOCK + + LOCKED + + LINK + + LOG + + LOOP + + LOW + + LOW_PRIORITY + + LTRIM + + MATCH + + MATCH_ANY + + MATCH_ALL + + MATCH_PHRASE + + MATCH_PHRASE_PREFIX + + MATCH_REGEXP + + MATCHED + + MATERIALIZED + + MAX + + MAXVALUE + + MEMBER + + MERGE + + MIN + + MINVALUE + + MODE + + MODIFY + + MOVEMENT + + NAMES - NONE - - NOORDER - - NOTHING - - NOTNULL - - NOVALIDATE - - NULLS - - NOWAIT - - OF - - OFF - - OPEN - - ORA - - ORDINALITY - - OVER - - OVERFLOW - - OVERLAPS - - OVERRIDING - - OVERWRITE - - PADDING - - PARALLEL - - PARENT - - PARSER - - PARTITION - - PARTITIONING - - PATH - - PERCENT - - PLACING - - PLAN - - PLUS - - PRECEDING - - PRIMARY - - POLICY - - PURGE - - QUERY - - QUICK - - QUIESCE - - RANGE + NAME + + NEVER + + NEXT + + K_NEXTVAL + NO + + NOCACHE + + NOKEEP + + NOLOCK + + NOMAXVALUE + + NOMINVALUE + + NONE + + NOORDER + + NOTHING + + NOTNULL + + NOVALIDATE + + NULLS + + NOWAIT + + OF + + OFF + + OPTION + + OPTIONALLY + + OPEN + + ORA + + ORDINALITY + + OUTFILE + + OVER + + OVERFLOW + + OVERLAPS + + OVERRIDING + + OVERWRITE + + PADDING + + PARALLEL + + PARENT + + PARSER + + PARTITION + + PARTITIONING - RAW - - READ + PATH + + PERCENT - REBUILD - - RECYCLEBIN - - RECURSIVE - - REFERENCES - - REFRESH - - REGEXP - - REJECT - - RESPECT - - RLIKE - - REGEXP_LIKE - - REGISTER - - REMOTE - - REMOVE - - RENAME - - REORGANIZE - - REPAIR - - REPEATABLE + PLACING + + PLAN + + PLUS + + PRECEDING + + PRIMARY + + POLICY + + PURGE + + QUERY + + QUICK + + QUIESCE + + RANGE + + RAW + + READ + + REBUILD + + RECYCLEBIN + + RECURSIVE + + REFERENCES - REPLACE - - RESET - - RESTART - - RESUMABLE - - RESUME - - RESTRICT - - RESTRICTED - - RETURN - - ROLLBACK - - ROLLUP - - ROOT - - ROW - - ROWS - - RTRIM - - SAFE_CAST - - SAFE_CONVERT - - SAVEPOINT + REFRESH + + REGEXP + + REJECT + + RESPECT + + RLIKE + + REGEXP_LIKE + + REGISTER + + REMOTE + + REMOVE + + RENAME + + REORGANIZE + + REPAIR + + REPEATABLE + + REPLACE + + RESET + + RESTART + + RESUMABLE - SCHEMA - - SEARCH - - SECURE - - SECURITY - - SEED - - SEQUENCE - - SEPARATOR - - SESSION - - SETS - - SHOW - - SHUTDOWN - - SHARE - - SIBLINGS + RESUME + + RESTRICT + + RESTRICTED + + RETURN + + ROLLBACK + + ROLLUP + + ROOT + + ROW + + ROWS + + RTRIM + + SAFE_CAST + + SAFE_CONVERT + + SAVEPOINT - SIMILAR - - SIZE - - SKIP - - SPATIAL - - STORED - - STREAM - - STRICT - - STRING - - STRUCT - - SUMMARIZE - - SUSPEND - - SWITCH - - SYMMETRIC - - SYNONYM - - SYSTEM - - SYSTEM_TIME - - SYSTEM_TIMESTAMP - - SYSTEM_VERSION - - TABLE - - TABLESPACE - - TRIGGER - - THEN - - TEMP - - K_TEXT_LITERAL - TEMPORARY - - THAN - - K_TIME_KEY_EXPR - TIMEOUT - - TO - - TRIM - - TRUNCATE - - TRY_CAST - - TRY_CONVERT - - TUMBLING - - TYPE - - UNLIMITED - - UNLOGGED - - UPDATE - - UPSERT - - UNQIESCE - - USER - - SIGNED - - K_STRING_FUNCTION_NAME - UNSIGNED - - VALIDATE - - VALIDATION - - VERBOSE - - VERSION - - VIEW - - VISIBLE - - VOLATILE - - CONCURRENTLY - - WAIT - - WITH TIES + SCHEMA + + SEARCH + + SECURE + + SECURITY + + SEED + + SEQUENCE + + SEPARATOR + + SESSION + + SETS + + SHOW + + SHUTDOWN + + SHARE + + SIBLINGS + + SIMILAR + + SIZE + + SKIP + + SPATIAL + + STALENESS + + STARTING + + STEP + + STORED + + STREAM + + STRICT + + STRING + + STRUCT + + SUMMARIZE + + SUSPEND + + SWITCH + + SYMMETRIC + + SYNONYM + + SYSTEM + + SYSTEM_TIME + + SYSTEM_TIMESTAMP + + SYSTEM_VERSION + + TABLE + + TABLESPACE + + TERMINATED + + TRIGGER + + THEN + + TEMP + + K_TEXT_LITERAL + TEMPORARY + + THAN + + K_TIME_KEY_EXPR + TIMEOUT + + TO + + TRIM + + TRUNCATE + + TRY_CAST + + TRY_CONVERT + + TUMBLING + + TYPE + + UNLIMITED + + UNLOGGED - WITHIN - - WITHOUT - - WITHOUT_ARRAY_WRAPPER - - WORK - - XML - - XMLAGG - - XMLDATA - - XMLSCHEMA - - XMLTEXT - - XSINIL - - YAML - - YES - - ZONE - + UPDATE + + UPSERT + + UNQIESCE + + USER + + SIGNED + + K_STRING_FUNCTION_NAME + UNSIGNED + + VALIDATE + + VALIDATION + + VERBOSE + + VERSION + + VIEW + + VISIBLE + + VOLATILE + + CONCURRENTLY + + WAIT + + WITH TIES + + WITHIN + + WITHOUT + + WITHOUT_ARRAY_WRAPPER + + WORK + + XML + + XMLAGG + + XMLDATA + + XMLSCHEMA + + XMLTEXT + + XSINIL + + YAML + + YES + + ZONE +

@@ -729,6 +763,7 @@ NonReservedWord
           | 'ALIGN'
           | 'ALTER'
           | 'ALWAYS'
+
           | 'ANALYSE'
           | 'ANALYZE'
           | 'APPEND_ONLY'
           | 'APPLY'
@@ -810,13 +845,17 @@ NonReservedWord
           | 'DRIVER'
           | 'DROP'
           | 'DUMP'
+
           | 'DUMPFILE'
           | 'DUPLICATE'
           | 'ELEMENTS'
+
           | 'ENCLOSED'
           | 'EMIT'
+
           | 'EMPTY'
           | 'ENABLE'
           | 'ENCODING'
           | 'ENCRYPTION'
           | 'END'
+
           | 'ESCAPED'
           | 'ENFORCED'
           | 'ENGINE'
           | 'ERROR'
@@ -835,6 +874,8 @@ NonReservedWord
           | 'EXPORT'
           | K_ISOLATION
           | 'FILTER'
+
           | 'FIELDS'
+
           | 'FILL'
           | 'FIRST'
           | 'FLUSH'
           | 'FOLLOWING'
@@ -842,6 +883,7 @@ NonReservedWord
           | 'FULLTEXT'
           | 'FUNCTION'
           | 'GRANT'
+
           | 'GROUPS'
           | 'GROUP_CONCAT'
           | 'GUARD'
           | 'HASH'
@@ -859,6 +901,7 @@ NonReservedWord
           | 'INFORMATION'
           | 'INSERT'
           | 'INTERLEAVE'
+
           | 'INTERPOLATE'
           | 'INTERPRET'
           | 'INVALIDATE'
           | 'INVERSE'
@@ -880,6 +923,7 @@ NonReservedWord
           | 'LEADING'
           | 'LESS'
           | 'LEVEL'
+
           | 'LINES'
           | 'LOCAL'
           | 'LOCK'
           | 'LOCKED'
@@ -926,9 +970,12 @@ NonReservedWord
           | 'NOWAIT'
           | 'OF'
           | 'OFF'
+
           | 'OPTION'
+
           | 'OPTIONALLY'
           | 'OPEN'
           | 'ORA'
           | 'ORDINALITY'
+
           | 'OUTFILE'
           | 'OVER'
           | 'OVERFLOW'
           | 'OVERLAPS'
@@ -1006,6 +1053,9 @@ NonReservedWord
           | 'SIZE'
           | 'SKIP'
           | 'SPATIAL'
+
           | 'STALENESS'
+
           | 'STARTING'
+
           | 'STEP'
           | 'STORED'
           | 'STREAM'
           | 'STRICT'
@@ -1022,6 +1072,7 @@ NonReservedWord
           | 'SYSTEM_VERSION'
           | 'TABLE'
           | 'TABLESPACE'
+
           | 'TERMINATED'
           | 'TRIGGER'
           | 'THEN'
           | 'TEMP'
@@ -1071,7 +1122,7 @@ NonReservedWord
           | 'ZONE'
Referenced by: -
+

====================================================================================================================== @@ -1081,7 +1132,7 @@ KeywordOrIdentifier .. raw:: html - + @@ -1099,7 +1150,9 @@ KeywordOrIdentifier STRING DATA - + + TYPE +
@@ -1111,7 +1164,8 @@ KeywordOrIdentifier
           | 'VALUE'
           | 'PUBLIC'
           | 'STRING'
-
           | 'DATA'
+
           | 'DATA'
+
           | 'TYPE'
@@ -3607,13 +3661,15 @@ Update OrderByElements - PlainLimit + PlainLimit + + OptionClause - ReturningClause + ReturningClause
- +
@@ -4148,16 +4204,18 @@ Delete WhereClause PreferringClause - - OrderByElements - - PlainLimit + + OrderByElements + + PlainLimit + + OptionClause - ReturningClause + ReturningClause
-
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? ReturningClause?
+
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? OptionClause? ReturningClause?
@@ -4170,7 +4228,7 @@ Merge .. raw:: html - + @@ -4188,11 +4246,13 @@ Merge MergeOperations - OutputClause - -
+ OutputClause + + OptionClause + +
-
Merge    ::= 'MERGE' 'INTO' TableWithAlias 'USING' FromItem 'ON' Expression MergeOperations OutputClause?
+
Referenced by:
@@ -4286,7 +4346,45 @@ MergeUpdateClause
         ::= 'UPDATE' 'SET' UpdateSets ( 'WHERE' Expression )? ( 'DELETE' 'WHERE' Expression )?
Referenced by: -
+ + + +====================================================================================================================== +MergeInsertClause +====================================================================================================================== + + +.. raw:: html + + + + + + INSERT + + ( + + ColumnList + ) + + VALUES + + ( + + SimpleExpressionList + ) + + WHERE + + Expression + +
+ + +
         ::= 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
+
+ Referenced by: +
====================================================================================================================== @@ -4296,7 +4394,7 @@ MergeWhenNotMatched .. raw:: html - + @@ -4305,34 +4403,25 @@ MergeWhenNotMatched NOT MATCHED - - AND - - Expression - THEN - - INSERT - - ( - - ColumnList - ) - - VALUES - - ( - - SimpleExpressionList - ) - - WHERE - - Expression - -
+ + BY + + S_IDENTIFIER + AND + + Expression + THEN + + MergeInsertClause + + MergeUpdateClause + DELETE + + +
-
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'AND' Expression )? 'THEN' 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
+
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'BY' S_IDENTIFIER )? ( 'AND' Expression )? 'THEN' ( MergeInsertClause | MergeUpdateClause | 'DELETE' )
Referenced by:
@@ -4424,7 +4513,7 @@ Column
+ ====================================================================================================================== @@ -4556,7 +4645,7 @@ RelObjectName
           | 'VALUES'
+ ====================================================================================================================== @@ -4783,39 +4872,45 @@ Select .. raw:: html - + WithList - FromQuery - - PlainSelect - - Values - - ParenthesedSelect - - Alias + FromQuery + + PivotQuery + + PlainSelect + + Values + + ParenthesedSelect + + Alias FromQueryFromSelect SetOperationList - OrderByElements - - LimitWithOffset - - Offset - - Fetch - - WithIsolation - -
- - + OrderByElements + + InterpolateClause + + OptionClause + + LimitWithOffset + + Offset + + Fetch + + WithIsolation + +
+ +
@@ -5501,7 +5596,7 @@ ParenthesedSelect
         ::= '(' Select ')'
+ ====================================================================================================================== @@ -5589,30 +5684,29 @@ LateralView .. raw:: html - - - - - LATERAL - - VIEW - - OUTER - - Function - - RelObjectName - AS - - RelObjectName - , - - RelObjectName - -
+ + + + + LATERAL + + VIEW + + OUTER + + Function + + RelObjectName + AS + + RelObjectName + , + + +
-
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )?
+
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )*
Referenced by:
@@ -5805,7 +5899,7 @@ PlainSelect .. raw:: html - + @@ -5837,142 +5931,157 @@ PlainSelect SQL_NO_CACHE SQL_CACHE - - AS - - STRUCT - - VALUE - - Top - - SelectItemsList - - IntoClause - FROM - - FromItem - - LateralViews - - JoinsList - FROM - - ONLY - - FromItem - - LateralViews - - JoinsList - FINAL - - KSQLWindowClause - - PreWhereClause - - WhereClause - - OracleHierarchicalQueryClause - - PreferringClause - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - Having - - GroupByColumnReferences - - Having - - Qualify - - OrderByElements - WINDOW - - RelObjectName - AS - - windowDefinition - , - - OrderByElements - - ForClause - EMIT - - CHANGES - - LimitBy - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - FOR - - NO - - KEY - - UPDATE - - KEY - - SHARE - - READ - - FETCH - - ONLY - - OF - - Table - - Wait - NOWAIT - - SKIP - - LOCKED - - SETTINGS - - UpdateSets - - OptimizeFor - INTO - - TEMP - - Table - WITH - - NO - - LOG - - -
+ + AS + + STRUCT + + VALUE + + Top + + SelectItemsList + + MySqlSelectIntoClause + + IntoClause + FROM + + FromItem + + LateralViews + + JoinsList + FROM + + ONLY + + FromItem + + LateralViews + + JoinsList + FINAL + + KSQLWindowClause + + PreWhereClause + + WhereClause + + OracleHierarchicalQueryClause + + PreferringClause + PARTITION + + BY + + ComplexExpressionList + ( + + ComplexExpressionList + ) + + Having + + GroupByColumnReferences + + Having + + Qualify + + OrderByElements + WINDOW + + RelObjectName + AS + + windowDefinition + , + + OrderByElements + + InterpolateClause + + ForClause + + OptionClause + EMIT + + CHANGES + + LimitWithOffset + BY + + ExpressionList + + Offset + + LimitWithOffset + + MySqlProcedureAnalyse + + Fetch + + WithIsolation + FOR + + NO + + KEY + + UPDATE + + KEY + + SHARE + + READ + + FETCH + + ONLY + + OF + + Table + , + + Wait + NOWAIT + + SKIP + + LOCKED + + OrderByElements + + InterpolateClause + + MySqlSelectIntoClause + SETTINGS + + UpdateSets + + OptimizeFor + INTO + + TEMP + + Table + WITH + + NO + + LOG + + +
         ::= K_SELECT 'STRAIGHT_JOIN'? Skip? First? Top? ( 'ALL' | 'DISTINCT' ( 'ON' '(' SelectItemsList ')' )? | 'DISTINCTROW' | 'UNIQUE' | 'SQL_CALC_FOUND_ROWS' | 'SQL_NO_CACHE' | 'SQL_CACHE' - )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? ForClause? ( 'EMIT' 'CHANGES' )? LimitBy? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' - ) ( 'OF' Table )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? )? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
+ )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList MySqlSelectIntoClause? IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? InterpolateClause? ForClause? OptionClause? ( 'EMIT' 'CHANGES' )? ( LimitWithOffset ( 'BY' ExpressionList )? )? Offset? LimitWithOffset? MySqlProcedureAnalyse? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' + ) ( 'OF' Table ( ',' Table )* )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? OrderByElements? InterpolateClause? )? MySqlSelectIntoClause? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
Referenced by:
@@ -5985,7 +6094,7 @@ SetOperationList .. raw:: html - + @@ -6005,22 +6114,24 @@ SetOperationList ParenthesedSelect - OrderByElements - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - -
+ OrderByElements + + InterpolateClause + + LimitWithOffset + + Offset + + LimitWithOffset + + Fetch + + WithIsolation + +
-
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
+
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? InterpolateClause? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
Referenced by:
@@ -6284,24 +6395,20 @@ SelectItem .. raw:: html - + - ConnectByPriorOperator - - XorExpression - - ConcatExpression - - Expression + ConnectByPriorOperator + + Condition - Alias + Alias
- +
         ::= ( ConnectByPriorOperator | Condition ) Alias?
@@ -6585,7 +6692,50 @@ PivotFunctionItems
         ::= FunctionItem ( ',' FunctionItem )*
Referenced by: -
+
+ + +====================================================================================================================== +PivotQuery +====================================================================================================================== + + +.. raw:: html + + + + + + PIVOT + + FromItem + ON + + ExpressionList + USING + + PivotFunctionItems + USING + + PivotFunctionItems + GROUP + + BY + + ExpressionList + GROUP + + BY + + ExpressionList + +
+ + +
         ::= 'PIVOT' FromItem ( ( 'ON' ExpressionList ( 'USING' PivotFunctionItems )? | 'USING' PivotFunctionItems ) ( 'GROUP' 'BY' ExpressionList )? | 'GROUP' 'BY' ExpressionList )
+
+ Referenced by: +
====================================================================================================================== @@ -6791,6 +6941,176 @@ IntoClause
+====================================================================================================================== +MySqlSelectIntoClause +====================================================================================================================== + + +.. raw:: html + + + + + + INTO + + OUTFILE + + S_CHAR_LITERAL + + MySqlSelectIntoOutfileTail + DUMPFILE + + S_CHAR_LITERAL + +
+ + +
         ::= 'INTO' ( 'OUTFILE' S_CHAR_LITERAL MySqlSelectIntoOutfileTail | 'DUMPFILE' S_CHAR_LITERAL )
+
+ Referenced by: +
+ + +====================================================================================================================== +MySqlProcedureAnalyse +====================================================================================================================== + + +.. raw:: html + + + + + + PROCEDURE + + ANALYSE + + ( + + S_LONG + , + + S_LONG + ) + + +
+ + +
         ::= 'PROCEDURE' 'ANALYSE' '(' ( S_LONG ( ',' S_LONG )? )? ')'
+
+ Referenced by: +
+ + +====================================================================================================================== +MySqlSelectIntoOutfileTail +====================================================================================================================== + + +.. raw:: html + + + + + + CHARACTER + + SET + + S_IDENTIFIER + BINARY + + MySqlSelectIntoFieldsClause + + MySqlSelectIntoLinesClause + +
+ + +
         ::= ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )? MySqlSelectIntoFieldsClause? MySqlSelectIntoLinesClause?
+
+ Referenced by: +
+ + +====================================================================================================================== +MySqlSelectIntoFieldsClause +====================================================================================================================== + + +.. raw:: html + + + + + + FIELDS + + COLUMNS + + TERMINATED + + BY + + S_CHAR_LITERAL + OPTIONALLY + + ENCLOSED + + BY + + S_CHAR_LITERAL + ESCAPED + + BY + + S_CHAR_LITERAL + +
+ + +
         ::= ( 'FIELDS' | 'COLUMNS' ) ( 'TERMINATED' 'BY' S_CHAR_LITERAL )? ( 'OPTIONALLY'? 'ENCLOSED' 'BY' S_CHAR_LITERAL )? ( 'ESCAPED' 'BY' S_CHAR_LITERAL )?
+
+ Referenced by: +
+ + +====================================================================================================================== +MySqlSelectIntoLinesClause +====================================================================================================================== + + +.. raw:: html + + + + + + LINES + + STARTING + + BY + + S_CHAR_LITERAL + TERMINATED + + BY + + S_CHAR_LITERAL + +
+ + +
         ::= 'LINES' ( 'STARTING' 'BY' S_CHAR_LITERAL )? ( 'TERMINATED' 'BY' S_CHAR_LITERAL )?
+
+ Referenced by: +
+ + ====================================================================================================================== ParenthesedFromItem ====================================================================================================================== @@ -6834,15 +7154,15 @@ FromItem TableFunction - Table - - ParenthesedFromItem - - ParenthesedSelect - - Pivot - - UnPivot + Table + + ParenthesedSelect + + Pivot + + UnPivot + + ParenthesedFromItem LateralSubSelect @@ -6864,14 +7184,14 @@ FromItem MySQLIndexHint - SQLServerHints + SQLServerHints
- +
+ ====================================================================================================================== @@ -6934,84 +7254,86 @@ JoinerExpression .. raw:: html - - - - - GLOBAL - - ANY - - ALL - - NATURAL - - LEFT - - SEMI - - OUTER - - ANY - - ALL - - RIGHT - - FULL - - OUTER - - ANY - - ALL - - INNER - - CROSS - - OUTER - - JoinHint - JOIN - - FETCH - - , - - OUTER - - STRAIGHT_JOIN - - APPLY - - FromItem - WITHIN - - ( - - JoinWindow - ) - - ON - - Expression - USING - - ( - - Column - , - - ) - - -
+ + + + + GLOBAL + + ANY + + ALL + + NATURAL + + LEFT + + SEMI + + OUTER + + ANY + + ALL + + RIGHT + + FULL + + OUTER + + ANY + + ALL + + INNER + + CROSS + + OUTER + + ARRAY + + JoinHint + JOIN + + FETCH + + , + + OUTER + + STRAIGHT_JOIN + + APPLY + + FromItem + WITHIN + + ( + + JoinWindow + ) + + ON + + Expression + USING + + ( + + Column + , + + ) + + +
         ::= 'GLOBAL'? ( 'ANY' | 'ALL' )? 'NATURAL'? ( 'LEFT' ( 'SEMI' | 'OUTER' | 'ANY' | 'ALL' )? | ( 'RIGHT' | 'FULL' ) ( 'OUTER' | 'ANY' | 'ALL' )? | 'INNER' | 'CROSS' - | 'OUTER' )? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
+ | 'OUTER' )? 'ARRAY'? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
@@ -7550,7 +7872,7 @@ OrderByElements
         ::= 'ORDER' 'SIBLINGS'? 'BY' OrderByElement ( ',' OrderByElement )*
+ ====================================================================================================================== @@ -7560,7 +7882,7 @@ OrderByElement .. raw:: html - + @@ -7578,22 +7900,247 @@ OrderByElement FIRST - LAST - - WITH - - ROLLUP - - -
+ LAST + + WithFill + WITH + + ROLLUP + + +
-
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? ( 'WITH' 'ROLLUP' )?
+
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? WithFill? ( 'WITH' 'ROLLUP' )?
Referenced by:
+====================================================================================================================== +WithFill +====================================================================================================================== + + +.. raw:: html + + + + + + WITH + + FILL + + FROM + + Expression + TO + + Expression + STEP + + Expression + STALENESS + + Expression + +
+ +
WithFill ::= 'WITH' 'FILL' ( 'FROM' Expression )? ( 'TO' Expression )? ( 'STEP' Expression )? ( 'STALENESS' Expression )?
+
+ Referenced by: +
+ + +====================================================================================================================== +InterpolateClause +====================================================================================================================== + + +.. raw:: html + + + + + + INTERPOLATE + + ( + + InterpolateElement + , + + ) + + +
+ + +
         ::= 'INTERPOLATE' ( '(' ( InterpolateElement ( ',' InterpolateElement )* )? ')' )?
+
+ + +====================================================================================================================== +InterpolateElement +====================================================================================================================== + + +.. raw:: html + + + + + + Column + AS + + Expression + +
+ + +
         ::= Column ( 'AS' Expression )?
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionHintParameter +====================================================================================================================== + + +.. raw:: html + + + + + + UserVariable + UNKNOWN + + Expression + +
+ + +
         ::= UserVariable 'UNKNOWN'
+
           | Expression
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionClause +====================================================================================================================== + + +.. raw:: html + + + + + + OPTION + + ( + + OptionHint + , + + ) + + +
+ + +
         ::= 'OPTION' '(' OptionHint ( ',' OptionHint )* ')'
+
+ + +====================================================================================================================== +OptionHint +====================================================================================================================== + + +.. raw:: html + + + + + + OptionHintName + = + + Expression + ( + + OptionHintParameter + , + + ) + + +
+ + +
         ::= OptionHintName ( '='? Expression | '(' OptionHintParameter ( ',' OptionHintParameter )* ')' )?
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionHintName +====================================================================================================================== + + +.. raw:: html + + + + + + S_IDENTIFIER + + NonReservedWord + JOIN + + FOR + + ORDER + + UNION + + EXCEPT + + INTERSECT + + USE + + OPTIMIZE + + FORCE + + UNKNOWN + + +
+ + +
         ::= ( S_IDENTIFIER | NonReservedWord | 'JOIN' | 'FOR' | 'ORDER' | 'UNION' | 'EXCEPT' | 'INTERSECT' | 'USE' | 'OPTIMIZE' + | 'FORCE' | 'UNKNOWN' )+
+
+ Referenced by: +
+ + ====================================================================================================================== JdbcParameter ====================================================================================================================== @@ -7617,7 +8164,7 @@ JdbcParameter
         ::= ( '?' | S_PARAMETER ) S_LONG?
+ ====================================================================================================================== @@ -7646,7 +8193,7 @@ LimitWithOffset
         ::= 'LIMIT' ( ParenthesedSelect | Expression ) ( ',' Expression )?
+ ====================================================================================================================== @@ -7675,30 +8222,6 @@ PlainLimit -====================================================================================================================== -LimitBy -====================================================================================================================== - - -.. raw:: html - - - - - - LimitWithOffset - BY - - ExpressionList - -
- - -
- Referenced by: -
- - ====================================================================================================================== Offset ====================================================================================================================== @@ -7822,7 +8345,7 @@ Top .. raw:: html - + @@ -7834,19 +8357,19 @@ Top : S_IDENTIFIER - ( - - AdditiveExpression - ) - - PERCENT - - WITH TIES - - -
+ ( + + SimpleExpression + ) + + PERCENT + + WITH TIES + + +
-
Top      ::= 'TOP' ( S_LONG | JdbcParameter | ':' S_IDENTIFIER? | '(' AdditiveExpression ')' ) 'PERCENT'? 'WITH TIES'?
+
Top      ::= 'TOP' ( S_LONG | JdbcParameter | ':' S_IDENTIFIER? | '(' SimpleExpression ')' ) 'PERCENT'? 'WITH TIES'?
Referenced by:
@@ -7915,19 +8438,19 @@ Expression .. raw:: html - + - - - XorExpression - -
+ + + Condition + +
-
         ::= XorExpression
+
         ::= Condition
+
====================================================================================================================== @@ -7937,80 +8460,48 @@ XorExpression .. raw:: html - - - - - OrExpression - XOR - - -
- - -
         ::= OrExpression ( 'XOR' OrExpression )*
-
- - -====================================================================================================================== -OrExpression -====================================================================================================================== - - -.. raw:: html - - - - - - AndExpression - OR - - -
+ + + + + AndChain + OR + + XOR + + +
- -
         ::= AndExpression ( 'OR' AndExpression )*
+ +
         ::= AndChain ( ( 'OR' | 'XOR' ) AndChain )*
Referenced by: -
+
====================================================================================================================== -AndExpression +AndChain ====================================================================================================================== .. raw:: html - + - - Condition - NOT - - ! - - ( - - XorExpression - ) - + + Condition AND && - - -
+ + +
- -
         ::= ( Condition | ( 'NOT' | '!' )? '(' XorExpression ')' ) ( ( 'AND' | '&&' ) ( Condition | ( 'NOT' | '!' )? '(' XorExpression ')' ) )*
+
AndChain ::= Condition ( ( 'AND' | '&&' ) Condition )*
Referenced by: -
+
====================================================================================================================== @@ -8070,7 +8561,7 @@ Condition
+ ====================================================================================================================== @@ -8102,9 +8593,9 @@ RegularConditionRHS OP_NOTEQUALSSTANDARD - OP_NOTEQUALSBANG - - OP_NOTEQUALSHAT + OP_NOTEQUALSBANG + ^= + *= =* @@ -8133,9 +8624,10 @@ RegularConditionRHS ?| - ?& - - OP_CONCAT + ?& + + || + - -# @@ -8154,13 +8646,14 @@ RegularConditionRHS + ) - +
-
         ::= ( '(' '+' ')' )? ( '>' | '<' | '=' | OP_GREATERTHANEQUALS | OP_MINORTHANEQUALS | OP_NOTEQUALSSTANDARD | OP_NOTEQUALSBANG | OP_NOTEQUALSHAT | '*=' | '=*' | '&&' | '&>' | '<&' | '@@' | '~' | '~*' | '!~' | '!~*' | '@>' | '<@' - | '?' | '?|' | '?&' | OP_CONCAT | '-' | '-#' | '<->' | '<#>' | '<=>' ) 'PRIOR'? ComparisonItem ( '(' '+' ')' )?
+
         ::= ( '(' '+' ')' )? ( '>' | '<' | '=' | OP_GREATERTHANEQUALS | OP_MINORTHANEQUALS | OP_NOTEQUALSSTANDARD | OP_NOTEQUALSBANG | '^=' | '*=' | '=*' | '&&' | '&>' | '<&' | '@@' | '~' | '~*' | '!~' | '!~*' | '@>' + | '<@' | '?' | '?|' | '?&' | '||' | '-' | '-#' | '<->' | '<#>' | '<=>' ) 'PRIOR'? + ComparisonItem ( '(' '+' ')' )?
Referenced by:
@@ -8246,7 +8739,7 @@ InExpression .. raw:: html - + @@ -8262,14 +8755,16 @@ InExpression IN - S_CHAR_LITERAL - - PrimaryExpression - -
+ S_CHAR_LITERAL + + ParenthesedSelect + + PrimaryExpression + +
-
         ::= ( '(' '+' ')' )? 'GLOBAL'? 'NOT'? 'IN' ( S_CHAR_LITERAL | PrimaryExpression )
+
         ::= ( '(' '+' ')' )? 'GLOBAL'? 'NOT'? 'IN' ( S_CHAR_LITERAL | ParenthesedSelect | PrimaryExpression )
Referenced by:
@@ -8643,7 +9138,7 @@ ExpressionList
           | ParenthesedExpressionList
+ ====================================================================================================================== @@ -8671,7 +9166,7 @@ ParenthesedExpressionList
         ::= '(' ( ComplexExpressionList | SimpleExpressionList )? ')'
+ ====================================================================================================================== @@ -8698,7 +9193,7 @@ SimpleExpressionList
         ::= SimpleExpression ( ',' ( LambdaExpression | SimpleExpression ) )*
+ ====================================================================================================================== @@ -8722,7 +9217,7 @@ ColumnList
         ::= Column ( ',' Column )*
+ ====================================================================================================================== @@ -8830,28 +9325,22 @@ ComparisonItem .. raw:: html - + - AnyComparisonExpression - - SimpleExpression - - ParenthesedExpressionList - - RowConstructor - - PrimaryExpression + AnyComparisonExpression + + RowConstructor + + SimpleExpression
         ::= AnyComparisonExpression
-
           | SimpleExpression
-
           | ParenthesedExpressionList
           | RowConstructor
-
           | PrimaryExpression
+
           | SimpleExpression
Referenced by:
@@ -8892,158 +9381,116 @@ SimpleExpression .. raw:: html - + UserVariable = - := - - ConcatExpression - -
+ := + + PrimaryExpression + +
-
         ::= ( UserVariable ( '=' | ':=' ) )? ConcatExpression
+
         ::= ( UserVariable ( '=' | ':=' ) )? PrimaryExpression
+
====================================================================================================================== -ConcatExpression +CharacterSetIntroducedString ====================================================================================================================== .. raw:: html - - - - - BitwiseAndOr - - OP_CONCAT - -
- - -
         ::= BitwiseAndOr ( OP_CONCAT BitwiseAndOr )*
-
- Referenced by: -
- - -====================================================================================================================== -BitwiseAndOr -====================================================================================================================== - - -.. raw:: html - - - - - - AdditiveExpression - | - - & - - << - - >> - - -
- - -
         ::= AdditiveExpression ( ( '|' | '&' | '<<' | '>>' ) AdditiveExpression )*
-
- Referenced by: -
- - -====================================================================================================================== -AdditiveExpression -====================================================================================================================== - - -.. raw:: html - - - - - - MultiplicativeExpression - + - - - - - -
+ + + + + S_IDENTIFIER + + S_CHAR_LITERAL + +
- -
         ::= MultiplicativeExpression ( ( '+' | '-' ) MultiplicativeExpression )*
+ +
         ::= S_IDENTIFIER S_CHAR_LITERAL
Referenced by: -
+
====================================================================================================================== -MultiplicativeExpression +MulChain ====================================================================================================================== .. raw:: html - - - - - BitwiseXor - * - - / - - DIV - - % - - -
+ + + + + PrimaryExpression + * + + / + + % + + ^ + + DIV + + +
- -
         ::= BitwiseXor ( ( '*' | '/' | 'DIV' | '%' ) BitwiseXor )*
+
MulChain ::= PrimaryExpression ( ( '*' | '/' | '%' | '^' | 'DIV' ) PrimaryExpression )*
Referenced by: -
+
====================================================================================================================== -BitwiseXor +AddChain ====================================================================================================================== .. raw:: html - - - - - PrimaryExpression - ^ - - -
+ + + + + MulChain + + + + - + + || + + & + + << + + >> + + | + + | + + +
- -
         ::= PrimaryExpression ( '^' PrimaryExpression )*
+
AddChain ::= MulChain ( ( '+' | '-' | '||' | '&' | '<<' | '>>' | '|' '|'? ) MulChain )*
- Referenced by: -
+ Not referenced by any. +
====================================================================================================================== @@ -9082,7 +9529,7 @@ PrimaryExpression .. raw:: html - + @@ -9100,145 +9547,155 @@ PrimaryExpression CaseWhenExpression - CharacterPrimary - - ImplicitCast - - JdbcParameter - - JdbcNamedParameter - - UserVariable + CharacterPrimary + + MapExpression + + ImplicitCast + + JdbcParameter + + JdbcNamedParameter - NumericBind - - ExtractExpression - - XMLSerializeExpr - - JsonFunction - - JsonAggregateFunction - - FullTextSearch - - CastExpression - - Function - - AnalyticExpression - - DateUnitExpression - - IntervalExpression - - S_DOUBLE - - S_LONG - - S_HEX - - AllColumns - - AllTableColumns - - K_TIME_KEY_EXPR - CURRENT - - DateTimeLiteralExpression - - StructType - ARRAY - - < - - ColDataType - > - - ArrayConstructor - - NextValExpression - - ConnectByRootOperator + UserVariable + + NumericBind + + ExtractExpression + + XMLSerializeExpr + + JsonFunction + + JsonAggregateFunction + + FullTextSearch + + CastExpression + + Function + + AnalyticExpression + + DateUnitExpression + + IntervalExpression + + S_DOUBLE + + S_LONG + + S_HEX + + AllColumns + + AllTableColumns + + K_TIME_KEY_EXPR + CURRENT + + DateTimeLiteralExpression + + StructType + ARRAY + + < + + ColDataType + > + + ArrayConstructor + + NextValExpression - ConnectByPriorOperator - ALL - - Column - ( - - + - - ) - - TRUE - - FALSE - - S_CHAR_LITERAL - {d - - {t - - {ts - - S_CHAR_LITERAL - } - - Select - - ParenthesedSelect - - ParenthesedExpressionList - -> - - Expression - . - - RelObjectName - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - - S_IDENTIFIER - - IntervalExpressionWithoutInterval - - ArrayExpression - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - - JsonExpression - AT - - K_DATETIMELITERAL - ZONE - - PrimaryExpression - -
+ ConnectByRootOperator + + ConnectByPriorOperator + + KeyExpression + ALL + + CharacterSetIntroducedString + + Column + ( + + + + + ) + + TRUE + + FALSE + + S_CHAR_LITERAL + {d + + {t + + {ts + + S_CHAR_LITERAL + } + + Select + + ParenthesedSelect + + ParenthesedExpressionList + -> + + Expression + . + + * + + . + + RelObjectName + COLLATE + + S_CHAR_LITERAL + + S_QUOTED_IDENTIFIER + + S_IDENTIFIER + + IntervalExpressionWithoutInterval + + ArrayExpression + :: + + ColDataType + -> + + : + + ->> + + #> + + #>> + + Expression + + SimpleExpression + + JsonExpression + AT + + K_DATETIMELITERAL + ZONE + + PrimaryExpression + +
-
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | 'ALL' | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
+
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | MapExpression | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | KeyExpression | 'ALL' | CharacterSetIntroducedString | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL+ | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' '*' )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
+
====================================================================================================================== @@ -9289,6 +9746,30 @@ ConnectByPriorOperator +====================================================================================================================== +KeyExpression +====================================================================================================================== + + +.. raw:: html + + + + + + KEY + + Expression + +
+ + +
         ::= 'KEY' Expression
+
+ Referenced by: +
+ + ====================================================================================================================== NextValExpression ====================================================================================================================== @@ -9336,7 +9817,7 @@ JdbcNamedParameter
         ::= ( ':' | '&' ) IdentifierChain
+ ====================================================================================================================== @@ -9414,7 +9895,7 @@ UserVariable
         ::= S_AT_IDENTIFIER IdentifierChain2
+ ====================================================================================================================== @@ -9545,6 +10026,60 @@ ArrayConstructor +====================================================================================================================== +MapExpression +====================================================================================================================== + + +.. raw:: html + + + + + + DATA_TYPE + { + + MapKeyExpression + : + + Expression + , + + } + + +
+ + +
         ::= DATA_TYPE '{' ( MapKeyExpression ':' Expression ( ',' MapKeyExpression ':' Expression )* )? '}'
+
+ Referenced by: +
+ + +====================================================================================================================== +MapKeyExpression +====================================================================================================================== + + +.. raw:: html + + + + + + Expression + +
+ + +
         ::= Expression
+
+ Referenced by: +
+ + ====================================================================================================================== StructParameters ====================================================================================================================== @@ -9842,7 +10377,7 @@ JsonKeyword
         ::= S_IDENTIFIER
+ ====================================================================================================================== @@ -9903,27 +10438,30 @@ JsonValueOnResponseBehavior .. raw:: html - + ERROR NULL - - DEFAULT - - Expression + + EMPTY + + DEFAULT + + Expression
         ::= 'ERROR'
           | 'NULL'
+
           | 'EMPTY'
           | 'DEFAULT' Expression
+ ====================================================================================================================== @@ -9933,28 +10471,35 @@ JsonQueryOnResponseBehavior .. raw:: html - + ERROR - NULL - - S_IDENTIFIER - ARRAY - - JsonKeyword - -
+ NULL + + TRUE + + FALSE + + EMPTY + + ARRAY + + JsonKeyword + +
         ::= 'ERROR'
           | 'NULL'
-
           | S_IDENTIFIER ( 'ARRAY' | JsonKeyword )
+
           | 'TRUE'
+
           | 'FALSE'
+
           | 'EMPTY' ( 'ARRAY' | JsonKeyword )?
Referenced by: -
+
====================================================================================================================== @@ -10055,11 +10600,11 @@ JsonValueBody RETURNING ColDataType - - JsonValueOnResponseBehavior - ON - - JsonKeyword + + JsonValueOnResponseBehavior + ON + + EMPTY JsonValueOnResponseBehavior ON @@ -10067,12 +10612,12 @@ JsonValueBody ERROR ) - +
-
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
+
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' 'EMPTY' )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
Referenced by:
@@ -10085,7 +10630,7 @@ JsonQueryBody .. raw:: html - + @@ -10130,25 +10675,25 @@ JsonQueryBody STRING JsonQueryOnResponseBehavior - ON - - JsonKeyword - - JsonQueryOnResponseBehavior - ON - - ERROR + ON + + EMPTY + + JsonQueryOnResponseBehavior + ON + + ERROR Expression , - - ) - - -
+ + ) + + +
-
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
+
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
Referenced by:
@@ -10312,7 +10857,7 @@ IntervalExpression .. raw:: html - + @@ -10326,16 +10871,16 @@ IntervalExpression S_CHAR_LITERAL - Expression - - S_IDENTIFIER - - K_DATE_LITERAL - -
+ Expression + + IntervalQualifier + + S_IDENTIFIER + +
-
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( S_IDENTIFIER | K_DATE_LITERAL )?
+
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( IntervalQualifier | S_IDENTIFIER )?
Referenced by:
@@ -10348,21 +10893,59 @@ IntervalExpressionWithoutInterval .. raw:: html - + - - - K_DATE_LITERAL - -
+ + + IntervalQualifier + +
-
         ::= K_DATE_LITERAL
+
         ::= IntervalQualifier
Referenced by:
+====================================================================================================================== +IntervalQualifier +====================================================================================================================== + + +.. raw:: html + + + + + + K_DATE_LITERAL + ( + + S_LONG + , + + S_LONG + ) + + TO + + K_DATE_LITERAL + ( + + S_LONG + ) + + +
+ + +
         ::= K_DATE_LITERAL ( '(' S_LONG ( ',' S_LONG )? ')' )? ( 'TO' K_DATE_LITERAL ( '(' S_LONG ')' )? )?
+
+ + ====================================================================================================================== KeepExpression ====================================================================================================================== @@ -10521,30 +11104,66 @@ WindowElement .. raw:: html - + ROWS RANGE - - BETWEEN - - WindowOffset - AND - - WindowOffset - -
+ + GROUPS + + BETWEEN + + WindowOffset + AND + + WindowOffset + + FrameExclusion + +
-
         ::= ( 'ROWS' | 'RANGE' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset
+
         ::= ( 'ROWS' | 'RANGE' | 'GROUPS' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset FrameExclusion?
+====================================================================================================================== +FrameExclusion +====================================================================================================================== + + +.. raw:: html + + + + + + EXCLUDE + + CURRENT + + ROW + + GROUP + + NO + + S_IDENTIFIER + +
+ + +
         ::= 'EXCLUDE' ( 'CURRENT' 'ROW' | 'GROUP' | 'NO'? S_IDENTIFIER )
+
+ Referenced by: +
+ + ====================================================================================================================== WindowOffset ====================================================================================================================== @@ -10672,17 +11291,19 @@ CastExpression ) - ColDataType + ColDataType + ARRAY + FORMAT S_CHAR_LITERAL ) - +
-
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
+
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType 'ARRAY'? ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
Referenced by:
@@ -10835,7 +11456,7 @@ FullTextSearch .. raw:: html - + @@ -10848,29 +11469,25 @@ FullTextSearch AGAINST - ( - - S_CHAR_LITERAL - - JdbcParameter - - JdbcNamedParameter - IN NATURAL LANGUAGE MODE - - IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION - - IN BOOLEAN MODE - - WITH QUERY EXPANSION - - ) - - -
+ ( + + SimpleExpression + IN NATURAL LANGUAGE MODE + + IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION + + IN BOOLEAN MODE + + WITH QUERY EXPANSION + + ) + + +
-
         ::= 'MATCH' '(' ColumnList ')' 'AGAINST' '(' ( S_CHAR_LITERAL | JdbcParameter | JdbcNamedParameter ) ( 'IN NATURAL LANGUAGE MODE' | 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION' - | 'IN BOOLEAN MODE' | 'WITH QUERY EXPANSION' )? ')'
+
         ::= 'MATCH' '(' ColumnList ')' 'AGAINST' '(' SimpleExpression ( 'IN NATURAL LANGUAGE MODE' | 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION' | + 'IN BOOLEAN MODE' | 'WITH QUERY EXPANSION' )? ')'
Referenced by:
@@ -10932,7 +11549,7 @@ Function
           | InternalFunction
+
====================================================================================================================== @@ -10971,7 +11588,7 @@ InternalFunction .. raw:: html - + @@ -10979,77 +11596,101 @@ InternalFunction RelObjectNames ( - - DISTINCT - - ALL - - UNIQUE - - TABLE - - ExpressionList - - OrderByElements - ON - - OVERFLOW - - TRUNCATE - - ERROR - - S_CHAR_LITERAL - WITH - - WITHOUT - - COUNT - - Select - HAVING - - MIN - - MAX - - Expression - IGNORE - - RESPECT - - NULLS - - PlainLimit - ) - - ( - - ExpressionList - ) - - . - - Function - - Column - IGNORE - - RESPECT - - NULLS - - KeepExpression - -
+ + DISTINCT + + ALL + + UNIQUE + + FunctionArgumentList + + Select + HAVING + + MIN + + MAX + + Expression + IGNORE + + RESPECT + + NULLS + + PlainLimit + ) + + ( + + ExpressionList + ) + + S_DOUBLE + . + + Function + + Column + IGNORE + + RESPECT + + NULLS + + KeepExpression + +
-
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )? | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
+
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( FunctionArgumentList | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( S_DOUBLE | '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
Referenced by:
+====================================================================================================================== +FunctionArgumentList +====================================================================================================================== + + +.. raw:: html + + + + + + TABLE + + ExpressionList + + OrderByElements + ON + + OVERFLOW + + TRUNCATE + + ERROR + + S_CHAR_LITERAL + WITH + + WITHOUT + + COUNT + + +
+ + +
         ::= 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )?
+
+ Referenced by: +
+ + ====================================================================================================================== XMLSerializeExpr ====================================================================================================================== @@ -11126,29 +11767,35 @@ JsonTableOnEmptyBehavior .. raw:: html - + ERROR NULL - - DEFAULT - - Expression - - S_IDENTIFIER - - JsonKeyword - ARRAY - + + TRUE + + FALSE + + DEFAULT + + Expression + + S_IDENTIFIER + + JsonKeyword + ARRAY +
         ::= 'ERROR'
           | 'NULL'
+
           | 'TRUE'
+
           | 'FALSE'
           | 'DEFAULT' Expression
           | S_IDENTIFIER ( JsonKeyword | 'ARRAY' )?
@@ -11223,7 +11870,7 @@ JsonTableColumnDefinition .. raw:: html - + @@ -11238,42 +11885,54 @@ JsonTableColumnDefinition JsonTableColumnsClause RelObjectName - FOR - - JsonKeyword - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - PATH - - Expression - - JsonTableWrapperClause - - JsonTableQuotesClause - - JsonTableOnEmptyBehavior - ON - - JsonKeyword - - JsonValueOnResponseBehavior - ON - - ERROR - - -
+ FOR + + ORDINALITY + + ColDataType + FORMAT + + JSON + + ENCODING + + JsonEncoding + + JsonKeyword + + JsonKeyword + EXISTS + + JsonTableWrapperClause + PATH + + Expression + + JsonTableWrapperClause + + JsonTableQuotesClause + + JsonTableOnEmptyBehavior + ON + + EMPTY + + JsonQueryOnResponseBehavior + ON + + ERROR + + JsonTableOnEmptyBehavior + ON + + EMPTY + + +
         ::= JsonKeyword 'PATH'? Expression ( 'AS' RelObjectName )? JsonTableColumnsClause
-
           | RelObjectName ( 'FOR' JsonKeyword | ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? )
+
           | RelObjectName ( 'FOR' 'ORDINALITY' | ColDataType? ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( JsonKeyword JsonKeyword )? 'EXISTS'? JsonTableWrapperClause? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? )
Referenced by:
@@ -11409,22 +12068,94 @@ JsonTableOnErrorClause .. raw:: html - + - ERROR - - S_IDENTIFIER - ON - - ERROR - - -
+ ERROR + + EMPTY + + TRUE + + FALSE + + NULL + + ON + + ERROR + + +
-
         ::= ( 'ERROR' | S_IDENTIFIER ) 'ON' 'ERROR'
+
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'ERROR'
+
+ Referenced by: +
+ + +====================================================================================================================== +JsonTableOnEmptyClause +====================================================================================================================== + + +.. raw:: html + + + + + + ERROR + + EMPTY + + TRUE + + FALSE + + NULL + + ON + + EMPTY + + +
+ + +
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'EMPTY'
+
+ Referenced by: +
+ + +====================================================================================================================== +JsonTableParsingTypeClause +====================================================================================================================== + + +.. raw:: html + + + + + + TYPE + + ( + + STRICT + + JsonKeyword + ) + + +
+ + +
         ::= 'TYPE' '(' ( 'STRICT' | JsonKeyword ) ')'
Referenced by:
@@ -11437,37 +12168,142 @@ JsonTableBody .. raw:: html - + + + + + ( + + Expression + FORMAT + + JSON + + , + + Expression + AS + + RelObjectName + + JsonKeyword + + JsonTablePassingClause + , + + JsonTableOnErrorClause + + JsonTableParsingTypeClause + + JsonTableOnEmptyClause + + JsonTableColumnsClause + + JsonTablePlanClause + + JsonTableOnErrorClause + ) + + +
+ + + +
+ Referenced by: +
+ + +====================================================================================================================== +XmlTablePassingClause +====================================================================================================================== + + +.. raw:: html + + + + + + Expression + AS + + RelObjectName + +
+ + +
         ::= Expression ( 'AS' RelObjectName )?
+
+ Referenced by: +
+ + +====================================================================================================================== +XmlTableColumnDefinition +====================================================================================================================== + + +.. raw:: html + + + + + + RelObjectName + FOR + + ORDINALITY + + ColDataType + PATH + + Expression + DEFAULT + + Expression + +
+ + +
         ::= RelObjectName ( 'FOR' 'ORDINALITY' | ColDataType ( 'PATH' Expression )? ( 'DEFAULT' Expression )? )
+
+ Referenced by: +
+ + +====================================================================================================================== +XmlTableBody +====================================================================================================================== + + +.. raw:: html + + ( - Expression - , - - Expression - AS - - RelObjectName - - JsonKeyword - - JsonTablePassingClause - , - - JsonTableColumnsClause - - JsonTablePlanClause - - JsonTableOnErrorClause - ) - - -
- - - + Expression + + S_IDENTIFIER + + XmlTablePassingClause + , + + COLUMNS + + XmlTableColumnDefinition + , + + ) + + +
+ + +
Referenced by:
@@ -11480,33 +12316,71 @@ TableFunction .. raw:: html - + - LATERAL - - JsonKeyword - - JsonTableBody - - Function - WITH - - OFFSET - - ORDINALITY - - -
+ LATERAL + + ROWS + + FROM + + RowsFromFunctionList + + JsonKeyword + + JsonTableBody + + S_IDENTIFIER + + XmlTableBody + + Function + WITH + + OFFSET + + ORDINALITY + + +
-
         ::= 'LATERAL'? ( JsonKeyword JsonTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
+
         ::= 'LATERAL'? ( 'ROWS' 'FROM' RowsFromFunctionList | JsonKeyword JsonTableBody | S_IDENTIFIER XmlTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
Referenced by:
+====================================================================================================================== +RowsFromFunctionList +====================================================================================================================== + + +.. raw:: html + + + + + + ( + + Function + , + + ) + + +
+ + +
         ::= '(' Function ( ',' Function )* ')'
+
+ Referenced by: +
+ + ====================================================================================================================== ColumnNamesWithParamsList ====================================================================================================================== @@ -11544,22 +12418,22 @@ IndexColumnWithParams .. raw:: html - - - - - RelObjectName - ( - - Expression - ) - - CreateParameter - -
+ + + + + RelObjectName + ( + + Expression + ) + + CreateParameter + +
-
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter?
+
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter*
@@ -11621,7 +12495,7 @@ CreateIndex .. raw:: html - + @@ -11645,20 +12519,60 @@ CreateIndex ON Table - - IndexColumnsWithParamsList + + IndexColumnsWithParamsList + INCLUDE + + ColumnsNamesList - CreateParameter + CreateParameter
-
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList CreateParameter*
+
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList ( 'INCLUDE' ColumnsNamesList )? CreateParameter*
Referenced by:
+====================================================================================================================== +ColumnDefinitionParameter +====================================================================================================================== + + +.. raw:: html + + + + + + CreateParameter + DEFERRABLE + + DISABLE + + ENABLE + + NOVALIDATE + + VALIDATE + + +
+ + +
         ::= CreateParameter
+
           | 'DEFERRABLE'
+
           | 'DISABLE'
+
           | 'ENABLE'
+
           | 'NOVALIDATE'
+
           | 'VALIDATE'
+
+ Referenced by: +
+ + ====================================================================================================================== ColumnDefinition ====================================================================================================================== @@ -11666,20 +12580,20 @@ ColumnDefinition .. raw:: html - + RelObjectName - ColDataType - - CreateParameter - -
+ ColDataType + + ColumnDefinitionParameter + +
- +
@@ -11762,6 +12676,38 @@ PathSpecification
+====================================================================================================================== +CreateDatabase +====================================================================================================================== + + +.. raw:: html + + + + + + DATABASE + + IF + + NOT + + EXISTS + + RelObjectName + + captureRest + +
+ + +
         ::= 'DATABASE' ( 'IF' 'NOT' 'EXISTS' )? RelObjectName captureRest?
+
+ Referenced by: +
+ + ====================================================================================================================== CreateTableConstraint ====================================================================================================================== @@ -11772,10 +12718,10 @@ CreateTableConstraint - - INDEX - - UNIQUE + + UNIQUE + + INDEX FULLTEXT @@ -11814,12 +12760,12 @@ CreateTableConstraint Expression ) - +
-
         ::= ( 'INDEX' | 'UNIQUE'? ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
+
         ::= 'UNIQUE'? ( 'INDEX' | ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
           | ( 'CONSTRAINT' RelObjectName )? ( ( 'PRIMARY' 'KEY' | 'UNIQUE' 'KEY'? ) ColumnNamesWithParamsList CreateParameter* | ForeignKeySpec | CheckConstraintSpec )
           | 'EXCLUDE' 'WHERE' ( '(' Expression ')' )*
@@ -11937,7 +12883,7 @@ DataType .. raw:: html - + @@ -11991,11 +12937,15 @@ DataType S_LONG MAX - - , - - S_LONG - ) + + BYTE + + CHAR + + , + + S_LONG + ) K_TEXT_LITERAL ARRAY @@ -12004,15 +12954,15 @@ DataType ColDataType > - - -
+ + +
           | 'ARRAY' '<' ColDataType '>'
           | ( K_DATETIMELITERAL | DT_ZONE | DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | 'CHAR' | 'JSON' | 'STRING' ) ( DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( ',' S_LONG )? ')' )?
+ 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? ( ',' S_LONG )? ')' )?
Referenced by:
@@ -12025,7 +12975,7 @@ ColDataType .. raw:: html - + @@ -12068,24 +13018,27 @@ ColDataType DATA - NAME - - . - - ColDataType - ( - - S_LONG - MAX - - BYTE - - CHAR - - S_CHAR_LITERAL - - S_IDENTIFIER - CHAR + NAME + + IntervalQualifier + . + + ColDataType + ( + + ColDataType + + S_LONG + MAX + + BYTE + + CHAR + + S_CHAR_LITERAL + + S_IDENTIFIER + CHAR , @@ -12095,23 +13048,22 @@ ColDataType S_LONG ] - - CHARACTER - - SET - - S_IDENTIFIER - BINARY - - -
+ + CHARACTER + + SET + + S_IDENTIFIER + BINARY + + +
-
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) ( - '.' ColDataType )? ) ( '(' ( ( ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
+
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) IntervalQualifier? ( '.' ColDataType )? ) ( '(' ( ( ColDataType | ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
+
====================================================================================================================== @@ -12396,20 +13348,23 @@ AlterExpressionUsingIndex .. raw:: html - + USING INDEX - - RelObjectName - -
+ + RelObjectName + TABLESPACE + + RelObjectName + +
-
         ::= 'USING' 'INDEX'? RelObjectName
+
         ::= 'USING' 'INDEX'? ( RelObjectName? 'TABLESPACE' )? RelObjectName
@@ -12477,7 +13432,7 @@ CreateParameter .. raw:: html - + @@ -12607,39 +13562,57 @@ CreateParameter SAMPLE ALWAYS - - = - - DEFAULT - - AS - - CHECK - - ( - - Expression - ) - - + - - - - - S_LONG - - S_DOUBLE - - AList - CHARACTER - - SET - - ARRAY - - ArrayConstructor - :: - - ColDataType + + VISIBLE + + INVISIBLE + + KEY_BLOCK_SIZE + + ALGORITHM + + LOCK + + NONE + + PARSER + + FULLTEXT + + SPATIAL + + = + + DEFAULT + + AS + + CHECK + + ( + + Expression + ) + + + + + - + + S_LONG + + S_DOUBLE + + AList + CHARACTER + + SET + + ARRAY + + ArrayConstructor + :: + + ColDataType
@@ -12697,6 +13670,15 @@ CreateParameter
           | 'MATERIALIZED'
           | 'SAMPLE'
           | 'ALWAYS'
+
           | 'VISIBLE'
+
           | 'INVISIBLE'
+
           | 'KEY_BLOCK_SIZE'
+
           | 'ALGORITHM'
+
           | 'LOCK'
+
           | 'NONE'
+
           | 'PARSER'
+
           | 'FULLTEXT'
+
           | 'SPATIAL'
           | '='
           | ( 'DEFAULT' | 'AS' | 'CHECK' ) ( '(' Expression ')' )?
           | ( '+' | '-' )? S_LONG
@@ -12707,7 +13689,7 @@ CreateParameter
           | '::' ColDataType
+
====================================================================================================================== @@ -12833,7 +13815,7 @@ ColumnsNamesList
         ::= '(' ColumnsNamesListItem ( ',' ColumnsNamesListItem )* ')'
+
====================================================================================================================== @@ -12900,7 +13882,7 @@ Drop .. raw:: html - + @@ -12935,15 +13917,23 @@ Drop CASCADE RESTRICT - - ON - - Table + + ALGORITHM + + NONE + + = + + ON + + Table + LOCK +
Drop     ::= 'DROP' 'MATERIALIZED'? ( S_IDENTIFIER | 'TEMPORARY'? 'TABLE' | 'INDEX' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'FUNCTION' ) - ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ON' Table )*
+ ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ALGORITHM' | 'NONE' | '=' | 'ON' Table | 'LOCK' )*
Referenced by:
@@ -13700,16 +14690,16 @@ AlterExpressionAddAlterModify COLUMNS IF - - NOT - - EXISTS - - AlterExpressionColumnChanges - - AlterExpressionColumnDataType - - AlterExpressionColumnDropNotNull + + NOT + + EXISTS + + AlterExpressionColumnChanges + + AlterExpressionColumnDataType + + AlterExpressionColumnDropNotNull AlterExpressionColumnChanges UNIQUE @@ -13736,12 +14726,12 @@ AlterExpressionAddAlterModify ENFORCED - AlterExpressionAddConstraint + AlterExpressionAddConstraint
-
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT' 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
+
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT'? 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
Referenced by:
@@ -14521,7 +15511,7 @@ Create .. raw:: html - + @@ -14533,28 +15523,30 @@ Create CreateFunctionStatement - CreateSchema + CreateSchema - CreateSequence - - CreateSynonym - - CreateTable - - CreateView - - CreatePolicy - TRIGGER - - DOMAIN - - captureRest - - CreateIndex + CreateDatabase + + CreateSequence + + CreateSynonym + + CreateTable + + CreateView + + CreatePolicy + TRIGGER + + DOMAIN + + captureRest + + CreateIndex
-
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
+
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateDatabase | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
Referenced by:
@@ -15175,7 +16167,7 @@ WHITESPACE
         ::= [ #x9#xD#xA]
+
====================================================================================================================== @@ -15423,7 +16415,7 @@ K_DATE_LITERAL
           | 'SECOND'
+
====================================================================================================================== @@ -15616,58 +16608,6 @@ OP_NOTEQUALSBANG
-====================================================================================================================== -OP_NOTEQUALSHAT -====================================================================================================================== - - -.. raw:: html - - - - - - ^ - - WHITESPACE - = - - -
- - -
         ::= '^' WHITESPACE* '='
-
- Referenced by: -
- - -====================================================================================================================== -OP_CONCAT -====================================================================================================================== - - -.. raw:: html - - - - - - | - - WHITESPACE - | - - -
- - -
         ::= '|' WHITESPACE* '|'
-
- - ====================================================================================================================== DT_ZONE ====================================================================================================================== @@ -15788,7 +16728,7 @@ DATA_TYPE
           | TYPE_TIMESTAMP
+
====================================================================================================================== @@ -16190,7 +17130,7 @@ S_DOUBLE
           | S_LONG ( '.' ( [eE] [+#x2D]? S_LONG )? | [eE] [+#x2D]? S_LONG )
+
====================================================================================================================== @@ -16211,7 +17151,7 @@ S_LONG
S_LONG   ::= DIGIT+
+ ====================================================================================================================== @@ -16256,14 +17196,14 @@ S_HEX - 0x - - HEX_VALUE + 0x + + HEX_DIGIT
S_HEX    ::= 'X' ( "'" HEX_VALUE* "'" ' '* )+
-
           | '0x' HEX_VALUE+
+
           | '0x' HEX_DIGIT+
@@ -16296,6 +17236,31 @@ HEX_VALUE +====================================================================================================================== +HEX_DIGIT +====================================================================================================================== + + +.. raw:: html + + + + + + [0-9] + + [A-F] + + +
+ + +
         ::= [0-9A-F]
+
+ Referenced by: +
+ + ====================================================================================================================== LINE_COMMENT ====================================================================================================================== @@ -16323,6 +17288,31 @@ LINE_COMMENT +====================================================================================================================== +HASH_LINE_COMMENT +====================================================================================================================== + + +.. raw:: html + + + + + + [#x1] + + [^#xD#xA] + + +
+ + +
         ::= #x1 [^#xD#xA]*
+
+ Not referenced by any. +
+ + ====================================================================================================================== MULTI_LINE_COMMENT ====================================================================================================================== @@ -16378,21 +17368,27 @@ S_IDENTIFIER .. raw:: html - + - - LETTER - - PART_LETTER - -
+ + LETTER + + PART_LETTER + $ + + PART_LETTER_NO_DOLLAR + + PART_LETTER + +
-
         ::= LETTER PART_LETTER*
+
         ::= LETTER PART_LETTER*
+
           | '$' ( PART_LETTER_NO_DOLLAR PART_LETTER* )?
+
====================================================================================================================== @@ -16402,25 +17398,55 @@ LETTER .. raw:: html - + UnicodeIdentifierStart Nd - $ - - _ - - [#x23] - + _ + + [#x23] +
           | Nd
-
           | [$_#x23]
+
           | [_#x23]
+
+ Referenced by: +
+ + +====================================================================================================================== +PART_LETTER_NO_DOLLAR +====================================================================================================================== + + +.. raw:: html + + + + + + UnicodeIdentifierStart + + UnicodeIdentifierExtend + _ + + @ + + [#x23] + + +
+ + +
         ::= UnicodeIdentifierStart
+
           | UnicodeIdentifierExtend
+
           | [_@#x23]
Referenced by:
@@ -16526,7 +17552,7 @@ UnicodeIdentifierStart
           | CJK
Referenced by: -
+ ====================================================================================================================== @@ -19855,7 +20881,7 @@ UnicodeIdentifierExtend
           | CJK
Referenced by: -
+ ====================================================================================================================== @@ -20785,7 +21811,7 @@ S_CHAR_LITERAL .. raw:: html - + @@ -20821,39 +21847,33 @@ S_CHAR_LITERAL [^'] ' - - $$ - - [^$] - - $$ - - q'( - - . - - )' - - q'[ - - . - - ]' - - q'' - - . - - '' - + + q'( + + . + + )' + + q'[ + + . + + ]' + + q'' + + . + + '' +
-
         ::= ( [UENRB] | 'RB' | '_utf8' )? ( "'" ( ( ESC | "\'" | [^'\] )* | ( "''" | [^'] )+ ) "'" | '$$' [^$]* '$$' | "q'{" .* "}'" | "q'(" .* ")'" | "q'[" .* "]'" | "q''" .* "''" )
+
         ::= ( [UENRB] | 'RB' | '_utf8' )? ( "'" ( ( ESC | "\'" | [^'\] )* | ( "''" | [^'] )+ ) "'" | "q'{" .* "}'" | "q'(" .* ")'" | "q'[" .* "]'" | "q''" .* "''" )
+ ====================================================================================================================== From 9c69d05130075bd2874e25b9f43ee7a6052d0ce4 Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 30 Aug 2026 09:08:24 +0700 Subject: [PATCH 039/387] doc: updated syntax diagrams Signed-off-by: Andreas Reichel --- build.gradle | 101 +- src/site/sphinx/syntax_stable.rst | 3740 ++++++++++++++++++----------- 2 files changed, 2328 insertions(+), 1513 deletions(-) diff --git a/build.gradle b/build.gradle index 508008fb52..5d225e5da7 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ def getVersion = { boolean considerSnapshot -> // for publishing a release, call Gradle with Environment Variable RELEASE: // RELEASE=true gradle JSQLParser:publish version = getVersion( !System.getenv("RELEASE") ) -group = 'com.github.jsqlparser' +group = 'com.manticore-projects.jsqlformatter' description = 'JSQLParser library' tasks.register('generateBuildInfo') { @@ -593,81 +593,56 @@ publish { dependsOn(check, gitChangelogTask, renderRR, xslt, xmldoc) } -publishing { - publications { - mavenJava(MavenPublication) { - artifactId = 'jsqlparser' +mavenPublishing { + publishToMavenCentral(true) + signAllPublications() - from components.java + coordinates(group, "jsqlparser", version) - versionMapping { - usage('java-api') { - fromResolutionOf('runtimeClasspath') - } - usage('java-runtime') { - fromResolutionResult() - } - } - - pom { - name.set('JSQLParser library') - description.set('Parse SQL Statements into Abstract Syntax Trees (AST)') - url.set('https://github.com/JSQLParser/JSqlParser') - - licenses { - license { - name.set('GNU Library or Lesser General Public License (LGPL) V2.1') - url.set('http://www.gnu.org/licenses/lgpl-2.1.html') - } - license { - name.set('The Apache Software License, Version 2.0') - url.set('http://www.apache.org/licenses/LICENSE-2.0.txt') - } - } - - developers { - developer { - id.set('twa') - name.set('Tobias Warneke') - email.set('t.warneke@gmx.net') - } - developer { - id.set('are') - name.set('Andreas Reichel') - email.set('andreas@manticore-projects.com') - } - } + pom { + name.set('JSQLParser library') + description.set('Parse SQL Statements into Abstract Syntax Trees (AST)') + url.set('https://github.com/JSQLParser/JSqlParser') - scm { - connection.set('scm:git:https://github.com/JSQLParser/JSqlParser.git') - developerConnection.set('scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git') - url.set('https://github.com/JSQLParser/JSqlParser.git') - } + licenses { + license { + name.set('GNU Library or Lesser General Public License (LGPL) V2.1') + url.set('http://www.gnu.org/licenses/lgpl-2.1.html') + } + license { + name.set('The Apache Software License, Version 2.0') + url.set('http://www.apache.org/licenses/LICENSE-2.0.txt') } } - } - repositories { - maven { - name = "ossrh" - def releasesRepoUrl = "https://central.sonatype.com/repository/maven-releases" - def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" - url(version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl) - - credentials { - username = providers.environmentVariable("ossrhUsername").orNull - password = providers.environmentVariable("ossrhPassword").orNull + developers { + developer { + id.set('twa') + name.set('Tobias Warneke') + email.set('t.warneke@gmx.net') + } + developer { + id.set('are') + name.set('Andreas Reichel') + email.set('andreas@manticore-projects.com') } } + + scm { + connection.set('scm:git:https://github.com/JSQLParser/JSqlParser.git') + developerConnection.set('scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git') + url.set('https://github.com/JSQLParser/JSqlParser.git') + } } } +// Fix signing task dependencies +tasks.withType(AbstractPublishToMaven).configureEach { + dependsOn(tasks.withType(Sign)) +} signing { - // don't sign SNAPSHOTS - if (!version.endsWith('SNAPSHOT')) { - sign publishing.publications.mavenJava - } + required { !version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publish") } } tasks.withType(JavaCompile).configureEach { diff --git a/src/site/sphinx/syntax_stable.rst b/src/site/sphinx/syntax_stable.rst index 05384cba76..3d574e557e 100644 --- a/src/site/sphinx/syntax_stable.rst +++ b/src/site/sphinx/syntax_stable.rst @@ -13,7 +13,7 @@ NonReservedWord .. raw:: html - + @@ -38,702 +38,716 @@ NonReservedWord ALTER ALWAYS - - ANALYZE - - APPEND_ONLY - - APPLY - - APPROXIMATE - - ARCHIVE - - ARRAY - - ASYMMETRIC - - AT - - ASC - - AUTHORIZATION - - AUTO - - AUTO_INCREMENT - - AZURE - - BASE64 + + ANALYSE + + ANALYZE + + APPEND_ONLY + + APPLY + + APPROXIMATE + + ARCHIVE + + ARRAY + + ASYMMETRIC + + AT + + ASC + + AUTHORIZATION + + AUTO + + AUTO_INCREMENT + + AZURE - BEFORE - - BEGIN - - BERNOULLI - - BINARY - - BIT - - BLOBSTORAGE - - BLOCK - - BOOLEAN - - BREADTH - - BRANCH - - BROWSE - - BY - - BYTES - - CACHE - - BUFFERS - - BYTE + BASE64 + + BEFORE + + BEGIN + + BERNOULLI + + BINARY + + BIT + + BLOBSTORAGE + + BLOCK + + BOOLEAN + + BREADTH + + BRANCH + + BROWSE + + BY + + BYTES + + CACHE + + BUFFERS - CALL - - CASCADE - - CASE + BYTE + + CALL + + CASCADE - CAST - - CERTIFICATE - - CHARACTER - - CHANGE - - CHANGES - - CHECKPOINT - - CHAR - - CLOSE - - CLOUD - - COALESCE - - COLLATE + CASE + + CAST + + CERTIFICATE + + CHARACTER + + CHANGE + + CHANGES + + CHECKPOINT + + CHAR + + CLOSE + + CLOUD + + COALESCE - COLUMN - - COLUMNS - - COMMIT - - COMMENT - - COMMENTS - - CONFLICT - - CONSTRAINTS - - CONVERT - - CORRESPONDING - - COSTS - - COUNT - - CREATED - - CYCLE - - DATABASE - - DATA - - DECLARE - - DBA_RECYCLEBIN - - DEFAULTS - - DEPTH - - DEFERRABLE - - DELAYED - - DELETE - - DELIMIT - - DELIMITER - - DESC - - DESCRIBE - - DISABLE - - DISCARD - - DISCONNECT - - DIV - - DDL - - DML - - DO - - DOMAIN - - DRIVER - - DROP - - DUMP - - DUMPFILE - - DUPLICATE - - ELEMENTS - - ENCLOSED - - EMIT - - EMPTY - - ENABLE - - ENCODING - - ENCRYPTION - - END - - ESCAPED - - ENFORCED - - ENGINE - - ERROR - - ESCAPE - - EXA - - EXCHANGE - - EXCLUDE - - EXCLUDING - - EXCLUSIVE - - EXEC - - EXECUTE - - EXPLAIN - - EXPLICIT - - EXTENDED - - EXTRACT - - EXPORT - - K_ISOLATION - FILTER - - FIELDS - - FIRST - - FLUSH - - FOLLOWING - - FORMAT - - FULLTEXT - - FUNCTION - - GRANT - - GROUP_CONCAT + COLLATE + + COLUMN + + COLUMNS + + COMMIT + + COMMENT + + COMMENTS + + CONFLICT + + CONSTRAINTS + + CONVERT + + CORRESPONDING + + COSTS + + COUNT + + CREATED + + CYCLE + + DATABASE + + DATA + + DECLARE + + DBA_RECYCLEBIN + + DEFAULTS + + DEPTH + + DEFERRABLE + + DELAYED + + DELETE + + DELIMIT + + DELIMITER + + DESC + + DESCRIBE + + DISABLE + + DISCARD + + DISCONNECT + + DIV + + DDL + + DML + + DO + + DOMAIN + + DRIVER + + DROP + + DUMP + + DUMPFILE + + DUPLICATE + + ELEMENTS + + ENCLOSED + + EMIT + + EMPTY + + ENABLE + + ENCODING + + ENCRYPTION + + END + + ESCAPED + + ENFORCED + + ENGINE + + ERROR + + ESCAPE + + EXA + + EXCHANGE + + EXCLUDE + + EXCLUDING + + EXCLUSIVE + + EXEC + + EXECUTE + + EXPLAIN + + EXPLICIT + + EXTENDED + + EXTRACT + + EXPORT + + K_ISOLATION + FILTER + + FIELDS + + FILL + + FIRST + + FLUSH + + FOLLOWING + + FORMAT + + FULLTEXT + + FUNCTION - GUARD - - HASH - - HIGH - - HIGH_PRIORITY - - HISTORY - - HOPPING - - IDENTIFIED + GRANT + + GROUPS + + GROUP_CONCAT + + GUARD + + HASH + + HIGH + + HIGH_PRIORITY - IDENTITY - - INCLUDE - - INCLUDE_NULL_VALUES - - INCLUDING - - INCREMENT - - INDEX - - INFORMATION - - INSERT - - INTERLEAVE - - INTERPRET - - INVALIDATE - - INVERSE - - INVISIBLE - - ISNULL - - JDBC - - JSON - - JSON_OBJECT - - JSON_OBJECTAGG - - JSON_ARRAY - - JSON_ARRAYAGG - - KEEP - - KEY_BLOCK_SIZE - - KEY - - KEYS - - KILL - - FN - - LAST - - LEADING - - LESS - - LEVEL - - LINES - - LOCAL - - LOCK - - LOCKED - - LINK - - LOG - - LOOP - - LOW - - LOW_PRIORITY - - LTRIM - - MATCH - - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - MATCHED - - MATERIALIZED - - MAX - - MAXVALUE - - MEMBER - - MERGE - - MIN - - MINVALUE - - MODE - - MODIFY - - MOVEMENT - - NAMES - - NAME - - NEVER - - NEXT - - K_NEXTVAL - NO - - NOCACHE - - NOKEEP - - NOLOCK - - NOMAXVALUE - - NOMINVALUE - - NONE - - NOORDER - - NOTHING - - NOTNULL - - NOVALIDATE - - NULLS - - NOWAIT - - OF - - OFF - - OPTIONALLY - - OPEN - - ORA - - ORDINALITY + HISTORY + + HOPPING + + IDENTIFIED + + IDENTITY + + INCLUDE + + INCLUDE_NULL_VALUES + + INCLUDING + + INCREMENT + + INDEX + + INFORMATION + + INSERT + + INTERLEAVE + + INTERPOLATE + + INTERPRET + + INVALIDATE + + INVERSE + + INVISIBLE + + ISNULL + + JDBC + + JSON + + JSON_OBJECT + + JSON_OBJECTAGG + + JSON_ARRAY + + JSON_ARRAYAGG + + KEEP + + KEY_BLOCK_SIZE + + KEY + + KEYS + + KILL + + FN + + LAST + + LEADING + + LESS + + LEVEL + + LINES + + LOCAL + + LOCK + + LOCKED + + LINK + + LOG + + LOOP + + LOW + + LOW_PRIORITY + + LTRIM + + MATCH + + MATCH_ANY + + MATCH_ALL + + MATCH_PHRASE + + MATCH_PHRASE_PREFIX + + MATCH_REGEXP + + MATCHED + + MATERIALIZED + + MAX + + MAXVALUE + + MEMBER + + MERGE + + MIN + + MINVALUE + + MODE + + MODIFY + + MOVEMENT + + NAMES + + NAME + + NEVER + + NEXT + + K_NEXTVAL + NO + + NOCACHE + + NOKEEP + + NOLOCK + + NOMAXVALUE + + NOMINVALUE + + NONE + + NOORDER + + NOTHING + + NOTNULL + + NOVALIDATE + + NULLS + + NOWAIT + + OF + + OFF - OUTFILE - - OVER - - OVERFLOW - - OVERLAPS - - OVERRIDING - - OVERWRITE - - PADDING - - PARALLEL - - PARENT - - PARSER - - PARTITION - - PARTITIONING - - PATH - - PERCENT - - PLACING - - PLAN - - PLUS - - PRECEDING - - PRIMARY - - POLICY - - PURGE - - QUERY - - QUICK - - QUIESCE - - RANGE - - RAW - - READ - - REBUILD - - RECYCLEBIN - - RECURSIVE - - REFERENCES - - REFRESH - - REGEXP - - REJECT - - RESPECT - - RLIKE - - REGEXP_LIKE - - REGISTER - - REMOTE - - REMOVE - - RENAME - - REORGANIZE - - REPAIR - - REPEATABLE - - REPLACE - - RESET - - RESTART - - RESUMABLE - - RESUME - - RESTRICT - - RESTRICTED - - RETURN - - ROLLBACK - - ROLLUP - - ROOT - - ROW - - ROWS - - RTRIM - - SAFE_CAST - - SAFE_CONVERT - - SAVEPOINT - - SCHEMA - - SEARCH - - SECURE - - SECURITY - - SEED - - SEQUENCE - - SEPARATOR - - SESSION - - SETS - - SHOW - - SHUTDOWN - - SHARE - - SIBLINGS - - SIMILAR - - SIZE - - SKIP - - SPATIAL - - STARTING + OPTION + + OPTIONALLY + + OPEN + + ORA + + ORDINALITY + + OUTFILE + + OVER + + OVERFLOW + + OVERLAPS + + OVERRIDING + + OVERWRITE + + PADDING + + PARALLEL + + PARENT + + PARSER + + PARTITION + + PARTITIONING + + PATH + + PERCENT + + PLACING + + PLAN + + PLUS + + PRECEDING + + PRIMARY + + POLICY + + PURGE + + QUERY + + QUICK + + QUIESCE + + RANGE + + RAW + + READ + + REBUILD + + RECYCLEBIN + + RECURSIVE + + REFERENCES + + REFRESH + + REGEXP + + REJECT + + RESPECT + + RLIKE + + REGEXP_LIKE + + REGISTER + + REMOTE + + REMOVE + + RENAME + + REORGANIZE + + REPAIR + + REPEATABLE + + REPLACE + + RESET + + RESTART + + RESUMABLE + + RESUME + + RESTRICT + + RESTRICTED + + RETURN + + ROLLBACK + + ROLLUP + + ROOT + + ROW + + ROWS + + RTRIM + + SAFE_CAST + + SAFE_CONVERT + + SAVEPOINT + + SCHEMA + + SEARCH + + SECURE + + SECURITY + + SEED + + SEQUENCE + + SEPARATOR + + SESSION + + SETS + + SHOW + + SHUTDOWN + + SHARE + + SIBLINGS - STORED - - STREAM - - STRICT - - STRING - - STRUCT - - SUMMARIZE - - SUSPEND + SIMILAR + + SIZE + + SKIP + + SPATIAL + + STALENESS + + STARTING + + STEP - SWITCH - - SYMMETRIC - - SYNONYM - - SYSTEM - - SYSTEM_TIME - - SYSTEM_TIMESTAMP - - SYSTEM_VERSION - - TABLE - - TABLESPACE - - TERMINATED - - TRIGGER - - THEN - - TEMP - - K_TEXT_LITERAL - TEMPORARY - - THAN - - K_TIME_KEY_EXPR - TIMEOUT - - TO + STORED + + STREAM + + STRICT + + STRING + + STRUCT + + SUMMARIZE + + SUSPEND + + SWITCH + + SYMMETRIC + + SYNONYM + + SYSTEM + + SYSTEM_TIME + + SYSTEM_TIMESTAMP + + SYSTEM_VERSION + + TABLE + + TABLESPACE + + TERMINATED + + TRIGGER + + THEN - TRIM - - TRUNCATE - - TRY_CAST - - TRY_CONVERT - - TUMBLING - - TYPE - - UNLIMITED - - UNLOGGED - - UPDATE - - UPSERT - - UNQIESCE - - USER - - SIGNED - - K_STRING_FUNCTION_NAME - UNSIGNED - - VALIDATE - - VALIDATION - - VERBOSE - - VERSION - - VIEW - - VISIBLE - - VOLATILE - - CONCURRENTLY - - WAIT - - WITH TIES - - WITHIN - - WITHOUT - - WITHOUT_ARRAY_WRAPPER - - WORK - - XML - - XMLAGG - - XMLDATA - - XMLSCHEMA - - XMLTEXT - - XSINIL - - YAML - - YES - - ZONE - + TEMP + + K_TEXT_LITERAL + TEMPORARY + + THAN + + K_TIME_KEY_EXPR + TIMEOUT + + TO + + TRIM + + TRUNCATE + + TRY_CAST + + TRY_CONVERT + + TUMBLING + + TYPE + + UNLIMITED + + UNLOGGED + + UPDATE + + UPSERT + + UNQIESCE + + USER + + SIGNED + + K_STRING_FUNCTION_NAME + UNSIGNED + + VALIDATE + + VALIDATION + + VERBOSE + + VERSION + + VIEW + + VISIBLE + + VOLATILE + + CONCURRENTLY + + WAIT + + WITH TIES + + WITHIN + + WITHOUT + + WITHOUT_ARRAY_WRAPPER + + WORK + + XML + + XMLAGG + + XMLDATA + + XMLSCHEMA + + XMLTEXT + + XSINIL + + YAML + + YES + + ZONE +
@@ -749,6 +763,7 @@ NonReservedWord
           | 'ALIGN'
           | 'ALTER'
           | 'ALWAYS'
+
           | 'ANALYSE'
           | 'ANALYZE'
           | 'APPEND_ONLY'
           | 'APPLY'
@@ -860,6 +875,7 @@ NonReservedWord
           | K_ISOLATION
           | 'FILTER'
           | 'FIELDS'
+
           | 'FILL'
           | 'FIRST'
           | 'FLUSH'
           | 'FOLLOWING'
@@ -867,6 +883,7 @@ NonReservedWord
           | 'FULLTEXT'
           | 'FUNCTION'
           | 'GRANT'
+
           | 'GROUPS'
           | 'GROUP_CONCAT'
           | 'GUARD'
           | 'HASH'
@@ -884,6 +901,7 @@ NonReservedWord
           | 'INFORMATION'
           | 'INSERT'
           | 'INTERLEAVE'
+
           | 'INTERPOLATE'
           | 'INTERPRET'
           | 'INVALIDATE'
           | 'INVERSE'
@@ -952,6 +970,7 @@ NonReservedWord
           | 'NOWAIT'
           | 'OF'
           | 'OFF'
+
           | 'OPTION'
           | 'OPTIONALLY'
           | 'OPEN'
           | 'ORA'
@@ -1034,7 +1053,9 @@ NonReservedWord
           | 'SIZE'
           | 'SKIP'
           | 'SPATIAL'
+
           | 'STALENESS'
           | 'STARTING'
+
           | 'STEP'
           | 'STORED'
           | 'STREAM'
           | 'STRICT'
@@ -1101,7 +1122,7 @@ NonReservedWord
           | 'ZONE'
Referenced by: -
+ ====================================================================================================================== @@ -1111,7 +1132,7 @@ KeywordOrIdentifier .. raw:: html - + @@ -1129,7 +1150,9 @@ KeywordOrIdentifier STRING DATA - + + TYPE +
@@ -1141,7 +1164,8 @@ KeywordOrIdentifier
           | 'VALUE'
           | 'PUBLIC'
           | 'STRING'
-
           | 'DATA'
+
           | 'DATA'
+
           | 'TYPE'
@@ -3637,13 +3661,15 @@ Update OrderByElements - PlainLimit + PlainLimit + + OptionClause - ReturningClause + ReturningClause
- +
@@ -4178,16 +4204,18 @@ Delete WhereClause PreferringClause - - OrderByElements - - PlainLimit + + OrderByElements + + PlainLimit + + OptionClause - ReturningClause + ReturningClause
-
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? ReturningClause?
+
Delete   ::= 'DELETE' 'LOW_PRIORITY'? 'QUICK'? 'IGNORE'? ( ( TableWithAlias ( ',' TableWithAlias )* OutputClause? )? 'FROM' )? ( TableWithAlias JoinsList? )? ( 'USING' FromItem ( ',' FromItem )* )? WhereClause? PreferringClause? OrderByElements? PlainLimit? OptionClause? ReturningClause?
@@ -4200,7 +4228,7 @@ Merge .. raw:: html - + @@ -4218,11 +4246,13 @@ Merge MergeOperations - OutputClause - -
+ OutputClause + + OptionClause + +
-
Merge    ::= 'MERGE' 'INTO' TableWithAlias 'USING' FromItem 'ON' Expression MergeOperations OutputClause?
+
Referenced by:
@@ -4316,7 +4346,45 @@ MergeUpdateClause
         ::= 'UPDATE' 'SET' UpdateSets ( 'WHERE' Expression )? ( 'DELETE' 'WHERE' Expression )?
Referenced by: -
+ + + +====================================================================================================================== +MergeInsertClause +====================================================================================================================== + + +.. raw:: html + + + + + + INSERT + + ( + + ColumnList + ) + + VALUES + + ( + + SimpleExpressionList + ) + + WHERE + + Expression + +
+ + +
         ::= 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
+
+ Referenced by: +
====================================================================================================================== @@ -4326,7 +4394,7 @@ MergeWhenNotMatched .. raw:: html - + @@ -4335,34 +4403,25 @@ MergeWhenNotMatched NOT MATCHED - - AND - - Expression - THEN - - INSERT - - ( - - ColumnList - ) - - VALUES - - ( - - SimpleExpressionList - ) - - WHERE - - Expression - -
+ + BY + + S_IDENTIFIER + AND + + Expression + THEN + + MergeInsertClause + + MergeUpdateClause + DELETE + + +
-
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'AND' Expression )? 'THEN' 'INSERT' ( '(' ColumnList ')' )? 'VALUES' '(' SimpleExpressionList ')' ( 'WHERE' Expression )?
+
         ::= 'WHEN' 'NOT' 'MATCHED' ( 'BY' S_IDENTIFIER )? ( 'AND' Expression )? 'THEN' ( MergeInsertClause | MergeUpdateClause | 'DELETE' )
Referenced by:
@@ -4454,7 +4513,7 @@ Column
+ ====================================================================================================================== @@ -4586,7 +4645,7 @@ RelObjectName
           | 'VALUES'
+ ====================================================================================================================== @@ -4813,39 +4872,45 @@ Select .. raw:: html - + WithList - FromQuery - - PlainSelect - - Values - - ParenthesedSelect - - Alias + FromQuery + + PivotQuery + + PlainSelect + + Values + + ParenthesedSelect + + Alias FromQueryFromSelect SetOperationList - OrderByElements - - LimitWithOffset - - Offset - - Fetch - - WithIsolation - -
- - + OrderByElements + + InterpolateClause + + OptionClause + + LimitWithOffset + + Offset + + Fetch + + WithIsolation + +
+ +
@@ -5619,30 +5684,29 @@ LateralView .. raw:: html - - - - - LATERAL - - VIEW - - OUTER - - Function - - RelObjectName - AS - - RelObjectName - , - - RelObjectName - -
+ + + + + LATERAL + + VIEW + + OUTER + + Function + + RelObjectName + AS + + RelObjectName + , + + +
-
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )?
+
         ::= 'LATERAL' 'VIEW' 'OUTER'? Function RelObjectName? 'AS' RelObjectName ( ',' RelObjectName )*
Referenced by:
@@ -5835,7 +5899,7 @@ PlainSelect .. raw:: html - + @@ -5867,145 +5931,157 @@ PlainSelect SQL_NO_CACHE SQL_CACHE - - AS - - STRUCT - - VALUE - - Top - - SelectItemsList - - MySqlSelectIntoClause - - IntoClause - FROM - - FromItem - - LateralViews - - JoinsList - FROM - - ONLY - - FromItem - - LateralViews - - JoinsList - FINAL - - KSQLWindowClause - - PreWhereClause - - WhereClause - - OracleHierarchicalQueryClause - - PreferringClause - PARTITION - - BY - - ComplexExpressionList - ( - - ComplexExpressionList - ) - - Having - - GroupByColumnReferences - - Having - - Qualify - - OrderByElements - WINDOW - - RelObjectName - AS - - windowDefinition - , - - OrderByElements - - ForClause - EMIT - - CHANGES - - LimitBy - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - FOR - - NO - - KEY - - UPDATE - - KEY - - SHARE - - READ - - FETCH - - ONLY - - OF - - Table - - Wait - NOWAIT - - SKIP - - LOCKED - - MySqlSelectIntoClause - SETTINGS - - UpdateSets - - OptimizeFor - INTO - - TEMP - - Table - WITH - - NO - - LOG - - -
+ + AS + + STRUCT + + VALUE + + Top + + SelectItemsList + + MySqlSelectIntoClause + + IntoClause + FROM + + FromItem + + LateralViews + + JoinsList + FROM + + ONLY + + FromItem + + LateralViews + + JoinsList + FINAL + + KSQLWindowClause + + PreWhereClause + + WhereClause + + OracleHierarchicalQueryClause + + PreferringClause + PARTITION + + BY + + ComplexExpressionList + ( + + ComplexExpressionList + ) + + Having + + GroupByColumnReferences + + Having + + Qualify + + OrderByElements + WINDOW + + RelObjectName + AS + + windowDefinition + , + + OrderByElements + + InterpolateClause + + ForClause + + OptionClause + EMIT + + CHANGES + + LimitWithOffset + BY + + ExpressionList + + Offset + + LimitWithOffset + + MySqlProcedureAnalyse + + Fetch + + WithIsolation + FOR + + NO + + KEY + + UPDATE + + KEY + + SHARE + + READ + + FETCH + + ONLY + + OF + + Table + , + + Wait + NOWAIT + + SKIP + + LOCKED + + OrderByElements + + InterpolateClause + + MySqlSelectIntoClause + SETTINGS + + UpdateSets + + OptimizeFor + INTO + + TEMP + + Table + WITH + + NO + + LOG + + +
         ::= K_SELECT 'STRAIGHT_JOIN'? Skip? First? Top? ( 'ALL' | 'DISTINCT' ( 'ON' '(' SelectItemsList ')' )? | 'DISTINCTROW' | 'UNIQUE' | 'SQL_CALC_FOUND_ROWS' | 'SQL_NO_CACHE' | 'SQL_CACHE' - )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList MySqlSelectIntoClause? IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? ForClause? ( 'EMIT' 'CHANGES' )? LimitBy? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' - ) ( 'OF' Table )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? )? MySqlSelectIntoClause? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
+ )? ( 'AS' ( 'STRUCT' | 'VALUE' ) )? Top? SelectItemsList MySqlSelectIntoClause? IntoClause? ( 'FROM' FromItem LateralViews? JoinsList? )? ( 'FROM' 'ONLY' FromItem LateralViews? JoinsList? )? 'FINAL'? KSQLWindowClause? PreWhereClause? WhereClause? OracleHierarchicalQueryClause? ( PreferringClause ( 'PARTITION' 'BY' ( ComplexExpressionList | '(' ComplexExpressionList ')' ) )? )? Having? GroupByColumnReferences? Having? Qualify? OrderByElements? ( 'WINDOW' RelObjectName 'AS' windowDefinition ( ',' RelObjectName 'AS' windowDefinition )* )? OrderByElements? InterpolateClause? ForClause? OptionClause? ( 'EMIT' 'CHANGES' )? ( LimitWithOffset ( 'BY' ExpressionList )? )? Offset? LimitWithOffset? MySqlProcedureAnalyse? Fetch? WithIsolation? ( 'FOR' ( ( 'NO' 'KEY' )? 'UPDATE' | 'KEY'? 'SHARE' | ( 'READ' | 'FETCH' ) 'ONLY' + ) ( 'OF' Table ( ',' Table )* )? Wait? ( 'NOWAIT' | 'SKIP' 'LOCKED' )? OrderByElements? InterpolateClause? )? MySqlSelectIntoClause? ( 'SETTINGS' UpdateSets )? OptimizeFor? ( 'INTO' 'TEMP' Table )? ( 'WITH' 'NO' 'LOG' )?
Referenced by:
@@ -6018,7 +6094,7 @@ SetOperationList .. raw:: html - + @@ -6038,22 +6114,24 @@ SetOperationList ParenthesedSelect - OrderByElements - - LimitWithOffset - - Offset - - LimitWithOffset - - Fetch - - WithIsolation - -
+ OrderByElements + + InterpolateClause + + LimitWithOffset + + Offset + + LimitWithOffset + + Fetch + + WithIsolation + +
-
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
+
         ::= ( ( 'UNION' | 'INTERSECT' | 'MINUS' | 'EXCEPT' ) SetOperationModifier? ( PlainSelect | Values | ParenthesedSelect ) )+ OrderByElements? InterpolateClause? LimitWithOffset? Offset? LimitWithOffset? Fetch? WithIsolation?
Referenced by:
@@ -6614,7 +6692,50 @@ PivotFunctionItems
         ::= FunctionItem ( ',' FunctionItem )*
Referenced by: -
+
+ + +====================================================================================================================== +PivotQuery +====================================================================================================================== + + +.. raw:: html + + + + + + PIVOT + + FromItem + ON + + ExpressionList + USING + + PivotFunctionItems + USING + + PivotFunctionItems + GROUP + + BY + + ExpressionList + GROUP + + BY + + ExpressionList + +
+ + +
         ::= 'PIVOT' FromItem ( ( 'ON' ExpressionList ( 'USING' PivotFunctionItems )? | 'USING' PivotFunctionItems ) ( 'GROUP' 'BY' ExpressionList )? | 'GROUP' 'BY' ExpressionList )
+
+ Referenced by: +
====================================================================================================================== @@ -6851,6 +6972,39 @@ MySqlSelectIntoClause
+====================================================================================================================== +MySqlProcedureAnalyse +====================================================================================================================== + + +.. raw:: html + + + + + + PROCEDURE + + ANALYSE + + ( + + S_LONG + , + + S_LONG + ) + + +
+ + +
         ::= 'PROCEDURE' 'ANALYSE' '(' ( S_LONG ( ',' S_LONG )? )? ')'
+
+ Referenced by: +
+ + ====================================================================================================================== MySqlSelectIntoOutfileTail ====================================================================================================================== @@ -7037,7 +7191,7 @@ FromItem
+ ====================================================================================================================== @@ -7100,84 +7254,86 @@ JoinerExpression .. raw:: html - - - - - GLOBAL - - ANY - - ALL - - NATURAL - - LEFT - - SEMI - - OUTER - - ANY - - ALL - - RIGHT - - FULL - - OUTER - - ANY - - ALL - - INNER - - CROSS - - OUTER - - JoinHint - JOIN - - FETCH - - , - - OUTER - - STRAIGHT_JOIN - - APPLY - - FromItem - WITHIN - - ( - - JoinWindow - ) - - ON - - Expression - USING - - ( - - Column - , - - ) - - -
+ + + + + GLOBAL + + ANY + + ALL + + NATURAL + + LEFT + + SEMI + + OUTER + + ANY + + ALL + + RIGHT + + FULL + + OUTER + + ANY + + ALL + + INNER + + CROSS + + OUTER + + ARRAY + + JoinHint + JOIN + + FETCH + + , + + OUTER + + STRAIGHT_JOIN + + APPLY + + FromItem + WITHIN + + ( + + JoinWindow + ) + + ON + + Expression + USING + + ( + + Column + , + + ) + + +
         ::= 'GLOBAL'? ( 'ANY' | 'ALL' )? 'NATURAL'? ( 'LEFT' ( 'SEMI' | 'OUTER' | 'ANY' | 'ALL' )? | ( 'RIGHT' | 'FULL' ) ( 'OUTER' | 'ANY' | 'ALL' )? | 'INNER' | 'CROSS' - | 'OUTER' )? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
+ | 'OUTER' )? 'ARRAY'? ( JoinHint? 'JOIN' 'FETCH'? | ',' 'OUTER'? | 'STRAIGHT_JOIN' | 'APPLY' ) FromItem ( ( 'WITHIN' '(' JoinWindow ')' )? ( 'ON' Expression )+ | 'USING' '(' Column ( ',' Column )* ')' )?
@@ -7716,7 +7872,7 @@ OrderByElements
         ::= 'ORDER' 'SIBLINGS'? 'BY' OrderByElement ( ',' OrderByElement )*
+ ====================================================================================================================== @@ -7726,7 +7882,7 @@ OrderByElement .. raw:: html - + @@ -7744,22 +7900,247 @@ OrderByElement FIRST - LAST - - WITH - - ROLLUP - - -
+ LAST + + WithFill + WITH + + ROLLUP + + +
-
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? ( 'WITH' 'ROLLUP' )?
+
         ::= Expression ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER ) )? ( 'ASC' | 'DESC' )? ( 'NULLS' ( 'FIRST' | 'LAST' )? )? WithFill? ( 'WITH' 'ROLLUP' )?
Referenced by:
+====================================================================================================================== +WithFill +====================================================================================================================== + + +.. raw:: html + + + + + + WITH + + FILL + + FROM + + Expression + TO + + Expression + STEP + + Expression + STALENESS + + Expression + +
+ +
WithFill ::= 'WITH' 'FILL' ( 'FROM' Expression )? ( 'TO' Expression )? ( 'STEP' Expression )? ( 'STALENESS' Expression )?
+
+ Referenced by: +
+ + +====================================================================================================================== +InterpolateClause +====================================================================================================================== + + +.. raw:: html + + + + + + INTERPOLATE + + ( + + InterpolateElement + , + + ) + + +
+ + +
         ::= 'INTERPOLATE' ( '(' ( InterpolateElement ( ',' InterpolateElement )* )? ')' )?
+
+ + +====================================================================================================================== +InterpolateElement +====================================================================================================================== + + +.. raw:: html + + + + + + Column + AS + + Expression + +
+ + +
         ::= Column ( 'AS' Expression )?
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionHintParameter +====================================================================================================================== + + +.. raw:: html + + + + + + UserVariable + UNKNOWN + + Expression + +
+ + +
         ::= UserVariable 'UNKNOWN'
+
           | Expression
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionClause +====================================================================================================================== + + +.. raw:: html + + + + + + OPTION + + ( + + OptionHint + , + + ) + + +
+ + +
         ::= 'OPTION' '(' OptionHint ( ',' OptionHint )* ')'
+
+ + +====================================================================================================================== +OptionHint +====================================================================================================================== + + +.. raw:: html + + + + + + OptionHintName + = + + Expression + ( + + OptionHintParameter + , + + ) + + +
+ + +
         ::= OptionHintName ( '='? Expression | '(' OptionHintParameter ( ',' OptionHintParameter )* ')' )?
+
+ Referenced by: +
+ + +====================================================================================================================== +OptionHintName +====================================================================================================================== + + +.. raw:: html + + + + + + S_IDENTIFIER + + NonReservedWord + JOIN + + FOR + + ORDER + + UNION + + EXCEPT + + INTERSECT + + USE + + OPTIMIZE + + FORCE + + UNKNOWN + + +
+ + +
         ::= ( S_IDENTIFIER | NonReservedWord | 'JOIN' | 'FOR' | 'ORDER' | 'UNION' | 'EXCEPT' | 'INTERSECT' | 'USE' | 'OPTIMIZE' + | 'FORCE' | 'UNKNOWN' )+
+
+ Referenced by: +
+ + ====================================================================================================================== JdbcParameter ====================================================================================================================== @@ -7812,7 +8193,7 @@ LimitWithOffset
         ::= 'LIMIT' ( ParenthesedSelect | Expression ) ( ',' Expression )?
+ ====================================================================================================================== @@ -7841,30 +8222,6 @@ PlainLimit -====================================================================================================================== -LimitBy -====================================================================================================================== - - -.. raw:: html - - - - - - LimitWithOffset - BY - - ExpressionList - -
- - -
- Referenced by: -
- - ====================================================================================================================== Offset ====================================================================================================================== @@ -8093,7 +8450,7 @@ Expression
         ::= Condition
+ ====================================================================================================================== @@ -8781,7 +9138,7 @@ ExpressionList
           | ParenthesedExpressionList
+ ====================================================================================================================== @@ -8836,7 +9193,7 @@ SimpleExpressionList
         ::= SimpleExpression ( ',' ( LambdaExpression | SimpleExpression ) )*
+ ====================================================================================================================== @@ -8860,7 +9217,7 @@ ColumnList
         ::= Column ( ',' Column )*
+ ====================================================================================================================== @@ -9044,6 +9401,30 @@ SimpleExpression +====================================================================================================================== +CharacterSetIntroducedString +====================================================================================================================== + + +.. raw:: html + + + + + + S_IDENTIFIER + + S_CHAR_LITERAL + +
+ + +
         ::= S_IDENTIFIER S_CHAR_LITERAL
+
+ Referenced by: +
+ + ====================================================================================================================== MulChain ====================================================================================================================== @@ -9148,7 +9529,7 @@ PrimaryExpression .. raw:: html - + @@ -9166,144 +9547,152 @@ PrimaryExpression CaseWhenExpression - CharacterPrimary - - ImplicitCast - - JdbcParameter - - JdbcNamedParameter - - UserVariable + CharacterPrimary + + MapExpression + + ImplicitCast + + JdbcParameter + + JdbcNamedParameter - NumericBind - - ExtractExpression - - XMLSerializeExpr - - JsonFunction - - JsonAggregateFunction - - FullTextSearch - - CastExpression - - Function - - AnalyticExpression - - DateUnitExpression - - IntervalExpression - - S_DOUBLE - - S_LONG - - S_HEX - - AllColumns - - AllTableColumns - - K_TIME_KEY_EXPR - CURRENT - - DateTimeLiteralExpression - - StructType - ARRAY - - < - - ColDataType - > - - ArrayConstructor - - NextValExpression - - ConnectByRootOperator + UserVariable + + NumericBind + + ExtractExpression + + XMLSerializeExpr + + JsonFunction + + JsonAggregateFunction + + FullTextSearch + + CastExpression + + Function + + AnalyticExpression + + DateUnitExpression + + IntervalExpression + + S_DOUBLE + + S_LONG + + S_HEX + + AllColumns + + AllTableColumns + + K_TIME_KEY_EXPR + CURRENT + + DateTimeLiteralExpression + + StructType + ARRAY + + < + + ColDataType + > + + ArrayConstructor + + NextValExpression - ConnectByPriorOperator - - KeyExpression - ALL - - Column - ( - - + - - ) - - TRUE - - FALSE - - S_CHAR_LITERAL - {d - - {t - - {ts - - S_CHAR_LITERAL - } - - Select - - ParenthesedSelect - - ParenthesedExpressionList - -> - - Expression - . - - RelObjectName - COLLATE - - S_CHAR_LITERAL - - S_QUOTED_IDENTIFIER - - S_IDENTIFIER - - IntervalExpressionWithoutInterval - - ArrayExpression - :: - - ColDataType - -> - - : - - ->> - - #> - - #>> - - Expression - - SimpleExpression - - JsonExpression - AT - - K_DATETIMELITERAL - ZONE - - PrimaryExpression - -
+ ConnectByRootOperator + + ConnectByPriorOperator + + KeyExpression + ALL + + CharacterSetIntroducedString + + Column + ( + + + + + ) + + TRUE + + FALSE + + S_CHAR_LITERAL + {d + + {t + + {ts + + S_CHAR_LITERAL + } + + Select + + ParenthesedSelect + + ParenthesedExpressionList + -> + + Expression + . + + * + + . + + RelObjectName + COLLATE + + S_CHAR_LITERAL + + S_QUOTED_IDENTIFIER + + S_IDENTIFIER + + IntervalExpressionWithoutInterval + + ArrayExpression + :: + + ColDataType + -> + + : + + ->> + + #> + + #>> + + Expression + + SimpleExpression + + JsonExpression + AT + + K_DATETIMELITERAL + ZONE + + PrimaryExpression + +
-
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | KeyExpression | 'ALL' | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
+
         ::= ( 'NOT' | '!' )? ( '+' | '-' | '~' )? ( 'NULL' | CaseWhenExpression | CharacterPrimary | MapExpression | ImplicitCast | JdbcParameter | JdbcNamedParameter | UserVariable | NumericBind | ExtractExpression | XMLSerializeExpr | JsonFunction | JsonAggregateFunction | FullTextSearch | CastExpression | Function AnalyticExpression? | DateUnitExpression | IntervalExpression | S_DOUBLE | S_LONG | S_HEX | AllColumns | AllTableColumns | K_TIME_KEY_EXPR | 'CURRENT' | DateTimeLiteralExpression | StructType | ( 'ARRAY' ( '<' ColDataType '>' )? )? ArrayConstructor | NextValExpression | ConnectByRootOperator | ConnectByPriorOperator | KeyExpression | 'ALL' | CharacterSetIntroducedString | Column ( '(' '+' ')' )? | 'TRUE' | 'FALSE' | S_CHAR_LITERAL+ | ( '{d' | '{t' | '{ts' ) S_CHAR_LITERAL '}' | Select | ParenthesedSelect | ParenthesedExpressionList ( '->' Expression )? ( '.' '*' )? ( '.' RelObjectName )* ) ( 'COLLATE' ( S_CHAR_LITERAL | S_QUOTED_IDENTIFIER | S_IDENTIFIER ) )? IntervalExpressionWithoutInterval? ArrayExpression? ( '::' ColDataType )* ( ( ( '->' | ':' | '->>' | '#>' | '#>>' ) ( Expression | SimpleExpression ) )+ JsonExpression )? ( 'AT' K_DATETIMELITERAL 'ZONE' PrimaryExpression )*
@@ -9506,7 +9895,7 @@ UserVariable
         ::= S_AT_IDENTIFIER IdentifierChain2
+ ====================================================================================================================== @@ -9637,6 +10026,60 @@ ArrayConstructor +====================================================================================================================== +MapExpression +====================================================================================================================== + + +.. raw:: html + + + + + + DATA_TYPE + { + + MapKeyExpression + : + + Expression + , + + } + + +
+ + +
         ::= DATA_TYPE '{' ( MapKeyExpression ':' Expression ( ',' MapKeyExpression ':' Expression )* )? '}'
+
+ Referenced by: +
+ + +====================================================================================================================== +MapKeyExpression +====================================================================================================================== + + +.. raw:: html + + + + + + Expression + +
+ + +
         ::= Expression
+
+ Referenced by: +
+ + ====================================================================================================================== StructParameters ====================================================================================================================== @@ -10414,7 +10857,7 @@ IntervalExpression .. raw:: html - + @@ -10428,16 +10871,16 @@ IntervalExpression S_CHAR_LITERAL - Expression - - S_IDENTIFIER - - K_DATE_LITERAL - -
+ Expression + + IntervalQualifier + + S_IDENTIFIER + +
-
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( S_IDENTIFIER | K_DATE_LITERAL )?
+
         ::= 'INTERVAL' ( '-'? ( S_LONG | S_DOUBLE ) | S_CHAR_LITERAL | Expression ) ( IntervalQualifier | S_IDENTIFIER )?
Referenced by:
@@ -10450,21 +10893,59 @@ IntervalExpressionWithoutInterval .. raw:: html - + - - - K_DATE_LITERAL - -
+ + + IntervalQualifier + +
-
         ::= K_DATE_LITERAL
+
         ::= IntervalQualifier
Referenced by:
+====================================================================================================================== +IntervalQualifier +====================================================================================================================== + + +.. raw:: html + + + + + + K_DATE_LITERAL + ( + + S_LONG + , + + S_LONG + ) + + TO + + K_DATE_LITERAL + ( + + S_LONG + ) + + +
+ + +
         ::= K_DATE_LITERAL ( '(' S_LONG ( ',' S_LONG )? ')' )? ( 'TO' K_DATE_LITERAL ( '(' S_LONG ')' )? )?
+
+ + ====================================================================================================================== KeepExpression ====================================================================================================================== @@ -10623,30 +11104,66 @@ WindowElement .. raw:: html - + ROWS RANGE - - BETWEEN - - WindowOffset - AND - - WindowOffset - -
+ + GROUPS + + BETWEEN + + WindowOffset + AND + + WindowOffset + + FrameExclusion + +
-
         ::= ( 'ROWS' | 'RANGE' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset
+
         ::= ( 'ROWS' | 'RANGE' | 'GROUPS' ) ( 'BETWEEN' WindowOffset 'AND' )? WindowOffset FrameExclusion?
+====================================================================================================================== +FrameExclusion +====================================================================================================================== + + +.. raw:: html + + + + + + EXCLUDE + + CURRENT + + ROW + + GROUP + + NO + + S_IDENTIFIER + +
+ + +
         ::= 'EXCLUDE' ( 'CURRENT' 'ROW' | 'GROUP' | 'NO'? S_IDENTIFIER )
+
+ Referenced by: +
+ + ====================================================================================================================== WindowOffset ====================================================================================================================== @@ -10774,17 +11291,19 @@ CastExpression ) - ColDataType + ColDataType + ARRAY + FORMAT S_CHAR_LITERAL ) - +
-
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
+
         ::= ( 'CAST' | 'SAFE_CAST' | 'TRY_CAST' | 'INTERPRET' ) '(' SimpleExpression 'AS' ( 'ROW' '(' ColumnDefinition ( ',' ColumnDefinition )* ')' | ColDataType 'ARRAY'? ) ( 'FORMAT' S_CHAR_LITERAL )? ')'
Referenced by:
@@ -11030,7 +11549,7 @@ Function
           | InternalFunction
+
====================================================================================================================== @@ -11069,7 +11588,7 @@ InternalFunction .. raw:: html - + @@ -11077,77 +11596,101 @@ InternalFunction RelObjectNames ( - - DISTINCT - - ALL - - UNIQUE - - TABLE - - ExpressionList - - OrderByElements - ON - - OVERFLOW - - TRUNCATE - - ERROR - - S_CHAR_LITERAL - WITH - - WITHOUT - - COUNT - - Select - HAVING - - MIN - - MAX - - Expression - IGNORE - - RESPECT - - NULLS - - PlainLimit - ) - - ( - - ExpressionList - ) - - . - - Function - - Column - IGNORE - - RESPECT - - NULLS - - KeepExpression - -
+ + DISTINCT + + ALL + + UNIQUE + + FunctionArgumentList + + Select + HAVING + + MIN + + MAX + + Expression + IGNORE + + RESPECT + + NULLS + + PlainLimit + ) + + ( + + ExpressionList + ) + + S_DOUBLE + . + + Function + + Column + IGNORE + + RESPECT + + NULLS + + KeepExpression + +
-
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )? | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
+
         ::= 'APPROXIMATE'? RelObjectNames '(' ( ( 'DISTINCT' | 'ALL' | 'UNIQUE' )? ( FunctionArgumentList | Select ) )? ( 'HAVING' ( 'MIN' | 'MAX' ) Expression )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? PlainLimit? ')' ( '(' ExpressionList ')' )? ( S_DOUBLE | '.' ( Function | Column ) )? ( ( 'IGNORE' | 'RESPECT' ) 'NULLS' )? KeepExpression?
Referenced by:
+====================================================================================================================== +FunctionArgumentList +====================================================================================================================== + + +.. raw:: html + + + + + + TABLE + + ExpressionList + + OrderByElements + ON + + OVERFLOW + + TRUNCATE + + ERROR + + S_CHAR_LITERAL + WITH + + WITHOUT + + COUNT + + +
+ + +
         ::= 'TABLE'? ExpressionList OrderByElements? ( 'ON' 'OVERFLOW' ( 'TRUNCATE' | 'ERROR' ) ( S_CHAR_LITERAL ( ( 'WITH' | 'WITHOUT' ) 'COUNT' )? )? )?
+
+ Referenced by: +
+ + ====================================================================================================================== XMLSerializeExpr ====================================================================================================================== @@ -11671,6 +12214,101 @@ JsonTableBody
+====================================================================================================================== +XmlTablePassingClause +====================================================================================================================== + + +.. raw:: html + + + + + + Expression + AS + + RelObjectName + +
+ + +
         ::= Expression ( 'AS' RelObjectName )?
+
+ Referenced by: +
+ + +====================================================================================================================== +XmlTableColumnDefinition +====================================================================================================================== + + +.. raw:: html + + + + + + RelObjectName + FOR + + ORDINALITY + + ColDataType + PATH + + Expression + DEFAULT + + Expression + +
+ + +
         ::= RelObjectName ( 'FOR' 'ORDINALITY' | ColDataType ( 'PATH' Expression )? ( 'DEFAULT' Expression )? )
+
+ Referenced by: +
+ + +====================================================================================================================== +XmlTableBody +====================================================================================================================== + + +.. raw:: html + + + + + + ( + + Expression + + S_IDENTIFIER + + XmlTablePassingClause + , + + COLUMNS + + XmlTableColumnDefinition + , + + ) + + +
+ + + +
+ Referenced by: +
+ + ====================================================================================================================== TableFunction ====================================================================================================================== @@ -11678,33 +12316,71 @@ TableFunction .. raw:: html - + - LATERAL - - JsonKeyword - - JsonTableBody - - Function - WITH - - OFFSET - - ORDINALITY - - -
+ LATERAL + + ROWS + + FROM + + RowsFromFunctionList + + JsonKeyword + + JsonTableBody + + S_IDENTIFIER + + XmlTableBody + + Function + WITH + + OFFSET + + ORDINALITY + + +
-
         ::= 'LATERAL'? ( JsonKeyword JsonTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
+
         ::= 'LATERAL'? ( 'ROWS' 'FROM' RowsFromFunctionList | JsonKeyword JsonTableBody | S_IDENTIFIER XmlTableBody | Function ) ( 'WITH' ( 'OFFSET' | 'ORDINALITY' ) )?
Referenced by:
+====================================================================================================================== +RowsFromFunctionList +====================================================================================================================== + + +.. raw:: html + + + + + + ( + + Function + , + + ) + + +
+ + +
         ::= '(' Function ( ',' Function )* ')'
+
+ Referenced by: +
+ + ====================================================================================================================== ColumnNamesWithParamsList ====================================================================================================================== @@ -11742,22 +12418,22 @@ IndexColumnWithParams .. raw:: html - - - - - RelObjectName - ( - - Expression - ) - - CreateParameter - -
+ + + + + RelObjectName + ( + + Expression + ) + + CreateParameter + +
-
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter?
+
         ::= ( RelObjectName | '(' Expression ')' ) CreateParameter*
@@ -11819,7 +12495,7 @@ CreateIndex .. raw:: html - + @@ -11843,20 +12519,60 @@ CreateIndex ON Table - - IndexColumnsWithParamsList + + IndexColumnsWithParamsList + INCLUDE + + ColumnsNamesList - CreateParameter + CreateParameter
-
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList CreateParameter*
+
         ::= CreateParameter? 'INDEX' ( 'IF' 'NOT' 'EXISTS' )? Index ( 'ON' Table UsingIndexType? | UsingIndexType? 'ON' Table ) IndexColumnsWithParamsList ( 'INCLUDE' ColumnsNamesList )? CreateParameter*
Referenced by:
+====================================================================================================================== +ColumnDefinitionParameter +====================================================================================================================== + + +.. raw:: html + + + + + + CreateParameter + DEFERRABLE + + DISABLE + + ENABLE + + NOVALIDATE + + VALIDATE + + +
+ + +
         ::= CreateParameter
+
           | 'DEFERRABLE'
+
           | 'DISABLE'
+
           | 'ENABLE'
+
           | 'NOVALIDATE'
+
           | 'VALIDATE'
+
+ Referenced by: +
+ + ====================================================================================================================== ColumnDefinition ====================================================================================================================== @@ -11864,20 +12580,20 @@ ColumnDefinition .. raw:: html - + RelObjectName - ColDataType - - CreateParameter - -
+ ColDataType + + ColumnDefinitionParameter + +
- +
@@ -11960,6 +12676,38 @@ PathSpecification
+====================================================================================================================== +CreateDatabase +====================================================================================================================== + + +.. raw:: html + + + + + + DATABASE + + IF + + NOT + + EXISTS + + RelObjectName + + captureRest + +
+ + +
         ::= 'DATABASE' ( 'IF' 'NOT' 'EXISTS' )? RelObjectName captureRest?
+
+ Referenced by: +
+ + ====================================================================================================================== CreateTableConstraint ====================================================================================================================== @@ -11970,10 +12718,10 @@ CreateTableConstraint - - INDEX - - UNIQUE + + UNIQUE + + INDEX FULLTEXT @@ -12012,12 +12760,12 @@ CreateTableConstraint Expression ) - +
-
         ::= ( 'INDEX' | 'UNIQUE'? ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
+
         ::= 'UNIQUE'? ( 'INDEX' | ( 'FULLTEXT' | 'SPATIAL' )? 'KEY' ) RelObjectName IndexColumnsWithParamsList CreateParameter*
           | ( 'CONSTRAINT' RelObjectName )? ( ( 'PRIMARY' 'KEY' | 'UNIQUE' 'KEY'? ) ColumnNamesWithParamsList CreateParameter* | ForeignKeySpec | CheckConstraintSpec )
           | 'EXCLUDE' 'WHERE' ( '(' Expression ')' )*
@@ -12227,7 +12975,7 @@ ColDataType .. raw:: html - + @@ -12270,24 +13018,27 @@ ColDataType DATA - NAME - - . - - ColDataType - ( - - S_LONG - MAX - - BYTE - - CHAR - - S_CHAR_LITERAL - - S_IDENTIFIER - CHAR + NAME + + IntervalQualifier + . + + ColDataType + ( + + ColDataType + + S_LONG + MAX + + BYTE + + CHAR + + S_CHAR_LITERAL + + S_IDENTIFIER + CHAR , @@ -12297,23 +13048,22 @@ ColDataType S_LONG ] - - CHARACTER - - SET - - S_IDENTIFIER - BINARY - - -
+ + CHARACTER + + SET + + S_IDENTIFIER + BINARY + + +
-
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) ( - '.' ColDataType )? ) ( '(' ( ( ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
+
         ::= ( 'STRUCT' '(' RelObjectNameExt ColDataType ( ',' RelObjectNameExt ColDataType )* ')' | DataType | ( S_IDENTIFIER | S_QUOTED_IDENTIFIER | K_DATETIMELITERAL | K_DATE_LITERAL | 'XML' | 'INTERVAL' | DT_ZONE | 'CHAR' | 'SET' | 'BINARY' | 'JSON' | 'STRING' | 'PUBLIC' | 'DATA' | 'NAME' ) IntervalQualifier? ( '.' ColDataType )? ) ( '(' ( ( ColDataType | ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? | S_CHAR_LITERAL | S_IDENTIFIER | 'CHAR' ) ','? )* ')' )? ( '[' S_LONG? ']' )* ( 'CHARACTER' 'SET' ( S_IDENTIFIER | 'BINARY' ) )?
+
====================================================================================================================== @@ -12598,20 +13348,23 @@ AlterExpressionUsingIndex .. raw:: html - + USING INDEX - - RelObjectName - -
+ + RelObjectName + TABLESPACE + + RelObjectName + +
-
         ::= 'USING' 'INDEX'? RelObjectName
+
         ::= 'USING' 'INDEX'? ( RelObjectName? 'TABLESPACE' )? RelObjectName
@@ -12679,7 +13432,7 @@ CreateParameter .. raw:: html - + @@ -12809,39 +13562,57 @@ CreateParameter SAMPLE ALWAYS - - = - - DEFAULT - - AS - - CHECK - - ( - - Expression - ) - - + - - - - - S_LONG - - S_DOUBLE - - AList - CHARACTER - - SET - - ARRAY - - ArrayConstructor - :: - - ColDataType + + VISIBLE + + INVISIBLE + + KEY_BLOCK_SIZE + + ALGORITHM + + LOCK + + NONE + + PARSER + + FULLTEXT + + SPATIAL + + = + + DEFAULT + + AS + + CHECK + + ( + + Expression + ) + + + + + - + + S_LONG + + S_DOUBLE + + AList + CHARACTER + + SET + + ARRAY + + ArrayConstructor + :: + + ColDataType
@@ -12899,6 +13670,15 @@ CreateParameter
           | 'MATERIALIZED'
           | 'SAMPLE'
           | 'ALWAYS'
+
           | 'VISIBLE'
+
           | 'INVISIBLE'
+
           | 'KEY_BLOCK_SIZE'
+
           | 'ALGORITHM'
+
           | 'LOCK'
+
           | 'NONE'
+
           | 'PARSER'
+
           | 'FULLTEXT'
+
           | 'SPATIAL'
           | '='
           | ( 'DEFAULT' | 'AS' | 'CHECK' ) ( '(' Expression ')' )?
           | ( '+' | '-' )? S_LONG
@@ -12909,7 +13689,7 @@ CreateParameter
           | '::' ColDataType
+
====================================================================================================================== @@ -13035,7 +13815,7 @@ ColumnsNamesList
         ::= '(' ColumnsNamesListItem ( ',' ColumnsNamesListItem )* ')'
+
====================================================================================================================== @@ -13102,7 +13882,7 @@ Drop .. raw:: html - + @@ -13137,15 +13917,23 @@ Drop CASCADE RESTRICT - - ON - - Table + + ALGORITHM + + NONE + + = + + ON + + Table + LOCK +
Drop     ::= 'DROP' 'MATERIALIZED'? ( S_IDENTIFIER | 'TEMPORARY'? 'TABLE' | 'INDEX' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'FUNCTION' ) - ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ON' Table )*
+ ( 'IF' 'EXISTS' )? Table FuncArgsList? ( S_IDENTIFIER | 'CASCADE' | 'RESTRICT' | 'ALGORITHM' | 'NONE' | '=' | 'ON' Table | 'LOCK' )*
Referenced by:
@@ -13902,16 +14690,16 @@ AlterExpressionAddAlterModify COLUMNS IF - - NOT - - EXISTS - - AlterExpressionColumnChanges - - AlterExpressionColumnDataType - - AlterExpressionColumnDropNotNull + + NOT + + EXISTS + + AlterExpressionColumnChanges + + AlterExpressionColumnDataType + + AlterExpressionColumnDropNotNull AlterExpressionColumnChanges UNIQUE @@ -13938,12 +14726,12 @@ AlterExpressionAddAlterModify ENFORCED - AlterExpressionAddConstraint + AlterExpressionAddConstraint
-
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT' 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
+
         ::= ( 'ADD' | 'ALTER' | 'MODIFY' ) ( 'PRIMARY' 'KEY' ColumnsNamesList AlterExpressionConstraintState AlterExpressionUsingIndex? | ( 'KEY' | 'INDEX' ) RelObjectName? UsingIndexType? IndexColumnsWithParamsList? IndexOptionList AlterExpressionConstraintState | ( 'SPATIAL' | 'FULLTEXT' ) ( 'INDEX' | 'KEY' )? RelObjectName? ColumnsNamesList IndexOptionList | RelObjectName 'COMMENT' S_CHAR_LITERAL | 'PARTITION' PartitionDefinitions | ( 'COLUMN' | 'COLUMNS' )? ( 'IF' 'NOT'? 'EXISTS' )? ( AlterExpressionColumnChanges | AlterExpressionColumnDataType | AlterExpressionColumnDropNotNull ) | AlterExpressionColumnChanges | 'UNIQUE' ( 'KEY' | 'INDEX' )? ( S_IDENTIFIER | S_QUOTED_IDENTIFIER )? ColumnsNamesList AlterExpressionUsingIndex? IndexWithComment? | ForeignKeySpec | 'CHECK' RelObjectName 'NOT'? 'ENFORCED' | AlterExpressionAddConstraint )
Referenced by:
@@ -14723,7 +15511,7 @@ Create .. raw:: html - + @@ -14735,28 +15523,30 @@ Create CreateFunctionStatement - CreateSchema + CreateSchema - CreateSequence - - CreateSynonym - - CreateTable - - CreateView - - CreatePolicy - TRIGGER - - DOMAIN - - captureRest - - CreateIndex + CreateDatabase + + CreateSequence + + CreateSynonym + + CreateTable + + CreateView + + CreatePolicy + TRIGGER + + DOMAIN + + captureRest + + CreateIndex
-
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
+
Create   ::= 'CREATE' ( 'OR' 'REPLACE' )? ( CreateFunctionStatement | CreateSchema | CreateDatabase | CreateSequence | CreateSynonym | CreateTable | CreateView | CreatePolicy | ( 'TRIGGER' | 'DOMAIN' )? captureRest | CreateIndex )
Referenced by:
@@ -15625,7 +16415,7 @@ K_DATE_LITERAL
           | 'SECOND'
+
====================================================================================================================== @@ -15938,7 +16728,7 @@ DATA_TYPE
           | TYPE_TIMESTAMP
+ ====================================================================================================================== @@ -16340,7 +17130,7 @@ S_DOUBLE
           | S_LONG ( '.' ( [eE] [+#x2D]? S_LONG )? | [eE] [+#x2D]? S_LONG )
+ ====================================================================================================================== @@ -16361,7 +17151,7 @@ S_LONG
S_LONG   ::= DIGIT+
+ ====================================================================================================================== @@ -16406,14 +17196,14 @@ S_HEX - 0x - - HEX_VALUE + 0x + + HEX_DIGIT
S_HEX    ::= 'X' ( "'" HEX_VALUE* "'" ' '* )+
-
           | '0x' HEX_VALUE+
+
           | '0x' HEX_DIGIT+
@@ -16446,6 +17236,31 @@ HEX_VALUE +====================================================================================================================== +HEX_DIGIT +====================================================================================================================== + + +.. raw:: html + + + + + + [0-9] + + [A-F] + + +
+ + +
         ::= [0-9A-F]
+
+ Referenced by: +
+ + ====================================================================================================================== LINE_COMMENT ====================================================================================================================== @@ -16473,6 +17288,31 @@ LINE_COMMENT +====================================================================================================================== +HASH_LINE_COMMENT +====================================================================================================================== + + +.. raw:: html + + + + + + [#x1] + + [^#xD#xA] + + +
+ + +
         ::= #x1 [^#xD#xA]*
+
+ Not referenced by any. +
+ + ====================================================================================================================== MULTI_LINE_COMMENT ====================================================================================================================== @@ -16548,7 +17388,7 @@ S_IDENTIFIER
           | '$' ( PART_LETTER_NO_DOLLAR PART_LETTER* )?
+ ====================================================================================================================== @@ -21033,7 +21873,7 @@ S_CHAR_LITERAL
         ::= ( [UENRB] | 'RB' | '_utf8' )? ( "'" ( ( ESC | "\'" | [^'\] )* | ( "''" | [^'] )+ ) "'" | "q'{" .* "}'" | "q'(" .* ")'" | "q'[" .* "]'" | "q''" .* "''" )
+ ====================================================================================================================== From a4145a81511fe0d5d74512af2b6041749acaf02f Mon Sep 17 00:00:00 2001 From: Andreas Reichel Date: Sun, 30 Aug 2026 10:15:55 +0700 Subject: [PATCH 040/387] build: automate the Gradle `publishToMavenCentral` task Signed-off-by: Andreas Reichel --- .github/workflows/ci.yml | 22 ++++++++++++++++------ build.gradle | 15 ++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3108f35b5..458fb4e75f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,8 +39,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: -# os: [ ubuntu-latest, windows-latest, macos-latest ] - os: [ ubuntu-latest, macos-latest ] + # os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest ] steps: - uses: actions/checkout@main with: @@ -75,10 +75,20 @@ jobs: uses: gradle/actions/setup-gradle@main - name: Publish with Gradle - run: ./gradlew publish + # Gradle only picks up a secret as a project property when it is exported with the + # ORG_GRADLE_PROJECT_ prefix. Plain `ossrhUsername: ...` was an ordinary environment + # variable that nothing ever read. + # + # No RELEASE variable here on purpose: getVersion() then produces a -SNAPSHOT version and + # the plugin routes it to the Central Portal snapshot repository. Set RELEASE=true only + # when you actually intend to cut a release. + run: ./gradlew publishToMavenCentral env: - ossrhUsername: ${{ secrets.OSSRHUSERNAME }} - ossrhPassword: ${{ secrets.OSSRHPASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.mavenCentralUsername }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.mavenCentralPassword }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.signingInMemoryKey }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.signingInMemoryKeyId }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.signingInMemoryKeyPassword }} - uses: actions/setup-python@main @@ -101,4 +111,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@main + uses: actions/deploy-pages@main \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5d225e5da7..9dd0c5f909 100644 --- a/build.gradle +++ b/build.gradle @@ -589,7 +589,9 @@ tasks.register('sphinx', Exec) { } } -publish { +// `publish` is Gradle's own task; the Central Portal upload runs under `publishToMavenCentral`. +// Both need the same prerequisites, or a CI job invoking the latter silently skips check and docs. +tasks.matching { it.name in ['publish', 'publishToMavenCentral'] }.configureEach { dependsOn(check, gitChangelogTask, renderRR, xslt, xmldoc) } @@ -636,14 +638,9 @@ mavenPublishing { } } -// Fix signing task dependencies -tasks.withType(AbstractPublishToMaven).configureEach { - dependsOn(tasks.withType(Sign)) -} - -signing { - required { !version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publish") } -} +// Signing and its task wiring are handled by mavenPublishing.signAllPublications() above. +// The manual `signing { required { ... hasTask("publish") } }` block that used to live here +// never matched the `publishToMavenCentral` task, so the two disagreed about whether to sign. tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' From e7167cc7dc15ce5fd6f49573b651ff7b1bdc4b3d Mon Sep 17 00:00:00 2001 From: fudian Date: Sun, 30 Aug 2026 11:41:02 +0800 Subject: [PATCH 041/387] feat(parser): add BIGQUERY, DATABRICKS and SNOWFLAKE dialect presets (#2515) Three warehouse presets for the lexer feature switches, each row taken from the vendor docs: BIGQUERY carries double quoted strings, # line comments, backslash escapes and the any-whitespace adjacent literal rule (GoogleSQL lexical structure; identifiers are backticked); DATABRICKS the same minus # comments (Spark literals; the double quoted identifiers switch is a non-default conf); SNOWFLAKE backslash escape sequences only, double quotes stay quoted identifiers (string and binary data types). Redshift was surveyed too and stays with the defaults on every switch, so it gains no preset entry. Follows up on the four warehouses named in #2512. --- .../jsqlparser/parser/AbstractJSqlParser.java | 10 +++- src/site/sphinx/usage.rst | 2 +- .../parser/CCJSqlParserUtilTest.java | 51 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 27880a2c71..cd4bfba7e4 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -37,7 +37,15 @@ public enum Dialect { Feature.allowHashLineComments, Feature.allowDoubleQuotedStrings), SQLSERVER(AdjacentStringLiterals.OFF, Feature.allowSquareBracketQuotation), POSTGRESQL( - AdjacentStringLiterals.NEWLINE), H2, EXASOL; + AdjacentStringLiterals.NEWLINE), H2, EXASOL, BIGQUERY( + AdjacentStringLiterals.WHITESPACE, + Feature.allowDoubleQuotedStrings, + Feature.allowHashLineComments, + Feature.allowBackslashEscapeCharacter), DATABRICKS( + AdjacentStringLiterals.WHITESPACE, + Feature.allowDoubleQuotedStrings, + Feature.allowBackslashEscapeCharacter), SNOWFLAKE( + Feature.allowBackslashEscapeCharacter); private final Set lexerFeatures; private final AdjacentStringLiterals adjacentStringLiterals; diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 3ad3190d98..e92173be9b 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -319,7 +319,7 @@ Additionally there are Features to control the Parser's effort at the cost of th .withBackslashEscapeCharacter(true) ); -Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. ``withDialect(Dialect.POSTGRESQL)`` and ``withDialect(Dialect.ANSI_SQL)`` turn on the newline rule for adjacent String Literals. Features set explicitly after the dialect preset win over the preset. +Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. ``withDialect(Dialect.POSTGRESQL)`` and ``withDialect(Dialect.ANSI_SQL)`` turn on the newline rule for adjacent String Literals. ``withDialect(Dialect.BIGQUERY)`` and ``withDialect(Dialect.DATABRICKS)`` turn on ``withDoubleQuotedStrings`` and ``withBackslashEscapeCharacter`` plus the any-whitespace rule for adjacent String Literals, the BigQuery preset additionally ``withHashLineComments``; ``withDialect(Dialect.SNOWFLAKE)`` turns on ``withBackslashEscapeCharacter`` only, keeping double quotes as quoted identifiers. Features set explicitly after the dialect preset win over the preset. .. code-block:: java diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 8add3b1bc1..40e61e708c 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -641,19 +641,51 @@ public void testDoubleQuotedStringsFeature() throws Exception { .parse("SELECT * FROM \"t\"", p -> p.withDoubleQuotedStrings(true)).toString()); } + @Test + public void testDialectPresetsWarehouses() throws Exception { + // BIGQUERY: double quoted strings, # line comments and backslash + // escapes (GoogleSQL "Lexical structure and syntax", identifiers are + // backticked), backticks stay unconditional + assertEquals("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42", CCJSqlParserUtil + .parse("SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24", + p -> p.withDialect(AbstractJSqlParser.Dialect.BIGQUERY)) + .toString()); + // DATABRICKS: double quoted strings and backslash escapes (Spark + // "Literals"), `#` stays the #2507 operator + assertEquals("SELECT \"a\" FROM t WHERE x = 'it\\'s'", CCJSqlParserUtil + .parse("SELECT \"a\" FROM t WHERE x = 'it\\'s'", + p -> p.withDialect(AbstractJSqlParser.Dialect.DATABRICKS)) + .toString()); + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24", + p -> p.withDialect(AbstractJSqlParser.Dialect.DATABRICKS)).toString()); + // SNOWFLAKE: backslash escape sequences ("String & binary data + // types"), `#` stays the operator + assertEquals("SELECT col FROM t WHERE a = 'x\\'yz'", CCJSqlParserUtil + .parse("SELECT col FROM t WHERE a = 'x\\'yz'", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)) + .toString()); + assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)).toString()); + } + @Test public void testDoubleQuotedStringsPreset() throws Exception { - // MYSQL and MARIADB presets carry the switch (default sql_mode reading) + // the string-default dialects: MYSQL and MARIADB (default sql_mode), + // BIGQUERY and DATABRICKS (string literals, identifiers are backticked) for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { - AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB}) { + AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB, + AbstractJSqlParser.Dialect.BIGQUERY, AbstractJSqlParser.Dialect.DATABRICKS}) { PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil .parse("SELECT \"abc\"", p -> p.withDialect(dialect))).getSelectBody(); assertTrue(select.getSelectItems().get(0).getExpression() instanceof StringValue); } // the identifier-default dialects keep the quoted identifier reading - PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"abc\"", - p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER))).getSelectBody(); - assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.SQLSERVER, AbstractJSqlParser.Dialect.SNOWFLAKE}) { + PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"abc\"", + p -> p.withDialect(dialect))).getSelectBody(); + assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column); + } } @Test @@ -717,6 +749,15 @@ public void testAdjacentStringLiteralsWhitespace() throws Exception { p -> p.withDoubleQuotedStrings(true).withAdjacentStringLiterals( AbstractJSqlParser.AdjacentStringLiterals.WHITESPACE)) .toString()); + // the BigQuery and Databricks presets carry it, the Snowflake preset + // does not (no adjacent-literal concatenation documented) + for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] { + AbstractJSqlParser.Dialect.BIGQUERY, AbstractJSqlParser.Dialect.DATABRICKS}) { + assertEquals("SELECT 'ab'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withDialect(dialect)).toString()); + } + assertEquals("SELECT 'a' 'b'", CCJSqlParserUtil.parse("SELECT 'a' 'b'", + p -> p.withDialect(AbstractJSqlParser.Dialect.SNOWFLAKE)).toString()); } @Test From 864181b74e42e8b8321823455d289349a870c709 Mon Sep 17 00:00:00 2001 From: fudian Date: Mon, 31 Aug 2026 21:50:29 +0800 Subject: [PATCH 042/387] fix: collect tables from all SELECT and DML trailing clauses in TablesNamesFinder (#2517) TablesNamesFinder visited only a subset of the Expression/Table-bearing children of PlainSelect, ParenthesedSelect, SetOperationList, Values, Delete, Update and FromItem-attached pivots, silently dropping tables located in INTO and INTO TEMP targets, DISTINCT ON, LATERAL VIEW generator functions, PREFERRING, GROUP BY (incl. GROUPING SETS), QUALIFY, WINDOW definitions, SETTINGS, PIVOT/UNPIVOT/PIVOT XML and the ORDER BY / LIMIT / LIMIT BY / OFFSET / FETCH clauses shared by every Select subclass (and by the MySQL single-table DELETE / UPDATE). Complete the traversal reusing the same ExpressionVisitor and FromItemVisitor default helpers used by SelectVisitorAdapter and StatementVisitorAdapter, and implement PivotVisitor so pivot contents (incl. the PivotXml IN subquery) are reached through dispatch. Signed-off-by: fudianchn --- .../sf/jsqlparser/util/TablesNamesFinder.java | 121 ++++++++++++- .../util/TablesNamesFinderTest.java | 159 ++++++++++++++++++ 2 files changed, 279 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 0e94b9c9e5..b3d1463d03 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -154,10 +154,14 @@ import net.sf.jsqlparser.statement.select.FunctionAllColumns; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; +import net.sf.jsqlparser.statement.select.LateralView; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.ParenthesedFromItem; import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.Pivot; import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.PivotVisitor; +import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; @@ -165,6 +169,7 @@ import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.TableFunction; +import net.sf.jsqlparser.statement.select.UnPivot; import net.sf.jsqlparser.statement.select.TableStatement; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; @@ -187,7 +192,7 @@ public class TablesNamesFinder implements SelectVisitor, FromItemVisitor, ExpressionVisitor, SelectItemVisitor, StatementVisitor, MergeOperationVisitor, - PipeOperatorVisitor { + PipeOperatorVisitor, PivotVisitor { private Set tables; private boolean allowColumnProcessing = false; @@ -330,6 +335,20 @@ public Void visit(ParenthesedSelect select, S context) { } } select.getSelect().accept((SelectVisitor) this, context); + visitOrderBy(select.getOrderByElements(), context); + if (select.getPivot() != null) { + select.getPivot().accept(this, context); + } + if (select.getUnPivot() != null) { + select.getUnPivot().accept(this, context); + } + visitLimit(select.getLimit(), context); + if (select.getOffset() != null) { + select.getOffset().getOffset().accept(this, context); + } + if (select.getFetch() != null) { + select.getFetch().getExpression().accept(this, context); + } return null; } @@ -346,6 +365,11 @@ public Void visit(PlainSelect plainSelect, S context) { withItem.accept((SelectVisitor) this, context); } } + if (plainSelect.getDistinct() != null) { + visitSelectItems(plainSelect.getDistinct().getOnSelectItems(), context); + } + visitTables(plainSelect.getIntoTables(), context); + if (plainSelect.getSelectItems() != null) { for (SelectItem item : plainSelect.getSelectItems()) { item.accept(this, context); @@ -356,6 +380,12 @@ public Void visit(PlainSelect plainSelect, S context) { plainSelect.getFromItem().accept(this, context); } + if (plainSelect.getLateralViews() != null) { + for (LateralView lateralView : plainSelect.getLateralViews()) { + lateralView.getGeneratorFunction().accept(this, context); + } + } + visitJoins(plainSelect.getJoins(), context); if (plainSelect.getPreWhere() != null) { plainSelect.getPreWhere().accept(this, context); @@ -364,13 +394,46 @@ public Void visit(PlainSelect plainSelect, S context) { plainSelect.getWhere().accept(this, context); } + visitPreferringClause(plainSelect.getPreferringClause(), context); + visit(plainSelect.getGroupBy(), context); + if (plainSelect.getHaving() != null) { plainSelect.getHaving().accept(this, context); } + if (plainSelect.getQualify() != null) { + plainSelect.getQualify().accept(this, context); + } + if (plainSelect.getOracleHierarchical() != null) { plainSelect.getOracleHierarchical().accept(this, context); } + + if (plainSelect.getWindowDefinitions() != null) { + for (WindowDefinition windowDefinition : plainSelect.getWindowDefinitions()) { + visitExpressions(windowDefinition.getPartitionExpressionList(), context); + visitOrderBy(windowDefinition.getOrderByElements(), context); + } + } + + if (plainSelect.getPivot() != null) { + plainSelect.getPivot().accept(this, context); + } + if (plainSelect.getUnPivot() != null) { + plainSelect.getUnPivot().accept(this, context); + } + + visitOrderBy(plainSelect.getOrderByElements(), context); + visitLimit(plainSelect.getLimit(), context); + visitLimit(plainSelect.getLimitBy(), context); + if (plainSelect.getOffset() != null) { + plainSelect.getOffset().getOffset().accept(this, context); + } + if (plainSelect.getFetch() != null) { + plainSelect.getFetch().getExpression().accept(this, context); + } + visitUpdateSets(plainSelect.getSettings(), context); + visitFromItem(plainSelect.getIntoTempTable(), context); return null; } @@ -431,6 +494,12 @@ public Void visit(Table table, S context) { if (!otherItemNames.contains(tableWholeName)) { tables.add(tableWholeName); } + if (table.getPivot() != null) { + table.getPivot().accept(this, context); + } + if (table.getUnPivot() != null) { + table.getUnPivot().accept(this, context); + } return null; } @@ -883,6 +952,14 @@ public Void visit(SetOperationList list, S context) { for (Select selectBody : list.getSelects()) { selectBody.accept((SelectVisitor) this, context); } + visitOrderBy(list.getOrderByElements(), context); + visitLimit(list.getLimit(), context); + if (list.getOffset() != null) { + list.getOffset().getOffset().accept(this, context); + } + if (list.getFetch() != null) { + list.getFetch().getExpression().accept(this, context); + } return null; } @@ -941,6 +1018,36 @@ public Void visit(FromQuery fromQuery, S context) { return null; } + @Override + public Void visit(Pivot pivot, S context) { + visitSelectItems(pivot.getFunctionItems(), context); + visitExpressions(pivot.getForColumns(), context); + visitSelectItems(pivot.getSingleInItems(), context); + visitSelectItems(pivot.getMultiInItems(), context); + return null; + } + + @Override + public Void visit(PivotXml pivotXml, S context) { + visit((Pivot) pivotXml, context); + if (pivotXml.getInSelect() != null) { + pivotXml.getInSelect().accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public Void visit(UnPivot unpivot, S context) { + for (Column column : unpivot.getUnPivotClause()) { + column.accept(this, context); + } + for (Column column : unpivot.getUnPivotForClause()) { + column.accept(this, context); + } + visitSelectItems(unpivot.getUnPivotInClause(), context); + return null; + } + @Override public Void visit(AggregatePipeOperator aggregate, Void context) { for (SelectItem selectItem : aggregate.getSelectItems()) { @@ -1220,6 +1327,8 @@ public Void visit(Delete delete, S context) { if (delete.getWhere() != null) { delete.getWhere().accept(this, context); } + visitOrderBy(delete.getOrderByElements(), context); + visitLimit(delete.getLimit(), context); visitOutputClause(delete.getOutputClause(), context); visitReturningClause(delete.getReturningClause(), context); return null; @@ -1279,6 +1388,8 @@ public Void visit(Update update, S context) { if (update.getWhere() != null) { update.getWhere().accept(this, context); } + visitOrderBy(update.getOrderByElements(), context); + visitLimit(update.getLimit(), context); visitOutputClause(update.getOutputClause(), context); visitReturningClause(update.getReturningClause(), context); return null; @@ -1790,6 +1901,14 @@ public void visit(Comment comment) { @Override public Void visit(Values values, S context) { values.getExpressions().accept(this, context); + visitOrderBy(values.getOrderByElements(), context); + visitLimit(values.getLimit(), context); + if (values.getOffset() != null) { + values.getOffset().getOffset().accept(this, context); + } + if (values.getFetch() != null) { + values.getFetch().getExpression().accept(this, context); + } return null; } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 53a33e9882..e7db44ab91 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -907,4 +907,163 @@ void testAnalyticFunctionsWithFilterClause() throws JSQLParserException { "MY_TABLE2"); } + @Test + void testSelectIntoTables() throws JSQLParserException { + String sqlStr = "SELECT * INTO MY_TABLE1 FROM MY_TABLE2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectIntoTempTable() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE2 INTO TEMP MY_TABLE1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testGroupBySubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 GROUP BY A, (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testGroupByGroupingSetsSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 GROUP BY GROUPING SETS ((A, (SELECT B FROM MY_TABLE2)), (C))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectQualifySubquery() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE1 QUALIFY (SELECT B FROM MY_TABLE2) = 1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testWindowDefinitionSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 WINDOW W AS (PARTITION BY (SELECT B FROM MY_TABLE2))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectOrderBySubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 ORDER BY A, (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectLimitSubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 LIMIT (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectLimitBySubquery() throws JSQLParserException { + String sqlStr = "SELECT A FROM MY_TABLE1 LIMIT 2 BY (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectOffsetSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 LIMIT 1 OFFSET (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectFetchSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 OFFSET 1 ROWS FETCH NEXT (SELECT B FROM MY_TABLE2) ROWS ONLY"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testSelectDistinctOnSubquery() throws JSQLParserException { + String sqlStr = "SELECT DISTINCT ON ((SELECT B FROM MY_TABLE2)) A FROM MY_TABLE1"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testLateralViewSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 LATERAL VIEW EXPLODE(ARRAY((SELECT B FROM MY_TABLE2))) V AS X"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testTablePivotXmlSubquery() throws JSQLParserException { + String sqlStr = + "SELECT * FROM MY_TABLE1 PIVOT XML (SUM(X) FOR Y IN (SELECT Z FROM MY_TABLE2))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testTablePivotSimple() throws JSQLParserException { + String sqlStr = "SELECT * FROM MY_TABLE1 PIVOT (SUM(X) FOR Y IN ('a', 'b'))"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1"); + } + + @Test + void testParenthesedOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "(SELECT A FROM MY_TABLE1) ORDER BY (SELECT B FROM MY_TABLE2) LIMIT (SELECT C FROM MY_TABLE3)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3"); + } + + @Test + void testSetOperationOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 UNION SELECT B FROM MY_TABLE2 ORDER BY (SELECT C FROM MY_TABLE3) LIMIT (SELECT D FROM MY_TABLE4)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4"); + } + + @Test + void testSelectSettingsSubquery() throws JSQLParserException { + String sqlStr = + "SELECT A FROM MY_TABLE1 SETTINGS X = (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testValuesOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "VALUES (1), (2) ORDER BY (SELECT A FROM MY_TABLE1) LIMIT (SELECT B FROM MY_TABLE2)"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testDeleteOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "DELETE FROM MY_TABLE1 WHERE A = 1 ORDER BY (SELECT B FROM MY_TABLE2) LIMIT 2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateOrderByLimitSubquery() throws JSQLParserException { + String sqlStr = + "UPDATE MY_TABLE1 SET A = 1 ORDER BY (SELECT B FROM MY_TABLE2) LIMIT 2"; + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + } From 9a32ff568f580178d9a27b101596aa43eb45d0b7 Mon Sep 17 00:00:00 2001 From: fudian Date: Mon, 31 Aug 2026 21:51:37 +0800 Subject: [PATCH 043/387] fix: prevent bare partition columns from inheriting the previous item's value (#2518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partitions() shares one production-level valueExpression across list items, so a bare (dynamic) partition column kept the value of the preceding item: PARTITION (dtime = '2024-04-03', region) deparsed as region = '2024-04-03', silently turning a dynamic partition into a static assignment. Reset the variable per item, same idiom as DeclareType, Alias and ColumnsNamesList. Fixes #2500 Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 +- .../statement/insert/InsertTest.java | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 62d22a38a6..d52b7a9c2e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4065,7 +4065,7 @@ List Partitions(): ( LOOKAHEAD(2) ( "," - tableColumn=Column() [ "=" valueExpression=Expression() ] + tableColumn=Column() { valueExpression = null; } [ "=" valueExpression=Expression() ] { partitions.add( new Partition (tableColumn, valueExpression)); } ) )* diff --git a/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java b/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java index 18a9019e1e..f575ede245 100644 --- a/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/insert/InsertTest.java @@ -966,6 +966,58 @@ void testInsertOverwrite() throws JSQLParserException { assertFalse(insert.isOverwrite()); } + @Test + // a dynamic partition column without a value must not inherit the value of a + // preceding static partition column (#2500) + void testInsertMixedStaticAndDynamicPartitions() throws JSQLParserException { + String sqlStr = + "INSERT OVERWRITE TABLE t PARTITION (dtime = '2024-04-03', region) SELECT * FROM a"; + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(0).getValue().toString()); + assertEquals("region", insert.getPartitions().get(1).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(1).getValue()); + + sqlStr = + "INSERT OVERWRITE TABLE t PARTITION (region, dtime = '2024-04-03') SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("region", insert.getPartitions().get(0).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(0).getValue()); + assertEquals("dtime", insert.getPartitions().get(1).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(1).getValue().toString()); + + sqlStr = "INSERT OVERWRITE TABLE t PARTITION" + + " (dtime = '2024-04-03', region, dept) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(3, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("'2024-04-03'", insert.getPartitions().get(0).getValue().toString()); + assertEquals("region", insert.getPartitions().get(1).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(1).getValue()); + assertEquals("dept", insert.getPartitions().get(2).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(2).getValue()); + + sqlStr = "INSERT OVERWRITE TABLE t PARTITION (a, b = '1', c) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(3, insert.getPartitions().size()); + assertEquals("a", insert.getPartitions().get(0).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(0).getValue()); + assertEquals("b", insert.getPartitions().get(1).getColumn().getColumnName()); + assertEquals("'1'", insert.getPartitions().get(1).getValue().toString()); + assertEquals("c", insert.getPartitions().get(2).getColumn().getColumnName()); + assertNull(insert.getPartitions().get(2).getValue()); + + sqlStr = "INSERT INTO TABLE t PARTITION (dtime = 20240403, region) SELECT * FROM a"; + insert = (Insert) assertSqlCanBeParsedAndDeparsed(sqlStr); + assertEquals(2, insert.getPartitions().size()); + assertEquals("dtime", insert.getPartitions().get(0).getColumn().getColumnName()); + assertEquals("20240403", insert.getPartitions().get(0).getValue().toString()); + assertNull(insert.getPartitions().get(1).getValue()); + assertFalse(insert.isOverwrite()); + } + @ParameterizedTest @ValueSource(strings = { "INSERT INTO mytable (foo) OVERRIDING SYSTEM VALUE VALUES (1)", From 33b03e80b3bcd8e414937c9cc0386e1a42db30ee Mon Sep 17 00:00:00 2001 From: fudian Date: Thu, 3 Sep 2026 14:35:55 +0800 Subject: [PATCH 044/387] fix: memoize long-chain predicate walks behind a size threshold (#2520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isFunctionAhead and isAllTableColumnsAhead walk the whole remaining dotted-name chain unbounded, and the phase-2 lookahead routines re-evaluate them at every position, so long chains like SELECT a.b.b... FROM t parse in quadratic time (a 64 KB statement hits the default 8 s Feature.timeOut). The walk now aborts past 8 delimiter/part pairs and restarts through a per-start-token cache; one filling walk records the answer for every eligible chain part (each part walks the identical suffix), keeping total walk work linear. Chains within the threshold finish in a single plain walk with no map access and no allocation. Full suite 4972/0 unchanged. This removes the predicate-layer share only; the remaining super-linear time sits in the phase-2 lookahead machinery itself. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 127 +++++++++++++++--- .../parser/LongChainPredicateTest.java | 94 +++++++++++++ 2 files changed, 205 insertions(+), 16 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d52b7a9c2e..1cb16a094c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -347,15 +347,55 @@ public class CCJSqlParser extends AbstractJSqlParser { * from column references like col, schema.col, a.b.c.col. * * Replaces LOOKAHEAD(16) on Function() with a targeted O(chain-length) check. + * Long chains are memoized per start token: the phase-2 lookahead routines + * re-evaluate the same walk many times per position and each chain part + * would walk the very same suffix again, which makes long chains parse in + * quadratic time. Chains up to CHAIN_CACHE_THRESHOLD parts skip the cache + * entirely, keeping the normal short-name path allocation- and map-free. */ + private static final int CHAIN_CACHE_THRESHOLD = 8; + + // Sentinel returned by the chain walks when the non-filling pass ran past + // CHAIN_CACHE_THRESHOLD delimiter/part pairs and the caller must retry + // through the per-token cache. + private static final int LONG_CHAIN = -1; + + private final Map isFunctionAheadCache = new HashMap(); + protected boolean isFunctionAhead() { + int r = isFunctionAheadWalk(false); + if (r != LONG_CHAIN) { + return r == 1; + } + Token key = getToken(1); + Boolean cached = isFunctionAheadCache.get(key); + if (cached != null) { + return cached; + } + boolean result = isFunctionAheadWalk(true) == 1; + isFunctionAheadCache.put(key, result); + return result; + } + + // 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) + && 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; + } + + // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded + // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass). + private int isFunctionAheadWalk(boolean fill) { try { int i = 1; Token t = getToken(i); // JDBC escape function: {fn ...} — must check for FN keyword if (t.image.equals("{")) { - return getToken(2).kind == K_FN; + return getToken(2).kind == K_FN ? 1 : 0; } // Optional APPROXIMATE keyword @@ -367,22 +407,30 @@ public class CCJSqlParser extends AbstractJSqlParser { // Exclude tokens that have their own dedicated branches // after Function() in PrimaryExpression if (isNonFunctionKeyword(t)) { - return false; + return 0; } // 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) { - return false; + return 0; } i++; // Walk through dotted name chain + int delimiters = 0; + List chainParts = fill ? new ArrayList() : null; while (true) { t = getToken(i); if (t.image.equals(".") || t.image.equals("..") || t.image.equals("...") || t.image.equals(":")) { + if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) { + return LONG_CHAIN; + } + if (fill) { + chainParts.add(getToken(i + 1)); + } i++; // skip delimiter i++; // skip next name part } else { @@ -390,20 +438,23 @@ public class CCJSqlParser extends AbstractJSqlParser { } } - // Must be followed by ( - if (getToken(i).kind != OPENING_BRACKET) { - return false; - } + // Must be followed by (, and not by the Oracle join syntax column(+) + boolean result = getToken(i).kind == OPENING_BRACKET + && !(getToken(i + 1).image.equals("+") + && getToken(i + 2).kind == CLOSING_BRACKET); - // Exclude Oracle join syntax: column(+) - if (getToken(i + 1).image.equals("+") - && getToken(i + 2).kind == CLOSING_BRACKET) { - return false; + // Each eligible chain part would walk the same remaining suffix and + // obtain the same answer, so memoize them all in one pass. + if (fill) { + for (Token part : chainParts) { + if (isFunctionAheadChainStartEligible(part)) { + isFunctionAheadCache.put(part, result); + } + } } - - return true; + return result ? 1 : 0; } catch (TokenMgrException e) { - return false; + return 0; } } @@ -736,8 +787,30 @@ public class CCJSqlParser extends AbstractJSqlParser { /** * Scans ahead through a dotted identifier chain and checks if '*' follows. * Identifies table.* patterns for AllTableColumns. + * Long chains are memoized per start token like {@link #isFunctionAhead()} + * (one walk fills every eligible chain part); short chains skip the cache. */ + private final Map isAllTableColumnsAheadCache = + new HashMap(); + protected boolean isAllTableColumnsAhead() { + int r = isAllTableColumnsWalk(false); + if (r != LONG_CHAIN) { + return r == 1; + } + Token key = getToken(1); + Boolean cached = isAllTableColumnsAheadCache.get(key); + if (cached != null) { + return cached; + } + boolean result = isAllTableColumnsWalk(true) == 1; + isAllTableColumnsAheadCache.put(key, result); + return result; + } + + // Returns 1 for true, 0 for false, or LONG_CHAIN when the walk exceeded + // CHAIN_CACHE_THRESHOLD delimiter/part pairs (only in the non-filling pass). + private int isAllTableColumnsWalk(boolean fill) { int i = 1; Token t = getToken(i); @@ -745,15 +818,23 @@ public class CCJSqlParser extends AbstractJSqlParser { 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) { - return false; + return 0; } i++; // Walk through dotted name chain + int delimiters = 0; + List chainParts = fill ? new ArrayList() : null; while (true) { t = getToken(i); if (t.image.equals(".") || t.image.equals("..") || t.image.equals("...")) { + if (!fill && ++delimiters > CHAIN_CACHE_THRESHOLD) { + return LONG_CHAIN; + } + if (fill) { + chainParts.add(getToken(i + 1)); + } i++; // skip delimiter i++; // skip next part (could be "*") } else { @@ -764,7 +845,21 @@ public class CCJSqlParser extends AbstractJSqlParser { // It's AllTableColumns if the chain ended on "*" // i.e., the last name part we skipped over was "*" // Back up: the last token consumed was at (i-1) - return getToken(i - 1).image.equals("*"); + boolean result = getToken(i - 1).image.equals("*"); + + // Each chain part that would pass the name-like guard walks the same + // remaining suffix and obtains the same answer. + if (fill) { + for (Token part : chainParts) { + if (part.kind == S_LONG || part.kind == S_DOUBLE || part.kind == S_HEX + || part.kind == S_CHAR_LITERAL || part.kind == OPENING_BRACKET + || part.kind == CLOSING_BRACKET || part.kind == EOF) { + continue; + } + isAllTableColumnsAheadCache.put(part, result); + } + } + return result ? 1 : 0; } /** diff --git a/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java b/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java new file mode 100644 index 0000000000..d1b19cdf1d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LongChainPredicateTest.java @@ -0,0 +1,94 @@ +package net.sf.jsqlparser.parser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.AllTableColumns; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; + +/** + * Guards the threshold-gated chain-walk memoization in isFunctionAhead() / + * isAllTableColumnsAhead(): chains on both sides of CHAIN_CACHE_THRESHOLD and inside the memoized + * path must keep the exact same AST shapes as the plain walk. The speed-up itself is + * constant-factor only and is carried by the measured numbers, not by a CI timing assertion. + */ +public class LongChainPredicateTest { + + private static String chain(int innerDelimiters, String last) { + // innerDelimiters = dots between s0..sN (chain length before the last part) + StringBuilder sb = new StringBuilder("s0"); + for (int i = 1; i <= innerDelimiters; i++) { + sb.append(".s").append(i); + } + return sb.append(".").append(last).toString(); + } + + private Object firstExpression(String sql) throws Exception { + Select select = (Select) CCJSqlParserUtil.parse(sql); + return ((PlainSelect) select).getSelectItems().get(0).getExpression(); + } + + @Test + void functionOnShortChainFastPath() throws Exception { + // total 8 delimiters: the walk stays inside the plain non-cached path + String sql = "SELECT " + chain(7, "f(1)") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Function, + "8-delimiter chain ending in ( must stay a Function"); + } + + @Test + void functionOnLongChainMemoPath() throws Exception { + // total 9 delimiters: the walk aborts past the threshold and goes through the cache + String sql = "SELECT " + chain(8, "f(1)") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Function, + "9-delimiter chain ending in ( must stay a Function on the memoized path"); + } + + @Test + void columnOnShortChainFastPath() throws Exception { + String sql = "SELECT " + chain(7, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + assertFalse(firstExpression(sql) instanceof Function); + } + + @Test + void columnOnLongChainMemoPath() throws Exception { + String sql = "SELECT " + chain(8, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + assertFalse(firstExpression(sql) instanceof Function); + } + + @Test + void columnOnVeryLongChainMemoPath() throws Exception { + String sql = "SELECT " + chain(40, "col") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(sql); + assertTrue(firstExpression(sql) instanceof Column); + } + + @Test + void allTableColumnsAcrossThreshold() throws Exception { + String shortChain = "SELECT " + chain(7, "*") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(shortChain); + assertTrue(firstExpression(shortChain) instanceof AllTableColumns); + + String longChain = "SELECT " + chain(8, "*") + " FROM t"; + assertSqlCanBeParsedAndDeparsed(longChain); + assertTrue(firstExpression(longChain) instanceof AllTableColumns); + } + + @Test + void oracleOuterJoinColumnPlusOnMemoPath() throws Exception { + // the column(+) exclusion must survive the memoized walk + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a, b WHERE " + chain(8, "x(+)") + " = b.x"); + } +} From 536fe3b62de6e43d11202cdbe73e5d708925e069 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 15:38:22 +0900 Subject: [PATCH 045/387] Add MySQL CREATE TABLE partition parsing (#2521) --- .../statement/create/table/CreateTable.java | 17 + .../create/table/PartitionDefinition.java | 225 +++++++++++++- .../create/table/TablePartitioning.java | 280 +++++++++++++++++ .../util/deparser/CreateTableDeParser.java | 3 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 292 ++++++++++++++++-- .../create/MySQLCreateTablePartitionTest.java | 175 +++++++++++ 6 files changed, 960 insertions(+), 32 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index d2be0e0264..acbe0b7184 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -36,6 +36,7 @@ public class CreateTable implements Statement { private boolean selectParenthesis; private boolean ifNotExists = false; private boolean orReplace = false; + private TablePartitioning partitioning; private RowMovement rowMovement; @@ -148,6 +149,14 @@ public void setOrReplace(boolean orReplace) { this.orReplace = orReplace; } + public TablePartitioning getPartitioning() { + return partitioning; + } + + public void setPartitioning(TablePartitioning partitioning) { + this.partitioning = partitioning; + } + public boolean isSelectParenthesis() { return selectParenthesis; } @@ -216,6 +225,9 @@ private void appendTableOptions(StringBuilder b) { if (options != null && options.length() > 0) { b.append(" ").append(options); } + if (partitioning != null) { + b.append(" ").append(partitioning); + } } private void appendTableProperties(StringBuilder b) { @@ -272,6 +284,11 @@ public CreateTable withIfNotExists(boolean ifNotExists) { return this; } + public CreateTable withPartitioning(TablePartitioning partitioning) { + this.setPartitioning(partitioning); + return this; + } + public CreateTable withRowMovement(RowMovement rowMovement) { this.setRowMovement(rowMovement); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java index 24d15dfd04..0f654326f2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java @@ -14,15 +14,51 @@ import net.sf.jsqlparser.statement.select.PlainSelect; public class PartitionDefinition implements Serializable { + + public enum ValueOperator { + LESS_THAN("VALUES LESS THAN"), IN("VALUES IN"); + + private final String sql; + + ValueOperator(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } + } + private String partitionName; private String partitionOperation; private List values; private String storageEngine; + private ValueOperator valueOperator; + private boolean maxValue; + private boolean subPartition; + private boolean storageKeyword; + private boolean storageEngineUseEquals = true; + private String comment; + private boolean commentUseEquals; + private String dataDirectory; + private boolean dataDirectoryUseEquals; + private String indexDirectory; + private boolean indexDirectoryUseEquals; + private String maxRows; + private boolean maxRowsUseEquals; + private String minRows; + private boolean minRowsUseEquals; + private String tablespace; + private boolean tablespaceUseEquals; + private List subPartitionDefinitions; + + public PartitionDefinition() {} public PartitionDefinition(String partitionName, String partitionOperation, List values, String storageEngine) { this.partitionName = partitionName; - this.partitionOperation = partitionOperation; + setPartitionOperation(partitionOperation); this.values = values; this.storageEngine = storageEngine; } @@ -41,6 +77,13 @@ public String getPartitionOperation() { public void setPartitionOperation(String partitionOperation) { this.partitionOperation = partitionOperation; + if (ValueOperator.LESS_THAN.toString().equalsIgnoreCase(partitionOperation)) { + valueOperator = ValueOperator.LESS_THAN; + } else if (ValueOperator.IN.toString().equalsIgnoreCase(partitionOperation)) { + valueOperator = ValueOperator.IN; + } else { + valueOperator = null; + } } public List getValues() { @@ -59,16 +102,186 @@ public void setStorageEngine(String storageEngine) { this.storageEngine = storageEngine; } + public ValueOperator getValueOperator() { + return valueOperator; + } + + public void setValueOperator(ValueOperator valueOperator) { + this.valueOperator = valueOperator; + this.partitionOperation = valueOperator != null ? valueOperator.toString() : null; + } + + public boolean isMaxValue() { + return maxValue; + } + + public void setMaxValue(boolean maxValue) { + this.maxValue = maxValue; + } + + public boolean isSubPartition() { + return subPartition; + } + + public void setSubPartition(boolean subPartition) { + this.subPartition = subPartition; + } + + public boolean isStorageKeyword() { + return storageKeyword; + } + + public void setStorageKeyword(boolean storageKeyword) { + this.storageKeyword = storageKeyword; + } + + public boolean isStorageEngineUseEquals() { + return storageEngineUseEquals; + } + + public void setStorageEngineUseEquals(boolean storageEngineUseEquals) { + this.storageEngineUseEquals = storageEngineUseEquals; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public boolean isCommentUseEquals() { + return commentUseEquals; + } + + public void setCommentUseEquals(boolean commentUseEquals) { + this.commentUseEquals = commentUseEquals; + } + + public String getDataDirectory() { + return dataDirectory; + } + + public void setDataDirectory(String dataDirectory) { + this.dataDirectory = dataDirectory; + } + + public boolean isDataDirectoryUseEquals() { + return dataDirectoryUseEquals; + } + + public void setDataDirectoryUseEquals(boolean dataDirectoryUseEquals) { + this.dataDirectoryUseEquals = dataDirectoryUseEquals; + } + + public String getIndexDirectory() { + return indexDirectory; + } + + public void setIndexDirectory(String indexDirectory) { + this.indexDirectory = indexDirectory; + } + + public boolean isIndexDirectoryUseEquals() { + return indexDirectoryUseEquals; + } + + public void setIndexDirectoryUseEquals(boolean indexDirectoryUseEquals) { + this.indexDirectoryUseEquals = indexDirectoryUseEquals; + } + + public String getMaxRows() { + return maxRows; + } + + public void setMaxRows(String maxRows) { + this.maxRows = maxRows; + } + + public boolean isMaxRowsUseEquals() { + return maxRowsUseEquals; + } + + public void setMaxRowsUseEquals(boolean maxRowsUseEquals) { + this.maxRowsUseEquals = maxRowsUseEquals; + } + + public String getMinRows() { + return minRows; + } + + public void setMinRows(String minRows) { + this.minRows = minRows; + } + + public boolean isMinRowsUseEquals() { + return minRowsUseEquals; + } + + public void setMinRowsUseEquals(boolean minRowsUseEquals) { + this.minRowsUseEquals = minRowsUseEquals; + } + + public String getTablespace() { + return tablespace; + } + + public void setTablespace(String tablespace) { + this.tablespace = tablespace; + } + + public boolean isTablespaceUseEquals() { + return tablespaceUseEquals; + } + + public void setTablespaceUseEquals(boolean tablespaceUseEquals) { + this.tablespaceUseEquals = tablespaceUseEquals; + } + + public List getSubPartitionDefinitions() { + return subPartitionDefinitions; + } + + public void setSubPartitionDefinitions( + List subPartitionDefinitions) { + this.subPartitionDefinitions = subPartitionDefinitions; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append("PARTITION ").append(partitionName) - .append(" ").append(partitionOperation) - .append(" (").append(PlainSelect.getStringList(values)) - .append(")"); + b.append(subPartition ? "SUBPARTITION " : "PARTITION ").append(partitionName); + if (partitionOperation != null) { + b.append(" ").append(partitionOperation); + if (maxValue) { + b.append(" MAXVALUE"); + } else { + b.append(" (").append(PlainSelect.getStringList(values)).append(")"); + } + } if (storageEngine != null) { - b.append(" ENGINE = ").append(storageEngine); + if (storageKeyword) { + b.append(" STORAGE"); + } + appendOption(b, "ENGINE", storageEngine, storageEngineUseEquals); + } + appendOption(b, "COMMENT", comment, commentUseEquals); + appendOption(b, "DATA DIRECTORY", dataDirectory, dataDirectoryUseEquals); + appendOption(b, "INDEX DIRECTORY", indexDirectory, indexDirectoryUseEquals); + appendOption(b, "MAX_ROWS", maxRows, maxRowsUseEquals); + appendOption(b, "MIN_ROWS", minRows, minRowsUseEquals); + appendOption(b, "TABLESPACE", tablespace, tablespaceUseEquals); + if (subPartitionDefinitions != null && !subPartitionDefinitions.isEmpty()) { + b.append(" ").append(PlainSelect.getStringList(subPartitionDefinitions, true, true)); } return b.toString(); } + + private static void appendOption(StringBuilder builder, String name, String value, + boolean useEquals) { + if (value != null) { + builder.append(" ").append(name).append(useEquals ? " = " : " ").append(value); + } + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java new file mode 100644 index 0000000000..fa0ce42c01 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java @@ -0,0 +1,280 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** + * MySQL table partitioning options used by {@code CREATE TABLE}. + */ +public class TablePartitioning implements Serializable { + + public enum Type { + HASH, KEY, RANGE, LIST + } + + private Type type; + private boolean linear; + private boolean columnsSyntax; + private Expression expression; + private ExpressionList columns; + private Integer algorithm; + private boolean algorithmUseEquals; + private Long partitions; + private TablePartitioning subPartitioning; + private List partitionDefinitions; + private List partitionOptions; + + public TablePartitioning() {} + + public TablePartitioning(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public boolean isLinear() { + return linear; + } + + public void setLinear(boolean linear) { + this.linear = linear; + } + + public boolean isColumnsSyntax() { + return columnsSyntax; + } + + public void setColumnsSyntax(boolean columnsSyntax) { + this.columnsSyntax = columnsSyntax; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public Integer getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(Integer algorithm) { + this.algorithm = algorithm; + } + + public boolean isAlgorithmUseEquals() { + return algorithmUseEquals; + } + + public void setAlgorithmUseEquals(boolean algorithmUseEquals) { + this.algorithmUseEquals = algorithmUseEquals; + } + + public Long getPartitions() { + return partitions; + } + + public void setPartitions(Long partitions) { + this.partitions = partitions; + } + + public TablePartitioning getSubPartitioning() { + return subPartitioning; + } + + public void setSubPartitioning(TablePartitioning subPartitioning) { + this.subPartitioning = subPartitioning; + } + + public List getPartitionDefinitions() { + return partitionDefinitions; + } + + public void setPartitionDefinitions(List partitionDefinitions) { + this.partitionDefinitions = partitionDefinitions; + } + + public List getPartitionOptions() { + return partitionOptions; + } + + public void setPartitionOptions(List partitionOptions) { + this.partitionOptions = partitionOptions; + } + + public TablePartitioning withType(Type type) { + setType(type); + return this; + } + + public TablePartitioning withLinear(boolean linear) { + setLinear(linear); + return this; + } + + public TablePartitioning withColumnsSyntax(boolean columnsSyntax) { + setColumnsSyntax(columnsSyntax); + return this; + } + + public TablePartitioning withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public TablePartitioning withColumns(ExpressionList columns) { + setColumns(columns); + return this; + } + + public TablePartitioning withAlgorithm(Integer algorithm) { + setAlgorithm(algorithm); + return this; + } + + public TablePartitioning withAlgorithmUseEquals(boolean algorithmUseEquals) { + setAlgorithmUseEquals(algorithmUseEquals); + return this; + } + + public TablePartitioning withPartitions(Long partitions) { + setPartitions(partitions); + return this; + } + + public TablePartitioning withSubPartitioning(TablePartitioning subPartitioning) { + setSubPartitioning(subPartitioning); + return this; + } + + public TablePartitioning withPartitionDefinitions( + List partitionDefinitions) { + setPartitionDefinitions(partitionDefinitions); + return this; + } + + public TablePartitioning withPartitionOptions(List partitionOptions) { + setPartitionOptions(partitionOptions); + return this; + } + + public TablePartitioning addColumns(Column... columns) { + ExpressionList collection = + Optional.ofNullable(getColumns()).orElseGet(ExpressionList::new); + Collections.addAll(collection, columns); + return withColumns(collection); + } + + public TablePartitioning addColumns(Collection columns) { + ExpressionList collection = + Optional.ofNullable(getColumns()).orElseGet(ExpressionList::new); + collection.addAll(columns); + return withColumns(collection); + } + + public TablePartitioning addPartitionDefinitions( + PartitionDefinition... partitionDefinitions) { + List collection = Optional.ofNullable(getPartitionDefinitions()) + .orElseGet(ArrayList::new); + Collections.addAll(collection, partitionDefinitions); + return withPartitionDefinitions(collection); + } + + public TablePartitioning addPartitionDefinitions( + Collection partitionDefinitions) { + List collection = Optional.ofNullable(getPartitionDefinitions()) + .orElseGet(ArrayList::new); + collection.addAll(partitionDefinitions); + return withPartitionDefinitions(collection); + } + + public TablePartitioning addPartitionOptions(String... partitionOptions) { + List collection = + Optional.ofNullable(getPartitionOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, partitionOptions); + return withPartitionOptions(collection); + } + + public TablePartitioning addPartitionOptions(Collection partitionOptions) { + List collection = + Optional.ofNullable(getPartitionOptions()).orElseGet(ArrayList::new); + collection.addAll(partitionOptions); + return withPartitionOptions(collection); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("PARTITION BY "); + appendMethod(builder); + if (partitions != null) { + builder.append(" PARTITIONS ").append(partitions); + } + if (subPartitioning != null) { + builder.append(" SUBPARTITION BY "); + subPartitioning.appendMethod(builder); + if (subPartitioning.getPartitions() != null) { + builder.append(" SUBPARTITIONS ").append(subPartitioning.getPartitions()); + } + } + if (partitionDefinitions != null && !partitionDefinitions.isEmpty()) { + builder.append(" ") + .append(PlainSelect.getStringList(partitionDefinitions, true, true)); + } + if (partitionOptions != null && !partitionOptions.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(partitionOptions, false, false)); + } + return builder.toString(); + } + + private void appendMethod(StringBuilder builder) { + if (linear) { + builder.append("LINEAR "); + } + builder.append(type); + if (algorithm != null) { + builder.append(" ALGORITHM"); + builder.append(algorithmUseEquals ? " = " : " ").append(algorithm); + } + if (columnsSyntax) { + builder.append(" COLUMNS"); + } + if (expression != null) { + builder.append(" (").append(expression).append(")"); + } else if (columns != null) { + builder.append(" ").append(PlainSelect.getStringList(columns, true, true)); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java index 1d9fc85f35..391b08889b 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -96,6 +96,9 @@ public void deParse(CreateTable createTable) { if (!"".equals(params)) { builder.append(' ').append(params); } + if (createTable.getPartitioning() != null) { + builder.append(' ').append(createTable.getPartitioning()); + } if (createTable.getRowMovement() != null) { builder.append(' ').append(createTable.getRowMovement().getMode().toString()) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1cb16a094c..d7e98b949f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1524,6 +1524,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1610,7 +1611,9 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1629,10 +1632,12 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1674,6 +1679,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1744,11 +1750,14 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -11538,6 +11547,7 @@ CreateTable CreateTable(boolean isUsingOrReplace): RowMovement rowMovement = null; String tableColumn = null; List columns = new ArrayList(); + TablePartitioning partitioning = null; } { { createTable.setOrReplace(isUsingOrReplace);} @@ -11579,7 +11589,10 @@ CreateTable CreateTable(boolean isUsingOrReplace): ) ) ] - ( LOOKAHEAD(2, { getToken(1).kind != K_AS }) parameter=CreateParameter() { tableOptions.addAll(parameter); } )* + ( LOOKAHEAD(2, { getToken(1).kind != K_AS + && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) }) + parameter=CreateParameter() { tableOptions.addAll(parameter); } )* + [ partitioning=CreateTablePartitioning() { createTable.setPartitioning(partitioning); } ] // see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725 // table properties , all these are optional @@ -12068,6 +12081,9 @@ List CreateParameter(): // "WITH PARSER" option, and the FULLTEXT / SPATIAL index types of CREATE INDEX. | tk= | tk= | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk="=" ) { param.add(tk.image); } @@ -12532,49 +12548,273 @@ void IndexOption(List list) : ) } -List PartitionDefinitions(): +TablePartitioning CreateTablePartitioning(): { - Token tk; - List partitionDefinitions = new ArrayList(); - PartitionDefinition partitionDef = null; + TablePartitioning partitioning = null; + TablePartitioning subPartitioning = null; + Expression expression = null; + ExpressionList columns = null; + List partitionDefinitions = null; + List parameter = null; + List partitionOptions = new ArrayList(); + Token tk = null; +} +{ + + ( + partitioning=HashOrKeyPartitioning() + | + { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } + ( + "(" expression=Expression() ")" { partitioning.setExpression(expression); } + | + "(" columns=ColumnList() ")" + { + partitioning.setColumnsSyntax(true); + partitioning.setColumns(columns); + } + ) + | + { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } + ( + "(" expression=Expression() ")" { partitioning.setExpression(expression); } + | + "(" columns=ColumnList() ")" + { + partitioning.setColumnsSyntax(true); + partitioning.setColumns(columns); + } + ) + ) + [ LOOKAHEAD({ getToken(1).kind == K_PARTITIONS }) + tk= { partitioning.setPartitions(Long.valueOf(tk.image)); } ] + [ + LOOKAHEAD({ getToken(1).kind == K_SUBPARTITION }) + subPartitioning=HashOrKeyPartitioning() + [ LOOKAHEAD({ getToken(1).kind == K_SUBPARTITIONS }) + tk= + { subPartitioning.setPartitions(Long.valueOf(tk.image)); } ] + { partitioning.setSubPartitioning(subPartitioning); } + ] + [ LOOKAHEAD(2) partitionDefinitions=PartitionDefinitions() + { partitioning.setPartitionDefinitions(partitionDefinitions); } ] + ( LOOKAHEAD(2, { getToken(1).kind != K_AS }) + parameter=CreateParameter() { partitionOptions.addAll(parameter); } )* + { + if (!partitionOptions.isEmpty()) { + partitioning.setPartitionOptions(partitionOptions); + } + return partitioning; + } +} + +TablePartitioning HashOrKeyPartitioning(): +{ + TablePartitioning partitioning = new TablePartitioning(); + Expression expression = null; + ExpressionList columns = null; + Token tk = null; + boolean useEquals = false; +} +{ + [ { partitioning.setLinear(true); } ] + ( + "(" expression=Expression() ")" + { + partitioning.setType(TablePartitioning.Type.HASH); + partitioning.setExpression(expression); + } + | + + [ [ "=" { useEquals = true; } ] tk= + { + partitioning.setAlgorithm(Integer.valueOf(tk.image)); + partitioning.setAlgorithmUseEquals(useEquals); + } + ] + "(" [ columns=ColumnList() ] ")" + { + partitioning.setType(TablePartitioning.Type.KEY); + if (columns == null) { + columns = new ExpressionList(); + } + partitioning.setColumns(columns); + } + ) + { + return partitioning; + } +} + +List PartitionValues(): +{ + List values = new ArrayList(); + Expression expression = null; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == K_MAXVALUE }) + { values.add("MAXVALUE"); } + | expression=Expression() { values.add(expression.toString()); } + ) + ( + "," + ( + LOOKAHEAD({ getToken(1).kind == K_MAXVALUE }) + { values.add("MAXVALUE"); } + | expression=Expression() { values.add(expression.toString()); } + ) + )* + { + return values; + } +} + +PartitionDefinition PartitionDefinition(): +{ + PartitionDefinition partitionDefinition = new PartitionDefinition(); + List values = null; + List subPartitionDefinitions = null; String partitionName = null; - String partitionOperation = null; - String storageEngine = null; - Expression exp = null; } { - "(" + partitionName=RelObjectName() + { partitionDefinition.setPartitionName(partitionName); } + [ + ( - - partitionName=RelObjectName() + { - List values = new ArrayList(); + partitionDefinition.setValueOperator( + PartitionDefinition.ValueOperator.LESS_THAN); } - ( - - ( - "(" exp = Expression() ")"{ - values.add(exp.toString()); - } - | { values.add("MAXVALUE"); } - ) { - partitionOperation = "VALUES LESS THAN"; + + { + partitionDefinition.setMaxValue(true); + partitionDefinition.setValues(Collections.singletonList("MAXVALUE")); } + | + "(" values=PartitionValues() ")" { partitionDefinition.setValues(values); } ) - [ "ENGINE" "=" tk= { storageEngine = tk.image; } ] + | + { - partitionDef = new PartitionDefinition(partitionName, partitionOperation, values, storageEngine); - partitionDefinitions.add(partitionDef); + partitionDefinition.setValueOperator(PartitionDefinition.ValueOperator.IN); } - [ "," ] - )* + "(" values=PartitionValues() ")" { partitionDefinition.setValues(values); } + ) + ] + ( PartitionDefinitionOption(partitionDefinition) )* + [ subPartitionDefinitions=SubpartitionDefinitions() + { partitionDefinition.setSubPartitionDefinitions(subPartitionDefinitions); } ] + { + return partitionDefinition; + } +} + +List PartitionDefinitions(): +{ + List partitionDefinitions = new ArrayList(); + PartitionDefinition partitionDefinition = null; +} +{ + "(" partitionDefinition=PartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } + ( "," partitionDefinition=PartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } )* ")" { return partitionDefinitions; } } +PartitionDefinition SubpartitionDefinition(): +{ + PartitionDefinition partitionDefinition = new PartitionDefinition(); + String partitionName = null; +} +{ + partitionName=RelObjectName() + { + partitionDefinition.setPartitionName(partitionName); + partitionDefinition.setSubPartition(true); + } + ( PartitionDefinitionOption(partitionDefinition) )* + { + return partitionDefinition; + } +} + +List SubpartitionDefinitions(): +{ + List partitionDefinitions = new ArrayList(); + PartitionDefinition partitionDefinition = null; +} +{ + "(" partitionDefinition=SubpartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } + ( "," partitionDefinition=SubpartitionDefinition() + { partitionDefinitions.add(partitionDefinition); } )* + ")" + { + return partitionDefinitions; + } +} + +void PartitionDefinitionOption(PartitionDefinition partitionDefinition): +{ + Token tk = null; + String value = null; + boolean useEquals = false; +} +{ + ( + [ { partitionDefinition.setStorageKeyword(true); } ] + [ "=" { useEquals = true; } ] value=RelObjectName() + { + partitionDefinition.setStorageEngine(value); + partitionDefinition.setStorageEngineUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setComment(tk.image); + partitionDefinition.setCommentUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setDataDirectory(tk.image); + partitionDefinition.setDataDirectoryUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setIndexDirectory(tk.image); + partitionDefinition.setIndexDirectoryUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setMaxRows(tk.image); + partitionDefinition.setMaxRowsUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] tk= + { + partitionDefinition.setMinRows(tk.image); + partitionDefinition.setMinRowsUseEquals(useEquals); + } + | + [ "=" { useEquals = true; } ] value=RelObjectName() + { + partitionDefinition.setTablespace(value); + partitionDefinition.setTablespaceUseEquals(useEquals); + } + ) +} + List PartitionNamesList() : { Token tk; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java new file mode 100644 index 0000000000..ce1a0f84cc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTablePartitionTest.java @@ -0,0 +1,175 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +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.assertTrue; + +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** MySQL 8.4 {@code CREATE TABLE} partition syntax verified against MySQL 8.4.11. */ +public class MySQLCreateTablePartitionTest { + + private static final List MYSQL84_PARTITION_STATEMENTS = List.of( + "CREATE TABLE mysql_partition_hash (id INT) " + + "PARTITION BY HASH (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_linear_hash (id INT) " + + "PARTITION BY LINEAR HASH (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_key (id INT) " + + "PARTITION BY LINEAR KEY ALGORITHM = 1 (id) PARTITIONS 4", + "CREATE TABLE mysql_partition_range (id INT) PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (10), " + + "PARTITION p1 VALUES LESS THAN (20), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE)", + "CREATE TABLE mysql_partition_range_columns (a INT, b INT) " + + "PARTITION BY RANGE COLUMNS (a, b) " + + "(PARTITION p0 VALUES LESS THAN (5, 12), " + + "PARTITION pmax VALUES LESS THAN (MAXVALUE, MAXVALUE))", + "CREATE TABLE mysql_partition_list (id INT) PARTITION BY LIST (id) " + + "(PARTITION p0 VALUES IN (1, 2), " + + "PARTITION p1 VALUES IN (3, 4))", + "CREATE TABLE mysql_partition_list_columns_multi " + + "(region VARCHAR (16), tier INT) " + + "PARTITION BY LIST COLUMNS (region, tier) " + + "(PARTITION p0 VALUES IN (('eu', 1), ('us', 2)), " + + "PARTITION p1 VALUES IN (('apac', 3)))", + "CREATE TABLE mysql_partition_sub_count (id INT, d DATE) " + + "PARTITION BY RANGE (YEAR(d)) " + + "SUBPARTITION BY HASH (id) SUBPARTITIONS 2 " + + "(PARTITION p0 VALUES LESS THAN (2025), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE)", + "CREATE TABLE mysql_partition_sub_explicit (id INT, d DATE) " + + "PARTITION BY RANGE (YEAR(d)) " + + "SUBPARTITION BY LINEAR KEY ALGORITHM = 2 (id) " + + "(PARTITION p0 VALUES LESS THAN (2025) " + + "(SUBPARTITION p0s0 ENGINE = InnoDB, " + + "SUBPARTITION p0s1 ENGINE = InnoDB), " + + "PARTITION pmax VALUES LESS THAN MAXVALUE " + + "(SUBPARTITION pmaxs0 ENGINE = InnoDB, " + + "SUBPARTITION pmaxs1 ENGINE = InnoDB))", + "CREATE TABLE mysql_partition_options (id INT) ENGINE = InnoDB " + + "PARTITION BY RANGE (id) " + + "(PARTITION p0 VALUES LESS THAN (10) ENGINE = InnoDB " + + "COMMENT = 'hot' MAX_ROWS = 100 MIN_ROWS = 1, " + + "PARTITION pmax VALUES LESS THAN MAXVALUE " + + "ENGINE InnoDB COMMENT 'cold')", + "CREATE TABLE mysql_partition_key_empty " + + "(id INT NOT NULL PRIMARY KEY, name VARCHAR (20)) " + + "PARTITION BY KEY () PARTITIONS 2", + "CREATE TABLE mysql_partition_hash_named (id INT) " + + "PARTITION BY HASH (id) PARTITIONS 2 " + + "(PARTITION p0 STORAGE ENGINE = InnoDB, " + + "PARTITION p1 ENGINE InnoDB)"); + + private static Stream mysql84PartitionStatements() { + return MYSQL84_PARTITION_STATEMENTS.stream(); + } + + @ParameterizedTest + @MethodSource("mysql84PartitionStatements") + public void testMySQL84CreateTablePartitions(String sql) throws JSQLParserException { + assertInstanceOf(CreateTable.class, assertSqlCanBeParsedAndDeparsed(sql)); + } + + @Test + public void testHashAndKeyPartitioningAst() throws JSQLParserException { + CreateTable hash = parse(MYSQL84_PARTITION_STATEMENTS.get(1)); + TablePartitioning hashPartitioning = hash.getPartitioning(); + assertEquals(TablePartitioning.Type.HASH, hashPartitioning.getType()); + assertTrue(hashPartitioning.isLinear()); + assertEquals("id", hashPartitioning.getExpression().toString()); + assertEquals(4L, hashPartitioning.getPartitions()); + + CreateTable key = parse(MYSQL84_PARTITION_STATEMENTS.get(2)); + TablePartitioning keyPartitioning = key.getPartitioning(); + assertEquals(TablePartitioning.Type.KEY, keyPartitioning.getType()); + assertTrue(keyPartitioning.isLinear()); + assertEquals(1, keyPartitioning.getAlgorithm()); + assertTrue(keyPartitioning.isAlgorithmUseEquals()); + assertEquals("id", keyPartitioning.getColumns().get(0).getColumnName()); + + CreateTable emptyKey = parse(MYSQL84_PARTITION_STATEMENTS.get(10)); + assertTrue(emptyKey.getPartitioning().getColumns().isEmpty()); + } + + @Test + public void testRangeAndListPartitionDefinitionsAst() throws JSQLParserException { + CreateTable range = parse(MYSQL84_PARTITION_STATEMENTS.get(4)); + TablePartitioning rangePartitioning = range.getPartitioning(); + assertEquals(TablePartitioning.Type.RANGE, rangePartitioning.getType()); + assertTrue(rangePartitioning.isColumnsSyntax()); + assertEquals("a", rangePartitioning.getColumns().get(0).getColumnName()); + assertEquals("b", rangePartitioning.getColumns().get(1).getColumnName()); + PartitionDefinition maxPartition = rangePartitioning.getPartitionDefinitions().get(1); + assertEquals(PartitionDefinition.ValueOperator.LESS_THAN, + maxPartition.getValueOperator()); + assertEquals(List.of("MAXVALUE", "MAXVALUE"), maxPartition.getValues()); + assertFalse(maxPartition.isMaxValue()); + + CreateTable list = parse(MYSQL84_PARTITION_STATEMENTS.get(5)); + PartitionDefinition listPartition = list.getPartitioning().getPartitionDefinitions().get(0); + assertEquals(PartitionDefinition.ValueOperator.IN, listPartition.getValueOperator()); + assertEquals(List.of("1", "2"), listPartition.getValues()); + } + + @Test + public void testSubPartitioningAst() throws JSQLParserException { + CreateTable createTable = parse(MYSQL84_PARTITION_STATEMENTS.get(8)); + TablePartitioning subPartitioning = createTable.getPartitioning().getSubPartitioning(); + assertNotNull(subPartitioning); + assertEquals(TablePartitioning.Type.KEY, subPartitioning.getType()); + assertTrue(subPartitioning.isLinear()); + assertEquals(2, subPartitioning.getAlgorithm()); + assertEquals("id", subPartitioning.getColumns().get(0).getColumnName()); + + PartitionDefinition first = createTable.getPartitioning().getPartitionDefinitions().get(0); + assertEquals(2, first.getSubPartitionDefinitions().size()); + assertTrue(first.getSubPartitionDefinitions().get(0).isSubPartition()); + assertEquals("InnoDB", first.getSubPartitionDefinitions().get(0).getStorageEngine()); + } + + @Test + public void testPartitionOptionsAst() throws JSQLParserException { + CreateTable createTable = parse(MYSQL84_PARTITION_STATEMENTS.get(9)); + PartitionDefinition first = createTable.getPartitioning().getPartitionDefinitions().get(0); + assertEquals("InnoDB", first.getStorageEngine()); + assertTrue(first.isStorageEngineUseEquals()); + assertEquals("'hot'", first.getComment()); + assertTrue(first.isCommentUseEquals()); + assertEquals("100", first.getMaxRows()); + assertEquals("1", first.getMinRows()); + + PartitionDefinition max = createTable.getPartitioning().getPartitionDefinitions().get(1); + assertTrue(max.isMaxValue()); + assertFalse(max.isStorageEngineUseEquals()); + assertFalse(max.isCommentUseEquals()); + + PartitionDefinition namedHash = parse(MYSQL84_PARTITION_STATEMENTS.get(11)) + .getPartitioning().getPartitionDefinitions().get(0); + assertTrue(namedHash.isStorageKeyword()); + } + + private static CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql); + } +} From 433143e4aaacfb511a1d60a84c3641df7541253a Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 15:39:21 +0900 Subject: [PATCH 046/387] Support PostgreSQL CREATE INDEX header options (#2527) --- .../statement/create/index/CreateIndex.java | 43 ++++++++++++-- .../util/deparser/CreateIndexDeParser.java | 15 +++-- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 15 ++++- .../create/PostgreSQLCreateIndexTest.java | 58 +++++++++++++++++++ 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index 0da992dd63..fd7573e202 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -23,6 +23,8 @@ public class CreateIndex implements Statement { private List tailParameters; private boolean indexTypeBeforeOn = false; private boolean usingIfNotExists = false; + private boolean concurrently; + private boolean only; public boolean isIndexTypeBeforeOn() { return indexTypeBeforeOn; @@ -41,6 +43,22 @@ public CreateIndex setUsingIfNotExists(boolean usingIfNotExists) { return this; } + public boolean isConcurrently() { + return concurrently; + } + + public void setConcurrently(boolean concurrently) { + this.concurrently = concurrently; + } + + public boolean isOnly() { + return only; + } + + public void setOnly(boolean only) { + this.only = only; + } + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); @@ -82,17 +100,24 @@ public String toString() { } buffer.append("INDEX "); + if (concurrently) { + buffer.append("CONCURRENTLY "); + } if (usingIfNotExists) { buffer.append("IF NOT EXISTS "); } - buffer.append(index.getName()); + if (index.getName() != null) { + buffer.append(index.getName()).append(" "); + } if (index.getUsing() != null && isIndexTypeBeforeOn()) { - buffer.append(" USING "); - buffer.append(index.getUsing()); + buffer.append("USING ").append(index.getUsing()).append(" "); } - buffer.append(" ON "); + buffer.append("ON "); + if (only) { + buffer.append("ONLY "); + } buffer.append(table.getFullyQualifiedName()); if (index.getUsing() != null && !isIndexTypeBeforeOn()) { @@ -134,4 +159,14 @@ public CreateIndex withTailParameters(List tailParameters) { this.setTailParameters(tailParameters); return this; } + + public CreateIndex withConcurrently(boolean concurrently) { + setConcurrently(concurrently); + return this; + } + + public CreateIndex withOnly(boolean only) { + setOnly(only); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java index cc5d71ba18..c765158121 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateIndexDeParser.java @@ -32,18 +32,25 @@ public void deParse(CreateIndex createIndex) { } builder.append("INDEX "); + if (createIndex.isConcurrently()) { + builder.append("CONCURRENTLY "); + } if (createIndex.isUsingIfNotExists()) { builder.append("IF NOT EXISTS "); } - builder.append(index.getName()); + if (index.getName() != null) { + builder.append(index.getName()).append(" "); + } String using = index.getUsing(); if (using != null && createIndex.isIndexTypeBeforeOn()) { - builder.append(" USING "); - builder.append(using); + builder.append("USING ").append(using).append(" "); } - builder.append(" ON "); + builder.append("ON "); + if (createIndex.isOnly()) { + builder.append("ONLY "); + } builder.append(createIndex.getTable().getFullyQualifiedName()); if (using != null && !createIndex.isIndexTypeBeforeOn()) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d7e98b949f..b8e419d2c3 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11277,11 +11277,20 @@ CreateIndex CreateIndex(): [ parameter=CreateParameter() ] + [ LOOKAHEAD({ getToken(1).kind == K_CONCURRENTLY }) + { createIndex.setConcurrently(true); } ] [ LOOKAHEAD(2) { createIndex.setUsingIfNotExists(true);} ] - index = Index() { index.setType(parameter.isEmpty() ? null : parameter.get(0)); } + [ LOOKAHEAD({ getToken(1).kind != K_ON && getToken(1).kind != K_USING }) + index = Index() ] + { + if (index == null) { + index = new Index(); + } + index.setType(parameter.isEmpty() ? null : parameter.get(0)); + } ( LOOKAHEAD(3)( - table=Table() + [ { createIndex.setOnly(true); } ] table=Table() [ indexType=UsingIndexType() { index.setUsing(indexType); } ] ) | @@ -11291,7 +11300,7 @@ CreateIndex CreateIndex(): createIndex.setIndexTypeBeforeOn(true); } ] - table=Table() + [ { createIndex.setOnly(true); } ] table=Table() ) ) colNames = IndexColumnsWithParamsList() diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java new file mode 100644 index 0000000000..23d8442f92 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSQLCreateIndexTest.java @@ -0,0 +1,58 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import org.junit.jupiter.api.Test; + +/** PostgreSQL {@code CREATE INDEX} syntax verified against PostgreSQL 18.6. */ +public class PostgreSQLCreateIndexTest { + + @Test + public void testCreateIndexConcurrently() throws JSQLParserException { + String sql = "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS pg_idx_email " + + "ON pg_index_test USING btree (email)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertTrue(createIndex.isConcurrently()); + assertTrue(createIndex.isUsingIfNotExists()); + assertFalse(createIndex.isOnly()); + assertEquals("pg_idx_email", createIndex.getIndex().getName()); + } + + @Test + public void testCreateIndexWithOmittedNameAndOnly() throws JSQLParserException { + String sql = "CREATE INDEX ON ONLY pg_index_test USING btree (id DESC)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertNull(createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + assertFalse(createIndex.isConcurrently()); + } + + @Test + public void testCreateNamedIndexOnOnlyTable() throws JSQLParserException { + String sql = "CREATE INDEX pg_idx_only ON ONLY pg_index_test (id)"; + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed(sql); + + assertEquals("pg_idx_only", createIndex.getIndex().getName()); + assertTrue(createIndex.isOnly()); + } +} From 125de36c05b6dd703f7bf9c802c069df8443ed2a Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 15:40:12 +0900 Subject: [PATCH 047/387] Support MySQL CHECK constraint enforcement (#2528) --- .../create/table/CheckConstraint.java | 18 +++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 8 +++- .../MySQLCreateTableConstraintTest.java | 39 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index 254fb3d01e..79d13b18ce 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -21,6 +21,8 @@ public class CheckConstraint extends NamedConstraint { private Expression expression; + private Boolean enforced; + public Table getTable() { return table; } @@ -37,6 +39,14 @@ public void setExpression(Expression expression) { this.expression = expression; } + public Boolean getEnforced() { + return enforced; + } + + public void setEnforced(Boolean enforced) { + this.enforced = enforced; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -44,6 +54,9 @@ public String toString() { b.append("CONSTRAINT ").append(getName()).append(" "); } b.append("CHECK (").append(expression).append(")"); + if (enforced != null) { + b.append(enforced ? " ENFORCED" : " NOT ENFORCED"); + } return b.toString(); } @@ -57,6 +70,11 @@ public CheckConstraint withExpression(Expression expression) { return this; } + public CheckConstraint withEnforced(Boolean enforced) { + setEnforced(enforced); + return this; + } + public E getExpression(Class type) { return type.cast(getExpression()); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index b8e419d2c3..ad4b494df5 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11950,11 +11950,17 @@ void ReferentialActionsOnIndex(ForeignKeyIndex fkIndex): CheckConstraint CheckConstraintSpec(String constraintName): { Expression exp = null; + Boolean enforced = null; } { ( LOOKAHEAD(2) "(" exp = Expression() ")" )* + [ + [ { enforced = false; } ] + { if (enforced == null) { enforced = true; } } + ] { - return new CheckConstraint().withName(constraintName).withExpression(exp); + return new CheckConstraint().withName(constraintName).withExpression(exp) + .withEnforced(enforced); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java new file mode 100644 index 0000000000..dc5b70911f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySQLCreateTableConstraintTest.java @@ -0,0 +1,39 @@ +/*- + * #%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.statement.create; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import org.junit.jupiter.api.Test; + +/** MySQL {@code CREATE TABLE} constraint syntax verified against MySQL 8.4.11. */ +public class MySQLCreateTableConstraintTest { + + @Test + public void testCheckConstraintEnforcement() throws JSQLParserException { + String sql = "CREATE TABLE mysql_check_enforcement (score INT, " + + "CONSTRAINT chk_positive CHECK (score >= 0) ENFORCED, " + + "CONSTRAINT chk_cap CHECK (score < 100) NOT ENFORCED)"; + + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(sql); + + CheckConstraint positive = (CheckConstraint) createTable.getIndexes().get(0); + assertEquals(Boolean.TRUE, positive.getEnforced()); + assertEquals("chk_positive", positive.getName()); + + CheckConstraint cap = (CheckConstraint) createTable.getIndexes().get(1); + assertEquals(Boolean.FALSE, cap.getEnforced()); + assertEquals("chk_cap", cap.getName()); + } +} From a6da40051291b3c0817e53ce1244a5006e7e70ab Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 15:43:18 +0900 Subject: [PATCH 048/387] fix(parser): allow VERSION in ALTER column clauses (#2530) VERSION is non-reserved, but restricted legacy productions reject it in DROP COLUMN and USING clauses. Accept the token in those productions and cover both PostgreSQL corpus cases. --- .../jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt | 3 ++- .../net/sf/jsqlparser/statement/alter/AlterTest.java | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index ad4b494df5..0abc7ad462 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2244,6 +2244,7 @@ Token KeywordOrIdentifier(): | tk = | tk = | tk = + | tk = ) { return tk; } } @@ -12065,7 +12066,7 @@ List CreateParameter(): | ( //@todo: implement a proper identifier - (tk= | tk= | tk=) + (tk= | tk= | tk= | tk=) { retval+=tk.image; } [ diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index a18215b790..e57eb7b838 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -648,6 +648,15 @@ public void testAlterTableAlterColumnDropNotNullIssue918() throws JSQLParserExce "ALTER TABLE \"user_table_t\" ALTER COLUMN name DROP NOT NULL"); } + @Test + public void testAlterTableVersionColumn() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE test_table DROP COLUMN version, DROP COLUMN other_column"); + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE test_table ALTER COLUMN version TYPE VARCHAR(32) " + + "USING version::VARCHAR(32)"); + } + @Test public void testAlterTableRenameColumn() throws JSQLParserException { // With Column Keyword From 8397d3a225e3fb380151044add1e4509d01955bf Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 15:45:29 +0900 Subject: [PATCH 049/387] fix(parser): model ALTER COLUMN nullability actions (#2529) Represent SET NOT NULL with a dedicated AST node and match DROP NOT NULL before the generic column-data-type branch. Cover parsing, deparsing, validation, and reflection. --- .../statement/alter/AlterExpression.java | 33 +++++++++++++++++++ .../validation/validator/AlterValidator.java | 6 ++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 26 ++++++++++++--- .../jsqlparser/statement/alter/AlterTest.java | 17 +++++++++- .../builder/ReflectionModelTest.java | 1 + .../validator/AlterValidatorTest.java | 6 ++++ 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index dd4e07f52a..7e2fea943e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -41,6 +41,7 @@ public class AlterExpression implements Serializable { private String columnOldName; private List colDataTypeList; private List columnDropNotNullList; + private List columnSetNotNullList; private List columnDropDefaultList; private List columnSetDefaultList; private List columnSetVisibilityList; @@ -382,6 +383,13 @@ public void addColDropNotNull(ColumnDropNotNull columnDropNotNull) { columnDropNotNullList.add(columnDropNotNull); } + public void addColSetNotNull(ColumnSetNotNull columnSetNotNull) { + if (columnSetNotNullList == null) { + columnSetNotNullList = new ArrayList<>(); + } + columnSetNotNullList.add(columnSetNotNull); + } + public List getColumnDropDefaultList() { return columnDropDefaultList; } @@ -527,6 +535,10 @@ public List getColumnDropNotNullList() { return columnDropNotNullList; } + public List getColumnSetNotNullList() { + return columnSetNotNullList; + } + public void addParameters(String... params) { if (parameters == null) { parameters = new ArrayList<>(); @@ -1134,6 +1146,9 @@ protected void toStringGeneral(StringBuilder b) { if (colDataTypeList.size() > 1) { b.append(")"); } + } else if (getColumnSetNotNullList() != null) { + b.append("COLUMN "); + b.append(PlainSelect.getStringList(columnSetNotNullList)); } else if (getColumnDropNotNullList() != null) { b.append("COLUMN "); b.append(PlainSelect.getStringList(columnDropNotNullList)); @@ -1467,6 +1482,24 @@ public String toString() { } } + public static final class ColumnSetNotNull implements Serializable { + + private final String columnName; + + public ColumnSetNotNull(String columnName) { + this.columnName = columnName; + } + + public String getColumnName() { + return columnName; + } + + @Override + public String toString() { + return columnName + " SET NOT NULL"; + } + } + public static final class ColumnDropDefault implements Serializable { private final String columnName; diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java index 4db652b960..a8aaf03ebe 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java @@ -17,6 +17,7 @@ import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull; import net.sf.jsqlparser.statement.alter.AlterOperation; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.ValidationUtil; @@ -47,6 +48,11 @@ public void validate(Alter alter, AlterExpression e) { ColumnDropNotNull::getColumnName)); } + if (e.getColumnSetNotNullList() != null) { + validateOptionalColumnNames(c, ValidationUtil.map(e.getColumnSetNotNullList(), + ColumnSetNotNull::getColumnName)); + } + if (e.getColDataTypeList() != null) { boolean validateForExist = !EnumSet.of(AlterOperation.ADD).contains(e.getOperation()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0abc7ad462..8ed4a18d54 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -12340,13 +12340,15 @@ Truncate Truncate(): /** * Parses common column-level changes shared between the COLUMN-prefixed and bare forms: - * DROP DEFAULT, SET DEFAULT, SET VISIBLE/INVISIBLE, and bracketed multi-column definitions. + * DROP DEFAULT, SET DEFAULT, SET NOT NULL, SET VISIBLE/INVISIBLE, and bracketed multi-column + * definitions. */ void AlterExpressionColumnChanges(AlterExpression alterExp): { AlterExpression.ColumnDataType alterExpressionColumnDataType = null; AlterExpression.ColumnDropDefault alterExpressionColumnDropDefault = null; AlterExpression.ColumnSetDefault alterExpressionColumnSetDefault = null; + AlterExpression.ColumnSetNotNull alterExpressionColumnSetNotNull = null; AlterExpression.ColumnSetVisibility alterExpressionColumnSetVisibility = null; } { @@ -12357,6 +12359,9 @@ void AlterExpressionColumnChanges(AlterExpression alterExp): LOOKAHEAD(3) alterExpressionColumnSetDefault = AlterExpressionColumnSetDefault() { alterExp.addColSetDefault(alterExpressionColumnSetDefault); } | + LOOKAHEAD(4) alterExpressionColumnSetNotNull = AlterExpressionColumnSetNotNull() + { alterExp.addColSetNotNull(alterExpressionColumnSetNotNull); } + | LOOKAHEAD(3) alterExpressionColumnSetVisibility = AlterExpressionColumnSetVisibility() { alterExp.addColSetVisibility(alterExpressionColumnSetVisibility); } | @@ -12414,6 +12419,19 @@ AlterExpression.ColumnDropNotNull AlterExpressionColumnDropNotNull(): } } +AlterExpression.ColumnSetNotNull AlterExpressionColumnSetNotNull(): +{ + String columnName = null; +} +{ + columnName = RelObjectName() + + + { + return new AlterExpression.ColumnSetNotNull(columnName); + } +} + AlterExpression.ColumnDropDefault AlterExpressionColumnDropDefault(): { String columnName = null; @@ -13277,11 +13295,11 @@ AlterExpression AlterExpressionAddAlterModify(): ( LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) | - LOOKAHEAD(2) alterExpressionColumnDataType = AlterExpressionColumnDataType() - { alterExp.addColDataType(alterExpressionColumnDataType); } - | LOOKAHEAD(3) alterExpressionColumnDropNotNull = AlterExpressionColumnDropNotNull() { alterExp.addColDropNotNull( alterExpressionColumnDropNotNull);} + | + LOOKAHEAD(2) alterExpressionColumnDataType = AlterExpressionColumnDataType() + { alterExp.addColDataType(alterExpressionColumnDataType); } ) ) | diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index e57eb7b838..66e947ac06 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -644,8 +644,23 @@ public void testAlterConstraintWithoutFKSourceColumnsIssue929() throws JSQLParse @Test public void testAlterTableAlterColumnDropNotNullIssue918() throws JSQLParserException { - assertSqlCanBeParsedAndDeparsed( + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( "ALTER TABLE \"user_table_t\" ALTER COLUMN name DROP NOT NULL"); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertNull(expression.getColDataTypeList()); + assertEquals("name", expression.getColumnDropNotNullList().get(0).getColumnName()); + assertTrue(expression.getColumnDropNotNullList().get(0).isWithNot()); + } + + @Test + public void testAlterTableAlterColumnSetNotNull() throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE user_table ALTER COLUMN name SET NOT NULL"); + AlterExpression expression = alter.getAlterExpressions().get(0); + + assertNull(expression.getColDataTypeList()); + assertEquals("name", expression.getColumnSetNotNullList().get(0).getColumnName()); } @Test diff --git a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java index 3fbfe8d962..64d7d83242 100644 --- a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java @@ -141,6 +141,7 @@ public class ReflectionModelTest { new net.sf.jsqlparser.statement.alter.AlterExpression(), new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType(false), new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull("name"), + new net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull("name"), new net.sf.jsqlparser.statement.merge.MergeInsert(), new net.sf.jsqlparser.statement.alter.DeferrableConstraint(), new net.sf.jsqlparser.statement.alter.EnableConstraint(), diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/AlterValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/AlterValidatorTest.java index f2aef4e403..408e0efa5c 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/AlterValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/AlterValidatorTest.java @@ -151,4 +151,10 @@ public void testAlterTableAlterColumnDropNotNullIssue918() throws JSQLParserExce DatabaseType.DATABASES); } + @Test + public void testAlterTableAlterColumnSetNotNull() throws JSQLParserException { + validateNoErrors("ALTER TABLE user_table ALTER COLUMN name SET NOT NULL", 1, + DatabaseType.DATABASES); + } + } From 6413a84724cc30981e4142a854a8cbb7ef8039a4 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 15:28:30 +0900 Subject: [PATCH 050/387] feat(parser): add column data type fragment parser (#2531) Expose strict CCJSqlParserUtil helpers for ColDataType fragments, including parser configuration and full-input validation. --- .../jsqlparser/parser/CCJSqlParserUtil.java | 56 +++++++++++++++++++ .../parser/CCJSqlParserUtilTest.java | 30 ++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index 31e0709758..eea0c7c635 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -30,6 +30,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.table.ColDataType; /** * Toolfunctions to start and use JSqlParser. @@ -199,6 +200,61 @@ public static Expression parseExpression(String expression, boolean allowPartial }); } + /** + * Parses a column data type fragment. The complete input must represent the data type; trailing + * tokens are rejected. + * + * @param columnDataType the column data type fragment to parse + * @return the parsed column data type, or {@code null} for a null or empty input + * @throws JSQLParserException when the input cannot be parsed completely + * @see #parseColDataType(String, Consumer) + */ + public static ColDataType parseColDataType(String columnDataType) throws JSQLParserException { + return parseColDataType(columnDataType, null); + } + + /** + * Parses a column data type fragment while allowing the parser to be configured. The complete + * input must represent the data type; trailing tokens are rejected. + * + * @param columnDataType the column data type fragment to parse + * @param consumer parser configuration callback, or {@code null} + * @return the parsed column data type, or {@code null} for a null or empty input + * @throws JSQLParserException when the input cannot be parsed completely + */ + public static ColDataType parseColDataType(String columnDataType, + Consumer consumer) throws JSQLParserException { + if (columnDataType == null || columnDataType.isEmpty()) { + return null; + } + + try { + return parseColDataType(columnDataType, false, consumer); + } catch (JSQLParserException ex) { + return parseColDataType(columnDataType, true, consumer); + } + } + + private static ColDataType parseColDataType(String columnDataType, boolean allowComplexParsing, + Consumer consumer) throws JSQLParserException { + CCJSqlParser parser = newParser(columnDataType) + .withAllowComplexParsing(allowComplexParsing); + if (consumer != null) { + consumer.accept(parser); + } + + try { + ColDataType result = parser.ColDataType(); + if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { + throw new JSQLParserException( + "could only parse partial column data type " + result); + } + return result; + } catch (ParseException ex) { + throw new JSQLParserException(columnDataType, ex); + } + } + @SuppressWarnings("PMD.CyclomaticComplexity") public static Expression parseExpression(String expressionStr, boolean allowPartialParse, Consumer consumer) throws JSQLParserException { diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index 40e61e708c..5aa92e1c78 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -20,6 +20,7 @@ import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.Statements; import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.table.ColDataType; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.TableStatement; @@ -39,6 +40,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -107,6 +109,34 @@ public void testParseExpression2() throws Exception { assertInstanceOf(ParenthesedExpressionList.class, mult.getRightExpression()); } + @Test + public void testParseColDataType() throws Exception { + assertNull(CCJSqlParserUtil.parseColDataType(null)); + assertNull(CCJSqlParserUtil.parseColDataType("")); + + ColDataType arrayType = CCJSqlParserUtil.parseColDataType("integer[][]"); + assertEquals("integer", arrayType.getDataType()); + assertEquals(2, arrayType.getArrayData().size()); + assertNull(arrayType.getArrayData().get(0)); + assertNull(arrayType.getArrayData().get(1)); + + AtomicBoolean configured = new AtomicBoolean(); + ColDataType enumType = + CCJSqlParserUtil.parseColDataType("enum('small','medium')", parser -> { + configured.set(true); + parser.withDialect(AbstractJSqlParser.Dialect.MYSQL); + }); + assertTrue(configured.get()); + assertEquals("enum", enumType.getDataType()); + assertEquals(List.of("'small'", "'medium'"), enumType.getArgumentsStringList()); + } + + @Test + public void testParseColDataTypeRejectsTrailingTokens() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType("varchar(255) NOT NULL")); + } + @Test public void testParseExpressionNonPartial() throws Exception { assertThrows(JSQLParserException.class, From c77bb0f0aa2c8d0320bf2a83644e0a2fcc1f685d Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 15:29:23 +0900 Subject: [PATCH 051/387] fix(parser): model PostgreSQL declarative partitions (#2535) * fix(parser): model PostgreSQL declarative partitions * fix(ci): keep partition tests compatible with Java 11 --- .../statement/alter/AlterExpression.java | 2 + .../alter/AlterExpressionPartition.java | 62 ++++++++- .../statement/alter/AlterOperation.java | 2 +- .../statement/create/table/CreateTable.java | 34 +++++ .../create/table/PartitionBound.java | 129 ++++++++++++++++++ .../create/table/TablePartitioning.java | 21 +++ .../util/deparser/CreateTableDeParser.java | 8 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 115 +++++++++++++++- .../jsqlparser/statement/alter/AlterTest.java | 42 ++++++ .../statement/create/CreateTableTest.java | 58 ++++++++ 10 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 7e2fea943e..4055eb8b1d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -830,6 +830,8 @@ protected boolean isPartitionOperation() { switch (operation) { case DISCARD_PARTITION: case IMPORT_PARTITION: + case ATTACH_PARTITION: + case DETACH_PARTITION: case TRUNCATE_PARTITION: case COALESCE_PARTITION: case REORGANIZE_PARTITION: diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java index dd9e37b6fe..a6e1c693e4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement.alter; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.PartitionBound; + /** * Internal subclass for partition maintenance operations within ALTER TABLE. Handles TRUNCATE, * COALESCE, REORGANIZE, EXCHANGE, ANALYZE, CHECK, OPTIMIZE, REBUILD, REPAIR PARTITION, PARTITION @@ -16,8 +19,65 @@ */ public class AlterExpressionPartition extends AlterExpression { + public enum DetachMode { + CONCURRENTLY, FINALIZE + } + + private Table partitionTable; + private PartitionBound partitionBound; + private DetachMode detachMode; + + public Table getPartitionTable() { + return partitionTable; + } + + public void setPartitionTable(Table partitionTable) { + this.partitionTable = partitionTable; + } + + public PartitionBound getPartitionBound() { + return partitionBound; + } + + public void setPartitionBound(PartitionBound partitionBound) { + this.partitionBound = partitionBound; + } + + public DetachMode getDetachMode() { + return detachMode; + } + + public void setDetachMode(DetachMode detachMode) { + this.detachMode = detachMode; + } + + public AlterExpressionPartition withPartitionTable(Table partitionTable) { + setPartitionTable(partitionTable); + return this; + } + + public AlterExpressionPartition withPartitionBound(PartitionBound partitionBound) { + setPartitionBound(partitionBound); + return this; + } + + public AlterExpressionPartition withDetachMode(DetachMode detachMode) { + setDetachMode(detachMode); + return this; + } + @Override protected void appendBody(StringBuilder b) { - toStringPartition(b); + if (getOperation() == AlterOperation.ATTACH_PARTITION) { + b.append("ATTACH PARTITION ").append(partitionTable).append(" ") + .append(partitionBound); + } else if (getOperation() == AlterOperation.DETACH_PARTITION) { + b.append("DETACH PARTITION ").append(partitionTable); + if (detachMode != null) { + b.append(" ").append(detachMode); + } + } else { + toStringPartition(b); + } } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index a33a98536b..9f7da8a8c7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index acbe0b7184..e5c8b6806a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -37,6 +37,8 @@ public class CreateTable implements Statement { private boolean ifNotExists = false; private boolean orReplace = false; private TablePartitioning partitioning; + private Table partitionOf; + private PartitionBound partitionBound; private RowMovement rowMovement; @@ -157,6 +159,22 @@ public void setPartitioning(TablePartitioning partitioning) { this.partitioning = partitioning; } + public Table getPartitionOf() { + return partitionOf; + } + + public void setPartitionOf(Table partitionOf) { + this.partitionOf = partitionOf; + } + + public PartitionBound getPartitionBound() { + return partitionBound; + } + + public void setPartitionBound(PartitionBound partitionBound) { + this.partitionBound = partitionBound; + } + public boolean isSelectParenthesis() { return selectParenthesis; } @@ -179,6 +197,9 @@ public String toString() { StringBuilder b = new StringBuilder(); appendCreateClause(b); appendColumnDefinitions(b); + if (partitionBound != null) { + b.append(" ").append(partitionBound); + } appendTableOptions(b); appendTableProperties(b); return b.toString(); @@ -202,6 +223,9 @@ private void appendCreateClause(StringBuilder b) { b.append("IF NOT EXISTS "); } b.append(table); + if (partitionOf != null) { + b.append(" PARTITION OF ").append(partitionOf); + } } private void appendColumnDefinitions(StringBuilder b) { @@ -289,6 +313,16 @@ public CreateTable withPartitioning(TablePartitioning partitioning) { return this; } + public CreateTable withPartitionOf(Table partitionOf) { + setPartitionOf(partitionOf); + return this; + } + + public CreateTable withPartitionBound(PartitionBound partitionBound) { + setPartitionBound(partitionBound); + return this; + } + public CreateTable withRowMovement(RowMovement rowMovement) { this.setRowMovement(rowMovement); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java new file mode 100644 index 0000000000..9baa2afe1e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionBound.java @@ -0,0 +1,129 @@ +/*- + * #%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.statement.create.table; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** A PostgreSQL declarative-partition bound. */ +public class PartitionBound implements Serializable { + + public enum Type { + RANGE, LIST, HASH, DEFAULT + } + + private Type type; + private ExpressionList fromExpressions; + private ExpressionList toExpressions; + private ExpressionList inExpressions; + private Expression modulus; + private Expression remainder; + + public PartitionBound() {} + + public PartitionBound(Type type) { + this.type = type; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public ExpressionList getFromExpressions() { + return fromExpressions; + } + + public void setFromExpressions(ExpressionList fromExpressions) { + this.fromExpressions = fromExpressions; + } + + public ExpressionList getToExpressions() { + return toExpressions; + } + + public void setToExpressions(ExpressionList toExpressions) { + this.toExpressions = toExpressions; + } + + public ExpressionList getInExpressions() { + return inExpressions; + } + + public void setInExpressions(ExpressionList inExpressions) { + this.inExpressions = inExpressions; + } + + public Expression getModulus() { + return modulus; + } + + public void setModulus(Expression modulus) { + this.modulus = modulus; + } + + public Expression getRemainder() { + return remainder; + } + + public void setRemainder(Expression remainder) { + this.remainder = remainder; + } + + public PartitionBound withType(Type type) { + setType(type); + return this; + } + + public PartitionBound withFromExpressions(ExpressionList fromExpressions) { + setFromExpressions(fromExpressions); + return this; + } + + public PartitionBound withToExpressions(ExpressionList toExpressions) { + setToExpressions(toExpressions); + return this; + } + + public PartitionBound withInExpressions(ExpressionList inExpressions) { + setInExpressions(inExpressions); + return this; + } + + public PartitionBound withModulus(Expression modulus) { + setModulus(modulus); + return this; + } + + public PartitionBound withRemainder(Expression remainder) { + setRemainder(remainder); + return this; + } + + @Override + public String toString() { + switch (type) { + case RANGE: + return "FOR VALUES FROM (" + fromExpressions + ") TO (" + toExpressions + ")"; + case LIST: + return "FOR VALUES IN (" + inExpressions + ")"; + case HASH: + return "FOR VALUES WITH (MODULUS " + modulus + ", REMAINDER " + remainder + ")"; + case DEFAULT: + return "DEFAULT"; + default: + return ""; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java index fa0ce42c01..2c8a9a77b7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TablePartitioning.java @@ -33,6 +33,7 @@ public enum Type { private boolean linear; private boolean columnsSyntax; private Expression expression; + private ExpressionList expressionList; private ExpressionList columns; private Integer algorithm; private boolean algorithmUseEquals; @@ -79,6 +80,19 @@ public void setExpression(Expression expression) { this.expression = expression; } + /** + * Returns the ordered partition key expressions when more than one key was declared. + * + * @return partition key expressions, or {@code null} for the legacy single-expression form + */ + public ExpressionList getExpressionList() { + return expressionList; + } + + public void setExpressionList(ExpressionList expressionList) { + this.expressionList = expressionList; + } + public ExpressionList getColumns() { return columns; } @@ -155,6 +169,11 @@ public TablePartitioning withExpression(Expression expression) { return this; } + public TablePartitioning withExpressionList(ExpressionList expressionList) { + setExpressionList(expressionList); + return this; + } + public TablePartitioning withColumns(ExpressionList columns) { setColumns(columns); return this; @@ -273,6 +292,8 @@ private void appendMethod(StringBuilder builder) { } if (expression != null) { builder.append(" (").append(expression).append(")"); + } else if (expressionList != null) { + builder.append(" (").append(expressionList).append(")"); } else if (columns != null) { builder.append(" ").append(PlainSelect.getStringList(columns, true, true)); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java index 391b08889b..0c0cbabeb5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -52,6 +52,10 @@ public void deParse(CreateTable createTable) { builder.append("IF NOT EXISTS "); } builder.append(createTable.getTable().getFullyQualifiedName()); + if (createTable.getPartitionOf() != null) { + builder.append(" PARTITION OF ") + .append(createTable.getPartitionOf().getFullyQualifiedName()); + } if (createTable.getColumns() != null && !createTable.getColumns().isEmpty()) { builder.append(" ("); @@ -92,6 +96,10 @@ public void deParse(CreateTable createTable) { builder.append(")"); } + if (createTable.getPartitionBound() != null) { + builder.append(' ').append(createTable.getPartitionBound()); + } + params = PlainSelect.getStringList(createTable.getTableOptionsStrings(), false, false); if (!"".equals(params)) { builder.append(' ').append(params); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8ed4a18d54..c8c8f2b171 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1460,6 +1460,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1521,6 +1522,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1564,6 +1566,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1640,6 +1643,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1708,6 +1712,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -11558,6 +11563,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): String tableColumn = null; List columns = new ArrayList(); TablePartitioning partitioning = null; + Table partitionOfTable = null; + PartitionBound partitionBound = null; } { { createTable.setOrReplace(isUsingOrReplace);} @@ -11571,6 +11578,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] table=Table() + [ LOOKAHEAD(2) partitionOfTable=Table() + { createTable.setPartitionOf(partitionOfTable); } ] [ LOOKAHEAD(2) ( LOOKAHEAD(3) ( "(" tableColumn=RelObjectName() { columns.add(tableColumn); } ("," tableColumn=RelObjectName() { columns.add(tableColumn); } )* ")" @@ -11599,6 +11608,9 @@ CreateTable CreateTable(boolean isUsingOrReplace): ) ) ] + [ LOOKAHEAD({ partitionOfTable != null }) + partitionBound=PostgreSqlPartitionBound() + { createTable.setPartitionBound(partitionBound); } ] ( LOOKAHEAD(2, { getToken(1).kind != K_AS && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) }) parameter=CreateParameter() { tableOptions.addAll(parameter); } )* @@ -12586,7 +12598,7 @@ TablePartitioning CreateTablePartitioning(): { TablePartitioning partitioning = null; TablePartitioning subPartitioning = null; - Expression expression = null; + ExpressionList expressionList = null; ExpressionList columns = null; List partitionDefinitions = null; List parameter = null; @@ -12600,7 +12612,13 @@ TablePartitioning CreateTablePartitioning(): | { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } ( - "(" expression=Expression() ")" { partitioning.setExpression(expression); } + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } | "(" columns=ColumnList() ")" { @@ -12611,7 +12629,13 @@ TablePartitioning CreateTablePartitioning(): | { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } ( - "(" expression=Expression() ")" { partitioning.setExpression(expression); } + "(" expressionList=SimpleExpressionList() ")" { + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } + } | "(" columns=ColumnList() ")" { @@ -12642,10 +12666,49 @@ TablePartitioning CreateTablePartitioning(): } } +PartitionBound PostgreSqlPartitionBound(): +{ + PartitionBound bound = null; + ExpressionList expressions = null; + Expression modulus = null; + Expression remainder = null; +} +{ + ( + { bound = new PartitionBound(PartitionBound.Type.DEFAULT); } + | + + ( + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.RANGE); + bound.setFromExpressions(expressions); + } + "(" expressions=SimpleExpressionList() ")" + { bound.setToExpressions(expressions); } + | + "(" expressions=SimpleExpressionList() ")" + { + bound = new PartitionBound(PartitionBound.Type.LIST); + bound.setInExpressions(expressions); + } + | + "(" modulus=Expression() "," + remainder=Expression() ")" + { + bound = new PartitionBound(PartitionBound.Type.HASH); + bound.setModulus(modulus); + bound.setRemainder(remainder); + } + ) + ) + { return bound; } +} + TablePartitioning HashOrKeyPartitioning(): { TablePartitioning partitioning = new TablePartitioning(); - Expression expression = null; + ExpressionList expressionList = null; ExpressionList columns = null; Token tk = null; boolean useEquals = false; @@ -12653,10 +12716,14 @@ TablePartitioning HashOrKeyPartitioning(): { [ { partitioning.setLinear(true); } ] ( - "(" expression=Expression() ")" + "(" expressionList=SimpleExpressionList() ")" { partitioning.setType(TablePartitioning.Type.HASH); - partitioning.setExpression(expression); + if (expressionList.size() == 1) { + partitioning.setExpression(expressionList.get(0)); + } else { + partitioning.setExpressionList(expressionList); + } } | @@ -13412,6 +13479,40 @@ AlterExpression AlterExpressionRenameOp(): { return alterExp; } } +AlterExpressionPartition AlterExpressionPostgreSqlPartitionOp(): +{ + AlterExpressionPartition alterExp = new AlterExpressionPartition(); + Table partitionTable = null; + PartitionBound partitionBound = null; +} +{ + ( + partitionTable=Table() + partitionBound=PostgreSqlPartitionBound() + { + alterExp.setOperation(AlterOperation.ATTACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + alterExp.setPartitionBound(partitionBound); + } + | + partitionTable=Table() + { + alterExp.setOperation(AlterOperation.DETACH_PARTITION); + alterExp.setPartitionTable(partitionTable); + } + [ + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.CONCURRENTLY); + } + | + { + alterExp.setDetachMode(AlterExpressionPartition.DetachMode.FINALIZE); + } + ] + ) + { return alterExp; } +} + /** * Dispatcher production for all ALTER TABLE expression types. * Delegates to focused sub-productions for each operation category. @@ -13562,6 +13663,8 @@ AlterExpression AlterExpression(): ) | alterExp = AlterExpressionDiscardOrImport() + | + alterExp = AlterExpressionPostgreSqlPartitionOp() | LOOKAHEAD(4) ( { alterExp = new AlterExpression(); } diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 66e947ac06..771bb8ec5d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -28,6 +28,7 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.ReferentialAction; @@ -42,6 +43,7 @@ import net.sf.jsqlparser.statement.create.table.Index.ColumnParams; import net.sf.jsqlparser.statement.create.table.NamedConstraint; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -2444,4 +2446,44 @@ public void testAlterTableAddMultiValuedIndexIssue2490() throws JSQLParserExcept assertSqlCanBeParsedAndDeparsed( "ALTER TABLE t ADD INDEX i31 ((CAST(data -> '$.zips' AS UNSIGNED ARRAY)))"); } + + @Test + public void testPostgreSqlAttachPartitionIssue2523() throws JSQLParserException { + String sql = "ALTER TABLE measurement ATTACH PARTITION measurement_y2026m01 " + + "FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')"; + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition expression = + (AlterExpressionPartition) alter.getAlterExpressions().get(0); + + assertEquals(AlterOperation.ATTACH_PARTITION, expression.getOperation()); + assertEquals("measurement_y2026m01", expression.getPartitionTable().getName()); + assertEquals(PartitionBound.Type.RANGE, expression.getPartitionBound().getType()); + } + + @Test + public void testPostgreSqlDetachPartitionModesIssue2523() throws JSQLParserException { + String concurrently = "ALTER TABLE measurement DETACH PARTITION " + + "measurement_y2026m01 CONCURRENTLY"; + Alter concurrentAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + concurrently, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition concurrentExpression = + (AlterExpressionPartition) concurrentAlter.getAlterExpressions().get(0); + assertEquals(AlterOperation.DETACH_PARTITION, concurrentExpression.getOperation()); + assertEquals(AlterExpressionPartition.DetachMode.CONCURRENTLY, + concurrentExpression.getDetachMode()); + + String finalizeSql = + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01 FINALIZE"; + Alter finalizeAlter = (Alter) assertSqlCanBeParsedAndDeparsed( + finalizeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + AlterExpressionPartition finalizeExpression = + (AlterExpressionPartition) finalizeAlter.getAlterExpressions().get(0); + assertEquals(AlterExpressionPartition.DetachMode.FINALIZE, + finalizeExpression.getDetachMode()); + + assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE measurement DETACH PARTITION measurement_y2026m01", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index a58caea808..845567bbe5 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -24,11 +24,13 @@ import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; +import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.relational.GreaterThan; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.create.table.ColDataType; @@ -36,7 +38,9 @@ import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.PartitionBound; import net.sf.jsqlparser.statement.create.table.RowMovementMode; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestException; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -1247,4 +1251,58 @@ void testUniqueIndexIssue1893() throws JSQLParserException { // A plain INDEX must still parse unchanged. assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a int, INDEX idx (a))", true); } + + @Test + void testPostgreSqlDeclarativePartitionParentsIssue2522() throws JSQLParserException { + String sql = "CREATE TABLE pg_range_parent (tenant_id int, " + + "happened_at timestamptz) PARTITION BY RANGE (tenant_id, happened_at)"; + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + sql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + TablePartitioning partitioning = createTable.getPartitioning(); + + assertEquals(TablePartitioning.Type.RANGE, partitioning.getType()); + assertEquals(List.of("tenant_id", "happened_at"), partitioning.getExpressionList() + .stream().map(Object::toString).collect(Collectors.toList())); + + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_list_parent (region text) PARTITION BY LIST (region)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE pg_hash_parent (tenant_id int) PARTITION BY HASH (tenant_id)", + true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @Test + void testPostgreSqlDeclarativePartitionChildrenIssue2522() throws JSQLParserException { + String rangeSql = "CREATE TABLE pg_range_p1 PARTITION OF pg_range_parent " + + "FOR VALUES FROM (1, MINVALUE) TO (2, MAXVALUE)"; + CreateTable range = (CreateTable) assertSqlCanBeParsedAndDeparsed( + rangeSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("pg_range_parent", range.getPartitionOf().getName()); + assertEquals(PartitionBound.Type.RANGE, range.getPartitionBound().getType()); + assertEquals(List.of("1", "MINVALUE"), range.getPartitionBound().getFromExpressions() + .stream().map(Object::toString).collect(Collectors.toList())); + assertEquals(List.of("2", "MAXVALUE"), range.getPartitionBound().getToExpressions() + .stream().map(Object::toString).collect(Collectors.toList())); + + String listSql = "CREATE TABLE pg_list_eu_us PARTITION OF pg_list_parent " + + "FOR VALUES IN ('eu', 'us')"; + CreateTable list = (CreateTable) assertSqlCanBeParsedAndDeparsed( + listSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.LIST, list.getPartitionBound().getType()); + + String hashSql = "CREATE TABLE pg_hash_p0 PARTITION OF pg_hash_parent " + + "FOR VALUES WITH (MODULUS 4, REMAINDER 0)"; + CreateTable hash = (CreateTable) assertSqlCanBeParsedAndDeparsed( + hashSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.HASH, hash.getPartitionBound().getType()); + assertEquals("4", hash.getPartitionBound().getModulus().toString()); + assertEquals("0", hash.getPartitionBound().getRemainder().toString()); + + String defaultSql = + "CREATE TABLE pg_list_default PARTITION OF pg_list_parent DEFAULT"; + CreateTable defaultPartition = (CreateTable) assertSqlCanBeParsedAndDeparsed( + defaultSql, true, parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(PartitionBound.Type.DEFAULT, defaultPartition.getPartitionBound().getType()); + } } From 3ba25467ab9d14c7795c67aea423a9d3b470762c Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 15:30:14 +0900 Subject: [PATCH 052/387] fix(parser): model PostgreSQL CREATE INDEX clauses (#2536) * fix(parser): model PostgreSQL CREATE INDEX clauses * fix(ci): reduce index column rendering complexity --- .../statement/create/index/CreateIndex.java | 93 +++++++++ .../statement/create/table/Index.java | 186 +++++++++++++++++- .../util/deparser/CreateIndexDeParser.java | 20 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 98 +++++++-- .../statement/create/CreateIndexTest.java | 55 ++++++ 5 files changed, 434 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index fd7573e202..c8156b5e8e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -12,9 +12,11 @@ import static java.util.stream.Collectors.joining; import java.util.*; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.statement.select.PlainSelect; public class CreateIndex implements Statement { @@ -25,6 +27,11 @@ public class CreateIndex implements Statement { private boolean usingIfNotExists = false; private boolean concurrently; private boolean only; + private List includeColumns; + private Boolean nullsDistinct; + private List storageParameters; + private String tableSpace; + private Expression where; public boolean isIndexTypeBeforeOn() { return indexTypeBeforeOn; @@ -59,6 +66,46 @@ public void setOnly(boolean only) { this.only = only; } + public List getIncludeColumns() { + return includeColumns; + } + + public void setIncludeColumns(List includeColumns) { + this.includeColumns = includeColumns; + } + + public Boolean getNullsDistinct() { + return nullsDistinct; + } + + public void setNullsDistinct(Boolean nullsDistinct) { + this.nullsDistinct = nullsDistinct; + } + + public List getStorageParameters() { + return storageParameters; + } + + public void setStorageParameters(List storageParameters) { + this.storageParameters = storageParameters; + } + + public String getTableSpace() { + return tableSpace; + } + + public void setTableSpace(String tableSpace) { + this.tableSpace = tableSpace; + } + + public Expression getWhere() { + return where; + } + + public void setWhere(Expression where) { + this.where = where; + } + @Override public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); @@ -135,6 +182,8 @@ public String toString() { buffer.append(")"); + appendPostgreSqlTail(buffer); + if (tailParameters != null) { for (String param : tailParameters) { buffer.append(" ").append(param); @@ -145,6 +194,25 @@ public String toString() { return buffer.toString(); } + private void appendPostgreSqlTail(StringBuilder buffer) { + if (includeColumns != null) { + buffer.append(" INCLUDE (").append(String.join(", ", includeColumns)).append(")"); + } + if (nullsDistinct != null) { + buffer.append(" NULLS ").append(nullsDistinct ? "DISTINCT" : "NOT DISTINCT"); + } + if (storageParameters != null) { + buffer.append(" WITH ") + .append(PlainSelect.getStringList(storageParameters, true, true)); + } + if (tableSpace != null) { + buffer.append(" TABLESPACE ").append(tableSpace); + } + if (where != null) { + buffer.append(" WHERE ").append(where); + } + } + public CreateIndex withTable(Table table) { this.setTable(table); return this; @@ -169,4 +237,29 @@ public CreateIndex withOnly(boolean only) { setOnly(only); return this; } + + public CreateIndex withIncludeColumns(List includeColumns) { + setIncludeColumns(includeColumns); + return this; + } + + public CreateIndex withNullsDistinct(Boolean nullsDistinct) { + setNullsDistinct(nullsDistinct); + return this; + } + + public CreateIndex withStorageParameters(List storageParameters) { + setStorageParameters(storageParameters); + return this; + } + + public CreateIndex withTableSpace(String tableSpace) { + setTableSpace(tableSpace); + return this; + } + + public CreateIndex withWhere(Expression where) { + setWhere(where); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index 114b30d147..9232f1daee 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -200,9 +200,22 @@ public void setCommentText(String commentText) { } public static class ColumnParams implements Serializable { + public enum SortOrder { + ASC, DESC + } + + public enum NullOrdering { + FIRST, LAST + } + public final String columnName; public final List params; private final Expression expression; + private String collation; + private String operatorClass; + private List