Skip to content

Commit 707a3b2

Browse files
committed
Support CockroachDB ALTER PRIMARY KEY with structured hash options
1 parent 329ee6b commit 707a3b2

11 files changed

Lines changed: 396 additions & 24 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;
49+
Feature.allowBackslashEscapeCharacter), INFORMIX, 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
@@ -392,16 +392,9 @@ public <S> T visit(CreateView createView, S context) {
392392
public <S> T visit(Alter alter, S context) {
393393
alter.getTable().accept(fromItemVisitor, context);
394394
for (AlterExpression action : alter.getAlterExpressions()) {
395-
if (action.getColDataTypeList() != null) {
396-
action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column,
397-
expression -> expression.accept(expressionVisitor, context),
398-
table -> table.accept(fromItemVisitor, context)));
399-
}
400-
if (action.getIndex() != null) {
401-
TableDefinitionTraversal.visit(action.getIndex(),
402-
expression -> expression.accept(expressionVisitor, context),
403-
table -> table.accept(fromItemVisitor, context));
404-
}
395+
TableDefinitionTraversal.visit(action,
396+
expression -> expression.accept(expressionVisitor, context),
397+
table -> table.accept(fromItemVisitor, context));
405398
}
406399
return null;
407400
}
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/statement/create/table/Index.java‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ private boolean hasParam(String expected) {
535535
}
536536
}
537537

538-
/** A named PostgreSQL index option with an optional value. */
538+
/** A named index storage option with an optional value. */
539539
public static class Option implements Serializable {
540540
private String name;
541541
private Expression value;
@@ -590,7 +590,21 @@ public Option withUseEquals(boolean useEquals) {
590590

591591
@Override
592592
public String toString() {
593-
return value == null ? name : name + (useEquals ? " = " : " ") + value;
593+
if (value == null) {
594+
return name;
595+
}
596+
StringBuilder builder = new StringBuilder();
597+
return appendTo(builder, expression -> builder.append(expression)).toString();
598+
}
599+
600+
public StringBuilder appendTo(StringBuilder builder,
601+
Consumer<Expression> expressionPrinter) {
602+
builder.append(name);
603+
if (value != null) {
604+
builder.append(useEquals ? " = " : " ");
605+
expressionPrinter.accept(value);
606+
}
607+
return builder;
594608
}
595609
}
596610
}

