Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

closes #320: implement findCollections command #340

Merged
merged 4 commits into from
Apr 17, 2023
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
21 changes: 21 additions & 0 deletions src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@
}
}
"""),
@ExampleObject(
name = "findCollections",
summary = "`FindCollections` command",
value =
"""
{
"findCollections": {
}
}
"""),
@ExampleObject(
name = "deleteCollection",
summary = "`DeleteCollection` command",
Expand Down Expand Up @@ -586,6 +596,17 @@
}
}
"""),
@ExampleObject(
name = "resultFindCollections",
summary = "`findCollections` command result",
value =
"""
{
"status": {
"collections": [ "events" ]
}
}
"""),
@ExampleObject(
name = "resultError",
summary = "Error result",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteManyCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.DropNamespaceCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCollectionsCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindNamespacesCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndDeleteCommand;
Expand Down Expand Up @@ -49,6 +50,7 @@
@JsonSubTypes.Type(value = DeleteOneCommand.class),
@JsonSubTypes.Type(value = DeleteManyCommand.class),
@JsonSubTypes.Type(value = DropNamespaceCommand.class),
@JsonSubTypes.Type(value = FindCollectionsCommand.class),
@JsonSubTypes.Type(value = FindCommand.class),
@JsonSubTypes.Type(value = FindNamespacesCommand.class),
@JsonSubTypes.Type(value = FindOneCommand.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum CommandStatus {
/** Status for reporting existing namespaces. */
@JsonProperty("namespaces")
EXISTING_NAMESPACES,
/** Status for reporting existing collections. */
@JsonProperty("collections")
EXISTING_COLLECTIONS,
/** The element has the list of inserted ids */
@JsonProperty("insertedIds")
INSERTED_IDS,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.stargate.sgv2.jsonapi.api.model.command.impl;

import com.fasterxml.jackson.annotation.JsonTypeName;
import io.stargate.sgv2.jsonapi.api.model.command.NamespaceCommand;
import io.stargate.sgv2.jsonapi.api.model.command.NoOptionsCommand;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema(description = "Command that lists all available collections in a namespace.")
@JsonTypeName("findCollections")
public record FindCollectionsCommand() implements NamespaceCommand, NoOptionsCommand {}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public NamespaceResource(CommandProcessor commandProcessor) {
schema = @Schema(anyOf = {CreateCollectionCommand.class}),
examples = {
@ExampleObject(ref = "createCollection"),
@ExampleObject(ref = "findCollections"),
@ExampleObject(ref = "deleteCollection"),
}))
@APIResponses(
Expand All @@ -69,6 +70,7 @@ public NamespaceResource(CommandProcessor commandProcessor) {
schema = @Schema(implementation = CommandResult.class),
examples = {
@ExampleObject(ref = "resultCreate"),
@ExampleObject(ref = "resultFindCollections"),
@ExampleObject(ref = "resultError"),
})))
@POST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public enum ErrorCode {

FILTER_UNRESOLVABLE("Unable to resolve the filter"),

NAMESPACE_DOES_NOT_EXIST("The provided namespace does not exist."),

SHRED_BAD_DOCUMENT_TYPE("Bad document type to shred"),

SHRED_BAD_DOCID_TYPE("Bad type for '_id' property"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.stargate.sgv2.jsonapi.service.operation.model.impl;

import io.smallrye.mutiny.Uni;
import io.stargate.bridge.proto.Schema;
import io.stargate.sgv2.api.common.schema.SchemaManager;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.exception.JsonApiException;
import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor;
import io.stargate.sgv2.jsonapi.service.operation.model.Operation;
import io.stargate.sgv2.jsonapi.service.schema.model.JsonapiTableMatcher;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* Find collection operation. Uses {@link SchemaManager} to fetch all valid jsonapi tables for a
* namespace. The schema check against the table is done in the {@link JsonapiTableMatcher}.
*
* @param schemaManager {@link SchemaManager}
* @param tableMatcher {@link JsonapiTableMatcher}
* @param commandContext {@link CommandContext}
*/
public record FindCollectionsOperation(
SchemaManager schemaManager, JsonapiTableMatcher tableMatcher, CommandContext commandContext)
implements Operation {

// missing keyspace function
private static final Function<String, Uni<? extends Schema.CqlKeyspaceDescribe>>
MISSING_KEYSPACE_FUNCTION =
keyspace -> {
String message = "Unknown namespace %s, you must create it first.".formatted(keyspace);
Exception exception = new JsonApiException(ErrorCode.NAMESPACE_DOES_NOT_EXIST, message);
return Uni.createFrom().failure(exception);
};

// shared table matcher instance
private static final JsonapiTableMatcher TABLE_MATCHER = new JsonapiTableMatcher();

public FindCollectionsOperation(SchemaManager schemaManager, CommandContext commandContext) {
this(schemaManager, TABLE_MATCHER, commandContext);
}

/** {@inheritDoc} */
@Override
public Uni<Supplier<CommandResult>> execute(QueryExecutor queryExecutor) {
String namespace = commandContext.namespace();

// get all valid tables
// get all tables
return schemaManager
.getTables(namespace, MISSING_KEYSPACE_FUNCTION)

// filter for valid collections
.filter(tableMatcher)

// map to name
.map(Schema.CqlTable::getName)

// get as list
.collect()
.asList()

// wrap into command result
.map(Result::new);
}

// simple result wrapper
private record Result(List<String> collections) implements Supplier<CommandResult> {

@Override
public CommandResult get() {
Map<CommandStatus, Object> statuses = Map.of(CommandStatus.EXISTING_COLLECTIONS, collections);
return new CommandResult(statuses);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.stargate.sgv2.jsonapi.service.resolver.model.impl;

import io.stargate.sgv2.api.common.schema.SchemaManager;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCollectionsCommand;
import io.stargate.sgv2.jsonapi.service.operation.model.Operation;
import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindCollectionsOperation;
import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

/** Command resolver for the {@link FindCollectionsCommand}. */
@ApplicationScoped
public class FindCollectionsCommandResolver implements CommandResolver<FindCollectionsCommand> {

private final SchemaManager schemaManager;

@Inject
public FindCollectionsCommandResolver(SchemaManager schemaManager) {
this.schemaManager = schemaManager;
}

/** {@inheritDoc} */
@Override
public Class<FindCollectionsCommand> getCommandClass() {
return FindCollectionsCommand.class;
}

/** {@inheritDoc} */
@Override
public Operation resolveCommand(CommandContext ctx, FindCollectionsCommand command) {
return new FindCollectionsOperation(schemaManager, ctx);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.stargate.sgv2.jsonapi.service.schema.model;

import io.stargate.bridge.proto.QueryOuterClass;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Predicate;

/** Interface for matching a CQL column name and type. */
public interface CqlColumnMatcher extends Predicate<QueryOuterClass.ColumnSpec> {

/** @return Column name for the matcher. */
String name();

/** @return If column type is matching. */
boolean typeMatches(QueryOuterClass.ColumnSpec columnSpec);

default boolean test(QueryOuterClass.ColumnSpec columnSpec) {
return Objects.equals(columnSpec.getName(), name()) && typeMatches(columnSpec);
}

/**
* Implementation that supports basic column types.
*
* @param name column name
* @param type basic type
*/
record BasicType(String name, QueryOuterClass.TypeSpec.Basic type) implements CqlColumnMatcher {

@Override
public boolean typeMatches(QueryOuterClass.ColumnSpec columnSpec) {
return Objects.equals(columnSpec.getType().getBasic(), type);
}
}

/**
* Implementation that supports map column type. Only basic values are supported as key/value.
*
* @param name column name
* @param keyType map key type
* @param valueType map value type
*/
record Map(
String name, QueryOuterClass.TypeSpec.Basic keyType, QueryOuterClass.TypeSpec.Basic valueType)
implements CqlColumnMatcher {

@Override
public boolean typeMatches(QueryOuterClass.ColumnSpec columnSpec) {
QueryOuterClass.TypeSpec type = columnSpec.getType();
if (!type.hasMap()) {
return false;
}

QueryOuterClass.TypeSpec.Map map = type.getMap();
return Objects.equals(map.getKey().getBasic(), keyType)
&& Objects.equals(map.getValue().getBasic(), valueType);
}
}

/**
* Implementation that supports tuple column type. Only basic values are supported as elements.
*
* @param name column name
* @param elements types of elements in the tuple
*/
record Tuple(String name, QueryOuterClass.TypeSpec.Basic... elements)
implements CqlColumnMatcher {

@Override
public boolean typeMatches(QueryOuterClass.ColumnSpec columnSpec) {
QueryOuterClass.TypeSpec type = columnSpec.getType();
if (!type.hasTuple()) {
return false;
}

QueryOuterClass.TypeSpec.Tuple map = type.getTuple();
java.util.List<QueryOuterClass.TypeSpec.Basic> elementTypes =
map.getElementsList().stream().map(QueryOuterClass.TypeSpec::getBasic).toList();
return Objects.equals(elementTypes, Arrays.asList(elements));
}
}

/**
* Implementation that supports set column type. Only basic values are supported as elements.
*
* @param name column name
* @param elementType type of elements in the set
*/
record Set(String name, QueryOuterClass.TypeSpec.Basic elementType) implements CqlColumnMatcher {

@Override
public boolean typeMatches(QueryOuterClass.ColumnSpec columnSpec) {
QueryOuterClass.TypeSpec type = columnSpec.getType();
if (!type.hasSet()) {
return false;
}

QueryOuterClass.TypeSpec.Set set = type.getSet();
return Objects.equals(set.getElement().getBasic(), elementType);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.stargate.sgv2.jsonapi.service.schema.model;

import io.stargate.bridge.proto.QueryOuterClass;
import io.stargate.bridge.proto.QueryOuterClass.TypeSpec.Basic;
import io.stargate.bridge.proto.Schema;
import java.util.List;
import java.util.function.Predicate;

/** Simple class that can check if table is a matching jsonapi table. */
public class JsonapiTableMatcher implements Predicate<Schema.CqlTable> {

private final Predicate<QueryOuterClass.ColumnSpec> primaryKeyPredicate;

private final Predicate<QueryOuterClass.ColumnSpec> columnsPredicate;

public JsonapiTableMatcher() {
primaryKeyPredicate = new CqlColumnMatcher.Tuple("key", Basic.TINYINT, Basic.VARCHAR);
columnsPredicate =
new CqlColumnMatcher.BasicType("tx_id", Basic.TIMEUUID)
.or(new CqlColumnMatcher.BasicType("doc_json", Basic.VARCHAR))
.or(new CqlColumnMatcher.Set("exist_keys", Basic.VARCHAR))
.or(new CqlColumnMatcher.Map("sub_doc_equals", Basic.VARCHAR, Basic.VARCHAR))
.or(new CqlColumnMatcher.Map("array_size", Basic.VARCHAR, Basic.INT))
.or(new CqlColumnMatcher.Map("array_equals", Basic.VARCHAR, Basic.VARCHAR))
.or(new CqlColumnMatcher.Set("array_contains", Basic.VARCHAR))
.or(new CqlColumnMatcher.Map("query_bool_values", Basic.VARCHAR, Basic.TINYINT))
.or(new CqlColumnMatcher.Map("query_dbl_values", Basic.VARCHAR, Basic.DECIMAL))
.or(new CqlColumnMatcher.Map("query_text_values", Basic.VARCHAR, Basic.VARCHAR))
.or(new CqlColumnMatcher.Set("query_null_values", Basic.VARCHAR));
}

/**
* Tests if the given table is a valid jsonapi table.
*
* @param cqlTable the table
* @return Returns true only if all the columns in the table are corresponding the jsonapi table
* schema.
*/
@Override
public boolean test(Schema.CqlTable cqlTable) {
// null safety
if (null == cqlTable) {
return false;
}

// partition columns
List<QueryOuterClass.ColumnSpec> partitionColumns = cqlTable.getPartitionKeyColumnsList();
if (partitionColumns.size() != 1 || !partitionColumns.stream().allMatch(primaryKeyPredicate)) {
return false;
}

// clustering columns
List<QueryOuterClass.ColumnSpec> clusteringColumns = cqlTable.getClusteringKeyColumnsList();
if (clusteringColumns.size() != 0) {
return false;
}

List<QueryOuterClass.ColumnSpec> columns = cqlTable.getColumnsList();
if (columns.size() != 11 || !columns.stream().allMatch(columnsPredicate)) {
return false;
}

return true;
}
}
Loading