Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CASSANDRA-19664 first approach - for copying Basic Table definition #3718

Open
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/antlr/Parser.g
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ cqlStatement returns [CQLStatement.Raw stmt]
| st42=addIdentityStatement { $stmt = st42; }
| st43=dropIdentityStatement { $stmt = st43; }
| st44=listSuperUsersStatement { $stmt = st44; }
| st45=copyTableStatement { $stmt = st45; }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason i used a new statement here is that :
1、The original createtablestatment code is too bloated. If the semantics of like are added, the code needs to be changed a lot.
2、In addition, I think this new copystatement can do things other than table cloning, and can also support semantic capabilities such as “CREATE TABLE AS”. Because they are similar to table copying functions, they are called copystatement here.

Besides, I have also prepared a patch, which is mainly to modify the pares of create table, that is, to implement the function of create table like in create table statement. If you think it is necessary, I can create a new PR to replace the current one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another pr here that implement CASSANDRA-19964, but use create table's parase path .
I am not sure which one is better , so I attach both of them.

;

/*
Expand Down Expand Up @@ -813,6 +814,17 @@ tableClusteringOrder[CreateTableStatement.Raw stmt]
: k=ident (K_ASC | K_DESC { ascending = false; } ) { $stmt.extendClusteringOrder(k, ascending); }
;

/**
* CREATE TABLE [IF NOT EXISTS] <NEW_TABLE> LIKE <OLD_TABLE> WITH <property> = <value> AND ...;
*/
copyTableStatement returns [CopyTableStatement.Raw stmt]
@init { boolean ifNotExists = false; }
: K_CREATE K_COLUMNFAMILY (K_IF K_NOT K_EXISTS { ifNotExists = true; } )?
newCf=columnFamilyName K_LIKE oldCf=columnFamilyName
{ $stmt = new CopyTableStatement.Raw(newCf, oldCf, ifNotExists); }
( K_WITH property[stmt.attrs] ( K_AND property[stmt.attrs] )*)?
;

