diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java index 09da9d71c7b..93efa2a366f 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java @@ -18,14 +18,10 @@ package org.apache.phoenix.expression.util.bson; import java.math.BigDecimal; -import java.text.NumberFormat; -import java.text.ParseException; import java.util.ArrayList; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.bson.BsonArray; import org.bson.BsonDecimal128; import org.bson.BsonDocument; @@ -102,7 +98,7 @@ private enum UpdateOp { public static void updateExpression(final BsonDocument updateExpression, final BsonDocument bsonDocument) { - LOGGER.info("Update Expression: {} , current bsonDocument: {}", updateExpression, bsonDocument); + LOGGER.debug("Update Expression: {} , current bsonDocument: {}", updateExpression, bsonDocument); if (updateExpression.containsKey("$SET")) { executeSetExpression((BsonDocument) updateExpression.get("$SET"), bsonDocument); @@ -578,73 +574,26 @@ private static void updateDocumentAtLeafNode(BsonValue newVal, UpdateOp updateOp } /** - * Retrieve the value to be updated for the given current value. If the current value does not - * contain any arithmetic operators, the current value is returned without any modifications. If - * the current value contains arithmetic expressions like "a + b" or "a - b", the values of - * operands are retrieved from the given document and if the values are numeric, the given - * arithmetic operation is performed. If the current value is a bson document with an entry from - * $IF_NOT_EXISTS to a document with a key and a fallback value, we lookup if the key is already - * present in the document. If it is, we return its value. Otherwise, we return the provided - * fallback value. If the current value is a bson document with $ADD or $SUBTRACT as key, we get - * the array of operands from this document and perform the corresponding operation. Operand can - * be an $IF_NOT_EXISTS bson document. If the current value is a bson document with $LIST_APPEND - * as key, the value is a two-element array of operands; each operand resolves to a BsonArray - * (literal array, a path string referring to an existing array attribute, or an $IF_NOT_EXISTS - * document whose resolved value is an array) and the two arrays are concatenated in order with - * duplicates preserved. + * Retrieve the value to be updated for the given current value. Arithmetic and other computed SET + * values are always carried as explicit BSON documents (never inferred from the textual content + * of a string value), so any non-document value - including a {@link BsonString} that happens to + * contain " + " or " - " - is treated as a literal and returned without modification. If the + * current value is a bson document with an entry from $IF_NOT_EXISTS to a document with a key and + * a fallback value, we lookup if the key is already present in the document. If it is, we return + * its value. Otherwise, we return the provided fallback value. If the current value is a bson + * document with $ADD or $SUBTRACT as key, we get the array of operands from this document and + * perform the corresponding operation. Operand can be an $IF_NOT_EXISTS bson document. If the + * current value is a bson document with $LIST_APPEND as key, the value is a two-element array of + * operands; each operand resolves to a BsonArray (literal array, a path string referring to an + * existing array attribute, or an $IF_NOT_EXISTS document whose resolved value is an array) and + * the two arrays are concatenated in order with duplicates preserved. * @param curValue The current value. * @param bsonDocument The document with all field key-value pairs. * @return Updated values to be used by SET operation. */ private static BsonValue getNewFieldValue(final BsonValue curValue, final BsonDocument bsonDocument) { - if ( - curValue != null && curValue.isString() - && (((BsonString) curValue).getValue().contains(" + ") - || ((BsonString) curValue).getValue().contains(" - ")) - ) { - String[] tokens = ((BsonString) curValue).getValue().split("\\s+"); - boolean addNum = true; - // Pattern pattern = Pattern.compile(":?[a-zA-Z0-9]+"); - Pattern pattern = Pattern.compile("[#:$]?[^\\s\\n]+"); - Number newNum = null; - for (String token : tokens) { - if (token.equals("+")) { - addNum = true; - continue; - } else if (token.equals("-")) { - addNum = false; - continue; - } - Matcher matcher = pattern.matcher(token); - if (matcher.find()) { - String operand = matcher.group(); - Number literalNum; - BsonValue topLevelValue = bsonDocument.get(operand); - BsonValue bsonValue = topLevelValue != null - ? topLevelValue - : CommonComparisonExpressionUtils.getFieldFromDocument(operand, bsonDocument); - - if (bsonValue == null && (literalNum = stringToNumber(operand)) != null) { - Number val = literalNum; - newNum = - newNum == null ? val : (addNum ? addNum(newNum, val) : subtractNum(newNum, val)); - } else { - if (bsonValue == null) { - throw new IllegalArgumentException("Operand " + operand + " does not exist"); - } - if (!bsonValue.isNumber() && !bsonValue.isDecimal128()) { - throw new IllegalArgumentException( - "Operand " + operand + " is not provided as number type"); - } - Number val = getNumberFromBsonNumber((BsonNumber) bsonValue); - newNum = - newNum == null ? val : (addNum ? addNum(newNum, val) : subtractNum(newNum, val)); - } - } - } - return getBsonNumberFromNumber(newNum); - } else if (curValue instanceof BsonDocument) { + if (curValue instanceof BsonDocument) { BsonDocument doc = (BsonDocument) curValue; if (doc.get("$IF_NOT_EXISTS") != null) { return resolveIfNotExists(doc, bsonDocument); @@ -823,34 +772,6 @@ private static String numberToString(Number number) { throw new RuntimeException("Number type is not known for number: " + number); } - /** - * Convert the given String to Number. - * @param number The String represented numeric value. - * @return The Number object. - */ - private static Number stringToNumber(String number) { - try { - return Integer.parseInt(number); - } catch (NumberFormatException e) { - // no-op - } - try { - return Long.parseLong(number); - } catch (NumberFormatException e) { - // no-op - } - try { - return Double.parseDouble(number); - } catch (NumberFormatException e) { - // no-op - } - try { - return NumberFormat.getInstance().parse(number); - } catch (ParseException e) { - return null; - } - } - /** * Convert Number to BsonNumber. * @param number The Number object. diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java index 73b6b21fda6..855795627d1 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java @@ -258,9 +258,9 @@ public void testUpdateExpressions() throws Exception { */ updateExp = "{\n" + " \"$SET\": {\n" + " \"Id1\": \"12345\",\n" - + " \"NestedList1[0]\": \"NestedList1[0] + 12.22\",\n" + + " \"NestedList1[0]\": { \"$ADD\": [ \"NestedList1[0]\", 12.22 ] },\n" + " \"NestedList1[3]\": null,\n" + " \"NestedList1[4]\": true,\n" - + " \"attr_5[0]\": \"attr_5[0] - 10\"\n" + " }\n" + "}"; + + " \"attr_5[0]\": { \"$SUBTRACT\": [ \"attr_5[0]\", 10 ] }\n" + " }\n" + "}"; stmt = conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?,?) ON DUPLICATE KEY UPDATE COL = BSON_UPDATE_EXPRESSION(COL, '" + updateExp diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java index a0ccdb37659..1f78df07c0e 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java @@ -232,7 +232,9 @@ public void testBsonOpsWithSqlConditionsUpdateSuccess() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -764,7 +766,9 @@ public void testBsonOpsWithDocumentConditionsUpdateSuccess() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -1163,7 +1167,9 @@ public void testBsonOpsWithSqlConditionsUpdateFailure() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -1536,7 +1542,9 @@ public void testBsonOpsWithDocumentConditionsUpdateFailure() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -1951,7 +1959,9 @@ public void testBsonOpsWithSqlConditionsUpdateSuccessWithTTL() throws Exception .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java index f35cc50f5c2..f7f226e1c47 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java @@ -60,6 +60,7 @@ import org.bson.BsonBinary; import org.bson.BsonDocument; import org.bson.BsonDouble; +import org.bson.BsonInt32; import org.bson.BsonNull; import org.bson.BsonString; import org.bson.Document; @@ -641,7 +642,9 @@ public void testConditionalUpsertReturnRow() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -725,7 +728,9 @@ public void testConditionalUpsertReturnRow() throws Exception { .append("new_field1", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft_new_val")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 123"))); + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonInt32(123)))))); stmt = conn.prepareStatement( "UPSERT INTO " + tableName + " VALUES (?) ON DUPLICATE KEY UPDATE COL = CASE WHEN" @@ -827,7 +832,9 @@ public void testConditionalUpsertReturnOldRow() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -907,7 +914,9 @@ public void testConditionalUpsertReturnOldRow() throws Exception { .append("new_field1", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft_new_val")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 123"))); + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonInt32(123)))))); stmt = conn.prepareStatement( "UPSERT INTO " + tableName + " VALUES (?) ON DUPLICATE KEY UPDATE_ONLY COL = CASE WHEN" diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java index 3e6a4f19891..9d1b477d17a 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java @@ -252,7 +252,9 @@ public void testBsonOpsWithSqlConditionsUpdateSuccess() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); @@ -487,7 +489,9 @@ public void testBsonOpsWithSqlConditionsUpdateFailure() throws Exception { .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095))) .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft")) .append("track[0].shot[2][0].city.problem[2]", - new BsonString("track[0].shot[2][0].city.problem[2] + 529.435"))) + new BsonDocument().append("$ADD", new BsonArray(Arrays.asList( + new BsonString("track[0].shot[2][0].city.problem[2]"), + new BsonDouble(529.435)))))) .append("$UNSET", new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull())); diff --git a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java index db6dc05481d..eff0c8a9c9b 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java @@ -21,6 +21,7 @@ import org.apache.phoenix.expression.util.bson.UpdateExpressionUtils; import org.bson.BsonBinaryReader; import org.bson.BsonDocument; +import org.bson.BsonString; import org.bson.RawBsonDocument; import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.DecoderContext; @@ -854,7 +855,7 @@ private static RawBsonDocument getUpdatedDocument2() { + " \"InPublication\" : false,\n" + " \"ColorBytes\" : {\n" + " \"$binary\" : {\n" + " \"base64\" : \"QmxhY2s=\",\n" + " \"subType\" : \"00\"\n" + " }\n" + " },\n" + " \"ISBN\" : \"111-1111111111\",\n" - + " \"NestedList1\" : [ -473.11999999999995, \"1234abcd\", [ \"xyz0123\", {\n" + + " \"NestedList1\" : [ \"NestedList1[0] + 12.22\", \"1234abcd\", [ \"xyz0123\", {\n" + " \"InPublication\" : false,\n" + " \"BinaryTitleSet\" : {\n" + " \"$set\" : [ {\n" + " \"$binary\" : {\n" + " \"base64\" : \"Qm9vayAxMDExIFRpdGxlIEJpbmFyeQ==\",\n" @@ -888,10 +889,10 @@ private static RawBsonDocument getUpdatedDocument2() { + " \"base64\" : \"MjA0OHU1bmJsd2plaVdGR1RIKDRiZjkzMA==\",\n" + " \"subType\" : \"00\"\n" + " }\n" + " },\n" + " \"n_attr_3\" : true,\n" + " \"n_attr_4\" : null,\n" + " \"n_attr_10\" : true,\n" - + " \"n_attr_20\" : \"str_val_0\"\n" + " },\n" + " \"attr_5\" : [ 1224, \"str001\", {\n" - + " \"$binary\" : {\n" + " \"base64\" : \"AAECAwQF\",\n" - + " \"subType\" : \"00\"\n" + " }\n" + " } ],\n" - + " \"NestedList12\" : [ -485.34, \"1234abcd\", [ {\n" + + " \"n_attr_20\" : \"str_val_0\"\n" + " },\n" + + " \"attr_5\" : [ \"attr_5[0] - 10\", \"str001\", {\n" + " \"$binary\" : {\n" + + " \"base64\" : \"AAECAwQF\",\n" + " \"subType\" : \"00\"\n" + " }\n" + + " } ],\n" + " \"NestedList12\" : [ -485.34, \"1234abcd\", [ {\n" + " \"$set\" : [ \"xyz01234\", \"xyz0123\", \"abc01234\" ]\n" + " }, {\n" + " \"$set\" : [ {\n" + " \"$binary\" : {\n" + " \"base64\" : \"dmFsMDE=\",\n" + " \"subType\" : \"00\"\n" + " }\n" + " }, {\n" + " \"$binary\" : {\n" @@ -942,7 +943,7 @@ private static RawBsonDocument getUpdatedDocument2() { // } // }, // "ISBN" : "111-1111111111", - // "NestedList1" : [ -473.11999999999995, "1234abcd", [ "xyz0123", { + // "NestedList1" : [ "NestedList1[0] + 12.22", "1234abcd", [ "xyz0123", { // "InPublication" : false, // "BinaryTitleSet" : { // "$set" : [ { @@ -1016,7 +1017,7 @@ private static RawBsonDocument getUpdatedDocument2() { // "n_attr_10" : true, // "n_attr_20" : "str_val_0" // }, - // "attr_5" : [ 1224, "str001", { + // "attr_5" : [ "attr_5[0] - 10", "str001", { // "$binary" : { // "base64" : "AAECAwQF", // "subType" : "00" @@ -1198,9 +1199,10 @@ public void testMixedSetExpressions() { String updateExpression = "{\n" + " \"$SET\": {\n" // 1. Simple key = value + " \"simpleField\": \"newValue\",\n" + " \"numericField\": 42,\n" - // 2. string-based arithmetic: fieldA + fieldB = 10 + 25 = 35 + // 2. literal string containing " + " is stored verbatim (NOT interpreted as arithmetic). + // Arithmetic is only performed via the explicit $ADD / $SUBTRACT document forms below. + " \"sumField\": \"fieldA + fieldB\",\n" - // 2b. string-based arithmetic with subtraction: fieldB - fieldA = 25 - 10 = 15 + // 2b. literal string containing " - " is stored verbatim (NOT interpreted as arithmetic). + " \"diffField\": \"fieldB - fieldA\",\n" // 3. Array element set: items[1] = 999 + " \"items[1]\": 999,\n" @@ -1233,9 +1235,10 @@ public void testMixedSetExpressions() { Assert.assertEquals("newValue", bsonDocument.getString("simpleField").getValue()); Assert.assertEquals(42, bsonDocument.getInt32("numericField").getValue()); - // 2. Verify string-based arithmetic - Assert.assertEquals(35, bsonDocument.getInt32("sumField").getValue()); - Assert.assertEquals(15, bsonDocument.getInt32("diffField").getValue()); + // 2. Verify literal strings containing " + " / " - " are stored verbatim and are never + // mistaken for arithmetic expressions. + Assert.assertEquals("fieldA + fieldB", bsonDocument.getString("sumField").getValue()); + Assert.assertEquals("fieldB - fieldA", bsonDocument.getString("diffField").getValue()); // 3. Verify array element set Assert.assertEquals(999, bsonDocument.getArray("items").get(1).asInt32().getValue()); @@ -1264,6 +1267,37 @@ public void testMixedSetExpressions() { Assert.assertEquals(25, bsonDocument.getInt32("fieldB").getValue()); } + /** + * Regression test for literal string SET values that contain " + " or " - ". Such values must be + * stored verbatim and must never be mistaken for arithmetic expressions (which previously threw + * "Operand ... does not exist"). Arithmetic is only performed via the explicit $ADD / $SUBTRACT + * document forms. + */ + @Test + public void testLiteralStringWithArithmeticOperatorsIsStoredVerbatim() { + BsonDocument bsonDocument = new BsonDocument(); + + // Literal JSON string containing " - ". + String propertiesLiteral = "{\"teamId\":\"abc123\"," + "\"teamName\":\"Foo - Bar Baz Service\"," + + "\"channelName\":\"#foo-notifications\"}"; + + BsonDocument setDoc = new BsonDocument(); + setDoc.put("properties", new BsonString(propertiesLiteral)); + // Literal string containing " + ". + setDoc.put("equation", new BsonString("E = mc^2 + offset")); + // Plain literal that looks like a subtraction of two field names. + setDoc.put("company", new BsonString("Acme - Widgets")); + + BsonDocument updateExpression = new BsonDocument(); + updateExpression.put("$SET", setDoc); + + UpdateExpressionUtils.updateExpression(updateExpression, bsonDocument); + + Assert.assertEquals(propertiesLiteral, bsonDocument.getString("properties").getValue()); + Assert.assertEquals("E = mc^2 + offset", bsonDocument.getString("equation").getValue()); + Assert.assertEquals("Acme - Widgets", bsonDocument.getString("company").getValue()); + } + private static BsonDocument seedListAppendDoc() { return BsonDocument .parse("{" + "\"events\": [\"a\", \"b\"]," + "\"numeric\": 42," + "\"text\": \"hello\","