Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ public void repairTable(
throws ExecutionException {
try {
createTableInternal(namespace, table, metadata, true);
updateIndexingPolicy(namespace, table, metadata);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
Expand Down
45 changes: 45 additions & 0 deletions core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,46 @@ columnName, getFullTableName(namespace, table)),
TableMetadata.newBuilder(tableMetadata).addSecondaryIndex(columnName).build());
}

private boolean rawIndexExists(String nonPrefixedNamespace, String table, String indexName) {
Copy link
Contributor

@feeblefakie feeblefakie Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private boolean rawIndexExists(String nonPrefixedNamespace, String table, String indexName) {
private boolean nativeIndexExists(String nonPrefixedNamespace, String table, String indexName) {

I felt raw sounds misleading. How about native?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment! It seems that the prefix "Raw" is used to refer to things not managed by ScalarDB metadata, such as addRawColumnToTable. Therefore, the name rawIndexExists is being used for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! OK, let's keep it as is for this PR.

@brfrn169 It's very minor, but I think raw sounds misleading , so let's revisit the naming at some point.

Namespace namespace = Namespace.of(namespacePrefix, nonPrefixedNamespace);
String globalIndexName =
String.join(
".",
getFullTableName(namespace, table),
DynamoAdmin.GLOBAL_INDEX_NAME_PREFIX,
indexName);
int retryCount = 0;
try {
while (true) {
DescribeTableResponse response =
client.describeTable(
DescribeTableRequest.builder()
.tableName(getFullTableName(namespace, table))
.build());
GlobalSecondaryIndexDescription description =
response.table().globalSecondaryIndexes().stream()
.filter(d -> d.indexName().equals(globalIndexName))
.findFirst()
.orElse(null);
if (description == null) {
return false;
}
if (description.indexStatus() == IndexStatus.ACTIVE) {
return true;
}
if (retryCount++ >= MAX_RETRY_COUNT) {
throw new IllegalStateException(
String.format(
"Waiting for the secondary index %s on the %s table to be active failed",
indexName, getFullTableName(namespace, table)));
}
Uninterruptibles.sleepUninterruptibly(waitingDurationSecs, TimeUnit.SECONDS);
}
} catch (ResourceNotFoundException e) {
return false;
}
}

private void waitForIndexCreation(Namespace namespace, String table, String columnName)
throws ExecutionException {
try {
Expand Down Expand Up @@ -1366,6 +1406,11 @@ public void repairTable(
throws ExecutionException {
try {
createTableInternal(nonPrefixedNamespace, table, metadata, true, options);
for (String indexColumnName : metadata.getSecondaryIndexNames()) {
if (!rawIndexExists(nonPrefixedNamespace, table, indexColumnName)) {
createIndex(nonPrefixedNamespace, table, indexColumnName, options);
}
}
} catch (RuntimeException e) {
throw new ExecutionException(
String.format(
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,8 @@ columnName, getFullTableName(namespace, table)),
}
}

private void createIndex(
@VisibleForTesting
void createIndex(
Connection connection, String schema, String table, String indexedColumn, boolean ifNotExists)
throws SQLException {
String indexName = getIndexName(schema, table, indexedColumn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -797,6 +798,43 @@ public void repairTable_ShouldCreateTableAndIndexesIfTheyDoNotExists() throws Ex
}
}

@Test
public void repairTable_WhenTableAlreadyExistsWithoutIndex_ShouldCreateIndex()
throws ExecutionException {
// Arrange
String namespace = "sample_ns";
String table = "tbl";
TableMetadata tableMetadata =
TableMetadata.newBuilder()
.addPartitionKey("c1")
.addClusteringKey("c4")
.addColumn("c1", DataType.INT)
.addColumn("c2", DataType.TEXT)
.addColumn("c3", DataType.BLOB)
.addColumn("c4", DataType.INT)
.addColumn("c5", DataType.BOOLEAN)
.addSecondaryIndex("c2")
.addSecondaryIndex("c4")
.build();
// The table already exists
when(clusterManager.getMetadata(namespace, table))
.thenReturn(mock(com.datastax.driver.core.TableMetadata.class));
when(cassandraSession.getCluster()).thenReturn(cluster);
when(cluster.getMetadata()).thenReturn(metadata);
when(metadata.getKeyspace(any())).thenReturn(keyspaceMetadata);
when(keyspaceMetadata.getTable(table))
.thenReturn(mock(com.datastax.driver.core.TableMetadata.class));

CassandraAdmin adminSpy = spy(cassandraAdmin);

// Act
adminSpy.repairTable(namespace, table, tableMetadata, Collections.emptyMap());

// Assert
verify(adminSpy)
.createSecondaryIndexes(namespace, table, tableMetadata.getSecondaryIndexNames(), true);
}

@Test
public void addNewColumnToTable_ShouldWorkProperly() throws ExecutionException {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,13 @@ public void repairTable_withStoredProcedure_ShouldNotAddStoredProcedure()
when(scripts.getStoredProcedure(CosmosAdmin.STORED_PROCEDURE_FILE_NAME))
.thenReturn(storedProcedure);

// Existing container properties
CosmosContainerResponse response = mock(CosmosContainerResponse.class);
when(database.createContainerIfNotExists(table, "/concatenatedPartitionKey"))
.thenReturn(response);
CosmosContainerProperties properties = mock(CosmosContainerProperties.class);
when(response.getProperties()).thenReturn(properties);

Comment on lines +896 to +902
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines are prepared to handle the addition of updateIndexingPolicy.

// Act Assert
admin.repairTable(namespace, table, tableMetadata, Collections.emptyMap());

Expand Down Expand Up @@ -943,6 +950,13 @@ public void repairTable_withoutStoredProcedure_ShouldCreateStoredProcedure()
when(cosmosException.getStatusCode()).thenReturn(404);
when(storedProcedure.read()).thenThrow(cosmosException);

// Existing container properties
CosmosContainerResponse response = mock(CosmosContainerResponse.class);
when(database.createContainerIfNotExists(table, "/concatenatedPartitionKey"))
.thenReturn(response);
CosmosContainerProperties properties = mock(CosmosContainerProperties.class);
when(response.getProperties()).thenReturn(properties);

Comment on lines +953 to +959
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

// Act
admin.repairTable(namespace, table, tableMetadata, Collections.emptyMap());

Expand Down Expand Up @@ -996,6 +1010,13 @@ public void repairTable_ShouldCreateTableIfNotExistsAndUpsertMetadata()
when(cosmosException.getStatusCode()).thenReturn(404);
when(storedProcedure.read()).thenThrow(cosmosException);

// Existing container properties
CosmosContainerResponse response = mock(CosmosContainerResponse.class);
when(database.createContainerIfNotExists(table, "/concatenatedPartitionKey"))
.thenReturn(response);
CosmosContainerProperties properties = mock(CosmosContainerProperties.class);
when(response.getProperties()).thenReturn(properties);

Comment on lines +1013 to +1019
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

// Act
admin.repairTable(namespace, table, tableMetadata, Collections.emptyMap());

Expand Down Expand Up @@ -1024,6 +1045,49 @@ public void repairTable_ShouldCreateTableIfNotExistsAndUpsertMetadata()
verify(scripts).createStoredProcedure(any());
}

@Test
public void repairTable_WhenTableAlreadyExistsWithoutIndex_ShouldCreateIndex()
throws ExecutionException {
// Arrange
String namespace = "ns";
String table = "tbl";
TableMetadata tableMetadata =
TableMetadata.newBuilder()
.addColumn("c1", DataType.INT)
.addColumn("c2", DataType.TEXT)
.addColumn("c3", DataType.BIGINT)
.addPartitionKey("c1")
.addSecondaryIndex("c3")
.build();
when(client.getDatabase(namespace)).thenReturn(database);
when(database.getContainer(table)).thenReturn(container);
// Metadata container
CosmosContainer metadataContainer = mock(CosmosContainer.class);
CosmosDatabase metadataDatabase = mock(CosmosDatabase.class);
when(client.getDatabase(METADATA_DATABASE)).thenReturn(metadataDatabase);
when(metadataDatabase.getContainer(CosmosAdmin.TABLE_METADATA_CONTAINER))
.thenReturn(metadataContainer);
// Stored procedure exists
CosmosScripts scripts = mock(CosmosScripts.class);
when(container.getScripts()).thenReturn(scripts);
CosmosStoredProcedure storedProcedure = mock(CosmosStoredProcedure.class);
when(scripts.getStoredProcedure(CosmosAdmin.STORED_PROCEDURE_FILE_NAME))
.thenReturn(storedProcedure);
// Existing container properties
CosmosContainerResponse response = mock(CosmosContainerResponse.class);
when(database.createContainerIfNotExists(table, "/concatenatedPartitionKey"))
.thenReturn(response);
CosmosContainerProperties properties = mock(CosmosContainerProperties.class);
when(response.getProperties()).thenReturn(properties);

// Act
admin.repairTable(namespace, table, tableMetadata, Collections.emptyMap());

// Assert
verify(container, times(1)).replace(properties);
verify(properties, times(1)).setIndexingPolicy(any(IndexingPolicy.class));
}

@Test
public void addNewColumnToTable_ShouldWorkProperly() throws ExecutionException {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,75 @@ public void repairTable_WithNonExistingTableAndMetadataTables_shouldCreateBothTa
.build());
}

@Test
public void repairTable_WhenTableAlreadyExistsWithoutIndex_ShouldCreateIndex()
throws ExecutionException {
// Arrange
TableMetadata metadata =
TableMetadata.newBuilder()
.addPartitionKey("c1")
.addColumn("c1", DataType.TEXT)
.addColumn("c2", DataType.INT)
.addColumn("c3", DataType.INT)
.addSecondaryIndex("c3")
.build();
// The table to repair exists
when(client.describeTable(DescribeTableRequest.builder().tableName(getFullTableName()).build()))
.thenReturn(tableIsActiveResponse);
// The metadata table exists
when(client.describeTable(
DescribeTableRequest.builder().tableName(getFullMetadataTableName()).build()))
.thenReturn(tableIsActiveResponse);
// Continuous backup check
when(client.describeContinuousBackups(any(DescribeContinuousBackupsRequest.class)))
.thenReturn(backupIsEnabledResponse);
// Table metadata to return in createIndex
GetItemResponse getItemResponse = mock(GetItemResponse.class);
when(client.getItem(any(GetItemRequest.class))).thenReturn(getItemResponse);
when(getItemResponse.item())
.thenReturn(
ImmutableMap.<String, AttributeValue>builder()
.put(
DynamoAdmin.METADATA_ATTR_TABLE,
AttributeValue.builder().s(getFullTableName()).build())
.put(
DynamoAdmin.METADATA_ATTR_COLUMNS,
AttributeValue.builder()
.m(
ImmutableMap.<String, AttributeValue>builder()
.put("c1", AttributeValue.builder().s("text").build())
.put("c2", AttributeValue.builder().s("int").build())
.put("c3", AttributeValue.builder().s("int").build())
.build())
.build())
.put(
DynamoAdmin.METADATA_ATTR_PARTITION_KEY,
AttributeValue.builder().l(AttributeValue.builder().s("c1").build()).build())
.put(
DynamoAdmin.METADATA_ATTR_SECONDARY_INDEX,
AttributeValue.builder().ss("c3").build())
.build());
// For waitForTableCreation in createIndex
DescribeTableResponse describeTableResponse = mock(DescribeTableResponse.class);
when(client.describeTable(any(DescribeTableRequest.class))).thenReturn(describeTableResponse);
TableDescription tableDescription = mock(TableDescription.class);
when(describeTableResponse.table()).thenReturn(tableDescription);
GlobalSecondaryIndexDescription globalSecondaryIndexDescription =
mock(GlobalSecondaryIndexDescription.class);
when(tableDescription.globalSecondaryIndexes())
.thenReturn(Collections.emptyList())
.thenReturn(Collections.singletonList(globalSecondaryIndexDescription));
String indexName = getFullTableName() + ".global_index.c3";
when(globalSecondaryIndexDescription.indexName()).thenReturn(indexName);
when(globalSecondaryIndexDescription.indexStatus()).thenReturn(IndexStatus.ACTIVE);

// Act
admin.repairTable(NAMESPACE, TABLE, metadata, ImmutableMap.of());

// Assert
verify(client, times(1)).updateTable(any(UpdateTableRequest.class));
}

@Test
public void addNewColumnToTable_ShouldWorkProperly() throws ExecutionException {
// Arrange
Expand Down
30 changes: 30 additions & 0 deletions core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,36 @@ public void repairTable_ShouldCallCreateTableAndAddTableMetadataCorrectly(RdbEng
verify(adminSpy).addTableMetadata(connection, namespace, table, metadata, true, true);
}

@ParameterizedTest
@EnumSource(RdbEngine.class)
public void repairTable_WhenTableAlreadyExistsWithoutIndex_ShouldCreateIndex(RdbEngine rdbEngine)
throws ExecutionException, SQLException {
// Arrange
String namespace = "my_ns";
String table = "foo_table";
TableMetadata metadata =
TableMetadata.newBuilder()
.addPartitionKey("c1")
.addClusteringKey("c2")
.addColumn("c1", DataType.INT)
.addColumn("c2", DataType.TEXT)
.addColumn("c3", DataType.BOOLEAN)
.addColumn("c4", DataType.BLOB)
.addSecondaryIndex("c3")
.addSecondaryIndex("c4")
.build();
when(connection.createStatement()).thenReturn(mock(Statement.class));
when(dataSource.getConnection()).thenReturn(connection);
JdbcAdmin adminSpy = spy(createJdbcAdminFor(rdbEngine));

// Act
adminSpy.repairTable(namespace, table, metadata, Collections.emptyMap());

// Assert
verify(adminSpy).createIndex(connection, namespace, table, "c3", true);
verify(adminSpy).createIndex(connection, namespace, table, "c4", true);
}

@Test
public void
createMetadataTableIfNotExists_WithInternalDbError_forMysql_shouldThrowInternalDbError()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.scalar.db.api;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import com.scalar.db.exception.storage.ExecutionException;
import com.scalar.db.io.DataType;
Expand Down Expand Up @@ -212,6 +213,21 @@ public void repairTable_ForNonExistingTableButExistingMetadata_ShouldCreateTable
assertThat(admin.getTableMetadata(getNamespace(), getTable())).isEqualTo(getTableMetadata());
}

@Test
public void
repairTable_WhenTableAlreadyExistsWithoutIndexAndMetadataSpecifiesIndex_ShouldCreateIndex()
throws Exception {
// Arrange
admin.dropIndex(getNamespace(), getTable(), COL_NAME5);

// Act
admin.repairTable(getNamespace(), getTable(), getTableMetadata(), getCreationOptions());
Comment on lines +220 to +224
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepare a situation where the index does not exist in the actual table, but the index is specified in the table metadata given when repairTable is executed.


// Assert
assertThatCode(() -> admin.dropIndex(getNamespace(), getTable(), COL_NAME5))
.doesNotThrowAnyException();
Comment on lines +227 to +228
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the fact that admin.dropIndex() doesn’t throw an exception really confirm that the index has been created?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brfrn169 admin.dropIndex drops an index in the underlying storage and updates the metadata. Therefore, if the method succeeds, my thinking is that the index can be considered to have been deleted. What do you think?

}

@Test
public void repairNamespace_ForExistingNamespace_ShouldDoNothing() throws Exception {
// Act
Expand Down