/**
* CREATE TYPE foo (
* <name1> <type1>,
Expand Down
1 change: 1 addition & 0 deletions src/java/org/apache/cassandra/audit/AuditLogEntryType.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum AuditLogEntryType
ALTER_KEYSPACE(AuditLogEntryCategory.DDL),
DROP_KEYSPACE(AuditLogEntryCategory.DDL),
CREATE_TABLE(AuditLogEntryCategory.DDL),
CREATE_TABLE_LIKE(AuditLogEntryCategory.DDL),
DROP_TABLE(AuditLogEntryCategory.DDL),
PREPARE_STATEMENT(AuditLogEntryCategory.PREPARE),
DROP_TRIGGER(AuditLogEntryCategory.DDL),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.cassandra.cql3.statements.schema;

import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QualifiedName;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.exceptions.AlreadyExistsException;
import org.apache.cassandra.schema.Indexes;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Keyspaces;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableParams;
import org.apache.cassandra.schema.Triggers;
import org.apache.cassandra.schema.UserFunctions;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Event.SchemaChange;

/**
* {@code CREATE TABLE [IF NOT EXISTS] <newtable> LIKE <oldtable> WITH <property> = <value>}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blerer this is a brief introduction of cql to CopyTableStatement, and I have already create CASSANDRA-20120 to create CEP-43's doc

*/
public final class CopyTableStatement extends AlterSchemaStatement
Maxwell-Guo marked this conversation as resolved.
Show resolved Hide resolved
{
private final String sourceKeyspace;
private final String sourceTableName;
private final String targetKeyspace;
private final String targetTableName;
private final boolean ifNotExists;
private final TableAttributes attrs;

public CopyTableStatement(String sourceKeyspace,
String targetKeyspace,
String sourceTableName,
String targetTableName,
boolean ifNotExists,
TableAttributes attrs)
{
super(targetKeyspace);
this.sourceKeyspace = sourceKeyspace;
this.targetKeyspace = targetKeyspace;
this.sourceTableName = sourceTableName;
this.targetTableName = targetTableName;
this.ifNotExists = ifNotExists;
this.attrs = attrs;
}

@Override
SchemaChange schemaChangeEvent(Keyspaces.KeyspacesDiff diff)
{
return new SchemaChange(SchemaChange.Change.CREATED, SchemaChange.Target.TABLE, targetKeyspace, targetTableName);
}

@Override
public void authorize(ClientState client)
{
client.ensureTablePermission(sourceKeyspace, sourceTableName, Permission.SELECT);
client.ensureAllTablesPermission(targetKeyspace, Permission.CREATE);
}

@Override
public AuditLogContext getAuditLogContext()
{
return new AuditLogContext(AuditLogEntryType.CREATE_TABLE_LIKE, targetKeyspace, targetTableName);
}

@Override
public Keyspaces apply(ClusterMetadata metadata)
{
Keyspaces schema = metadata.schema.getKeyspaces();
KeyspaceMetadata sourceKeyspaceMeta = schema.getNullable(sourceKeyspace);
TableMetadata sourceTableMeta = sourceKeyspaceMeta.getTableNullable(sourceTableName);

if (null == sourceKeyspaceMeta)
throw ire("Source Keyspace '%s' doesn't exist", sourceKeyspace);

if (null == sourceTableMeta)
throw ire("Souce Table '%s'.'%s' doesn't exist", sourceKeyspace, sourceTableName);

if (sourceTableMeta.isIndex())
throw ire("Cannot use CREATE TABLE LIKE on a index table '%s'.'%s'.", sourceKeyspace, sourceTableName);

if (sourceTableMeta.isView())
throw ire("Cannot use CREATE TABLE LIKE on a materialized view '%s'.'%s'.", sourceKeyspace, sourceTableName);

KeyspaceMetadata targetKeyspaceMeta = schema.getNullable(targetKeyspace);
if (null == targetKeyspaceMeta)
throw ire("Target Keyspace '%s' doesn't exist", targetKeyspace);

if (targetKeyspaceMeta.hasTable(targetTableName))
Maxwell-Guo marked this conversation as resolved.
Show resolved Hide resolved
{
if(ifNotExists)
return schema;

throw new AlreadyExistsException(targetKeyspace, targetTableName);
}
// todo support udt for differenet ks latter
if (!sourceKeyspace.equalsIgnoreCase(targetKeyspace) && !sourceKeyspaceMeta.types.isEmpty())
throw ire("Cannot use CREATE TABLE LIKE across different keyspace when source table have UDTs.");

String sourceCQLString = sourceTableMeta.toCqlString(false, false, true, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we ignore the dropped columns, if the seconds input argument is true that means the dropped column will be consider .
Then the cql will be like : create table tb (pk int primary key, cn int, dn int) with xxxx ; alter table tb drop dn;

// add all user functions to be able to give a good error message to the user if the alter references
// a function from another keyspace
UserFunctions.Builder ufBuilder = UserFunctions.builder().add();
for (KeyspaceMetadata ksm : schema)
ufBuilder.add(ksm.userFunctions);

TableMetadata.Builder targetBuilder = CreateTableStatement.parse(sourceCQLString,
targetKeyspace,
targetTableName,
sourceKeyspaceMeta.types,
ufBuilder.build())
.indexes(Indexes.none())
.triggers(Triggers.none());

TableParams originalParams = targetBuilder.build().params;
TableParams newTableParams = attrs.asAlteredTableParams(originalParams);
TableMetadata table = targetBuilder.params(newTableParams)
.id(TableId.get(metadata))
.build();
table.validate();

if (targetKeyspaceMeta.replicationStrategy.hasTransientReplicas()
Copy link
Contributor

@smiklosovic smiklosovic Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Maxwell-Guo This is interesting corner-case. I think that if there was not a possibility to specify different source keyspace from the target one, then we would not need this ... Hm. I think it needs to be there then.

Copy link
Contributor Author

@Maxwell-Guo Maxwell-Guo Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea at the time was that copying a table is also an act of creating a new table, the only difference is that target table has same schema with source table.

We have no way to control the target keyspace because the keyspaces of the two tables may be the same or completely different.

So I kept it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

&& table.params.readRepair != ReadRepairStrategy.NONE)
{
throw ire("read_repair must be set to 'NONE' for transiently replicated keyspaces");
}

if (!table.params.compression.isEnabled())
Copy link
Contributor

@smiklosovic smiklosovic Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Maxwell-Guo

So this says that: "if the compression is not enabled on the source table, then be sure that guardrail enables uncompressed sstables".

While it does make sense when creating the original table for the first time, does it still make sense to do when we are copying it?

If we disabled the compression on source table, then I think that we already do have a guardrail for that, correct? But if we change the guardrail in runtime after the original table was created (e.g. via JMX), then copying such table will not be possible anymore?

So, whether we can copy a table or not when the source table has disabled compression only depends on what the guardrail is set to in the runtime.

I prefer to not check this. I would rather create a copy of the source table no matter what and we can propagate back to the client (the message will appear in cqlsh when copying table) that if guardrail is different, then a user might notified about that, but this is optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we disabled the compression on source table, then I think that we already do have a guardrail for that, correct? But if we change the guardrail in runtime after the original table was created (e.g. via JMX), then copying such table will not be possible anymore?

Yes, that is why I kept it.

The interval between creating two tables may be long, and it is uncertain whether the user has any new ideas about table compression during this period. Therefore, for the behavior of copying the table, except for copying the table schema(column, datatype, mask, table params), creating udt, index, trigger (if needed), and do some validations on udt、indexes and triggers, I have aligned other behaviors and validations with creating a new table.

Copy link
Contributor

@smiklosovic smiklosovic Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it again, I think how it is done is OK, because we can also do create table ks.tb_copy like ks.tb with compression = disabled and using the guardrail here is perfectly sensible.

Guardrails.uncompressedTablesEnabled.ensureEnabled(state);

return schema.withAddedOrUpdated(targetKeyspaceMeta.withSwapped(targetKeyspaceMeta.tables.with(table)));
}

public final static class Raw extends CQLStatement.Raw
{
private final QualifiedName oldName;
private final QualifiedName newName;
private final boolean ifNotExists;
public final TableAttributes attrs = new TableAttributes();

public Raw(QualifiedName newName, QualifiedName oldName, boolean ifNotExists)
{
this.newName = newName;
this.oldName = oldName;
this.ifNotExists = ifNotExists;
}

@Override
public CQLStatement prepare(ClientState state)
{
String oldKeyspace = oldName.hasKeyspace() ? oldName.getKeyspace() : state.getKeyspace();
String newKeyspace = newName.hasKeyspace() ? newName.getKeyspace() : state.getKeyspace();
return new CopyTableStatement(oldKeyspace, newKeyspace, oldName.getName(), newName.getName(), ifNotExists, attrs);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -479,12 +479,21 @@ public String defaultCompactValueName()
}
}

public static TableMetadata.Builder parse(String cql, String keyspace, String table, Types types, UserFunctions userFunctions)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that I am missing something. Why did you change that code?

Copy link
Contributor

@smiklosovic smiklosovic Dec 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blerer the original version of that method (which calls this one) is still there. We just create this one in order to be able to specify custom table name with types and functions. If we parse CQL for old table, we just want to be able to put there table name etc for new table directly upon parsing.

{
Raw createTable = CQLFragmentParser.parseAny(CqlParser::createTableStatement, cql, "CREATE TABLE")
.keyspace(keyspace);

if (table != null)
createTable.table(table);

return createTable.prepare(null) // works around a messy ClientState/QueryProcessor class init deadlock
.builder(types, userFunctions);
}

public static TableMetadata.Builder parse(String cql, String keyspace)
{
return CQLFragmentParser.parseAny(CqlParser::createTableStatement, cql, "CREATE TABLE")
.keyspace(keyspace)
.prepare(null) // works around a messy ClientState/QueryProcessor class init deadlock
.builder(Types.none(), UserFunctions.none());
return parse(cql, keyspace, null, Types.none(), UserFunctions.none());
}

public final static class Raw extends CQLStatement.Raw
Expand Down Expand Up @@ -538,6 +547,12 @@ public Raw keyspace(String keyspace)
return this;
}

public Raw table(String table)
{
name.setName(table, true);
return this;
}

public String table()
{
return name.getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@

import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.tcm.ClusterMetadata;

import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

Expand Down Expand Up @@ -77,6 +80,28 @@ public void testIdClash() throws IOException
}
}

