This is an automated email from the ASF dual-hosted git repository. blerer pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push: new f7c71f6 Allow DELETE and TRUNCATE to work on Virtual Tables if the implementation allows it f7c71f6 is described below commit f7c71f65c000c2c3ef7df1b034b8fdd822a396d8 Author: Aleksei Zotov <azotc...@gmail.com> AuthorDate: Fri Jul 23 21:45:12 2021 +0400 Allow DELETE and TRUNCATE to work on Virtual Tables if the implementation allows it patch by Aleksei Zoto; reviewed by Benjamin Lerer and Chris Lohfink for CASSANDRA-16806 --- CHANGES.txt | 3 +- doc/source/new/virtualtables.rst | 22 +- .../cassandra/cql3/statements/DeleteStatement.java | 4 +- .../cql3/statements/ModificationStatement.java | 1 + .../cql3/statements/TruncateStatement.java | 31 +- .../db/virtual/AbstractMutableVirtualTable.java | 398 ++++++++++ .../cassandra/db/virtual/AbstractVirtualTable.java | 8 +- .../apache/cassandra/db/virtual/VirtualTable.java | 7 +- .../cql3/validation/entities/VirtualTableTest.java | 815 ++++++++++++++++++--- 9 files changed, 1174 insertions(+), 115 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6b95485..3fb4df5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.1 + * Allow DELETE and TRUNCATE to work on Virtual Tables if the implementation allows it (CASSANDRA-16806) * Include SASI components to snapshots (CASSANDRA-15134) * Fix missed wait latencies in the output of `nodetool tpstats -F` (CASSANDRA-16938) * Reduce native transport max frame size to 16MB (CASSANDRA-16886) @@ -38,7 +39,7 @@ Merged from 3.0: 4.0.1 * Tolerate missing DNS entry when completing a host replacement (CASSANDRA-16873) - * Harden PrunableArrayQueue against Pruner implementations that might throw exceptions (CASSANDRA-16866) + * Harden PrunableArrayQueue against Pruner implementations that might throw exceptions (CASSANDRA-16866) * Move RepairedDataInfo to the execution controller rather than the ReadCommand to avoid unintended sharing (CASSANDRA-16721) * Bump zstd-jni version to 1.5.0-4 (CASSANDRA-16884) * Remove assumption that all urgent messages are small (CASSANDRA-16877) diff --git a/doc/source/new/virtualtables.rst b/doc/source/new/virtualtables.rst index 1c8766c..0cb988f 100644 --- a/doc/source/new/virtualtables.rst +++ b/doc/source/new/virtualtables.rst @@ -38,15 +38,15 @@ How are Virtual Tables different from regular tables? Virtual tables and virtual keyspaces are quite different from regular tables and keyspaces respectively such as: -- Virtual tables are read-only, but it is likely to change +- Virtual tables support modifications only if the underlaying implementation allows it - Virtual tables are not replicated - Virtual tables are local only and non distributed - Virtual tables have no associated SSTables - Consistency level of the queries sent virtual tables are ignored -- Virtual tables are managed by Cassandra and a user cannot run DDL to create new virtual tables or DML to modify existing virtual tables +- Virtual tables are managed by Cassandra and a user cannot run DDL to create new virtual tables to modify existing virtual tables - Virtual tables are created in special keyspaces and not just any keyspace -- All existing virtual tables use ``LocalPartitioner``. Since a virtual table is not replicated the partitioner sorts in order of partition keys instead of by their hash. -- Making advanced queries with ``ALLOW FILTERING`` and aggregation functions may be used with virtual tables even though in normal tables we don't recommend it +- All existing virtual tables use ``LocalPartitioner``. Since a virtual table is not replicated the partitioner sorts in order of partition keys instead of by their hash. +- Making advanced queries with ``ALLOW FILTERING`` and aggregation functions may be used with virtual tables even though in normal tables we don't recommend it Virtual Keyspaces ^^^^^^^^^^^^^^^^^ @@ -66,21 +66,21 @@ Virtual Table Limitations Virtual tables and virtual keyspaces have some limitations initially though some of these could change such as: -- Cannot alter or drop virtual keyspaces or tables -- Cannot truncate virtual tables - Expiring columns are not supported by virtual tables +- Custom timestamps are not supported by virtual tables - Conditional updates are not supported by virtual tables -- Cannot create tables in virtual keyspaces -- Cannot perform any operations against virtual keyspace - Secondary indexes are not supported on virtual tables -- Cannot create functions in virtual keyspaces -- Cannot create types in virtual keyspaces - Materialized views are not supported on virtual tables -- Virtual tables don't support ``DELETE`` statements +- Virtual tables support modifications only if the underlaying implementation allows it - Cannot ``CREATE TRIGGER`` against a virtual table - Conditional ``BATCH`` statements cannot include mutations for virtual tables - Cannot include a virtual table statement in a logged batch - Mutations for virtual and regular tables cannot exist in the same batch +- Cannot alter or drop virtual keyspaces or tables +- Cannot create functions in virtual keyspaces +- Cannot create types in virtual keyspaces +- Cannot create tables in virtual keyspaces +- Cannot perform any operations against virtual keyspace - Cannot create aggregates in virtual keyspaces; but may run aggregate functions on select Listing and Describing Virtual Tables diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index 1a92196..9ac29a0 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -145,8 +145,6 @@ public class DeleteStatement extends ModificationStatement Conditions conditions, Attributes attrs) { - checkFalse(metadata.isVirtual(), "Virtual tables don't support DELETE statements"); - Operations operations = new Operations(type); for (Operation.RawDeletion deletion : deletions) @@ -177,6 +175,8 @@ public class DeleteStatement extends ModificationStatement if (stmt.hasConditions() && !restrictions.hasAllPKColumnsRestrictedByEqualities()) { + checkFalse(stmt.isVirtual(), "DELETE statements must restrict all PRIMARY KEY columns with equality relations"); + checkFalse(operations.appliesToRegularColumns(), "DELETE statements must restrict all PRIMARY KEY columns with equality relations in order to delete non static columns"); diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 087f3b0..4ff9928 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -266,6 +266,7 @@ public abstract class ModificationStatement implements CQLStatement checkFalse(isCounter() && attrs.isTimestampSet(), "Cannot provide custom timestamp for counter updates"); checkFalse(isCounter() && attrs.isTimeToLiveSet(), "Cannot provide custom TTL for counter updates"); checkFalse(isView(), "Cannot directly modify a materialized view"); + checkFalse(isVirtual() && attrs.isTimestampSet(), "Custom timestamp is not supported by virtual tables"); checkFalse(isVirtual() && attrs.isTimeToLiveSet(), "Expiring columns are not supported by virtual tables"); checkFalse(isVirtual() && hasConditions(), "Conditional updates are not supported by virtual tables"); } diff --git a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java index 206d116..2c1c994 100644 --- a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java @@ -25,8 +25,10 @@ import org.apache.cassandra.auth.Permission; import org.apache.cassandra.cql3.*; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.exceptions.*; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; @@ -66,9 +68,13 @@ public class TruncateStatement extends QualifiedStatement implements CQLStatemen throw new InvalidRequestException("Cannot TRUNCATE materialized view directly; must truncate base table instead"); if (metaData.isVirtual()) - throw new InvalidRequestException("Cannot truncate virtual tables"); - - StorageProxy.truncateBlocking(keyspace(), name()); + { + executeForVirtualTable(metaData.id); + } + else + { + StorageProxy.truncateBlocking(keyspace(), name()); + } } catch (UnavailableException | TimeoutException e) { @@ -86,10 +92,14 @@ public class TruncateStatement extends QualifiedStatement implements CQLStatemen throw new InvalidRequestException("Cannot TRUNCATE materialized view directly; must truncate base table instead"); if (metaData.isVirtual()) - throw new InvalidRequestException("Cannot truncate virtual tables"); - - ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(name()); - cfs.truncateBlocking(); + { + executeForVirtualTable(metaData.id); + } + else + { + ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(name()); + cfs.truncateBlocking(); + } } catch (Exception e) { @@ -97,7 +107,12 @@ public class TruncateStatement extends QualifiedStatement implements CQLStatemen } return null; } - + + private void executeForVirtualTable(TableId id) + { + VirtualKeyspaceRegistry.instance.getTableNullable(id).truncate(); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java b/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java new file mode 100644 index 0000000..c273e51 --- /dev/null +++ b/src/java/org/apache/cassandra/db/virtual/AbstractMutableVirtualTable.java @@ -0,0 +1,398 @@ +/* + * 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.db.virtual; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Optional; + +import com.google.common.base.Preconditions; +import com.google.common.collect.BoundType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; + +import org.apache.commons.lang.ArrayUtils; + +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; + +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +/** + * An abstract virtual table implementation that builds the resultset on demand and allows fine-grained source + * modification via INSERT/UPDATE, DELETE and TRUNCATE operations. + * + * Virtual table implementation need to be thread-safe has they can be called from different threads. + */ +public abstract class AbstractMutableVirtualTable extends AbstractVirtualTable +{ + + protected AbstractMutableVirtualTable(TableMetadata metadata) + { + super(metadata); + } + + @Override + public final void apply(PartitionUpdate update) + { + ColumnValues partitionKey = ColumnValues.from(metadata(), update.partitionKey()); + + if (update.deletionInfo().isLive()) + update.forEach(row -> + { + ColumnValues clusteringColumns = ColumnValues.from(metadata(), row.clustering()); + + if (row.deletion().isLive()) + { + if (row.columnCount() == 0) + { + applyColumnUpdate(partitionKey, clusteringColumns, Optional.empty()); + } + else + { + row.forEach(columnData -> + { + checkFalse(columnData.column().isComplex(), "Complex type columns are not supported by table %s", metadata); + + Cell<?> cell = (Cell<?>) columnData; + + if (cell.isTombstone()) + applyColumnDeletion(partitionKey, clusteringColumns, columnName(cell)); + else + applyColumnUpdate(partitionKey, + clusteringColumns, + Optional.of(ColumnValue.from(cell))); + }); + } + } + else + applyRowDeletion(partitionKey, clusteringColumns); + }); + else + { + // MutableDeletionInfo may have partition delete or range tombstone list or both + if (update.deletionInfo().hasRanges()) + update.deletionInfo() + .rangeIterator(false) + .forEachRemaining(rt -> applyRangeTombstone(partitionKey, toRange(rt.deletedSlice()))); + + if (!update.deletionInfo().getPartitionDeletion().isLive()) + applyPartitionDeletion(partitionKey); + } + } + + protected void applyPartitionDeletion(ColumnValues partitionKey) + { + throw invalidRequest("Partition deletion is not supported by table %s", metadata); + } + + private Range<ColumnValues> toRange(Slice slice) + { + ClusteringBound<?> startBound = slice.start(); + ClusteringBound<?> endBound = slice.end(); + + if (startBound.isBottom()) + { + if (endBound.isTop()) + return Range.all(); + + return Range.upTo(ColumnValues.from(metadata(), endBound), boundType(endBound)); + } + + if (endBound.isTop()) + return Range.downTo(ColumnValues.from(metadata(), startBound), boundType(startBound)); + + ColumnValues start = ColumnValues.from(metadata(), startBound); + BoundType startType = boundType(startBound); + + ColumnValues end = ColumnValues.from(metadata(), endBound); + BoundType endType = boundType(endBound); + + return Range.range(start, startType, end, endType); + } + + private static BoundType boundType(ClusteringBound<?> bound) + { + return bound.isInclusive() ? BoundType.CLOSED : BoundType.OPEN; + } + + protected void applyRangeTombstone(ColumnValues partitionKey, Range<ColumnValues> range) + { + throw invalidRequest("Range deletion is not supported by table %s", metadata); + } + + protected void applyRowDeletion(ColumnValues partitionKey, ColumnValues clusteringColumnValues) + { + throw invalidRequest("Row deletion is not supported by table %s", metadata); + } + + protected void applyColumnDeletion(ColumnValues partitionKey, ColumnValues clusteringColumns, String columnName) + { + throw invalidRequest("Column deletion is not supported by table %s", metadata); + } + + protected void applyColumnUpdate(ColumnValues partitionKey, + ColumnValues clusteringColumns, + Optional<ColumnValue> columnValue) + { + throw invalidRequest("Column modification is not supported by table %s", metadata); + } + + private static String columnName(Cell<?> cell) + { + return cell.column().name.toCQLString(); + } + + /** + * A set of partition key or clustering column values. + */ + public static final class ColumnValues implements Comparable<ColumnValues> + { + /** + * An empty set of column values. + */ + private static final ColumnValues EMPTY = new ColumnValues(ImmutableList.of(), ArrayUtils.EMPTY_OBJECT_ARRAY); + + /** + * The column metadata for the set of columns. + */ + private final ImmutableList<ColumnMetadata> metadata; + + /** + * The column values. The number of values can be smaller than the number of values if only + * a sub-set of the column values is specified (e.g. clustering prefix). + */ + private final Object[] values; + + /** + * Returns the set of column values corresponding to the specified partition key. + * + * @param metadata the table metadata + * @param partitionKey the partition key + * @return the set of columns values corresponding to the specified partition key + */ + public static ColumnValues from(TableMetadata metadata, DecoratedKey partitionKey) + { + if (metadata.partitionKeyType instanceof CompositeType) + { + ByteBuffer[] buffers = ((CompositeType) metadata.partitionKeyType).split(partitionKey.getKey()); + return ColumnValues.from(metadata.partitionKeyColumns(), buffers); + } + + return ColumnValues.from(metadata.partitionKeyColumns(), partitionKey.getKey()); + } + + /** + * Returns the set of column values corresponding to the specified clustering prefix. + * + * @param metadata the table metadata + * @param prefix the clustering prefix + * @return the set of columns values corresponding to the specified clustering prefix + */ + public static ColumnValues from(TableMetadata metadata, ClusteringPrefix<?> prefix) + { + if (prefix == Clustering.EMPTY) + return EMPTY; + + return ColumnValues.from(metadata.clusteringColumns(), prefix.getBufferArray()); + } + + private static ColumnValues from(ImmutableList<ColumnMetadata> metadata, ByteBuffer... buffers) + { + return new ColumnValues(metadata, convert(metadata, buffers)); + } + + /** + * Create a {@code ColumnValues} for the specified set of columns. + * + * @param metadata the partition or clustering columns metadata + * @param values the partition or clustering column values + */ + public ColumnValues(List<ColumnMetadata> metadata, Object... values) + { + this.metadata = ImmutableList.copyOf(metadata); + this.values = values; + } + + /** + * Deserializes the column values. + * + * @param metadata the column metadata + * @param buffers the serialized column values + * @return the deserialized column values + */ + private static Object[] convert(ImmutableList<ColumnMetadata> metadata, ByteBuffer[] buffers) + { + Object[] values = new Object[buffers.length]; + for (int i = 0; i < buffers.length; i++) + { + values[i] = metadata.get(i).type.compose(buffers[i]); + } + return values; + } + + /** + * Returns the name of the specified column + * + * @param i the column index + * @return the column name + */ + public String name(int i) + { + Preconditions.checkPositionIndex(i, values.length); + return metadata.get(i).name.toCQLString(); + } + + /** + * Returns the value for the specified column + * + * @param i the column index + * @return the column value + */ + @SuppressWarnings("unchecked") + public <V> V value(int i) + { + Preconditions.checkPositionIndex(i, values.length); + return (V) values[i]; + } + + /** + * Returns the number of column values. + * + * @return the number of column values. + */ + public int size() + { + return values.length; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append('['); + for (int i = 0, m = metadata.size(); i <m; i++) + { + if (i != 0) + builder.append(", "); + + builder.append(metadata.get(i).name.toCQLString()) + .append(" : "); + + if (i < values.length) + builder.append(i < values.length ? values[i].toString() : "unspecified"); + } + return builder.append(']').toString(); + } + + @Override + public int compareTo(ColumnValues o) + { + assert metadata.equals(o.metadata); + + int s1 = size(); + int s2 = o.size(); + int minSize = Math.min(s1, s2); + + for (int i = 0; i < minSize; i++) + { + int cmp = compare(values[i], o.values[i]); + if (cmp != 0) + return cmp; + } + + return 0; + } + + @SuppressWarnings("unchecked") + private <T extends Comparable<T>> int compare(Object c1, Object c2) + { + return ((T) c1).compareTo((T) c2); + } + } + + /** + * A regular column value. + */ + public static final class ColumnValue + { + /** + * The column metadata + */ + private final ColumnMetadata metadata; + + /** + * The column value + */ + private final Object value; + + /** + * Returns the column value corresponding to the specified cell. + * + * @param cell the column cell metadata + * @return the column value corresponding to the specified cell + */ + public static ColumnValue from(Cell<?> cell) + { + ColumnMetadata metadata = cell.column(); + return new ColumnValue(metadata, metadata.type.compose(cell.buffer())); + } + + private ColumnValue(ColumnMetadata metadata, Object value) + { + this.metadata = metadata; + this.value = value; + } + + /** + * Returns the column name. + * + * @return the column name + */ + public String name() + { + return metadata.name.toCQLString(); + } + + /** + * Returns the column value. + * + * @return the column value + */ + @SuppressWarnings("unchecked") + public <V> V value() + { + return (V) value; + } + + @Override + public String toString() + { + return String.format("%s : %s", name(), value()); + } + } +} diff --git a/src/java/org/apache/cassandra/db/virtual/AbstractVirtualTable.java b/src/java/org/apache/cassandra/db/virtual/AbstractVirtualTable.java index c2de1db..96fb7f9 100644 --- a/src/java/org/apache/cassandra/db/virtual/AbstractVirtualTable.java +++ b/src/java/org/apache/cassandra/db/virtual/AbstractVirtualTable.java @@ -48,7 +48,7 @@ public abstract class AbstractVirtualTable implements VirtualTable protected AbstractVirtualTable(TableMetadata metadata) { if (!metadata.isVirtual()) - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Cannot instantiate a non-virtual table"); this.metadata = metadata; } @@ -127,6 +127,12 @@ public abstract class AbstractVirtualTable implements VirtualTable throw new InvalidRequestException("Modification is not supported by table " + metadata); } + @Override + public void truncate() + { + throw new InvalidRequestException("Truncation is not supported by table " + metadata); + } + public interface DataSet { boolean isEmpty(); diff --git a/src/java/org/apache/cassandra/db/virtual/VirtualTable.java b/src/java/org/apache/cassandra/db/virtual/VirtualTable.java index ea196ca..5373f4c 100644 --- a/src/java/org/apache/cassandra/db/virtual/VirtualTable.java +++ b/src/java/org/apache/cassandra/db/virtual/VirtualTable.java @@ -48,7 +48,7 @@ public interface VirtualTable TableMetadata metadata(); /** - * Applies the specified update. + * Applies the specified update, if supported. * @param update the update to apply */ void apply(PartitionUpdate update); @@ -71,4 +71,9 @@ public interface VirtualTable * @return the rows corresponding to the requested data. */ UnfilteredPartitionIterator select(DataRange dataRange, ColumnFilter columnFilter); + + /** + * Truncates data from the underlying source, if supported. + */ + void truncate(); } diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java index 9808c96..5d3b134 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/VirtualTableTest.java @@ -18,10 +18,23 @@ package org.apache.cassandra.cql3.validation.entities; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.NavigableMap; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.Nonnull; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; + +import org.apache.commons.lang3.tuple.Pair; + import org.junit.BeforeClass; import org.junit.Test; @@ -31,18 +44,18 @@ import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.LongType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.partitions.Partition; -import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.db.virtual.AbstractMutableVirtualTable; import org.apache.cassandra.db.virtual.AbstractVirtualTable; import org.apache.cassandra.db.virtual.SimpleDataSet; import org.apache.cassandra.db.virtual.VirtualKeyspace; import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry; import org.apache.cassandra.db.virtual.VirtualTable; -import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.StorageServiceMBean; import org.apache.cassandra.triggers.ITrigger; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -52,53 +65,152 @@ public class VirtualTableTest extends CQLTester private static final String VT1_NAME = "vt1"; private static final String VT2_NAME = "vt2"; private static final String VT3_NAME = "vt3"; + private static final String VT4_NAME = "vt4"; - private static class WritableVirtualTable extends AbstractVirtualTable + // As long as we execute test queries using execute (and not executeNet) the virtual tables implementation + // do not need to be thread-safe. We choose to do it to avoid issues if the test framework was changed or somebody + // decided to use the class with executeNet. It also provide a better example in case somebody is looking + // at the test for learning how to create mutable virtual tables + private static class MutableVirtualTable extends AbstractMutableVirtualTable { - private final ColumnMetadata valueColumn; - private final Map<String, Integer> backingMap = new HashMap<>(); + // <pk1, pk2> -> c1 -> c2 -> <v1, v2> + private final Map<Pair<String, String>, NavigableMap<String, NavigableMap<String, Pair<Number, Number>>>> backingMap = new ConcurrentHashMap<>(); - WritableVirtualTable(String keyspaceName, String tableName) + MutableVirtualTable(String keyspaceName, String tableName) { super(TableMetadata.builder(keyspaceName, tableName) .kind(TableMetadata.Kind.VIRTUAL) - .addPartitionKeyColumn("key", UTF8Type.instance) - .addRegularColumn("value", Int32Type.instance) + .addPartitionKeyColumn("pk1", UTF8Type.instance) + .addPartitionKeyColumn("pk2", UTF8Type.instance) + .addClusteringColumn("c1", UTF8Type.instance) + .addClusteringColumn("c2", UTF8Type.instance) + .addRegularColumn("v1", Int32Type.instance) + .addRegularColumn("v2", LongType.instance) .build()); - valueColumn = metadata().regularColumns().getSimple(0); } @Override public DataSet data() { SimpleDataSet data = new SimpleDataSet(metadata()); - backingMap.forEach((key, value) -> data.row(key).column("value", value)); + backingMap.forEach((pkPair, c1Map) -> + c1Map.forEach((c1, c2Map) -> + c2Map.forEach((c2, valuePair) -> data.row(pkPair.getLeft(), pkPair.getRight(), c1, c2) + .column("v1", valuePair.getLeft()) + .column("v2", valuePair.getRight())))); return data; } @Override - public void apply(PartitionUpdate update) + protected void applyPartitionDeletion(ColumnValues partitionKeyColumns) + { + backingMap.remove(toPartitionKey(partitionKeyColumns)); + } + + @Override + protected void applyRangeTombstone(ColumnValues partitionKeyColumns, Range<ColumnValues> range) + { + Optional<NavigableMap<String, NavigableMap<String, Pair<Number, Number>>>> mayBePartition = getPartition(partitionKeyColumns); + + if (!mayBePartition.isPresent()) + return; + + NavigableMap<String, NavigableMap<String, Pair<Number, Number>>> selection = mayBePartition.get(); + + for (String c1 : ImmutableList.copyOf(selection.keySet())) + { + NavigableMap<String, Pair<Number, Number>> rows = selection.get(c1); + + for (String c2 : ImmutableList.copyOf(selection.get(c1).keySet())) + { + if (range.contains(new ColumnValues(metadata().clusteringColumns(), c1, c2))) + rows.remove(c2); + } + + if (rows.isEmpty()) + selection.remove(c1); + } + } + + @Override + protected void applyRowDeletion(ColumnValues partitionKeyColumns, ColumnValues clusteringColumns) + { + getRows(partitionKeyColumns, clusteringColumns.value(0)).ifPresent(rows -> rows.remove(clusteringColumns.value(1))); + } + + @Override + protected void applyColumnDeletion(ColumnValues partitionKeyColumns, ColumnValues clusteringColumns, String columnName) + { + getRows(partitionKeyColumns, clusteringColumns.value(0)).ifPresent(rows -> rows.computeIfPresent(clusteringColumns.value(1), + (c, p) -> updateColumn(p, columnName, null))); + } + + @Override + protected void applyColumnUpdate(ColumnValues partitionKeyColumns, + ColumnValues clusteringColumns, + Optional<ColumnValue> mayBeColumnValue) { - String key = (String) metadata().partitionKeyType.compose(update.partitionKey().getKey()); - update.forEach(row -> - { - Integer value = Int32Type.instance.compose(row.getCell(valueColumn).buffer()); - backingMap.put(key, value); - }); + Pair<String, String> pkPair = toPartitionKey(partitionKeyColumns); + backingMap.computeIfAbsent(pkPair, ignored -> new ConcurrentSkipListMap<>()) + .computeIfAbsent(clusteringColumns.value(0), ignored -> new ConcurrentSkipListMap<>()) + .compute(clusteringColumns.value(1), (ignored, p) -> updateColumn(p, mayBeColumnValue)); + } + + @Override + public void truncate() + { + backingMap.clear(); + } + + private Optional<NavigableMap<String, Pair<Number, Number>>> getRows(ColumnValues partitionKeyColumns, Comparable<?> firstClusteringColumn) + { + return getPartition(partitionKeyColumns).map(p -> p.get(firstClusteringColumn)); + } + + private Optional<NavigableMap<String, NavigableMap<String, Pair<Number, Number>>>> getPartition(ColumnValues partitionKeyColumns) + { + Pair<String, String> pk = toPartitionKey(partitionKeyColumns); + return Optional.ofNullable(backingMap.get(pk)); + } + + private Pair<String, String> toPartitionKey(ColumnValues partitionKey) + { + return Pair.of(partitionKey.value(0), partitionKey.value(1)); + } + + private static Pair<Number, Number> updateColumn(@Nonnull Pair<Number, Number> row, + String columnName, + Number newValue) + { + return "v1".equals(columnName) ? Pair.of(newValue, row.getRight()) + : Pair.of(row.getLeft(), newValue); + } + + private static Pair<Number, Number> updateColumn(Pair<Number, Number> row, + Optional<ColumnValue> mayBeColumnValue) + { + Pair<Number, Number> r = row != null ? row : Pair.of(null, null); + + if (mayBeColumnValue.isPresent()) + { + ColumnValue newValue = mayBeColumnValue.get(); + return updateColumn(r, newValue.name(), newValue.value()); + } + + return r; } } @BeforeClass public static void setUpClass() { - TableMetadata vt1Metadata = - TableMetadata.builder(KS_NAME, VT1_NAME) - .kind(TableMetadata.Kind.VIRTUAL) - .addPartitionKeyColumn("pk", UTF8Type.instance) - .addClusteringColumn("c", UTF8Type.instance) - .addRegularColumn("v1", Int32Type.instance) - .addRegularColumn("v2", LongType.instance) - .build(); + TableMetadata vt1Metadata = TableMetadata.builder(KS_NAME, VT1_NAME) + .kind(TableMetadata.Kind.VIRTUAL) + .addPartitionKeyColumn("pk", UTF8Type.instance) + .addClusteringColumn("c", UTF8Type.instance) + .addRegularColumn("v1", Int32Type.instance) + .addRegularColumn("v2", LongType.instance) + .build(); SimpleDataSet vt1data = new SimpleDataSet(vt1Metadata); @@ -116,18 +228,17 @@ public class VirtualTableTest extends CQLTester return vt1data; } }; - VirtualTable vt2 = new WritableVirtualTable(KS_NAME, VT2_NAME); - - TableMetadata vt3Metadata = - TableMetadata.builder(KS_NAME, VT3_NAME) - .kind(TableMetadata.Kind.VIRTUAL) - .addPartitionKeyColumn("pk1", UTF8Type.instance) - .addPartitionKeyColumn("pk2", UTF8Type.instance) - .addClusteringColumn("ck1", UTF8Type.instance) - .addClusteringColumn("ck2", UTF8Type.instance) - .addRegularColumn("v1", Int32Type.instance) - .addRegularColumn("v2", LongType.instance) - .build(); + VirtualTable vt2 = new MutableVirtualTable(KS_NAME, VT2_NAME); + + TableMetadata vt3Metadata = TableMetadata.builder(KS_NAME, VT3_NAME) + .kind(TableMetadata.Kind.VIRTUAL) + .addPartitionKeyColumn("pk1", UTF8Type.instance) + .addPartitionKeyColumn("pk2", UTF8Type.instance) + .addClusteringColumn("ck1", UTF8Type.instance) + .addClusteringColumn("ck2", UTF8Type.instance) + .addRegularColumn("v1", Int32Type.instance) + .addRegularColumn("v2", LongType.instance) + .build(); SimpleDataSet vt3data = new SimpleDataSet(vt3Metadata); @@ -141,13 +252,104 @@ public class VirtualTableTest extends CQLTester return vt3data; } }; - VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(vt1, vt2, vt3))); + + TableMetadata vt4Metadata = TableMetadata.builder(KS_NAME, VT4_NAME) + .kind(TableMetadata.Kind.VIRTUAL) + .addPartitionKeyColumn("pk", UTF8Type.instance) + .addRegularColumn("v", LongType.instance) + .build(); + + // As long as we execute test queries using execute (and not executeNet) the virtual tables implementation + // do not need to be thread-safe. We choose to do it to avoid issues if the test framework was changed or somebody + // decided to use the class with executeNet. It also provide a better example in case somebody is looking + // at the test for learning how to create mutable virtual tables + VirtualTable vt4 = new AbstractMutableVirtualTable(vt4Metadata) + { + // CHM cannot be used here as they do not accept null values + private final AtomicReference<Map<String, Long>> table = new AtomicReference<Map<String, Long>>(Collections.emptyMap()); + + @Override + public DataSet data() + { + SimpleDataSet data = new SimpleDataSet(metadata()); + table.get().forEach((pk, v) -> data.row(pk).column("v", v)); + return data; + } + + @Override + protected void applyPartitionDeletion(ColumnValues partitionKey) + { + Map<String, Long> oldMap; + Map<String, Long> newMap; + do + { + oldMap = table.get(); + newMap = new HashMap<>(oldMap); + newMap.remove(partitionKey.value(0)); + } + while(!table.compareAndSet(oldMap, newMap)); + } + + @Override + protected void applyColumnDeletion(ColumnValues partitionKey, + ColumnValues clusteringColumns, + String columnName) + { + Map<String, Long> oldMap; + Map<String, Long> newMap; + do + { + oldMap = table.get(); + + if (!oldMap.containsKey(partitionKey.value(0))) + break; + + newMap = new HashMap<>(oldMap); + newMap.put(partitionKey.value(0), null); + } + while(!table.compareAndSet(oldMap, newMap)); + } + + @Override + protected void applyColumnUpdate(ColumnValues partitionKey, + ColumnValues clusteringColumns, + Optional<ColumnValue> columnValue) + { + Map<String, Long> oldMap; + Map<String, Long> newMap; + do + { + oldMap = table.get(); + if (oldMap.containsKey(partitionKey.value(0)) && !columnValue.isPresent()) + break; + newMap = new HashMap<>(oldMap); + newMap.put(partitionKey.value(0), columnValue.isPresent() ? columnValue.get().value() : null); + } + while(!table.compareAndSet(oldMap, newMap)); + } + + @Override + public void truncate() + { + Map<String, Long> oldMap; + do + { + oldMap = table.get(); + if (oldMap.isEmpty()) + break; + } + while(!table.compareAndSet(oldMap, Collections.emptyMap())); + } + + }; + + VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(vt1, vt2, vt3, vt4))); CQLTester.setUpClass(); } @Test - public void testQueries() throws Throwable + public void testReadOperationsOnReadOnlyTable() throws Throwable { assertRowsNet(executeNet("SELECT * FROM test_virtual_ks.vt1 WHERE pk = 'UNKNOWN'")); @@ -232,7 +434,7 @@ public class VirtualTableTest extends CQLTester } @Test - public void testQueriesOnTableWithMultiplePks() throws Throwable + public void testReadOperationsOnReadOnlyTableWithMultiplePks() throws Throwable { assertRowsNet(executeNet("SELECT * FROM test_virtual_ks.vt3 WHERE pk1 = 'UNKNOWN' AND pk2 = 'UNKNOWN'")); @@ -249,76 +451,510 @@ public class VirtualTableTest extends CQLTester } @Test - public void testModifications() throws Throwable + public void testDMLOperationsOnMutableCompositeTable() throws Throwable + { + // check for a clean state + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + + // fill the table, test UNLOGGED batch + execute("BEGIN UNLOGGED BATCH " + + "UPDATE test_virtual_ks.vt2 SET v1 = 1, v2 = 1 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 2, v2 = 2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 3, v2 = 3 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 4, v2 = 4 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_3';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 5, v2 = 5 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_5';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 6, v2 = 6 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_6';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 7, v2 = 7 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 8, v2 = 8 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_2' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 9, v2 = 9 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 10, v2 = 10 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_2';" + + "APPLY BATCH"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // update a single column with UPDATE + execute("UPDATE test_virtual_ks.vt2 SET v1 = 11 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1'"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 11, 1L)); + + // update multiple columns with UPDATE + execute("UPDATE test_virtual_ks.vt2 SET v1 = 111, v2 = 111 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1'"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 111, 111L)); + + // update a single columns with INSERT + execute("INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2, v2) VALUES ('pk1_1', 'pk2_1', 'c1_1', 'c2_2', 22)"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_2'"), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 22L)); + + // update multiple columns with INSERT + execute("INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2, v1, v2) VALUES ('pk1_1', 'pk2_1', 'c1_1', 'c2_2', 222, 222)"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_2'"), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 222, 222L)); + + // delete a single partition + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // delete a first-level range (one-sided limit) + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 <= 'c1_1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // delete a first-level range (two-sided limit) + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 > 'c1_1' AND c1 < 'c1_3'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // delete multiple rows + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L)); + + // delete a second-level range (one-sided limit) + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 > 'c2_5'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L)); + + // delete a second-level range (two-sided limit) + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 >= 'c2_3' AND c2 < 'c2_5'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L)); + + // delete a single row + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_5'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L)); + + // delete a single column + execute("DELETE v1 FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", null, 3L)); + + // truncate + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + } + + @Test + public void testRangeDeletionWithMulticolumnRestrictionsOnMutableTable() throws Throwable + { + // check for a clean state + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + + // fill the table, test UNLOGGED batch + execute("BEGIN UNLOGGED BATCH " + + "UPDATE test_virtual_ks.vt2 SET v1 = 1, v2 = 1 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 2, v2 = 2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 3, v2 = 3 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 4, v2 = 4 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_3';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 5, v2 = 5 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_5';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 6, v2 = 6 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_6';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 7, v2 = 7 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 8, v2 = 8 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_2' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 9, v2 = 9 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 10, v2 = 10 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 11, v2 = 11 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_3';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 12, v2 = 12 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_4';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 13, v2 = 13 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_5';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 14, v2 = 14 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_3' AND c2 = 'c2_1';" + + "APPLY BATCH"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_2", "c2_4", 12, 12L), + row("pk1_1", "pk2_3", "c1_2", "c2_5", 13, 13L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns equality + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND (c1, c2) = ('c1_1', 'c2_5')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_1", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_2", "c2_4", 12, 12L), + row("pk1_1", "pk2_3", "c1_2", "c2_5", 13, 13L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns with slice on both side of different length + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 >= 'c1_1' AND (c1, c2) <= ('c1_1', 'c2_5')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_2", "c2_4", 12, 12L), + row("pk1_1", "pk2_3", "c1_2", "c2_5", 13, 13L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND (c1, c2) > ('c1_2', 'c2_3') AND (c1) < ('c1_3')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns with slice on both side of different length + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 >= 'c1_1' AND (c1, c2) <= ('c1_1', 'c2_5')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns with only top slice + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND (c1, c2) < ('c1_2', 'c2_2')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_1", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns with only bottom slice + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND (c1, c2) > ('c1_1', 'c2_1')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L), + row("pk1_1", "pk2_3", "c1_3", "c2_1", 14, 14L)); + + // Test deletion with multiple columns IN + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND (c1, c2) IN (('c1_2', 'c2_2'), ('c1_3', 'c2_1'), ('c1_4', 'c2_1'))"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_2", "pk2_1", "c1_1", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_2", "c2_1", 8, 8L), + row("pk1_1", "pk2_3", "c1_2", "c2_3", 11, 11L)); + + // truncate + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + } + + @Test + public void testDMLOperationsOnMutableNonCompositeTable() throws Throwable + { + // check for a clean state + execute("TRUNCATE test_virtual_ks.vt4"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt4")); + + // fill the table, test UNLOGGED batch + execute("BEGIN UNLOGGED BATCH " + + "INSERT INTO test_virtual_ks.vt4 (pk, v) VALUES ('pk1', 1);" + + "INSERT INTO test_virtual_ks.vt4 (pk, v) VALUES ('pk2', 2);" + + "INSERT INTO test_virtual_ks.vt4 (pk, v) VALUES ('pk3', 3);" + + "INSERT INTO test_virtual_ks.vt4 (pk, v) VALUES ('pk4', 4);" + + "APPLY BATCH"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk2", 2L), + row("pk3", 3L), + row("pk4", 4L)); + + execute("UPDATE test_virtual_ks.vt4 SET v = 3 WHERE pk = 'pk1'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 3L), + row("pk2", 2L), + row("pk3", 3L), + row("pk4", 4L)); + + // update a single columns with INSERT + execute("INSERT INTO test_virtual_ks.vt4 (pk, v) VALUES ('pk1', 1);"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk2", 2L), + row("pk3", 3L), + row("pk4", 4L)); + + // update no column via INSERT + execute("INSERT INTO test_virtual_ks.vt4 (pk) VALUES ('pk1');"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk2", 2L), + row("pk3", 3L), + row("pk4", 4L)); + + // insert new primary key only + execute("INSERT INTO test_virtual_ks.vt4 (pk) VALUES ('pk5');"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk2", 2L), + row("pk3", 3L), + row("pk4", 4L), + row("pk5", null)); + + // delete a single partition + execute("DELETE FROM test_virtual_ks.vt4 WHERE pk = 'pk2'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk3", 3L), + row("pk4", 4L), + row("pk5", null)); + + // delete a single column + execute("DELETE v FROM test_virtual_ks.vt4 WHERE pk = 'pk4'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt4"), + row("pk1", 1L), + row("pk3", 3L), + row("pk4", null), + row("pk5", null)); + + // truncate + execute("TRUNCATE test_virtual_ks.vt4"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt4")); + } + + @Test + public void testInsertRowWithoutRegularColumnsOperationOnMutableTable() throws Throwable + { + // check for a clean state + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + + // insert a primary key without columns + execute("INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2) VALUES ('pk1_1', 'pk2_1', 'c1_1', 'c2_2')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_2'"), + row("pk1_1", "pk2_1", "c1_1", "c2_2", null, null)); + + // truncate + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + } + + @Test + public void testDeleteWithInOperationsOnMutableTable() throws Throwable { - // check for clean state - assertRows(execute("SELECT * FROM test_virtual_ks.vt2")); + // check for a clean state + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); // fill the table, test UNLOGGED batch execute("BEGIN UNLOGGED BATCH " + - "UPDATE test_virtual_ks.vt2 SET value = 1 WHERE key ='pk1';" + - "UPDATE test_virtual_ks.vt2 SET value = 2 WHERE key ='pk2';" + - "UPDATE test_virtual_ks.vt2 SET value = 3 WHERE key ='pk3';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 1, v2 = 1 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 2, v2 = 2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 = 'c2_2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 3, v2 = 3 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 4, v2 = 4 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_2' AND c2 = 'c2_3';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 5, v2 = 5 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_1' AND c2 = 'c2_5';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 6, v2 = 6 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 = 'c1_2' AND c2 = 'c2_6';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 7, v2 = 7 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 8, v2 = 8 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 = 'c2_2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 9, v2 = 9 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_1' AND c2 = 'c2_1';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 10, v2 = 10 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 = 'c1_2' AND c2 = 'c2_2';" + "APPLY BATCH"); - assertRows(execute("SELECT * FROM test_virtual_ks.vt2"), - row("pk1", 1), - row("pk2", 2), - row("pk3", 3)); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_1", "pk2_1", "c1_1", "c2_1", 1, 1L), + row("pk1_1", "pk2_2", "c1_1", "c2_2", 2, 2L), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_2", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_1", "c2_2", 8, 8L), + row("pk1_1", "pk2_3", "c1_1", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // delete multiple partitions with IN + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 IN('pk2_1', 'pk2_2')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_2", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_1", "c2_2", 8, 8L), + row("pk1_1", "pk2_3", "c1_1", "c2_1", 9, 9L), + row("pk1_1", "pk2_3", "c1_2", "c2_2", 10, 10L)); + + // delete multiple rows via first-level IN + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2_3' AND c1 IN('c1_1', 'c1_2')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_2", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", 6, 6L), + row("pk1_2", "pk2_2", "c1_1", "c2_1", 7, 7L), + row("pk1_2", "pk2_2", "c1_1", "c2_2", 8, 8L)); + + // delete multiple rows via second-level IN + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_2' AND c1 = 'c1_1' AND c2 IN('c2_1', 'c2_2')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_1", 3, 3L), + row("pk1_2", "pk2_1", "c1_2", "c2_3", 4, 4L), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", 6, 6L)); + + // delete multiple rows with first-level IN and second-level range (one-sided limit) + execute("DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 IN('c1_1', 'c1_2') AND c2 <= 'c2_3'"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_5", 5, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", 6, 6L)); + + // delete multiple rows via first-level and second-level IN + execute("DELETE v1 FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2_1' AND c1 IN('c1_1', 'c1_2') AND c2 IN('c2_5', 'c2_6')"); + assertRowsIgnoringOrder(execute("SELECT * FROM test_virtual_ks.vt2"), + row("pk1_2", "pk2_1", "c1_1", "c2_5", null, 5L), + row("pk1_2", "pk2_1", "c1_2", "c2_6", null, 6L)); + + // truncate + execute("TRUNCATE test_virtual_ks.vt2"); + assertEmpty(execute("SELECT * FROM test_virtual_ks.vt2")); + } - // test that LOGGED batches don't allow virtual table updates + @Test + public void testInvalidDMLOperationsOnMutableTable() throws Throwable + { + // test that LOGGED batch doesn't allow virtual table updates assertInvalidMessage("Cannot include a virtual table statement in a logged batch", - "BEGIN BATCH " + - "UPDATE test_virtual_ks.vt2 SET value = 1 WHERE key ='pk1';" + - "UPDATE test_virtual_ks.vt2 SET value = 2 WHERE key ='pk2';" + - "UPDATE test_virtual_ks.vt2 SET value = 3 WHERE key ='pk3';" + - "APPLY BATCH"); + "BEGIN BATCH " + + "UPDATE test_virtual_ks.vt2 SET v1 = 1 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 3 WHERE pk1 = 'pk1_3' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "APPLY BATCH"); // test that UNLOGGED batch doesn't allow mixing updates for regular and virtual tables - createTable("CREATE TABLE %s (key text PRIMARY KEY, value int)"); + createTable("CREATE TABLE %s (pk1 text, pk2 text, c1 text, c2 text, v1 int, v2 bigint, PRIMARY KEY ((pk1, pk2), c1, c2))"); assertInvalidMessage("Mutations for virtual and regular tables cannot exist in the same batch", - "BEGIN UNLOGGED BATCH " + - "UPDATE test_virtual_ks.vt2 SET value = 1 WHERE key ='pk1'" + - "UPDATE %s SET value = 2 WHERE key ='pk2'" + - "UPDATE test_virtual_ks.vt2 SET value = 3 WHERE key ='pk3'" + - "APPLY BATCH"); - - // update a single value with UPDATE - execute("UPDATE test_virtual_ks.vt2 SET value = 11 WHERE key ='pk1'"); - assertRows(execute("SELECT * FROM test_virtual_ks.vt2 WHERE key = 'pk1'"), - row("pk1", 11)); - - // update a single value with INSERT - executeNet("INSERT INTO test_virtual_ks.vt2 (key, value) VALUES ('pk2', 22)"); - assertRows(execute("SELECT * FROM test_virtual_ks.vt2 WHERE key = 'pk2'"), - row("pk2", 22)); - - // test that deletions are (currently) rejected - assertInvalidMessage("Virtual tables don't support DELETE statements", - "DELETE FROM test_virtual_ks.vt2 WHERE key ='pk1'"); + "BEGIN UNLOGGED BATCH " + + "UPDATE test_virtual_ks.vt2 SET v1 = 1 WHERE pk1 = 'pk1_1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "UPDATE %s SET v1 = 2 WHERE pk1 = 'pk1_2' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "UPDATE test_virtual_ks.vt2 SET v1 = 3 WHERE pk1 = 'pk1_3' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2';" + + "APPLY BATCH"); + + // test that TIMESTAMP is (currently) rejected with INSERT and UPDATE + assertInvalidMessage("Custom timestamp is not supported by virtual tables", + "INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2, v1, v2) VALUES ('pk1', 'pk2', 'c1', 'c2', 1, 11) USING TIMESTAMP 123456789"); + assertInvalidMessage("Custom timestamp is not supported by virtual tables", + "UPDATE test_virtual_ks.vt2 USING TIMESTAMP 123456789 SET v1 = 1, v2 = 11 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2'"); // test that TTL is (currently) rejected with INSERT and UPDATE assertInvalidMessage("Expiring columns are not supported by virtual tables", - "INSERT INTO test_virtual_ks.vt2 (key, value) VALUES ('pk1', 11) USING TTL 86400"); + "INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2, v1, v2) VALUES ('pk1', 'pk2', 'c1', 'c2', 1, 11) USING TTL 86400"); assertInvalidMessage("Expiring columns are not supported by virtual tables", - "UPDATE test_virtual_ks.vt2 USING TTL 86400 SET value = 11 WHERE key ='pk1'"); + "UPDATE test_virtual_ks.vt2 USING TTL 86400 SET v1 = 1, v2 = 11 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2'"); - // test that LWT is (currently) rejected with virtual tables in batches + // test that LWT is (currently) rejected with BATCH assertInvalidMessage("Conditional BATCH statements cannot include mutations for virtual tables", - "BEGIN UNLOGGED BATCH " + - "UPDATE test_virtual_ks.vt2 SET value = 3 WHERE key ='pk3' IF value = 2;" + - "APPLY BATCH"); + "BEGIN UNLOGGED BATCH " + + "UPDATE test_virtual_ks.vt2 SET v1 = 3 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2' IF v1 = 2;" + + "APPLY BATCH"); - // test that LWT is (currently) rejected with virtual tables in UPDATEs + // test that LWT is (currently) rejected with INSERT and UPDATE assertInvalidMessage("Conditional updates are not supported by virtual tables", - "UPDATE test_virtual_ks.vt2 SET value = 3 WHERE key ='pk3' IF value = 2"); - - // test that LWT is (currently) rejected with virtual tables in INSERTs + "INSERT INTO test_virtual_ks.vt2 (pk1, pk2, c1, c2, v1) VALUES ('pk1', 'pk2', 'c1', 'c2', 2) IF NOT EXISTS"); assertInvalidMessage("Conditional updates are not supported by virtual tables", - "INSERT INTO test_virtual_ks.vt2 (key, value) VALUES ('pk2', 22) IF NOT EXISTS"); + "UPDATE test_virtual_ks.vt2 SET v1 = 3 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 = 'c2' IF v1 = 2"); + + // test that row DELETE without full primary key with equality relation is (currently) rejected + assertInvalidMessage("Some partition key parts are missing: pk2", + "DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND c1 = 'c1' AND c2 > 'c2'"); + assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function) for DELETE statements", + "DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 > 'pk2' AND c1 = 'c1' AND c2 > 'c2'"); + assertInvalidMessage("KEY column \"c2\" cannot be restricted as preceding column \"c1\" is not restricted", + "DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c2 > 'c2'"); + assertInvalidMessage("Clustering column \"c2\" cannot be restricted (preceding column \"c1\" is restricted by a non-EQ relation)", + "DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 > 'c1' AND c2 > 'c2'"); + assertInvalidMessage("DELETE statements must restrict all PRIMARY KEY columns with equality relations", + "DELETE FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 > 'c2' IF v1 = 2"); + + // test that column DELETE without full primary key with equality relation is (currently) rejected + assertInvalidMessage("Range deletions are not supported for specific columns", + "DELETE v1 FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1'"); + assertInvalidMessage("Range deletions are not supported for specific columns", + "DELETE v1 FROM test_virtual_ks.vt2 WHERE pk1 = 'pk1' AND pk2 = 'pk2' AND c1 = 'c1' AND c2 > 'c2'"); + } + + @Test + public void testInvalidDMLOperationsOnReadOnlyTable() throws Throwable + { + assertInvalidMessage("Modification is not supported by table test_virtual_ks.vt1", + "INSERT INTO test_virtual_ks.vt1 (pk, c, v1, v2) VALUES ('pk1_1', 'ck1_1', 11, 11)"); + + assertInvalidMessage("Modification is not supported by table test_virtual_ks.vt1", + "UPDATE test_virtual_ks.vt1 SET v1 = 11, v2 = 11 WHERE pk = 'pk1_1' AND c = 'ck1_1'"); + + assertInvalidMessage("Modification is not supported by table test_virtual_ks.vt1", + "DELETE FROM test_virtual_ks.vt1 WHERE pk = 'pk1_1' AND c = 'ck1_1'"); + + assertInvalidMessage("Error during truncate: Truncation is not supported by table test_virtual_ks.vt1", + "TRUNCATE TABLE test_virtual_ks.vt1"); } @Test - public void testInvalidDDLOperations() throws Throwable + public void testInvalidDDLOperationsOnVirtualKeyspaceAndReadOnlyTable() throws Throwable { assertInvalidMessage("Virtual keyspace 'test_virtual_ks' is not user-modifiable", "DROP KEYSPACE test_virtual_ks"); @@ -338,9 +974,6 @@ public class VirtualTableTest extends CQLTester assertInvalidMessage("Virtual keyspace 'test_virtual_ks' is not user-modifiable", "ALTER TABLE test_virtual_ks.vt1 DROP v1"); - assertInvalidMessage("Error during truncate: Cannot truncate virtual tables", - "TRUNCATE TABLE test_virtual_ks.vt1"); - assertInvalidMessage("Virtual keyspace 'test_virtual_ks' is not user-modifiable", "CREATE INDEX ON test_virtual_ks.vt1 (v1)"); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org