‎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.table.CheckConstraint;
1920
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
2021
import net.sf.jsqlparser.statement.create.table.ColumnOption;
@@ -29,6 +30,23 @@
2930
public final class TableDefinitionTraversal {
3031
private TableDefinitionTraversal() {}
3132

33+
/** Visits the structured definitions and expressions belonging to a single ALTER action. */
34+
public static void visit(AlterExpression action, Consumer<Expression> expressions,
35+
Consumer<Table> tables) {
36+
if (action.getColDataTypeList() != null) {
37+
action.getColDataTypeList().forEach(column -> visit(column, expressions, tables));
38+
}
39+
if (action.getIndex() != null) {
40+
visit(action.getIndex(), expressions, tables);
41+
}
42+
if (action instanceof AlterExpressionPrimaryKey) {
43+
AlterExpressionPrimaryKey primaryKey = (AlterExpressionPrimaryKey) action;
44+
if (primaryKey.isUsingHash()) {
45+
accept(primaryKey.getBucketCount(), expressions);
46+
}
47+
}
48+
}
49+
3250
public static void visit(CreateTable table, Consumer<Expression> expressions,
3351
Consumer<Table> tables) {
3452
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
@@ -1651,16 +1651,9 @@ public void visit(CreateView createView) {
16511651
public <S> Void visit(Alter alter, S context) {
16521652
for (net.sf.jsqlparser.statement.alter.AlterExpression action : alter
16531653
.getAlterExpressions()) {
1654-
if (action.getColDataTypeList() != null) {
1655-
action.getColDataTypeList().forEach(column -> TableDefinitionTraversal.visit(column,
1656-
expression -> expression.accept(this, context),
1657-
table -> visit(table, context)));
1658-
}
1659-
if (action.getIndex() != null) {
1660-
TableDefinitionTraversal.visit(action.getIndex(),
1661-
expression -> expression.accept(this, context),
1662-
table -> visit(table, context));
1663-
}
1654+
TableDefinitionTraversal.visit(action,
1655+
expression -> expression.accept(this, context),
1656+
table -> visit(table, context));
16641657
}
16651658
return alter.getTable().accept(this, context);
16661659
}

‎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
@@ -14999,6 +14999,57 @@ AlterExpression AlterExpressionAddAlterModify():
1499914999
{ return alterExp; }
1500015000
}
1500115001

15002+
/** Parses CockroachDB primary-key elements without legacy raw index parameters. */
15003+
Index.ColumnParams CockroachPrimaryKeyColumn():
15004+
{
15005+
Index.ColumnParams column;
15006+
String name;
15007+
Expression expression;
15008+
Token token;
15009+
}
15010+
{
15011+
(
15012+
LOOKAHEAD({ isFunctionAhead() }) expression=Function()
15013+
{ column = new Index.ColumnParams(expression).withExpressionParenthesized(false); }
15014+
|
15015+
name=RelObjectName() { column = new Index.ColumnParams(name); }
15016+
|
15017+
"(" expression=Expression() ")" { column = new Index.ColumnParams(expression); }
15018+
)
15019+
[ (token=<S_IDENTIFIER> | token=<S_QUOTED_IDENTIFIER>) { column.setOperatorClass(token.image); } ]
15020+
[ (token=<K_ASC> | token=<K_DESC>) {
15021+
column.setSortOrder(Index.ColumnParams.SortOrder.valueOf(token.image.toUpperCase(Locale.ROOT)));
15022+
} ]
15023+
[ <K_NULLS> (token=<K_FIRST> | token=<K_LAST>) {
15024+
column.setNullOrdering(Index.ColumnParams.NullOrdering.valueOf(token.image.toUpperCase(Locale.ROOT)));
15025+
} ]
15026+
{ return column; }
15027+
}
15028+
15029+
AlterExpressionPrimaryKey CockroachAlterPrimaryKey():
15030+
{
15031+
AlterExpressionPrimaryKey action = new AlterExpressionPrimaryKey();
15032+
List<Index.ColumnParams> columns = new ArrayList<Index.ColumnParams>();
15033+
Index.ColumnParams column;
15034+
List<Index.Option> options;
15035+
Expression bucketCount;
15036+
Token token;
15037+
}
15038+
{
15039+
<K_ALTER> <K_PRIMARY> <K_KEY> <K_USING> <K_COLUMNS>
15040+
"(" column=CockroachPrimaryKeyColumn() { columns.add(column); }
15041+
( "," column=CockroachPrimaryKeyColumn() { columns.add(column); } )* ")"
15042+
{ action.getIndex().setColumns(columns); }
15043+
[ <K_USING> <K_HASH> { action.setUsingHash(true); }
15044+
[ LOOKAHEAD(<K_WITH> <S_IDENTIFIER>) <K_WITH> token=<S_IDENTIFIER> {
15045+
requireDdlSyntax("BUCKET_COUNT".equalsIgnoreCase(token.image), "Expected BUCKET_COUNT");
15046+
}
15047+
"=" bucketCount=Expression() { action.setBucketCount(bucketCount); } ]
15048+
]
15049+
[ <K_WITH> options=PostgreSqlIndexOptions() { action.getIndex().setStorageParameters(options); } ]
15050+
{ return action; }
15051+
}
15052+
1500215053
/**
1500315054
* Parses all RENAME variants within ALTER TABLE.
1500415055
* Handles: RENAME [COLUMN] old TO new, RENAME [TO|AS] tablename, RENAME tablename,
@@ -15111,6 +15162,10 @@ AlterExpression AlterExpression():
1511115162
{
1511215163

1511315164
(
15165+
LOOKAHEAD(<K_ALTER> <K_PRIMARY> <K_KEY>,
15166+
{ Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) })
15167+
alterExp = CockroachAlterPrimaryKey()
15168+
|
1511415169
alterExp = AlterExpressionAddAlterModify()
1511515170
|
1511615171
(

‎src/site/sphinx/usage.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,17 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
713713
- ``withBackslashEscapeCharacter`` only, double quotes stay quoted identifiers
714714
* - ``INFORMIX``
715715
- Informix ``ALTER TABLE ... ADD CONSTRAINT`` definitions with optional trailing constraint names
716+
* - ``COCKROACHDB``
717+
- ``ALTER TABLE ... ALTER PRIMARY KEY USING COLUMNS (...)`` with optional hash sharding and storage parameters
716718

717719
Features set explicitly *after* the preset win over it.
718720

721+
CockroachDB primary-key changes require ``parser.withDialect(Dialect.COCKROACHDB)``.
722+
Their action is an ``AlterExpressionPrimaryKey`` with key elements and storage
723+
parameters in ``getIndex()``. ``isUsingHash()`` preserves ``USING HASH``, while
724+
``getBucketCount()`` holds the legacy ``WITH BUCKET_COUNT = expression`` value.
725+
The newer ``WITH (bucket_count = expression)`` form uses the index storage parameters.
726+
719727
Informix's constraint form requires an explicit dialect selection:
720728

721729
.. code-block:: java

0 commit comments

Comments
 (0)