diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CollectionCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CollectionCommand.java index 825e12085a..8208e744b1 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CollectionCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CollectionCommand.java @@ -20,7 +20,8 @@ @JsonSubTypes.Type(value = InsertManyCommand.class), @JsonSubTypes.Type(value = UpdateManyCommand.class), @JsonSubTypes.Type(value = UpdateOneCommand.class), - @JsonSubTypes.Type(value = AddIndexCommand.class), + // We have only collection resource that is used for api tables + @JsonSubTypes.Type(value = CreateIndexCommand.class), @JsonSubTypes.Type(value = DropIndexCommand.class), }) public interface CollectionCommand extends Command {} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java index bc18ad5815..e52640c0d8 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java @@ -39,11 +39,11 @@ public interface Command { /** Enum class for API command name. This is what user uses for command json body. */ enum CommandName { - ADD_INDEX("addIndex"), COUNT_DOCUMENTS("countDocuments"), CREATE_COLLECTION("createCollection"), - CREATE_NAMESPACE("createNamespace"), + CREATE_INDEX("createIndex"), CREATE_KEYSPACE("createKeyspace"), + CREATE_NAMESPACE("createNamespace"), CREATE_TABLE("createTable"), DELETE_COLLECTION("deleteCollection"), DELETE_MANY("deleteMany"), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/AddIndexCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/AddIndexCommand.java deleted file mode 100644 index 4751f13f72..0000000000 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/AddIndexCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.stargate.sgv2.jsonapi.api.model.command.impl; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand; -import io.stargate.sgv2.jsonapi.api.model.command.NoOptionsCommand; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -// TODO, hide table feature detail before it goes public, -// https://github.com/stargate/data-api/pull/1360 -// @Schema(description = "Command that creates an index for a column in a table.") -@JsonTypeName("addIndex") -public record AddIndexCommand( - @NotNull - @Size(min = 1, max = 48) - @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*") - @Schema(description = "Name of the column to create the index on") - String column, - @NotNull - @Size(min = 1, max = 48) - @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*") - @Schema(description = "Unique name for the index.") - String indexName) - implements NoOptionsCommand, CollectionCommand { - - /** {@inheritDoc} */ - @Override - public CommandName commandName() { - return CommandName.ADD_INDEX; - } -} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateIndexCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateIndexCommand.java new file mode 100644 index 0000000000..00153d7ced --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateIndexCommand.java @@ -0,0 +1,94 @@ +package io.stargate.sgv2.jsonapi.api.model.command.impl; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand; +import io.stargate.sgv2.jsonapi.service.schema.SimilarityFunction; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +// TODO, hide table feature detail before it goes public +@Schema(description = "Command that creates an index for a column in a table.") +@JsonTypeName("createIndex") +public record CreateIndexCommand( + @NotNull + @Size(min = 1, max = 48) + @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*") + @Schema(description = "Name of the column to create the index on") + String name, + @NotNull + @Schema( + description = "Definition for created index for a column.", + type = SchemaType.OBJECT) + Definition definition, + @Nullable @Schema(description = "Creating index command option.", type = SchemaType.OBJECT) + Options options) + implements CollectionCommand { + public record Definition( + @NotNull + @Size(min = 1, max = 48) + @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*") + @Schema(description = "Name of the column for which index to be created.") + String column, + @Nullable @Schema(description = "Different indexing options.", type = SchemaType.OBJECT) + Options options) { + // This is index definition options for text and vector column types. + public record Options( + @Nullable + @Schema( + description = "Ignore case in matching string values.", + defaultValue = "true", + type = SchemaType.BOOLEAN, + implementation = Boolean.class) + Boolean caseSensitive, + @Nullable + @Schema( + description = "When set to true, perform Unicode normalization on indexed strings.", + defaultValue = "false", + type = SchemaType.BOOLEAN, + implementation = Boolean.class) + Boolean normalize, + @Nullable + @Schema( + description = + "When set to true, index will converts alphabetic, numeric, and symbolic characters to the ascii equivalent, if one exists.", + defaultValue = "false", + type = SchemaType.BOOLEAN, + implementation = Boolean.class) + Boolean ascii, + @Nullable + @Pattern( + regexp = "(dot_product|cosine|euclidean)", + message = "function name can only be 'dot_product', 'cosine' or 'euclidean'") + @Schema( + description = + "Similarity function algorithm that needs to be used for vector search", + defaultValue = "cosine", + type = SchemaType.STRING, + implementation = String.class) + SimilarityFunction metric, + @Nullable + @Size(min = 1, max = 48) + @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*") + @Schema(description = "Model name used to generate the embeddings.") + String sourceModel) {} + } + + // This is index command option irrespective of column definition. + public record Options( + @Schema( + description = "Flag to ignore if index already exists", + defaultValue = "false", + type = SchemaType.BOOLEAN, + implementation = Boolean.class) + Boolean ifNotExists) {} + + /** {@inheritDoc} */ + @Override + public CommandName commandName() { + return CommandName.CREATE_INDEX; + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java index ec46b6aa23..229ffe6d56 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java @@ -109,10 +109,10 @@ public CollectionResource(MeteredCommandProcessor meteredCommandProcessor) { InsertOneCommand.class, InsertManyCommand.class, UpdateManyCommand.class, - UpdateOneCommand.class + UpdateOneCommand.class, // TODO, hide table feature detail before it goes public, // https://github.com/stargate/data-api/pull/1360 - // AddIndexCommand.class, + // CreateIndexCommand.class, // DropIndexCommand.class }), examples = { diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/constants/VectorConstant.java b/src/main/java/io/stargate/sgv2/jsonapi/config/constants/VectorConstant.java new file mode 100644 index 0000000000..dee738cf5b --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/constants/VectorConstant.java @@ -0,0 +1,14 @@ +package io.stargate.sgv2.jsonapi.config.constants; + +import io.smallrye.config.ConfigMapping; +import java.util.Set; + +@ConfigMapping(prefix = "stargate.jsonapi.vector") +public interface VectorConstant { + /* + Supported Source Models for Vector Index in Cassandra + */ + Set SUPPORTED_SOURCES = + Set.of( + "ada002", "openai_v3_small", "openai_v3_large", "bert", "gecko", "nv_qa_4", "cohere_v3"); +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/SchemaException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/SchemaException.java index ff9ec460fd..622f788c93 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/SchemaException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/SchemaException.java @@ -18,6 +18,7 @@ public enum Code implements ErrorCode { COLUMN_TYPE_INCORRECT, COLUMN_TYPE_UNSUPPORTED, INVALID_CONFIGURATION, + INVALID_INDEX_DEFINITION, INVALID_VECTORIZE_CONFIGURATION, LIST_TYPE_INCORRECT_DEFINITION, MAP_TYPE_INCORRECT_DEFINITION, diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedCreateIndex.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedCreateIndex.java new file mode 100644 index 0000000000..1ae9d39232 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedCreateIndex.java @@ -0,0 +1,90 @@ +package io.stargate.sgv2.jsonapi.service.cqldriver.override; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.schema.DefaultCreateIndex; +import com.datastax.oss.driver.internal.querybuilder.schema.OptionsUtils; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.UnmodifiableIterator; +import java.util.Map; + +/** + * An extension of the {@link DefaultCreateIndex} class, This is needed because the column name + * appended to the builder needs to use `asCql(true)` to keep the quotes. + */ +public class ExtendedCreateIndex extends DefaultCreateIndex { + + public ExtendedCreateIndex(DefaultCreateIndex defaultCreateIndex) { + + super( + defaultCreateIndex.getIndex(), + defaultCreateIndex.isIfNotExists(), + defaultCreateIndex.getKeyspace(), + defaultCreateIndex.getTable(), + defaultCreateIndex.getColumnToIndexType(), + defaultCreateIndex.getUsingClass(), + // This is fine as the internal options object is ImmutableMap + (ImmutableMap) defaultCreateIndex.getOptions()); + } + + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("CREATE "); + if (this.getUsingClass() != null) { + builder.append("CUSTOM "); + } + + builder.append("INDEX"); + if (this.isIfNotExists()) { + builder.append(" IF NOT EXISTS"); + } + + if (this.getIndex() != null) { + builder.append(' ').append(this.getIndex().asCql(true)); + } + + if (this.getTable() == null) { + return builder.toString(); + } else { + builder.append(" ON "); + CqlHelper.qualify(this.getKeyspace(), this.getTable(), builder); + if (this.getColumnToIndexType().isEmpty()) { + return builder.toString(); + } else { + builder.append(" ("); + boolean firstColumn = true; + UnmodifiableIterator var3 = this.getColumnToIndexType().entrySet().iterator(); + + while (var3.hasNext()) { + Map.Entry entry = (Map.Entry) var3.next(); + if (firstColumn) { + firstColumn = false; + } else { + builder.append(","); + } + + if (((String) entry.getValue()).equals("__NO_INDEX_TYPE")) { + builder.append(entry.getKey().asCql(true)); + } else { + builder + .append((String) entry.getValue()) + .append("(") + .append(entry.getKey().asCql(true)) + .append(")"); + } + } + + builder.append(")"); + if (this.getUsingClass() != null) { + builder.append(" USING '").append(this.getUsingClass()).append('\''); + } + + if (!this.getOptions().isEmpty()) { + builder.append(OptionsUtils.buildOptions(this.getOptions(), true)); + } + + return builder.toString(); + } + } + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedVectorType.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedVectorType.java new file mode 100644 index 0000000000..5ee0d617aa --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedVectorType.java @@ -0,0 +1,19 @@ +package io.stargate.sgv2.jsonapi.service.cqldriver.override; + +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.internal.core.type.DefaultVectorType; + +/** + * Extended vector type to support vector size This is needed because java drivers + * DataTypes.vectorOf() method has a bug + */ +public class ExtendedVectorType extends DefaultVectorType { + public ExtendedVectorType(DataType subtype, int vectorSize) { + super(subtype, vectorSize); + } + + @Override + public String asCql(boolean includeFrozen, boolean pretty) { + return "VECTOR<" + getElementType().asCql(includeFrozen, pretty) + "," + getDimensions() + ">"; + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/AddIndexOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/AddIndexOperation.java deleted file mode 100644 index 65f28bcb0e..0000000000 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/AddIndexOperation.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.stargate.sgv2.jsonapi.service.operation.tables; - -import com.datastax.oss.driver.api.core.cql.SimpleStatement; -import io.smallrye.mutiny.Uni; -import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; -import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; -import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; -import io.stargate.sgv2.jsonapi.service.cqldriver.executor.QueryExecutor; -import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; -import io.stargate.sgv2.jsonapi.service.operation.Operation; -import io.stargate.sgv2.jsonapi.service.operation.collections.SchemaChangeResult; -import java.util.function.Supplier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of the add index operation. - * - * @param context Command context, carries keyspace of the table. - * @param columnName Name of the column to create the index on. - * @param indexName Unique name for the index. - */ -public record AddIndexOperation( - CommandContext context, String columnName, String indexName) - implements Operation { - private static final Logger logger = LoggerFactory.getLogger(AddIndexOperation.class); - - private static final String ADD_INDEX_TEMPLATE = - "CREATE CUSTOM INDEX IF NOT EXISTS \"%s\" ON %s.%s (\"%s\") USING 'StorageAttachedIndex'"; - - @Override - public Uni> execute( - DataApiRequestInfo dataApiRequestInfo, QueryExecutor queryExecutor) { - logger.info( - "Executing AddIndexOperation for {} {} {} {}", - context.schemaObject().name().keyspace(), - context.schemaObject().name().table(), - columnName, - indexName); - String cql = - ADD_INDEX_TEMPLATE.formatted( - indexName, - context.schemaObject().name().keyspace(), - context.schemaObject().name().table(), - columnName); - SimpleStatement query = SimpleStatement.newInstance(cql); - // execute - return queryExecutor - .executeDropSchemaChange(dataApiRequestInfo, query) - - // if we have a result always respond positively - .map(any -> new SchemaChangeResult(any.wasApplied())); - } -} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttempt.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttempt.java new file mode 100644 index 0000000000..a78f76b3dd --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttempt.java @@ -0,0 +1,143 @@ +package io.stargate.sgv2.jsonapi.service.operation.tables; + +import static io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil.cqlIdentifierFromUserInput; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.querybuilder.SchemaBuilder; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndex; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndexOnTable; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndexStart; +import com.datastax.oss.driver.internal.querybuilder.schema.DefaultCreateIndex; +import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; +import io.stargate.sgv2.jsonapi.service.cqldriver.override.ExtendedCreateIndex; +import io.stargate.sgv2.jsonapi.service.operation.SchemaAttempt; +import io.stargate.sgv2.jsonapi.service.schema.SimilarityFunction; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/* + An attempt to create index for a table's column. +*/ +public class CreateIndexAttempt extends SchemaAttempt { + private final CqlIdentifier columnName; + private final CqlIdentifier indexName; + private final TextIndexOptions textIndexOptions; + private final VectorIndexOptions vectorIndexOptions; + private final DataType dataType; + private final boolean ifNotExists; + + /* + * @param position The position of the attempt in the sequence, for create index it's always 0. + * @param schemaObject The schema object representing the table. + * @param columnName The name of the column to create the index on. + * @param dataType The data type of the column. + * @param indexName The name of the index to create. + * @param textIndexOptions The options for a text column type index. + * @param vectorIndexOptions The options for a vector column type index. + * @param ifNotExists Flag to ignore if index already exists. + * @return The attempt to create the index. + */ + protected CreateIndexAttempt( + int position, + TableSchemaObject schemaObject, + CqlIdentifier columnName, + DataType dataType, + CqlIdentifier indexName, + TextIndexOptions textIndexOptions, + VectorIndexOptions vectorIndexOptions, + boolean ifNotExists) { + super(position, schemaObject, new SchemaRetryPolicy(2, Duration.ofMillis(10))); + + this.columnName = columnName; + this.dataType = dataType; + this.indexName = indexName; + this.textIndexOptions = textIndexOptions; + this.vectorIndexOptions = vectorIndexOptions; + this.ifNotExists = ifNotExists; + setStatus(OperationStatus.READY); + } + + /* + * Options for a text index. + */ + public record TextIndexOptions(Boolean caseSensitive, Boolean normalize, Boolean ascii) { + + public Map getOptions() { + Map options = new HashMap<>(); + if (caseSensitive != null) { + options.put("case_sensitive", caseSensitive); + } + if (normalize != null) { + options.put("normalize", normalize); + } + if (ascii != null) { + options.put("ascii", ascii); + } + return options; + } + } + + /* + * Options for a vector index. + */ + public record VectorIndexOptions(SimilarityFunction similarityFunction, String sourceModel) { + public Map getOptions() { + Map options = new HashMap<>(); + if (similarityFunction != null) { + options.put("similarity_function", similarityFunction.getMetric()); + } + if (sourceModel != null) { + options.put("source_model", sourceModel); + } + return options; + } + } + + @Override + protected SimpleStatement buildStatement() { + CqlIdentifier keyspaceIdentifier = cqlIdentifierFromUserInput(schemaObject.name().keyspace()); + CqlIdentifier tableIdentifier = cqlIdentifierFromUserInput(schemaObject.name().table()); + + // Set as StorageAttachedIndex as default + CreateIndexStart createIndexStart = + SchemaBuilder.createIndex(indexName).custom("StorageAttachedIndex"); + + // If `ifNotExists` is true, then set the flag to ignore if index already exists + if (ifNotExists) { + createIndexStart = createIndexStart.ifNotExists(); + } + // Set the keyspace and table name + final CreateIndexOnTable createIndexOnTable = + createIndexStart.onTable(keyspaceIdentifier, tableIdentifier); + + // Set the column name + CreateIndex createIndex; + if (dataType instanceof MapType) { + createIndex = createIndexOnTable.andColumnEntries(columnName); + } else if (dataType instanceof ListType || dataType instanceof SetType) { + createIndex = createIndexOnTable.andColumnValues(columnName); + } else { + createIndex = createIndexOnTable.andColumn(columnName); + } + // Set the options for the index + Map options = new HashMap<>(); + if (textIndexOptions != null && !textIndexOptions.getOptions().isEmpty()) { + createIndex = createIndex.withOption("OPTIONS", textIndexOptions.getOptions()); + } + if (vectorIndexOptions != null && !vectorIndexOptions.getOptions().isEmpty()) { + createIndex = createIndex.withOption("OPTIONS", vectorIndexOptions.getOptions()); + } + + // Hack code to fix the issue with respect to quoted columns + ExtendedCreateIndex extendedCreateIndex = + new ExtendedCreateIndex((DefaultCreateIndex) createIndex); + + return extendedCreateIndex.build(); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttemptBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttemptBuilder.java new file mode 100644 index 0000000000..2981cbc0a6 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateIndexAttemptBuilder.java @@ -0,0 +1,66 @@ +package io.stargate.sgv2.jsonapi.service.operation.tables; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; +import io.stargate.sgv2.jsonapi.service.schema.SimilarityFunction; +import io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil; + +/** Builder for a {@link CreateIndexAttempt}. */ +public class CreateIndexAttemptBuilder { + private final int position; + private TableSchemaObject schemaObject; + private CqlIdentifier columnName; + private DataType dataType; + private CqlIdentifier indexName; + private CreateIndexAttempt.TextIndexOptions textIndexOptions; + private CreateIndexAttempt.VectorIndexOptions vectorIndexOptions; + private boolean ifNotExists; + + public CreateIndexAttemptBuilder( + int position, TableSchemaObject schemaObject, String columnName, String indexName) { + this.position = position; + this.schemaObject = schemaObject; + this.columnName = CqlIdentifierUtil.cqlIdentifierFromUserInput(columnName); + this.indexName = CqlIdentifierUtil.cqlIdentifierFromUserInput(indexName); + this.dataType = schemaObject.tableMetadata().getColumn(this.columnName).get().getType(); + } + + public CreateIndexAttemptBuilder ifNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + return this; + } + + public CreateIndexAttemptBuilder textIndexOptions( + Boolean caseSensitive, Boolean normalize, Boolean ascii) { + this.textIndexOptions = + new CreateIndexAttempt.TextIndexOptions(caseSensitive, normalize, ascii); + return this; + } + + public CreateIndexAttemptBuilder vectorIndexOptions( + SimilarityFunction similarityFunction, String sourceModel) { + this.vectorIndexOptions = + new CreateIndexAttempt.VectorIndexOptions(similarityFunction, sourceModel); + return this; + } + + public CreateIndexAttempt build() { + // Validate required fields + if (schemaObject == null || columnName == null || dataType == null || indexName == null) { + throw new IllegalStateException( + "SchemaObject, columnName, dataType, and indexName must not be null"); + } + + // Create and return the CreateIndexAttempt object + return new CreateIndexAttempt( + position, + schemaObject, + columnName, + dataType, + indexName, + textIndexOptions, + vectorIndexOptions, + ifNotExists); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/DropIndexOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/DropIndexOperation.java index 6b8dc731dd..4afacaafa9 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/DropIndexOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/DropIndexOperation.java @@ -29,7 +29,7 @@ public record DropIndexOperation(CommandContext context, Stri public Uni> execute( DataApiRequestInfo dataApiRequestInfo, QueryExecutor queryExecutor) { logger.info( - "Executing AddIndexOperation for {} {} {}", + "Executing DropIndexOperation for {} {} {}", context.schemaObject().name().keyspace(), context.schemaObject().name().table(), indexName); diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/AddIndexCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/AddIndexCommandResolver.java deleted file mode 100644 index 4edaf9b946..0000000000 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/AddIndexCommandResolver.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.stargate.sgv2.jsonapi.service.resolver; - -import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; -import io.stargate.sgv2.jsonapi.api.model.command.impl.AddIndexCommand; -import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; -import io.stargate.sgv2.jsonapi.service.operation.Operation; -import io.stargate.sgv2.jsonapi.service.operation.tables.AddIndexOperation; -import jakarta.enterprise.context.ApplicationScoped; - -/** Resolver for the {@link AddIndexCommand}. */ -@ApplicationScoped -public class AddIndexCommandResolver implements CommandResolver { - @Override - public Class getCommandClass() { - return AddIndexCommand.class; - } - - @Override - public Operation resolveTableCommand( - CommandContext ctx, AddIndexCommand command) { - return new AddIndexOperation(ctx, command.column(), command.indexName()); - } -} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/CreateIndexCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/CreateIndexCommandResolver.java new file mode 100644 index 0000000000..e08f673cb6 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/CreateIndexCommandResolver.java @@ -0,0 +1,123 @@ +package io.stargate.sgv2.jsonapi.service.resolver; + +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.VectorType; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateIndexCommand; +import io.stargate.sgv2.jsonapi.config.DebugModeConfig; +import io.stargate.sgv2.jsonapi.config.OperationsConfig; +import io.stargate.sgv2.jsonapi.config.constants.VectorConstant; +import io.stargate.sgv2.jsonapi.exception.SchemaException; +import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; +import io.stargate.sgv2.jsonapi.service.operation.GenericOperation; +import io.stargate.sgv2.jsonapi.service.operation.Operation; +import io.stargate.sgv2.jsonapi.service.operation.OperationAttemptContainer; +import io.stargate.sgv2.jsonapi.service.operation.SchemaAttemptPage; +import io.stargate.sgv2.jsonapi.service.operation.tables.CreateIndexAttemptBuilder; +import io.stargate.sgv2.jsonapi.service.operation.tables.TableDriverExceptionHandler; +import io.stargate.sgv2.jsonapi.service.schema.SimilarityFunction; +import io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Resolver for the {@link CreateIndexCommand}. */ +@ApplicationScoped +public class CreateIndexCommandResolver implements CommandResolver { + @Override + public Class getCommandClass() { + return CreateIndexCommand.class; + } + ; + + @Override + public Operation resolveTableCommand( + CommandContext ctx, CreateIndexCommand command) { + + String columnName = command.definition().column(); + String indexName = command.name(); + final CreateIndexCommand.Definition.Options definitionOptions = command.definition().options(); + + TableMetadata tableMetadata = ctx.schemaObject().tableMetadata(); + // Validate Column present in Table + final Optional column = + ctx.schemaObject() + .tableMetadata() + .getColumn(CqlIdentifierUtil.cqlIdentifierFromUserInput(columnName)); + ColumnMetadata columnMetadata = + column.orElseThrow( + () -> + SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of("reason", "Column not defined in the table"))); + Boolean caseSensitive = definitionOptions != null ? definitionOptions.caseSensitive() : null; + Boolean normalize = definitionOptions != null ? definitionOptions.normalize() : null; + Boolean ascii = definitionOptions != null ? definitionOptions.ascii() : null; + SimilarityFunction similarityFunction = + definitionOptions != null ? definitionOptions.metric() : null; + String sourceModel = definitionOptions != null ? definitionOptions.sourceModel() : null; + if (definitionOptions != null) { + // Validate Options + if (!columnMetadata.getType().equals(DataTypes.TEXT)) { + if (caseSensitive != null || normalize != null || ascii != null) { + throw SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "`caseSensitive`, `normalize` and `ascii` options are valid only for `text` column")); + } + } + if (!(columnMetadata.getType() instanceof VectorType)) { + if (similarityFunction != null || sourceModel != null) { + throw SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "`metric` and `sourceModel` options are valid only for `vector` type column")); + } + } else { + if (similarityFunction != null && sourceModel != null) { + throw SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "Only one of `metric` or `sourceModel` options should be used for `vector` type column")); + } + if (sourceModel != null && !VectorConstant.SUPPORTED_SOURCES.contains(sourceModel)) { + throw SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "Invalid `sourceModel`. Supported source models are: " + + VectorConstant.SUPPORTED_SOURCES)); + } + } + } + + // Command level option for ifNotExists + boolean ifNotExists = false; + final CreateIndexCommand.Options commandOptions = command.options(); + if (commandOptions != null && commandOptions.ifNotExists() != null) { + ifNotExists = commandOptions.ifNotExists(); + } + + // Default Similarity Function to COSINE + if (columnMetadata.getType() instanceof VectorType + && similarityFunction == null + && sourceModel == null) { + similarityFunction = SimilarityFunction.COSINE; + } + + var attempt = + new CreateIndexAttemptBuilder(0, ctx.schemaObject(), columnName, indexName) + .ifNotExists(ifNotExists) + .textIndexOptions(caseSensitive, normalize, ascii) + .vectorIndexOptions(similarityFunction, sourceModel) + .build(); + var attempts = new OperationAttemptContainer<>(List.of(attempt)); + var pageBuilder = + SchemaAttemptPage.builder() + .debugMode(ctx.getConfig(DebugModeConfig.class).enabled()) + .useErrorObjectV2(ctx.getConfig(OperationsConfig.class).extendError()); + + return new GenericOperation<>(attempts, pageBuilder, new TableDriverExceptionHandler()); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/DropIndexCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/DropIndexCommandResolver.java index dc2cad3b07..2005ec8d18 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/DropIndexCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/DropIndexCommandResolver.java @@ -1,14 +1,13 @@ package io.stargate.sgv2.jsonapi.service.resolver; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; -import io.stargate.sgv2.jsonapi.api.model.command.impl.AddIndexCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.DropIndexCommand; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject; import io.stargate.sgv2.jsonapi.service.operation.Operation; import io.stargate.sgv2.jsonapi.service.operation.tables.DropIndexOperation; import jakarta.enterprise.context.ApplicationScoped; -/** Resolver for the {@link AddIndexCommand}. */ +/** Resolver for the {@link DropIndexCommand}. */ @ApplicationScoped public class DropIndexCommandResolver implements CommandResolver { @Override diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/schema/SimilarityFunction.java b/src/main/java/io/stargate/sgv2/jsonapi/service/schema/SimilarityFunction.java index c0780122d6..c120bfaf64 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/schema/SimilarityFunction.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/schema/SimilarityFunction.java @@ -1,27 +1,44 @@ package io.stargate.sgv2.jsonapi.service.schema; import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import java.util.HashMap; +import java.util.Map; /** * The similarity function used for the vector index. This is only applicable if the vector index is * enabled. */ public enum SimilarityFunction { - COSINE, - EUCLIDEAN, - DOT_PRODUCT, - UNDEFINED; + COSINE("cosine"), + EUCLIDEAN("euclidean"), + DOT_PRODUCT("dot_product"), + UNDEFINED("undefined"); + + private String metric; + private static Map FUNCTIONS_MAP = new HashMap<>(); + + static { + for (SimilarityFunction similarityFunction : SimilarityFunction.values()) { + FUNCTIONS_MAP.put(similarityFunction.getMetric(), similarityFunction); + } + } + + private SimilarityFunction(String metric) { + this.metric = metric; + } + + public String getMetric() { + return metric; + } // TODO: store the name of the enum in the enum itself public static SimilarityFunction fromString(String similarityFunction) { if (similarityFunction == null) return UNDEFINED; - return switch (similarityFunction.toLowerCase()) { - case "cosine" -> COSINE; - case "euclidean" -> EUCLIDEAN; - case "dot_product" -> DOT_PRODUCT; - default -> - throw ErrorCodeV1.VECTOR_SEARCH_INVALID_FUNCTION_NAME.toApiException( - "'%s'", similarityFunction); - }; + SimilarityFunction function = FUNCTIONS_MAP.get(similarityFunction); + if (function == null) { + throw ErrorCodeV1.VECTOR_SEARCH_INVALID_FUNCTION_NAME.toApiException( + "'%s'", similarityFunction); + } + return function; } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/schema/tables/ComplexApiDataType.java b/src/main/java/io/stargate/sgv2/jsonapi/service/schema/tables/ComplexApiDataType.java index 28afa03974..27a2650ad9 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/schema/tables/ComplexApiDataType.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/schema/tables/ComplexApiDataType.java @@ -2,7 +2,7 @@ import com.datastax.oss.driver.api.core.type.DataType; import com.datastax.oss.driver.api.core.type.DataTypes; -import com.datastax.oss.driver.internal.core.type.DefaultVectorType; +import io.stargate.sgv2.jsonapi.service.cqldriver.override.ExtendedVectorType; /** Interface defining the api data type for complex types */ public abstract class ComplexApiDataType implements ApiDataType { @@ -84,23 +84,4 @@ public DataType getCqlType() { ApiDataTypeDefs.from(getValueType()).get().getCqlType(), getDimension()); } } - - /** - * Extended vector type to support vector size This is needed because java drivers - * DataTypes.vectorOf() method has a bug - */ - public static class ExtendedVectorType extends DefaultVectorType { - public ExtendedVectorType(DataType subtype, int vectorSize) { - super(subtype, vectorSize); - } - - @Override - public String asCql(boolean includeFrozen, boolean pretty) { - return "VECTOR<" - + getElementType().asCql(includeFrozen, pretty) - + "," - + getDimensions() - + ">"; - } - } } diff --git a/src/main/resources/errors.yaml b/src/main/resources/errors.yaml index 60414daf2a..5af81895e3 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -382,6 +382,12 @@ request-errors: title: Unable to parse vectorize configuration, schema invalid. body: |- Unable to parse vectorize configuration, schema invalid for field ${field}. + + - scope: SCHEMA + code: INVALID_INDEX_DEFINITION + title: Provided index configuration is not valid. + body: |- + Provided index configuration is not valid: ${reason}. # ================================================================================================================ diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResourceIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResourceIntegrationTest.java index 1ceccf5a0c..4b6e87a087 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResourceIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResourceIntegrationTest.java @@ -79,7 +79,7 @@ public void unknownCommand() { .body( "errors[0].message", startsWith( - "Provided command unknown: \"unknownCommand\" not one of \"CollectionCommand\"s: known commands are [addIndex, countDocuments,")); + "Provided command unknown: \"unknownCommand\" not one of \"CollectionCommand\"s: known commands are [countDocuments,")); } @Test diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AddTableIndexIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AddTableIndexIntegrationTest.java deleted file mode 100644 index 7614f79b5b..0000000000 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AddTableIndexIntegrationTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.stargate.sgv2.jsonapi.api.v1.tables; - -import static org.hamcrest.Matchers.*; - -import io.quarkus.test.common.WithTestResource; -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; -import io.stargate.sgv2.jsonapi.testresource.DseTestResource; -import java.util.Map; -import org.junit.jupiter.api.*; - -@QuarkusIntegrationTest -@WithTestResource(value = DseTestResource.class, restrictToAnnotatedClass = false) -@TestClassOrder(ClassOrderer.OrderAnnotation.class) -class AddTableIndexIntegrationTest extends AbstractTableIntegrationTestBase { - String testTableName = "tableForAddIndexTest"; - - @BeforeAll - public final void createSimpleTable() { - createTableWithColumns( - testTableName, - Map.of( - "id", - Map.of("type", "text"), - "age", - Map.of("type", "int"), - "vehicleId", - Map.of("type", "text"), - "name", - Map.of("type", "text")), - "id"); - } - - @Nested - @Order(1) - class AddIndexSuccess { - - @Test - public void addIndexBasic() { - DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) - .postCommand( - "addIndex", - """ - { - "column": "age", - "indexName": "age_index" - } - """) - .hasNoErrors() - .body("status.ok", is(1)); - } - - @Test - public void addIndexCaseSensitive() { - DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) - .postCommand( - "addIndex", - """ - { - "column": "vehicleId", - "indexName": "vehicleId_idx" - } - """) - .hasNoErrors() - .body("status.ok", is(1)); - } - } - - @Nested - @Order(2) - class AddIndexFailure { - @Test - public void tryAddIndexMissingColumn() { - DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) - .postCommand( - "addIndex", - """ - { - "column": "city", - "indexName": "city_index" - } - """) - .hasSingleApiError(ErrorCodeV1.INVALID_QUERY, "Undefined column name city"); - } - } -} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIndexIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIndexIntegrationTest.java new file mode 100644 index 0000000000..02282903f6 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIndexIntegrationTest.java @@ -0,0 +1,430 @@ +package io.stargate.sgv2.jsonapi.api.v1.tables; + +import static org.hamcrest.Matchers.*; + +import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders; +import io.stargate.sgv2.jsonapi.exception.SchemaException; +import io.stargate.sgv2.jsonapi.testresource.DseTestResource; +import java.util.Map; +import org.junit.jupiter.api.*; + +@QuarkusIntegrationTest +@WithTestResource(value = DseTestResource.class, restrictToAnnotatedClass = false) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +class CreateTableIndexIntegrationTest extends AbstractTableIntegrationTestBase { + String testTableName = "tableForCreateIndexTest"; + + @BeforeAll + public final void createSimpleTable() { + createTableWithColumns( + testTableName, + Map.ofEntries( + Map.entry("id", Map.of("type", "text")), + Map.entry("age", Map.of("type", "int")), + Map.entry("comment", Map.of("type", "text")), + Map.entry("vehicle_id", Map.of("type", "text")), + Map.entry("vehicle_id_1", Map.of("type", "text")), + Map.entry("vehicle_id_2", Map.of("type", "text")), + Map.entry("vehicle_id_3", Map.of("type", "text")), + Map.entry("vehicle_id_4", Map.of("type", "text")), + Map.entry("invalid_text", Map.of("type", "int")), + Map.entry("physicalAddress", Map.of("type", "text")), + Map.entry("list_type", Map.of("type", "list", "valueType", "text")), + Map.entry("set_type", Map.of("type", "set", "valueType", "text")), + Map.entry("map_type", Map.of("type", "map", "keyType", "text", "valueType", "text")), + Map.entry("vector_type_1", Map.of("type", "vector", "dimension", 1024)), + Map.entry("vector_type_2", Map.of("type", "vector", "dimension", 1536)), + Map.entry("vector_type_3", Map.of("type", "vector", "dimension", 1024))), + "id"); + } + + @Nested + @Order(1) + class CreateIndexSuccess { + + @Test + public void createIndexBasic() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "age_idx", + "definition": { + "column": "age" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexCaseSensitive() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vehicle_id_idx", + "definition": { + "column": "vehicle_id" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexCaseInsensitive() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vehicle_id_1_idx", + "definition": { + "column": "vehicle_id_1", + "options": { + "caseSensitive": false + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexConvertAscii() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vehicle_id_2_idx", + "definition": { + "column": "vehicle_id_2", + "options": { + "ascii": true + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexNormalize() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vehicle_id_3_idx", + "definition": { + "column": "vehicle_id_3", + "options": { + "normalize": true + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createTextIndexAllOptions() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vehicle_id_4_idx", + "definition": { + "column": "vehicle_id_4", + "options": { + "caseSensitive": true, + "normalize": true, + "ascii": true + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createListIndex() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "list_type_idx", + "definition": { + "column": "list_type" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createSetIndex() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "set_type_idx", + "definition": { + "column": "set_type" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createMapIndex() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "map_type_idx", + "definition": { + "column": "map_type" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createVectorIndex() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vector_type_1_idx", + "definition": { + "column": "vector_type_1" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createVectorIndexWithSourceModel() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vector_type_2_idx", + "definition": { + "column": "vector_type_2", + "options": { + "sourceModel": "openai_v3_small" + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createVectorIndexWithMetric() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vector_type_3_idx", + "definition": { + "column": "vector_type_3", + "options": { + "metric": "euclidean" + } + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexForQuotedColumn() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "physicalAddress_idx", + "definition": { + "column": "physicalAddress" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + + @Test + public void createIndexForWithIfNotExist() { + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "comment_idx", + "definition": { + "column": "comment" + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "comment_idx", + "definition": { + "column": "comment" + }, + "options": { + "ifNotExists": true + } + } + """) + .hasNoErrors() + .body("status.ok", is(1)); + } + } + + @Nested + @Order(2) + class CreateIndexFailure { + @Test + public void tryCreateIndexMissingColumn() { + final SchemaException schemaException = + SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of("reason", "Column not defined in the table")); + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "city_index", + "definition": { + "column": "city" + } + } + """) + .hasSingleApiError( + SchemaException.Code.INVALID_INDEX_DEFINITION, + SchemaException.class, + schemaException.body); + } + + @Test + public void nonTextOptions() { + final SchemaException schemaException = + SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "`caseSensitive`, `normalize` and `ascii` options are valid only for `text` column")); + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "invalid_text_idx", + "definition": { + "column": "invalid_text", + "options": { + "caseSensitive": true + } + } + } + """) + .hasSingleApiError( + SchemaException.Code.INVALID_INDEX_DEFINITION, + SchemaException.class, + schemaException.body); + } + + @Test + public void nonVectorOptions() { + final SchemaException schemaException = + SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "`metric` and `sourceModel` options are valid only for `vector` type column")); + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "invalid_text_idx", + "definition": { + "column": "invalid_text", + "options": { + "metric": "cosine" + } + } + } + """) + .hasSingleApiError( + SchemaException.Code.INVALID_INDEX_DEFINITION, + SchemaException.class, + schemaException.body); + } + + @Test + public void vectorTypeAllConfigOptions() { + final SchemaException schemaException = + SchemaException.Code.INVALID_INDEX_DEFINITION.get( + Map.of( + "reason", + "Only one of `metric` or `sourceModel` options should be used for `vector` type column")); + DataApiCommandSenders.assertTableCommand(keyspaceName, testTableName) + .postCommand( + "createIndex", + """ + { + "name": "vector_type_3_idx", + "definition": { + "column": "vector_type_3", + "options": { + "metric": "cosine", + "sourceModel": "mistral-embed" + } + } + } + """) + .hasSingleApiError( + SchemaException.Code.INVALID_INDEX_DEFINITION, + SchemaException.class, + schemaException.body); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/DropTableIndexIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/DropTableIndexIntegrationTest.java index 4923f4be75..46f4a70704 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/DropTableIndexIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/DropTableIndexIntegrationTest.java @@ -50,19 +50,21 @@ class DropIndexSuccess { @Test @Order(1) public void dropIndex() { - String addIndexJson = + String createIndexJson = """ - { - "addIndex": { - "column": "age", - "indexName": "age_index" - } - } - """; + { + "createIndex": { + "name": "age_idx", + "definition": { + "column": "age" + } + } + } + """; given() .headers(getHeaders()) .contentType(ContentType.JSON) - .body(addIndexJson) + .body(createIndexJson) .when() .post(CollectionResource.BASE_PATH, keyspaceName, simpleTableName) .then() @@ -73,7 +75,7 @@ public void dropIndex() { """ { "dropIndex": { - "indexName": "age_index" + "indexName": "age_idx" } } """; diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiTableCommandSender.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiTableCommandSender.java index 1daabf7bc4..71f599918d 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiTableCommandSender.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiTableCommandSender.java @@ -38,7 +38,8 @@ public DataApiResponseValidator postInsertOne(String docAsJSON) { public DataApiResponseValidator postCreateIndex(String columnName, String indexName) { String createIndex = - "{\"column\":\"%s\", \"indexName\":\"%s\"}".formatted(columnName, indexName); - return postCommand("addIndex", createIndex); + "{\"name\": \"%s\", \"definition\": { \"column\": \"%s\"} }" + .formatted(indexName, columnName); + return postCommand("createIndex", createIndex); } }