Skip to content

Commit 7c6a9fc

Browse files
minleejaemj
andauthored
Support CockroachDB ALTER PRIMARY KEY with structured hash options (#2585)
Co-authored-by: mj <mj@mjui-MacBookPro.local>
1 parent 7cc8638 commit 7c6a9fc

10 files changed

Lines changed: 380 additions & 22 deletions

File tree

‎src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public enum Dialect {
4646
AdjacentStringLiterals.WHITESPACE,
4747
Feature.allowDoubleQuotedStrings,
4848
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
49-
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS;
49+
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB;
5050

5151
private final Set<Feature> lexerFeatures;
5252
private final AdjacentStringLiterals adjacentStringLiterals;

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -397,16 +397,9 @@ public <S> T visit(CreateView createView, S context) {
397397
public <S> T visit(Alter alter, S context) {
398398
alter.getTable().accept(fromItemVisitor, context);
399399
for (AlterExpression action : alter.getAlterExpressions()) {
400-
if (action.getColDataTypeList() != null) {
401-
action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column,
402-
expression -> expression.accept(expressionVisitor, context),
403-
table -> table.accept(fromItemVisitor, context)));
404-
}
405-
if (action.getIndex() != null) {
406-
TableDefinitionTraversal.visit(action.getIndex(),
407-
expression -> expression.accept(expressionVisitor, context),
408-
table -> table.accept(fromItemVisitor, context));
409-
}
400+
TableDefinitionTraversal.visit(action,
401+
expression -> expression.accept(expressionVisitor, context),
402+
table -> table.accept(fromItemVisitor, context));
410403
}
411404
return null;
412405
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.alter;
11+
12+
import java.util.Iterator;
13+
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.expression.Expression;
15+
import net.sf.jsqlparser.statement.create.table.Index;
16+
17+
/**
18+
* CockroachDB's ALTER PRIMARY KEY USING COLUMNS operation. Key elements and storage options are
19+
* available through {@link #getIndex()}; hash sharding and the legacy WITH BUCKET_COUNT expression
20+
* are represented separately.
21+
*/
22+
public class AlterExpressionPrimaryKey extends AlterExpression {
23+
private boolean usingHash;
24+
private Expression bucketCount;
25+
26+
public AlterExpressionPrimaryKey() {
27+
setOperation(AlterOperation.ALTER_PRIMARY_KEY);
28+
setIndex(new Index().withType("PRIMARY KEY"));
29+
}
30+
31+
public boolean isUsingHash() {
32+
return usingHash;
33+
}
34+
35+
public void setUsingHash(boolean usingHash) {
36+
this.usingHash = usingHash;
37+
}
38+
39+
public Expression getBucketCount() {
40+
return bucketCount;
41+
}
42+
43+
public void setBucketCount(Expression bucketCount) {
44+
this.bucketCount = bucketCount;
45+
}
46+
47+
@Override
48+
protected void appendBody(StringBuilder builder) {
49+
appendDefinition(builder, expression -> builder.append(expression));
50+
}
51+
52+
/** Shares statement rendering while preserving expression visitor customization. */
53+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
54+
appendDefinition(builder, expressionPrinter);
55+
appendCommonTail(builder);
56+
return builder;
57+
}
58+
59+
private void appendDefinition(StringBuilder builder, Consumer<Expression> expressionPrinter) {
60+
builder.append("ALTER PRIMARY KEY USING COLUMNS (");
61+
if (getIndex().getColumns() != null) {
62+
for (Iterator<Index.ColumnParams> columns = getIndex().getColumns().iterator(); columns
63+
.hasNext();) {
64+
columns.next().appendTo(builder, expressionPrinter);
65+
if (columns.hasNext()) {
66+
builder.append(", ");
67+
}
68+
}
69+
}
70+
builder.append(')');
71+
appendSharding(builder, expressionPrinter);
72+
appendStorageOptions(builder, expressionPrinter);
73+
}
74+
75+
private void appendSharding(StringBuilder builder, Consumer<Expression> expressionPrinter) {
76+
if (usingHash) {
77+
builder.append(" USING HASH");
78+
if (bucketCount != null) {
79+
builder.append(" WITH BUCKET_COUNT = ");
80+
expressionPrinter.accept(bucketCount);
81+
}
82+
}
83+
}
84+
85+
private void appendStorageOptions(StringBuilder builder,
86+
Consumer<Expression> expressionPrinter) {
87+
if (getIndex().getStorageParameters() != null) {
88+
builder.append(" WITH (");
89+
for (Iterator<Index.Option> options =
90+
getIndex().getStorageParameters().iterator(); options.hasNext();) {
91+
options.next().appendTo(builder, expressionPrinter);
92+
if (options.hasNext()) {
93+
builder.append(", ");
94+
}
95+
}
96+
builder.append(')');
97+
}
98+
}
99+
}

‎src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import java.util.Locale;
1313

1414
public enum AlterOperation {
15-
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;
15+
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;
1616

1717
public static AlterOperation from(String operation) {
1818
return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT));

‎src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import net.sf.jsqlparser.schema.Table;
1616
import net.sf.jsqlparser.statement.LikeClause;
1717
import net.sf.jsqlparser.statement.alter.AlterExpression;
18+
import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey;
1819
import net.sf.jsqlparser.statement.create.index.CreateIndex;
1920
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
2021
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
@@ -40,6 +41,23 @@ public static void visit(CreateIndex createIndex, Consumer<Expression> expressio
4041
accept(createIndex.getWhere(), expressions);
4142
}
4243

44+
/** Visits the structured definitions and expressions belonging to a single ALTER action. */
45+
public static void visit(AlterExpression action, Consumer<Expression> expressions,
46+
Consumer<Table> tables) {
47+
if (action.getColDataTypeList() != null) {
48+
action.getColDataTypeList().forEach(column -> visit(column, expressions, tables));
49+
}
50+
if (action.getIndex() != null) {
51+
visit(action.getIndex(), expressions, tables);
52+
}
53+
if (action instanceof AlterExpressionPrimaryKey) {
54+
AlterExpressionPrimaryKey primaryKey = (AlterExpressionPrimaryKey) action;
55+
if (primaryKey.isUsingHash()) {
56+
accept(primaryKey.getBucketCount(), expressions);
57+
}
58+
}
59+
}
60+
4361
public static void visit(CreateTable table, Consumer<Expression> expressions,
4462
Consumer<Table> tables) {
4563
if (table.getTableElements() != null) {

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,16 +1660,9 @@ public void visit(CreateView createView) {
16601660
public <S> Void visit(Alter alter, S context) {
16611661
for (net.sf.jsqlparser.statement.alter.AlterExpression action : alter
16621662
.getAlterExpressions()) {
1663-
if (action.getColDataTypeList() != null) {
1664-
action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column,
1665-
expression -> expression.accept(this, context),
1666-
table -> visit(table, context)));
1667-
}
1668-
if (action.getIndex() != null) {
1669-
TableDefinitionTraversal.visit(action.getIndex(),
1670-
expression -> expression.accept(this, context),
1671-
table -> visit(table, context));
1672-
}
1663+
TableDefinitionTraversal.visit(action,
1664+
expression -> expression.accept(this, context),
1665+
table -> visit(table, context));
16731666
}
16741667
return alter.getTable().accept(this, context);
16751668
}

‎src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.sf.jsqlparser.statement.alter.Alter;
1313
import net.sf.jsqlparser.expression.ExpressionVisitor;
1414
import net.sf.jsqlparser.statement.alter.AlterExpression;
15+
import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey;
1516
import net.sf.jsqlparser.statement.create.table.DefaultConstraint;
1617
import net.sf.jsqlparser.statement.select.PlainSelect;
1718
import java.util.Iterator;
@@ -48,6 +49,11 @@ public void deParse(Alter alter) {
4849
}
4950

5051
private void deParseAction(AlterExpression action) {
52+
if (action instanceof AlterExpressionPrimaryKey) {
53+
((AlterExpressionPrimaryKey) action).appendTo(builder,
54+
expression -> expression.accept(expressionVisitor, null));
55+
return;
56+
}
5157
if (action.getIndex() instanceof DefaultConstraint) {
5258
builder.append(action.getOperation()).append(' ');
5359
new TableElementDeParser(builder, expressionVisitor).deParse(action.getIndex());

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15305,6 +15305,57 @@ AlterExpression AlterExpressionAddAlterModify():
1530515305
{ return alterExp; }
1530615306
}
1530715307

15308+
/** Parses CockroachDB primary-key elements without legacy raw index parameters. */
15309+
Index.ColumnParams CockroachPrimaryKeyColumn():
15310+
{
15311+
Index.ColumnParams column;
15312+
String name;
15313+
Expression expression;
15314+
Token token;
15315+
}
15316+
{
15317+
(
15318+
LOOKAHEAD({ isFunctionAhead() }) expression=Function()
15319+
{ column = new Index.ColumnParams(expression).withExpressionParenthesized(false); }
15320+
|
15321+
name=RelObjectName() { column = new Index.ColumnParams(name); }
15322+
|
15323+
"(" expression=Expression() ")" { column = new Index.ColumnParams(expression); }
15324+
)
15325+
[ (token=<S_IDENTIFIER> | token=<S_QUOTED_IDENTIFIER>) { column.setOperatorClass(token.image); } ]
15326+
[ (token=<K_ASC> | token=<K_DESC>) {
15327+
column.setSortOrder(Index.ColumnParams.SortOrder.valueOf(token.image.toUpperCase(Locale.ROOT)));
15328+
} ]
15329+
[ <K_NULLS> (token=<K_FIRST> | token=<K_LAST>) {
15330+
column.setNullOrdering(Index.ColumnParams.NullOrdering.valueOf(token.image.toUpperCase(Locale.ROOT)));
15331+
} ]
15332+
{ return column; }
15333+
}
15334+
15335+
AlterExpressionPrimaryKey CockroachAlterPrimaryKey():
15336+
{
15337+
AlterExpressionPrimaryKey action = new AlterExpressionPrimaryKey();
15338+
List<Index.ColumnParams> columns = new ArrayList<Index.ColumnParams>();
15339+
Index.ColumnParams column;
15340+
List<Index.Option> options;
15341+
Expression bucketCount;
15342+
Token token;
15343+
}
15344+
{
15345+
<K_ALTER> <K_PRIMARY> <K_KEY> <K_USING> <K_COLUMNS>
15346+
"(" column=CockroachPrimaryKeyColumn() { columns.add(column); }
15347+
( "," column=CockroachPrimaryKeyColumn() { columns.add(column); } )* ")"
15348+
{ action.getIndex().setColumns(columns); }
15349+
[ <K_USING> <K_HASH> { action.setUsingHash(true); }
15350+
[ LOOKAHEAD(<K_WITH> <S_IDENTIFIER>) <K_WITH> token=<S_IDENTIFIER> {
15351+
requireDdlSyntax("BUCKET_COUNT".equalsIgnoreCase(token.image), "Expected BUCKET_COUNT");
15352+
}
15353+
"=" bucketCount=Expression() { action.setBucketCount(bucketCount); } ]
15354+
]
15355+
[ <K_WITH> options=PostgreSqlIndexOptions() { action.getIndex().setStorageParameters(options); } ]
15356+
{ return action; }
15357+
}
15358+
1530815359
/**
1530915360
* Parses all RENAME variants within ALTER TABLE.
1531015361
* Handles: RENAME [COLUMN] old TO new, RENAME [TO|AS] tablename, RENAME tablename,
@@ -15417,6 +15468,10 @@ AlterExpression AlterExpression():
1541715468
{
1541815469

1541915470
(
15471+
LOOKAHEAD(<K_ALTER> <K_PRIMARY> <K_KEY>,
15472+
{ Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) })
15473+
alterExp = CockroachAlterPrimaryKey()
15474+
|
1542015475
alterExp = AlterExpressionAddAlterModify()
1542115476
|
1542215477
(

‎src/site/sphinx/usage.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
736736
- GoogleSQL ``CREATE [UNIQUE] NULL_FILTERED INDEX`` with a separate null-filtering flag
737737
* - ``DORIS``
738738
- ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints
739+
* - ``COCKROACHDB``
740+
- ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters
739741

740742
Features set explicitly *after* the preset win over it.
741743

@@ -750,6 +752,12 @@ Doris distribution hints require ``parser.withDialect(Dialect.DORIS)``.
750752
the existing SQL Server hints use ``Position.BEFORE_JOIN``. Rendering preserves
751753
both the position and the brackets around a Doris hint.
752754

755+
CockroachDB primary-key changes require ``parser.withDialect(Dialect.COCKROACHDB)``.
756+
Their action is an ``AlterExpressionPrimaryKey`` with key elements and storage
757+
parameters in ``getIndex()``. ``isUsingHash()`` preserves ``USING HASH``, while
758+
``getBucketCount()`` holds the legacy ``WITH BUCKET_COUNT = expression`` value.
759+
The newer ``WITH (bucket_count = expression)`` form uses the index storage parameters.
760+
753761
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
754762
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
755763
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words

0 commit comments

Comments
 (0)