diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 0125913e8..5211b05d2 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -46,7 +46,7 @@ public enum Dialect { AdjacentStringLiterals.WHITESPACE, Feature.allowDoubleQuotedStrings, Feature.allowBackslashEscapeCharacter), SNOWFLAKE( - Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS; + Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB; private final Set lexerFeatures; private final AdjacentStringLiterals adjacentStringLiterals; diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 554ab1d24..0ece5d171 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -397,16 +397,9 @@ public T visit(CreateView createView, S context) { public T visit(Alter alter, S context) { alter.getTable().accept(fromItemVisitor, context); for (AlterExpression action : alter.getAlterExpressions()) { - if (action.getColDataTypeList() != null) { - action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column, - expression -> expression.accept(expressionVisitor, context), - table -> table.accept(fromItemVisitor, context))); - } - if (action.getIndex() != null) { - TableDefinitionTraversal.visit(action.getIndex(), - expression -> expression.accept(expressionVisitor, context), - table -> table.accept(fromItemVisitor, context)); - } + TableDefinitionTraversal.visit(action, + expression -> expression.accept(expressionVisitor, context), + table -> table.accept(fromItemVisitor, context)); } return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java new file mode 100644 index 000000000..7121fd3cd --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionPrimaryKey.java @@ -0,0 +1,99 @@ +/*- + * #%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 java.util.Iterator; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.create.table.Index; + +/** + * CockroachDB's ALTER PRIMARY KEY USING COLUMNS operation. Key elements and storage options are + * available through {@link #getIndex()}; hash sharding and the legacy WITH BUCKET_COUNT expression + * are represented separately. + */ +public class AlterExpressionPrimaryKey extends AlterExpression { + private boolean usingHash; + private Expression bucketCount; + + public AlterExpressionPrimaryKey() { + setOperation(AlterOperation.ALTER_PRIMARY_KEY); + setIndex(new Index().withType("PRIMARY KEY")); + } + + public boolean isUsingHash() { + return usingHash; + } + + public void setUsingHash(boolean usingHash) { + this.usingHash = usingHash; + } + + public Expression getBucketCount() { + return bucketCount; + } + + public void setBucketCount(Expression bucketCount) { + this.bucketCount = bucketCount; + } + + @Override + protected void appendBody(StringBuilder builder) { + appendDefinition(builder, expression -> builder.append(expression)); + } + + /** Shares statement rendering while preserving expression visitor customization. */ + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + appendDefinition(builder, expressionPrinter); + appendCommonTail(builder); + return builder; + } + + private void appendDefinition(StringBuilder builder, Consumer expressionPrinter) { + builder.append("ALTER PRIMARY KEY USING COLUMNS ("); + if (getIndex().getColumns() != null) { + for (Iterator columns = getIndex().getColumns().iterator(); columns + .hasNext();) { + columns.next().appendTo(builder, expressionPrinter); + if (columns.hasNext()) { + builder.append(", "); + } + } + } + builder.append(')'); + appendSharding(builder, expressionPrinter); + appendStorageOptions(builder, expressionPrinter); + } + + private void appendSharding(StringBuilder builder, Consumer expressionPrinter) { + if (usingHash) { + builder.append(" USING HASH"); + if (bucketCount != null) { + builder.append(" WITH BUCKET_COUNT = "); + expressionPrinter.accept(bucketCount); + } + } + } + + private void appendStorageOptions(StringBuilder builder, + Consumer expressionPrinter) { + if (getIndex().getStorageParameters() != null) { + builder.append(" WITH ("); + for (Iterator options = + getIndex().getStorageParameters().iterator(); options.hasNext();) { + options.next().appendTo(builder, expressionPrinter); + if (options.hasNext()) { + builder.append(", "); + } + } + builder.append(')'); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index 9f7da8a8c..52bdc23ce 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 25cc9696a..aabd31ee3 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.LikeClause; import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.table.CheckConstraint; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; @@ -40,6 +41,23 @@ public static void visit(CreateIndex createIndex, Consumer expressio accept(createIndex.getWhere(), expressions); } + /** Visits the structured definitions and expressions belonging to a single ALTER action. */ + public static void visit(AlterExpression action, Consumer expressions, + Consumer tables) { + if (action.getColDataTypeList() != null) { + action.getColDataTypeList().forEach(column -> visit(column, expressions, tables)); + } + if (action.getIndex() != null) { + visit(action.getIndex(), expressions, tables); + } + if (action instanceof AlterExpressionPrimaryKey) { + AlterExpressionPrimaryKey primaryKey = (AlterExpressionPrimaryKey) action; + if (primaryKey.isUsingHash()) { + accept(primaryKey.getBucketCount(), expressions); + } + } + } + public static void visit(CreateTable table, Consumer expressions, Consumer
tables) { if (table.getTableElements() != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 2791915e9..5c6cbdbfe 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1660,16 +1660,9 @@ public void visit(CreateView createView) { public Void visit(Alter alter, S context) { for (net.sf.jsqlparser.statement.alter.AlterExpression action : alter .getAlterExpressions()) { - if (action.getColDataTypeList() != null) { - action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column, - expression -> expression.accept(this, context), - table -> visit(table, context))); - } - if (action.getIndex() != null) { - TableDefinitionTraversal.visit(action.getIndex(), - expression -> expression.accept(this, context), - table -> visit(table, context)); - } + TableDefinitionTraversal.visit(action, + expression -> expression.accept(this, context), + table -> visit(table, context)); } return alter.getTable().accept(this, context); } 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 2cd0507cb..6922525d9 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -12,6 +12,7 @@ import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.statement.select.PlainSelect; import java.util.Iterator; @@ -48,6 +49,11 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action instanceof AlterExpressionPrimaryKey) { + ((AlterExpressionPrimaryKey) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action.getIndex() instanceof DefaultConstraint) { builder.append(action.getOperation()).append(' '); new TableElementDeParser(builder, expressionVisitor).deParse(action.getIndex()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6edc965ac..d94936b53 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -15305,6 +15305,57 @@ AlterExpression AlterExpressionAddAlterModify(): { return alterExp; } } +/** Parses CockroachDB primary-key elements without legacy raw index parameters. */ +Index.ColumnParams CockroachPrimaryKeyColumn(): +{ + Index.ColumnParams column; + String name; + Expression expression; + Token token; +} +{ + ( + LOOKAHEAD({ isFunctionAhead() }) expression=Function() + { column = new Index.ColumnParams(expression).withExpressionParenthesized(false); } + | + name=RelObjectName() { column = new Index.ColumnParams(name); } + | + "(" expression=Expression() ")" { column = new Index.ColumnParams(expression); } + ) + [ (token= | token=) { column.setOperatorClass(token.image); } ] + [ (token= | token=) { + column.setSortOrder(Index.ColumnParams.SortOrder.valueOf(token.image.toUpperCase(Locale.ROOT))); + } ] + [ (token= | token=) { + column.setNullOrdering(Index.ColumnParams.NullOrdering.valueOf(token.image.toUpperCase(Locale.ROOT))); + } ] + { return column; } +} + +AlterExpressionPrimaryKey CockroachAlterPrimaryKey(): +{ + AlterExpressionPrimaryKey action = new AlterExpressionPrimaryKey(); + List columns = new ArrayList(); + Index.ColumnParams column; + List options; + Expression bucketCount; + Token token; +} +{ + + "(" column=CockroachPrimaryKeyColumn() { columns.add(column); } + ( "," column=CockroachPrimaryKeyColumn() { columns.add(column); } )* ")" + { action.getIndex().setColumns(columns); } + [ { action.setUsingHash(true); } + [ LOOKAHEAD( ) token= { + requireDdlSyntax("BUCKET_COUNT".equalsIgnoreCase(token.image), "Expected BUCKET_COUNT"); + } + "=" bucketCount=Expression() { action.setBucketCount(bucketCount); } ] + ] + [ options=PostgreSqlIndexOptions() { action.getIndex().setStorageParameters(options); } ] + { return action; } +} + /** * Parses all RENAME variants within ALTER TABLE. * Handles: RENAME [COLUMN] old TO new, RENAME [TO|AS] tablename, RENAME tablename, @@ -15417,6 +15468,10 @@ AlterExpression AlterExpression(): { ( + LOOKAHEAD( , + { Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) }) + alterExp = CockroachAlterPrimaryKey() + | alterExp = AlterExpressionAddAlterModify() | ( diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index dd076b031..1b7a94661 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -736,6 +736,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ - GoogleSQL ``CREATE [UNIQUE] NULL_FILTERED INDEX`` with a separate null-filtering flag * - ``DORIS`` - ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints + * - ``COCKROACHDB`` + - ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters Features set explicitly *after* the preset win over it. @@ -750,6 +752,12 @@ Doris distribution hints require ``parser.withDialect(Dialect.DORIS)``. the existing SQL Server hints use ``Position.BEFORE_JOIN``. Rendering preserves both the position and the brackets around a Doris hint. +CockroachDB primary-key changes require ``parser.withDialect(Dialect.COCKROACHDB)``. +Their action is an ``AlterExpressionPrimaryKey`` with key elements and storage +parameters in ``getIndex()``. ``isUsingHash()`` preserves ``USING HASH``, while +``getBucketCount()`` holds the legacy ``WITH BUCKET_COUNT = expression`` value. +The newer ``WITH (bucket_count = expression)`` form uses the index storage parameters. + With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and ``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()`` for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java new file mode 100644 index 000000000..580c5febe --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/CockroachAlterPrimaryKeyTest.java @@ -0,0 +1,186 @@ +/*- + * #%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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +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.ValueSource; + +class CockroachAlterPrimaryKeyTest { + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + } + + private static AlterExpressionPrimaryKey primaryKey(Alter alter) { + return (AlterExpressionPrimaryKey) alter.getAlterExpressions().get(0); + } + + @Test + void parsesOriginalReproducerIssue1743() throws Exception { + String sql = "ALTER TABLE FEATURE_SWITCH_CONFIG ALTER PRIMARY KEY " + + "USING COLUMNS (FEATURE_NAME) USING HASH"; + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + AlterExpressionPrimaryKey action = primaryKey(alter); + assertEquals(AlterOperation.ALTER_PRIMARY_KEY, action.getOperation()); + assertEquals(Index.Kind.PRIMARY_KEY, action.getIndex().getKind()); + assertEquals(List.of("FEATURE_NAME"), action.getIndex().getColumnsNames()); + assertTrue(action.isUsingHash()); + assertNull(action.getBucketCount()); + assertEquals(Set.of("FEATURE_SWITCH_CONFIG"), new TablesNamesFinder<>().getTables(alter)); + } + + @ParameterizedTest + @ValueSource(strings = {"", " USING HASH", " USING HASH WITH BUCKET_COUNT = 8", + " USING HASH WITH (bucket_count = 8)", " WITH (fillfactor = 70)"}) + void preservesKeyOrderOptionsAndRoundTrips(String suffix) throws Exception { + String sql = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (tenant_id ASC, \"key\" DESC)" + + suffix; + Alter alter = (Alter) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false, + parser -> parser.withDialect(Dialect.COCKROACHDB)); + StringBuilder output = new StringBuilder(); + alter.accept(new StatementDeParser(output), null); + for (String rendered : List.of(alter.toString(), output.toString())) { + AlterExpressionPrimaryKey action = primaryKey(parse(rendered)); + assertEquals(List.of("tenant_id ASC", "\"key\" DESC"), + action.getIndex().getColumnsNames()); + assertEquals(suffix.contains("USING HASH"), action.isUsingHash()); + if (suffix.contains("BUCKET_COUNT")) { + assertEquals(8, ((LongValue) action.getBucketCount()).getValue()); + } else { + assertNull(action.getBucketCount()); + } + if (suffix.contains("WITH (")) { + Index.Option option = action.getIndex().getStorageParameters().get(0); + assertEquals(suffix.contains("bucket_count") ? "bucket_count" : "fillfactor", + option.getName()); + assertEquals(suffix.contains("bucket_count") ? 8 : 70, + ((LongValue) option.getValue()).getValue()); + } + } + } + + @Test + void exposesStructuredExpressionKeysAndSupportsMutation() throws Exception { + Alter alter = parse("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS " + + "(lower(name) text_ops DESC, (id + 1) ASC)"); + AlterExpressionPrimaryKey action = primaryKey(alter); + Index.ColumnParams first = action.getIndex().getColumns().get(0); + assertTrue(first.getExpression() instanceof Function); + assertFalse(first.isExpressionParenthesized()); + assertEquals("text_ops", first.getOperatorClass()); + assertNull(first.getParams()); + first.setSortOrder(Index.ColumnParams.SortOrder.ASC); + action.setUsingHash(true); + action.setBucketCount(new LongValue(16)); + String expected = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS " + + "(lower(name) text_ops ASC, (id + 1) ASC) USING HASH WITH BUCKET_COUNT = 16"; + TestUtils.assertDeparse(alter, expected); + assertEquals(expected, parse(expected).toString()); + } + + @Test + void requiresCockroachDialectAndKeepsExistingAlterForms() throws Exception { + String sql = "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + for (Dialect dialect : Dialect.values()) { + if (dialect != Dialect.COCKROACHDB) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)), + dialect.name()); + } + } + for (String existing : List.of("ALTER TABLE t ADD PRIMARY KEY (id)", + "ALTER TABLE t ALTER COLUMN id TYPE BIGINT", "ALTER TABLE t DROP PRIMARY KEY")) { + String originalOutput = CCJSqlParserUtil.parse(existing).toString(); + assertEquals(existing, originalOutput.trim()); + TestUtils.assertStatementCanBeDeparsedAs(parse(existing), originalOutput); + } + } + + @Test + void visitsAndRewritesHashAndStorageExpressions() throws Exception { + Alter alter = parse("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) " + + "USING HASH WITH BUCKET_COUNT = 4 WITH (fillfactor = 70)"); + List values = new ArrayList<>(); + ExpressionVisitorAdapter visitor = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + values.add(value.getValue()); + return null; + } + }; + alter.accept(new StatementVisitorAdapter(new SelectVisitorAdapter(visitor)), + null); + assertEquals(2, values.size()); + assertEquals(Set.of(4L, 70L), Set.copyOf(values)); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 1); + } + }; + expressions.setBuilder(output); + new AlterDeParser(output, expressions).deParse(alter); + assertEquals("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) " + + "USING HASH WITH BUCKET_COUNT = 5 WITH (fillfactor = 71)", output.toString()); + assertEquals(output.toString(), parse(output.toString()).toString()); + } + + @Test + void keepsFollowingActionsAndStatements() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements("ALTER TABLE t ALTER PRIMARY KEY " + + "USING COLUMNS (id) USING HASH WITH BUCKET_COUNT = 4, ADD COLUMN note INT; SELECT 1;", + parser -> parser.withDialect(Dialect.COCKROACHDB)); + assertEquals(2, statements.size()); + Alter alter = (Alter) statements.get(0); + assertEquals(2, alter.getAlterExpressions().size()); + assertEquals(AlterOperation.ADD, alter.getAlterExpressions().get(1).getOperation()); + assertEquals(alter.toString(), parse(alter.toString()).toString()); + } + + @Test + void rejectsIncompleteKeysAndHashOptions() { + for (String sql : List.of("ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS ()", + "ALTER TABLE t ALTER PRIMARY KEY USING (id)", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id,)", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING BTREE", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH WITH BUCKET_COUNT =", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) WITH BUCKET_COUNT = 8", + "ALTER TABLE t ALTER PRIMARY KEY USING COLUMNS (id) USING HASH WITH wrong = 8")) { + assertThrows(JSQLParserException.class, () -> parse(sql), sql); + } + } +}