Skip to content

Commit

Permalink
Implement $eq support for sub document (#98)
Browse files Browse the repository at this point in the history
Co-authored-by: Tatu Saloranta <87213665+tatu-at-datastax@users.noreply.github.com>
  • Loading branch information
maheshrajamani and tatu-at-datastax authored Feb 10, 2023
1 parent c4ff8a5 commit 75514ec
Show file tree
Hide file tree
Showing 11 changed files with 589 additions and 314 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.math.BigDecimal;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
Expand Down Expand Up @@ -41,7 +42,6 @@ public static ComparisonExpression eq(String path, Object value) {
*
* <p>e.g. {"username" : "aaron"}
*
* @param path Json node path
* @param value Value returned by the deserializer
* @return {@link ComparisonExpression} with equal operator
*/
Expand Down Expand Up @@ -74,6 +74,9 @@ private static JsonLiteral<?> getLiteral(Object value) {
if (value instanceof List) {
return new JsonLiteral<>((List<Object>) value, JsonType.ARRAY);
}
if (value instanceof Map) {
return new JsonLiteral<>((Map<String, Object>) value, JsonType.SUB_DOC);
}
throw new JsonApiException(
ErrorCode.FILTER_UNRESOLVABLE,
String.format("Unsupported filter value type %s", value.getClass()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ComparisonExpression;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterOperator;
Expand All @@ -16,6 +17,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -70,8 +72,18 @@ private ComparisonExpression createComparisonExpression(Map.Entry<String, JsonNo
final Iterator<Map.Entry<String, JsonNode>> fields = entry.getValue().fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> updateField = fields.next();
FilterOperator operator =
FilterOperator.FilterOperatorUtils.getComparisonOperator(updateField.getKey());
FilterOperator operator = null;
try {
operator = FilterOperator.FilterOperatorUtils.getComparisonOperator(updateField.getKey());
} catch (JsonApiException exception) {
// If getComparisonOperator returns an exception, check for subdocument equality condition,
// this will happen when shortcut is used "filter" : { "size" : { "w": 21, "h": 14} }
if (updateField.getKey().startsWith("$")) {
throw exception;
} else {
return ComparisonExpression.eq(entry.getKey(), jsonNodeValue(entry.getValue()));
}
}
JsonNode value = updateField.getValue();
// @TODO: Need to add array and sub-document value type to this condition
expression.add(operator, jsonNodeValue(entry.getKey(), value));
Expand Down Expand Up @@ -105,6 +117,17 @@ private static Object jsonNodeValue(JsonNode node) {
}
return arrayVals;
}
case OBJECT:
{
ObjectNode objectNode = (ObjectNode) node;
Map<String, Object> values = new LinkedHashMap<>(objectNode.size());
final Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
final Map.Entry<String, JsonNode> nextField = fields.next();
values.put(nextField.getKey(), jsonNodeValue(nextField.getValue()));
}
return values;
}
default:
throw new JsonApiException(
ErrorCode.UNSUPPORTED_FILTER_DATA_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected QueryOuterClass.Query getCreateTable(String keyspace, String table) {
+ " doc_json text,"
+ " doc_properties map<text, int>,"
+ " exist_keys set<text>,"
+ " sub_doc_equals set<text>,"
+ " sub_doc_equals map<text, text>,"
+ " array_size map<text, int>,"
+ " array_equals map<text, text>,"
+ " array_contains set<text>,"
Expand Down Expand Up @@ -72,7 +72,7 @@ protected List<QueryOuterClass.Query> getIndexStatements(String keyspace, String
.build());

String subDocEquals =
"CREATE CUSTOM INDEX IF NOT EXISTS %s_sub_doc_equals ON %s.%s (sub_doc_equals) USING 'StorageAttachedIndex'";
"CREATE CUSTOM INDEX IF NOT EXISTS %s_sub_doc_equals ON %s.%s (entries(sub_doc_equals)) USING 'StorageAttachedIndex'";
statements.add(
QueryOuterClass.Query.newBuilder()
.setCql(String.format(subDocEquals, table, keyspace, table))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;

Expand Down Expand Up @@ -264,13 +265,23 @@ public SizeFilter(String path, Integer size) {
}
}

/** Filter for document where array has specified number of elements */
/** Filter for document where array matches (data in same order) as the array in request */
public static class ArrayEqualsFilter extends MapFilterBase<String> {
public ArrayEqualsFilter(DocValueHasher hasher, String path, List<Object> arrayData) {
super("array_equals", path, Operator.EQ, getHash(hasher, arrayData));
}
}

/**
* Filter for document where field is subdocument and matches (same subfield in same order) the
* filter sub document
*/
public static class SubDocEqualsFilter extends MapFilterBase<String> {
public SubDocEqualsFilter(DocValueHasher hasher, String path, Map<String, Object> subDocData) {
super("sub_doc_equals", path, Operator.EQ, getHash(hasher, subDocData));
}
}

private static QueryOuterClass.Value getValue(Object value) {
if (value instanceof String) {
return Values.of((String) value);
Expand All @@ -289,20 +300,7 @@ private static String getHashValue(DocValueHasher hasher, String path, Object ar
}

private static String getHash(DocValueHasher hasher, Object arrayValue) {
if (arrayValue == null) {
return hasher.nullValue().hash().hash();
} else if (arrayValue instanceof String) {
return hasher.stringValue((String) arrayValue).hash().hash();
} else if (arrayValue instanceof BigDecimal) {
return hasher.numberValue((BigDecimal) arrayValue).hash().hash();
} else if (arrayValue instanceof Boolean) {
return hasher.booleanValue((Boolean) arrayValue).hash().hash();
} else if (arrayValue instanceof List) {
return hasher.arrayHash((List<Object>) arrayValue).hash();
}
throw new JsonApiException(
ErrorCode.UNSUPPORTED_FILTER_DATA_TYPE,
String.format("Unsupported filter data type %s", arrayValue.getClass()));
return hasher.getHash(arrayValue).hash();
}

private static QueryOuterClass.Value getDocumentIdValue(DocumentId value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;

/**
Expand All @@ -42,6 +43,7 @@ public abstract class FilterableResolver<T extends Command & Filterable> {
private static final Object ALL_GROUP = new Object();
private static final Object SIZE_GROUP = new Object();
private static final Object ARRAY_EQUALS = new Object();
private static final Object SUB_DOC_EQUALS = new Object();

private final boolean findOne;
private final boolean readDocument;
Expand Down Expand Up @@ -86,7 +88,9 @@ public FilterableResolver(ObjectMapper objectMapper, boolean findOne, boolean re
.capture(SIZE_GROUP)
.compareValues("*", EnumSet.of(ArrayComparisonOperator.SIZE), JsonType.NUMBER)
.capture(ARRAY_EQUALS)
.compareValues("*", EnumSet.of(ValueComparisonOperator.EQ), JsonType.ARRAY);
.compareValues("*", EnumSet.of(ValueComparisonOperator.EQ), JsonType.ARRAY)
.capture(SUB_DOC_EQUALS)
.compareValues("*", EnumSet.of(ValueComparisonOperator.EQ), JsonType.SUB_DOC);
}

protected ReadOperation resolve(CommandContext commandContext, T command) {
Expand Down Expand Up @@ -234,6 +238,16 @@ private ReadOperation findDynamic(CommandContext commandContext, CaptureGroups<T
new DocValueHasher(), expression.path(), expression.value())));
}

final CaptureGroup<Map<String, Object>> subDocEqualsGroups =
(CaptureGroup<Map<String, Object>>) captures.getGroupIfPresent(SUB_DOC_EQUALS);
if (subDocEqualsGroups != null) {
subDocEqualsGroups.consumeAllCaptures(
expression ->
filters.add(
new FindOperation.SubDocEqualsFilter(
new DocValueHasher(), expression.path(), expression.value())));
}

FilteringOptions filteringOptions = getFilteringOption(captures.command());
return new FindOperation(
commandContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ DocValueHash calcArrayHash(ArrayNode n) {
return DocValueHash.constructBoundedHash(DocValueType.ARRAY, sb.toString());
}

public DocValueHash arrayHash(List<Object> arrayData) {
private DocValueHash arrayHash(List<Object> arrayData) {
// Array hash consists of header line (type prefix + element count)
// followed by one line per element, containing element hash.
// Lines are separated by linefeeds; no trailing linefeed
Expand All @@ -117,18 +117,7 @@ public DocValueHash arrayHash(List<Object> arrayData) {
sb.append(DocValueType.ARRAY.prefix()).append(arrayData.size());

for (Object arrayValue : arrayData) {
DocValueHash childHash = null;
if (arrayValue == null) {
childHash = nullValue().hash();
} else if (arrayValue instanceof String) {
childHash = stringValue((String) arrayValue).hash();
} else if (arrayValue instanceof BigDecimal) {
childHash = numberValue((BigDecimal) arrayValue).hash();
} else if (arrayValue instanceof Boolean) {
childHash = booleanValue((Boolean) arrayValue).hash();
} else if (arrayValue instanceof List) {
childHash = arrayHash((List<Object>) arrayValue);
}
DocValueHash childHash = getHash(arrayValue);
sb.append(LINE_SEPARATOR).append(childHash.hash());
}
return DocValueHash.constructBoundedHash(DocValueType.ARRAY, sb.toString());
Expand Down Expand Up @@ -156,4 +145,44 @@ private DocValueHash calcObjectHash(ObjectNode n) {

return DocValueHash.constructBoundedHash(DocValueType.OBJECT, sb.toString());
}

private DocValueHash objectHash(Map<String, Object> n) {
// Array hash consists of header line (type prefix + element count)
// followed by two line per element, containing name (NOT path!) on first line
// and element hash on second.
// Lines are separated by linefeeds; no trailing linefeed
StringBuilder sb = new StringBuilder(10 + 24 * n.size());

// Header line: type prefix ('O') and element length, so f.ex "O7"
sb.append(DocValueType.OBJECT.prefix()).append(n.size());

Iterator<Map.Entry<String, Object>> it = n.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Object> entry = it.next();
sb.append(LINE_SEPARATOR).append(entry.getKey());
DocValueHash childHash = getHash(entry.getValue());
sb.append(LINE_SEPARATOR).append(childHash.hash());
}

return DocValueHash.constructBoundedHash(DocValueType.OBJECT, sb.toString());
}

public DocValueHash getHash(Object value) {
if (value == null) {
return nullValue().hash();
} else if (value instanceof String) {
return stringValue((String) value).hash();
} else if (value instanceof BigDecimal) {
return numberValue((BigDecimal) value).hash();
} else if (value instanceof Boolean) {
return booleanValue((Boolean) value).hash();
} else if (value instanceof List) {
return arrayHash((List<Object>) value);
} else if (value instanceof Map) {
return objectHash((Map<String, Object>) value);
}
throw new JsonApiException(
ErrorCode.UNSUPPORTED_FILTER_DATA_TYPE,
String.format("Unsupported filter data type %s", value.getClass()));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.stargate.sgv2.jsonapi.api.model.command.deserializers;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.junit.QuarkusTest;
Expand All @@ -15,9 +14,10 @@
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.JsonType;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ValueComparisonOperation;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ValueComparisonOperator;
import io.stargate.sgv2.jsonapi.exception.JsonApiException;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -182,13 +182,21 @@ public void mustHandleSize() throws Exception {
}

@Test
public void unsupportedFilterTypes() {
String json = """
{"boolType": {"a" : "b"}}
""";

Throwable throwable = catchThrowable(() -> objectMapper.readValue(json, FilterClause.class));
assertThat(throwable).isInstanceOf(JsonApiException.class);
public void mustHandleSubDocEq() throws Exception {
String json =
"""
{"sub_doc" : {"col": 2}}
""";
Map<String, Object> value = new LinkedHashMap<>();
value.put("col", new BigDecimal(2));
final ComparisonExpression expectedResult =
new ComparisonExpression(
"sub_doc",
List.of(
new ValueComparisonOperation(
ValueComparisonOperator.EQ, new JsonLiteral(value, JsonType.SUB_DOC))));
FilterClause filterClause = objectMapper.readValue(json, FilterClause.class);
assertThat(filterClause.comparisonExpressions()).hasSize(1).contains(expectedResult);
}
}
}
Loading

0 comments on commit 75514ec

Please sign in to comment.