Skip to content

Commit 989f041

Browse files
committed
Support CREATE TABLE LIKE with INDEXES
patch by Maxwell Guo; reviewed by Stefan Miklosovic, Sam Tunnicliffe for CASSANDRA-19965
1 parent ffdc4b7 commit 989f041

File tree

13 files changed

+424
-173
lines changed

13 files changed

+424
-173
lines changed

CHANGES.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
5.1
2+
* Support CREATE TABLE LIKE WITH INDEXES (CASSANDRA-19965)
23
* Invalidate relevant prepared statements on every change to TableMetadata (CASSANDRA-20318)
34
* Add per type max size guardrails (CASSANDRA-19677)
45
* Make it possible to abort all kinds of multi step operations (CASSANDRA-20217)

doc/cql3/CQL.textile

+7-4
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,19 @@ bc(syntax)..
414414
<copy-table-stmt> ::= CREATE ( TABLE | COLUMNFAMILY ) ( IF NOT EXISTS )? <newtablename> LIKE <oldtablename>
415415
( WITH <option> ( AND <option>)* )?
416416

417-
<option> ::= <property>
417+
<option> ::= <property> | INDEXES
418418

419-
p.
419+
p.
420420
__Sample:__
421421

422422
bc(sample)..
423423
CREATE TABLE newtb1 LIKE oldtb;
424424

425425
CREATE TABLE newtb2 LIKE oldtb WITH compaction = { 'class' : 'LeveledCompactionStrategy' };
426-
p.
426+
427+
CREATE TABLE newtb4 LIKE oldtb WITH INDEXES AND compaction = { 'class' : 'LeveledCompactionStrategy' };
428+
429+
p.
427430
The @COPY TABLE@ statement creates a new table which is a clone of old table. The new table have the same column numbers, column names, column data types, column data mask with the old table. The new table is defined by a "name":#copyNewTableName, and the name of the old table being cloned is defined by a "name":#copyOldTableName . The table options of the new table can be defined by setting "copyoptions":#copyTableOptions. Note that the @CREATE COLUMNFAMILY LIKE@ syntax is supported as an alias for @CREATE TABLE like@ (for historical reasons).
428431

429432
Attempting to create an already existing table will return an error unless the @IF NOT EXISTS@ option is used. If it is used, the statement will be a no-op if the table already exists.
@@ -438,7 +441,7 @@ The old table name defines the already existed table.
438441

439442
h4(#copyTableOptions). @<copyoptions>@
440443

441-
The @COPY TABLE@ statement supports a number of options that controls the configuration of a new table. These options can be specified after the @WITH@ keyword, and all options are the same as those options when creating a table except for id .
444+
The @COPY TABLE@ statement supports a number of options that controls the configuration of a new table. These options can be specified after the @WITH@ keyword, and all options are the same as those options when creating a table except for id. Besides the options can also be specified with keyword INDEXES which means copy source table's indexes.
442445

443446
h3(#alterTableStmt). ALTER TABLE
444447

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
create_table_statement::= CREATE TABLE [ IF NOT EXISTS ] new_table_name LIKE old_table_name
22
[ WITH table_options ]
3-
table_options::= options [ AND table_options ]
3+
table_options::= INDEXES | options [ AND table_options ]

doc/modules/cassandra/examples/CQL/create_table_like.cql

+3
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ CREATE TABLE newtb3 LIKE oldtb WITH compaction = { 'class' : 'LeveledCompactionS
1212
AND compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32 }
1313
AND cdc = true;
1414

15+
CREATE TABLE newtb4 LIKE oldtb WITH INDEXES;
16+
17+
CREATE TABLE newtb6 LIKE oldtb WITH INDEXES AND compaction = { 'class' : 'LeveledCompactionStrategy' };

pylib/cqlshlib/cql3handling.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,9 @@ def dequote_value(cqlword):
387387
( ender="," [propmapkey]=<term> ":" [propmapval]=<term> )*
388388
ender="}"
389389
;
390+
<propertyOrOption> ::= <property>
391+
| "INDEXES"
392+
;
390393
391394
'''
392395

@@ -1314,7 +1317,7 @@ def create_cf_composite_primary_key_comma_completer(ctxt, cass):
13141317
<copyTableStatement> ::= "CREATE" wat=("COLUMNFAMILY" | "TABLE" ) ("IF" "NOT" "EXISTS")?
13151318
( tks=<nonSystemKeyspaceName> dot="." )? tcf=<cfOrKsName>
13161319
"LIKE" ( sks=<nonSystemKeyspaceName> dot="." )? scf=<cfOrKsName>
1317-
( "WITH" <property> ( "AND" <property> )* )?
1320+
( "WITH" <propertyOrOption> ( "AND" <propertyOrOption> )* )?
13181321
;
13191322
'''
13201323

pylib/cqlshlib/test/test_cqlsh_completion.py

+12-16
Original file line numberDiff line numberDiff line change
@@ -796,9 +796,9 @@ def test_complete_in_create_table_like(self):
796796
choices=['<new_table_name>'])
797797
self.trycompletions('CREATE TABLE ' + quoted_keyspace + '.new_table L',
798798
immediate='IKE ')
799-
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table W',
799+
self.trycompletions('CREATE TABLE new_table LIKE old_table W',
800800
immediate='ITH ')
801-
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH ',
801+
self.trycompletions('CREATE TABLE new_table LIKE old_table WITH ',
802802
choices=['allow_auto_snapshot',
803803
'bloom_filter_fp_chance', 'compaction',
804804
'compression',
@@ -808,23 +808,16 @@ def test_complete_in_create_table_like(self):
808808
'memtable',
809809
'memtable_flush_period_in_ms',
810810
'caching', 'comment',
811-
'min_index_interval', 'speculative_retry', 'additional_write_policy', 'cdc', 'read_repair'])
812-
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH ',
813-
choices=['allow_auto_snapshot',
814-
'bloom_filter_fp_chance', 'compaction',
815-
'compression',
816-
'default_time_to_live', 'gc_grace_seconds',
817-
'incremental_backups',
818-
'max_index_interval',
819-
'memtable',
820-
'memtable_flush_period_in_ms',
821-
'caching', 'comment',
822-
'min_index_interval', 'speculative_retry', 'additional_write_policy', 'cdc', 'read_repair'])
811+
'min_index_interval',
812+
'speculative_retry', 'additional_write_policy',
813+
'cdc', 'read_repair',
814+
'INDEXES'])
815+
self.trycompletions('CREATE TABLE new_table LIKE old_table WITH INDEXES ',
816+
choices=[';' , '=', 'AND'])
823817
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH bloom_filter_fp_chance ',
824818
immediate='= ')
825819
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH bloom_filter_fp_chance = ',
826820
choices=['<float_between_0_and_1>'])
827-
828821
self.trycompletions('CREATE TABLE ' + 'new_table LIKE old_table WITH compaction ',
829822
immediate="= {'class': '")
830823
self.trycompletions('CREATE TABLE ' + "new_table LIKE old_table WITH compaction = "
@@ -868,7 +861,10 @@ def test_complete_in_create_table_like(self):
868861
'memtable',
869862
'memtable_flush_period_in_ms',
870863
'caching', 'comment',
871-
'min_index_interval', 'speculative_retry', 'additional_write_policy', 'cdc', 'read_repair'])
864+
'min_index_interval',
865+
'speculative_retry', 'additional_write_policy',
866+
'cdc', 'read_repair',
867+
'INDEXES'])
872868
self.trycompletions('CREATE TABLE ' + "new_table LIKE old_table WITH compaction = "
873869
+ "{'class': 'TimeWindowCompactionStrategy', '",
874870
choices=['compaction_window_unit', 'compaction_window_size',

src/antlr/Lexer.g

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ K_TABLES: ( C O L U M N F A M I L I E S
9898
K_MATERIALIZED:M A T E R I A L I Z E D;
9999
K_VIEW: V I E W;
100100
K_INDEX: I N D E X;
101+
K_INDEXES: I N D E X E S;
101102
K_CUSTOM: C U S T O M;
102103
K_ON: O N;
103104
K_TO: T O;

src/antlr/Parser.g

+11-1
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,16 @@ copyTableStatement returns [CopyTableStatement.Raw stmt]
836836
: K_CREATE K_COLUMNFAMILY (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
837837
newCf=columnFamilyName K_LIKE oldCf=columnFamilyName
838838
{ $stmt = new CopyTableStatement.Raw(newCf, oldCf, ifNotExists); }
839-
( K_WITH property[stmt.attrs] ( K_AND property[stmt.attrs] )*)?
839+
( K_WITH propertyOrOption[stmt] ( K_AND propertyOrOption[stmt] )*)?
840+
;
841+
842+
propertyOrOption[CopyTableStatement.Raw stmt]
843+
: tableLikeSingleOption[stmt]
844+
| property[stmt.attrs]
845+
;
846+
847+
tableLikeSingleOption[CopyTableStatement.Raw stmt]
848+
: K_INDEXES {$stmt.withLikeOption(CopyTableStatement.CreateLikeOption.INDEXES);}
840849
;
841850

842851
/**
@@ -2084,5 +2093,6 @@ basic_unreserved_keyword returns [String str]
20842093
| K_ANN
20852094
| K_BETWEEN
20862095
| K_CHECK
2096+
| K_INDEXES
20872097
) { $str = $k.text; }
20882098
;

src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java

+68-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.apache.cassandra.cql3.statements.schema;
2020

2121
import java.nio.ByteBuffer;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Optional;
2325
import java.util.Set;
2426
import java.util.stream.Collectors;
@@ -35,6 +37,9 @@
3537
import org.apache.cassandra.db.marshal.UserType;
3638
import org.apache.cassandra.db.marshal.VectorType;
3739
import org.apache.cassandra.exceptions.AlreadyExistsException;
40+
import org.apache.cassandra.index.sai.StorageAttachedIndex;
41+
import org.apache.cassandra.schema.ColumnMetadata;
42+
import org.apache.cassandra.schema.IndexMetadata;
3843
import org.apache.cassandra.schema.Indexes;
3944
import org.apache.cassandra.schema.KeyspaceMetadata;
4045
import org.apache.cassandra.schema.Keyspaces;
@@ -46,6 +51,7 @@
4651
import org.apache.cassandra.schema.Triggers;
4752
import org.apache.cassandra.schema.UserFunctions;
4853
import org.apache.cassandra.service.ClientState;
54+
import org.apache.cassandra.service.ClientWarn;
4955
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
5056
import org.apache.cassandra.tcm.ClusterMetadata;
5157
import org.apache.cassandra.transport.Event.SchemaChange;
@@ -61,12 +67,14 @@ public final class CopyTableStatement extends AlterSchemaStatement
6167
private final String targetTableName;
6268
private final boolean ifNotExists;
6369
private final TableAttributes attrs;
70+
private final CreateLikeOption createLikeOption;
6471

6572
public CopyTableStatement(String sourceKeyspace,
6673
String targetKeyspace,
6774
String sourceTableName,
6875
String targetTableName,
6976
boolean ifNotExists,
77+
CreateLikeOption createLikeOption,
7078
TableAttributes attrs)
7179
{
7280
super(targetKeyspace);
@@ -75,6 +83,7 @@ public CopyTableStatement(String sourceKeyspace,
7583
this.sourceTableName = sourceTableName;
7684
this.targetTableName = targetTableName;
7785
this.ifNotExists = ifNotExists;
86+
this.createLikeOption = createLikeOption;
7887
this.attrs = attrs;
7988
}
8089

@@ -196,6 +205,7 @@ public Keyspaces apply(ClusterMetadata metadata)
196205

197206
TableParams originalParams = targetBuilder.build().params;
198207
TableParams newTableParams = attrs.asAlteredTableParams(originalParams);
208+
maybeCopyIndexes(targetBuilder, sourceTableMeta, targetKeyspaceMeta);
199209

200210
TableMetadata table = targetBuilder.params(newTableParams)
201211
.id(TableId.get(metadata))
@@ -229,12 +239,59 @@ public void validate(ClientState state)
229239
validateDefaultTimeToLive(attrs.asNewTableParams());
230240
}
231241

242+
private void maybeCopyIndexes(TableMetadata.Builder builder, TableMetadata sourceTableMeta, KeyspaceMetadata targetKeyspaceMeta)
243+
{
244+
if (createLikeOption != CreateLikeOption.INDEXES || sourceTableMeta.indexes.isEmpty())
245+
return;
246+
247+
Set<String> customIndexes = Sets.newTreeSet();
248+
List<IndexMetadata> indexesToCopy = new ArrayList<>();
249+
for (IndexMetadata indexMetadata : sourceTableMeta.indexes)
250+
{
251+
// only sai and legacy secondary index is supported
252+
if (indexMetadata.isCustom() && !StorageAttachedIndex.class.getCanonicalName().equals(indexMetadata.getIndexClassName()))
253+
{
254+
customIndexes.add(indexMetadata.name);
255+
continue;
256+
}
257+
258+
ColumnMetadata targetColumn = sourceTableMeta.getColumn(UTF8Type.instance.decompose(indexMetadata.options.get("target")));
259+
String indexName;
260+
// The rules for generating the index names of the target table are:
261+
// (1) If the source table's index names follow the pattern sourcetablename_columnname_idx_number, the index names are considered to be generated by the system,
262+
// then we directly replace the name of source table with the name of target table, and increment the number after idx to avoid index name conflicts.
263+
// (2) Index names that do not follow the above pattern are considered user-defined, so the index names are retained and increment the number after idx to avoid conflicts.
264+
if (indexMetadata.name.startsWith(sourceTableName + "_" + targetColumn.name + "_idx"))
265+
{
266+
String baseName = IndexMetadata.generateDefaultIndexName(targetTableName, targetColumn.name);
267+
indexName = targetKeyspaceMeta.findAvailableIndexName(baseName, indexesToCopy, targetKeyspaceMeta);
268+
}
269+
else
270+
{
271+
indexName = targetKeyspaceMeta.findAvailableIndexName(indexMetadata.name, indexesToCopy, targetKeyspaceMeta);
272+
}
273+
indexesToCopy.add(IndexMetadata.fromSchemaMetadata(indexName, indexMetadata.kind, indexMetadata.options));
274+
}
275+
276+
if (!indexesToCopy.isEmpty())
277+
builder.indexes(Indexes.builder().add(indexesToCopy).build());
278+
279+
if (!customIndexes.isEmpty())
280+
ClientWarn.instance.warn(String.format("Source table %s.%s to copy indexes from to %s.%s has custom indexes. These indexes were not copied: %s",
281+
sourceKeyspace,
282+
sourceTableName,
283+
targetKeyspace,
284+
targetTableName,
285+
customIndexes));
286+
}
287+
232288
public final static class Raw extends CQLStatement.Raw
233289
{
234290
private final QualifiedName oldName;
235291
private final QualifiedName newName;
236292
private final boolean ifNotExists;
237293
public final TableAttributes attrs = new TableAttributes();
294+
private CreateLikeOption createLikeOption = null;
238295

239296
public Raw(QualifiedName newName, QualifiedName oldName, boolean ifNotExists)
240297
{
@@ -248,7 +305,17 @@ public CQLStatement prepare(ClientState state)
248305
{
249306
String oldKeyspace = oldName.hasKeyspace() ? oldName.getKeyspace() : state.getKeyspace();
250307
String newKeyspace = newName.hasKeyspace() ? newName.getKeyspace() : state.getKeyspace();
251-
return new CopyTableStatement(oldKeyspace, newKeyspace, oldName.getName(), newName.getName(), ifNotExists, attrs);
308+
return new CopyTableStatement(oldKeyspace, newKeyspace, oldName.getName(), newName.getName(), ifNotExists, createLikeOption, attrs);
309+
}
310+
311+
public void withLikeOption(CreateLikeOption option)
312+
{
313+
this.createLikeOption = option;
252314
}
253315
}
316+
317+
public enum CreateLikeOption
318+
{
319+
INDEXES;
320+
}
254321
}

src/java/org/apache/cassandra/schema/KeyspaceMetadata.java

+36-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.apache.cassandra.schema;
1919

2020
import java.io.IOException;
21+
import java.util.Collection;
22+
import java.util.Collections;
2123
import java.util.HashSet;
2224
import java.util.Optional;
2325
import java.util.Set;
@@ -207,19 +209,49 @@ public Stream<TableMetadata> tablesUsingFunction(Function function)
207209

208210
public String findAvailableIndexName(String baseName)
209211
{
210-
if (!hasIndex(baseName))
212+
return findAvailableIndexName(baseName, Collections.emptySet(), this);
213+
}
214+
215+
/**
216+
* find an avaiable index name based on the indexes in target keyspace and indexes collections
217+
* @param baseName the base name of index
218+
* @param indexes find out whether there is any conflict with baseName in the indexes
219+
* @param keyspaceMetadata find out whether there is any conflict with baseName in keyspaceMetadata
220+
* */
221+
public String findAvailableIndexName(String baseName, Collection<IndexMetadata> indexes, KeyspaceMetadata keyspaceMetadata)
222+
{
223+
if (!hasIndex(baseName, indexes, keyspaceMetadata))
211224
return baseName;
212225

213-
int i = 1;
214226
do
215227
{
216-
String name = baseName + '_' + i++;
217-
if (!hasIndex(name))
228+
String name = generateIndexName(baseName);
229+
if (!hasIndex(name, indexes, keyspaceMetadata))
218230
return name;
231+
baseName = name;
219232
}
220233
while (true);
221234
}
222235

236+
private String generateIndexName(String baseName)
237+
{
238+
if (baseName.matches(".*_\\d+$"))
239+
{
240+
int lastUnderscoreIndex = baseName.lastIndexOf('_');
241+
String numberStr = baseName.substring(lastUnderscoreIndex + 1);
242+
int number = Integer.parseInt(numberStr) + 1;
243+
return baseName.substring(0, lastUnderscoreIndex + 1) + number;
244+
}
245+
246+
return baseName + "_1";
247+
}
248+
249+
private boolean hasIndex(String baseName, Collection<IndexMetadata> indexes, KeyspaceMetadata keyspaceMetadata)
250+
{
251+
return any(indexes, t -> t.name.equals(baseName)) ||
252+
any(keyspaceMetadata.tables, t -> t.indexes.has(baseName));
253+
}
254+
223255
public Optional<TableMetadata> findIndexedTable(String indexName)
224256
{
225257
for (TableMetadata table : tablesAndViews())

test/unit/org/apache/cassandra/cql3/AlterSchemaStatementTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public class AlterSchemaStatementTest extends CQLTester
4040
"CREATE TABLE ks.t1 (k int PRIMARY KEY)",
4141
"ALTER MATERIALIZED VIEW ks.v1 WITH compaction = { 'class' : 'LeveledCompactionStrategy' }",
4242
"ALTER TABLE ks.t1 ADD v int",
43-
"CREATE TABLE ks.tb like ks1.tb"
43+
"CREATE TABLE ks.tb like ks1.tb",
44+
"CREATE TABLE ks.tb like ks1.tb WITH indexes"
4445
};
4546
private final ClientState clientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 1234));
4647

0 commit comments

Comments
 (0)