diff --git a/pom.xml b/pom.xml
index 24e8dc3d61..5e5c2722cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,6 +97,22 @@
test-jartest
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+ net.javacrumbs.json-unit
+ json-unit
+ 2.35.0
+ test
+
diff --git a/src/main/java/io/stargate/sgv3/docsapi/exception/ErrorCode.java b/src/main/java/io/stargate/sgv3/docsapi/exception/ErrorCode.java
index 48453c2977..0fd86b877f 100644
--- a/src/main/java/io/stargate/sgv3/docsapi/exception/ErrorCode.java
+++ b/src/main/java/io/stargate/sgv3/docsapi/exception/ErrorCode.java
@@ -6,6 +6,8 @@ public enum ErrorCode {
/** Command error codes. */
COMMAND_NOT_IMPLEMENTED("The provided command is not implemented."),
+ DOCUMENT_UNPARSEABLE("Unable to parse the document"),
+
FILTER_UNRESOLVABLE("Unable to resolve the filter"),
SHRED_BAD_DOCUMENT_TYPE("Bad document type to shred"),
@@ -18,7 +20,9 @@ public enum ErrorCode {
SHRED_UNRECOGNIZED_NODE_TYPE("Unrecognized JSON node type in input document"),
- UNSUPPORTED_FILTER_DATA_TYPE("Unsupported filter data type");
+ UNSUPPORTED_FILTER_DATA_TYPE("Unsupported filter data type"),
+
+ UNSUPPORTED_FILTER_OPERATION("Unsupported filter operator");
private final String message;
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/bridge/config/DocumentConfig.java b/src/main/java/io/stargate/sgv3/docsapi/service/bridge/config/DocumentConfig.java
index e8cd1b627f..5b77acbc6e 100644
--- a/src/main/java/io/stargate/sgv3/docsapi/service/bridge/config/DocumentConfig.java
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/bridge/config/DocumentConfig.java
@@ -26,9 +26,24 @@
@ConfigMapping(prefix = "stargate.document")
public interface DocumentConfig {
- /** @return Defines the maximum document page size, defaults to 20. */
- @Max(100)
+ /** @return Defines the maximum document page size, defaults to 500. */
+ @Max(500)
+ @Positive
+ @WithDefault("100")
+ int maxPageSize();
+
+ /** @return Defines the default document page size, defaults to 20. */
+ @Max(500)
@Positive
@WithDefault("20")
- int pageSize();
+ int defaultPageSize();
+
+ /**
+ * @return Defines the maximum limit of document that can be returned for a request, defaults to
+ * 1000.
+ */
+ @Max(Integer.MAX_VALUE)
+ @Positive
+ @WithDefault("1000")
+ int maxLimit();
}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/bridge/executor/QueryExecutor.java b/src/main/java/io/stargate/sgv3/docsapi/service/bridge/executor/QueryExecutor.java
index ae6ca64c45..a164e4dec9 100644
--- a/src/main/java/io/stargate/sgv3/docsapi/service/bridge/executor/QueryExecutor.java
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/bridge/executor/QueryExecutor.java
@@ -52,10 +52,10 @@ public Uni executeRead(
}
if (pageSize.isPresent()) {
- int page = Math.min(pageSize.get(), documentConfig.pageSize());
+ int page = Math.min(pageSize.get(), documentConfig.maxPageSize());
params.setPageSize(Int32Value.of(page));
} else {
- params.setPageSize(Int32Value.of(documentConfig.pageSize()));
+ params.setPageSize(Int32Value.of(documentConfig.defaultPageSize()));
}
return queryBridge(
QueryOuterClass.Query.newBuilder(query).setParameters(params).buildPartial());
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/Operation.java b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/Operation.java
index 324aab553d..cd2e673d2b 100644
--- a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/Operation.java
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/Operation.java
@@ -1,7 +1,6 @@
package io.stargate.sgv3.docsapi.service.operation.model;
import io.smallrye.mutiny.Uni;
-import io.stargate.sgv3.docsapi.api.model.command.CommandContext;
import io.stargate.sgv3.docsapi.api.model.command.CommandResult;
import io.stargate.sgv3.docsapi.service.bridge.executor.QueryExecutor;
import java.util.function.Supplier;
@@ -25,9 +24,5 @@
* OperationExecutor}
*/
public interface Operation {
-
- /** @return The context of the command responsible for this operation. */
- CommandContext commandContext();
-
Uni> execute(QueryExecutor queryExecutor);
}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/ReadOperation.java b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/ReadOperation.java
new file mode 100644
index 0000000000..0db8d793df
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/ReadOperation.java
@@ -0,0 +1,80 @@
+package io.stargate.sgv3.docsapi.service.operation.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.smallrye.mutiny.Uni;
+import io.stargate.bridge.grpc.BytesValues;
+import io.stargate.bridge.grpc.Values;
+import io.stargate.bridge.proto.QueryOuterClass;
+import io.stargate.sgv3.docsapi.exception.DocsException;
+import io.stargate.sgv3.docsapi.exception.ErrorCode;
+import io.stargate.sgv3.docsapi.service.bridge.executor.QueryExecutor;
+import io.stargate.sgv3.docsapi.service.operation.model.impl.ReadDocument;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * ReadOperation interface which all find command operations will use. It also provides the
+ * implementation to excute and query and parse the result set as {@link FindResponse}
+ */
+public interface ReadOperation extends Operation {
+ static String[] documentColumns = {"key", "tx_id", "doc_json"};
+ static String[] documentKeyColumns = {"key", "tx_id"};
+
+ /**
+ * Default implementation to query and parse the result set
+ *
+ * @param queryExecutor
+ * @param query
+ * @param pagingState
+ * @param readDocument This flag is set to false if the read is done to just identify the document
+ * id and tx_id to perform another DML operation
+ * @param objectMapper
+ * @return
+ */
+ default Uni findDocument(
+ QueryExecutor queryExecutor,
+ QueryOuterClass.Query query,
+ String pagingState,
+ boolean readDocument,
+ ObjectMapper objectMapper) {
+ return queryExecutor
+ .executeRead(query, Optional.ofNullable(pagingState), Optional.empty())
+ .onItem()
+ .transform(
+ rSet -> {
+ int remaining = rSet.getRowsCount();
+ int colCount = rSet.getColumnsCount();
+ List documents = new ArrayList<>(remaining);
+ Iterator rowIterator = rSet.getRowsList().stream().iterator();
+ while (--remaining >= 0 && rowIterator.hasNext()) {
+ QueryOuterClass.Row row = rowIterator.next();
+ ReadDocument document = null;
+ try {
+ document =
+ new ReadDocument(
+ Values.string(row.getValues(0)), // key
+ Optional.of(Values.uuid(row.getValues(1))), // tx_id
+ readDocument
+ ? objectMapper.readTree(Values.string(row.getValues(2)))
+ : null);
+ } catch (JsonProcessingException e) {
+ throw new DocsException(ErrorCode.DOCUMENT_UNPARSEABLE);
+ }
+ documents.add(document);
+ }
+ return new FindResponse(documents, extractPagingStateFromResultSet(rSet));
+ });
+ }
+
+ private String extractPagingStateFromResultSet(QueryOuterClass.ResultSet rSet) {
+ if (rSet.hasPagingState()) {
+ return BytesValues.toBase64(rSet.getPagingState());
+ }
+ return null;
+ }
+
+ public static record FindResponse(List docs, String pagingState) {}
+}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/FindOperation.java b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/FindOperation.java
new file mode 100644
index 0000000000..5986ea2082
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/FindOperation.java
@@ -0,0 +1,259 @@
+package io.stargate.sgv3.docsapi.service.operation.model.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.smallrye.mutiny.Uni;
+import io.stargate.bridge.grpc.Values;
+import io.stargate.bridge.proto.QueryOuterClass;
+import io.stargate.sgv2.api.common.cql.builder.BuiltCondition;
+import io.stargate.sgv2.api.common.cql.builder.Predicate;
+import io.stargate.sgv2.api.common.cql.builder.QueryBuilder;
+import io.stargate.sgv3.docsapi.api.model.command.CommandContext;
+import io.stargate.sgv3.docsapi.api.model.command.CommandResult;
+import io.stargate.sgv3.docsapi.exception.DocsException;
+import io.stargate.sgv3.docsapi.exception.ErrorCode;
+import io.stargate.sgv3.docsapi.service.bridge.executor.QueryExecutor;
+import io.stargate.sgv3.docsapi.service.operation.model.ReadOperation;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Full dynamic query generation for any of the types of filtering we can do against the the db
+ * table.
+ *
+ *
Create with a series of filters that are implicitly AND'd together.
+ */
+public record FindOperation(
+ CommandContext commandContext,
+ List filters,
+ String pagingState,
+ int limit,
+ boolean readDocument,
+ ObjectMapper objectMapper)
+ implements ReadOperation {
+
+ @Override
+ public Uni> execute(QueryExecutor queryExecutor) {
+ QueryOuterClass.Query query = buildSelectQuery();
+ return findDocument(queryExecutor, query, pagingState, readDocument, objectMapper)
+ .onItem()
+ .transform(docs -> new ReadOperationPage(docs.docs(), docs.pagingState()));
+ }
+
+ private QueryOuterClass.Query buildSelectQuery() {
+ List conditions = new ArrayList<>(filters.size());
+ for (DBFilterBase filter : filters) {
+ conditions.add(filter.get());
+ }
+ return new QueryBuilder()
+ .select()
+ .column(readDocument ? documentColumns : documentKeyColumns)
+ .from(commandContext.database(), commandContext.collection())
+ .where(conditions)
+ .limit(limit)
+ .build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ FindOperation that = (FindOperation) o;
+ return limit == that.limit
+ && readDocument == that.readDocument
+ && commandContext.equals(that.commandContext)
+ && filters.equals(that.filters)
+ && Objects.equals(pagingState, that.pagingState);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(commandContext, filters, pagingState, limit, readDocument);
+ }
+
+ /** Base for the DB filters / conditions that we want to update the dynamic query */
+ public abstract static class DBFilterBase implements Supplier {}
+
+ /** Filter for the map columns we have in the super shredding table. */
+ public abstract static class MapFilterBase extends DBFilterBase {
+
+ // NOTE: we can only do eq until SAI indexes are updated , waiting for >, < etc
+ public enum Operator {
+ EQ
+ }
+
+ private final String columnName;
+ private final String key;
+ private final Operator operator;
+ private final T value;
+
+ protected MapFilterBase(String columnName, String key, Operator operator, T value) {
+ this.columnName = columnName;
+ this.key = key;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MapFilterBase> that = (MapFilterBase>) o;
+ return columnName.equals(that.columnName)
+ && key.equals(that.key)
+ && operator == that.operator
+ && value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(columnName, key, operator, value);
+ }
+
+ @Override
+ public BuiltCondition get() {
+ switch (operator) {
+ case EQ:
+ return BuiltCondition.of(
+ BuiltCondition.LHS.mapAccess(columnName, Values.of(key)),
+ Predicate.EQ,
+ getValue(value));
+ default:
+ throw new DocsException(
+ ErrorCode.UNSUPPORTED_FILTER_OPERATION,
+ String.format("Unsupported map operation %s on column %s", operator, columnName));
+ }
+ }
+ }
+
+ /** Filters db documents based on a text field value */
+ public static class TextFilter extends MapFilterBase {
+ public TextFilter(String path, Operator operator, String value) {
+ super("query_text_values", path, operator, value);
+ }
+ }
+
+ /** Filters db documents based on a boolean field value */
+ public static class BoolFilter extends MapFilterBase {
+ public BoolFilter(String path, Operator operator, Boolean value) {
+ super("query_bool_values", path, operator, value);
+ }
+ }
+
+ /** Filters db documents based on a numeric field value */
+ public static class NumberFilter extends MapFilterBase {
+ public NumberFilter(String path, Operator operator, BigDecimal value) {
+ super("query_dbl_values", path, operator, value);
+ }
+ }
+
+ /** Filters db documents based on a document id field value */
+ public static class IDFilter extends DBFilterBase {
+ public enum Operator {
+ EQ;
+ }
+
+ protected final Operator operator;
+ protected final String value;
+
+ public IDFilter(Operator operator, String value) {
+ this.operator = operator;
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IDFilter idFilter = (IDFilter) o;
+ return operator == idFilter.operator && value.equals(idFilter.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(operator, value);
+ }
+
+ @Override
+ public BuiltCondition get() {
+ switch (operator) {
+ case EQ:
+ return BuiltCondition.of(BuiltCondition.LHS.column("key"), Predicate.EQ, getValue(value));
+ default:
+ throw new DocsException(
+ ErrorCode.UNSUPPORTED_FILTER_OPERATION,
+ String.format("Unsupported id column operation %s", operator));
+ }
+ }
+ }
+ /**
+ * DB filter / condition for testing a set value Note: we can only do CONTAINS until SAI indexes
+ * are updated
+ */
+ public abstract static class SetFilterBase extends DBFilterBase {
+ public enum Operator {
+ CONTAINS;
+ }
+
+ protected final String columnName;
+ protected final T value;
+ protected final Operator operator;
+
+ protected SetFilterBase(String columnName, T value, Operator operator) {
+ this.columnName = columnName;
+ this.value = value;
+ this.operator = operator;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetFilterBase> that = (SetFilterBase>) o;
+ return columnName.equals(that.columnName)
+ && value.equals(that.value)
+ && operator == that.operator;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(columnName, value, operator);
+ }
+
+ @Override
+ public BuiltCondition get() {
+ switch (operator) {
+ case CONTAINS:
+ return BuiltCondition.of(columnName, Predicate.CONTAINS, getValue(value));
+ default:
+ throw new DocsException(
+ ErrorCode.UNSUPPORTED_FILTER_OPERATION,
+ String.format("Unsupported set operation %s on column %s", operator, columnName));
+ }
+ }
+ }
+
+ /**
+ * Filter for document where a field == null
+ *
+ *
NOTE: cannot do != null until we get NOT CONTAINS in the DB for set
+ */
+ public static class IsNullFilter extends SetFilterBase {
+ public IsNullFilter(String path) {
+ super("query_null_values", path, Operator.CONTAINS);
+ }
+ }
+
+ private static QueryOuterClass.Value getValue(Object value) {
+ if (value instanceof String) {
+ return Values.of((String) value);
+ } else if (value instanceof BigDecimal) {
+ return Values.of((BigDecimal) value);
+ } else if (value instanceof Boolean) {
+ return Values.of((Boolean) value);
+ }
+ return Values.of((String) null);
+ }
+}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadDocument.java b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadDocument.java
new file mode 100644
index 0000000000..2f192574ec
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadDocument.java
@@ -0,0 +1,14 @@
+package io.stargate.sgv3.docsapi.service.operation.model.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Represents a document read from the database
+ *
+ * @param id Document Id identifying the document
+ * @param txnId Unique UUID resenting point in time of a document, used for LWT transactions
+ * @param document JsonNode representation of the document
+ */
+public record ReadDocument(String id, Optional txnId, JsonNode document) {}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadOperationPage.java b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadOperationPage.java
new file mode 100644
index 0000000000..32bf6bff83
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/operation/model/impl/ReadOperationPage.java
@@ -0,0 +1,21 @@
+package io.stargate.sgv3.docsapi.service.operation.model.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.stargate.sgv3.docsapi.api.model.command.CommandResult;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/** FindOperation response implementing the {@link CommandResult} */
+public record ReadOperationPage(List docs, String pagingState)
+ implements Supplier {
+
+ @Override
+ public CommandResult get() {
+ final List jsonNodes = new ArrayList<>();
+ docs.stream().forEach(doc -> jsonNodes.add(doc.document()));
+ final CommandResult.ResponseData responseData =
+ new CommandResult.ResponseData(jsonNodes, pagingState);
+ return new CommandResult(responseData);
+ }
+}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/FindOneCommandResolver.java b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/FindOneCommandResolver.java
new file mode 100644
index 0000000000..a9aaeae812
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/FindOneCommandResolver.java
@@ -0,0 +1,32 @@
+package io.stargate.sgv3.docsapi.service.resolver.model.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.stargate.sgv3.docsapi.api.model.command.impl.FindOneCommand;
+import io.stargate.sgv3.docsapi.service.resolver.model.impl.matcher.FilterableResolver;
+import java.util.Optional;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+/** Resolves the {@link FindOneCommand } */
+@ApplicationScoped
+public class FindOneCommandResolver extends FilterableResolver {
+
+ @Inject
+ public FindOneCommandResolver(ObjectMapper objectMapper) {
+ super(objectMapper, true, true);
+ }
+
+ public FindOneCommandResolver() {
+ this(null);
+ }
+
+ @Override
+ public Class getCommandClass() {
+ return FindOneCommand.class;
+ }
+
+ @Override
+ protected Optional getFilteringOption(FindOneCommand command) {
+ return Optional.of(new FilteringOptions(1, null));
+ }
+}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroup.java b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroup.java
new file mode 100644
index 0000000000..ce13fbbfce
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroup.java
@@ -0,0 +1,47 @@
+package io.stargate.sgv3.docsapi.service.resolver.model.impl.matcher;
+
+import io.stargate.sgv3.docsapi.api.model.command.Command;
+import io.stargate.sgv3.docsapi.api.model.command.clause.filter.ComparisonExpression;
+import io.stargate.sgv3.docsapi.api.model.command.clause.filter.FilterOperation;
+import io.stargate.sgv3.docsapi.api.model.command.clause.filter.FilterOperator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * The per API request result of running a {@link FilterMatcher.Capture} against a {@link Command}.
+ *
+ *
Is identified by the same Marker as the capture, collects the values the capture matched with
+ * and provides a way for the resolver to pull them out to use in commands.
+ *
+ *
Values from all of the operations that matched, they will be multiple values when we start
+ * using greedy matching e.g. match all the string comparison operations in {"username": "foo",
+ * "address.street": "bar"} Path in the {@link ComparisonExpression} that was matched, e.g.
+ * "address.street"
+ *
+ *
Created by the {@link CaptureGroups} via a builder
+ */
+public record CaptureGroup(Map>> captures) {
+
+ void consumeAllCaptures(Consumer> consumer) {
+ captures.forEach(
+ (key, operations) -> {
+ operations.forEach(
+ operation ->
+ consumer.accept(
+ new CaptureExpression(
+ key, operation.operator(), operation.operand().value())));
+ });
+ }
+
+ public void withCapture(String path, List> capture) {
+ captures.put(path, capture);
+ }
+
+ /**
+ * Here so we have simple consumer for consumeCaptures.
+ *
+ *
May also need to expand this to include the operation.
+ */
+ public static record CaptureExpression(String path, FilterOperator operator, TYPE value) {}
+}
diff --git a/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroups.java b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroups.java
new file mode 100644
index 0000000000..96e9fe5058
--- /dev/null
+++ b/src/main/java/io/stargate/sgv3/docsapi/service/resolver/model/impl/matcher/CaptureGroups.java
@@ -0,0 +1,54 @@
+package io.stargate.sgv3.docsapi.service.resolver.model.impl.matcher;
+
+import io.stargate.sgv3.docsapi.api.model.command.Command;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * All the {@link CaptureGroup}s we got from matching against a command.
+ *
+ *
This is "result" of running the FilterMatcher, and the value we pass to the resolver function
+ * so it has raw command and all the groups. See
+ *
+ *
Each Capture you create from {@link FilterMatcher#capture(Object)} with a Marker is available
+ * here as a {@link CaptureGroup} via {@link #getGroup(Object)}.
+ *
+ *
Created in the {@link FilterMatcher} via a builder
+ *
+ *
T - The {@link Command} that is filtered against
+ */
+public class CaptureGroups {
+ private final T command;
+ private final Map