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 7e434aa50b..c2216ee808 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 @@ -57,6 +57,11 @@ public void corruptMetadata(String namespace, String table) { // Do nothing } + @Override + public void deleteMetadata(String namespace, String table) throws Exception { + // Do nothing + } + @Override public void dropNamespace(String namespace) { String dropKeyspaceQuery = 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 3f960f137b..0adb16eefc 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 @@ -86,6 +86,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.TABLE_METADATA_CONTAINER); + container.deleteItem( + fullTableName, new PartitionKey(fullTableName), new CosmosItemRequestOptions()); + } + /** * Retrieve the stored procedure for the given table * 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 c5c362c517..7d6b2b6e40 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 @@ -29,6 +29,11 @@ protected AdminTestUtils getAdminTestUtils(String testName) { return new DynamoAdminTestUtils(getProperties(testName)); } + @Override + @Disabled("DynamoDB does not have a concept of namespaces") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Override @Disabled("DynamoDB does not support dropping columns") public void dropColumnFromTable_DropColumnForEachExistingDataType_ShouldDropColumnsCorrectly() {} 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 3193d1e342..5ffa9fc5f0 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 @@ -28,6 +28,11 @@ protected AdminTestUtils getAdminTestUtils(String testName) { return new DynamoAdminTestUtils(getProperties(testName)); } + @Override + @Disabled("DynamoDB does not have a concept of namespaces") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Override @Disabled("DynamoDB does not support dropping columns") public void dropColumnFromTable_DropColumnForEachExistingDataType_ShouldDropColumnsCorrectly() {} 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 dece9cd6d6..ad3fd80c98 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 @@ -171,6 +171,20 @@ 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(); 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 c0b453c71b..f04611413a 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 @@ -89,6 +89,12 @@ private boolean isWideningColumnTypeConversionNotFullySupported() { return JdbcTestUtils.isOracle(rdbEngine) || JdbcTestUtils.isSqlite(rdbEngine); } + @Test + @Override + @DisabledIf("isSqlite") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Test @Override @DisabledIf("isDb2") 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 63cdd489fa..9b41df3f03 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 @@ -90,6 +90,12 @@ private boolean isWideningColumnTypeConversionNotFullySupported() { return JdbcTestUtils.isOracle(rdbEngine) || JdbcTestUtils.isSqlite(rdbEngine); } + @Test + @Override + @DisabledIf("isSqlite") + public void + dropNamespace_ForNamespaceWithNonScalarDBManagedTables_ShouldThrowIllegalArgumentException() {} + @Test @Override @DisabledIf("isDb2") 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 c0a83577c2..4afdb05a74 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 @@ -64,6 +64,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); 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 55e9f5a276..e1b1c9cac0 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 @@ -116,6 +116,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 void dropNamespace(String namespace) throws SQLException { boolean existsOnCassandra = namespaceExistsOnCassandra(namespace); 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 f808b03249..c4723f5fdf 100644 --- a/core/src/main/java/com/scalar/db/common/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/CoreError.java @@ -843,6 +843,12 @@ public enum CoreError implements ScalarDbError { "With Oracle, setting a condition on a BLOB column when using a selection operation is not supported. Condition: %s", "", ""), + NAMESPACE_WITH_NON_SCALARDB_TABLES_CANNOT_BE_DROPPED( + Category.USER_ERROR, + "0249", + "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 98554dad02..c2c3f3eb0c 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 @@ -344,10 +344,18 @@ 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(); getNamespacesContainer() .deleteItem(new CosmosNamespace(namespace), new CosmosItemRequestOptions()); deleteMetadataDatabaseIfEmpty(); + } catch (IllegalArgumentException e) { + throw e; } catch (RuntimeException e) { throw new ExecutionException(String.format("Deleting the %s database failed", namespace), e); } @@ -785,4 +793,10 @@ private boolean containerExists(String databaseId, String containerId) throws Co 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 abdfa15354..9956fbcad1 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 @@ -45,7 +45,7 @@ public class JdbcAdmin implements DistributedStorageAdmin { public static final String METADATA_TABLE = "metadata"; public static final String NAMESPACES_TABLE = "namespaces"; - @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"; @@ -481,6 +481,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)); deleteFromNamespacesTable(connection, namespace); deleteNamespacesTableAndMetadataSchemaIfEmpty(connection); @@ -703,6 +709,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 { String selectQuery = 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 8d0ec467eb..f914228a85 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 @@ -583,4 +583,9 @@ public void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( .buildMessage(orderingOnBlobColumn.get())); } } + + @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 fed20d8a81..74363ecc37 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 @@ -504,4 +504,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 da6d1f973d..60f0cf14f5 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 @@ -535,4 +535,9 @@ public void bindBlobColumnToPreparedStatement( preparedStatement.setBinaryStream(index, inputStream); } } + + @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 c7339c8451..0815be9def 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 @@ -395,4 +395,9 @@ public String tryAddIfNotExistsToCreateIndexSql(String createIndexSql) { 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 9b489090e3..c833c25817 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 @@ -431,4 +431,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 cdeb2ce414..895420d63f 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 @@ -367,4 +367,10 @@ public void throwIfAlterColumnTypeNotSupported(DataType from, DataType to) { 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 48de3ff4d9..ec930a25f9 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 @@ -325,4 +325,6 @@ default void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( */ default void throwIfConjunctionsColumnNotSupported( Set conjunctions, TableMetadata metadata) {} + + String getTableNamesInNamespaceSql(); } diff --git a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTest.java b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTest.java index 0e1477c2a6..2aa946a67a 100644 --- a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTest.java +++ b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosAdminTest.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; @@ -587,7 +588,8 @@ public void dropTable_WithMetadataLeft_ShouldDropContainerAndOnlyDeleteMetadata( // Arrange String namespace = "ns"; CosmosDatabase metadataDatabase = mock(CosmosDatabase.class); - when(client.getDatabase(any())).thenReturn(database, metadataDatabase); + when(client.getDatabase(namespace)).thenReturn(database); + when(client.getDatabase(METADATA_DATABASE)).thenReturn(metadataDatabase); CosmosContainer namespacesContainer = mock(CosmosContainer.class); when(metadataDatabase.getContainer(anyString())).thenReturn(namespacesContainer); @@ -597,6 +599,12 @@ public void dropTable_WithMetadataLeft_ShouldDropContainerAndOnlyDeleteMetadata( .thenReturn(pagedIterable); when(pagedIterable.stream()).thenReturn(Stream.of(new CosmosNamespace(METADATA_DATABASE))); + @SuppressWarnings("unchecked") + CosmosPagedIterable emptyContainerPagedIterable = + mock(CosmosPagedIterable.class); + when(emptyContainerPagedIterable.stream()).thenReturn(Stream.empty()); + when(database.readAllContainers()).thenReturn(emptyContainerPagedIterable); + @SuppressWarnings("unchecked") CosmosPagedIterable containerPagedIterable = mock(CosmosPagedIterable.class); @@ -613,7 +621,7 @@ public void dropTable_WithMetadataLeft_ShouldDropContainerAndOnlyDeleteMetadata( admin.dropNamespace(namespace); // Assert - verify(client).getDatabase(namespace); + verify(client, times(2)).getDatabase(namespace); verify(database).delete(); verify(client, times(3)).getDatabase(METADATA_DATABASE); verify(metadataDatabase, times(2)).getContainer(CosmosAdmin.NAMESPACES_CONTAINER); @@ -628,10 +636,17 @@ public void dropNamespace_WithExistingDatabaseAndSomeNamespacesLeft_ShouldDropDa // Arrange String namespace = "ns"; CosmosDatabase metadataDatabase = mock(CosmosDatabase.class); - when(client.getDatabase(any())).thenReturn(database, metadataDatabase); + when(client.getDatabase(namespace)).thenReturn(database); + when(client.getDatabase(METADATA_DATABASE)).thenReturn(metadataDatabase); CosmosContainer namespacesContainer = mock(CosmosContainer.class); when(metadataDatabase.getContainer(anyString())).thenReturn(namespacesContainer); + @SuppressWarnings("unchecked") + CosmosPagedIterable emptyContainerPagedIterable = + mock(CosmosPagedIterable.class); + when(emptyContainerPagedIterable.stream()).thenReturn(Stream.empty()); + when(database.readAllContainers()).thenReturn(emptyContainerPagedIterable); + @SuppressWarnings("unchecked") CosmosPagedIterable pagedIterable = mock(CosmosPagedIterable.class); when(namespacesContainer.queryItems(anyString(), any(), any())).thenReturn(pagedIterable); @@ -642,7 +657,7 @@ public void dropNamespace_WithExistingDatabaseAndSomeNamespacesLeft_ShouldDropDa admin.dropNamespace(namespace); // Assert - verify(client).getDatabase(namespace); + verify(client, times(2)).getDatabase(namespace); verify(database).delete(); verify(client, times(2)).getDatabase(METADATA_DATABASE); verify(metadataDatabase, times(2)).getContainer(CosmosAdmin.NAMESPACES_CONTAINER); @@ -651,6 +666,33 @@ public void dropNamespace_WithExistingDatabaseAndSomeNamespacesLeft_ShouldDropDa verify(metadataDatabase, never()).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(METADATA_DATABASE)).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/JdbcAdminTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java index ea34a9b24d..284947e7f9 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java @@ -8,6 +8,7 @@ import static com.scalar.db.storage.jdbc.JdbcAdmin.hasDifferentClusteringOrders; 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; @@ -2005,6 +2006,7 @@ private void dropNamespace_WithOnlyNamespaceSchemaLeftForX_shouldDropSchemaAndNa Connection connection = mock(Connection.class); Statement dropNamespaceStmt = mock(Statement.class); + PreparedStatement isNamespaceEmptyStatementMock = mock(PreparedStatement.class); PreparedStatement deleteFromNamespaceTablePrepStmt = mock(PreparedStatement.class); Statement selectAllFromNamespaceTablePrepStmt = mock(Statement.class); Statement selectAllFromMetadataTablePrepStmt = mock(Statement.class); @@ -2018,7 +2020,12 @@ private void dropNamespace_WithOnlyNamespaceSchemaLeftForX_shouldDropSchemaAndNa selectAllFromMetadataTablePrepStmt, dropNamespaceTableStmt, dropMetadataSchemaStmt); - when(connection.prepareStatement(anyString())).thenReturn(deleteFromNamespaceTablePrepStmt); + // Mock for isNamespaceEmpty() check - returns empty ResultSet (namespace is empty) + ResultSet emptyResultSet = mock(ResultSet.class); + when(emptyResultSet.next()).thenReturn(false); + when(isNamespaceEmptyStatementMock.executeQuery()).thenReturn(emptyResultSet); + when(connection.prepareStatement(anyString())) + .thenReturn(isNamespaceEmptyStatementMock, deleteFromNamespaceTablePrepStmt); when(dataSource.getConnection()).thenReturn(connection); // Only the metadata schema is left ResultSet resultSet = @@ -2119,12 +2126,18 @@ private void dropNamespace_WithOtherNamespaceLeftForX_shouldOnlyDropNamespace( PreparedStatement deleteFromNamespaceTableMock = mock(PreparedStatement.class); Statement selectNamespaceStatementMock = mock(Statement.class); if (rdbEngine != RdbEngine.SQLITE) { + PreparedStatement getTableNamesPrepStmt = mock(PreparedStatement.class); when(connection.createStatement()) .thenReturn(dropNamespaceStatementMock, selectNamespaceStatementMock); + ResultSet emptyResultSet = mock(ResultSet.class); + when(emptyResultSet.next()).thenReturn(false); + when(getTableNamesPrepStmt.executeQuery()).thenReturn(emptyResultSet); + when(connection.prepareStatement(anyString())) + .thenReturn(getTableNamesPrepStmt, deleteFromNamespaceTableMock); } else { when(connection.createStatement()).thenReturn(selectNamespaceStatementMock); + when(connection.prepareStatement(anyString())).thenReturn(deleteFromNamespaceTableMock); } - when(connection.prepareStatement(anyString())).thenReturn(deleteFromNamespaceTableMock); when(dataSource.getConnection()).thenReturn(connection); // Namespaces table contains other namespaces ResultSet resultSet = @@ -2145,6 +2158,76 @@ private void dropNamespace_WithOtherNamespaceLeftForX_shouldOnlyDropNamespace( verify(selectNamespaceStatementMock).executeQuery(selectNamespaceStatement); } + @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); + PreparedStatement deleteFromNamespaceTableMock = mock(PreparedStatement.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(anyString())) + .thenReturn(getTableNamesPrepStmt, deleteFromNamespaceTableMock); + 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 cc40db8d85..451746621b 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 @@ -423,6 +423,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 02ddc910f9..42ecb473a9 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 @@ -56,6 +56,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 and the table metadata for the coordinator tables are present or not. *