@Test
public void testCreateLikeTable() throws IOException
{
try (Cluster cluster = init(Cluster.build(2).start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.sourcetb (k int primary key, v text)"));
TableId node1id = tableId(cluster.get(1), "sourcetb");
TableId node2id = tableId(cluster.get(2), "sourcetb");
assertEquals(node1id, node2id);
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".targettb LIKE " + KEYSPACE + ".sourcetb");
TableId node1id2 = tableId(cluster.get(1), "targettb");
TableId node2id2 = tableId(cluster.get(2), "targettb");
assertNotEquals(node1id, node1id2);
assertEquals(node1id2, node2id2);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.sourcetb(k, v) VALUES (1, 'v1')"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.targettb(k, v) VALUES (1, 'v1')"), ConsistencyLevel.QUORUM);
Object[] row = row(1, "v1");
assertRows(cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.sourcetb"), ConsistencyLevel.QUORUM), row);
assertRows(cluster.coordinator(1).execute(withKeyspace("SELECT * FROM %s.targettb "), ConsistencyLevel.QUORUM), row);
}
}

long epoch(IInvokableInstance inst)
{
return inst.callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,16 @@ public void userTables() throws IOException
cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl DROP value"));
cluster.forEach(i -> assertTableMetricsExist(i, KEYSPACE, "tbl"));

cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".targettb LIKE " + KEYSPACE + ".tbl");
cluster.forEach(i -> assertTableMetricsExist(i, KEYSPACE, "targettb"));

