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

POC for table projections and using JSONCodec #1314

Merged
merged 7 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1,128 @@
package io.stargate.sgv2.jsonapi.api.model.command;

import io.stargate.sgv2.jsonapi.service.cqldriver.executor.*;

/**
* Interface for any clause of a {@link Command} to implement if it can be validated against a
* {@link SchemaObject}.
*
* <p>Code that wants to validate a clause should call {@link #maybeValidate(CommandContext,
* ValidatableCommandClause)} with the clause.
*
* <p>Example:
*
* <pre>
* ValidatableCommandClause.maybeValidate(commandContext, command.filterClause());
* </pre>
*/
public interface ValidatableCommandClause {

/**
* Calls the supplied validatable clause to validate against the {@link SchemaObject} from the
* {@link CommandContext} using one of the dedicated validate*Command methods on the interface.
*
* <p>NOTE: Classes that want to validate a clause should call this method, not the non-static
* methods on the interface directly.
*
* @param commandContext The context the command is running against, including the {@link
* SchemaObject}
* @param validatable An object that implements {@link ValidatableCommandClause}, may be null
* @param <T> Type of the {@link SchemaObject}
*/
static <T extends SchemaObject> void maybeValidate(
CommandContext<T> commandContext, ValidatableCommandClause validatable) {
if (validatable == null) {
return;
}

switch (commandContext.schemaObject().type) {
case COLLECTION:
validatable.validateCollectionCommand(commandContext.asCollectionContext());
break;
case TABLE:
validatable.validateTableCommand(commandContext.asTableContext());
break;
case KEYSPACE:
validatable.validateNamespaceCommand(commandContext.asKeyspaceContext());
break;
case DATABASE:
validatable.validateDatabaseCommand(commandContext.asDatabaseContext());
break;
default:
throw new UnsupportedOperationException(
String.format("Unsupported schema type: %s", commandContext.schemaObject().type));
}
}

/**
* Implementations should implement this method if they support validation against a {@link
* CollectionSchemaObject}.
*
* <p>Only implement this method if the clause supports Collections, the default implementation is
* to fail.
*
* @param commandContext {@link CommandContext<CollectionSchemaObject>} to validate against
*/
default void validateCollectionCommand(CommandContext<CollectionSchemaObject> commandContext) {
// there error is a fallback to make sure it is implemented if it should be
// commands are tested well
throw new UnsupportedOperationException(
String.format(
"%s Clause does not support validating for Collections, target was %s",
getClass().getSimpleName(), commandContext.schemaObject().name));
}

/**
* Implementations should implement this method if they support validation against a {@link
* TableSchemaObject}.
*
* <p>Only implement this method if the clause supports Tables, the default implementation is to
* fail.
*
* @param commandContext {@link CommandContext<TableSchemaObject>} to validate against
*/
default void validateTableCommand(CommandContext<TableSchemaObject> commandContext) {
// there error is a fallback to make sure it is implemented if it should be
// commands are tested well
throw new UnsupportedOperationException(
String.format(
"%s Clause does not support validating for Tables, target was %s",
getClass().getSimpleName(), commandContext.schemaObject().name));
}

/**
* Implementations should implement this method if they support validation against a {@link
* KeyspaceSchemaObject}.
*
* <p>Only implement this method if the clause supports Keyspaces, the default implementation is
* to fail.
*
* @param commandContext {@link CommandContext<KeyspaceSchemaObject>} to validate against
*/
default void validateNamespaceCommand(CommandContext<KeyspaceSchemaObject> commandContext) {
// there error is a fallback to make sure it is implemented if it should be
// commands are tested well
throw new UnsupportedOperationException(
String.format(
"%s Clause does not support validating for Namespaces, target was %s",
getClass().getSimpleName(), commandContext.schemaObject().name));
}

/**
* Implementations should implement this method if they support validation against a {@link
* DatabaseSchemaObject}.
*
* <p>Only implement this method if the clause supports Databases, the default implementation is
* to fail.
*
* @param commandContext {@link CommandContext<DatabaseSchemaObject>} to validate against
*/
default void validateDatabaseCommand(CommandContext<DatabaseSchemaObject> commandContext) {
// there error is a fallback to make sure it is implemented if it should be
// commands are tested well
throw new UnsupportedOperationException(
String.format(
"%s Clause does not support validating for Databases, target was %s",
getClass().getSimpleName(), commandContext.schemaObject().name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.ValidatableCommandClause;
import io.stargate.sgv2.jsonapi.api.model.command.deserializers.FilterClauseDeserializer;
import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.CollectionSchemaObject;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject;
import io.stargate.sgv2.jsonapi.service.projection.IndexingProjector;
import java.util.List;
import java.util.Map;
Expand All @@ -20,35 +22,38 @@
"""
{"name": "Aaron", "country": "US"}
""")
public record FilterClause(LogicalExpression logicalExpression) {
public record FilterClause(LogicalExpression logicalExpression)
implements ValidatableCommandClause {

public void validate(CommandContext<?> commandContext) {
@Override
public void validateTableCommand(CommandContext<TableSchemaObject> commandContext) {
// TODO HACK AARON - this is a temporary fix to allow the tests to pass
return;
}

if (commandContext.schemaObject().type != CollectionSchemaObject.TYPE) {
return;
}
@Override
public void validateCollectionCommand(CommandContext<CollectionSchemaObject> commandContext) {

IndexingProjector indexingProjector =
commandContext.asCollectionContext().schemaObject().indexingProjector();
IndexingProjector indexingProjector = commandContext.schemaObject().indexingProjector();

// If nothing specified, everything indexed
if (indexingProjector.isIdentityProjection()) {
return;
}
validateLogicalExpression(logicalExpression, indexingProjector);
validateCollectionLogicalExpression(logicalExpression, indexingProjector);
}

public void validateLogicalExpression(
private void validateCollectionLogicalExpression(
LogicalExpression logicalExpression, IndexingProjector indexingProjector) {
for (LogicalExpression subLogicalExpression : logicalExpression.logicalExpressions) {
validateLogicalExpression(subLogicalExpression, indexingProjector);
validateCollectionLogicalExpression(subLogicalExpression, indexingProjector);
}
for (ComparisonExpression subComparisonExpression : logicalExpression.comparisonExpressions) {
validateComparisonExpression(subComparisonExpression, indexingProjector);
validateCollectionComparisonExpression(subComparisonExpression, indexingProjector);
}
}

public void validateComparisonExpression(
private void validateCollectionComparisonExpression(
ComparisonExpression comparisonExpression, IndexingProjector indexingProjector) {
String path = comparisonExpression.getPath();
boolean isPathIndexed =
Expand Down Expand Up @@ -79,15 +84,16 @@ public void validateComparisonExpression(
// If path is an object (like address), validate the incremental path (like address.city)
if (operand.type() == JsonType.ARRAY || operand.type() == JsonType.SUB_DOC) {
if (operand.value() instanceof Map<?, ?> map) {
validateMap(indexingProjector, map, path);
validateCollectionMap(indexingProjector, map, path);
}
if (operand.value() instanceof List<?> list) {
validateList(indexingProjector, list, path);
validateCollectionList(indexingProjector, list, path);
}
}
}

private void validateMap(IndexingProjector indexingProjector, Map<?, ?> map, String currentPath) {
private void validateCollectionMap(
IndexingProjector indexingProjector, Map<?, ?> map, String currentPath) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
String incrementalPath = currentPath + "." + entry.getKey();
if (!indexingProjector.isPathIncluded(incrementalPath)) {
Expand All @@ -96,21 +102,22 @@ private void validateMap(IndexingProjector indexingProjector, Map<?, ?> map, Str
}
// continue build the incremental path if the value is a map
if (entry.getValue() instanceof Map<?, ?> valueMap) {
validateMap(indexingProjector, valueMap, incrementalPath);
validateCollectionMap(indexingProjector, valueMap, incrementalPath);
}
// continue build the incremental path if the value is a list
if (entry.getValue() instanceof List<?> list) {
validateList(indexingProjector, list, incrementalPath);
validateCollectionList(indexingProjector, list, incrementalPath);
}
}
}

private void validateList(IndexingProjector indexingProjector, List<?> list, String currentPath) {
private void validateCollectionList(
IndexingProjector indexingProjector, List<?> list, String currentPath) {
for (Object element : list) {
if (element instanceof Map<?, ?> map) {
validateMap(indexingProjector, map, currentPath);
validateCollectionMap(indexingProjector, map, currentPath);
} else if (element instanceof List<?> sublList) {
validateList(indexingProjector, sublList, currentPath);
validateCollectionList(indexingProjector, sublList, currentPath);
} else if (element instanceof String) {
// no need to build incremental path, validate current path
if (!indexingProjector.isPathIncluded(currentPath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.ValidatableCommandClause;
import io.stargate.sgv2.jsonapi.api.model.command.deserializers.SortClauseDeserializer;
import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants;
import io.stargate.sgv2.jsonapi.exception.ErrorCode;
import io.stargate.sgv2.jsonapi.service.cqldriver.executor.CollectionSchemaObject;
import io.stargate.sgv2.jsonapi.service.projection.IndexingProjector;
import jakarta.validation.Valid;
import java.util.List;
Expand All @@ -25,7 +27,8 @@
"""
{"user.age" : -1, "user.name" : 1}
""")
public record SortClause(@Valid List<SortExpression> sortExpressions) {
public record SortClause(@Valid List<SortExpression> sortExpressions)
implements ValidatableCommandClause {

public boolean hasVsearchClause() {
return sortExpressions != null
Expand All @@ -42,28 +45,20 @@ public boolean hasVectorizeSearchClause() {
.equals(DocumentConstants.Fields.VECTOR_EMBEDDING_TEXT_FIELD);
}

public void validate(CommandContext<?> commandContext) {

switch (commandContext.schemaObject().type) {
case COLLECTION:
IndexingProjector indexingProjector =
commandContext.asCollectionContext().schemaObject().indexingProjector();
// If nothing specified, everything indexed
if (indexingProjector.isIdentityProjection()) {
return;
}
// validate each path in sortExpressions
for (SortExpression sortExpression : sortExpressions) {
if (!indexingProjector.isPathIncluded(sortExpression.path())) {
throw ErrorCode.UNINDEXED_SORT_PATH.toApiException(
"sort path '%s' is not indexed", sortExpression.path());
}
}
break;
default:
throw ErrorCode.SERVER_INTERNAL_ERROR.toApiException(
"SortClause validation is not supported for schemaObject type: %s",
commandContext.schemaObject().type);
@Override
public void validateCollectionCommand(CommandContext<CollectionSchemaObject> commandContext) {
IndexingProjector indexingProjector =
commandContext.asCollectionContext().schemaObject().indexingProjector();
// If nothing specified, everything indexed
if (indexingProjector.isIdentityProjection()) {
return;
}
// validate each path in sortExpressions
for (SortExpression sortExpression : sortExpressions) {
if (!indexingProjector.isPathIncluded(sortExpression.path())) {
throw ErrorCode.UNINDEXED_SORT_PATH.toApiException(
"sort path '%s' is not indexed", sortExpression.path());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
import java.util.*;
import org.eclipse.microprofile.config.ConfigProvider;

/** {@link StdDeserializer} for the {@link FilterClause}. */
/**
* {@link StdDeserializer} for the {@link FilterClause}.
*
* <p>TIDY: this class has a lot of string constants for filter operations that we have defined as
* constants elsewhere
*/
public class FilterClauseDeserializer extends StdDeserializer<FilterClause> {
private final OperationsConfig operationsConfig;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private Uni<SchemaObject> loadSchemaObject(
}

if (apiTablesEnabled) {
return new TableSchemaObject(namespace, collectionName);
return new TableSchemaObject(table);
}

// Target is not a collection and we are not supporting tables
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package io.stargate.sgv2.jsonapi.service.cqldriver.executor;

import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;

public class TableSchemaObject extends SchemaObject {

public static final SchemaObjectType TYPE = SchemaObjectType.TABLE;

/** Represents missing schema, e.g. when we are running a create table. */
public static final TableSchemaObject MISSING = new TableSchemaObject(SchemaObjectName.MISSING);
// public static final TableSchemaObject MISSING = new
// TableSchemaObject(SchemaObjectName.MISSING);

public final TableMetadata tableMetadata;

// TODO: hold the table meta data, need to work out how we handle mock tables in test etc.
// public final TableMetadata tableMetadata;

public TableSchemaObject(String keyspace, String name) {
this(new SchemaObjectName(keyspace, name));
}

public TableSchemaObject(SchemaObjectName name) {
super(TYPE, name);
public TableSchemaObject(TableMetadata tableMetadata) {
super(
TYPE,
new SchemaObjectName(
tableMetadata.getKeyspace().asCql(false), tableMetadata.getName().asCql(false)));
this.tableMetadata = tableMetadata;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ public enum BuiltConditionPredicate {
IN("IN"),
CONTAINS("CONTAINS"),
NOT_CONTAINS("NOT CONTAINS"),
CONTAINS_KEY("CONTAINS KEY"),
;
CONTAINS_KEY("CONTAINS KEY");

private final String cql;
public final String cql;

BuiltConditionPredicate(String cql) {
this.cql = cql;
}

// TIDY - remove this use of toString() it should be used for log msg's etc, not core
// functionality. This is called to build the CQL string we execute.
@Override
public String toString() {
return cql;
Expand Down
Loading