diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Projectable.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Projectable.java new file mode 100644 index 0000000000..8439c36532 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Projectable.java @@ -0,0 +1,15 @@ +package io.stargate.sgv2.jsonapi.api.model.command; + +import com.fasterxml.jackson.databind.JsonNode; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; + +/* + * All the commands that need Projection definitions will have to implement this. + */ +public interface Projectable { + JsonNode projectionDefinition(); + + default DocumentProjector buildProjector() { + return DocumentProjector.createFromDefinition(projectionDefinition()); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindCommand.java index 83b437878f..9f1439ec0d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindCommand.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.JsonNode; import io.stargate.sgv2.jsonapi.api.model.command.Filterable; +import io.stargate.sgv2.jsonapi.api.model.command.Projectable; import io.stargate.sgv2.jsonapi.api.model.command.ReadCommand; import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause; @@ -15,9 +17,10 @@ @JsonTypeName("find") public record FindCommand( @Valid @JsonProperty("filter") FilterClause filterClause, + @JsonProperty("projection") JsonNode projectionDefinition, @Valid @JsonProperty("sort") SortClause sortClause, @Valid @Nullable Options options) - implements ReadCommand, Filterable { + implements ReadCommand, Filterable, Projectable { public record Options( @Valid diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneAndUpdateCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneAndUpdateCommand.java index 2328317b32..b3a42275ee 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneAndUpdateCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneAndUpdateCommand.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.JsonNode; import io.stargate.sgv2.jsonapi.api.model.command.Filterable; +import io.stargate.sgv2.jsonapi.api.model.command.Projectable; import io.stargate.sgv2.jsonapi.api.model.command.ReadCommand; import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause; import io.stargate.sgv2.jsonapi.api.model.command.clause.update.UpdateClause; @@ -18,9 +20,10 @@ @JsonTypeName("findOneAndUpdate") public record FindOneAndUpdateCommand( @Valid @JsonProperty("filter") FilterClause filterClause, + @JsonProperty("projection") JsonNode projectionDefinition, @NotNull @Valid @JsonProperty("update") UpdateClause updateClause, @Valid @Nullable Options options) - implements ReadCommand, Filterable { + implements ReadCommand, Filterable, Projectable { @Schema( name = "FindOneAndUpdateCommand.Options", diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneCommand.java index be9caeb95a..0a80cce43f 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/FindOneCommand.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.JsonNode; import io.stargate.sgv2.jsonapi.api.model.command.Filterable; +import io.stargate.sgv2.jsonapi.api.model.command.Projectable; import io.stargate.sgv2.jsonapi.api.model.command.ReadCommand; import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause; @@ -13,5 +15,6 @@ @JsonTypeName("findOne") public record FindOneCommand( @Valid @JsonProperty("filter") FilterClause filterClause, + @JsonProperty("projection") JsonNode projectionDefinition, @Valid @JsonProperty("sort") SortClause sortClause) - implements ReadCommand, Filterable {} + implements ReadCommand, Filterable, Projectable {} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java index 57929e0765..09a215fad0 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java @@ -34,6 +34,8 @@ public enum ErrorCode { UNSUPPORTED_OPERATION("Unsupported operation class"), + UNSUPPORTED_PROJECTION_PARAM("Unsupported projection parameter"), + UNSUPPORTED_UPDATE_DATA_TYPE("Unsupported update data type"), UNSUPPORTED_UPDATE_OPERATION("Unsupported update operation"), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java index dcacf47e88..bd8a7657ab 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java @@ -1,6 +1,7 @@ package io.stargate.sgv2.jsonapi.service.operation.model; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.mutiny.Uni; import io.stargate.bridge.grpc.BytesValues; @@ -10,6 +11,7 @@ import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadDocument; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.ArrayList; import java.util.Iterator; @@ -18,7 +20,7 @@ /** * 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} + * implementation to execute and query and parse the result set as {@link FindResponse} */ public interface ReadOperation extends Operation { String[] documentColumns = {"key", "tx_id", "doc_json"}; @@ -41,7 +43,8 @@ default Uni findDocument( String pagingState, int pageSize, boolean readDocument, - ObjectMapper objectMapper) { + ObjectMapper objectMapper, + DocumentProjector projection) { return queryExecutor .executeRead(query, Optional.ofNullable(pagingState), pageSize) .onItem() @@ -55,13 +58,16 @@ default Uni findDocument( QueryOuterClass.Row row = rowIterator.next(); ReadDocument document = null; try { + JsonNode root = + readDocument ? objectMapper.readTree(Values.string(row.getValues(2))) : null; + if (root != null) { + projection.applyProjection(root); + } document = new ReadDocument( getDocumentId(row.getValues(0)), // key Values.uuid(row.getValues(1)), // tx_id - readDocument - ? objectMapper.readTree(Values.string(row.getValues(2))) - : null); + root); } catch (JsonProcessingException e) { throw new JsonApiException(ErrorCode.DOCUMENT_UNPARSEABLE); } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java index 5f52b5dc6e..7b1d90aa96 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java @@ -15,6 +15,7 @@ import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.ArrayList; import java.util.List; @@ -24,6 +25,7 @@ public record FindOperation( CommandContext commandContext, List filters, + DocumentProjector projection, String pagingState, int limit, int pageSize, @@ -64,7 +66,8 @@ public Uni getDocuments( pagingState, pageSize, ReadType.DOCUMENT == readType, - objectMapper); + objectMapper, + projection); } default -> { JsonApiException failure = diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java index 7f06f6b928..125656757c 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java @@ -181,6 +181,11 @@ private Uni processUpdate( if (returnDocumentInResponse) { documentToReturn = returnUpdatedDocument ? updatedDocument : originalDocument; + // 24-Mar-2023, tatu: Will need to add projection in follow-up PR; + // one we get here is identity-projection which does nothing. + // + // DocumentProjector projector = findOperation().projection(); + // projector.applyProjection(documentToReturn); } return new UpdatedDocument( writableShreddedDocument.id(), upsert, documentToReturn, null); diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjector.java b/src/main/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjector.java new file mode 100644 index 0000000000..6fea055009 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjector.java @@ -0,0 +1,194 @@ +package io.stargate.sgv2.jsonapi.service.projection; + +import com.fasterxml.jackson.databind.JsonNode; +import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; +import io.stargate.sgv2.jsonapi.exception.ErrorCode; +import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Helper class that implements functionality needed to support projections on documents fetched via + * various {@code find} commands. + */ +public class DocumentProjector { + /** Pseudo-projector that makes no modifications to documents */ + private static final DocumentProjector IDENTITY_PROJECTOR = new DocumentProjector(null, true); + + private final ProjectionLayer rootLayer; + + /** Whether this projector is inclusion- ({@code true}) or exclusion ({@code false}) based. */ + private final boolean inclusion; + + private DocumentProjector(ProjectionLayer rootLayer, boolean inclusion) { + this.rootLayer = rootLayer; + this.inclusion = inclusion; + } + + public static DocumentProjector createFromDefinition(JsonNode projectionDefinition) { + if (projectionDefinition == null) { + return identityProjector(); + } + if (!projectionDefinition.isObject()) { + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, + ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage() + + ": definition must be OBJECT, was " + + projectionDefinition.getNodeType()); + } + return PathCollector.collectPaths(projectionDefinition).buildProjector(); + } + + public static DocumentProjector identityProjector() { + return IDENTITY_PROJECTOR; + } + + public boolean isInclusion() { + return inclusion; + } + + public void applyProjection(JsonNode document) { + if (rootLayer != null) { // null -> identity projection (no-op) + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, "Non-identity Projections not yet supported"); + } + } + + // Mostly for deserialization tests + @Override + public boolean equals(Object o) { + if (o instanceof DocumentProjector) { + DocumentProjector other = (DocumentProjector) o; + return (this.inclusion == other.inclusion) && Objects.equals(this.rootLayer, other.rootLayer); + } + return false; + } + + @Override + public int hashCode() { + return rootLayer.hashCode(); + } + + /** + * Helper object used to traverse and collection inclusion/exclusion path definitions and verify + * that there are only one or the other (except for doc id). Does not build data structures for + * actual matching. + */ + private static class PathCollector { + private List paths = new ArrayList<>(); + + private int exclusions, inclusions; + + private Boolean idInclusion = null; + + private PathCollector() {} + + static PathCollector collectPaths(JsonNode def) { + return new PathCollector().collectFromObject(def, null); + } + + public DocumentProjector buildProjector() { + if (isIdentityProjection()) { + return DocumentProjector.identityProjector(); + } + + // One more thing: do we need to add document id? + if (inclusions > 0) { // inclusion-based projection + // doc-id included unless explicitly excluded + return new DocumentProjector( + ProjectionLayer.buildLayers(paths, !Boolean.FALSE.equals(idInclusion)), true); + } else { // exclusion-based + // doc-id excluded only if explicitly excluded + return new DocumentProjector( + ProjectionLayer.buildLayers(paths, Boolean.FALSE.equals(idInclusion)), false); + } + } + + /** + * Accessor to use for checking if collected paths indicate "empty" (no-operation) projection: + * if so, caller can avoid actual construction or evaluation. + */ + boolean isIdentityProjection() { + // Only the case if we have no non-doc-id inclusions/exclusions AND + // doc-id is included (by default or explicitly) + return paths.isEmpty() && !Boolean.FALSE.equals(idInclusion); + } + + PathCollector collectFromObject(JsonNode ob, String parentPath) { + var it = ob.fields(); + while (it.hasNext()) { + var entry = it.next(); + String path = entry.getKey(); + if (parentPath != null) { + path = parentPath + "." + path; + } + JsonNode value = entry.getValue(); + if (value.isNumber()) { + // "0" means exclude (like false); any other number include + if (BigDecimal.ZERO.equals(value.decimalValue())) { + addExclusion(path); + } else { + addInclusion(path); + } + } else if (value.isBoolean()) { + if (value.booleanValue()) { + addInclusion(path); + } else { + addExclusion(path); + } + } else if (value.isObject()) { + // Nested definitions allowed, too + collectFromObject(value, path); + } else { + // Unknown JSON node type; error + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, + ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage() + + ": path ('" + + path + + "') value must be NUMBER, BOOLEAN or OBJECT, was " + + value.getNodeType()); + } + } + return this; + } + + private void addExclusion(String path) { + if (DocumentConstants.Fields.DOC_ID.equals(path)) { + idInclusion = false; + } else { + // Must not mix exclusions and inclusions + if (inclusions > 0) { + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, + ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage() + + ": cannot exclude '" + + path + + "' on inclusion projection"); + } + ++exclusions; + paths.add(path); + } + } + + private void addInclusion(String path) { + if (DocumentConstants.Fields.DOC_ID.equals(path)) { + idInclusion = true; + } else { + // Must not mix exclusions and inclusions + if (exclusions > 0) { + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, + ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage() + + ": cannot include '" + + path + + "' on exclusion projection"); + } + ++inclusions; + paths.add(path); + } + } + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/projection/ProjectionLayer.java b/src/main/java/io/stargate/sgv2/jsonapi/service/projection/ProjectionLayer.java new file mode 100644 index 0000000000..7a322290d0 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/projection/ProjectionLayer.java @@ -0,0 +1,114 @@ +package io.stargate.sgv2.jsonapi.service.projection; + +import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; +import io.stargate.sgv2.jsonapi.exception.ErrorCode; +import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Helper class that handles projection traversal for one level of nesting. Layers are either + * non-terminal (branches) or terminal (leaves) + */ +class ProjectionLayer { + private static final Pattern DOT = Pattern.compile(Pattern.quote(".")); + + /** Whether this layer is terminal (matching) or branch (non-matching) */ + private final boolean isTerminal; + + /** + * Full path either to this layer (terminal), or to the first path through it (non-terminal) -- + * needed for conflict reporting. + */ + private final String fullPath; + + /** For non-terminal layers, segment-indexed next layers */ + private final Map nextLayers; + + ProjectionLayer(boolean terminal, String fullPath) { + isTerminal = terminal; + this.fullPath = fullPath; + nextLayers = isTerminal ? null : new HashMap<>(); + } + + public static ProjectionLayer buildLayers(Collection dotPaths, boolean addDocId) { + // Root is always branch (not terminal): + ProjectionLayer root = new ProjectionLayer(false, ""); + for (String fullPath : dotPaths) { + String[] segments = DOT.split(fullPath); + buildPath(fullPath, root, segments); + } + // May need to add doc-id inclusion/exclusion as well + if (addDocId) { + buildPath( + DocumentConstants.Fields.DOC_ID, root, new String[] {DocumentConstants.Fields.DOC_ID}); + } + return root; + } + + static void buildPath(String fullPath, ProjectionLayer layer, String[] segments) { + // First create branches + final int last = segments.length - 1; + for (int i = 0; i < last; ++i) { + // Try to find or create branch + layer = layer.findOrCreateBranch(fullPath, segments[i]); + } + // And then attach terminal (leaf) + layer.addTerminal(fullPath, segments[last]); + } + + ProjectionLayer findOrCreateBranch(String fullPath, String segment) { + // Cannot proceed past terminal layer (shorter path): + if (isTerminal) { + reportPathConflict(this.fullPath, fullPath); + } + ProjectionLayer next = nextLayers.get(segment); + if (next == null) { + next = new ProjectionLayer(false, fullPath); + nextLayers.put(segment, next); + } + return next; + } + + void addTerminal(String fullPath, String segment) { + // Cannot proceed past terminal layer (shorter path): + if (isTerminal) { + reportPathConflict(this.fullPath, fullPath); + } + // But will also not allow existing longer path: + ProjectionLayer next = nextLayers.get(segment); + if (next != null) { + reportPathConflict(fullPath, next.fullPath); + } + nextLayers.put(segment, new ProjectionLayer(true, fullPath)); + } + + void reportPathConflict(String fullPath1, String fullPath2) { + throw new JsonApiException( + ErrorCode.UNSUPPORTED_PROJECTION_PARAM, + ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage() + + ": projection path conflict between '" + + fullPath1 + + "' and '" + + fullPath2 + + "'"); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof ProjectionLayer)) return false; + ProjectionLayer other = (ProjectionLayer) o; + return (this.isTerminal == other.isTerminal) + && Objects.equals(this.fullPath, other.fullPath) + && Objects.equals(this.nextLayers, other.nextLayers); + } + + @Override + public int hashCode() { + return Objects.hash(isTerminal, fullPath, nextLayers); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteManyCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteManyCommandResolver.java index 95abcdeccb..382ee3a4e3 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteManyCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteManyCommandResolver.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import java.util.List; @@ -54,6 +55,7 @@ private FindOperation getFindOperation(CommandContext commandContext, DeleteMany return new FindOperation( commandContext, filters, + DocumentProjector.identityProjector(), null, documentConfig.maxDocumentDeleteCount() + 1, documentConfig.defaultPageSize(), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java index 4c34ab16c6..f29241d584 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import java.util.List; @@ -46,6 +47,14 @@ public Class getCommandClass() { private FindOperation getFindOperation(CommandContext commandContext, DeleteOneCommand command) { List filters = resolve(commandContext, command); - return new FindOperation(commandContext, filters, null, 1, 1, ReadType.KEY, objectMapper); + return new FindOperation( + commandContext, + filters, + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.KEY, + objectMapper); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java index 7debd142e0..cec90fc30d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java @@ -45,6 +45,13 @@ public Operation resolveCommand(CommandContext commandContext, FindCommand comma int pageSize = documentConfig.defaultPageSize(); String pagingState = command.options() != null ? command.options().pagingState() : null; return new FindOperation( - commandContext, filters, pagingState, limit, pageSize, ReadType.DOCUMENT, objectMapper); + commandContext, + filters, + command.buildProjector(), + pagingState, + limit, + pageSize, + ReadType.DOCUMENT, + objectMapper); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java index b7f2b77cb9..233cefb35a 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import io.stargate.sgv2.jsonapi.service.shredding.Shredder; @@ -67,6 +68,16 @@ public Operation resolveCommand(CommandContext commandContext, FindOneAndUpdateC private FindOperation getFindOperation( CommandContext commandContext, FindOneAndUpdateCommand command) { List filters = resolve(commandContext, command); - return new FindOperation(commandContext, filters, null, 1, 1, ReadType.DOCUMENT, objectMapper); + return new FindOperation( + commandContext, + filters, + // 24-Mar-2023, tatu: Since we update the document, need to avoid modifications on + // read path, hence pass identity projector. + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolver.java index fbd2e691b3..50b4adb711 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolver.java @@ -32,8 +32,15 @@ public Class getCommandClass() { @Override public Operation resolveCommand(CommandContext commandContext, FindOneCommand command) { - List filters = resolve(commandContext, command); - return new FindOperation(commandContext, filters, null, 1, 1, ReadType.DOCUMENT, objectMapper); + return new FindOperation( + commandContext, + filters, + command.buildProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java index 3ad8d44dea..e8c666474d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import io.stargate.sgv2.jsonapi.service.shredding.Shredder; @@ -67,6 +68,7 @@ private FindOperation getFindOperation(CommandContext commandContext, UpdateMany return new FindOperation( commandContext, filters, + DocumentProjector.identityProjector(), null, documentConfig.maxDocumentUpdateCount() + 1, documentConfig.defaultPageSize(), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java index eb77947fb9..7bda4dd87b 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import io.stargate.sgv2.jsonapi.service.shredding.Shredder; @@ -64,6 +65,14 @@ public Operation resolveCommand(CommandContext commandContext, UpdateOneCommand private FindOperation getFindOperation(CommandContext commandContext, UpdateOneCommand command) { List filters = resolve(commandContext, command); - return new FindOperation(commandContext, filters, null, 1, 1, ReadType.DOCUMENT, objectMapper); + return new FindOperation( + commandContext, + filters, + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java index b5f61ac234..05bb899038 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java @@ -18,6 +18,7 @@ import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.model.DocValueHasher; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.List; @@ -90,6 +91,7 @@ public void deleteWithId() { List.of( new DBFilterBase.IDFilter( DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1"))), + DocumentProjector.identityProjector(), null, 1, 1, @@ -142,6 +144,7 @@ public void deleteWithIdNoData() { List.of( new DBFilterBase.IDFilter( DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1"))), + DocumentProjector.identityProjector(), null, 1, 1, @@ -211,6 +214,7 @@ public void deleteWithDynamic() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 1, 1, @@ -317,6 +321,7 @@ public void deleteWithDynamicRetry() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 1, 1, @@ -425,6 +430,7 @@ public void deleteWithDynamicRetryFailure() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 1, 1, @@ -528,6 +534,7 @@ public void deleteWithDynamicRetryConcurrentDelete() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 1, 1, @@ -613,6 +620,7 @@ public void deleteManyWithDynamic() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 3, 2, @@ -696,6 +704,7 @@ public void deleteManyWithDynamicPaging() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 3, 1, @@ -785,6 +794,7 @@ public void deleteManyWithDynamicPagingAndMoreData() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 3, 1, @@ -845,6 +855,7 @@ public void deleteWithNoResult() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 1, 1, @@ -965,6 +976,7 @@ public void errorPartial() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 3, 3, @@ -1135,6 +1147,7 @@ public void errorAll() { List.of( new DBFilterBase.TextFilter( "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1")), + DocumentProjector.identityProjector(), null, 3, 3, diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java index 7a3a2f6317..575f8a17c2 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java @@ -18,6 +18,7 @@ import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.model.DocValueHasher; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.List; @@ -98,7 +99,14 @@ public void findAll() throws Exception { FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(), null, 20, 20, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(), + DocumentProjector.identityProjector(), + null, + 20, + 20, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -166,7 +174,14 @@ public void findWithId() throws Exception { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -219,7 +234,14 @@ public void findWithIdNoData() { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -284,7 +306,14 @@ public void findWithDynamic() throws Exception { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -351,7 +380,14 @@ public void findWithBooleanFilter() throws Exception { "registration_active", DBFilterBase.MapFilterBase.Operator.EQ, true); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -414,7 +450,14 @@ public void findWithExistsFilter() throws Exception { DBFilterBase.ExistsFilter filter = new DBFilterBase.ExistsFilter("registration_active", true); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -480,7 +523,15 @@ public void findWithAllFilter() throws Exception { new DBFilterBase.AllFilter(new DocValueHasher(), "tags", "tag1"), new DBFilterBase.AllFilter(new DocValueHasher(), "tags", "tag2")); FindOperation operation = - new FindOperation(COMMAND_CONTEXT, filters, null, 1, 1, ReadType.DOCUMENT, objectMapper); + new FindOperation( + COMMAND_CONTEXT, + filters, + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -544,7 +595,14 @@ public void findWithSizeFilter() throws Exception { DBFilterBase.SizeFilter filter = new DBFilterBase.SizeFilter("tags", 2); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -610,7 +668,14 @@ public void findWithArrayEqualFilter() throws Exception { new DBFilterBase.ArrayEqualsFilter(new DocValueHasher(), "tags", List.of("tag1", "tag2")); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -677,7 +742,14 @@ public void findWithSubDocEqualFilter() throws Exception { new DocValueHasher(), "sub_doc", Map.of("col", "val")); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Supplier execute = operation @@ -735,7 +807,14 @@ public void failurePropagated() { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation operation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); Throwable failure = operation @@ -803,7 +882,14 @@ public void findWithId() { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); ReadOperation.FindResponse result = findOperation @@ -868,7 +954,14 @@ public void findWithIdWithIdRetry() { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); ReadOperation.FindResponse result = findOperation @@ -931,7 +1024,14 @@ public void findWithDynamic() { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); ReadOperation.FindResponse result = findOperation @@ -996,7 +1096,14 @@ public void findWithDynamicWithIdRetry() { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DBFilterBase.IDFilter idFilter = new DBFilterBase.IDFilter( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationRetryTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationRetryTest.java index 93940ec98c..aa4328edfa 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationRetryTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationRetryTest.java @@ -20,6 +20,7 @@ import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.Shredder; import io.stargate.sgv2.jsonapi.service.shredding.model.DocValueHasher; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; @@ -213,7 +214,14 @@ public void findOneAndUpdateWithRetry() throws Exception { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -409,7 +417,14 @@ public void findAndUpdateWithRetryFailure() throws Exception { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -614,7 +629,14 @@ public void findAndUpdateWithRetryFailureWithUpsert() throws Exception { new DBFilterBase.TextFilter("username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -876,7 +898,14 @@ public void findAndUpdateWithRetryPartialFailure() throws Exception { new DBFilterBase.TextFilter("status", DBFilterBase.MapFilterBase.Operator.EQ, "active"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 3, 3, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 3, + 3, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -1195,7 +1224,14 @@ public void findOneAndUpdateWithRetryMultipleFailure() throws Exception { new DBFilterBase.TextFilter("status", DBFilterBase.MapFilterBase.Operator.EQ, "active"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 3, 3, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 3, + 3, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java index b952e69922..dd051df6e5 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java @@ -20,6 +20,7 @@ import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.Shredder; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import io.stargate.sgv2.jsonapi.service.shredding.model.WritableShreddedDocument; @@ -155,7 +156,14 @@ public void happyPath() throws Exception { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -275,7 +283,14 @@ public void withUpsert() throws Exception { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -339,7 +354,14 @@ public void noData() { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 1, 1, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 1, + 1, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -535,7 +557,14 @@ public void happyPath() throws Exception { new DBFilterBase.TextFilter("status", DBFilterBase.MapFilterBase.Operator.EQ, "active"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 21, 20, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -656,7 +685,14 @@ public void withUpsert() throws Exception { DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1")); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 21, 20, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( @@ -716,7 +752,14 @@ public void noData() { new DBFilterBase.TextFilter("status", DBFilterBase.MapFilterBase.Operator.EQ, "active"); FindOperation findOperation = new FindOperation( - COMMAND_CONTEXT, List.of(filter), null, 21, 20, ReadType.DOCUMENT, objectMapper); + COMMAND_CONTEXT, + List.of(filter), + DocumentProjector.identityProjector(), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); DocumentUpdater documentUpdater = DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjectorTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjectorTest.java new file mode 100644 index 0000000000..1af890b048 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/projection/DocumentProjectorTest.java @@ -0,0 +1,135 @@ +package io.stargate.sgv2.jsonapi.service.projection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import io.stargate.sgv2.jsonapi.exception.ErrorCode; +import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import javax.inject.Inject; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +public class DocumentProjectorTest { + @Inject ObjectMapper objectMapper; + + // Tests for validating issues with Projection definitions + @Nested + class ProjectorDefValidation { + @Test + public void verifyProjectionJsonObject() throws Exception { + JsonNode def = objectMapper.readTree(" [ 1, 2, 3 ]"); + Throwable t = catchThrowable(() -> DocumentProjector.createFromDefinition(def)); + assertThat(t) + .isInstanceOf(JsonApiException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNSUPPORTED_PROJECTION_PARAM) + .hasMessage("Unsupported projection parameter: definition must be OBJECT, was ARRAY"); + } + + @Test + public void verifyNoIncludeAfterExclude() throws Exception { + JsonNode def = + objectMapper.readTree( + """ + { "excludeMe" : 0, + "excludeMeToo" : 0, + "include.me" : 1 + } + """); + Throwable t = catchThrowable(() -> DocumentProjector.createFromDefinition(def)); + assertThat(t) + .isInstanceOf(JsonApiException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNSUPPORTED_PROJECTION_PARAM) + .hasMessage( + "Unsupported projection parameter: cannot include 'include.me' on exclusion projection"); + } + + @Test + public void verifyNoPathOverlap() throws Exception { + JsonNode def = + objectMapper.readTree( + """ + { "branch" : 1, + "branch.x.leaf" : 1, + "include.me" : 1 + } + """); + Throwable t = catchThrowable(() -> DocumentProjector.createFromDefinition(def)); + assertThat(t) + .isInstanceOf(JsonApiException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNSUPPORTED_PROJECTION_PARAM) + .hasMessage( + "Unsupported projection parameter: projection path conflict between 'branch' and 'branch.x.leaf'"); + + // Should be caught regardless of ordering (longer vs shorter path first) + JsonNode def2 = + objectMapper.readTree( + """ + { "a.y.leaf" : 1, + "a" : 1, + "value" : 1 + } + """); + Throwable t2 = catchThrowable(() -> DocumentProjector.createFromDefinition(def2)); + assertThat(t2) + .isInstanceOf(JsonApiException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNSUPPORTED_PROJECTION_PARAM) + .hasMessage( + "Unsupported projection parameter: projection path conflict between 'a' and 'a.y.leaf'"); + } + + @Test + public void verifyNoExcludeAfterInclude() throws Exception { + JsonNode def = + objectMapper.readTree( + """ + { "includeMe" : 1, + "misc" : { + "nested": { + "do" : true, + "dont" : false + } + }, + "includeMe2" : 1 + } + """); + Throwable t = catchThrowable(() -> DocumentProjector.createFromDefinition(def)); + assertThat(t) + .isInstanceOf(JsonApiException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNSUPPORTED_PROJECTION_PARAM) + .hasMessage( + "Unsupported projection parameter: cannot exclude 'misc.nested.dont' on inclusion projection"); + } + + @Test + public void verifyProjectionEquality() throws Exception { + String defStr1 = "{ \"field1\" : 1, \"field2\": 1 }"; + String defStr2 = "{ \"field1\" : 0, \"field2\": 0 }"; + + DocumentProjector proj1 = + DocumentProjector.createFromDefinition(objectMapper.readTree(defStr1)); + assertThat(proj1.isInclusion()).isTrue(); + DocumentProjector proj2 = + DocumentProjector.createFromDefinition(objectMapper.readTree(defStr2)); + assertThat(proj2.isInclusion()).isFalse(); + + // First, verify equality of identical definitions + assertThat(proj1) + .isEqualTo(DocumentProjector.createFromDefinition(objectMapper.readTree(defStr1))); + assertThat(proj2) + .isEqualTo(DocumentProjector.createFromDefinition(objectMapper.readTree(defStr2))); + + // Then inequality + assertThat(proj1) + .isNotEqualTo(DocumentProjector.createFromDefinition(objectMapper.readTree(defStr2))); + assertThat(proj2) + .isNotEqualTo(DocumentProjector.createFromDefinition(objectMapper.readTree(defStr1))); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java index c5c9396d9c..b251d8b951 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; @@ -13,6 +14,7 @@ import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; +import io.stargate.sgv2.jsonapi.service.projection.DocumentProjector; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.List; import javax.inject.Inject; @@ -49,6 +51,7 @@ public void idFilterCondition() throws Exception { List.of( new DBFilterBase.IDFilter( DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("id"))), + DocumentProjector.identityProjector(), null, documentConfig.maxLimit(), documentConfig.defaultPageSize(), @@ -80,6 +83,7 @@ public void noFilterCondition() throws Exception { new FindOperation( commandContext, List.of(), + DocumentProjector.identityProjector(), null, documentConfig.maxLimit(), documentConfig.defaultPageSize(), @@ -115,6 +119,7 @@ public void noFilterConditionWithOptions() throws Exception { new FindOperation( commandContext, List.of(), + DocumentProjector.identityProjector(), "dlavjhvbavkjbna", 10, documentConfig.defaultPageSize(), @@ -149,6 +154,95 @@ public void dynamicFilterCondition() throws Exception { List.of( new DBFilterBase.TextFilter( "col", DBFilterBase.MapFilterBase.Operator.EQ, "val")), + DocumentProjector.identityProjector(), + null, + documentConfig.maxLimit(), + documentConfig.defaultPageSize(), + ReadType.DOCUMENT, + objectMapper); + assertThat(operation) + .isInstanceOf(FindOperation.class) + .satisfies( + op -> { + assertThat(op).isEqualTo(expected); + }); + } + } + + @Nested + class FindCommandResolveWithProjection { + @Test + public void idFilterConditionAndProjection() throws Exception { + final JsonNode projectionDef = + objectMapper.readTree( + """ + { + "field1" : 1, + "field2" : 1 + } + """); + String json = + """ + { + "find": { + "filter" : {"_id" : "id"}, + "projection": %s + } + } + """ + .formatted(projectionDef); + FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = findCommandResolver.resolveCommand(commandContext, findCommand); + FindOperation expected = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.IDFilter( + DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("id"))), + DocumentProjector.createFromDefinition(projectionDef), + null, + documentConfig.maxLimit(), + documentConfig.defaultPageSize(), + ReadType.DOCUMENT, + objectMapper); + assertThat(operation) + .isInstanceOf(FindOperation.class) + .satisfies( + op -> { + assertThat(op).isEqualTo(expected); + }); + } + + @Test + public void noFilterConditionWithProjection() throws Exception { + final JsonNode projectionDef = + objectMapper.readTree( + """ + { + "field1" : 1, + "field2" : 1 + } + """); + String json = + """ + { + "find": { + "projection" : %s + } + } + """ + .formatted(projectionDef); + + FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = + findCommandResolver.resolveCommand(commandContext, findOneCommand); + FindOperation expected = + new FindOperation( + commandContext, + List.of(), + DocumentProjector.createFromDefinition(projectionDef), null, documentConfig.maxLimit(), documentConfig.defaultPageSize(),