From 1ec22b03dd76bcb8db931580f5b4b2ce75823131 Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:15:32 +0200 Subject: [PATCH 1/8] Support BigQuery array subscript OFFSET(n) BigQuery addresses array elements positionally with OFFSET(n), ORDINAL(n), SAFE_OFFSET(n) and SAFE_ORDINAL(n). The latter three already parsed as functions; OFFSET is a reserved keyword and needed an explicit array-index rule. The accessor is kept in the AST as a Function so it deparses unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 27 +++++++++++++++++-- .../statement/select/BigQueryTest.java | 23 ++++++++++++++++ 2 files changed, 48 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 4ab8d15051..85de84c61d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -9459,7 +9459,7 @@ Expression ArrayExpression(Expression obj): { Expression stopExpr = null; } { "[" - [LOOKAHEAD(3) idxExpr = SimpleExpression()] + [LOOKAHEAD(3) idxExpr = ArrayIndexExpression()] [ (":" { startExpr=idxExpr; idxExpr=null; }) [stopExpr = SimpleExpression()] @@ -9468,7 +9468,7 @@ Expression ArrayExpression(Expression obj): { { expr = new ArrayExpression(obj, idxExpr, startExpr, stopExpr); } ( LOOKAHEAD(2) "[" - [LOOKAHEAD(3) idxExpr = SimpleExpression()] + [LOOKAHEAD(3) idxExpr = ArrayIndexExpression()] [ (":" { startExpr=idxExpr; idxExpr=null; }) [stopExpr = SimpleExpression()] @@ -9479,6 +9479,29 @@ Expression ArrayExpression(Expression obj): { { return expr; } } +/** + * Array subscript index. BigQuery addresses array elements positionally with + * OFFSET(n), ORDINAL(n), SAFE_OFFSET(n) and SAFE_ORDINAL(n). Only OFFSET needs + * an explicit rule here, the others are plain identifiers and parse as functions. + */ +Expression ArrayIndexExpression() : +{ + Token token; + Expression expr; +} +{ + ( + LOOKAHEAD(2) token = "(" expr = SimpleExpression() ")" + { + return new Function() + .withName(token.image) + .withParameters(new ExpressionList(expr)); + } + | + expr = SimpleExpression() { return expr; } + ) +} + Expression NavigationStep(Expression base): { Expression step = null; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 81a3217844..a49dbb513e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.select; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ArrayExpression; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -110,4 +111,26 @@ void testTimeSeriesFunction() throws JSQLParserException { TableFunction function = select.getFromItem(TableFunction.class); Assertions.assertEquals("TABLE", function.getFunction().getExtraKeyword()); } + + @Test + void testArrayOffsetAccessor() throws JSQLParserException { + String sqlStr = "SELECT arr[OFFSET(0)] FROM t"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + ArrayExpression arrayExpression = + (ArrayExpression) select.getSelectItem(0).getExpression(); + + Assertions.assertEquals("OFFSET(0)", arrayExpression.getIndexExpression().toString()); + } + + @Test + void testArraySafeOffsetAndOrdinalAccessors() throws JSQLParserException { + String sqlStr = "SELECT arr[ORDINAL(1)], arr[SAFE_OFFSET(2)], arr[SAFE_ORDINAL(3)] FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testOffsetClauseStillParses() throws JSQLParserException { + String sqlStr = "SELECT a FROM t LIMIT 10 OFFSET 5"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } From 9dd197a7d54040e278e9b8783535028798dac69a Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:19:11 +0200 Subject: [PATCH 2/8] Support BigQuery UNNEST ... WITH OFFSET after the alias BigQuery writes UNNEST(array) [AS alias] WITH OFFSET [AS offsetAlias], where WITH OFFSET follows the table alias and carries an alias of its own. The pre-existing pre-alias form (WITH ORDINALITY|OFFSET AS t(a, b)) is untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../statement/select/TableFunction.java | 40 +++++++++++++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 7 ++++ .../statement/select/BigQueryTest.java | 27 +++++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java index 51e4057ec8..359ab7e770 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java @@ -24,6 +24,8 @@ public class TableFunction extends Function implements FromItem { private Function function; private ParenthesedExpressionList rowsFromFunctions; private String withClause = null; + private boolean withOffset = false; + private Alias offsetAlias = null; public TableFunction(Function function) { this.function = function; @@ -121,6 +123,37 @@ public TableFunction setPrefix(String prefix) { return this; } + /** + * BigQuery's {@code UNNEST(array) [AS alias] WITH OFFSET [AS offsetAlias]}, where the + * {@code WITH OFFSET} clause follows the table alias and carries an alias of its own. + */ + public boolean isWithOffset() { + return withOffset; + } + + public void setWithOffset(boolean withOffset) { + this.withOffset = withOffset; + } + + public TableFunction withWithOffset(boolean withOffset) { + setWithOffset(withOffset); + return this; + } + + public Alias getOffsetAlias() { + return offsetAlias; + } + + public void setOffsetAlias(Alias offsetAlias) { + this.offsetAlias = offsetAlias; + this.withOffset = true; + } + + public TableFunction withOffsetAlias(Alias offsetAlias) { + setOffsetAlias(offsetAlias); + return this; + } + public String getWithClause() { return withClause; } @@ -211,6 +244,13 @@ public StringBuilder appendTo(StringBuilder builder) { if (alias != null) { builder.append(alias); } + + if (withOffset) { + builder.append(" WITH OFFSET"); + if (offsetAlias != null) { + builder.append(offsetAlias); + } + } return builder; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 85de84c61d..156bb59f98 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -7642,6 +7642,13 @@ FromItem FromItem() #FromItem: ) [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { fromItem.setAlias(alias); } ] + [ + // BigQuery: UNNEST(array) AS alias WITH OFFSET AS offsetAlias + LOOKAHEAD(2, { fromItem instanceof TableFunction }) + { ((TableFunction) fromItem).setWithOffset(true); } + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() + { ((TableFunction) fromItem).setOffsetAlias(alias); } ] + ] [ LOOKAHEAD(2, {fromItem instanceof Table }) timeTravelStr = TimeTravelAfterAlias() { ((Table) fromItem).setTimeTravelStrAfterAlias(timeTravelStr); } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index a49dbb513e..cfba8e1d90 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -133,4 +133,31 @@ void testOffsetClauseStillParses() throws JSQLParserException { String sqlStr = "SELECT a FROM t LIMIT 10 OFFSET 5"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testUnnestWithOffsetAfterAlias() throws JSQLParserException { + String sqlStr = "SELECT x, pos FROM UNNEST([1, 2, 3]) AS x WITH OFFSET AS pos"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + TableFunction unnest = (TableFunction) select.getFromItem(); + + Assertions.assertTrue(unnest.isWithOffset()); + Assertions.assertEquals("x", unnest.getAlias().getName()); + Assertions.assertEquals("pos", unnest.getOffsetAlias().getName()); + } + + @Test + void testUnnestWithOffsetWithoutOffsetAlias() throws JSQLParserException { + String sqlStr = "SELECT * FROM t, UNNEST(t.arr) AS c WITH OFFSET"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUnnestWithOrdinalityAliasStillBindsToTableFunction() throws JSQLParserException { + String sqlStr = "SELECT * FROM UNNEST(ARRAY[1, 2, 3]) WITH ORDINALITY AS t (a, b)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + TableFunction unnest = (TableFunction) select.getFromItem(); + + Assertions.assertFalse(unnest.isWithOffset()); + Assertions.assertEquals("t", unnest.getAlias().getName()); + } } From aaac0c3502fc4bb0a81bf00044c802b472b1cc86 Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:21:45 +0200 Subject: [PATCH 3/8] Support ORDER BY after IGNORE NULLS in aggregate functions BigQuery orders aggregate arguments as ARRAY_AGG(expr [IGNORE NULLS] [ORDER BY key] [LIMIT n]), so the ORDER BY may follow the null handling. Only the pre-null-handling position was accepted before. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 10 +++++++++- .../statement/select/BigQueryTest.java | 18 ++++++++++++++++++ 2 files changed, 27 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 156bb59f98..089484de42 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11519,6 +11519,7 @@ Function InternalFunction(boolean escaped): Column attributeColumn = null; Token attributeToken = null; Limit limit; + List orderByList = null; List keywordArgs = null; } { @@ -11568,6 +11569,12 @@ Function InternalFunction(boolean escaped): ) ] + // BigQuery orders aggregate arguments after the null handling: + // ARRAY_AGG(expr [IGNORE NULLS] [ORDER BY key] [LIMIT n]) + [ + LOOKAHEAD(2) orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } + ] + [ limit = PlainLimit() { retval.setLimit(limit); } ] @@ -11651,7 +11658,8 @@ ExpressionList FunctionArgumentList(Function retval): { if ("XMLFOREST".equalsIgnoreCase(retval.getName())) { return XmlForestArguments(); } } [ LOOKAHEAD(2) extraKeywordToken = { retval.setExtraKeyword(extraKeywordToken.image); } ] expressionList=ExpressionList() - [ orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } ] + // explicit lookahead: InternalFunction accepts ORDER BY after IGNORE NULLS too + [ LOOKAHEAD(2) orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } ] // https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/LISTAGG.html [ diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index cfba8e1d90..45ce6e5ff6 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -160,4 +160,22 @@ void testUnnestWithOrdinalityAliasStillBindsToTableFunction() throws JSQLParserE Assertions.assertFalse(unnest.isWithOffset()); Assertions.assertEquals("t", unnest.getAlias().getName()); } + + @Test + void testAggregateFunctionIgnoreNullsBeforeOrderBy() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(x IGNORE NULLS ORDER BY x) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAggregateFunctionIgnoreNullsOrderByLimit() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(DISTINCT x IGNORE NULLS ORDER BY x DESC LIMIT 5) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAggregateFunctionOrderByWithoutNullHandling() throws JSQLParserException { + String sqlStr = "SELECT ARRAY_AGG(x ORDER BY x) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } From 6d3316c607dc3bec3022cf6025b2173ded5f1492 Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:24:50 +0200 Subject: [PATCH 4/8] Support BigQuery PARTITION BY expression in CREATE TABLE BigQuery partitions by a bare expression rather than a partitioning method: PARTITION BY d, PARTITION BY DATE(ts), PARTITION BY RANGE_BUCKET(id, GENERATE_ARRAY(0, 100, 10)). Adds TablePartitioning.Type.EXPRESSION, which renders the expression without a method keyword or parentheses. The MySQL HASH/KEY/RANGE/LIST forms keep priority. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../create/table/TablePartitioning.java | 11 +++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 20 ++++++++-- .../statement/select/BigQueryTest.java | 38 +++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) 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 70b127dbba..1c96a39ea1 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 @@ -27,7 +27,12 @@ public class TablePartitioning implements Serializable { public enum Type { - HASH, KEY, RANGE, LIST + HASH, KEY, RANGE, LIST, + /** + * BigQuery partitions by a bare expression, without a partitioning method keyword: + * {@code PARTITION BY DATE(ts)}. + */ + EXPRESSION } private Type type; @@ -285,6 +290,10 @@ public void appendTo(StringBuilder builder, Consumer expressionPrint } private void appendMethod(StringBuilder builder, Consumer expressionPrinter) { + if (type == Type.EXPRESSION) { + expressionPrinter.accept(expression); + return; + } if (linear) { builder.append("LINEAR "); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 089484de42..49412e25fa 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14895,6 +14895,7 @@ TablePartitioning CreateTablePartitioning(): { TablePartitioning partitioning = null; TablePartitioning subPartitioning = null; + Expression expression = null; ExpressionList expressionList = null; ExpressionList columns = null; List partitionDefinitions = null; @@ -14905,9 +14906,11 @@ TablePartitioning CreateTablePartitioning(): { ( - partitioning=HashOrKeyPartitioning() + // explicit lookaheads: the method keywords win over the BigQuery + // bare-expression alternative below, which they share a prefix with + LOOKAHEAD(2) partitioning=HashOrKeyPartitioning() | - { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } + LOOKAHEAD(2) { partitioning = new TablePartitioning(TablePartitioning.Type.RANGE); } ( "(" expressionList=SimpleExpressionList() ")" { if (expressionList.size() == 1) { @@ -14924,7 +14927,7 @@ TablePartitioning CreateTablePartitioning(): } ) | - { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } + LOOKAHEAD(2) { partitioning = new TablePartitioning(TablePartitioning.Type.LIST); } ( "(" expressionList=SimpleExpressionList() ")" { if (expressionList.size() == 1) { @@ -14940,6 +14943,17 @@ TablePartitioning CreateTablePartitioning(): partitioning.setColumns(columns); } ) + | + // BigQuery partitions by a bare expression: PARTITION BY d, PARTITION BY DATE(ts), + // PARTITION BY RANGE_BUCKET(id, GENERATE_ARRAY(0, 100, 10)) + LOOKAHEAD({ getToken(1).kind != K_HASH && getToken(1).kind != K_KEY + && getToken(1).kind != K_LINEAR && getToken(1).kind != K_RANGE + && getToken(1).kind != K_LIST }) + expression=SimpleExpression() + { + partitioning = new TablePartitioning(TablePartitioning.Type.EXPRESSION); + partitioning.setExpression(expression); + } ) [ LOOKAHEAD({ getToken(1).kind == K_PARTITIONS }) tk= { partitioning.setPartitions(Long.valueOf(tk.image)); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 45ce6e5ff6..4965ed9952 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -11,6 +11,8 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.ArrayExpression; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -178,4 +180,40 @@ void testAggregateFunctionOrderByWithoutNullHandling() throws JSQLParserExceptio String sqlStr = "SELECT ARRAY_AGG(x ORDER BY x) FROM t"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testCreateTablePartitionByExpression() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.t (id INT64, ts TIMESTAMP) PARTITION BY DATE(ts)"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(TablePartitioning.Type.EXPRESSION, + createTable.getPartitioning().getType()); + Assertions.assertEquals("DATE(ts)", + createTable.getPartitioning().getExpression().toString()); + } + + @Test + void testCreateTablePartitionByColumnWithOptions() throws JSQLParserException { + String sqlStr = + "CREATE TABLE ds.t (id INT64, d DATE) PARTITION BY d OPTIONS (description = 'x')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTableAsSelectPartitionByAndClusterBy() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.t PARTITION BY d CLUSTER BY id " + + "AS SELECT 1 AS id, CURRENT_DATE() AS d"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTablePartitionByHashStillParses() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (id INT) PARTITION BY HASH (id)"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(TablePartitioning.Type.HASH, + createTable.getPartitioning().getType()); + } } From 163ff406bad54d45d6a00b10d1542a31157293ab Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:27:49 +0200 Subject: [PATCH 5/8] Support BigQuery ASSERT statement Adds ASSERT expression [AS description] as a statement, wired through the statement visitors, deparser and table-name finder. ASSERT stays a non-reserved keyword. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../jsqlparser/statement/AssertStatement.java | 81 +++++++++++++++++++ .../statement/StatementFeatureVisitor.java | 7 ++ .../statement/StatementVisitor.java | 6 ++ .../statement/StatementVisitorAdapter.java | 5 ++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 +++ .../util/deparser/StatementDeParser.java | 7 ++ .../validator/StatementValidator.java | 11 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 19 +++++ .../statement/select/BigQueryTest.java | 25 ++++++ 9 files changed, 173 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/AssertStatement.java diff --git a/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java new file mode 100644 index 0000000000..0879faf685 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/AssertStatement.java @@ -0,0 +1,81 @@ +/*- + * #%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; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +/** + * BigQuery's {@code ASSERT expression [AS description]}, which fails the script when the expression + * does not evaluate to {@code TRUE}. + * + * @see Debugging + * statements + */ +public class AssertStatement implements Statement { + private Expression expression; + private StringValue description; + + public AssertStatement() {} + + public AssertStatement(Expression expression) { + this.expression = expression; + } + + public AssertStatement(Expression expression, StringValue description) { + this.expression = expression; + this.description = description; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public AssertStatement withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public StringValue getDescription() { + return description; + } + + public void setDescription(StringValue description) { + this.description = description; + } + + public AssertStatement withDescription(StringValue description) { + setDescription(description); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ASSERT ").append(expression); + if (description != null) { + builder.append(" AS ").append(description); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 861612fecd..2152db4e05 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -521,6 +521,13 @@ public Void visit(Drop drop, S context) { return null; } + @Override + public Void visit(AssertStatement assertStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA); + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 8ec9dc1bc0..789df0a75c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -371,6 +371,12 @@ default void visit(RenameTableStatement renameTableStatement) { this.visit(renameTableStatement, null); } + T visit(AssertStatement assertStatement, S context); + + default void visit(AssertStatement assertStatement) { + this.visit(assertStatement, null); + } + T visit(PurgeStatement purgeStatement, S context); default void visit(PurgeStatement purgeStatement) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index ab05725ad5..e699fcc880 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -618,6 +618,11 @@ public T visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public T visit(AssertStatement assertStatement, S context) { + return null; + } + @Override public T visit(PurgeStatement purgeStatement, 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 3bf8df2e0b..95ad144bf8 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -209,6 +209,7 @@ import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.update.UpdateSet; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.AssertStatement; /** @@ -2464,6 +2465,17 @@ public void visit(RenameTableStatement renameTableStatement) { StatementVisitor.super.visit(renameTableStatement); } + @Override + public Void visit(AssertStatement assertStatement, S context) { + assertStatement.getExpression().accept(this, context); + return null; + } + + @Override + public void visit(AssertStatement assertStatement) { + StatementVisitor.super.visit(assertStatement); + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { 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 69cb9770cb..f801a55bda 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -97,6 +97,7 @@ import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.AssertStatement; public class StatementDeParser extends AbstractDeParser implements StatementVisitor { @@ -552,6 +553,12 @@ public StringBuilder visit(RenameTableStatement renameTableStatement, S cont return builder; } + @Override + public StringBuilder visit(AssertStatement assertStatement, S context) { + assertStatement.appendTo(builder); + return builder; + } + @Override public StringBuilder visit(PurgeStatement purgeStatement, S context) { purgeStatement.appendTo(builder); 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 6764ef740d..95d520a070 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 @@ -99,6 +99,7 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.statement.AssertStatement; /** * @author gitmotte @@ -477,6 +478,12 @@ public Void visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public Void visit(AssertStatement assertStatement, S context) { + // TODO: not yet implemented + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { // TODO: not yet implemented @@ -693,6 +700,10 @@ public void visit(RenameTableStatement renameTableStatement) { visit(renameTableStatement, null); } + public void visit(AssertStatement assertStatement) { + visit(assertStatement, null); + } + public void visit(PurgeStatement purgeStatement) { visit(purgeStatement, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 49412e25fa..046ef31050 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1933,6 +1933,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2912,6 +2913,8 @@ Statement SingleStatement() : | LOOKAHEAD({ isKeywordAhead("REVOKE") }) stm = Revoke(false) | + LOOKAHEAD({ getToken(1).kind == K_ASSERT }) stm = AssertStatement() + | stm = PurgeStatement() | stm = SessionStatement() @@ -4313,6 +4316,22 @@ RenameTableStatement RenameTableStatement(): { } } +/** + * BigQuery's ASSERT expression [AS description] debugging statement. + */ +AssertStatement AssertStatement() #AssertStatement: +{ + AssertStatement assertStatement = new AssertStatement(); + Expression expression; + Token token; +} +{ + expression=Expression() { assertStatement.setExpression(expression); } + [ token= + { assertStatement.setDescription(new StringValue(token.image)); } ] + { return assertStatement; } +} + PurgeStatement PurgeStatement(): { PurgeStatement purgeStatement = null; Table table; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 4965ed9952..923054fd68 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.ArrayExpression; +import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestUtils; @@ -216,4 +217,28 @@ void testCreateTablePartitionByHashStillParses() throws JSQLParserException { Assertions.assertEquals(TablePartitioning.Type.HASH, createTable.getPartitioning().getType()); } + + @Test + void testAssertStatementWithDescription() throws JSQLParserException { + String sqlStr = "ASSERT (SELECT COUNT(*) FROM t) > 0 AS 'table is empty'"; + AssertStatement assertStatement = + (AssertStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'table is empty'", assertStatement.getDescription().toString()); + } + + @Test + void testAssertStatementWithoutDescription() throws JSQLParserException { + String sqlStr = "ASSERT EXISTS(SELECT 1 FROM t)"; + AssertStatement assertStatement = + (AssertStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(assertStatement.getDescription()); + } + + @Test + void testAssertRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT assert FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } } From c70cf0a306e4d18a438a70cca3087ee97e00e27c Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:31:01 +0200 Subject: [PATCH 6/8] Support BigQuery EXPORT DATA statement Adds EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query. Options are kept as an expression list, and the exported query is reachable from TablesNamesFinder. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../statement/StatementFeatureVisitor.java | 8 ++ .../statement/StatementVisitor.java | 7 ++ .../statement/StatementVisitorAdapter.java | 6 ++ .../statement/export/ExportDataStatement.java | 90 +++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 +++ .../util/deparser/StatementDeParser.java | 7 ++ .../validator/StatementValidator.java | 11 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 ++++++ .../statement/select/BigQueryTest.java | 31 +++++++ 9 files changed, 200 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 2152db4e05..040ef08988 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -93,6 +93,7 @@ import java.util.Locale; import java.util.Set; import java.util.function.Predicate; +import net.sf.jsqlparser.statement.export.ExportDataStatement; /** * Derives a {@link StatementFeatures} verdict from a statement tree. @@ -528,6 +529,13 @@ public Void visit(AssertStatement assertStatement, S context) { return null; } + @Override + public Void visit(ExportDataStatement exportDataStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA, StmtFeature.MODIFIES_DATA); + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 789df0a75c..0b80f60ad5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -68,6 +68,7 @@ import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.export.ExportDataStatement; public interface StatementVisitor { @@ -377,6 +378,12 @@ default void visit(AssertStatement assertStatement) { this.visit(assertStatement, null); } + T visit(ExportDataStatement exportDataStatement, S context); + + default void visit(ExportDataStatement exportDataStatement) { + this.visit(exportDataStatement, null); + } + T visit(PurgeStatement purgeStatement, S context); default void visit(PurgeStatement purgeStatement) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index e699fcc880..0c908b0da4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -89,6 +89,7 @@ import java.util.List; import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.statement.export.ExportDataStatement; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class StatementVisitorAdapter implements StatementVisitor { @@ -623,6 +624,11 @@ public T visit(AssertStatement assertStatement, S context) { return null; } + @Override + public T visit(ExportDataStatement exportDataStatement, S context) { + return null; + } + @Override public T visit(PurgeStatement purgeStatement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java b/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java new file mode 100644 index 0000000000..e9571a397a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/export/ExportDataStatement.java @@ -0,0 +1,90 @@ +/*- + * #%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.export; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.select.Select; + +/** + * BigQuery's {@code EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query}. + * + * @see Export + * statements + */ +public class ExportDataStatement implements Statement { + private String connectionName; + private ExpressionList options; + private Select select; + + public String getConnectionName() { + return connectionName; + } + + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + public ExportDataStatement withConnectionName(String connectionName) { + setConnectionName(connectionName); + return this; + } + + public ExpressionList getOptions() { + return options; + } + + public void setOptions(ExpressionList options) { + this.options = options; + } + + public ExportDataStatement withOptions(ExpressionList options) { + setOptions(options); + return this; + } + + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public ExportDataStatement withSelect(Select select) { + setSelect(select); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("EXPORT DATA"); + if (connectionName != null) { + builder.append(" WITH CONNECTION ").append(connectionName); + } + if (options != null) { + builder.append(" OPTIONS (").append(options).append(")"); + } + builder.append(" AS ").append(select); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.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 95ad144bf8..4acaa4e07f 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -210,6 +210,7 @@ import net.sf.jsqlparser.statement.update.UpdateSet; import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.export.ExportDataStatement; /** @@ -2476,6 +2477,17 @@ public void visit(AssertStatement assertStatement) { StatementVisitor.super.visit(assertStatement); } + @Override + public Void visit(ExportDataStatement exportDataStatement, S context) { + exportDataStatement.getSelect().accept((SelectVisitor) this, context); + return null; + } + + @Override + public void visit(ExportDataStatement exportDataStatement) { + StatementVisitor.super.visit(exportDataStatement); + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { 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 f801a55bda..81523bb606 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -98,6 +98,7 @@ import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.export.ExportDataStatement; public class StatementDeParser extends AbstractDeParser implements StatementVisitor { @@ -559,6 +560,12 @@ public StringBuilder visit(AssertStatement assertStatement, S context) { return builder; } + @Override + public StringBuilder visit(ExportDataStatement exportDataStatement, S context) { + exportDataStatement.appendTo(builder); + return builder; + } + @Override public StringBuilder visit(PurgeStatement purgeStatement, S context) { purgeStatement.appendTo(builder); 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 95d520a070..ea48547c38 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 @@ -100,6 +100,7 @@ import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; import net.sf.jsqlparser.statement.AssertStatement; +import net.sf.jsqlparser.statement.export.ExportDataStatement; /** * @author gitmotte @@ -484,6 +485,12 @@ public Void visit(AssertStatement assertStatement, S context) { return null; } + @Override + public Void visit(ExportDataStatement exportDataStatement, S context) { + // TODO: not yet implemented + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { // TODO: not yet implemented @@ -704,6 +711,10 @@ public void visit(AssertStatement assertStatement) { visit(assertStatement, null); } + public void visit(ExportDataStatement exportDataStatement) { + visit(exportDataStatement, null); + } + public void visit(PurgeStatement purgeStatement) { visit(purgeStatement, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 046ef31050..083cd778b8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2915,6 +2915,8 @@ Statement SingleStatement() : | LOOKAHEAD({ getToken(1).kind == K_ASSERT }) stm = AssertStatement() | + LOOKAHEAD({ getToken(1).kind == K_EXPORT && getToken(2).kind == K_DATA }) stm = ExportDataStatement() + | stm = PurgeStatement() | stm = SessionStatement() @@ -4316,6 +4318,32 @@ RenameTableStatement RenameTableStatement(): { } } +/** + * BigQuery's EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query. + */ +ExportDataStatement ExportDataStatement() #ExportDataStatement: +{ + ExportDataStatement exportDataStatement = new ExportDataStatement(); + ExpressionList options; + ObjectNames connectionName; + Select select; +} +{ + + [ + LOOKAHEAD({ getToken(1).kind == K_WITH && "CONNECTION".equalsIgnoreCase(getToken(2).image) }) + connectionName=RelObjectNames() + { exportDataStatement.setConnectionName(String.join(".", connectionName.getNames())); } + ] + [ + LOOKAHEAD({ isKeywordAhead("OPTIONS") }) + "(" options=ExpressionList() ")" + { exportDataStatement.setOptions(options); } + ] + select=Select() { exportDataStatement.setSelect(select); } + { return exportDataStatement; } +} + /** * BigQuery's ASSERT expression [AS description] debugging statement. */ diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 923054fd68..d587857550 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -13,6 +13,7 @@ import net.sf.jsqlparser.expression.ArrayExpression; import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.export.ExportDataStatement; import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -241,4 +242,34 @@ void testAssertRemainsUsableAsIdentifier() throws JSQLParserException { String sqlStr = "SELECT assert FROM t"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testExportData() throws JSQLParserException { + String sqlStr = "EXPORT DATA OPTIONS (uri = 'gs://bucket/*.csv', format = 'CSV', " + + "overwrite = true) AS SELECT * FROM t"; + ExportDataStatement exportData = + (ExportDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(3, exportData.getOptions().size()); + Assertions.assertNull(exportData.getConnectionName()); + } + + @Test + void testExportDataWithConnection() throws JSQLParserException { + String sqlStr = "EXPORT DATA WITH CONNECTION myproject.us.myconnection " + + "OPTIONS (uri = 'gs://bucket/*.csv') AS SELECT a FROM t"; + ExportDataStatement exportData = + (ExportDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("myproject.us.myconnection", exportData.getConnectionName()); + } + + @Test + void testExportDataTablesNamesFinder() throws JSQLParserException { + String sqlStr = "EXPORT DATA OPTIONS (uri = 'gs://bucket/*.csv') AS SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } } From 13aee6c14e3ff2b36b301cbaad02862f9158580a Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:34:55 +0200 Subject: [PATCH 7/8] Support BigQuery LOAD DATA statement Adds LOAD DATA {INTO | OVERWRITE} table [(column_definitions)] [OPTIONS (option_list)] FROM FILES (file_options) [WITH PARTITION COLUMNS] [WITH CONNECTION connection]. LOAD becomes a non-reserved keyword so that a statement never starts with a bare identifier, which would make statement starts ambiguous elsewhere. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../statement/StatementFeatureVisitor.java | 8 + .../statement/StatementVisitor.java | 7 + .../statement/StatementVisitorAdapter.java | 6 + .../statement/load/LoadDataStatement.java | 156 ++++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 12 ++ .../util/deparser/StatementDeParser.java | 7 + .../validator/StatementValidator.java | 11 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 46 ++++++ .../statement/select/BigQueryTest.java | 45 +++++ 9 files changed, 298 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/load/LoadDataStatement.java diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 040ef08988..51dd3493ab 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -94,6 +94,7 @@ import java.util.Set; import java.util.function.Predicate; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; /** * Derives a {@link StatementFeatures} verdict from a statement tree. @@ -536,6 +537,13 @@ public Void visit(ExportDataStatement exportDataStatement, S context) { return null; } + @Override + public Void visit(LoadDataStatement loadDataStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_DATA); + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 0b80f60ad5..ae2b286768 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -69,6 +69,7 @@ import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; public interface StatementVisitor { @@ -384,6 +385,12 @@ default void visit(ExportDataStatement exportDataStatement) { this.visit(exportDataStatement, null); } + T visit(LoadDataStatement loadDataStatement, S context); + + default void visit(LoadDataStatement loadDataStatement) { + this.visit(loadDataStatement, null); + } + T visit(PurgeStatement purgeStatement, S context); default void visit(PurgeStatement purgeStatement) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 0c908b0da4..e3df12b1a4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -90,6 +90,7 @@ import java.util.List; import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class StatementVisitorAdapter implements StatementVisitor { @@ -629,6 +630,11 @@ public T visit(ExportDataStatement exportDataStatement, S context) { return null; } + @Override + public T visit(LoadDataStatement loadDataStatement, S context) { + return null; + } + @Override public T visit(PurgeStatement purgeStatement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/statement/load/LoadDataStatement.java b/src/main/java/net/sf/jsqlparser/statement/load/LoadDataStatement.java new file mode 100644 index 0000000000..3e04159f1e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/load/LoadDataStatement.java @@ -0,0 +1,156 @@ +/*- + * #%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.load; + +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.select.PlainSelect; + +import java.util.List; + +/** + * BigQuery's {@code LOAD DATA {INTO | OVERWRITE} table [(column_definitions)] [OPTIONS + * (option_list)] FROM FILES (file_options) [WITH PARTITION COLUMNS] [WITH CONNECTION connection]}. + * + * @see Load + * statements + */ +public class LoadDataStatement implements Statement { + private boolean overwrite; + private Table table; + private List columnDefinitions; + private ExpressionList options; + private ExpressionList fileOptions; + private boolean withPartitionColumns; + private String connectionName; + + public boolean isOverwrite() { + return overwrite; + } + + public void setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + } + + public LoadDataStatement withOverwrite(boolean overwrite) { + setOverwrite(overwrite); + return this; + } + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public LoadDataStatement withTable(Table table) { + setTable(table); + return this; + } + + public List getColumnDefinitions() { + return columnDefinitions; + } + + public void setColumnDefinitions(List columnDefinitions) { + this.columnDefinitions = columnDefinitions; + } + + public LoadDataStatement withColumnDefinitions(List columnDefinitions) { + setColumnDefinitions(columnDefinitions); + return this; + } + + public ExpressionList getOptions() { + return options; + } + + public void setOptions(ExpressionList options) { + this.options = options; + } + + public LoadDataStatement withOptions(ExpressionList options) { + setOptions(options); + return this; + } + + public ExpressionList getFileOptions() { + return fileOptions; + } + + public void setFileOptions(ExpressionList fileOptions) { + this.fileOptions = fileOptions; + } + + public LoadDataStatement withFileOptions(ExpressionList fileOptions) { + setFileOptions(fileOptions); + return this; + } + + public boolean isWithPartitionColumns() { + return withPartitionColumns; + } + + public void setWithPartitionColumns(boolean withPartitionColumns) { + this.withPartitionColumns = withPartitionColumns; + } + + public LoadDataStatement withWithPartitionColumns(boolean withPartitionColumns) { + setWithPartitionColumns(withPartitionColumns); + return this; + } + + public String getConnectionName() { + return connectionName; + } + + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + public LoadDataStatement withConnectionName(String connectionName) { + setConnectionName(connectionName); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("LOAD DATA ").append(overwrite ? "OVERWRITE " : "INTO ").append(table); + if (columnDefinitions != null && !columnDefinitions.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(columnDefinitions, true, true)); + } + if (options != null) { + builder.append(" OPTIONS (").append(options).append(")"); + } + builder.append(" FROM FILES (").append(fileOptions).append(")"); + if (withPartitionColumns) { + builder.append(" WITH PARTITION COLUMNS"); + } + if (connectionName != null) { + builder.append(" WITH CONNECTION ").append(connectionName); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.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 4acaa4e07f..658dfea877 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -211,6 +211,7 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; /** @@ -2488,6 +2489,17 @@ public void visit(ExportDataStatement exportDataStatement) { StatementVisitor.super.visit(exportDataStatement); } + @Override + public Void visit(LoadDataStatement loadDataStatement, S context) { + loadDataStatement.getTable().accept(this, context); + return null; + } + + @Override + public void visit(LoadDataStatement loadDataStatement) { + StatementVisitor.super.visit(loadDataStatement); + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { 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 81523bb606..c8f4e3902e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -99,6 +99,7 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; public class StatementDeParser extends AbstractDeParser implements StatementVisitor { @@ -566,6 +567,12 @@ public StringBuilder visit(ExportDataStatement exportDataStatement, S contex return builder; } + @Override + public StringBuilder visit(LoadDataStatement loadDataStatement, S context) { + loadDataStatement.appendTo(builder); + return builder; + } + @Override public StringBuilder visit(PurgeStatement purgeStatement, S context) { purgeStatement.appendTo(builder); 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 ea48547c38..1ed3a2ec92 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 @@ -101,6 +101,7 @@ import net.sf.jsqlparser.util.validation.metadata.NamedObject; import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; /** * @author gitmotte @@ -491,6 +492,12 @@ public Void visit(ExportDataStatement exportDataStatement, S context) { return null; } + @Override + public Void visit(LoadDataStatement loadDataStatement, S context) { + // TODO: not yet implemented + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { // TODO: not yet implemented @@ -715,6 +722,10 @@ public void visit(ExportDataStatement exportDataStatement) { visit(exportDataStatement, null); } + public void visit(LoadDataStatement loadDataStatement) { + visit(loadDataStatement, null); + } + public void visit(PurgeStatement purgeStatement) { visit(purgeStatement, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 083cd778b8..8af5467177 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -72,6 +72,7 @@ import net.sf.jsqlparser.statement.create.view.*; import net.sf.jsqlparser.statement.delete.*; import net.sf.jsqlparser.statement.drop.*; import net.sf.jsqlparser.statement.insert.*; +import net.sf.jsqlparser.statement.load.*; import net.sf.jsqlparser.statement.execute.*; import net.sf.jsqlparser.statement.piped.*; import net.sf.jsqlparser.statement.select.*; @@ -2101,6 +2102,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2917,6 +2919,8 @@ Statement SingleStatement() : | LOOKAHEAD({ getToken(1).kind == K_EXPORT && getToken(2).kind == K_DATA }) stm = ExportDataStatement() | + LOOKAHEAD({ getToken(1).kind == K_LOAD && getToken(2).kind == K_DATA }) stm = LoadDataStatement() + | stm = PurgeStatement() | stm = SessionStatement() @@ -4318,6 +4322,48 @@ RenameTableStatement RenameTableStatement(): { } } +/** + * BigQuery's LOAD DATA {INTO | OVERWRITE} table [(column_definitions)] [OPTIONS (option_list)] + * FROM FILES (file_options) [WITH PARTITION COLUMNS] [WITH CONNECTION connection]. + */ +LoadDataStatement LoadDataStatement() #LoadDataStatement: +{ + LoadDataStatement loadDataStatement = new LoadDataStatement(); + List columnDefinitions = new ArrayList(); + ColumnDefinition columnDefinition; + ExpressionList options; + ObjectNames connectionName; + Table table; +} +{ + + ( | { loadDataStatement.setOverwrite(true); } ) + table=Table() { loadDataStatement.setTable(table); } + [ + LOOKAHEAD(2) "(" + columnDefinition=ColumnDefinition() { columnDefinitions.add(columnDefinition); } + ( "," columnDefinition=ColumnDefinition() { columnDefinitions.add(columnDefinition); } )* + ")" { loadDataStatement.setColumnDefinitions(columnDefinitions); } + ] + [ + LOOKAHEAD({ isKeywordAhead("OPTIONS") }) + "(" options=ExpressionList() ")" { loadDataStatement.setOptions(options); } + ] + + LOOKAHEAD({ isKeywordAhead("FILES") }) + "(" options=ExpressionList() ")" { loadDataStatement.setFileOptions(options); } + [ + LOOKAHEAD(2) + { loadDataStatement.setWithPartitionColumns(true); } + ] + [ + LOOKAHEAD({ getToken(1).kind == K_WITH && "CONNECTION".equalsIgnoreCase(getToken(2).image) }) + connectionName=RelObjectNames() + { loadDataStatement.setConnectionName(String.join(".", connectionName.getNames())); } + ] + { return loadDataStatement; } +} + /** * BigQuery's EXPORT DATA [WITH CONNECTION connection] OPTIONS (option_list) AS query. */ diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index d587857550..0c443637db 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.statement.AssertStatement; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.export.ExportDataStatement; +import net.sf.jsqlparser.statement.load.LoadDataStatement; import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; @@ -272,4 +273,48 @@ void testExportDataTablesNamesFinder() throws JSQLParserException { new net.sf.jsqlparser.util.TablesNamesFinder() .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); } + + @Test + void testLoadDataIntoFromFiles() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO mydataset.mytable " + + "FROM FILES (format = 'AVRO', uris = ['gs://bucket/*.avro'])"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertFalse(loadData.isOverwrite()); + Assertions.assertEquals("mydataset.mytable", loadData.getTable().getFullyQualifiedName()); + Assertions.assertEquals(2, loadData.getFileOptions().size()); + } + + @Test + void testLoadDataOverwriteWithColumnDefinitions() throws JSQLParserException { + String sqlStr = "LOAD DATA OVERWRITE ds.t (id INT64, name STRING) " + + "FROM FILES (format = 'CSV', uris = ['gs://b/f.csv'])"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(loadData.isOverwrite()); + Assertions.assertEquals(2, loadData.getColumnDefinitions().size()); + } + + @Test + void testLoadDataWithOptionsAndConnection() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO ds.t OPTIONS (description = 'x') " + + "FROM FILES (format = 'PARQUET') WITH PARTITION COLUMNS " + + "WITH CONNECTION myproject.us.conn"; + LoadDataStatement loadData = + (LoadDataStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("myproject.us.conn", loadData.getConnectionName()); + Assertions.assertTrue(loadData.isWithPartitionColumns()); + } + + @Test + void testLoadDataTablesNamesFinder() throws JSQLParserException { + String sqlStr = "LOAD DATA INTO ds.t FROM FILES (format = 'CSV')"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } } From eecd758e5bbd3efc57b98e5772e9f2f87c153fba Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Tue, 15 Sep 2026 14:37:46 +0200 Subject: [PATCH 8/8] Support CREATE [SNAPSHOT] TABLE ... CLONE BigQuery snapshots and clones a table with CREATE [SNAPSHOT] TABLE target CLONE source [FOR SYSTEM_TIME AS OF ...]. The clone source is kept on CreateTable and rendered by both toString() and CreateTableDeParser. SNAPSHOT itself already parsed as a create option. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2 --- .../statement/create/table/CreateTable.java | 20 +++++++++++++ .../util/deparser/CreateTableDeParser.java | 3 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 7 +++++ .../statement/select/BigQueryTest.java | 28 +++++++++++++++++++ 4 files changed, 58 insertions(+) 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 3dbcb4600b..d248f7879d 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 List tableElements; private Select select; private Table likeTable; + private Table cloneTable; private ColDataType ofType; private boolean selectParenthesis; private boolean ifNotExists = false; @@ -236,6 +237,22 @@ public void setLikeTable(Table likeTable, boolean parenthesis) { this.selectParenthesis = parenthesis; } + /** + * BigQuery's {@code CREATE [SNAPSHOT] TABLE target CLONE source [FOR SYSTEM_TIME AS OF ...]}. + */ + public Table getCloneTable() { + return cloneTable; + } + + public void setCloneTable(Table cloneTable) { + this.cloneTable = cloneTable; + } + + public CreateTable withCloneTable(Table cloneTable) { + setCloneTable(cloneTable); + return this; + } + public boolean isIfNotExists() { return ifNotExists; } @@ -330,6 +347,9 @@ private void appendCreateClause(StringBuilder b) { if (partitionOf != null) { b.append(" PARTITION OF ").append(partitionOf); } + if (cloneTable != null) { + b.append(" CLONE ").append(cloneTable); + } } private void appendColumnDefinitions(StringBuilder b) { 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 c45bac2882..faed455c30 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -62,6 +62,9 @@ public void deParse(CreateTable createTable) { builder.append(" PARTITION OF ") .append(createTable.getPartitionOf().getFullyQualifiedName()); } + if (createTable.getCloneTable() != null) { + builder.append(" CLONE ").append(createTable.getCloneTable()); + } if (createTable.getColumns() != null && !createTable.getColumns().isEmpty()) { builder.append(" ("); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8af5467177..0ddb836c58 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1975,6 +1975,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -13556,6 +13557,12 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ LOOKAHEAD(2) { createTable.setIfNotExists(true); }] table=Table() + [ + // BigQuery: CREATE [SNAPSHOT] TABLE target CLONE source [FOR SYSTEM_TIME AS OF ...] + LOOKAHEAD(2) likeTable=Table() + [ LOOKAHEAD(2) tableColumn=TimeTravelAfterAlias() { likeTable.setTimeTravel(tableColumn); } ] + { createTable.setCloneTable(likeTable); likeTable = null; } + ] [ ofType=ColDataType() { createTable.setOfType(ofType); } ] [ LOOKAHEAD(2) partitionOfTable=Table() { createTable.setPartitionOf(partitionOfTable); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java index 0c443637db..33a74b2cc0 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java @@ -317,4 +317,32 @@ void testLoadDataTablesNamesFinder() throws JSQLParserException { new net.sf.jsqlparser.util.TablesNamesFinder() .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); } + + @Test + void testCreateSnapshotTableClone() throws JSQLParserException { + String sqlStr = "CREATE SNAPSHOT TABLE ds.snap CLONE ds.t"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("ds.t", createTable.getCloneTable().getFullyQualifiedName()); + } + + @Test + void testCreateSnapshotTableCloneForSystemTimeAsOf() throws JSQLParserException { + String sqlStr = "CREATE SNAPSHOT TABLE IF NOT EXISTS ds.snap CLONE ds.t " + + "FOR SYSTEM_TIME AS OF CURRENT_TIMESTAMP()"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTableCloneWithOptions() throws JSQLParserException { + String sqlStr = "CREATE TABLE ds.copy CLONE ds.t OPTIONS (description = 'x')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCloneRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT clone FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } }