diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java index c9276af84..0b09255e9 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterConstraintAttributes.java @@ -38,6 +38,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; 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 a6881a702..3d2c33273 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -482,8 +482,7 @@ public List getColumnSetDefaultList() { * existing action-specific API to add or remove entries. */ public List getColumnDefaults() { - if (operation != AlterOperation.ALTER || columnSetDefaultList == null - || constraintType != null && constraintSymbol != null + if (!usesColumnActionBody() || columnSetDefaultList == null || columnDropDefaultList != null && !columnDropDefaultList.isEmpty()) { return Collections.emptyList(); } @@ -1077,7 +1076,7 @@ protected void appendBody(StringBuilder b) { b.append(optionalSpecifier); } else if (hasExplicitConstraintHeader()) { toStringConstraintAlter(b); - } else if (hasAlterColumnValueActions()) { + } else if (usesColumnActionBody()) { toStringAlterColumn(b); } else if (isSimpleKeywordOperation()) { toStringSimpleKeyword(b); @@ -1101,6 +1100,32 @@ private boolean hasAlterColumnValueActions() { || columnSetVisibilityList != null && !columnSetVisibilityList.isEmpty()); } + /** + * Whether the selected body renders ALTER COLUMN default or visibility actions. Subclasses + * overriding {@link #appendBody(StringBuilder)} must reflect their dispatch here as well. + */ + protected boolean usesColumnActionBody() { + return hasAlterColumnValueActions() && !hasExplicitConstraintHeader(); + } + + /** + * Whether this action currently renders its column definitions through the general body. + * Inactive column metadata is retained in the AST but must not select a different body when + * deparsing. Subclasses with their own body dispatch override this method accordingly. + */ + public boolean hasActiveColumnDefinitions() { + return hasColumnDefinitionPayload() && operation != AlterOperation.UNSPECIFIC + && !hasExplicitConstraintHeader() && !usesColumnActionBody() + && !isSimpleKeywordOperation() && !isRenameOperation() && !isDropSpecialOperation() + && operation != AlterOperation.CONVERT && operation != AlterOperation.COLLATE + && !isPartitionOperation(); + } + + /** Whether column definitions take precedence within the general body. */ + protected boolean hasColumnDefinitionPayload() { + return commentText == null && columnName == null && colDataTypeList != null; + } + /** * Appends the common tail (parameters, index comment) shared by all ALTER expressions. */ diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java index 49511719f..2d1f3741b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionCharset.java @@ -25,6 +25,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java index 71d5d91f1..1fbb5a95e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionDrop.java @@ -24,6 +24,17 @@ public List getColumnIdentityActions() { return getColumnIdentityActionsFromDefinitions(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return hasColumnDefinitionPayload() && !isDropSpecialOperation() + && getOperation() != AlterOperation.DROP_PARTITION; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java index 9df2b1fb8..98e795923 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java @@ -35,6 +35,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; 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 ad97b1183..8297531fc 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPartition.java @@ -270,6 +270,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java index e8e170190..b89fd40d3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java @@ -39,6 +39,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java index 5c4176271..e296ac92f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionRename.java @@ -50,6 +50,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return getOperation() == AlterOperation.RENAME && hasColumnDefinitionPayload(); + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java index 811bc898a..5db554653 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java @@ -41,6 +41,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java index 53ded8ff5..03b79dc81 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterForeignDataOptions.java @@ -47,6 +47,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java index 58d52a635..2de16add3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/RelationAlterAction.java @@ -246,6 +246,16 @@ public List getColumnIdentityActions() { return Collections.emptyList(); } + @Override + protected boolean usesColumnActionBody() { + return false; + } + + @Override + public boolean hasActiveColumnDefinitions() { + return false; + } + @Override public boolean hasActiveTableDefinition() { return false; diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index 551c17eef..a08e4f11e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -89,7 +89,7 @@ private void deParseAction(AlterExpression action) { expression -> expression.accept(expressionVisitor, null)); return; } - if (action.getColDataTypeList() != null) { + if (action.hasActiveColumnDefinitions()) { action.appendColumnDefinitionsTo(builder, expression -> expression.accept(expressionVisitor, null)); } else { diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 498a9de48..7a3fc2b93 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -625,7 +625,18 @@ read-only, but its entries are the original mutable AST objects: returns their SQL text. Existing ``ColumnSetDefault`` constructors still accept opaque strings, for which ``getDefaultExpression()`` returns ``null``. Replacing an expression also replaces any opaque value. Unrelated actions -return an empty list. +return an empty list. This lookup follows the action's current rendering path: +changing an operation or retaining unused metadata does not make a fixed-body +action expose ``SET DEFAULT`` entries it will not render. + +``hasActiveColumnDefinitions()`` reports whether the action currently renders +its column definitions through the general ALTER body. ``getColDataTypeList()`` +continues to expose stored metadata even when another body takes precedence. +``AlterDeParser`` uses the active path, so expression visitors rewrite the +selected default or column expressions without rendering inactive metadata. +Custom ``AlterExpression`` subclasses overriding ``appendBody`` should also +override ``usesColumnActionBody()`` and ``hasActiveColumnDefinitions()`` when +they change those dispatch rules. Structured column attributes ============================ diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java new file mode 100644 index 000000000..26fb12c3c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRendererDispatchTest.java @@ -0,0 +1,258 @@ +/*- + * #%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.alter; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType; +import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetDefault; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +class AlterRendererDispatchTest { + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"MYSQL", "POSTGRESQL"}) + void parsedDefaultsRemainEditableAndUseTheExpressionVisitor(Dialect dialect) + throws JSQLParserException { + Alter statement = parse("ALTER TABLE t ALTER COLUMN amount SET DEFAULT (1 + 2)", dialect); + AlterExpression action = statement.getAlterExpressions().get(0); + addInactiveColumn(action); + ColumnDefaultAction defaultAction = action.getColumnDefaults().get(0); + assertSame(action.getColumnSetDefaultList().get(0), defaultAction); + defaultAction.setColumnName("renamed"); + defaultAction.setDefaultExpression(CCJSqlParserUtil.parseExpression("(3 + 4)")); + assertFalse(action.hasActiveColumnDefinitions()); + String expected = "ALTER TABLE t ALTER COLUMN renamed SET DEFAULT (3 + 4)"; + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + assertEquals(expected, parse(expected, dialect).toString()); + + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN renamed SET DEFAULT (103 + 104)", + deparse(statement, visited)); + assertEquals(List.of(3L, 4L), visited); + } + + @Test + void dropFallbackKeepsIdentityBodyInsteadOfInactiveDefaultMetadata() { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + ColumnDataType column = new ColumnDataType(false).withColumnName("id"); + column.setIdentityAlterations( + List.of(new IdentityAlteration(IdentityAlteration.Kind.RESTART))); + action.addColDataType(column); + action.addColSetDefault(ColumnSetDefault.fromExpression("other", new LongValue(1))); + Alter statement = statement(action); + assertTrue(action.hasActiveColumnDefinitions()); + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals("ALTER TABLE t ALTER COLUMN id RESTART", statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(statement.toString(), deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + @Test + void dropFallbackStillRewritesItsActiveColumnExpressionAndAppendsTailOnce() + throws JSQLParserException { + AlterExpressionDrop action = new AlterExpressionDrop(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + ColumnDataType column = new ColumnDataType(true).withColumnName("id") + .withColDataType(new ColDataType("INT")); + column.setUsingExpression(CCJSqlParserUtil.parseExpression("id + 2")); + action.addColDataType(column); + action.addColSetDefault(ColumnSetDefault.fromExpression("other", new LongValue(1))); + action.addParameters("CASCADE"); + Alter statement = statement(action); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN id TYPE INT USING id + 102 CASCADE", + deparse(statement, visited)); + assertEquals(List.of(2L), visited); + } + + static Stream fixedBodies() { + return Stream.of( + Arguments.of("ALTER TABLE t RENAME TO renamed", Dialect.MYSQL, + AlterExpressionRename.class), + Arguments.of("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4", Dialect.MYSQL, + AlterExpressionCharset.class), + Arguments.of("ALTER TABLE t ENGINE = InnoDB", Dialect.MYSQL, + AlterExpressionTableOption.class), + Arguments.of("ALTER TABLE t ALTER CONSTRAINT ck DEFERRABLE", Dialect.POSTGRESQL, + AlterConstraintAttributes.class), + Arguments.of("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id)", + Dialect.COCKROACHDB, AlterExpressionPrimaryKey.class), + Arguments.of("ALTER TABLE t DETACH PARTITION p", Dialect.POSTGRESQL, + AlterExpressionPartition.class), + Arguments.of("ALTER TABLE t ORDER BY id", Dialect.MYSQL, + AlterExpressionOrderBy.class), + Arguments.of("ALTER TABLE t OWNER TO alice", Dialect.POSTGRESQL, + RelationAlterAction.class), + Arguments.of("ALTER FOREIGN TABLE t OPTIONS (ADD filename 'file')", + Dialect.POSTGRESQL, AlterForeignDataOptions.class)); + } + + @ParameterizedTest + @MethodSource("fixedBodies") + void specializedBodiesIgnoreUnrelatedDefaultAndColumnMetadata(String sql, Dialect dialect, + Class type) throws JSQLParserException { + Alter statement = parse(sql, dialect); + AlterExpression action = assertInstanceOf(type, statement.getAlterExpressions().get(0)); + String expected = statement.toString(); + action.addColSetDefault(ColumnSetDefault.fromExpression("inactive", new LongValue(99))); + addInactiveColumn(action); + assertTrue(action.getColumnDefaults().isEmpty()); + assertFalse(action.hasActiveColumnDefinitions()); + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(expected, deparse(statement, visited)); + assertTrue(visited.isEmpty()); + + // A changed operation does not replace a subclass's own body dispatch. + action.setOperation(AlterOperation.ALTER); + assertTrue(action.getColumnDefaults().isEmpty()); + assertFalse(action.hasActiveColumnDefinitions()); + assertRenderersAgree(statement); + assertEquals(statement.toString(), deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + static Stream prioritizedBodies() { + return Stream.of( + body(action -> action + .addColDropDefault(new AlterExpression.ColumnDropDefault("id"))), + body(action -> action.addColSetVisibility( + new AlterExpression.ColumnSetVisibility("id", true))), + body(action -> { + action.setConstraintType("CHECK"); + action.setConstraintSymbol("ck"); + action.setEnforced(true); + }), + body(action -> { + action.setOperation(AlterOperation.RENAME_TABLE); + action.setNewTableName("renamed"); + }), + body(action -> action.setOperation(AlterOperation.DROP_PRIMARY_KEY)), + body(action -> { + action.setOperation(AlterOperation.UNSPECIFIC); + action.setOptionalSpecifier("vendor_action"); + }), + body(action -> { + action.setOperation(AlterOperation.COMMENT); + action.setCommentText("'description'"); + })); + } + + private static Arguments body(Consumer configure) { + return Arguments.of(configure); + } + + @ParameterizedTest + @MethodSource("prioritizedBodies") + void baseBodyPrecedencePreventsInactiveColumnExpressionsFromBeingRendered( + Consumer configure) { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + configure.accept(action); + Alter statement = statement(action); + String expected = statement.toString(); + addInactiveColumn(action); + assertFalse(action.hasActiveColumnDefinitions()); + assertTrue(action.getColumnDefaults().isEmpty()); + assertEquals(expected, statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals(expected, deparse(statement, visited)); + assertTrue(visited.isEmpty()); + } + + @Test + void activeDefaultKeepsPriorityOverStaleRenameMetadata() { + AlterExpression action = new AlterExpression(); + action.setOperation(AlterOperation.ALTER); + action.hasColumn(true); + action.addColSetDefault(ColumnSetDefault.fromExpression("id", new LongValue(1))); + action.setOldIndex(new Index().withName("old_index")); + action.setIndex(new Index().withName("new_index")); + addInactiveColumn(action); + assertEquals(1, action.getColumnDefaults().size()); + assertFalse(action.hasActiveColumnDefinitions()); + Alter statement = statement(action); + assertEquals("ALTER TABLE t ALTER COLUMN id SET DEFAULT 1", statement.toString()); + assertRenderersAgree(statement); + List visited = new ArrayList<>(); + assertEquals("ALTER TABLE t ALTER COLUMN id SET DEFAULT 101", deparse(statement, visited)); + assertEquals(List.of(1L), visited); + } + + private static void addInactiveColumn(AlterExpression action) { + ColumnDataType column = new ColumnDataType(true).withColumnName("inactive") + .withColDataType(new ColDataType("INT")); + column.setUsingExpression(new LongValue(999)); + action.addColDataType(column); + } + + private static Alter parse(String sql, Dialect dialect) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + } + + private static Alter statement(AlterExpression action) { + Alter statement = new Alter(); + statement.setTable(new Table("t")); + statement.addAlterExpression(action); + return statement; + } + + private static void assertRenderersAgree(Alter statement) { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + } + + private static String deparse(Alter statement, List visited) { + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertNull(context); + visited.add(value.getValue()); + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), sql), null); + return sql.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index a1a5879ac..721a2d849 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -18,10 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; @@ -29,7 +32,12 @@ import net.sf.jsqlparser.statement.DescribeStatement; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.select.AllColumns; +import net.sf.jsqlparser.statement.select.Join; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -1065,30 +1073,31 @@ void testUpdateOrderByLimitSubquery() throws JSQLParserException { } @Test - void testManyDerivedNamesDoNotScaleQuadratically() throws JSQLParserException { - // Each aliased WITH item is remembered in otherItemNames and every plain table - // reference used to be matched against it with a linear ArrayList.contains, so a - // statement with n CTE aliases and n tables cost O(n^2) to walk. + void testManyDerivedNamesDoNotScaleQuadratically() { + // Every plain table reference used to be matched against the CTE aliases with + // ArrayList.contains, so n aliases and n tables cost O(n^2) to walk. final int n = 30000; - final StringBuilder sb = new StringBuilder("WITH "); + final PlainSelect statement = new PlainSelect() + .withSelectItems(new SelectItem<>(new AllColumns())) + .withFromItem(new Table("t0")); + final List> withItems = new ArrayList<>(n); + final List joins = new ArrayList<>(n - 1); for (int i = 0; i < n; i++) { + PlainSelect body = + new PlainSelect().withSelectItems(new SelectItem<>(new LongValue(1))); + withItems.add(new WithItem<>(new ParenthesedSelect().withSelect(body), + new Alias("a" + i))); if (i > 0) { - sb.append(", "); + joins.add(new Join().withSimple(true).withRightItem(new Table("t" + i))); } - sb.append("a").append(i).append(" AS (SELECT 1)"); - } - sb.append(" SELECT * FROM "); - for (int i = 0; i < n; i++) { - if (i > 0) { - sb.append(", "); - } - sb.append("t").append(i); } + statement.setWithItemsList(withItems); + statement.setJoins(joins); - // Parsing is linear and left outside the timed region; only the table walk is measured. - final Statement statement = CCJSqlParserUtil.parse(sb.toString()); + // Build the AST directly: this tests table traversal, and parsing a large SQL + // fixture can hit the parser's independent timeout before traversal even starts. final Set tables = assertTimeoutPreemptively(Duration.ofSeconds(2), - () -> new TablesNamesFinder<>().getTables(statement)); + () -> new TablesNamesFinder<>().getTables((Statement) statement)); assertEquals(n, tables.size()); }