blob: 9a260323f95b43a74bd40968737a50c4aba638ee [file] [log] [blame]
/*
* 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.schema;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import com.google.common.collect.ImmutableMap;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.Util;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.db.marshal.ByteType;
import org.apache.cassandra.db.marshal.BytesType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.locator.NetworkTopologyStrategy;
import org.apache.cassandra.utils.FBUtilities;
import static java.util.Collections.singleton;
import static org.apache.cassandra.Util.throwAssert;
import static org.apache.cassandra.cql3.CQLTester.assertRows;
import static org.apache.cassandra.cql3.CQLTester.row;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class MigrationManagerTest
{
private static final String KEYSPACE1 = "keyspace1";
private static final String KEYSPACE3 = "keyspace3";
private static final String KEYSPACE6 = "keyspace6";
private static final String EMPTY_KEYSPACE = "test_empty_keyspace";
private static final String TABLE1 = "standard1";
private static final String TABLE2 = "standard2";
private static final String TABLE1i = "indexed1";
@Rule
public ExpectedException thrown = ExpectedException.none();
@BeforeClass
public static void defineSchema() throws ConfigurationException
{
SchemaLoader.prepareServer();
SchemaLoader.startGossiper();
SchemaLoader.createKeyspace(KEYSPACE1,
KeyspaceParams.simple(1),
SchemaLoader.standardCFMD(KEYSPACE1, TABLE1),
SchemaLoader.standardCFMD(KEYSPACE1, TABLE2));
SchemaLoader.createKeyspace(KEYSPACE3,
KeyspaceParams.simple(5),
SchemaLoader.standardCFMD(KEYSPACE1, TABLE1),
SchemaLoader.compositeIndexCFMD(KEYSPACE3, TABLE1i, true));
SchemaLoader.createKeyspace(KEYSPACE6,
KeyspaceParams.simple(1),
SchemaLoader.compositeIndexCFMD(KEYSPACE6, TABLE1i, true));
}
@Test
public void testTableMetadataBuilder() throws ConfigurationException
{
TableMetadata.Builder builder =
TableMetadata.builder(KEYSPACE1, "TestApplyCFM_CF")
.addPartitionKeyColumn("keys", BytesType.instance)
.addClusteringColumn("col", BytesType.instance)
.comment("No comment")
.gcGraceSeconds(100000)
.compaction(CompactionParams.stcs(ImmutableMap.of("min_threshold", "500", "max_threshold", "500")));
for (int i = 0; i < 5; i++)
{
ByteBuffer name = ByteBuffer.wrap(new byte[] { (byte)i });
builder.addRegularColumn(ColumnIdentifier.getInterned(name, BytesType.instance), ByteType.instance);
}
TableMetadata table = builder.build();
// we'll be adding this one later. make sure it's not already there.
assertNull(table.getColumn(ByteBuffer.wrap(new byte[]{ 5 })));
// add one.
ColumnMetadata addIndexDef = ColumnMetadata.regularColumn(table, ByteBuffer.wrap(new byte[] { 5 }), BytesType.instance);
builder.addColumn(addIndexDef);
// remove one.
ColumnMetadata removeIndexDef = ColumnMetadata.regularColumn(table, ByteBuffer.wrap(new byte[] { 0 }), BytesType.instance);
builder.removeRegularOrStaticColumn(removeIndexDef.name);
TableMetadata table2 = builder.build();
for (int i = 1; i < table2.columns().size(); i++)
assertNotNull(table2.getColumn(ByteBuffer.wrap(new byte[]{ 1 })));
assertNull(table2.getColumn(ByteBuffer.wrap(new byte[]{ 0 })));
assertNotNull(table2.getColumn(ByteBuffer.wrap(new byte[]{ 5 })));
}
@Test
public void testInvalidNames()
{
String[] valid = {"1", "a", "_1", "b_", "__", "1_a"};
for (String s : valid)
assertTrue(SchemaConstants.isValidName(s));
String[] invalid = {"b@t", "dash-y", "", " ", "dot.s", ".hidden"};
for (String s : invalid)
assertFalse(SchemaConstants.isValidName(s));
}
@Test
public void addNewCfToBogusKeyspace()
{
TableMetadata newCf = addTestTable("MadeUpKeyspace", "NewCF", "new cf");
try
{
MigrationManager.announceNewTable(newCf);
throw new AssertionError("You shouldn't be able to do anything to a keyspace that doesn't exist.");
}
catch (ConfigurationException expected)
{
}
}
@Test
public void addNewTable() throws ConfigurationException
{
final String ksName = KEYSPACE1;
final String tableName = "anewtable";
KeyspaceMetadata original = Schema.instance.getKeyspaceMetadata(ksName);
TableMetadata cfm = addTestTable(original.name, tableName, "A New Table");
assertFalse(Schema.instance.getKeyspaceMetadata(ksName).tables.get(cfm.name).isPresent());
MigrationManager.announceNewTable(cfm);
assertTrue(Schema.instance.getKeyspaceMetadata(ksName).tables.get(cfm.name).isPresent());
assertEquals(cfm, Schema.instance.getKeyspaceMetadata(ksName).tables.get(cfm.name).get());
// now read and write to it.
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (key, col, val) VALUES (?, ?, ?)",
ksName, tableName),
"key0", "col0", "val0");
// flush to exercise more than just hitting the memtable
ColumnFamilyStore cfs = Keyspace.open(ksName).getColumnFamilyStore(tableName);
assertNotNull(cfs);
cfs.forceBlockingFlush();
// and make sure we get out what we put in
UntypedResultSet rows = QueryProcessor.executeInternal(String.format("SELECT * FROM %s.%s", ksName, tableName));
assertRows(rows, row("key0", "col0", "val0"));
}
@Test
public void dropCf() throws ConfigurationException
{
// sanity
final KeyspaceMetadata ks = Schema.instance.getKeyspaceMetadata(KEYSPACE1);
assertNotNull(ks);
final TableMetadata cfm = ks.tables.getNullable(TABLE1);
assertNotNull(cfm);
// write some data, force a flush, then verify that files exist on disk.
for (int i = 0; i < 100; i++)
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (key, name, val) VALUES (?, ?, ?)",
KEYSPACE1, TABLE1),
"dropCf", "col" + i, "anyvalue");
ColumnFamilyStore store = Keyspace.open(cfm.keyspace).getColumnFamilyStore(cfm.name);
assertNotNull(store);
store.forceBlockingFlush();
assertTrue(store.getDirectories().sstableLister(Directories.OnTxnErr.THROW).list().size() > 0);
MigrationManager.announceTableDrop(ks.name, cfm.name, false);
assertFalse(Schema.instance.getKeyspaceMetadata(ks.name).tables.get(cfm.name).isPresent());
// any write should fail.
boolean success = true;
try
{
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (key, name, val) VALUES (?, ?, ?)",
KEYSPACE1, TABLE1),
"dropCf", "col0", "anyvalue");
}
catch (Throwable th)
{
success = false;
}
assertFalse("This mutation should have failed since the CF no longer exists.", success);
// verify that the files are gone.
Supplier<Object> lambda = () -> {
for (File file : store.getDirectories().sstableLister(Directories.OnTxnErr.THROW).listFiles())
{
if (file.getPath().endsWith("Data.db") && !new File(file.getPath().replace("Data.db", "Compacted")).exists())
return false;
}
return true;
};
Util.spinAssertEquals(true, lambda, 30);
}
@Test
public void addNewKS() throws ConfigurationException
{
TableMetadata cfm = addTestTable("newkeyspace1", "newstandard1", "A new cf for a new ks");
KeyspaceMetadata newKs = KeyspaceMetadata.create(cfm.keyspace, KeyspaceParams.simple(5), Tables.of(cfm));
MigrationManager.announceNewKeyspace(newKs);
assertNotNull(Schema.instance.getKeyspaceMetadata(cfm.keyspace));
assertEquals(Schema.instance.getKeyspaceMetadata(cfm.keyspace), newKs);
// test reads and writes.
QueryProcessor.executeInternal("INSERT INTO newkeyspace1.newstandard1 (key, col, val) VALUES (?, ?, ?)",
"key0", "col0", "val0");
ColumnFamilyStore store = Keyspace.open(cfm.keyspace).getColumnFamilyStore(cfm.name);
assertNotNull(store);
store.forceBlockingFlush();
UntypedResultSet rows = QueryProcessor.executeInternal("SELECT * FROM newkeyspace1.newstandard1");
assertRows(rows, row("key0", "col0", "val0"));
}
@Test
public void dropKSUnflushed() throws ConfigurationException
{
// sanity
final KeyspaceMetadata ks = Schema.instance.getKeyspaceMetadata(KEYSPACE3);
assertNotNull(ks);
final TableMetadata cfm = ks.tables.getNullable(TABLE1);
assertNotNull(cfm);
// write some data
for (int i = 0; i < 100; i++)
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (key, name, val) VALUES (?, ?, ?)",
KEYSPACE3, TABLE1),
"dropKs", "col" + i, "anyvalue");
MigrationManager.announceKeyspaceDrop(ks.name);
assertNull(Schema.instance.getKeyspaceMetadata(ks.name));
}
@Test
public void createEmptyKsAddNewCf() throws ConfigurationException
{
assertNull(Schema.instance.getKeyspaceMetadata(EMPTY_KEYSPACE));
KeyspaceMetadata newKs = KeyspaceMetadata.create(EMPTY_KEYSPACE, KeyspaceParams.simple(5));
MigrationManager.announceNewKeyspace(newKs);
assertNotNull(Schema.instance.getKeyspaceMetadata(EMPTY_KEYSPACE));
String tableName = "added_later";
TableMetadata newCf = addTestTable(EMPTY_KEYSPACE, tableName, "A new CF to add to an empty KS");
//should not exist until apply
assertFalse(Schema.instance.getKeyspaceMetadata(newKs.name).tables.get(newCf.name).isPresent());
//add the new CF to the empty space
MigrationManager.announceNewTable(newCf);
assertTrue(Schema.instance.getKeyspaceMetadata(newKs.name).tables.get(newCf.name).isPresent());
assertEquals(Schema.instance.getKeyspaceMetadata(newKs.name).tables.get(newCf.name).get(), newCf);
// now read and write to it.
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (key, col, val) VALUES (?, ?, ?)",
EMPTY_KEYSPACE, tableName),
"key0", "col0", "val0");
ColumnFamilyStore cfs = Keyspace.open(newKs.name).getColumnFamilyStore(newCf.name);
assertNotNull(cfs);
cfs.forceBlockingFlush();
UntypedResultSet rows = QueryProcessor.executeInternal(String.format("SELECT * FROM %s.%s", EMPTY_KEYSPACE, tableName));
assertRows(rows, row("key0", "col0", "val0"));
}
@Test
public void testUpdateKeyspace() throws ConfigurationException
{
// create a keyspace to serve as existing.
TableMetadata cf = addTestTable("UpdatedKeyspace", "AddedStandard1", "A new cf for a new ks");
KeyspaceMetadata oldKs = KeyspaceMetadata.create(cf.keyspace, KeyspaceParams.simple(5), Tables.of(cf));
MigrationManager.announceNewKeyspace(oldKs);
assertNotNull(Schema.instance.getKeyspaceMetadata(cf.keyspace));
assertEquals(Schema.instance.getKeyspaceMetadata(cf.keyspace), oldKs);
// names should match.
KeyspaceMetadata newBadKs2 = KeyspaceMetadata.create(cf.keyspace + "trash", KeyspaceParams.simple(4));
try
{
MigrationManager.announceKeyspaceUpdate(newBadKs2);
throw new AssertionError("Should not have been able to update a KS with an invalid KS name.");
}
catch (ConfigurationException ex)
{
// expected.
}
Map<String, String> replicationMap = new HashMap<>();
replicationMap.put(ReplicationParams.CLASS, NetworkTopologyStrategy.class.getName());
replicationMap.put("replication_factor", "1");
KeyspaceMetadata newKs = KeyspaceMetadata.create(cf.keyspace, KeyspaceParams.create(true, replicationMap));
MigrationManager.announceKeyspaceUpdate(newKs);
KeyspaceMetadata newFetchedKs = Schema.instance.getKeyspaceMetadata(newKs.name);
assertEquals(newFetchedKs.params.replication.klass, newKs.params.replication.klass);
assertFalse(newFetchedKs.params.replication.klass.equals(oldKs.params.replication.klass));
}
/*
@Test
public void testUpdateColumnFamilyNoIndexes() throws ConfigurationException
{
// create a keyspace with a cf to update.
CFMetaData cf = addTestTable("UpdatedCfKs", "Standard1added", "A new cf that will be updated");
KSMetaData ksm = KSMetaData.testMetadata(cf.ksName, SimpleStrategy.class, KSMetaData.optsWithRF(1), cf);
MigrationManager.announceNewKeyspace(ksm);
assertNotNull(Schema.instance.getKSMetaData(cf.ksName));
assertEquals(Schema.instance.getKSMetaData(cf.ksName), ksm);
assertNotNull(Schema.instance.getTableMetadataRef(cf.ksName, cf.cfName));
// updating certain fields should fail.
CFMetaData newCfm = cf.copy();
newCfm.defaultValidator(BytesType.instance);
newCfm.minCompactionThreshold(5);
newCfm.maxCompactionThreshold(31);
// test valid operations.
newCfm.comment("Modified comment");
MigrationManager.announceTableUpdate(newCfm); // doesn't get set back here.
newCfm.readRepairChance(0.23);
MigrationManager.announceTableUpdate(newCfm);
newCfm.gcGraceSeconds(12);
MigrationManager.announceTableUpdate(newCfm);
newCfm.defaultValidator(UTF8Type.instance);
MigrationManager.announceTableUpdate(newCfm);
newCfm.minCompactionThreshold(3);
MigrationManager.announceTableUpdate(newCfm);
newCfm.maxCompactionThreshold(33);
MigrationManager.announceTableUpdate(newCfm);
// can't test changing the reconciler because there is only one impl.
// check the cumulative affect.
assertEquals(Schema.instance.getTableMetadataRef(cf.ksName, cf.cfName).getComment(), newCfm.getComment());
assertEquals(Schema.instance.getTableMetadataRef(cf.ksName, cf.cfName).getReadRepairChance(), newCfm.getReadRepairChance(), 0.0001);
assertEquals(Schema.instance.getTableMetadataRef(cf.ksName, cf.cfName).getGcGraceSeconds(), newCfm.getGcGraceSeconds());
assertEquals(UTF8Type.instance, Schema.instance.getTableMetadataRef(cf.ksName, cf.cfName).getDefaultValidator());
// Change tableId
newCfm = new CFMetaData(cf.ksName, cf.cfName, cf.cfType, cf.comparator);
CFMetaData.copyOpts(newCfm, cf);
try
{
cf.apply(newCfm);
throw new AssertionError("Should have blown up when you used a different id.");
}
catch (ConfigurationException expected) {}
// Change cfName
newCfm = new CFMetaData(cf.ksName, cf.cfName + "_renamed", cf.cfType, cf.comparator);
CFMetaData.copyOpts(newCfm, cf);
try
{
cf.apply(newCfm);
throw new AssertionError("Should have blown up when you used a different name.");
}
catch (ConfigurationException expected) {}
// Change ksName
newCfm = new CFMetaData(cf.ksName + "_renamed", cf.cfName, cf.cfType, cf.comparator);
CFMetaData.copyOpts(newCfm, cf);
try
{
cf.apply(newCfm);
throw new AssertionError("Should have blown up when you used a different keyspace.");
}
catch (ConfigurationException expected) {}
// Change cf type
newCfm = new CFMetaData(cf.ksName, cf.cfName, ColumnFamilyType.Super, cf.comparator);
CFMetaData.copyOpts(newCfm, cf);
try
{
cf.apply(newCfm);
throw new AssertionError("Should have blwon up when you used a different cf type.");
}
catch (ConfigurationException expected) {}
// Change comparator
newCfm = new CFMetaData(cf.ksName, cf.cfName, cf.cfType, new SimpleDenseCellNameType(TimeUUIDType.instance));
CFMetaData.copyOpts(newCfm, cf);
try
{
cf.apply(newCfm);
throw new AssertionError("Should have blown up when you used a different comparator.");
}
catch (ConfigurationException expected) {}
}
*/
@Test
public void testDropIndex() throws ConfigurationException
{
// persist keyspace definition in the system keyspace
SchemaKeyspace.makeCreateKeyspaceMutation(Schema.instance.getKeyspaceMetadata(KEYSPACE6), FBUtilities.timestampMicros()).build().applyUnsafe();
ColumnFamilyStore cfs = Keyspace.open(KEYSPACE6).getColumnFamilyStore(TABLE1i);
String indexName = TABLE1i + "_birthdate_key_index";
// insert some data. save the sstable descriptor so we can make sure it's marked for delete after the drop
QueryProcessor.executeInternal(String.format(
"INSERT INTO %s.%s (key, c1, birthdate, notbirthdate) VALUES (?, ?, ?, ?)",
KEYSPACE6,
TABLE1i),
"key0", "col0", 1L, 1L);
cfs.forceBlockingFlush();
ColumnFamilyStore indexCfs = cfs.indexManager.getIndexByName(indexName)
.getBackingTable()
.orElseThrow(throwAssert("Cannot access index cfs"));
Descriptor desc = indexCfs.getLiveSSTables().iterator().next().descriptor;
// drop the index
TableMetadata meta = cfs.metadata();
IndexMetadata existing = meta.indexes
.get(indexName)
.orElseThrow(throwAssert("Index not found"));
MigrationManager.announceTableUpdate(meta.unbuild().indexes(meta.indexes.without(existing.name)).build());
// check
assertTrue(cfs.indexManager.listIndexes().isEmpty());
LifecycleTransaction.waitForDeletions();
assertFalse(new File(desc.filenameFor(Component.DATA)).exists());
}
@Test
public void testValidateNullKeyspace() throws Exception
{
TableMetadata.Builder builder = TableMetadata.builder(null, TABLE1).addPartitionKeyColumn("partitionKey", BytesType.instance);
TableMetadata table1 = builder.build();
thrown.expect(ConfigurationException.class);
thrown.expectMessage(null + "." + TABLE1 + ": Keyspace name must not be empty");
table1.validate();
}
@Test
public void testValidateCompatibilityIDMismatch() throws Exception
{
TableMetadata.Builder builder = TableMetadata.builder(KEYSPACE1, TABLE1).addPartitionKeyColumn("partitionKey", BytesType.instance);
TableMetadata table1 = builder.build();
TableMetadata table2 = table1.unbuild().id(TableId.generate()).build();
thrown.expect(ConfigurationException.class);
thrown.expectMessage(KEYSPACE1 + "." + TABLE1 + ": Table ID mismatch");
table1.validateCompatibility(table2);
}
@Test
public void testValidateCompatibilityNameMismatch() throws Exception
{
TableMetadata.Builder builder1 = TableMetadata.builder(KEYSPACE1, TABLE1).addPartitionKeyColumn("partitionKey", BytesType.instance);
TableMetadata.Builder builder2 = TableMetadata.builder(KEYSPACE1, TABLE2).addPartitionKeyColumn("partitionKey", BytesType.instance);
TableMetadata table1 = builder1.build();
TableMetadata table2 = builder2.build();
thrown.expect(ConfigurationException.class);
thrown.expectMessage(KEYSPACE1 + "." + TABLE1 + ": Table mismatch");
table1.validateCompatibility(table2);
}
@Test
public void testEvolveSystemKeyspaceNew()
{
TableMetadata table = addTestTable("ks0", "t", "");
KeyspaceMetadata keyspace = KeyspaceMetadata.create("ks0", KeyspaceParams.simple(1), Tables.of(table));
Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace, 0);
assertTrue(mutation.isPresent());
Schema.instance.merge(singleton(mutation.get()));
assertEquals(keyspace, Schema.instance.getKeyspaceMetadata("ks0"));
}
@Test
public void testEvolveSystemKeyspaceExistsUpToDate()
{
TableMetadata table = addTestTable("ks1", "t", "");
KeyspaceMetadata keyspace = KeyspaceMetadata.create("ks1", KeyspaceParams.simple(1), Tables.of(table));
// create the keyspace, verify it's there
Schema.instance.merge(singleton(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace, 0).build()));
assertEquals(keyspace, Schema.instance.getKeyspaceMetadata("ks1"));
Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace, 0);
assertFalse(mutation.isPresent());
}
@Test
public void testEvolveSystemKeyspaceChanged()
{
TableMetadata table0 = addTestTable("ks2", "t", "");
KeyspaceMetadata keyspace0 = KeyspaceMetadata.create("ks2", KeyspaceParams.simple(1), Tables.of(table0));
// create the keyspace, verify it's there
Schema.instance.merge(singleton(SchemaKeyspace.makeCreateKeyspaceMutation(keyspace0, 0).build()));
assertEquals(keyspace0, Schema.instance.getKeyspaceMetadata("ks2"));
TableMetadata table1 = table0.unbuild().comment("comment").build();
KeyspaceMetadata keyspace1 = KeyspaceMetadata.create("ks2", KeyspaceParams.simple(1), Tables.of(table1));
Optional<Mutation> mutation = MigrationManager.evolveSystemKeyspace(keyspace1, 1);
assertTrue(mutation.isPresent());
Schema.instance.merge(singleton(mutation.get()));
assertEquals(keyspace1, Schema.instance.getKeyspaceMetadata("ks2"));
}
private TableMetadata addTestTable(String ks, String cf, String comment)
{
return
TableMetadata.builder(ks, cf)
.addPartitionKeyColumn("key", UTF8Type.instance)
.addClusteringColumn("col", UTF8Type.instance)
.addRegularColumn("val", UTF8Type.instance)
.comment(comment)
.build();
}
}