diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminCaseSensitivityIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminCaseSensitivityIntegrationTest.java index 8def2e329b..a1005f6bd4 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminCaseSensitivityIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminCaseSensitivityIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminCaseSensitivityIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Properties; public class CassandraAdminCaseSensitivityIntegrationTest @@ -18,6 +19,11 @@ protected String getSystemNamespaceName(Properties properties) { .orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CassandraAdminTestUtils(getProperties(testName)); + } + @Override protected boolean isTimestampTypeSupported() { return false; diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminIntegrationTest.java index bba53f36bc..48bc092817 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Properties; public class CassandraAdminIntegrationTest extends DistributedStorageAdminIntegrationTestBase { @@ -17,6 +18,11 @@ protected String getSystemNamespaceName(Properties properties) { .orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CassandraAdminTestUtils(getProperties(testName)); + } + @Override protected boolean isTimestampTypeSupported() { return false; diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java index 24c1b2ad8f..e5cae073ef 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/CassandraAdminTestUtils.java @@ -1,5 +1,8 @@ package com.scalar.db.storage.cassandra; +import static com.datastax.driver.core.Metadata.quoteIfNecessary; + +import com.datastax.driver.core.schemabuilder.SchemaBuilder; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.util.AdminTestUtils; import java.util.Properties; @@ -28,6 +31,11 @@ public void corruptMetadata(String namespace, String table) { // Do nothing } + @Override + public void deleteMetadata(String namespace, String table) throws Exception { + // Do nothing + } + @Override public boolean namespaceExists(String namespace) { return clusterManager.getSession().getCluster().getMetadata().getKeyspace(namespace) != null; @@ -38,6 +46,14 @@ public boolean tableExists(String namespace, String table) { return clusterManager.getMetadata(namespace, table) != null; } + @Override + public void dropTable(String namespace, String table) { + String dropTableQuery = + SchemaBuilder.dropTable(quoteIfNecessary(namespace), quoteIfNecessary(table)) + .getQueryString(); + clusterManager.getSession().execute(dropTableQuery); + } + @Override public void close() { clusterManager.close(); diff --git a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminCaseSensitivityIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminCaseSensitivityIntegrationTest.java index 0ea6fca2c7..f24d633a39 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminCaseSensitivityIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminCaseSensitivityIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminCaseSensitivityIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Map; import java.util.Properties; @@ -18,6 +19,11 @@ protected Map getCreationOptions() { return CosmosEnv.getCreationOptions(); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CosmosAdminTestUtils(getProperties(testName)); + } + @Override protected String getSystemNamespaceName(Properties properties) { return new CosmosConfig(new DatabaseConfig(properties)) diff --git a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminIntegrationTest.java index 14bd5ae415..ebe48cf1b6 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Map; import java.util.Properties; @@ -17,6 +18,11 @@ protected Map getCreationOptions() { return CosmosEnv.getCreationOptions(); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new CosmosAdminTestUtils(getProperties(testName)); + } + @Override protected String getSystemNamespaceName(Properties properties) { return new CosmosConfig(new DatabaseConfig(properties)) diff --git a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java index c4e62f960a..767ee3f22b 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cosmos/CosmosAdminTestUtils.java @@ -76,6 +76,15 @@ public void corruptMetadata(String namespace, String table) { container.upsertItem(corruptedMetadata); } + @Override + public void deleteMetadata(String namespace, String table) { + String fullTableName = getFullTableName(namespace, table); + CosmosContainer container = + client.getDatabase(metadataDatabase).getContainer(CosmosAdmin.METADATA_CONTAINER); + container.deleteItem( + fullTableName, new PartitionKey(fullTableName), new CosmosItemRequestOptions()); + } + /** * Retrieve the stored procedure for the given table * @@ -117,6 +126,11 @@ public boolean tableExists(String namespace, String table) { return true; } + @Override + public void dropTable(String namespace, String table) { + client.getDatabase(namespace).getContainer(table).delete(); + } + @Override public void close() { client.close(); diff --git a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminCaseSensitivityIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminCaseSensitivityIntegrationTest.java index 2a6826d856..255cce3fa7 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminCaseSensitivityIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminCaseSensitivityIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminCaseSensitivityIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.Disabled; @@ -25,6 +26,11 @@ protected boolean isIndexOnBooleanColumnSupported() { return false; } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new DynamoAdminTestUtils(getProperties(testName)); + } + @Override protected String getSystemNamespaceName(Properties properties) { return new DynamoConfig(new DatabaseConfig(properties)) @@ -35,6 +41,12 @@ protected String getSystemNamespaceName(Properties properties) { // Since DynamoDB doesn't have the namespace concept, some behaviors around the namespace are // different from the other adapters. So disable several tests that check such behaviors + @Disabled + @Test + @Override + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Disabled @Test @Override diff --git a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminIntegrationTest.java index eba3ca2880..63f33d365a 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminIntegrationTest.java @@ -2,6 +2,7 @@ import com.scalar.db.api.DistributedStorageAdminIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.util.AdminTestUtils; import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.Disabled; @@ -24,6 +25,11 @@ protected boolean isIndexOnBooleanColumnSupported() { return false; } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new DynamoAdminTestUtils(getProperties(testName)); + } + @Override protected String getSystemNamespaceName(Properties properties) { return new DynamoConfig(new DatabaseConfig(properties)) @@ -34,6 +40,12 @@ protected String getSystemNamespaceName(Properties properties) { // Since DynamoDB doesn't have the namespace concept, some behaviors around the namespace are // different from the other adapters. So disable several tests that check such behaviors + @Disabled + @Test + @Override + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Disabled @Test @Override diff --git a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java index 74e80302fb..32d376ee84 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/dynamo/DynamoAdminTestUtils.java @@ -139,6 +139,31 @@ public void corruptMetadata(String namespace, String table) { .build()); } + @Override + public void deleteMetadata(String namespace, String table) { + String fullTableName = + getFullTableName(Namespace.of(namespacePrefix, namespace).prefixed(), table); + Map keyToDelete = new HashMap<>(); + keyToDelete.put("table", AttributeValue.builder().s(fullTableName).build()); + + client.deleteItem( + DeleteItemRequest.builder() + .tableName(getFullTableName(metadataNamespace, DynamoAdmin.METADATA_TABLE)) + .key(keyToDelete) + .build()); + } + + @Override + public void dropTable(String nonPrefixedNamespace, String table) { + String namespace = Namespace.of(namespacePrefix, nonPrefixedNamespace).prefixed(); + client.deleteTable( + DeleteTableRequest.builder().tableName(getFullTableName(namespace, table)).build()); + if (!waitForTableDeletion(namespace, table)) { + throw new RuntimeException( + String.format("Deleting the %s table timed out", getFullTableName(namespace, table))); + } + } + @Override public boolean namespaceExists(String namespace) throws Exception { // Dynamo has no concept of namespace diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java index 60a7fd6ed7..57001f388e 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java @@ -3,6 +3,7 @@ import com.scalar.db.api.DistributedStorageAdminCaseSensitivityIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.util.AdminTestUtils; import java.util.Properties; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -25,6 +26,11 @@ protected String getSystemNamespaceName(Properties properties) { .orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new JdbcAdminTestUtils(getProperties(testName)); + } + // Since SQLite doesn't have persistent namespaces, some behaviors around the namespace are // different from the other adapters. So disable several tests that check such behaviors. @@ -33,6 +39,12 @@ private boolean isSqlite() { return JdbcEnv.isSqlite(); } + @Test + @Override + @DisabledIf("isSqlite") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Test @Override @DisabledIf("isSqlite") diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java index ad47d37b7a..b97490c581 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java @@ -3,6 +3,7 @@ import com.scalar.db.api.DistributedStorageAdminIntegrationTestBase; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.util.AdminTestUtils; import java.util.Properties; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -24,6 +25,11 @@ protected String getSystemNamespaceName(Properties properties) { .orElse(DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME); } + @Override + protected AdminTestUtils getAdminTestUtils(String testName) { + return new JdbcAdminTestUtils(getProperties(testName)); + } + // Since SQLite doesn't have persistent namespaces, some behaviors around the namespace are // different from the other adapters. So disable several tests that check such behaviors. @@ -32,6 +38,12 @@ private boolean isSqlite() { return JdbcEnv.isSqlite(); } + @Test + @Override + @DisabledIf("isSqlite") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Test @Override @DisabledIf("isSqlite") diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java index 0fbcb9b909..47f2a51940 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminTestUtils.java @@ -56,6 +56,22 @@ public void corruptMetadata(String namespace, String table) throws Exception { execute(insertCorruptedMetadataStatement); } + @Override + public void deleteMetadata(String namespace, String table) throws Exception { + String deleteMetadataStatement = + "DELETE FROM " + + rdbEngine.encloseFullTableName(metadataSchema, JdbcAdmin.METADATA_TABLE) + + " WHERE " + + rdbEngine.enclose(JdbcAdmin.METADATA_COL_FULL_TABLE_NAME) + + " = ?"; + try (Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = + connection.prepareStatement(deleteMetadataStatement)) { + preparedStatement.setString(1, getFullTableName(namespace, table)); + preparedStatement.executeUpdate(); + } + } + private void execute(String sql) throws SQLException { try (Connection connection = dataSource.getConnection()) { JdbcAdmin.execute(connection, sql); @@ -112,6 +128,12 @@ public boolean tableExists(String namespace, String table) throws Exception { } } + @Override + public void dropTable(String namespace, String table) throws Exception { + String dropTableStatement = "DROP TABLE " + rdbEngine.encloseFullTableName(namespace, table); + execute(dropTableStatement); + } + @Override public void close() throws SQLException { dataSource.close(); diff --git a/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java index 3b4d05beb7..c23f7c1b5b 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTestUtils.java @@ -1,7 +1,9 @@ package com.scalar.db.storage.multistorage; +import static com.datastax.driver.core.Metadata.quoteIfNecessary; import static com.scalar.db.util.ScalarDbUtils.getFullTableName; +import com.datastax.driver.core.schemabuilder.SchemaBuilder; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.storage.cassandra.ClusterManager; import com.scalar.db.storage.jdbc.JdbcAdmin; @@ -87,6 +89,25 @@ public void corruptMetadata(String namespace, String table) throws Exception { execute(insertCorruptedMetadataStatement); } + @Override + public void deleteMetadata(String namespace, String table) throws Exception { + // Do nothing for Cassandra + + // for JDBC + String deleteMetadataStatement = + "DELETE FROM " + + rdbEngine.encloseFullTableName(jdbcMetadataSchema, JdbcAdmin.METADATA_TABLE) + + " WHERE " + + rdbEngine.enclose(JdbcAdmin.METADATA_COL_FULL_TABLE_NAME) + + " = ?"; + try (Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = + connection.prepareStatement(deleteMetadataStatement)) { + preparedStatement.setString(1, getFullTableName(namespace, table)); + preparedStatement.executeUpdate(); + } + } + @Override public boolean namespaceExists(String namespace) throws SQLException { boolean existsOnCassandra = namespaceExistsOnCassandra(namespace); @@ -143,6 +164,33 @@ public boolean tableExists(String namespace, String table) throws Exception { return existsOnCassandra || existsOnJdbc; } + @Override + public void dropTable(String namespace, String table) throws Exception { + boolean existsOnCassandra = tableExistsOnCassandra(namespace, table); + boolean existsOnJdbc = tableExistsOnJdbc(namespace, table); + + if (existsOnCassandra && existsOnJdbc) { + throw new IllegalStateException( + String.format( + "The %s table should not exist on both storages", + getFullTableName(namespace, table))); + } else if (!(existsOnCassandra || existsOnJdbc)) { + throw new IllegalStateException( + String.format( + "The %s table does not exist on both storages", getFullTableName(namespace, table))); + } + + if (existsOnCassandra) { + String dropTableQuery = + SchemaBuilder.dropTable(quoteIfNecessary(namespace), quoteIfNecessary(table)) + .getQueryString(); + clusterManager.getSession().execute(dropTableQuery); + } else { + String dropTableStatement = "DROP TABLE " + rdbEngine.encloseFullTableName(namespace, table); + execute(dropTableStatement); + } + } + private boolean tableExistsOnCassandra(String namespace, String table) { return clusterManager.getMetadata(namespace, table) != null; } diff --git a/core/src/main/java/com/scalar/db/common/CoreError.java b/core/src/main/java/com/scalar/db/common/CoreError.java index fff02ce179..91762de22f 100644 --- a/core/src/main/java/com/scalar/db/common/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/CoreError.java @@ -678,6 +678,12 @@ public enum CoreError implements ScalarDbError { "Mutations across multiple storages are not allowed. Mutations: %s", "", ""), + NAMESPACE_WITH_NON_SCALARDB_TABLES_CANNOT_BE_DROPPED( + Category.USER_ERROR, + "0216", + "The namespace has non-ScalarDB tables and cannot be dropped. Namespace: %s; Tables in the namespace: %s", + "", + ""), // // Errors for the concurrency error category diff --git a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java index 83ede432a3..e42fd13e1f 100644 --- a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java @@ -322,7 +322,15 @@ private void deleteTableMetadata(String namespace, String table) throws Executio @Override public void dropNamespace(String namespace) throws ExecutionException { try { + Set remainingTables = getRawTableNames(namespace); + if (!remainingTables.isEmpty()) { + throw new IllegalArgumentException( + CoreError.NAMESPACE_WITH_NON_SCALARDB_TABLES_CANNOT_BE_DROPPED.buildMessage( + namespace, remainingTables)); + } client.getDatabase(namespace).delete(); + } catch (IllegalArgumentException e) { + throw e; } catch (RuntimeException e) { throw new ExecutionException("Deleting the database failed", e); } @@ -640,4 +648,10 @@ private boolean metadataContainerExists() { public StorageInfo getStorageInfo(String namespace) { return STORAGE_INFO; } + + private Set getRawTableNames(String namespace) { + return client.getDatabase(namespace).readAllContainers().stream() + .map(CosmosContainerProperties::getId) + .collect(Collectors.toSet()); + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java index ea505638c3..1ae0dce4fd 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java @@ -44,7 +44,7 @@ public class JdbcAdmin implements DistributedStorageAdmin { public static final String METADATA_TABLE = "metadata"; - @VisibleForTesting static final String METADATA_COL_FULL_TABLE_NAME = "full_table_name"; + @VisibleForTesting public static final String METADATA_COL_FULL_TABLE_NAME = "full_table_name"; @VisibleForTesting static final String METADATA_COL_COLUMN_NAME = "column_name"; @VisibleForTesting static final String METADATA_COL_DATA_TYPE = "data_type"; @VisibleForTesting static final String METADATA_COL_KEY_TYPE = "key_type"; @@ -379,6 +379,12 @@ private void deleteMetadataSchema(Connection connection) throws SQLException { @Override public void dropNamespace(String namespace) throws ExecutionException { try (Connection connection = dataSource.getConnection()) { + Set remainingTables = getInternalTableNames(connection, namespace); + if (!remainingTables.isEmpty()) { + throw new IllegalArgumentException( + CoreError.NAMESPACE_WITH_NON_SCALARDB_TABLES_CANNOT_BE_DROPPED.buildMessage( + namespace, remainingTables)); + } execute(connection, rdbEngine.dropNamespaceSql(namespace)); } catch (SQLException e) { rdbEngine.dropNamespaceTranslateSQLException(e, namespace); @@ -584,6 +590,24 @@ public Set getNamespaceTableNames(String namespace) throws ExecutionExce } } + private Set getInternalTableNames(Connection connection, String namespace) + throws SQLException { + String sql = rdbEngine.getTableNamesInNamespaceSql(); + if (Strings.isNullOrEmpty(sql)) { + return Collections.emptySet(); + } + Set tableNames = new HashSet<>(); + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, namespace); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + tableNames.add(resultSet.getString(1)); + } + } + } + return tableNames; + } + @Override public boolean namespaceExists(String namespace) throws ExecutionException { if (metadataSchema.equals(namespace)) { diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java index f16ec37a18..f0864a719e 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java @@ -480,4 +480,9 @@ private String getProjection(String columnName, DataType dataType) { } return enclose(columnName); } + + @Override + public String getTableNamesInNamespaceSql() { + return "SELECT TABNAME FROM SYSCAT.TABLES WHERE TABSCHEMA = ? AND TYPE = 'T'"; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java index aa9c7f74a7..8ad42a8f74 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java @@ -456,4 +456,9 @@ public void setConnectionToReadOnly(Connection connection, boolean readOnly) thr // Observed performance degradation when using read-only connections in MySQL. So we do not // set the read-only mode for MySQL connections. } + + @Override + public String getTableNamesInNamespaceSql() { + return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?"; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java index bbd23ce2ef..66bd090124 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java @@ -410,4 +410,9 @@ public String getEscape(LikeExpression likeExpression) { getTimeTypeStrategy() { return timeTypeEngine; } + + @Override + public String getTableNamesInNamespaceSql() { + return "SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = ?"; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java index 01edddc897..dff77f7309 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java @@ -365,4 +365,9 @@ public Driver getDriver() { getTimeTypeStrategy() { return timeTypeEngine; } + + @Override + public String getTableNamesInNamespaceSql() { + return "SELECT table_name FROM information_schema.tables WHERE table_schema = ?"; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java index b3e2173933..866a2f8b16 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java @@ -381,4 +381,9 @@ public Map getConnectionProperties(JdbcConfig config) { getTimeTypeStrategy() { return timeTypeEngine; } + + @Override + public String getTableNamesInNamespaceSql() { + return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?"; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java index e9afca46cb..774c222ae6 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java @@ -342,4 +342,10 @@ public RdbEngineTimeTypeStrategy getTimeTypeStrategy( public void setConnectionToReadOnly(Connection connection, boolean readOnly) { // Do nothing. SQLite does not support read-only mode. } + + @Override + public String getTableNamesInNamespaceSql() { + // Do nothing. Namespace is just a table prefix in the SQLite implementation. + return null; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java index 61025d49c8..d8ef4f2cd7 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java @@ -244,4 +244,6 @@ default void setConnectionToReadOnly(Connection connection, boolean readOnly) throws SQLException { connection.setReadOnly(readOnly); } + + String getTableNamesInNamespaceSql(); } diff --git a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTestBase.java b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTestBase.java index b626f3fbbf..fe2d571b44 100644 --- a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTestBase.java +++ b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTestBase.java @@ -2,6 +2,7 @@ import static com.scalar.db.util.ScalarDbUtils.getFullTableName; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; @@ -512,6 +513,12 @@ public void dropNamespace_WithExistingDatabase_ShouldDropDatabase() throws Execu String namespace = "ns"; when(client.getDatabase(any())).thenReturn(database); + @SuppressWarnings("unchecked") + CosmosPagedIterable emptyContainerIterable = + mock(CosmosPagedIterable.class); + when(emptyContainerIterable.stream()).thenReturn(Stream.empty()); + when(database.readAllContainers()).thenReturn(emptyContainerIterable); + // Act admin.dropNamespace(namespace); @@ -519,6 +526,33 @@ public void dropNamespace_WithExistingDatabase_ShouldDropDatabase() throws Execu verify(database).delete(); } + @Test + public void dropNamespace_WithNonScalarDBTableLeft_ShouldThrowIllegalArgumentException() { + // Arrange + String namespace = "ns"; + CosmosDatabase metadataDatabase = mock(CosmosDatabase.class); + when(client.getDatabase(namespace)).thenReturn(database); + when(client.getDatabase(metadataDatabaseName)).thenReturn(metadataDatabase); + CosmosContainer namespacesContainer = mock(CosmosContainer.class); + when(metadataDatabase.getContainer(anyString())).thenReturn(namespacesContainer); + + @SuppressWarnings("unchecked") + CosmosPagedIterable containerPagedIterable = + mock(CosmosPagedIterable.class); + when(containerPagedIterable.stream()) + .thenReturn(Stream.of(mock(CosmosContainerProperties.class))); + when(database.readAllContainers()).thenReturn(containerPagedIterable); + + @SuppressWarnings("unchecked") + CosmosPagedIterable pagedIterable = mock(CosmosPagedIterable.class); + when(namespacesContainer.queryItems(anyString(), any(), any())).thenReturn(pagedIterable); + when(pagedIterable.stream()).thenReturn(Stream.empty()); + + // Act Assert + assertThatCode(() -> admin.dropNamespace(namespace)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void truncateTable_WithExistingRecords_ShouldDeleteAllRecords() throws ExecutionException { // Arrange diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTestBase.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTestBase.java index 386f2c0895..da621eee4e 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTestBase.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTestBase.java @@ -10,6 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -1618,9 +1619,14 @@ private void dropSchema_forX_shouldDropSchema( Connection connection = mock(Connection.class); Statement dropSchemaStatement = mock(Statement.class); + PreparedStatement getTableNamesPrepStmt = mock(PreparedStatement.class); + ResultSet emptyResultSet = mock(ResultSet.class); when(dataSource.getConnection()).thenReturn(connection); when(connection.createStatement()).thenReturn(dropSchemaStatement); + when(connection.prepareStatement(any())).thenReturn(getTableNamesPrepStmt); + when(emptyResultSet.next()).thenReturn(false); + when(getTableNamesPrepStmt.executeQuery()).thenReturn(emptyResultSet); // Act admin.dropNamespace(namespace); @@ -1629,6 +1635,74 @@ private void dropSchema_forX_shouldDropSchema( verify(dropSchemaStatement).execute(expectedDropSchemaStatement); } + @Test + public void dropNamespace_WithNonScalarDBTableLeftForMysql_ShouldThrowIllegalArgumentException() + throws Exception { + dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException(RdbEngine.MYSQL); + } + + @Test + public void + dropNamespace_WithNonScalarDBTableLeftForPostgresql_ShouldThrowIllegalArgumentException() + throws Exception { + dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException( + RdbEngine.POSTGRESQL); + } + + @Test + public void + dropNamespace_WithNonScalarDBTableLeftForSqlServer_ShouldThrowIllegalArgumentException() + throws Exception { + dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException( + RdbEngine.SQL_SERVER); + } + + @Test + public void dropNamespace_WithNonScalarDBTableLeftForOracle_ShouldThrowIllegalArgumentException() + throws Exception { + dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException( + RdbEngine.ORACLE); + } + + @Test + public void dropNamespace_WithNonScalarDBTableLeftForSqlite_ShouldThrowIllegalArgumentException() + throws Exception { + // Do nothing. SQLite does not have a concept of namespaces. + } + + @Test + public void dropNamespace_WithNonScalarDBTableLeftForDb2_ShouldThrowIllegalArgumentException() + throws Exception { + dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException(RdbEngine.DB2); + } + + private void dropNamespace_WithNonScalarDBTableLeftForX_ShouldThrowIllegalArgumentException( + RdbEngine rdbEngine) throws Exception { + // Arrange + String namespace = "my_ns"; + JdbcAdmin admin = createJdbcAdminFor(rdbEngine); + + Connection connection = mock(Connection.class); + Statement dropNamespaceStatementMock = mock(Statement.class); + Statement selectNamespaceStatementMock = mock(Statement.class); + PreparedStatement getTableNamesPrepStmt = mock(PreparedStatement.class); + when(connection.createStatement()) + .thenReturn(dropNamespaceStatementMock, selectNamespaceStatementMock); + ResultSet emptyResultSet = mock(ResultSet.class); + when(emptyResultSet.next()).thenReturn(true).thenReturn(false); + when(getTableNamesPrepStmt.executeQuery()).thenReturn(emptyResultSet); + when(connection.prepareStatement(any())).thenReturn(getTableNamesPrepStmt); + when(dataSource.getConnection()).thenReturn(connection); + // Namespaces table does not contain other namespaces + ResultSet resultSet = mock(ResultSet.class); + when(resultSet.next()).thenReturn(false); + when(selectNamespaceStatementMock.executeQuery(anyString())).thenReturn(resultSet); + + // Act Assert + assertThatCode(() -> admin.dropNamespace(namespace)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void getNamespaceTables_forMysql_ShouldReturnTableNames() throws Exception { getNamespaceTables_forX_ShouldReturnTableNames( diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminIntegrationTestBase.java index 59c5f407e5..630e70fbbb 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageAdminIntegrationTestBase.java @@ -8,6 +8,7 @@ import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.db.service.StorageFactory; +import com.scalar.db.util.AdminTestUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.LocalDate; @@ -213,6 +214,8 @@ protected Map getCreationOptions() { return Collections.emptyMap(); } + protected abstract AdminTestUtils getAdminTestUtils(String testName); + @AfterAll public void afterAll() throws Exception { try { @@ -415,6 +418,29 @@ public void dropNamespace_ForNonEmptyNamespace_ShouldThrowIllegalArgumentExcepti } } + @Test + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() + throws Exception { + AdminTestUtils adminTestUtils = getAdminTestUtils(getTestName()); + String nonManagedTable = "non_managed_table"; + try { + // Arrange + admin.createNamespace(namespace3, getCreationOptions()); + admin.createTable(namespace3, nonManagedTable, getTableMetadata(), getCreationOptions()); + adminTestUtils.deleteMetadata(namespace3, nonManagedTable); + + // Act Assert + assertThatThrownBy(() -> admin.dropNamespace(namespace3)) + .isInstanceOf(IllegalArgumentException.class); + } finally { + adminTestUtils.dropTable(namespace3, nonManagedTable); + admin.dropNamespace(namespace3, true); + + adminTestUtils.close(); + } + } + @Test public void dropNamespace_IfExists_ForNonExistingNamespace_ShouldNotThrowAnyException() { // Arrange diff --git a/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java b/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java index f548040d54..a3bf9a62aa 100644 --- a/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java +++ b/integration-test/src/main/java/com/scalar/db/util/AdminTestUtils.java @@ -42,6 +42,15 @@ public AdminTestUtils(Properties coordinatorStorageProperties) { */ public abstract void corruptMetadata(String namespace, String table) throws Exception; + /** + * Deletes the metadata for the specified table. + * + * @param namespace the namespace + * @param table the table name + * @throws Exception if an error occurs + */ + public abstract void deleteMetadata(String namespace, String table) throws Exception; + /** * Returns whether the table metadata for the coordinator tables are present or not. * @@ -95,6 +104,15 @@ public boolean areTableMetadataForCoordinatorTablesPresent() throws Exception { */ public abstract boolean tableExists(String namespace, String table) throws Exception; + /** + * Drops the table in the underlying storage. + * + * @param namespace a namespace + * @param table a table + * @throws Exception if an errors occurs + */ + public abstract void dropTable(String namespace, String table) throws Exception; + /** * Closes connections to the storage *