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 d98a0ba1c1..4b22d3a5a9 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -670,6 +670,9 @@ public enum Feature { * @see DeclareStatement */ declare, + + /** SQL Server local table variables in queries and DML targets. */ + tableVariable, /** * @see SetStatement */ diff --git a/src/main/java/net/sf/jsqlparser/schema/Table.java b/src/main/java/net/sf/jsqlparser/schema/Table.java index 1de14a8a5e..b79abbde7a 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Table.java +++ b/src/main/java/net/sf/jsqlparser/schema/Table.java @@ -44,6 +44,8 @@ public class Table extends ASTNodeAccessImpl private List partDelimiters = new ArrayList<>(); + private boolean tableVariable; + // holds the various `time travel` syntax for BigQuery, RedShift, Snowflake or RedShift private String timeTravelStr = null; @@ -67,6 +69,20 @@ public class Table extends ASTNodeAccessImpl public Table() {} + /** Whether this reference names a local table variable rather than a catalog table. */ + public boolean isTableVariable() { + return tableVariable; + } + + public void setTableVariable(boolean tableVariable) { + this.tableVariable = tableVariable; + } + + public Table withTableVariable(boolean tableVariable) { + setTableVariable(tableVariable); + return this; + } + /** * Instantiates a new Table. * @@ -527,6 +543,9 @@ public Table setResolvedTable(Table resolvedTable) { * @return the provided table */ public Table setUnsetCatalogAndSchema(String currentCatalogName, String currentSchemaName) { + if (tableVariable) { + return this; + } String databaseName = getDatabaseName(); if (databaseName == null || databaseName.isEmpty()) { setDatabaseName(currentCatalogName); @@ -561,6 +580,7 @@ public static Table[] setUnsetCatalogAndSchema(String currentCatalogName, @Override public Table clone() { Table clone = new Table(this.getFullyQualifiedName()); + clone.setTableVariable(tableVariable); clone.setResolvedTable(this.resolvedTable != null ? this.resolvedTable.clone() : null); return clone; } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 34270bc3a3..a05fbac86c 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -510,7 +510,9 @@ protected String extractTableName(Table table) { @Override public Void visit(Table table, S context) { String tableWholeName = extractTableName(table); - if (!otherItemNames.contains(tableWholeName)) { + if (table.isTableVariable()) { + otherItemNames.add(tableWholeName); + } else if (!otherItemNames.contains(tableWholeName)) { tables.add(tableWholeName); } if (table.getPivot() != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 431e4d7622..92ea4e0f4d 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -84,6 +84,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.orderBy, Feature.orderByNullOrdering, Feature.tableStatement, + Feature.tableVariable, Feature.function).unmodifyable(); /** diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java index a59e787162..5e7f7c8adb 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java @@ -22,26 +22,20 @@ */ public enum SqlServerVersion implements Version { V2019("2019", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver15 + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver15 Feature.select, Feature.selectInto, Feature.withItem, - Feature.selectGroupBy, Feature.function, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver15 + Feature.selectGroupBy, Feature.function, // https://docs.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver15 // see user_defined_function Feature.tableFunction, Feature.selectHaving, Feature.orderBy, Feature.distinct, - Feature.withItem, Feature.withItemRecursive, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver15 - Feature.top, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver15 + Feature.withItem, Feature.withItemRecursive, // https://docs.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?view=sql-server-ver15 + Feature.top, // https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver15 Feature.offset, Feature.offsetParam, Feature.fetch, Feature.fetchFirst, Feature.fetchNext, @@ -67,26 +61,19 @@ public enum SqlServerVersion implements Version { // https://docs.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-ver15 Feature.insert, Feature.insertValues, - Feature.insertFromSelect, - // https://docs.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver15 + Feature.insertFromSelect, // https://docs.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver15 Feature.update, Feature.updateFrom, // https://docs.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-ver15 - Feature.delete, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15 + Feature.delete, // https://docs.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-ver15 Feature.truncate, - Feature.drop, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-table-transact-sql?view=sql-server-ver15 - Feature.dropTable, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-index-transact-sql?view=sql-server-ver15 - Feature.dropIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-view-transact-sql?view=sql-server-ver15 - Feature.dropView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-schema-transact-sql?view=sql-server-ver15 - Feature.dropSchema, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-sequence-transact-sql?view=sql-server-ver15 + Feature.drop, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-table-transact-sql?view=sql-server-ver15 + Feature.dropTable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-index-transact-sql?view=sql-server-ver15 + Feature.dropIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-view-transact-sql?view=sql-server-ver15 + Feature.dropView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-schema-transact-sql?view=sql-server-ver15 + Feature.dropSchema, // https://docs.microsoft.com/en-us/sql/t-sql/statements/drop-sequence-transact-sql?view=sql-server-ver15 Feature.dropSequence, Feature.dropTableIfExists, Feature.dropIndexIfExists, Feature.dropViewIfExists, Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, @@ -99,38 +86,25 @@ public enum SqlServerVersion implements Version { Feature.set, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql?view=sql-server-ver15 - Feature.alterTable, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-sequence-transact-sql?view=sql-server-ver15 - Feature.alterSequence, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-view-transact-sql?view=sql-server-ver15 - Feature.alterView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-index-transact-sql?view=sql-server-ver15 - Feature.alterIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver15 - Feature.createIndex, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-sequence-transact-sql?view=sql-server-ver15 - Feature.createSequence, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver15 + Feature.alterTable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-sequence-transact-sql?view=sql-server-ver15 + Feature.alterSequence, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-view-transact-sql?view=sql-server-ver15 + Feature.alterView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-index-transact-sql?view=sql-server-ver15 + Feature.alterIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver15 + Feature.createIndex, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-sequence-transact-sql?view=sql-server-ver15 + Feature.createSequence, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver15 Feature.createTable, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver15 + Feature.createTableFromSelect, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-function-transact-sql?view=sql-server-ver15 // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-procedure-transact-sql?view=sql-server-ver15 Feature.functionalStatement, Feature.createProcedure, Feature.createFunction, Feature.block, Feature.declare, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-schema-transact-sql?view=sql-server-ver15 - Feature.createSchema, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql?view=sql-server-ver15 - Feature.createView, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver15 - Feature.createTrigger, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver15 - Feature.merge, - // https://docs.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver15 - Feature.grant, - // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/commit-transaction-transact-sql?view=sql-server-ver15 - Feature.commit, - // special sql-server features + Feature.tableVariable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-schema-transact-sql?view=sql-server-ver15 + Feature.createSchema, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql?view=sql-server-ver15 + Feature.createView, // https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver15 + Feature.createTrigger, // https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver15 + Feature.merge, // https://docs.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver15 + Feature.grant, // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/commit-transaction-transact-sql?view=sql-server-ver15 + Feature.commit, // special sql-server features // https://docs.microsoft.com/en-us/sql/relational-databases/xml/for-xml-sql-server?view=sql-server-ver15 Feature.selectForXmlPath, Feature.use, Feature.allowSquareBracketQuotation, // 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 b9f22aada4..91926be1a0 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 @@ -239,8 +239,12 @@ public Void visit(ParenthesedSelect selectBody, S context) { @Override public Void visit(Table table, S context) { - validateNameWithAlias(NamedObject.table, table.getFullyQualifiedName(), - ValidationUtil.getAlias(table.getAlias())); + if (table.isTableVariable()) { + validateFeature(Feature.tableVariable); + } else { + validateNameWithAlias(NamedObject.table, table.getFullyQualifiedName(), + ValidationUtil.getAlias(table.getAlias())); + } validateOptional(table.getPivot(), p -> p.accept(this, context)); validateOptional(table.getUnPivot(), up -> up.accept(this, context)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 860ab3821c..5bfe053dc3 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4637,7 +4637,7 @@ Insert Insert(): { insert.setOverwrite(true); insert.setTableKeyword(true); } | [ LOOKAHEAD(2) { insert.setTableKeyword(true); }] ) - ] table=Table() + ] table=TableReference() [ LOOKAHEAD(2) "(" partitions=Partitions() ")" ] [ LOOKAHEAD({ isAliasAhead() }) [ { useAs = true; } ] name=RelObjectName() { table.setAlias(new Alias(name,useAs)); }] @@ -5264,13 +5264,39 @@ Table Table() #TableName : } } +/** A table source or DML target; catalog-only DDL continues to use Table(). */ +Table TableReference() #void : +{ + Table table; +} +{ + ( table=TableVariable() | table=Table() ) + { return table; } +} + +Table TableVariable() #TableName : +{ + Token variable; + Table table; +} +{ + variable= + { + requireAccessSyntax(!variable.image.startsWith("@@"), + "A table variable must be a local variable"); + table = new Table(variable.image, false).withTableVariable(true); + linkAST(table, jjtThis); + return table; + } +} + Table TableWithAlias(): { Table table = null; Alias alias = null; } { - table=Table() + table=TableReference() [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); }] { return table; } } @@ -5278,12 +5304,10 @@ Table TableWithAlias(): Table TableWithAliasAndMysqlIndexHint(): { Table table = null; - Alias alias = null; MySQLIndexHint indexHint = null; } { - table=Table() - [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); } ] + table=TableWithAlias() [ LOOKAHEAD(2) indexHint=MySQLIndexHint() { table.setHint(indexHint); } ] { return table; } } @@ -7045,7 +7069,7 @@ FromItem FromItem() #FromItem: || (getToken(1).kind == K_LATERAL && getToken(2).kind != OPENING_BRACKET) }) fromItem=TableFunction() | - LOOKAHEAD(3) fromItem=Table() + LOOKAHEAD(3) fromItem=TableReference() | LOOKAHEAD({ isNestedSetOperationAhead() }) ( fromItem=ParenthesedSelect() diff --git a/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java b/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java new file mode 100644 index 0000000000..66d6a4a7de --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/schema/TableVariableTest.java @@ -0,0 +1,167 @@ +/*- + * #%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.schema; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.UserVariable; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TableVariableTest { + private Statement roundtrip(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(false)); + assertThat(statement).isNotInstanceOf(UnsupportedStatement.class); + StringBuilder text = new StringBuilder(); + statement.accept(new StatementDeParser(text), null); + assertThat(text.toString()).isEqualTo(statement.toString()); + assertThat(CCJSqlParserUtil.parse(text.toString()).toString()) + .isEqualTo(statement.toString()); + return statement; + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT columnName FROM @table", "SELECT v.* FROM @table AS v", + "SELECT v.id FROM @table v JOIN dbo.source s ON v.id = s.id", + "SELECT * FROM (SELECT id FROM @table) t", + "WITH t AS (SELECT id FROM @table) SELECT * FROM t", + "INSERT INTO @table VALUES (1)", "INSERT INTO @table (id) SELECT id FROM source", + "UPDATE @table SET id = 2", "UPDATE t SET id = 3 FROM @table AS t", + "DELETE FROM @table", "DELETE @table WHERE id = 1", + "DELETE t FROM @table AS t", + "MERGE INTO @table AS t USING source AS s ON t.id = s.id " + + "WHEN MATCHED THEN UPDATE SET t.id = s.id"}) + void supportsTableVariablesInQueriesAndDml(String sql) throws JSQLParserException { + roundtrip(sql); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.values())), sql)) + .isEmpty(); + } + + @Test + void retainsTypedReferencesInExistingDmlModels() throws JSQLParserException { + Table from = (Table) ((PlainSelect) roundtrip("SELECT id FROM @t AS v")).getFromItem(); + Table inserted = ((Insert) roundtrip("INSERT INTO @t VALUES (1)")).getTable(); + Table updated = ((Update) roundtrip("UPDATE @t SET id = 2")).getTable(); + Table deleted = ((Delete) roundtrip("DELETE FROM @t")).getTable(); + for (Table table : List.of(from, inserted, updated, deleted)) { + assertThat(table.isTableVariable()).isTrue(); + assertThat(table.getName()).isEqualTo("@t"); + assertThat(table.getSchemaName()).isNull(); + assertThat(table.getASTNode()).isNotNull(); + Table copy = table.clone(); + assertThat(copy.isTableVariable()).isTrue(); + assertThat(copy.getName()).isEqualTo("@t"); + table.setUnsetCatalogAndSchema("catalog", "schema"); + assertThat(table.getFullyQualifiedName()).isEqualTo("@t"); + } + assertThat(from.getAlias().getName()).isEqualTo("v"); + } + + @Test + void separatesLocalSourcesFromCatalogTables() throws JSQLParserException { + String sql = "SELECT v.id FROM @t v JOIN dbo.source s ON v.id = s.id"; + assertThat(TablesNamesFinder.findTables(sql)).containsExactly("dbo.source"); + assertThat(TablesNamesFinder.findTablesOrOtherSources(sql)) + .containsExactlyInAnyOrder("@t", "dbo.source"); + assertThat(TablesNamesFinder.findTables("INSERT INTO @t SELECT id FROM source")) + .containsExactly("source"); + assertThat(TablesNamesFinder.findTables("UPDATE @t SET id = 2")).isEmpty(); + assertThat(TablesNamesFinder.findTables("DELETE FROM @t")).isEmpty(); + } + + @Test + void skipsOnlyLocalTableMetadataLookups() { + List checkedNames = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + checkedNames.add(named.getFqn()); + return true; + }; + assertThat(Validation.validate(List.of(FeaturesAllowed.SELECT, metadata), + "SELECT 1 FROM @t CROSS JOIN dbo.source")).isEmpty(); + assertThat(checkedNames).containsExactly("dbo.source"); + checkedNames.clear(); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.values()), metadata), + "INSERT INTO @t VALUES (1)", "DELETE FROM @t")).isEmpty(); + assertThat(checkedNames).isEmpty(); + } + + @Test + void validatesTheDialectFeature() { + assertThat(Validation.validate(List.of(DatabaseType.SQLSERVER), "SELECT id FROM @t")) + .isEmpty(); + assertThat(Validation.validate(List.of(FeaturesAllowed.SELECT), "SELECT id FROM @t")) + .isEmpty(); + assertThat(Validation.validate(List.of(new FeaturesAllowed(Feature.select)), + "SELECT id FROM @t").toString()).contains("tableVariable not allowed"); + assertThat(Validation.validate(List.of(DatabaseType.POSTGRESQL), "SELECT id FROM @t") + .toString()).contains("tableVariable not supported"); + } + + @Test + void preservesScalarVariablesDeclarationsAndFollowingStatements() throws JSQLParserException { + PlainSelect select = (PlainSelect) roundtrip("SELECT @value FROM @t"); + assertThat(select.getSelectItem(0).getExpression()).isInstanceOf(UserVariable.class); + Statements statements = CCJSqlParserUtil.parseStatements( + "DECLARE @t TABLE (id INT); INSERT INTO @t VALUES (1); " + + "UPDATE @t SET id = 2; SELECT id FROM @t; DELETE FROM @t; SELECT 42;"); + assertThat(statements).hasSize(6); + assertThat(statements.get(5).toString()).isEqualTo("SELECT 42"); + assertThat(CCJSqlParserUtil.parseStatements(statements.toString())).hasSize(6); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM dbo.t", "SELECT * FROM #temp", + "SELECT * FROM \"@catalog_table\"", "UPDATE t USE INDEX (idx) SET id = 1", + "CREATE INDEX idx ON t (id)", "SELECT id INTO copy FROM source"}) + void preservesCatalogTableGrammar(String sql) throws JSQLParserException { + Statement statement = roundtrip(sql); + if (statement instanceof PlainSelect) { + assertThat(((Table) ((PlainSelect) statement).getFromItem()).isTableVariable()) + .isFalse(); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * FROM @@global", "SELECT * FROM dbo.@t", + "SELECT id INTO @t FROM source", + "CREATE INDEX idx ON @t (id)", "ALTER TABLE @t ADD value INT", + "DROP TABLE @t", "TRUNCATE TABLE @t", "SELECT @t.* FROM @t"}) + void rejectsCatalogOnlyAndMalformedReferences(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, p -> p.withUnsupportedStatements(false))); + } + + @Test + void doesNotAddCreateTableVariableSyntax() throws JSQLParserException { + // CREATE's existing fallback returns UnsupportedStatement even in strict mode. + assertThat(CCJSqlParserUtil.parse("CREATE TABLE @t (id INT)", + p -> p.withUnsupportedStatements(false))).isInstanceOf(UnsupportedStatement.class); + } +}