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

Add support for "star" inclusion/exclusion (include all/exclude all) #1008

Merged
merged 10 commits into from
Apr 1, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
Expand All @@ -22,12 +23,24 @@ public class DocumentProjector {
* No-op projector that does not modify documents. Considered "exclusion" projector since "no
* exclusions" is conceptually what happens ("no inclusions" would drop all content)
*/
private static final DocumentProjector IDENTITY_PROJECTOR =
private static final DocumentProjector DEFAULT_PROJECTOR =
new DocumentProjector(null, false, false);

private static final DocumentProjector IDENTITY_PROJECTOR_WITH_SIMILARITY =
private static final DocumentProjector DEFAULT_PROJECTOR_WITH_SIMILARITY =
new DocumentProjector(null, false, true);

private static final DocumentProjector INCLUDE_ALL_PROJECTOR =
new DocumentProjector(null, false, false);

private static final DocumentProjector INCLUDE_ALL_PROJECTOR_WITH_SIMILARITY =
new DocumentProjector(null, false, true);

private static final DocumentProjector EXCLUDE_ALL_PROJECTOR =
new DocumentProjector(null, true, false);

private static final DocumentProjector EXCLUDE_ALL_PROJECTOR_WITH_SIMILARITY =
new DocumentProjector(null, true, true);

private final ProjectionLayer rootLayer;

/** Whether this projector is inclusion- ({@code true}) or exclusion ({@code false}) based. */
Expand All @@ -43,18 +56,22 @@ private DocumentProjector(
this.includeSimilarityScore = includeSimilarityScore;
}

public static DocumentProjector defaultProjector() {
return DEFAULT_PROJECTOR;
}

public static DocumentProjector createFromDefinition(JsonNode projectionDefinition) {
return createFromDefinition(projectionDefinition, false);
}

public static DocumentProjector createFromDefinition(
JsonNode projectionDefinition, boolean includeSimilarity) {
if (projectionDefinition == null) {
// First special case: "simple" default projection
if (projectionDefinition == null || projectionDefinition.isEmpty()) {
if (includeSimilarity) {
return identityProjectorWithSimilarity();
} else {
return identityProjector();
return DEFAULT_PROJECTOR_WITH_SIMILARITY;
}
return DEFAULT_PROJECTOR;
}
if (!projectionDefinition.isObject()) {
throw new JsonApiException(
Expand All @@ -63,15 +80,36 @@ public static DocumentProjector createFromDefinition(
+ ": definition must be OBJECT, was "
+ projectionDefinition.getNodeType());
}
// Special cases: "star-include/exclude"
if (projectionDefinition.size() == 1) {
Map.Entry<String, JsonNode> entry = projectionDefinition.fields().next();
if ("*".equals(entry.getKey())) {
boolean includeAll = extractIncludeOrExclude(entry.getKey(), entry.getValue());
if (includeAll) {
return includeSimilarity ? INCLUDE_ALL_PROJECTOR_WITH_SIMILARITY : INCLUDE_ALL_PROJECTOR;
}
return includeSimilarity ? EXCLUDE_ALL_PROJECTOR_WITH_SIMILARITY : EXCLUDE_ALL_PROJECTOR;
}
}
return PathCollector.collectPaths(projectionDefinition, includeSimilarity).buildProjector();
}

public static DocumentProjector identityProjector() {
return IDENTITY_PROJECTOR;
}

public static DocumentProjector identityProjectorWithSimilarity() {
return IDENTITY_PROJECTOR_WITH_SIMILARITY;
private static boolean extractIncludeOrExclude(String path, JsonNode value) {
if (value.isNumber()) {
// "0" means exclude (like false); any other number include
return !BigDecimal.ZERO.equals(value.decimalValue());
}
if (value.isBoolean()) {
return value.booleanValue();
}
// Unknown JSON node type; error
throw new JsonApiException(
ErrorCode.UNSUPPORTED_PROJECTION_PARAM,
ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage()
+ ": path ('"
+ path
+ "') value must be NUMBER or BOOLEAN, was "
+ value.getNodeType());
}

public boolean isInclusion() {
Expand All @@ -87,7 +125,12 @@ public void applyProjection(JsonNode document) {
}

public void applyProjection(JsonNode document, Float similarityScore) {
if (rootLayer == null) { // null -> identity projection (no-op)
// null -> either include-add or exclude-all; but logic may seem counter-intuitive
if (rootLayer == null) {
if (inclusion) { // exclude-all
((ObjectNode) document).removeAll();
}
// In either case, we may need to add similarity score if present
if (includeSimilarityScore && similarityScore != null) {
((ObjectNode) document)
.put(DocumentConstants.Fields.VECTOR_FUNCTION_PROJECTION_FIELD, similarityScore);
Expand Down Expand Up @@ -148,8 +191,8 @@ static PathCollector collectPaths(JsonNode def, boolean includeSimilarity) {
}

public DocumentProjector buildProjector() {
if (isIdentityProjection()) {
return identityProjector();
if (isDefaultProjection()) {
return defaultProjector();
}

// One more thing: do we need to add document id?
Expand All @@ -172,7 +215,7 @@ public DocumentProjector buildProjector() {
* Accessor to use for checking if collected paths indicate "empty" (no-operation) projection:
* if so, caller can avoid actual construction or evaluation.
*/
boolean isIdentityProjection() {
boolean isDefaultProjection() {
// Only the case if we have no non-doc-id inclusions/exclusions AND
// doc-id is included (by default or explicitly)
return paths.isEmpty() && slices.isEmpty() && !Boolean.FALSE.equals(idInclusion);
Expand Down Expand Up @@ -215,6 +258,13 @@ PathCollector collectFromObject(JsonNode ob, String parentPath) {
continue;
}

// Special rule for "*": only allowed as single root-level entry;
if ("*".equals(path)) {
throw new JsonApiException(
ErrorCode.UNSUPPORTED_PROJECTION_PARAM,
ErrorCode.UNSUPPORTED_PROJECTION_PARAM.getMessage()
+ ": wildcard ('*') only allowed as the only root-level path");
}
if (parentPath != null) {
path = parentPath + "." + path;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private FindOperation getFindOperation(CommandContext commandContext, DeleteMany
return FindOperation.unsorted(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
null,
operationsConfig.maxDocumentDeleteCount() + 1,
operationsConfig.defaultPageSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private FindOperation getFindOperation(CommandContext commandContext, DeleteOneC
return FindOperation.vsearchSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.KEY,
objectMapper,
vector);
Expand All @@ -74,7 +74,7 @@ private FindOperation getFindOperation(CommandContext commandContext, DeleteOneC
return FindOperation.sortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
// For in memory sorting we read more data than needed, so defaultSortPageSize like 100
operationsConfig.defaultSortPageSize(),
ReadType.SORTED_DOCUMENT,
Expand All @@ -88,7 +88,7 @@ private FindOperation getFindOperation(CommandContext commandContext, DeleteOneC
return FindOperation.unsortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.KEY,
objectMapper);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private FindOperation getFindOperation(
return FindOperation.vsearchSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper,
vector);
Expand All @@ -78,7 +78,7 @@ private FindOperation getFindOperation(
return FindOperation.sortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
// For in memory sorting we read more data than needed, so defaultSortPageSize like 100
operationsConfig.defaultSortPageSize(),
ReadType.SORTED_DOCUMENT,
Expand All @@ -92,7 +92,7 @@ private FindOperation getFindOperation(
return FindOperation.unsortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private FindOperation getFindOperation(
return FindOperation.vsearchSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper,
vector);
Expand All @@ -96,7 +96,7 @@ private FindOperation getFindOperation(
return FindOperation.sortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
// For in memory sorting we read more data than needed, so defaultSortPageSize like 100
operationsConfig.defaultSortPageSize(),
ReadType.SORTED_DOCUMENT,
Expand All @@ -110,7 +110,7 @@ private FindOperation getFindOperation(
return FindOperation.unsortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private FindOperation getFindOperation(
return FindOperation.vsearchSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper,
vector);
Expand All @@ -100,7 +100,7 @@ private FindOperation getFindOperation(
logicalExpression,
// 24-Mar-2023, tatu: Since we update the document, need to avoid modifications on
// read path, hence pass identity projector.
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
// For in memory sorting we read more data than needed, so defaultSortPageSize like 100
operationsConfig.defaultSortPageSize(),
ReadType.SORTED_DOCUMENT,
Expand All @@ -116,7 +116,7 @@ private FindOperation getFindOperation(
logicalExpression,
// 24-Mar-2023, tatu: Since we update the document, need to avoid modifications on
// read path, hence pass identity projector.
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public Operation resolveCommand(CommandContext commandContext, UpdateManyCommand
false,
upsert,
shredder,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
operationsConfig.maxDocumentUpdateCount(),
operationsConfig.lwt().retries());
}
Expand All @@ -68,7 +68,7 @@ private FindOperation getFindOperation(CommandContext commandContext, UpdateMany
return FindOperation.unsorted(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
null != command.options() ? command.options().pageState() : null,
Integer.MAX_VALUE,
operationsConfig.defaultPageSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public Operation resolveCommand(CommandContext commandContext, UpdateOneCommand
false,
upsert,
shredder,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
1,
operationsConfig.lwt().retries());
}
Expand All @@ -81,7 +81,7 @@ private FindOperation getFindOperation(CommandContext commandContext, UpdateOneC
return FindOperation.vsearchSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper,
vector);
Expand All @@ -93,7 +93,7 @@ private FindOperation getFindOperation(CommandContext commandContext, UpdateOneC
return FindOperation.sortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
// For in memory sorting we read more data than needed, so defaultSortPageSize like 100
operationsConfig.defaultSortPageSize(),
ReadType.SORTED_DOCUMENT,
Expand All @@ -107,7 +107,7 @@ private FindOperation getFindOperation(CommandContext commandContext, UpdateOneC
return FindOperation.unsortedSingle(
commandContext,
logicalExpression,
DocumentProjector.identityProjector(),
DocumentProjector.defaultProjector(),
ReadType.DOCUMENT,
objectMapper);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,63 @@ public void byIdEmptyProjection() {
"""));
}

@Test
public void byIdIncludeAllProjection() {
given()
.header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken())
.contentType(ContentType.JSON)
.body(
"""
{
"find": {
"filter" : {"_id" : "doc5"},
"projection": { "*": 1 }
}
}
""")
.when()
.post(CollectionResource.BASE_PATH, namespaceName, collectionName)
.then()
.statusCode(200)
.body("status", is(nullValue()))
.body("errors", is(nullValue()))
.body("data.documents", hasSize(1))
.body(
"data.documents[0]",
jsonEquals(
"""
{
"_id": "doc5",
"username": "user5",
"sub_doc" : { "a": 5, "b": { "c": "v1", "d": false } }
}
"""));
}

@Test
public void byIdExcludeAllProjection() {
given()
.header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken())
.contentType(ContentType.JSON)
.body(
"""
{
"find": {
"filter" : {"_id" : "doc5"},
"projection": { "*": 0 }
}
}
""")
.when()
.post(CollectionResource.BASE_PATH, namespaceName, collectionName)
.then()
.statusCode(200)
.body("status", is(nullValue()))
.body("errors", is(nullValue()))
.body("data.documents", hasSize(1))
.body("data.documents[0]", jsonEquals("{}"));
}

// https://github.com/stargate/jsonapi/issues/572 -- is passing empty Object for "sort" ok?
@Test
public void byIdEmptySort() {
Expand Down
Loading
Loading