http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index 7a2a1ba..deaf375 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -47,19 +47,14 @@ public class UpdateStatement extends ModificationStatement private static final Constants.Value EMPTY = new Constants.Value(ByteBufferUtil.EMPTY_BYTE_BUFFER); private UpdateStatement(StatementType type, - int boundTerms, + VariableSpecifications bindVariables, TableMetadata metadata, Operations operations, StatementRestrictions restrictions, Conditions conditions, Attributes attrs) { - super(type, boundTerms, metadata, operations, restrictions, conditions, attrs); - } - - public boolean requireFullClusteringKey() - { - return true; + super(type, bindVariables, metadata, operations, restrictions, conditions, attrs); } @Override @@ -124,7 +119,7 @@ public class UpdateStatement extends ModificationStatement * @param columnValues list of column values (corresponds to names) * @param ifNotExists true if an IF NOT EXISTS condition was specified, false otherwise */ - public ParsedInsert(CFName name, + public ParsedInsert(QualifiedName name, Attributes.Raw attrs, List<ColumnMetadata.Raw> columnNames, List<Term.Raw> columnValues, @@ -137,7 +132,7 @@ public class UpdateStatement extends ModificationStatement @Override protected ModificationStatement prepareInternal(TableMetadata metadata, - VariableSpecifications boundNames, + VariableSpecifications bindVariables, Conditions conditions, Attributes attrs) { @@ -170,7 +165,7 @@ public class UpdateStatement extends ModificationStatement else { Operation operation = new Operation.SetValue(value).prepare(metadata, def); - operation.collectMarkerSpecification(boundNames); + operation.collectMarkerSpecification(bindVariables); operations.add(operation); } } @@ -180,13 +175,13 @@ public class UpdateStatement extends ModificationStatement StatementRestrictions restrictions = new StatementRestrictions(type, metadata, whereClause.build(), - boundNames, + bindVariables, applyOnlyToStaticColumns, false, false); return new UpdateStatement(type, - boundNames.size(), + bindVariables, metadata, operations, restrictions, @@ -203,7 +198,7 @@ public class UpdateStatement extends ModificationStatement private final Json.Raw jsonValue; private final boolean defaultUnset; - public ParsedInsertJson(CFName name, Attributes.Raw attrs, Json.Raw jsonValue, boolean defaultUnset, boolean ifNotExists) + public ParsedInsertJson(QualifiedName name, Attributes.Raw attrs, Json.Raw jsonValue, boolean defaultUnset, boolean ifNotExists) { super(name, StatementType.INSERT, attrs, null, ifNotExists, false); this.jsonValue = jsonValue; @@ -212,14 +207,14 @@ public class UpdateStatement extends ModificationStatement @Override protected ModificationStatement prepareInternal(TableMetadata metadata, - VariableSpecifications boundNames, + VariableSpecifications bindVariables, Conditions conditions, Attributes attrs) { checkFalse(metadata.isCounter(), "INSERT statements are not allowed on counter tables, use UPDATE instead"); Collection<ColumnMetadata> defs = metadata.columns(); - Json.Prepared prepared = jsonValue.prepareAndCollectMarkers(metadata, defs, boundNames); + Json.Prepared prepared = jsonValue.prepareAndCollectMarkers(metadata, defs, bindVariables); WhereClause.Builder whereClause = new WhereClause.Builder(); Operations operations = new Operations(type); @@ -238,7 +233,7 @@ public class UpdateStatement extends ModificationStatement else { Operation operation = new Operation.SetValue(raw).prepare(metadata, def); - operation.collectMarkerSpecification(boundNames); + operation.collectMarkerSpecification(bindVariables); operations.add(operation); } } @@ -248,13 +243,13 @@ public class UpdateStatement extends ModificationStatement StatementRestrictions restrictions = new StatementRestrictions(type, metadata, whereClause.build(), - boundNames, + bindVariables, applyOnlyToStaticColumns, false, false); return new UpdateStatement(type, - boundNames.size(), + bindVariables, metadata, operations, restrictions, @@ -279,7 +274,7 @@ public class UpdateStatement extends ModificationStatement * @param whereClause the where clause * @param ifExists flag to check if row exists * */ - public ParsedUpdate(CFName name, + public ParsedUpdate(QualifiedName name, Attributes.Raw attrs, List<Pair<ColumnMetadata.Raw, Operation.RawUpdate>> updates, WhereClause whereClause, @@ -293,7 +288,7 @@ public class UpdateStatement extends ModificationStatement @Override protected ModificationStatement prepareInternal(TableMetadata metadata, - VariableSpecifications boundNames, + VariableSpecifications bindVariables, Conditions conditions, Attributes attrs) { @@ -306,18 +301,18 @@ public class UpdateStatement extends ModificationStatement checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY part %s found in SET part", def.name); Operation operation = entry.right.prepare(metadata, def); - operation.collectMarkerSpecification(boundNames); + operation.collectMarkerSpecification(bindVariables); operations.add(operation); } StatementRestrictions restrictions = newRestrictions(metadata, - boundNames, + bindVariables, operations, whereClause, conditions); return new UpdateStatement(type, - boundNames.size(), + bindVariables, metadata, operations, restrictions,
http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/UseStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java index 02a678a..d48ff62 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java @@ -25,7 +25,7 @@ import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; -public class UseStatement extends ParsedStatement implements CQLStatement +public class UseStatement extends CQLStatement.Raw implements CQLStatement { private final String keyspace; @@ -34,17 +34,12 @@ public class UseStatement extends ParsedStatement implements CQLStatement this.keyspace = keyspace; } - public int getBoundTerms() + public UseStatement prepare(ClientState state) { - return 0; + return this; } - public Prepared prepare() throws InvalidRequestException - { - return new Prepared(this); - } - - public void checkAccess(ClientState state) throws UnauthorizedException + public void authorize(ClientState state) throws UnauthorizedException { state.validateLogin(); } @@ -59,7 +54,7 @@ public class UseStatement extends ParsedStatement implements CQLStatement return new ResultMessage.SetKeyspace(keyspace); } - public ResultMessage executeInternal(QueryState state, QueryOptions options) throws InvalidRequestException + public ResultMessage executeLocally(QueryState state, QueryOptions options) throws InvalidRequestException { // In production, internal queries are exclusively on the system keyspace and 'use' is thus useless // but for some unit tests we need to set the keyspace (e.g. for tests with DROP INDEX) http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java new file mode 100644 index 0000000..bbd5746 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterKeyspaceStatement.java @@ -0,0 +1,104 @@ +/* + * 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 java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.KeyspaceAttributes; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.locator.LocalStrategy; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.Event.SchemaChange.Change; + +public final class AlterKeyspaceStatement extends AlterSchemaStatement +{ + private final KeyspaceAttributes attrs; + + public AlterKeyspaceStatement(String keyspaceName, KeyspaceAttributes attrs) + { + super(keyspaceName); + this.attrs = attrs; + } + + public Keyspaces apply(Keyspaces schema) + { + attrs.validate(); + + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + if (null == keyspace) + throw ire("Keyspace '%s' doesn't exist", keyspaceName); + + KeyspaceMetadata newKeyspace = keyspace.withSwapped(attrs.asAlteredKeyspaceParams(keyspace.params)); + + if (newKeyspace.params.replication.klass.equals(LocalStrategy.class)) + throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); + + newKeyspace.params.validate(keyspaceName); + + return schema.withAddedOrUpdated(newKeyspace); + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + return new SchemaChange(Change.UPDATED, keyspaceName); + } + + public void authorize(ClientState client) + { + client.ensureKeyspacePermission(keyspaceName, Permission.ALTER); + } + + @Override + Set<String> clientWarnings(KeyspacesDiff diff) + { + KeyspaceDiff keyspaceDiff = diff.altered.get(0); + + AbstractReplicationStrategy before = keyspaceDiff.before.createReplicationStrategy(); + AbstractReplicationStrategy after = keyspaceDiff.after.createReplicationStrategy(); + + return before.getReplicationFactor() < after.getReplicationFactor() + ? ImmutableSet.of("When increasing replication factor you need to run a full (-full) repair to distribute the data.") + : ImmutableSet.of(); + } + + public static final class Raw extends CQLStatement.Raw + { + private final String keyspaceName; + private final KeyspaceAttributes attrs; + + public Raw(String keyspaceName, KeyspaceAttributes attrs) + { + this.keyspaceName = keyspaceName; + this.attrs = attrs; + } + + public AlterKeyspaceStatement prepare(ClientState state) + { + return new AlterKeyspaceStatement(keyspaceName, attrs); + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java new file mode 100644 index 0000000..1776bf1 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java @@ -0,0 +1,161 @@ +/* + * 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 java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.auth.AuthenticatedUser; +import org.apache.cassandra.auth.IResource; +import org.apache.cassandra.auth.RoleResource; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.MigrationManager; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SchemaTransformation; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.FBUtilities; + +abstract class AlterSchemaStatement implements CQLStatement, SchemaTransformation +{ + protected final String keyspaceName; // name of the keyspace affected by the statement + + protected AlterSchemaStatement(String keyspaceName) + { + this.keyspaceName = keyspaceName; + } + + public void validate(ClientState state) + { + // no-op; validation is performed while executing the statement, in apply() + } + + public void authorize(ClientState client) + { + // TODO: drop default impl + } + + public ResultMessage execute(QueryState state, QueryOptions options, long queryStartNanoTime) + { + return execute(state, false); + } + + public ResultMessage executeLocally(QueryState state, QueryOptions options) + { + return execute(state, true); + } + + /** + * TODO: drop default impl, document + */ + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + return null; + } + + /** + * Schema alteration may result in a new database object (keyspace, table, role, function) being created capable of + * having permissions GRANTed on it. The creator of the object (the primary role assigned to the AuthenticatedUser + * performing the operation) is automatically granted ALL applicable permissions on the object. This is a hook for + * subclasses to override in order indicate which resources to to perform that grant on when the statement is executed. + * + * Only called if the transformation resulted in a non-empty diff. + */ + Set<IResource> createdResources(KeyspacesDiff diff) + { + return ImmutableSet.of(); + } + + /** + * Schema alteration might produce a client warning (e.g. a warning to run full repair when increading RF of a keyspace). + * This method should be used to generate them instead of calling warn() in transformation code. + * + * Only called if the transformation resulted in a non-empty diff. + */ + Set<String> clientWarnings(KeyspacesDiff diff) + { + return ImmutableSet.of(); + } + + public ResultMessage execute(QueryState state, boolean locally) + { + if (SchemaConstants.isSystemKeyspace(keyspaceName)) + throw ire("System keyspace '%s' is not user-modifiable", keyspaceName); + + validateKeyspaceName(); + + KeyspacesDiff diff = MigrationManager.alterSchema(this, FBUtilities.timestampMicros(), locally); + + if (diff.isEmpty()) + return new ResultMessage.Void(); + + /* + * When a schema alteration results in a new db object being created, we grant permissions on the new + * object to the user performing the request if: + * - the user is not anonymous + * - the configured IAuthorizer supports granting of permissions (not all do, AllowAllAuthorizer doesn't and + * custom external implementations may not) + */ + AuthenticatedUser user = state.getClientState().getUser(); + if (null != user && !user.isAnonymous()) + createdResources(diff).forEach(r -> grantPermissionsOnResource(r, user)); + + clientWarnings(diff).forEach(ClientWarn.instance::warn); + + return new ResultMessage.SchemaChange(schemaChangeEvent(diff)); + } + + private void validateKeyspaceName() + { + if (!SchemaConstants.isValidName(keyspaceName)) + { + throw ire("Keyspace name must not be empty, more than %d characters long, " + + "or contain non-alphanumeric-underscore characters (got '%s')", + SchemaConstants.NAME_LENGTH, keyspaceName); + } + } + + private void grantPermissionsOnResource(IResource resource, AuthenticatedUser user) + { + try + { + DatabaseDescriptor.getAuthorizer() + .grant(AuthenticatedUser.SYSTEM_USER, + resource.applicablePermissions(), + resource, + RoleResource.role(user.getName())); + } + catch (UnsupportedOperationException e) + { + // not a problem - grant is an optional method on IAuthorizer + } + } + + static InvalidRequestException ire(String format, Object... args) + { + return new InvalidRequestException(String.format(format, args)); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java new file mode 100644 index 0000000..f0cd58d --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -0,0 +1,341 @@ +/* + * 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 java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.statements.TableAttributes; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.schema.ViewMetadata; +import org.apache.cassandra.schema.Views; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; + +import static java.lang.String.join; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.isEmpty; +import static com.google.common.collect.Iterables.transform; + +public abstract class AlterTableStatement extends AlterSchemaStatement +{ + protected final String tableName; + + public AlterTableStatement(String keyspaceName, String tableName) + { + super(keyspaceName); + this.tableName = tableName; + } + + public void authorize(ClientState client) + { + client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER); + } + + SchemaChange schemaChangeEvent(Keyspaces.KeyspacesDiff diff) + { + return new SchemaChange(SchemaChange.Change.UPDATED, SchemaChange.Target.TABLE, keyspaceName, tableName); + } + + public Keyspaces apply(Keyspaces schema) + { + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + + TableMetadata table = null == keyspace + ? null + : keyspace.getTableOrViewNullable(tableName); + + if (null == table) + throw ire("Table '%s.%s' doesn't exist", keyspaceName, tableName); + + if (table.isView()) + throw ire("Cannot use ALTER TABLE on a materialized view; use ALTER MATERIALIZED VIEW instead"); + + return schema.withAddedOrUpdated(apply(keyspace, table)); + } + + abstract KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table); + + public static class AddColumns extends AlterTableStatement + { + private final Collection<ColumnAdded> newColumns; + + public AddColumns(String keyspaceName, String tableName, Collection<ColumnAdded> newColumns) + { + super(keyspaceName, tableName); + this.newColumns = newColumns; + } + + public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + { + if (table.isCompactTable()) + throw ire("Cannot add new column to a COMPACT STORAGE table"); + + TableMetadata.Builder tableBuilder = table.unbuild(); + Views.Builder viewsBuilder = keyspace.views.unbuild(); + newColumns.forEach(c -> addColumn(keyspace, table, c, tableBuilder, viewsBuilder)); + + return keyspace.withSwapped(keyspace.tables.withSwapped(tableBuilder.build())) + .withSwapped(viewsBuilder.build()); + } + + private void addColumn(KeyspaceMetadata keyspace, + TableMetadata table, + ColumnAdded column, + TableMetadata.Builder tableBuilder, + Views.Builder viewsBuilder) + { + ColumnIdentifier name = column.name.getIdentifier(table); + AbstractType<?> type = column.type.prepare(keyspaceName, keyspace.types).getType(); + boolean isStatic = column.isStatic; + + if (null != tableBuilder.getColumn(name)) + throw ire("Column with name '%s' already exists", name); + + if (isStatic && table.clusteringColumns().isEmpty()) + throw ire("Static columns are only useful (and thus allowed) if the table has at least one clustering column"); + + ColumnMetadata droppedColumn = table.getDroppedColumn(name.bytes); + if (null != droppedColumn) + { + // After #8099, not safe to re-add columns of incompatible types - until *maybe* deser logic with dropped + // columns is pushed deeper down the line. The latter would still be problematic in cases of schema races. + if (!droppedColumn.type.isValueCompatibleWith(type)) + { + throw ire("Cannot re-add a previously dropped column '%s' of type %s, incompatible with previous type %s", + name, + type.asCQL3Type(), + droppedColumn.type.asCQL3Type()); + } + + // Cannot re-add a dropped counter column. See #7831. + if (table.isCounter()) + throw ire("Cannot re-add previously dropped counter column %s", name); + } + + if (type.isCollection() && type.isMultiCell()) + { + if (table.isCompactTable()) + throw ire("Cannot use non-frozen collections in COMPACT STORAGE tables"); + + if (table.isSuper()) + throw ire("Cannot use non-frozen collections with super column families"); + } + + if (isStatic) + tableBuilder.addStaticColumn(name, type); + else + tableBuilder.addRegularColumn(name, type); + + if (!isStatic) + { + for (ViewMetadata view : keyspace.views.forTable(table.id)) + { + if (view.includeAllColumns) + { + ColumnMetadata viewColumn = ColumnMetadata.regularColumn(view.metadata, name.bytes, type); + viewsBuilder.put(viewsBuilder.get(view.name()).withAddedRegularColumn(viewColumn)); + } + } + } + } + } + + public static class DropColumns extends AlterTableStatement + { + private final Collection<ColumnMetadata.Raw> removedColumns; + private final long timestamp; + + public DropColumns(String keyspaceName, String tableName, Collection<ColumnMetadata.Raw> removedColumns, long timestamp) + { + super(keyspaceName, tableName); + this.removedColumns = removedColumns; + this.timestamp = timestamp; + } + + public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + { + if (!table.isCQLTable()) + throw ire("Cannot drop columns from a non-CQL3 table"); + + TableMetadata.Builder builder = table.unbuild(); + removedColumns.forEach(c -> dropColumn(keyspace, table, c, builder)); + + return keyspace.withSwapped(keyspace.tables.withSwapped(builder.build())); + } + + private void dropColumn(KeyspaceMetadata keyspace, TableMetadata table, ColumnMetadata.Raw column, TableMetadata.Builder builder) + { + ColumnIdentifier name = column.getIdentifier(table); + + ColumnMetadata currentColumn = table.getColumn(name); + if (null == currentColumn) + throw ire("Column %s was not found in table '%s'", name, table); + + if (currentColumn.isPrimaryKeyColumn()) + throw ire("Cannot drop PRIMARY KEY column %s", name); + + // TODO: some day try and find a way to not rely on Keyspace/IndexManager/Index to find dependent indexes + Set<IndexMetadata> dependentIndexes = Keyspace.openAndGetStore(table).indexManager.getDependentIndexes(currentColumn); + if (!dependentIndexes.isEmpty()) + { + throw ire("Cannot drop column %s because it has dependent secondary indexes (%s)", + currentColumn, + join(", ", transform(dependentIndexes, i -> i.name))); + } + + Iterable<ViewMetadata> dependentViews = filter(keyspace.views.forTable(table.id), v -> v.includes(name)); + if (!isEmpty(dependentViews)) + { + throw ire("Cannot drop column %s because it has dependent materialized views (%s)", + currentColumn, + join(", ", transform(dependentViews, ViewMetadata::name))); + } + + builder.removeRegularOrStaticColumn(name); + builder.recordColumnDrop(currentColumn, timestamp); + } + } + + public static class RenameColumns extends AlterTableStatement + { + private final Map<ColumnMetadata.Raw, ColumnMetadata.Raw> renamedColumns; + + public RenameColumns(String keyspaceName, String tableName, Map<ColumnMetadata.Raw, ColumnMetadata.Raw> renamedColumns) + { + super(keyspaceName, tableName); + this.renamedColumns = renamedColumns; + } + + public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + { + TableMetadata.Builder tableBuilder = table.unbuild(); + Views.Builder viewsBuilder = keyspace.views.unbuild(); + renamedColumns.forEach((o, n) -> renameColumn(keyspace, table, o, n, tableBuilder, viewsBuilder)); + + return keyspace.withSwapped(keyspace.tables.withSwapped(tableBuilder.build())) + .withSwapped(viewsBuilder.build()); + } + + private void renameColumn(KeyspaceMetadata keyspace, + TableMetadata table, + ColumnMetadata.Raw oldName, + ColumnMetadata.Raw newName, + TableMetadata.Builder tableBuilder, + Views.Builder viewsBuilder) + { + ColumnIdentifier oldColumnName = oldName.getIdentifier(table); + ColumnIdentifier newColumnName = newName.getIdentifier(table); + + ColumnMetadata column = table.getColumn(oldColumnName); + if (null == column) + throw ire("Column %s was not found in table %s", oldColumnName, table); + + if (!column.isPrimaryKeyColumn()) + throw ire("Cannot rename non PRIMARY KEY column %s", oldColumnName); + + if (null != table.getColumn(newColumnName)) + { + throw ire("Cannot rename column %s to %s in table '%s'; another column with that name already exists", + oldColumnName, + newColumnName, + table); + } + + // TODO: some day try and find a way to not rely on Keyspace/IndexManager/Index to find dependent indexes + Set<IndexMetadata> dependentIndexes = Keyspace.openAndGetStore(table).indexManager.getDependentIndexes(column); + if (!dependentIndexes.isEmpty()) + { + throw ire("Can't rename column %s because it has dependent secondary indexes (%s)", + oldColumnName, + join(", ", transform(dependentIndexes, i -> i.name))); + } + + for (ViewMetadata view : keyspace.views.forTable(table.id)) + { + if (view.includes(oldColumnName)) + { + ColumnIdentifier oldViewColumn = oldName.getIdentifier(view.metadata); + ColumnIdentifier newViewColumn = newName.getIdentifier(view.metadata); + + viewsBuilder.put(viewsBuilder.get(view.name()).withRenamedPrimaryKeyColumn(oldViewColumn, newViewColumn)); + } + } + + tableBuilder.renamePrimaryKeyColumn(oldColumnName, newColumnName); + } + } + + public static class AlterOptions extends AlterTableStatement + { + private final TableAttributes attrs; + + public AlterOptions(String keyspaceName, String tableName, TableAttributes attrs) + { + super(keyspaceName, tableName); + this.attrs = attrs; + } + + public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table) + { + attrs.validate(); + + TableParams params = attrs.asAlteredTableParams(table.params); + + if (table.isCounter() && params.defaultTimeToLive > 0) + throw ire("Cannot set default_time_to_live on a table with counters"); + + if (!isEmpty(keyspace.views.forTable(table.id)) && params.gcGraceSeconds == 0) + { + throw ire("Cannot alter gc_grace_seconds of the base table of a " + + "materialized view to 0, since this value is used to TTL " + + "undelivered updates. Setting gc_grace_seconds too low might " + + "cause undelivered updates to expire " + + "before being replayed."); + } + + return keyspace.withSwapped(keyspace.tables.withSwapped(table.withSwapped(params))); + } + } + + public static class ColumnAdded + { + final ColumnMetadata.Raw name; + final CQL3Type.Raw type; + final boolean isStatic; + + ColumnAdded(ColumnMetadata.Raw name, CQL3Type.Raw type, boolean isStatic) + { + this.name = name; + this.type = type; + this.isStatic = isStatic; + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java new file mode 100644 index 0000000..42adfe0 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTypeStatement.java @@ -0,0 +1,149 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; + +import static java.lang.String.join; +import static java.util.function.Predicate.isEqual; +import static java.util.stream.Collectors.toList; + +import static org.apache.cassandra.utils.ByteBufferUtil.bytes; + +public abstract class AlterTypeStatement extends AlterSchemaStatement +{ + protected final String typeName; + + public AlterTypeStatement(String keyspaceName, String typeName) + { + super(keyspaceName); + this.typeName = typeName; + } + + public void authorize(ClientState client) + { + client.ensureKeyspacePermission(keyspaceName, Permission.ALTER); + } + + SchemaChange schemaChangeEvent(Keyspaces.KeyspacesDiff diff) + { + return new SchemaChange(SchemaChange.Change.UPDATED, SchemaChange.Target.TYPE, keyspaceName, typeName); + } + + public Keyspaces apply(Keyspaces schema) + { + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + + UserType type = null == keyspace + ? null + : keyspace.types.getNullable(bytes(typeName)); + + if (null == type) + throw ire("Type '%s.%s' doesn't exist", keyspaceName, typeName); + + return schema.withAddedOrUpdated(keyspace.withUpdatedUserType(apply(keyspace, type))); + } + + abstract UserType apply(KeyspaceMetadata keyspace, UserType type); + + public static final class AddField extends AlterTypeStatement + { + private final FieldIdentifier fieldName; + private final CQL3Type.Raw type; + + public AddField(String keyspaceName, String typeName, FieldIdentifier fieldName, CQL3Type.Raw type) + { + super(keyspaceName, typeName); + this.fieldName = fieldName; + this.type = type; + } + + UserType apply(KeyspaceMetadata keyspace, UserType userType) + { + if (userType.fieldPosition(fieldName) >= 0) + throw ire("Cannot add field %s to type %s: a field with name %s already exists", fieldName, userType, fieldName); + + AbstractType<?> fieldType = type.prepare(keyspaceName, keyspace.types).getType(); + if (fieldType.referencesUserType(userType.name)) + throw ire("Cannot add new field %s of type %s to user type %s as it would create a circular reference", fieldName, type, userType); + + List<FieldIdentifier> fieldNames = new ArrayList<>(userType.fieldNames()); fieldNames.add(fieldName); + List<AbstractType<?>> fieldTypes = new ArrayList<>(userType.fieldTypes()); fieldTypes.add(fieldType); + + return new UserType(keyspaceName, userType.name, fieldNames, fieldTypes, userType.isMultiCell()); + } + } + + public static final class RenameFields extends AlterTypeStatement + { + private final Map<FieldIdentifier, FieldIdentifier> renamedFields; + + public RenameFields(String keyspaceName, String typeName, Map<FieldIdentifier, FieldIdentifier> renamedFields) + { + super(keyspaceName, typeName); + this.renamedFields = renamedFields; + } + + UserType apply(KeyspaceMetadata keyspace, UserType userType) + { + List<String> dependentAggregates = + keyspace.functions + .udas() + .filter(uda -> null != uda.initialCondition() && uda.stateType().referencesUserType(userType.name)) + .map(uda -> uda.name().toString()) + .collect(toList()); + + if (!dependentAggregates.isEmpty()) + { + throw ire("Cannot alter user type %s as it is still used in INITCOND by aggregates %s", + typeName, + join(", ", dependentAggregates)); + } + + List<FieldIdentifier> fieldNames = new ArrayList<>(userType.fieldNames()); + + renamedFields.forEach((oldName, newName) -> + { + int idx = userType.fieldPosition(oldName); + if (idx < 0) + throw ire("Unkown field %s in type %s", oldName, typeName); + fieldNames.set(idx, newName); + }); + + fieldNames.forEach(name -> + { + if (fieldNames.stream().filter(isEqual(name)).count() > 1) + throw ire("Duplicate field name %s in type %s", name, typeName); + }); + + return new UserType(keyspaceName, userType.name, fieldNames, userType.fieldTypes(), userType.isMultiCell()); + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java new file mode 100644 index 0000000..2de352e --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java @@ -0,0 +1,105 @@ +/* + * 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.auth.Permission; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.statements.TableAttributes; +import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.Event.SchemaChange.Change; +import org.apache.cassandra.transport.Event.SchemaChange.Target; + +public final class AlterViewStatement extends AlterSchemaStatement +{ + private final String viewName; + private final TableAttributes attrs; + + public AlterViewStatement(String keyspaceName, String viewName, TableAttributes attrs) + { + super(keyspaceName); + this.viewName = viewName; + this.attrs = attrs; + } + + public Keyspaces apply(Keyspaces schema) + { + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + + ViewMetadata view = null == keyspace + ? null + : keyspace.views.getNullable(viewName); + + if (null == view) + throw ire("Materialized view '%s.%s' doesn't exist", keyspaceName, viewName); + + attrs.validate(); + + TableParams params = attrs.asAlteredTableParams(view.metadata.params); + + if (params.gcGraceSeconds == 0) + { + throw ire("Cannot alter gc_grace_seconds of a materialized view to 0, since this " + + "value is used to TTL undelivered updates. Setting gc_grace_seconds too " + + "low might cause undelivered updates to expire before being replayed."); + } + + if (params.defaultTimeToLive > 0) + { + throw ire("Cannot set or alter default_time_to_live for a materialized view. " + + "Data in a materialized view always expire at the same time than " + + "the corresponding data in the parent table."); + } + + ViewMetadata newView = view.copy(view.metadata.withSwapped(params)); + return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.views.withSwapped(newView))); + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, viewName); + } + + public void authorize(ClientState client) + { + ViewMetadata view = Schema.instance.getView(keyspaceName, viewName); + if (null != view) + client.ensureTablePermission(keyspaceName, view.baseTableName, Permission.ALTER); + } + + public static final class Raw extends CQLStatement.Raw + { + private final QualifiedName name; + private final TableAttributes attrs; + + public Raw(QualifiedName name, TableAttributes attrs) + { + this.name = name; + this.attrs = attrs; + } + + public AlterViewStatement prepare(ClientState state) + { + String keyspaceName = name.hasKeyspace() ? name.getKeyspace() : state.getKeyspace(); + return new AlterViewStatement(keyspaceName, name.getName(), attrs); + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java new file mode 100644 index 0000000..04e3c81 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateAggregateStatement.java @@ -0,0 +1,231 @@ +/* + * 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 java.nio.ByteBuffer; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; + +import org.apache.cassandra.auth.FunctionResource; +import org.apache.cassandra.auth.IResource; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.functions.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.ProtocolVersion; + +import static java.lang.String.format; +import static java.lang.String.join; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; + +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Iterables.transform; + +public final class CreateAggregateStatement extends AlterSchemaStatement +{ + private final String aggregateName; + private final List<CQL3Type.Raw> rawArgumentTypes; + private final CQL3Type.Raw rawStateType; + private final FunctionName stateFunctionName; + private final FunctionName finalFunctionName; + private final Term.Raw rawInitialValue; + private final boolean orReplace; + private final boolean ifNotExists; + + public CreateAggregateStatement(String keyspaceName, + String aggregateName, + List<CQL3Type.Raw> rawArgumentTypes, + CQL3Type.Raw rawStateType, + FunctionName stateFunctionName, + FunctionName finalFunctionName, + Term.Raw rawInitialValue, + boolean orReplace, + boolean ifNotExists) + { + super(keyspaceName); + this.aggregateName = aggregateName; + this.rawArgumentTypes = rawArgumentTypes; + this.rawStateType = rawStateType; + this.stateFunctionName = stateFunctionName; + this.finalFunctionName = finalFunctionName; + this.rawInitialValue = rawInitialValue; + this.orReplace = orReplace; + this.ifNotExists = ifNotExists; + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + throw new UnsupportedOperationException(); + } + + @Override + Set<IResource> createdResources(KeyspacesDiff diff) + { + return ImmutableSet.of(FunctionResource.functionFromCql(keyspaceName, aggregateName, rawArgumentTypes)); + } + + public Keyspaces apply(Keyspaces schema) + { + if (ifNotExists && orReplace) + throw ire("Cannot use both 'OR REPLACE' and 'IF NOT EXISTS' directives"); + + rawArgumentTypes.stream() + .filter(CQL3Type.Raw::isFrozen) + .findFirst() + .ifPresent(t -> { throw ire("Argument '%s' cannot be frozen; remove frozen<> modifier from '%s'", t, t); }); + + if (rawStateType.isFrozen()) + throw ire("State type '%s' cannot be frozen; remove frozen<> modifier from '%s'", rawStateType, rawStateType); + + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + if (null == keyspace) + throw ire("Keyspace '%s' doesn't exist", keyspaceName); + + /* + * Resolve the state function + */ + + // TODO replace Lists.transform use + List<AbstractType<?>> argumentTypes = Lists.transform(rawArgumentTypes, t -> t.prepare(keyspaceName, keyspace.types).getType()); + AbstractType<?> stateType = rawStateType.prepare(keyspaceName, keyspace.types).getType(); + List<AbstractType<?>> stateFunctionArguments = Lists.newArrayList(concat(singleton(stateType), argumentTypes)); + + Function stateFunction = keyspace.functions.find(stateFunctionName, stateFunctionArguments).orElse(null); + if (null == stateFunction) + throw ire("State function %s does not exist", stateFunctionString()); + + if (stateFunction.isAggregate()) + throw ire("State function %s is not a scalar function", stateFunctionString()); + + if (!stateFunction.returnType().equals(stateType)) + { + throw ire("State function %s return type must be the same as the first argument type - check STYPE, argument and return types", + stateFunctionString()); + } + + /* + * Resolve the final function and return type + */ + + Function finalFunction = null; + AbstractType<?> returnType = stateFunction.returnType(); + + if (null != finalFunctionName) + { + finalFunction = keyspace.functions.find(finalFunctionName, singletonList(stateType)).orElse(null); + if (null == finalFunction) + throw ire("Final function %s does not exist", finalFunctionString()); + + if (finalFunction.isAggregate()) + throw ire("Final function %s is not a scalar function", finalFunctionString()); + + // override return type with that of the final function + returnType = finalFunction.returnType(); + } + + /* + * Validate initial condition + */ + + // TODO: WTF? + ByteBuffer initialValue = null; + if (null != rawInitialValue) + { + initialValue = Terms.asBytes(keyspaceName, initialValue.toString(), stateType); + + if (null != initialValue) + { + try + { + stateType.validate(initialValue); + } + catch (MarshalException e) + { + throw ire("Invalid value for INITCOND of type %s", stateType.asCQL3Type()); + } + } + + // Converts initcond to a CQL literal and parse it back to avoid another CASSANDRA-11064 + String initialValueString = stateType.asCQL3Type().toCQLLiteral(initialValue, ProtocolVersion.CURRENT); + assert !Terms.asBytes(keyspaceName, initialValueString, stateType).equals(initialValue); + + if (Constants.NULL_LITERAL != rawInitialValue && UDHelper.isNullOrEmpty(stateType, initialValue)) + throw ire("INITCOND must not be empty for all types except TEXT, ASCII, BLOB"); + } + + if (!((UDFunction) stateFunction).isCalledOnNullInput() && null == initialValue) + { + throw ire("Cannot create aggregate '%s' without INITCOND because state function %s does not accept 'null' arguments", + aggregateName, + stateFunctionName); + } + + /* + * Create or replace + */ + + Function existingAggregate = keyspace.functions.find(new FunctionName(keyspaceName, aggregateName), argumentTypes).orElse(null); + if (null != existingAggregate) + { + if (!existingAggregate.isAggregate()) + throw ire("Aggregate '%s' cannot replace a function", aggregateName); + + if (ifNotExists) + return schema; + + if (!orReplace) + throw ire("Aggregate '%s' already exists", aggregateName); + + if (!returnType.isCompatibleWith(existingAggregate.returnType())) + { + throw ire("Cannot replace aggregate '%s', the new return type %s is not compatible with the return type %s of existing function", + aggregateName, + returnType.asCQL3Type(), + existingAggregate.returnType().asCQL3Type()); + } + } + + UDAggregate aggregate = + new UDAggregate(new FunctionName(keyspaceName, aggregateName), + argumentTypes, + returnType, + (ScalarFunction) stateFunction, + (ScalarFunction) finalFunction, + initialValue); + + return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.functions.withAddedOrUpdated(aggregate))); + } + + private String stateFunctionString() + { + return format("%s(%s)", stateFunctionName, join(", ", transform(concat(singleton(rawStateType), rawArgumentTypes), Object::toString))); + } + + private String finalFunctionString() + { + return format("%s(%s)", finalFunctionName, rawStateType); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java new file mode 100644 index 0000000..00b98f5 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateFunctionStatement.java @@ -0,0 +1,239 @@ +/* + * 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 java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; + +import org.apache.cassandra.auth.FunctionResource; +import org.apache.cassandra.auth.IResource; +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.cql3.functions.UDFunction; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.schema.Functions.FunctionsDiff; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.Event.SchemaChange.Change; +import org.apache.cassandra.transport.Event.SchemaChange.Target; + +import static java.util.stream.Collectors.toList; + +public final class CreateFunctionStatement extends AlterSchemaStatement +{ + private final String functionName; + private final List<ColumnIdentifier> argumentNames; + private final List<CQL3Type.Raw> rawArgumentTypes; + private final CQL3Type.Raw rawReturnType; + private final boolean calledOnNullInput; + private final String language; + private final String body; + private final boolean orReplace; + private final boolean ifNotExists; + + public CreateFunctionStatement(String keyspaceName, + String functionName, + List<ColumnIdentifier> argumentNames, + List<CQL3Type.Raw> rawArgumentTypes, + CQL3Type.Raw rawReturnType, + boolean calledOnNullInput, + String language, + String body, + boolean orReplace, + boolean ifNotExists) + { + super(keyspaceName); + this.functionName = functionName; + this.argumentNames = argumentNames; + this.rawArgumentTypes = rawArgumentTypes; + this.rawReturnType = rawReturnType; + this.calledOnNullInput = calledOnNullInput; + this.language = language; + this.body = body; + this.orReplace = orReplace; + this.ifNotExists = ifNotExists; + } + + // TODO: replace affected aggregates !! + public Keyspaces apply(Keyspaces schema) + { + if (ifNotExists && orReplace) + throw ire("Cannot use both 'OR REPLACE' and 'IF NOT EXISTS' directives"); + + UDFunction.assertUdfsEnabled(language); + + if (new HashSet<>(argumentNames).size() != argumentNames.size()) + throw ire("Duplicate argument names for given function %s with argument names %s", functionName, argumentNames); + + rawArgumentTypes.stream() + .filter(CQL3Type.Raw::isFrozen) + .findFirst() + .ifPresent(t -> { throw ire("Argument '%s' cannot be frozen; remove frozen<> modifier from '%s'", t, t); }); + + if (rawReturnType.isFrozen()) + throw ire("Return type '%s' cannot be frozen; remove frozen<> modifier from '%s'", rawReturnType, rawReturnType); + + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + if (null == keyspace) + throw ire("Keyspace '%s' doesn't exist", keyspaceName); + + // TODO: replace Lists.transform use + List<AbstractType<?>> argumentTypes = Lists.transform(rawArgumentTypes, t -> t.prepare(keyspaceName, keyspace.types).getType()); + AbstractType<?> returnType = rawReturnType.prepare(keyspaceName, keyspace.types).getType(); + + Function existingFunction = keyspace.functions.find(new FunctionName(keyspaceName, functionName), argumentTypes).orElse(null); + if (null != existingFunction) + { + if (existingFunction.isAggregate()) + throw ire("Function '%s' cannot replace an aggregate", functionName); + + if (ifNotExists) + return schema; + + if (!orReplace) + throw ire("Function '%s' already exists", functionName); + + if (calledOnNullInput != ((UDFunction) existingFunction).isCalledOnNullInput()) + { + throw ire("Function '%s' must have %s directive", + functionName, + calledOnNullInput ? "CALLED ON NULL INPUT" : "RETURNS NULL ON NULL INPUT"); + } + + if (!returnType.isCompatibleWith(existingFunction.returnType())) + { + throw ire("Cannot replace function '%s', the new return type %s is not compatible with the return type %s of existing function", + functionName, + returnType.asCQL3Type(), + existingFunction.returnType().asCQL3Type()); + } + } + + UDFunction function = + UDFunction.create(new FunctionName(keyspaceName, functionName), + argumentNames, + argumentTypes, + returnType, + calledOnNullInput, + language, + body); + + return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.functions.withAddedOrUpdated(function))); + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + assert diff.altered.size() == 1; + FunctionsDiff<UDFunction> udfsDiff = diff.altered.get(0).udfs; + + assert udfsDiff.created.size() + udfsDiff.altered.size() == 1; + boolean created = !udfsDiff.created.isEmpty(); + + return new SchemaChange(created ? Change.CREATED : Change.UPDATED, + Target.FUNCTION, + keyspaceName, + functionName, + rawArgumentTypes.stream().map(CQL3Type.Raw::toString).collect(toList())); + } + + public void authorize(ClientState client) + { + FunctionName name = new FunctionName(keyspaceName, functionName); + + // TODO: replace lists.transform use + if (Schema.instance.findFunction(name, Lists.transform(rawArgumentTypes, t -> t.prepare(keyspaceName).getType())).isPresent() && orReplace) + client.ensurePermission(Permission.ALTER, FunctionResource.functionFromCql(keyspaceName, functionName, rawArgumentTypes)); + else + client.ensurePermission(Permission.CREATE, FunctionResource.keyspace(keyspaceName)); + } + + @Override + Set<IResource> createdResources(KeyspacesDiff diff) + { + assert diff.altered.size() == 1; + FunctionsDiff<UDFunction> udfsDiff = diff.altered.get(0).udfs; + + assert udfsDiff.created.size() + udfsDiff.altered.size() == 1; + + return udfsDiff.created.isEmpty() + ? ImmutableSet.of() + : ImmutableSet.of(FunctionResource.functionFromCql(keyspaceName, functionName, rawArgumentTypes)); + } + + public static final class Raw extends CQLStatement.Raw + { + private final FunctionName name; + private final List<ColumnIdentifier> argumentNames; + private final List<CQL3Type.Raw> rawArgumentTypes; + private final CQL3Type.Raw rawReturnType; + private final boolean calledOnNullInput; + private final String language; + private final String body; + private final boolean orReplace; + private final boolean ifNotExists; + + public Raw(FunctionName name, + List<ColumnIdentifier> argumentNames, + List<CQL3Type.Raw> rawArgumentTypes, + CQL3Type.Raw rawReturnType, + boolean calledOnNullInput, + String language, + String body, + boolean orReplace, + boolean ifNotExists) + { + this.name = name; + this.argumentNames = argumentNames; + this.rawArgumentTypes = rawArgumentTypes; + this.rawReturnType = rawReturnType; + this.calledOnNullInput = calledOnNullInput; + this.language = language; + this.body = body; + this.orReplace = orReplace; + this.ifNotExists = ifNotExists; + } + + public CreateFunctionStatement prepare(ClientState state) + { + String keyspaceName = name.hasKeyspace() ? name.keyspace : state.getKeyspace(); + + return new CreateFunctionStatement(keyspaceName, + name.name, + argumentNames, + rawArgumentTypes, + rawReturnType, + calledOnNullInput, + language, + body, + orReplace, + ifNotExists); + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java new file mode 100644 index 0000000..c8849e7 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java @@ -0,0 +1,237 @@ +/* + * 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 java.util.*; + +import com.google.common.collect.Lists; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.statements.IndexAttributes; +import org.apache.cassandra.cql3.statements.IndexTarget; +import org.apache.cassandra.cql3.statements.IndexTarget.Type; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.Event.SchemaChange.Change; +import org.apache.cassandra.transport.Event.SchemaChange.Target; + +import static com.google.common.collect.Iterables.tryFind; + +public final class CreateIndexStatement extends AlterSchemaStatement +{ + private final String indexName; + private final String tableName; + private final List<IndexTarget.Raw> rawIndexTargets; + private final IndexAttributes attrs; + private final boolean ifNotExists; + + public CreateIndexStatement(String keyspaceName, + String tableName, + String indexName, + List<IndexTarget.Raw> rawIndexTargets, + IndexAttributes attrs, + boolean ifNotExists) + { + super(keyspaceName); + this.tableName = tableName; + this.indexName = indexName; + this.rawIndexTargets = rawIndexTargets; + this.attrs = attrs; + this.ifNotExists = ifNotExists; + } + + public Keyspaces apply(Keyspaces schema) + { + attrs.validate(); + + KeyspaceMetadata keyspace = schema.getNullable(keyspaceName); + if (null == keyspace) + throw ire("Keyspace '%s' doesn't exist", keyspaceName); + + TableMetadata table = keyspace.getTableOrViewNullable(tableName); + if (null == table) + throw ire("Table '%s' doesn't exist", tableName); + + if (null != indexName && keyspace.hasIndex(indexName)) + { + if (ifNotExists) + return schema; + + throw ire("Index '%s' already exists", indexName); + } + + if (table.isCounter()) + throw ire("Secondary indexes on counter tables aren't supported"); + + if (table.isView()) + throw ire("Secondary indexes on materialized views aren't supported"); + + if (table.isCompactTable() && !table.isStaticCompactTable()) + throw ire("Secondary indexes are not supported on COMPACT STORAGE tables that have clustering columns"); + + List<IndexTarget> indexTargets = Lists.transform(rawIndexTargets, t -> t.prepare(table)); + + if (indexTargets.isEmpty() && !attrs.isCustom) + throw ire("Only CUSTOM indexes can be created without specifying a target column"); + + if (indexTargets.size() > 1) + { + if (!attrs.isCustom) + throw ire("Only CUSTOM indexes support multiple columns"); + + Set<ColumnIdentifier> columns = new HashSet<>(); + for (IndexTarget target : indexTargets) + if (!columns.add(target.column)) + throw ire("Duplicate column '%s' in index target list", target.column); + } + + indexTargets.forEach(t -> validateIndexTarget(table, t)); + + String name = null == indexName ? generateIndexName(keyspace, indexTargets) : indexName; + + IndexMetadata.Kind kind; + if (attrs.isCustom) + kind = IndexMetadata.Kind.CUSTOM; + else + kind = table.isCompound() ? IndexMetadata.Kind.COMPOSITES : IndexMetadata.Kind.KEYS; + + Map<String, String> options = attrs.isCustom ? attrs.getOptions() : Collections.emptyMap(); + + IndexMetadata index = IndexMetadata.fromIndexTargets(indexTargets, name, kind, options); + + // check to disallow creation of an index which duplicates an existing one in all but name + IndexMetadata equalIndex = tryFind(table.indexes, i -> i.equalsWithoutName(index)).orNull(); + if (null != equalIndex) + { + if (ifNotExists) + return schema; + + throw ire("Index %s is a duplicate of existing index %s", index.name, equalIndex.name); + } + + TableMetadata newTable = table.withSwapped(table.indexes.with(index)); + newTable.validate(); + + return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.tables.withSwapped(newTable))); + } + + private void validateIndexTarget(TableMetadata table, IndexTarget target) + { + ColumnMetadata column = table.getColumn(target.column); + + if (null == column) + throw ire("Column '%s' doesn't exist", target.column); + + if (column.type.referencesDuration()) + { + if (column.type.isCollection()) + throw ire("Secondary indexes are not supported on collections containing durations"); + + if (column.type.isTuple()) + throw ire("Secondary indexes are not supported on tuples containing durations"); + + if (column.type.isUDT()) + throw ire("Secondary indexes are not supported on UDTs containing durations"); + + throw ire("Secondary indexes are not supported on duration columns"); + } + + // TODO: we could lift that limitation? + if (table.isCompactTable() && column.isPrimaryKeyColumn()) + throw ire("Secondary indexes are not supported on PRIMARY KEY columns in COMPACT STORAGE tables"); + + if (column.isPartitionKey() && table.partitionKeyColumns().size() == 1) + throw ire("Cannot create secondary index on the only partition key column %s", column); + + if (column.type.isFrozenCollection() && target.type != Type.FULL) + throw ire("Cannot create %s() index on frozen column %s. Frozen collections only support full() indexes", target.type, column); + + if (!column.type.isFrozenCollection() && target.type == Type.FULL) + throw ire("full() indexes can only be created on frozen collections"); + + if (!column.type.isCollection() && target.type != Type.SIMPLE) + throw ire("Cannot create %s() index on %s. Non-collection columns only support simple indexes", target.type, column); + + if (!(column.type instanceof MapType && column.type.isMultiCell()) && (target.type == Type.KEYS || target.type == Type.KEYS_AND_VALUES)) + throw ire("Cannot create index on %s of column %s with non-map type", target.type, column); + + if (column.type.isUDT() && column.type.isMultiCell()) + throw ire("Cannot create index on non-frozen UDT column %s", column); + } + + private String generateIndexName(KeyspaceMetadata keyspace, List<IndexTarget> targets) + { + String baseName = targets.size() == 1 + ? IndexMetadata.generateDefaultIndexName(tableName, targets.get(0).column) + : IndexMetadata.generateDefaultIndexName(tableName); + return keyspace.findAvailableIndexName(baseName); + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + return new SchemaChange(Change.UPDATED, Target.TABLE, keyspaceName, tableName); + } + + public void authorize(ClientState client) + { + client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER); + } + + public static final class Raw extends CQLStatement.Raw + { + private final QualifiedName tableName; + private final QualifiedName indexName; + private final List<IndexTarget.Raw> rawIndexTargets; + private final IndexAttributes attrs; + private final boolean ifNotExists; + + public Raw(QualifiedName tableName, + QualifiedName indexName, + List<IndexTarget.Raw> rawIndexTargets, + IndexAttributes attrs, + boolean ifNotExists) + { + this.tableName = tableName; + this.indexName = indexName; + this.rawIndexTargets = rawIndexTargets; + this.attrs = attrs; + this.ifNotExists = ifNotExists; + } + + public CreateIndexStatement prepare(ClientState state) + { + String keyspaceName = tableName.hasKeyspace() + ? tableName.getKeyspace() + : indexName.hasKeyspace() ? indexName.getKeyspace() : state.getKeyspace(); + + if (tableName.hasKeyspace() && !keyspaceName.equals(tableName.getKeyspace())) + throw ire("Keyspace name '%s' doesn't match table name '%s'", keyspaceName, tableName); + + if (indexName.hasKeyspace() && !keyspaceName.equals(indexName.getKeyspace())) + throw ire("Keyspace name '%s' doesn't match index name '%s'", keyspaceName, tableName); + + return new CreateIndexStatement(keyspaceName, tableName.getName(), indexName.getName(), rawIndexTargets, attrs, ifNotExists); + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/aaddbd49/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java new file mode 100644 index 0000000..5ea0859 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateKeyspaceStatement.java @@ -0,0 +1,108 @@ +/* + * 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 java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.apache.cassandra.auth.*; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.statements.KeyspaceAttributes; +import org.apache.cassandra.exceptions.AlreadyExistsException; +import org.apache.cassandra.locator.LocalStrategy; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams.Option; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.Event.SchemaChange; +import org.apache.cassandra.transport.Event.SchemaChange.Change; + +public final class CreateKeyspaceStatement extends AlterSchemaStatement +{ + private final KeyspaceAttributes attrs; + private final boolean ifNotExists; + + public CreateKeyspaceStatement(String keyspaceName, KeyspaceAttributes attrs, boolean ifNotExists) + { + super(keyspaceName); + this.attrs = attrs; + this.ifNotExists = ifNotExists; + } + + public Keyspaces apply(Keyspaces schema) + { + attrs.validate(); + + if (!attrs.hasOption(Option.REPLICATION)) + throw ire("Missing mandatory option '%s'", Option.REPLICATION); + + if (schema.containsKeyspace(keyspaceName)) + { + if (ifNotExists) + return schema; + + throw new AlreadyExistsException(keyspaceName); + } + + KeyspaceMetadata keyspace = KeyspaceMetadata.create(keyspaceName, attrs.asNewKeyspaceParams()); + + if (keyspace.params.replication.klass.equals(LocalStrategy.class)) + throw ire("Unable to use given strategy class: LocalStrategy is reserved for internal use."); + + keyspace.params.validate(keyspaceName); + + return schema.withAddedOrUpdated(keyspace); + } + + SchemaChange schemaChangeEvent(KeyspacesDiff diff) + { + return new SchemaChange(Change.CREATED, keyspaceName); + } + + public void authorize(ClientState client) + { + client.ensureAllKeyspacesPermission(Permission.CREATE); + } + + @Override + Set<IResource> createdResources(KeyspacesDiff diff) + { + return ImmutableSet.of(DataResource.keyspace(keyspaceName), FunctionResource.keyspace(keyspaceName)); + } + + public static final class Raw extends CQLStatement.Raw + { + public final String keyspaceName; + private final KeyspaceAttributes attrs; + private final boolean ifNotExists; + + public Raw(String keyspaceName, KeyspaceAttributes attrs, boolean ifNotExists) + { + this.keyspaceName = keyspaceName; + this.attrs = attrs; + this.ifNotExists = ifNotExists; + } + + public CreateKeyspaceStatement prepare(ClientState state) + { + return new CreateKeyspaceStatement(keyspaceName, attrs, ifNotExists); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org