// drop and make sure table no longer exists
cluster.schemaChange(withKeyspace("DROP TABLE %s.tbl"));
cluster.forEach(i -> assertTableMetricsDoesNotExist(i, KEYSPACE, "tbl"));

cluster.schemaChange(withKeyspace("DROP TABLE %s.targettb"));
cluster.forEach(i -> assertTableMetricsDoesNotExist(i, KEYSPACE, "tbl"));

cluster.schemaChange(withKeyspace("DROP KEYSPACE %s"));
cluster.forEach(i -> assertKeyspaceMetricDoesNotExists(i, KEYSPACE));

Expand Down
24 changes: 24 additions & 0 deletions test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,30 @@ public void testCqlTriggerAuditing() throws Throwable
executeAndAssert(cql, AuditLogEntryType.DROP_TRIGGER);
}


@Test
public void testCqlCreateTableLikeAuditing() throws Throwable
{
String cql = "CREATE TABLE " + KEYSPACE + "." + createTableName() + " (id int primary key, v1 text, v2 text)";
executeAndAssert(cql, AuditLogEntryType.CREATE_TABLE);

cql = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + "." + createTableName() + " (id int primary key, v1 text, v2 text)";
executeAndAssert(cql, AuditLogEntryType.CREATE_TABLE);

String sourceTable = currentTable();

cql = "CREATE TABLE " + KEYSPACE + "." + createTableName() + " LIKE " + KEYSPACE + "." + sourceTable;
executeAndAssert(cql, AuditLogEntryType.CREATE_TABLE_LIKE);

cql = "INSERT INTO " + KEYSPACE + '.' + currentTable() + " (id, v1, v2) VALUES (?, ?, ?)";
executeAndAssertWithPrepare(cql, AuditLogEntryType.UPDATE, 1, "insert_audit", "test");

cql = "SELECT id, v1, v2 FROM " + KEYSPACE + '.' + currentTable() + " WHERE id = ?";
ResultSet rs = executeAndAssertWithPrepare(cql, AuditLogEntryType.SELECT, 1);

assertEquals(1, rs.all().size());
}

