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

Implement $eq support for sub document #98

Merged
merged 13 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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) {
maheshrajamani marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -6,30 +6,27 @@

/**
* Immutable value class used as key for entries in Shredded document representations: constructed
* from either nested path in input documents (during "shredding"), or when "un-shredding" back to
* Document from Shredded representations.
* from nested path in input documents (during "shredding") to match "dot notation" path used for
* query filtering (and possibly projection)
*
* <p>Internally path is simply expressed as a {@link String} where segments are separated by comma
* ({@code "."}) and segments themselves are either:
*
* <ul>
* <li>Escaped Object property name (see below about escaping)
* <li>Decorated Array element index (see below for details)
* <li>Object property name (see below about escaping)
* <li>Array element index (see below for details)
* </ul>
*
* <p>Array element indexes are enclosed in brackets, so the first element's segment would be
* expressed as String {@code "[0]"}. Property names are included as is -- so property {@code
* "name"} has segment {@code name} -- with the exception that due to need to distinguish
* encoding-characters comma and opening bracket, escaping is needed. Backslash character {@code
* '\\'} is used for escaping, preceding character to escape. Backslash itself also needs escaping.
* This means that property name like {@code "a.b"} is encoded as {@code "a\\.b"}.
* <p>No escaping is used so path itself is ambiguous (segment "1" can mean either Array index #1 or
* Object property "1") without context document, but will NOT be ambiguous when applied to specific
* target document where context node's type (Array or Object) is known.
*
* <p>As a simple example consider following JSON document:
*
* <pre>
* { "name" : "Bob",
* "values" : [ 1, 2 ],
* "[extra.stuff]" : true
* "extra.stuff" : true
* }
* </pre>
*
Expand All @@ -38,9 +35,9 @@
* <ul>
* <li>{@code "name"}
* <li>{@code "values"}
* <li>{@code "values.[0]"}
* <li>{@code "values.[1]"}
* <li>{@code "\\[extra\\.stuff]"}
* <li>{@code "values.0"}
* <li>{@code "values.1"}
* <li>{@code "extra.stuff"}
* </ul>
*
* <p>Instances can be sorted; sorting order uses underlying encoded path value.
Expand Down Expand Up @@ -121,37 +118,11 @@ public Builder nestedValueBuilder() {
}

public Builder property(String propName) {
// Properties trickier since need to escape '.', '[' and `\`
final String encodedProp = encodePropertyName(propName);
childPath = (basePath == null) ? encodedProp : (basePath + '.' + encodedProp);
// Simple as we no longer apply escaping:
childPath = (basePath == null) ? propName : (basePath + '.' + propName);
return this;
}

static String encodePropertyName(String propName) {
// First: loop through to see if anything to escape; if not, can return as-is
final int len = propName.length();
int i = 0;
for (; i < len; ++i) {
char c = propName.charAt(i);
if (c == '.' || c == '[' || c == '\\') {
break;
}
}
// common case: nothing to escape
if (i == len) {
return propName;
}
StringBuilder sb = new StringBuilder(len + 3);
for (i = 0; i < len; ++i) {
char c = propName.charAt(i);
if (c == '.' || c == '[' || c == '\\') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}

public Builder index(int index) {
// Indexes are easy as no escaping needed
StringBuilder sb;
Expand All @@ -161,7 +132,7 @@ public Builder index(int index) {
} else {
sb = new StringBuilder(basePath).append('.');
}
childPath = sb.append('[').append(index).append(']').toString();
childPath = sb.append(index).toString();
return this;
}

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()));
}
}
Loading