From 8fa31ba3eeb9aaa9799e8172c8d1fb689ad0b5f3 Mon Sep 17 00:00:00 2001 From: Andrey G Date: Thu, 26 Jan 2023 15:55:33 +0200 Subject: [PATCH] FMWK-126 Release 2.0.0 preparation (#91) Co-authored-by: yrizhkov --- README.md | 104 ++++++--- pom.xml | 10 +- .../documentapi/DocumentApiException.java | 4 +- .../documentapi/IAerospikeDocumentClient.java | 9 +- .../batch/AbstractBatchOperation.java | 11 +- .../documentapi/jsonpath/JsonPathObject.java | 21 +- .../documentapi/jsonpath/JsonPathParser.java | 32 ++- .../documentapi/token/TokenType.java | 2 +- .../documentapi/token/WildcardToken.java | 17 +- .../aerospike/documentapi/BaseTestConfig.java | 1 - .../documentapi/DocumentAPIBatchTests.java | 199 ++++++++++++++++-- .../jsonpath/JsonPathFunctionsTests.java | 10 +- 12 files changed, 308 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 4eea72a..3a53c22 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This project provides an API for accessing and mutating Aerospike [Collection Data Type](https://www.aerospike.com/docs/client/java/index.html) (CDT) -objects using a [JSONPath](https://goessner.net/articles/JsonPath/) syntax. +objects using [JSONPath](https://goessner.net/articles/JsonPath/) syntax. This effectively provides a document API, with CDT objects used to represent JSON documents in the Aerospike database. @@ -15,8 +15,8 @@ The documentation for this project can be found on [javadoc.io](https://www.java ### Assumptions - * Familiarity with the Aerospike client for Java (see [Introduction - Java Client](https://www.aerospike.com/docs/client/java/index.html)) - * Some knowledge of Aerospike CDTs (see reference above) +- Familiarity with the Aerospike client for Java (see [Introduction - Java Client](https://www.aerospike.com/docs/client/java/index.html)) +- Some knowledge of the Aerospike CDTs (see reference above) ## Getting Started Blog Posts @@ -36,7 +36,7 @@ Add the Maven dependency: com.aerospike aerospike-document-api - 1.2.0 + 2.0.0 ``` @@ -88,8 +88,8 @@ The Aerospike Document Client is instantiated as follows * You can create a new AerospikeClient using other constructors - in this example we are using IP and Port only. ``` java - AerospikeClient client = new AerospikeClient(AEROSPIKE_SERVER_IP, AEROSPIKE_SERVER_PORT); - AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); +AerospikeClient client = new AerospikeClient(AEROSPIKE_SERVER_IP, AEROSPIKE_SERVER_PORT); +AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); ``` ### Create @@ -97,11 +97,11 @@ The Aerospike Document Client is instantiated as follows We add the example JSON document to our Aerospike database as follows ``` java - JsonNode jsonNode = JsonConverters.convertStringToJsonNode(jsonString); - // For details of Aerospike namespace/set/key see https://www.aerospike.com/docs/architecture/data-model.html - Key tommyLeeJonesDBKey = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "tommy-lee-jones.json"); - String documentBinName = "documentBin"; - documentClient.put(tommyLeeJonesDBKey, documentBinName, jsonNode); +JsonNode jsonNode = JsonConverters.convertStringToJsonNode(jsonString); +// For details of Aerospike namespace/set/key see https://www.aerospike.com/docs/architecture/data-model.html +Key tommyLeeJonesDBKey = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, "tommy-lee-jones.json"); +String documentBinName = "documentBin"; +documentClient.put(tommyLeeJonesDBKey, documentBinName, jsonNode); ``` ### Insert @@ -109,9 +109,9 @@ We add the example JSON document to our Aerospike database as follows We can add filmography for 2019 using the JSONPath ```$.selected_filmography.2019``` ```java - List _2019Films = new Vector(); - _2019Films.add("Ad Astra"); - documentClient.put(tommyLeeJonesDBKey, documentBinName, "$.selected_filmography.2019",_2019Films); +List _2019Films = new Vector(); +_2019Films.add("Ad Astra"); +documentClient.put(tommyLeeJonesDBKey, documentBinName, "$.selected_filmography.2019",_2019Films); ``` ### Update @@ -119,7 +119,7 @@ We can add filmography for 2019 using the JSONPath ```$.selected_filmography.201 Update Jones' IMDB ranking using the JSONPath ```$.imdb_rank.rank``` ``` java - documentClient.put(tommyLeeJonesDBKey, documentBinName, "$.imdb_rank.rank",45); +documentClient.put(tommyLeeJonesDBKey, documentBinName, "$.imdb_rank.rank",45); ``` ### Append @@ -127,8 +127,8 @@ Update Jones' IMDB ranking using the JSONPath ```$.imdb_rank.rank``` We can append to 'Rotten Tomatoes' list of best films using the reference ```$.best_films_ranked[0].films``` ```java - documentClient.append(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films","Rolling Thunder"); - documentClient.append(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films","The Three Burials"); +documentClient.append(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films","Rolling Thunder"); +documentClient.append(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films","The Three Burials"); ``` ### Delete @@ -136,7 +136,7 @@ We can append to 'Rotten Tomatoes' list of best films using the reference ```$.b We can delete a node e.g. the Medium reviewer's rankings ```java - documentClient.delete(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[1]"); +documentClient.delete(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[1]"); ``` ### Get @@ -144,7 +144,7 @@ We can delete a node e.g. the Medium reviewer's rankings We can find out the name of Jones' best film according to 'Rotten Tomatoes' using the JSONPath ```$.best_films_ranked[0].films[0]``` ```java - documentClient.get(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films[0]"); +documentClient.get(tommyLeeJonesDBKey, documentBinName, "$.best_films_ranked[0].films[0]"); ``` ## JSONPath Queries @@ -379,20 +379,51 @@ jsonPath = "$.authentication.login[?(@.id > 10)]"; objectFromDB = documentClient.get(TEST_AEROSPIKE_KEY, bins, jsonPath); ``` +### JSONPath query operations + +Depending on how JSONPath query operations run they can be split into 2 types. + +#### 1-step JSONPath query operations + +Operations that use JSONPath containing only array and/or map elements. + +Examples: + + $.store.book, $[0], $.store.book[0], $.store.book[0][1].title. + +#### 2-step JSONPath query operations + +Operations that use JSONPath containing wildcards, recursive descent, filters, functions, scripts. + +Examples: + + $.store.book[*].author, $.store..price, $.store.book[?(@.price < 10)], $..book[(@.length-1)]. + ## Batch operations Starting at version `2.0.0` there is support for batch operations. -You can now send operations (GET, PUT, APPEND, DELETE) in batches using json path or JSONPath query -for single and multiple bins. +You can now send CRUD operations (PUT, GET, APPEND, DELETE) in batches using JSONPath +for single and multiple bins. +Each operation in a batch is performed on a single Aerospike key. + +Limitations: -Keys related limitations: +| | Unique key
within batch | Non-unique key
within batch | Multiple batch operations
having the same key and the same bin(s) | +|-------------------------------------------------------|-----------------------------|------------------------------------------------------------|-----------------------------------------------------------------------| +| [1-step operation](#1-step-jsonpath-query-operations) | Supported | Order of operations with non-unique keys is not guaranteed | Only 1-step GET operations, order not guaranteed | +| [2-step operation](#2-step-jsonpath-query-operations) | Supported | Not supported | Not supported | -- Operations order in a batch is preserved only for the operations with different keys. -- JSONPath queries operations are allowed in a batch only if they don't have repeating keys. +Results are returned as a List of BatchRecord objects, each of them contains the following: -A use-case example can be sending a batch of operations at once to update bins storing events, -or append values for single bins storing analytics, when many steps of the same kind need to be executed in sequence. +- Aerospike key. +- Result code (0 in case of operation finished successfully or another predefined number + referring to a particular exception / error). +- Record (contains requested values mapped to their respective bin names, + relevant in case of the GET operation). + +A use-case example can be sending a batch of operations at once to update bins storing events, +or append values for single bins storing analytics, when many steps of the same kind need to be performed. ### Using batch operations @@ -441,30 +472,33 @@ BatchOperation operation5 = new GetBatchOperation( BatchOperation operation6 = new PutBatchOperation( key6, Collections.singletonList(documentBinName2), - "$.authentication..device", - "Mobile" + "$.best_filmes_ranked[*].films[0]", + "Men In Black 2" ); -// Get from multiple similarly structured bins +// Assuming we have multiple similarly structured bins to read from String binName1 = "events1Bin"; String binName2 = "events2Bin"; +String binName3 = "events3Bin"; List bins = new ArrayList<>(); bins.add(binName1); bins.add(binName2); +bins.add(binName3); BatchOperation operation7 = new GetBatchOperation( key7, bins, - "$.authentication.logout.name" + "$.imdb_rank.source" ); -// Collecting operations +// Collecting operations and running List batchOpsList = new ArrayList<>(); -batchOpsList.add(operation1, operation2, operation3, operation4, operation5, operation6, operation7); -documentClient.batchPerform(batchOpsList, true); +batchOpsList.add(operation1, operation2, operation3, operation4, + operation5, operation6, operation7); +List results = documentClient.batchPerform(batchOpsList, true); +// Checking that all operations finished successfully +assertEquals(0, results.stream().filter(res -> res.resultCode != 0).count()); ``` - ## References * See [AerospikeDocumentClient.java](https://github.com/aerospike/aerospike-document-lib/blob/main/src/main/java/com/aerospike/documentapi/AerospikeDocumentClient.java) for full details of the API. - diff --git a/pom.xml b/pom.xml index 49bea15..8aa7e41 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.aerospike aerospike-document-api - 1.2.0 + 2.0.0 Aerospike Document API This project provides an API which allows Aerospike CDT (Collection Data Type) objects to be accessed @@ -29,11 +29,11 @@ 3.3.0 1.6 3.0.0-M7 - 2.13.3 - 6.1.4 + 2.14.1 + 6.1.6 2.7.0 - 1.18.22 - 5.9.0 + 1.18.24 + 5.9.2 4.9.0 diff --git a/src/main/java/com/aerospike/documentapi/DocumentApiException.java b/src/main/java/com/aerospike/documentapi/DocumentApiException.java index 15231eb..6dec4ab 100644 --- a/src/main/java/com/aerospike/documentapi/DocumentApiException.java +++ b/src/main/java/com/aerospike/documentapi/DocumentApiException.java @@ -51,7 +51,7 @@ public JsonPrefixException(String jsonString) { */ public static class JsonPathException extends DocumentApiException { public JsonPathException(String jsonString) { - super(String.format("'%s' does not match JsonPath format", jsonString)); + super(String.format("'%s' does not match JSONPath format", jsonString)); } } @@ -69,7 +69,7 @@ public JsonAppendException(String jsonString) { */ public static class JsonPathParseException extends DocumentApiException { public JsonPathParseException(String jsonPathPart) { - super(String.format("Unable to parse '%s' as jsonPath token", jsonPathPart)); + super(String.format("Unable to parse '%s' as JSONPath token", jsonPathPart)); } } } diff --git a/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java b/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java index 6d43539..e4e4231 100644 --- a/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java +++ b/src/main/java/com/aerospike/documentapi/IAerospikeDocumentClient.java @@ -108,9 +108,12 @@ public interface IAerospikeDocumentClient { /** * Perform batch operations. - *

Operations order is preserved only for the operations with different keys.

- *

The order and consistency of one-step (JSON path) operations with the same keys is not guaranteed.

- *

Two-step (JSONPath query) operations with the same keys are not allowed in a batch.

+ * + *

Operations order is preserved only for those 1-step operations + * (with JSONPath that contains only array and/or map elements) + * that have unique Aerospike keys within a batch.

+ *

Every 2-step operation (with JSONPath containing wildcards, recursive descent, filters, functions, scripts) + * should have unique Aerospike key within a batch. * * @param batchOperations a list of batch operations to apply. * @param parallel whether batch processing stream operations should run in parallel. diff --git a/src/main/java/com/aerospike/documentapi/batch/AbstractBatchOperation.java b/src/main/java/com/aerospike/documentapi/batch/AbstractBatchOperation.java index 99523aa..c150aaa 100644 --- a/src/main/java/com/aerospike/documentapi/batch/AbstractBatchOperation.java +++ b/src/main/java/com/aerospike/documentapi/batch/AbstractBatchOperation.java @@ -10,8 +10,6 @@ import com.aerospike.documentapi.jsonpath.PathDetails; import com.aerospike.documentapi.util.Lut; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Value; import java.util.Collection; import java.util.Collections; @@ -91,7 +89,7 @@ protected Map firstStepQueryResults() { } protected Object firstStepJsonPathQuery(Map.Entry entry) { - throw new UnsupportedOperationException("Not implemented"); + throw new UnsupportedOperationException("Raw use of a method that should be called from extending classes"); } protected Operation toPutOperation(String binName, Object objToPut, PathDetails pathDetails) { @@ -125,11 +123,4 @@ record = new Record(bins, 0, 0); return new BatchRecord(key, record, -2, false, true); } - - @Value - @RequiredArgsConstructor - protected static class Bin { - String name; - Object value; - } } diff --git a/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathObject.java b/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathObject.java index fe1ce79..157485a 100644 --- a/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathObject.java +++ b/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathObject.java @@ -2,7 +2,6 @@ import com.aerospike.documentapi.token.ContextAwareToken; import com.aerospike.documentapi.token.Token; -import com.aerospike.documentapi.token.TokenType; import java.util.ArrayList; import java.util.List; @@ -79,8 +78,22 @@ public void setJsonPathSecondStepQuery(String jsonPathSecondStepQuery) { public void appendToJsonPathQuery(Token queryToken) { String tokenString = queryToken.getQueryConcatString(); - jsonPathSecondStepQuery += jsonPathSecondStepQuery.isEmpty() - ? queryToken.getType() == TokenType.FUNCTION ? DOT + tokenString : tokenString - : DOT + tokenString; + switch (queryToken.getType()) { + case LIST: + case LIST_WILDCARD: + case SCAN: + case FILTER: + jsonPathSecondStepQuery += tokenString; + break; + case ROOT: + throw new UnsupportedOperationException( + "Unsupported operation: root token cannot be added to JSONPath query"); + case MAP: + case FUNCTION: + case WILDCARD: + default: + jsonPathSecondStepQuery += DOT + tokenString; + break; + } } } diff --git a/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathParser.java b/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathParser.java index 712fc80..8eac854 100644 --- a/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathParser.java +++ b/src/main/java/com/aerospike/documentapi/jsonpath/JsonPathParser.java @@ -17,14 +17,7 @@ */ public class JsonPathParser { - static final String DOCUMENT_ROOT_TOKEN = "$"; public static final String DEEP_SCAN = ".."; - - // Paths should match this pattern i.e. key[index1][index2]... - static final Pattern PATH_PATTERN = Pattern.compile("^([^\\[^\\]]*)(\\[(\\d+)\\])*$"); - // This pattern to extract index1,index2 ... - static final Pattern INDEX_PATTERN = Pattern.compile("(\\[(\\d+)\\])"); - public static final List functionIndication = Arrays.asList( "min()", "max()", @@ -43,8 +36,8 @@ public class JsonPathParser { public static final char OPEN_BRACKET = '['; public static final char CLOSE_BRACKET = ']'; public static final char WILDCARD = '*'; - - // Store representation of json path tokens + static final String DOCUMENT_ROOT_TOKEN = "$"; + // For storing representation of json path tokens private final JsonPathObject jsonPathObject; public JsonPathParser() { @@ -90,9 +83,10 @@ public JsonPathObject parse(String jsonString) { pathSplit = Collections.unmodifiableList(combineFilterParts(pathSplit)); List prev = null; - for (String pathPart : pathSplit) { + for (int i = 0; i < pathSplit.size(); i++) { + String pathPart = pathSplit.get(i); List curr = parseToken(pathPart); - if (skipIteration(curr, prev)) { + if (skipIteration(curr, prev, pathSplit.size() == i + 1)) { prev = curr; continue; } @@ -105,7 +99,8 @@ public JsonPathObject parse(String jsonString) { private List combineFilterParts(List pathList) { List newList = new ArrayList<>(); String prev = null; - String filter = "", preFilter = ""; + String filter = ""; + String preFilter = ""; boolean filterProcStarted = false; for (int i = 0; i < pathList.size(); i++) { String curr = pathList.get(i); @@ -140,13 +135,13 @@ private List combineFilterParts(List pathList) { return newList; } - private boolean skipIteration(List curr, List prev) { - boolean res = false; + private boolean skipIteration(List curr, List prev, boolean isLast) { + boolean res; // if path ends with a map wildcard after a map or a list element like $.example.*, $.example[10].* or $.* res = (curr != null && prev != null && curr.size() == 1 && prev.size() == 1 - && curr.get(0).getType() == TokenType.WILDCARD + && curr.get(0).getType() == TokenType.WILDCARD && isLast && curr.get(0).getString().charAt(0) == WILDCARD // "*", not "[*]" && (prev.get(0).getType() == TokenType.MAP || prev.get(0).getType() == TokenType.LIST || prev.get(0).getType() == TokenType.ROOT)) @@ -159,7 +154,8 @@ private boolean skipIteration(List curr, List prev) { private void validatePathSplit(String jsonPath, List jsonPathSplit) { Iterator iter = jsonPathSplit.listIterator(); - String prev = null, next = null; + String prev = null; + String next; while (iter.hasNext()) { next = iter.next(); if (next.equals("")) { @@ -207,7 +203,7 @@ private List processDefaultOrFail(String token) { tokenOpt = FilterToken.match(token); if (tokenOpt.isPresent()) return Collections.singletonList(tokenOpt.get()); - throw new DocumentApiException.JsonPathException(token); + throw new DocumentApiException.JsonPathParseException(token); } @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -227,7 +223,7 @@ private List processPartWithBracketsOrFail(String token) { if (tokenOpt.isPresent()) return Collections.singletonList(tokenOpt.get()); List tokens = ListToken.parseToList(token); - if (tokens.size() > 0) return tokens; + if (!tokens.isEmpty()) return tokens; throw new DocumentApiException.JsonPathException(token); } diff --git a/src/main/java/com/aerospike/documentapi/token/TokenType.java b/src/main/java/com/aerospike/documentapi/token/TokenType.java index 057cb73..f21bb2e 100644 --- a/src/main/java/com/aerospike/documentapi/token/TokenType.java +++ b/src/main/java/com/aerospike/documentapi/token/TokenType.java @@ -1,5 +1,5 @@ package com.aerospike.documentapi.token; public enum TokenType { - ROOT, LIST, MAP, SCAN, WILDCARD, FILTER, FUNCTION + ROOT, LIST, MAP, SCAN, WILDCARD, LIST_WILDCARD, FILTER, FUNCTION } diff --git a/src/main/java/com/aerospike/documentapi/token/WildcardToken.java b/src/main/java/com/aerospike/documentapi/token/WildcardToken.java index 07d90d6..eb7195c 100644 --- a/src/main/java/com/aerospike/documentapi/token/WildcardToken.java +++ b/src/main/java/com/aerospike/documentapi/token/WildcardToken.java @@ -5,10 +5,13 @@ import static com.aerospike.documentapi.jsonpath.JsonPathParser.CLOSE_BRACKET; import static com.aerospike.documentapi.jsonpath.JsonPathParser.OPEN_BRACKET; import static com.aerospike.documentapi.jsonpath.JsonPathParser.WILDCARD; +import static com.aerospike.documentapi.token.TokenType.LIST_WILDCARD; public class WildcardToken extends Token { - private final String LIST_WILDCARD = OPEN_BRACKET + String.valueOf(WILDCARD) + CLOSE_BRACKET; + private static final String WILDCARD_LIST_ELEM = OPEN_BRACKET + String.valueOf(WILDCARD) + CLOSE_BRACKET; + + private boolean isInList; public WildcardToken(String strPart) { if (!String.valueOf(WILDCARD).equals(strPart)) @@ -17,10 +20,14 @@ public WildcardToken(String strPart) { } public WildcardToken(String strPart, boolean inList) { - if (!String.valueOf(WILDCARD).equals(strPart) && !LIST_WILDCARD.equals(strPart)) + if (!String.valueOf(WILDCARD).equals(strPart) && !WILDCARD_LIST_ELEM.equals(strPart)) throw new IllegalArgumentException(); - if (inList) setString(LIST_WILDCARD); - else setString(strPart); + if (inList) { + setString(WILDCARD_LIST_ELEM); + isInList = true; + } else { + setString(strPart); + } } public static Optional match(String strPart) { @@ -35,7 +42,7 @@ public static Optional match(String strPart) { @Override public TokenType getType() { - return TokenType.WILDCARD; + return isInList ? LIST_WILDCARD : TokenType.WILDCARD; } @Override diff --git a/src/test/java/com/aerospike/documentapi/BaseTestConfig.java b/src/test/java/com/aerospike/documentapi/BaseTestConfig.java index e7b7b48..06894e2 100644 --- a/src/test/java/com/aerospike/documentapi/BaseTestConfig.java +++ b/src/test/java/com/aerospike/documentapi/BaseTestConfig.java @@ -19,7 +19,6 @@ public class BaseTestConfig { public static final String AEROSPIKE_SET = "documentAPI"; public static final String JSON_EXAMPLE_KEY = "jsonExampleKey"; - public static final String JSON_EXAMPLE_BIN = "jsonExampleBin"; public static final String DOCUMENT_BIN_NAME = "documentBin"; public static final Key TEST_AEROSPIKE_KEY = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, JSON_EXAMPLE_KEY); diff --git a/src/test/java/com/aerospike/documentapi/DocumentAPIBatchTests.java b/src/test/java/com/aerospike/documentapi/DocumentAPIBatchTests.java index b0caa10..52fefb0 100644 --- a/src/test/java/com/aerospike/documentapi/DocumentAPIBatchTests.java +++ b/src/test/java/com/aerospike/documentapi/DocumentAPIBatchTests.java @@ -2,6 +2,7 @@ import com.aerospike.client.BatchRecord; import com.aerospike.client.Key; +import com.aerospike.client.ResultCode; import com.aerospike.documentapi.batch.AppendBatchOperation; import com.aerospike.documentapi.batch.BatchOperation; import com.aerospike.documentapi.batch.DeleteBatchOperation; @@ -23,6 +24,10 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static com.aerospike.client.ResultCode.BIN_TYPE_ERROR; +import static com.aerospike.client.ResultCode.OP_NOT_APPLICABLE; +import static com.aerospike.client.ResultCode.PARAMETER_ERROR; +import static com.aerospike.client.ResultCode.PARSE_ERROR; import static com.aerospike.documentapi.DocumentAPIBatchTests.BatchOperationEnum.APPEND; import static com.aerospike.documentapi.DocumentAPIBatchTests.BatchOperationEnum.DELETE; import static com.aerospike.documentapi.DocumentAPIBatchTests.BatchOperationEnum.GET; @@ -73,6 +78,7 @@ void testPositiveBatchGet() { }); List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); int i = 0; for (Map.Entry entry : jsonPathsMap.entrySet()) { @@ -125,15 +131,65 @@ void testNegativeBatchGet() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertTrue(results.stream().anyMatch(res -> res.resultCode != ResultCode.OK)); // making sure all records contain the resulting record == null and the necessary resulting code - // PARAMETER_ERROR = 4, BIN_TYPE_ERROR = 12, OP_NOT_APPLICABLE = 26 - Integer[] errorCodes = {4, 12, 26}; + Integer[] errorCodes = {PARAMETER_ERROR, BIN_TYPE_ERROR, OP_NOT_APPLICABLE}; batchOpsList.forEach(batchOp -> assertTrue(batchOp.getBatchRecord().record == null && (Arrays.asList(errorCodes).contains(batchOp.getBatchRecord().resultCode)))); } + /** + * Check the correct document content retrieval in a batch of single step operations with the same keys. + *

    + *
  • The whole document.
  • + *
  • First level element.
  • + *
  • An array element.
  • + *
  • A map element.
  • + *
+ */ + @Test + void testPositiveBatchGetSameKeys() { + // Load the test document + JsonNode jsonNode = JsonConverters.convertStringToJsonNode(testMaterialJson); + Map jsonNodeAsMap = JsonConverters.convertJsonNodeToMap(jsonNode); + + Map jsonPathsMap = new LinkedHashMap<>(); + jsonPathsMap.put("$", jsonNodeAsMap); + jsonPathsMap.put("$.example1", jsonNodeAsMap.get("example1")); + jsonPathsMap.put("$.example3[1]", ((List) jsonNodeAsMap.get("example3")).get(1)); + jsonPathsMap.put("$.example4.key10", ((Map) jsonNodeAsMap.get("example4")).get("key10")); + Iterator iterator = jsonPathsMap.keySet().iterator(); + + List batchOpsList = new ArrayList<>(); + + // the same key and the same document bin, different jsonPath strings + IntStream.range(0, jsonPathsMap.size()).forEachOrdered(i -> { + Key key = new Key(AEROSPIKE_NAMESPACE, AEROSPIKE_SET, JSON_EXAMPLE_KEY + "1111"); + String binName = DOCUMENT_BIN_NAME + "22"; + documentClient.put(key, binName, jsonNode); + + BatchOperation batchOp = new GetBatchOperation( + key, + Collections.singletonList(binName), + iterator.next() + ); + batchOpsList.add(batchOp); + }); + + // read operations with the same keys referring to the same bins can run in a batch + List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); + + int i = 0; + for (Map.Entry entry : jsonPathsMap.entrySet()) { + String binName = batchOpsList.get(i).getBinNames().iterator().next(); + assertTrue(TestJsonConverters.jsonEquals(batchRecords.get(i).record.getValue(binName), entry.getValue())); + i++; + } + } + /** * Check a batch of single step PUT operations. *
    @@ -163,7 +219,8 @@ void testPositiveBatchPut() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, results.stream().filter(res -> res.resultCode != ResultCode.OK).count()); // Check the value put previously for (BatchOperation batchOp : batchOpsList) { @@ -195,7 +252,7 @@ void testPositiveBatchPutWildcard() { // putting to a new position inputsList.add(new BatchOperationInput("$.example2[*].key01[4]", PUT)); // putting to a new position in a more complex structure - inputsList.add(new BatchOperationInput("$.example2[*].key07[*].key01[4]", PUT)); + inputsList.add(new BatchOperationInput("$.example2[*].key07[*].key10[4]", PUT)); // putting to a new key inputsList.add(new BatchOperationInput("$.example3[*].key76", PUT)); @@ -210,7 +267,8 @@ void testPositiveBatchPutWildcard() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, results.stream().filter(res -> res.resultCode != ResultCode.OK).count()); // Check the value put previously for (BatchOperation batchOp : batchOpsList) { @@ -263,7 +321,8 @@ void testNegativeBatchPut() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertTrue(results.stream().anyMatch(res -> res.resultCode != ResultCode.OK)); // making sure all records contain the resulting record == null and the necessary resulting code // OP_NOT_APPLICABLE = 26, PARAMETER_ERROR = 4 @@ -300,7 +359,8 @@ void testPositiveBatchAppend() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, results.stream().filter(res -> res.resultCode != ResultCode.OK).count()); for (BatchOperation batchOp : batchOpsList) { List appendedList = (List) documentClient.get(batchOp.getKey(), @@ -346,11 +406,11 @@ void testNegativeBatchAppend() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertTrue(results.stream().anyMatch(res -> res.resultCode != ResultCode.OK)); // making sure all records contain the resulting record == null and the necessary resulting code - // PARAMETER_ERROR = 4, BIN_TYPE_ERROR = 12, OP_NOT_APPLICABLE = 26 - Integer[] errorCodes = {4, 12, 26}; + Integer[] errorCodes = {PARAMETER_ERROR, BIN_TYPE_ERROR, OP_NOT_APPLICABLE}; batchOpsList.forEach(batchOp -> assertTrue(batchOp.getBatchRecord().record == null && (Arrays.asList(errorCodes).contains(batchOp.getBatchRecord().resultCode)))); } @@ -407,7 +467,8 @@ void testPositiveBatchDelete() { || originalObject instanceof List); } - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, results.stream().filter(res -> res.resultCode != ResultCode.OK).count()); // checking the deleted objects for (BatchOperation batchOp : batchOpsList) { @@ -460,9 +521,10 @@ void testNegativeBatchDelete() { false ); - documentClient.batchPerform(batchOpsList, true); + List results = documentClient.batchPerform(batchOpsList, true); + assertTrue(results.stream().anyMatch(res -> res.resultCode != ResultCode.OK)); - Integer[] errorCodes = {4, 12, 26}; + Integer[] errorCodes = {PARAMETER_ERROR, BIN_TYPE_ERROR, OP_NOT_APPLICABLE}; for (BatchOperation batchOp : batchOpsList) { // in case of deleting a non-existing key of the existing map // the response has a non-null record with resultCode == 0 @@ -473,7 +535,6 @@ void testNegativeBatchDelete() { assertNull(batchOp.getBatchRecord().record.bins.get(batchOp.getBinNames().iterator().next())); } else { // making sure all records contain the resulting record == null and the necessary resulting code - // PARAMETER_ERROR = 4, BIN_TYPE_ERROR = 12, OP_NOT_APPLICABLE = 26 assertNull(batchOp.getBatchRecord().record); assertTrue(Arrays.asList(errorCodes).contains(batchOp.getBatchRecord().resultCode)); } @@ -491,7 +552,8 @@ void testPositiveBatchDeleteRootElement() { Collections.singletonList(DOCUMENT_BIN_NAME), jsonPath ); - documentClient.batchPerform(Collections.singletonList(batchOp), true); + List results = documentClient.batchPerform(Collections.singletonList(batchOp), true); + assertEquals(0, results.stream().filter(res -> res.resultCode != ResultCode.OK).count()); Object objectFromDB = documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertTrue(((Map) objectFromDB).isEmpty()); @@ -534,6 +596,89 @@ void testPositiveBatchMix() { ); List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); + + final Object[] originalObject = new Object[1]; + + Map jsonNodeAsMap = JsonConverters.convertJsonNodeToMap(jsonNode); + int i = 0; + for (BatchRecord batchRecord : batchRecords) { + BatchOperation batchOp = batchOpsList.get(i); + String binName = batchOp.getBinNames().iterator().next(); + + switch (i) { + case 0: + assertTrue(TestJsonConverters.jsonEquals(batchRecord.record.getValue(binName), jsonNodeAsMap)); + break; + case 1: + Object objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath()); + // Check that the last element in the list we put to is the initial put value + assertNotNull(objFromDb); + assertTrue(TestJsonConverters.jsonEquals(objFromDb, objToPut)); + break; + case 2: + List appendedList = (List) documentClient.get(batchOp.getKey(), + batchOp.getBinNames().iterator().next(), batchOp.getJsonPath() + ); + // Check that the last element in the list we appended to is the value we added + assertNotNull(appendedList); + assertTrue(TestJsonConverters.jsonEquals(appendedList.get(appendedList.size() - 1), objToAppend)); + break; + case 3: + assertTrue(jsonPathDoesNotExist( + batchOp.getKey(), + binName, + batchOp.getJsonPath(), + originalObject) + ); + break; + } + i++; + } + } + + /** + * Check a batch of different types single step operations (each with a different bin) using the same keys. + *
      + *
    • Reading the whole json.
    • + *
    • Putting a new key into an existing map.
    • + *
    • Appending to an array referenced by an index.
    • + *
    • Deleting a map entry using a map reference.
    • + *
    + */ + @Test + void testPositiveBatchMixSameKeys() { + // Load the test document + JsonNode jsonNode = JsonConverters.convertStringToJsonNode(testMaterialJson); + + List inputsList = new ArrayList<>(); + // reading the whole json + inputsList.add(new BatchOperationInput("$", GET)); + // putting a new key into an existing map + inputsList.add(new BatchOperationInput("$.example1.key27", PUT)); + // appending to an array referenced by an index + inputsList.add(new BatchOperationInput("$.example1.key01", APPEND)); + // deleting a map entry using a map reference + inputsList.add(new BatchOperationInput("$.example1.key01", DELETE)); + + int objToPut = 86; + int objToAppend = 87; + + // adding similar document bins with different jsonPath strings and different operations + List batchOpsList = createBatchOperations( + jsonNode, + inputsList, + objToPut, + objToAppend, + true + ); + + // operations order is not guaranteed + // in this example each operation uses a different bin, so no overlapping / conflicts + // otherwise (referring to the same bins) write-type operations with the same keys cannot run in a batch + List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); final Object[] originalObject = new Object[1]; @@ -618,6 +763,7 @@ void testPositiveBatchMix2StepWildcard() { ); List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); Object objFromDb, modifiedJson, expectedObject; int i = 0; @@ -637,7 +783,8 @@ void testPositiveBatchMix2StepWildcard() { assertTrue(TestJsonConverters.jsonEquals(batchRecord.record.getValue(binName), expectedObject)); break; case 3: - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath() + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath() ); modifiedJson = JsonPath.parse(testMaterialJson).set(inputsList.get(i).getJsonPath(), objToPut).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); @@ -646,7 +793,8 @@ void testPositiveBatchMix2StepWildcard() { assertTrue(TestJsonConverters.jsonEquals(objFromDb, expectedObject)); break; case 4: - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath() + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath() ); modifiedJson = JsonPath.parse(testMaterialJson).add(inputsList.get(i).getJsonPath(), objToAppend).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); @@ -655,7 +803,8 @@ void testPositiveBatchMix2StepWildcard() { assertTrue(TestJsonConverters.jsonEquals(objFromDb, expectedObject)); break; case 5: - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath() + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath() ); modifiedJson = JsonPath.parse(testMaterialJson).delete(inputsList.get(i).getJsonPath()).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); @@ -708,6 +857,7 @@ void testPositiveBatchMix2StepWildcardMultipleBins() { ); List batchRecords = documentClient.batchPerform(batchOpsList, true); + assertEquals(0, batchRecords.stream().filter(res -> res.resultCode != ResultCode.OK).count()); Object objFromDb, modifiedJson, expectedObject; DocumentContext context = JsonPath.parse(testMaterialJson); @@ -723,7 +873,8 @@ void testPositiveBatchMix2StepWildcardMultipleBins() { expectedObject = JsonPath.read(testMaterialJson, inputsList.get(i).getJsonPath()); assertTrue(TestJsonConverters.jsonEquals(batchRecord.record.getValue(binName), expectedObject)); } else if (batchOp.getClass().equals(PUT.getBatchOperationClass())) { - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath()); + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath()); modifiedJson = context.set(inputsList.get(i).getJsonPath(), objToPut).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); @@ -731,14 +882,16 @@ void testPositiveBatchMix2StepWildcardMultipleBins() { assertNotNull(objFromDb); assertTrue(TestJsonConverters.jsonEquals(objFromDb, expectedObject)); } else if (batchOp.getClass().equals(APPEND.getBatchOperationClass())) { - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath()); + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath()); modifiedJson = context.add(inputsList.get(i).getJsonPath(), objToAppend).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); assertNotNull(objFromDb); assertTrue(TestJsonConverters.jsonEquals(objFromDb, expectedObject)); } else if (batchOp.getClass().equals(DELETE.getBatchOperationClass())) { - objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), batchOp.getJsonPath()); + objFromDb = documentClient.get(batchOp.getKey(), batchOp.getBinNames().iterator().next(), + batchOp.getJsonPath()); modifiedJson = context.delete(inputsList.get(i).getJsonPath()).json(); expectedObject = JsonPath.read(modifiedJson, inputsList.get(i).getJsonPath()); @@ -822,7 +975,7 @@ void testNegativeBatchWriteJsonIntKeys() { List batchRecords = documentClient.batchPerform(batchOpsList, true); // making sure batch record contains the correct result code - Integer[] errorCodes = {-2, 26}; + Integer[] errorCodes = {PARSE_ERROR, OP_NOT_APPLICABLE}; batchRecords.forEach(batchRec -> assertTrue((Arrays.asList(errorCodes).contains(batchRec.resultCode)))); } diff --git a/src/test/java/com/aerospike/documentapi/jsonpath/JsonPathFunctionsTests.java b/src/test/java/com/aerospike/documentapi/jsonpath/JsonPathFunctionsTests.java index 4e9c565..be004b9 100644 --- a/src/test/java/com/aerospike/documentapi/jsonpath/JsonPathFunctionsTests.java +++ b/src/test/java/com/aerospike/documentapi/jsonpath/JsonPathFunctionsTests.java @@ -24,7 +24,7 @@ static void setUp() { void testLength() { AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); - String jsonPath = "$.[*].x1.length()"; + String jsonPath = "$.*.x1.length()"; JSONArray result = (JSONArray) documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertArrayEquals(new Integer[]{5, 5, 5, 5}, result.toArray()); @@ -37,7 +37,7 @@ void testLength() { void testMin() { AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); - String jsonPath = "$.[*].x1.min()"; + String jsonPath = "$.*.x1.min()"; JSONArray result = (JSONArray) documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertArrayEquals(new Double[]{1.0, 1.0, 1.0, 1.0}, result.toArray()); @@ -50,7 +50,7 @@ void testMin() { void testMax() { AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); - String jsonPath = "$.[*].x1.max()"; + String jsonPath = "$.*.x1.max()"; JSONArray result = (JSONArray) documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertArrayEquals(new Double[]{5.0, 5.0, 5.0, 5.0}, result.toArray()); @@ -63,7 +63,7 @@ void testMax() { void testAvg() { AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); - String jsonPath = "$.[*].x1.avg()"; + String jsonPath = "$.*.x1.avg()"; JSONArray result = (JSONArray) documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertArrayEquals(new Double[]{3.0, 3.0, 3.0, 3.0}, result.toArray()); @@ -76,7 +76,7 @@ void testAvg() { void testStddev() { AerospikeDocumentClient documentClient = new AerospikeDocumentClient(client); - String jsonPath = "$.[*].x1.stddev()"; + String jsonPath = "$.*.x1.stddev()"; JSONArray result = (JSONArray) documentClient.get(TEST_AEROSPIKE_KEY, DOCUMENT_BIN_NAME, jsonPath); assertArrayEquals(new Double[]{1.4142135623730951, 1.4142135623730951, 1.4142135623730951, 1.4142135623730951}, result.toArray());