@Test
public void testCqlAggregateAuditing() throws Throwable
{
Expand Down
52 changes: 52 additions & 0 deletions test/unit/org/apache/cassandra/auth/GrantAndRevokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,56 @@ public void testWarnings() throws Throwable
res = executeNet("REVOKE SELECT, MODIFY ON KEYSPACE revoke_yeah FROM " + user);
assertWarningsContain(res.getExecutionInfo().getWarnings(), "Role '" + user + "' was not granted MODIFY on <keyspace revoke_yeah>");
}

@Test
public void testCreateTableLikeAuthorize() throws Throwable
{
useSuperUser();

// two keyspaces
executeNet("CREATE KEYSPACE ks1 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
executeNet("CREATE KEYSPACE ks2 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
executeNet("CREATE TABLE ks1.sourcetb (id int PRIMARY KEY, val text)");
executeNet("CREATE USER '" + user + "' WITH PASSWORD '" + pass + "'");

// same keyspace
// has no select permission on source table
ResultSet res = executeNet("REVOKE SELECT ON TABLE ks1.sourcetb FROM " + user);
assertWarningsContain(res.getExecutionInfo().getWarnings(), "Role '" + user + "' was not granted SELECT on <table ks1.sourcetb>");

useUser(user, pass);
assertUnauthorizedQuery("User user has no SELECT permission on <table ks1.sourcetb> or any of its parents",
"CREATE TABLE ks1.targetTb LIKE ks1.sourcetb");

// has select permission on source table no create permission on target keyspace
useSuperUser();
executeNet("GRANT SELECT ON TABLE ks1.sourcetb TO " + user);
res = executeNet("REVOKE CREATE ON KEYSPACE ks1 FROM " + user);
assertWarningsContain(res.getExecutionInfo().getWarnings(), "Role '" + user + "' was not granted CREATE on <keyspace ks1>");

useUser(user, pass);
assertUnauthorizedQuery("User user has no CREATE permission on <all tables in ks1> or any of its parents",
"CREATE TABLE ks1.targetTb LIKE ks1.sourcetb");

// different keyspaces
// has select permission on source table no create permission on target keyspace
useSuperUser();
executeNet("GRANT SELECT ON TABLE ks1.sourcetb TO " + user);
res = executeNet("REVOKE CREATE ON KEYSPACE ks2 FROM " + user);
assertWarningsContain(res.getExecutionInfo().getWarnings(), "Role '" + user + "' was not granted CREATE on <keyspace ks2>");

useUser(user, pass);
assertUnauthorizedQuery("User user has no CREATE permission on <all tables in ks2> or any of its parents",
"CREATE TABLE ks2.targetTb LIKE ks1.sourcetb");

// source keyspace and table does not exists
assertUnauthorizedQuery("User user has no SELECT permission on <table ks1.tbnotexist> or any of its parents",
"CREATE TABLE ks2.targetTb LIKE ks1.tbnotexist");
assertUnauthorizedQuery("User user has no SELECT permission on <table ksnotexists.sourcetb> or any of its parents",
"CREATE TABLE ks2.targetTb LIKE ksnotexists.sourcetb");
// target keyspace does not exists
assertUnauthorizedQuery("User user has no CREATE permission on <all tables in ksnotexists> or any of its parents",
"CREATE TABLE ksnotexists.targetTb LIKE ks1.sourcetb");

Maxwell-Guo marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading