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 @@ -4,6 +4,7 @@

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.data.ByteUtils;
import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata;
import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -13,6 +14,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;

Expand Down Expand Up @@ -114,6 +116,62 @@ public static Map<String, String> createCustomProperties(
return customProperties;
}

/**
* Computes the extensions payload that drops {@code indexName}'s vector-index profile from the
* table that owns it. Used to keep the {@link
* SchemaConstants.MetadataFieldsNames#VECTOR_INDEX_PROFILES} extension in sync when an index is
* dropped, so a profile record does not outlive its index.
*
* <p>The owning table is found by scanning {@code keyspaceMetadata} for the table whose indexes
* contain {@code indexName}. Returns empty when there is nothing to do — no table owns the index,
* or the owning table has no stored profile for it — so the caller can skip the extra DDL.
*
* <p>When a rewrite is needed, the existing vectorize config and the other indexes' profiles are
* read back and included so the clobbering extension write does not lose them (the same approach
* as the create side, see {@link #createCustomProperties(Map, Map, ObjectMapper)}).
*/
public static Optional<IndexProfileRemoval> removeIndexProfile(
KeyspaceMetadata keyspaceMetadata, CqlIdentifier indexName, ObjectMapper objectMapper) {
Objects.requireNonNull(keyspaceMetadata, "keyspaceMetadata must not be null");
Objects.requireNonNull(indexName, "indexName must not be null");
Objects.requireNonNull(objectMapper, "objectMapper must not be null");

var owningTable =
keyspaceMetadata.getTables().values().stream()
.filter(table -> table.getIndexes().containsKey(indexName))
.findFirst();
if (owningTable.isEmpty()) {
return Optional.empty();
}

var tableMetadata = owningTable.get();
var profiles = VectorIndexProfileDefinition.from(tableMetadata, objectMapper);
// null def => remove; false return => no entry existed, so there is nothing to rewrite.
if (!VectorIndexProfileDefinition.putOrRemove(
profiles, cqlIdentifierToJsonKey(indexName), null)) {
return Optional.empty();
}

// Read the vectorize config back so the full-replace extension write preserves it. The stored
// keys are the column identifiers' internal form, so reconstruct the CqlIdentifier keys that
// createCustomProperties expects.
var vectorDefs =
VectorizeDefinition.from(tableMetadata, objectMapper).entrySet().stream()
.collect(
Collectors.toMap(
entry -> CqlIdentifier.fromInternal(entry.getKey()), Map.Entry::getValue));

var customProperties = createCustomProperties(vectorDefs, profiles, objectMapper);
return Optional.of(new IndexProfileRemoval(tableMetadata.getName(), customProperties));
}

/**
* The result of {@link #removeIndexProfile}: the table to alter and the complete extensions
* payload to write (with the dropped index's profile removed and everything else preserved).
*/
public record IndexProfileRemoval(
CqlIdentifier tableName, Map<String, String> customProperties) {}

private static String writeJson(Object value, ObjectMapper objectMapper) {
try {
return objectMapper.writeValueAsString(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.stargate.sgv2.jsonapi.service.operation.tables;

import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.alterTable;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.DefaultDriverExceptionHandler;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableExtensions;
import io.stargate.sgv2.jsonapi.service.operation.SchemaDBTask;
import io.stargate.sgv2.jsonapi.service.schema.KeyspaceSchemaObject;
import java.util.Map;
import java.util.Objects;

/**
* Removes a dropped index's entry from its owning table's vector-index-profiles extension, so a
* profile record does not outlive the index it described.
*
* <p>This runs as a keyspace-scoped sibling to {@link DropIndexDBTask} so the two can share one
* {@link io.stargate.sgv2.jsonapi.service.operation.tasks.TaskGroup}; a {@link
* io.stargate.sgv2.jsonapi.service.schema.tables.TableSchemaObject}-typed {@link AlterTableDBTask}
* (used by the create side) could not, because a TaskGroup has a single schema-object type. The
* owning table and the rewritten extensions payload are resolved at command-resolve time via {@link
* TableExtensions#removeIndexProfile}; this task only issues the {@code ALTER TABLE ... WITH
* extensions = {...}}.
*/
public class DropVectorIndexProfileDBTask extends SchemaDBTask<KeyspaceSchemaObject> {

private final CqlIdentifier tableName;
private final Map<String, String> customProperties;

public DropVectorIndexProfileDBTask(
int position,
KeyspaceSchemaObject schemaObject,
SchemaDBTask.SchemaRetryPolicy schemaRetryPolicy,
DefaultDriverExceptionHandler.Factory<KeyspaceSchemaObject> exceptionHandlerFactory,
CqlIdentifier tableName,
Map<String, String> customProperties) {
super(position, schemaObject, schemaRetryPolicy, exceptionHandlerFactory);

this.tableName = Objects.requireNonNull(tableName, "tableName must not be null");
this.customProperties =
Objects.requireNonNull(customProperties, "customProperties must not be null");
setStatus(TaskStatus.READY);
}

public static DropVectorIndexProfileDBTaskBuilder builder(KeyspaceSchemaObject schemaObject) {
return new DropVectorIndexProfileDBTaskBuilder(schemaObject);
}

@Override
protected SimpleStatement buildStatement() {

// The owning table lives in this keyspace; take the keyspace from the schema object identifier,
// mirroring DropIndexDBTask which builds its statement the same way.
var extensions = TableExtensions.toExtensions(customProperties);
return alterTable(schemaObject.identifier().keyspace(), tableName)
.withOption(TableExtensions.TABLE_OPTIONS_EXTENSION_KEY.asInternal(), extensions)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.stargate.sgv2.jsonapi.service.operation.tables;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import io.stargate.sgv2.jsonapi.service.operation.SchemaDBTask;
import io.stargate.sgv2.jsonapi.service.operation.tasks.TaskBuilder;
import io.stargate.sgv2.jsonapi.service.schema.KeyspaceSchemaObject;
import java.util.Map;
import java.util.Objects;

/** Builds a {@link DropVectorIndexProfileDBTask}. */
public class DropVectorIndexProfileDBTaskBuilder
extends TaskBuilder<
DropVectorIndexProfileDBTask, KeyspaceSchemaObject, DropVectorIndexProfileDBTaskBuilder> {

private CqlIdentifier tableName;
private Map<String, String> customProperties;
private SchemaDBTask.SchemaRetryPolicy schemaRetryPolicy;

protected DropVectorIndexProfileDBTaskBuilder(KeyspaceSchemaObject schemaObject) {
super(schemaObject);
}

public DropVectorIndexProfileDBTaskBuilder withTableName(CqlIdentifier tableName) {
this.tableName = Objects.requireNonNull(tableName, "tableName must not be null");
return this;
}

public DropVectorIndexProfileDBTaskBuilder withCustomProperties(
Map<String, String> customProperties) {
this.customProperties =
Objects.requireNonNull(customProperties, "customProperties must not be null");
return this;
}

public DropVectorIndexProfileDBTaskBuilder withSchemaRetryPolicy(
SchemaDBTask.SchemaRetryPolicy schemaRetryPolicy) {
this.schemaRetryPolicy =
Objects.requireNonNull(schemaRetryPolicy, "schemaRetryPolicy cannot be null");
return this;
}

public DropVectorIndexProfileDBTask build() {

Objects.requireNonNull(tableName, "tableName must not be null");
Objects.requireNonNull(customProperties, "customProperties must not be null");
Objects.requireNonNull(schemaRetryPolicy, "schemaRetryPolicy cannot be null");

return new DropVectorIndexProfileDBTask(
nextPosition(),
schemaObject,
schemaRetryPolicy,
getExceptionHandlerFactory(),
tableName,
customProperties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@

import static io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil.cqlIdentifierFromUserInput;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.impl.DropIndexCommand;
import io.stargate.sgv2.jsonapi.config.OperationsConfig;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.DefaultDriverExceptionHandler;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableExtensions;
import io.stargate.sgv2.jsonapi.service.operation.Operation;
import io.stargate.sgv2.jsonapi.service.operation.SchemaDBTask;
import io.stargate.sgv2.jsonapi.service.operation.SchemaDBTaskPage;
import io.stargate.sgv2.jsonapi.service.operation.keyspaces.KeyspaceDriverExceptionHandler;
import io.stargate.sgv2.jsonapi.service.operation.tables.DropIndexDBTask;
import io.stargate.sgv2.jsonapi.service.operation.tables.DropIndexExceptionHandler;
import io.stargate.sgv2.jsonapi.service.operation.tables.DropVectorIndexProfileDBTask;
import io.stargate.sgv2.jsonapi.service.operation.tasks.TaskGroup;
import io.stargate.sgv2.jsonapi.service.operation.tasks.TaskOperation;
import io.stargate.sgv2.jsonapi.service.schema.KeyspaceSchemaObject;
import io.stargate.sgv2.jsonapi.util.ApiOptionUtils;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.Duration;

/** Resolver for the {@link DropIndexCommand}. */
Expand All @@ -24,6 +30,8 @@ public class DropIndexCommandResolver implements CommandResolver<DropIndexComman

private static final boolean IF_EXISTS_DEFAULT = false;

@Inject ObjectMapper objectMapper;

@Override
public Class<DropIndexCommand> getCommandClass() {
return DropIndexCommand.class;
Expand All @@ -33,39 +41,79 @@ public Class<DropIndexCommand> getCommandClass() {
public Operation<KeyspaceSchemaObject> resolveKeyspaceCommand(
CommandContext<KeyspaceSchemaObject> commandContext, DropIndexCommand command) {

var schemaObject = commandContext.schemaObject();
var indexName = cqlIdentifierFromUserInput(command.name());
// Check if the index exists, we check if columns exist before trying to drop them so do for
// indexes as well

var taskBuilder =
DropIndexDBTask.builder(commandContext.schemaObject())
.withSchemaRetryPolicy(
new SchemaDBTask.SchemaRetryPolicy(
commandContext
.config()
.get(OperationsConfig.class)
.databaseConfig()
.ddlRetries(),
Duration.ofMillis(
commandContext
.config()
.get(OperationsConfig.class)
.databaseConfig()
.ddlRetryDelayMillis())));

taskBuilder.withExceptionHandlerFactory(
DefaultDriverExceptionHandler.Factory.withIdentifier(
DropIndexExceptionHandler::new, indexName));

taskBuilder
.withIndexName(indexName)
.withIfExists(
ApiOptionUtils.getOrDefault(
command.options(), DropIndexCommand.Options::ifExists, IF_EXISTS_DEFAULT));

var taskGroup = new TaskGroup<>(taskBuilder.build());

return new TaskOperation<>(
taskGroup, SchemaDBTaskPage.accumulator(DropIndexDBTask.class, commandContext));
var schemaRetryPolicy =
new SchemaDBTask.SchemaRetryPolicy(
commandContext.config().get(OperationsConfig.class).databaseConfig().ddlRetries(),
Duration.ofMillis(
commandContext
.config()
.get(OperationsConfig.class)
.databaseConfig()
.ddlRetryDelayMillis()));

var dropIndexTask =
DropIndexDBTask.builder(schemaObject)
.withSchemaRetryPolicy(schemaRetryPolicy)
.withExceptionHandlerFactory(
DefaultDriverExceptionHandler.Factory.withIdentifier(
DropIndexExceptionHandler::new, indexName))
.withIndexName(indexName)
.withIfExists(
ApiOptionUtils.getOrDefault(
command.options(), DropIndexCommand.Options::ifExists, IF_EXISTS_DEFAULT))
.build();

// Also drop the index's vector-index profile (if any) from the owning table's extensions, so
// the profile record does not outlive the index. Null when the keyspace metadata is unknown or
// the owning table has no stored profile for this index, in which case only the drop runs.
var profileCleanupTask = buildProfileCleanupTask(schemaObject, indexName, schemaRetryPolicy);

if (profileCleanupTask == null) {
return new TaskOperation<>(
new TaskGroup<>(dropIndexTask),
SchemaDBTaskPage.accumulator(DropIndexDBTask.class, commandContext));
}

// Sequential so the extension cleanup only runs if the index drop succeeded.
TaskGroup<SchemaDBTask<KeyspaceSchemaObject>, KeyspaceSchemaObject> taskGroup =
new TaskGroup<>(true);
taskGroup.add(dropIndexTask);
taskGroup.add(profileCleanupTask);

@SuppressWarnings("unchecked")
Class<SchemaDBTask<KeyspaceSchemaObject>> taskClass =
(Class<SchemaDBTask<KeyspaceSchemaObject>>) (Class<?>) SchemaDBTask.class;
return new TaskOperation<>(taskGroup, SchemaDBTaskPage.accumulator(taskClass, commandContext));
}

/**
* Builds the cleanup task that removes the dropped index's profile from its owning table's
* extensions, or null when there is nothing to clean up (keyspace metadata unknown, no owning
* table, or no stored profile for this index).
*/
private DropVectorIndexProfileDBTask buildProfileCleanupTask(
KeyspaceSchemaObject schemaObject,
CqlIdentifier indexName,
SchemaDBTask.SchemaRetryPolicy schemaRetryPolicy) {

return schemaObject
.keyspaceMetadata()
.flatMap(
keyspaceMetadata ->
TableExtensions.removeIndexProfile(keyspaceMetadata, indexName, objectMapper))
.map(
removal ->
DropVectorIndexProfileDBTask.builder(schemaObject)
.withSchemaRetryPolicy(schemaRetryPolicy)
.withExceptionHandlerFactory(KeyspaceDriverExceptionHandler::new)
.withTableName(removal.tableName())
.withCustomProperties(removal.customProperties())
.build())
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.IndexUsage;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.VectorConfig;
import java.util.Objects;
import java.util.Optional;

/**
* A Keyspace in the API.
Expand Down Expand Up @@ -39,6 +40,18 @@ public KeyspaceSchemaObject(Tenant tenant, KeyspaceMetadata keyspaceMetadata) {
Objects.requireNonNull(keyspaceMetadata, "keyspaceMetadata must not be null");
}

/**
* The Cassandra metadata for this keyspace, when known.
*
* <p>Empty when the object was built via the {@link
* #KeyspaceSchemaObject(SchemaObjectIdentifier)} test constructor, which carries no metadata.
* Present for objects built from live schema, where it lets callers reach the keyspace's tables
* and their indexes (e.g. to find the table that owns a named index).
*/
public Optional<KeyspaceMetadata> keyspaceMetadata() {
return Optional.ofNullable(keyspaceMetadata);
}

@Override
public VectorConfig vectorConfig() {
return VectorConfig.NOT_ENABLED_CONFIG;
Expand Down
Loading