
Signed-off-by: Joe Hu <jowhuw@amazon.com> Signed-off-by: Joe Hu <joehu888@gmail.com> Co-authored-by: Joe Hu <jowhuw@amazon.com>
4061 lines
174 KiB
Python
4061 lines
174 KiB
Python
from utils_json import DEFAULT_MAX_PATH_LIMIT, DEFAULT_MAX_DOCUMENT_SIZE, \
|
|
DEFAULT_WIKIPEDIA_COMPACT_PATH, DEFAULT_WIKIPEDIA_PATH, \
|
|
JSON_INFO_METRICS_SECTION, JSON_INFO_NAMES
|
|
from valkey.exceptions import ResponseError, NoPermissionError
|
|
from valkeytests.conftest import resource_port_tracker
|
|
import pytest
|
|
import glob
|
|
import logging
|
|
import os
|
|
import random
|
|
import struct
|
|
import json
|
|
from math import isclose, isnan, isinf, frexp
|
|
from json_test_case import JsonTestCase
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
DATA_ORGANISM = '''
|
|
{
|
|
"heavy_animal" : 200,
|
|
"plants" : [
|
|
{
|
|
"name" : "Cactus",
|
|
"height" : 120,
|
|
"weight" : 90
|
|
},
|
|
{
|
|
"name" : "Ghost Plant",
|
|
"height" : "Unknown",
|
|
"weight" : "Unknown"
|
|
},
|
|
{
|
|
"name" : "Redwood",
|
|
"height" : 4200,
|
|
"weight" : 50000
|
|
}
|
|
],
|
|
"animals" : [
|
|
{
|
|
"name" : "Platypus",
|
|
"length" : 24,
|
|
"weight" : 5
|
|
},
|
|
{
|
|
"fish" : [
|
|
{
|
|
"name" : "Bass",
|
|
"length" : 34,
|
|
"weight" : 5
|
|
},
|
|
{
|
|
"name" : "Swordfish",
|
|
"length" : 177,
|
|
"weight" : 200
|
|
}
|
|
|
|
]
|
|
},
|
|
{
|
|
"mammals" : [
|
|
{
|
|
"name" : "Platypus",
|
|
"length" : 24,
|
|
"weight" : 5
|
|
},
|
|
{
|
|
"name" : "Horse",
|
|
"height" : 68,
|
|
"weight" : 660
|
|
},
|
|
{
|
|
"primates" : [
|
|
{
|
|
"name" : "Monkey",
|
|
"height" : 18,
|
|
"weight" : 30
|
|
},
|
|
{
|
|
"name" : "Baboon",
|
|
"height" : 26,
|
|
"weight" : 50
|
|
},
|
|
{
|
|
"apes" : [
|
|
{
|
|
"name" : "Chimpanzee",
|
|
"height" : 66,
|
|
"weight" : 130
|
|
},
|
|
{
|
|
"name" : "Gorilla",
|
|
"height" : 66,
|
|
"weight" : 400
|
|
},
|
|
{
|
|
"name" : "Orangutan",
|
|
"height" : 70,
|
|
"weight" : 300
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
'''
|
|
|
|
# valkey keys
|
|
wikipedia = 'wikipedia'
|
|
wikipedia2 = 'wikipedia2'
|
|
wikipedia3 = 'wikipedia3'
|
|
wikipedia4 = 'wikipedia4'
|
|
store = 'store'
|
|
store2 = 'store2'
|
|
organism = 'organism'
|
|
organism2 = 'organism2'
|
|
str_key = 'str_key'
|
|
k1 = 'k1'
|
|
k2 = 'k2'
|
|
k3 = 'k3'
|
|
k4 = 'k4'
|
|
k5 = 'k5'
|
|
k6 = 'k6'
|
|
k7 = 'k7'
|
|
k8 = 'k8'
|
|
k9 = 'k9'
|
|
k10 = 'k10'
|
|
k11 = 'k11'
|
|
k12 = 'k12'
|
|
k = 'k'
|
|
nonexistentkey = 'nonexistentkey'
|
|
nonexistentpath = 'nonexistentpath'
|
|
input = 'input'
|
|
input2 = 'input2'
|
|
arr = 'arr'
|
|
foo = 'foo'
|
|
baz = 'baz'
|
|
|
|
|
|
# Base Test class containing all core json module tests
|
|
class TestJsonBasic(JsonTestCase):
|
|
|
|
def setup_data(self):
|
|
client = self.server.get_new_client()
|
|
client.config_set(
|
|
'json.max-path-limit', DEFAULT_MAX_PATH_LIMIT)
|
|
client.config_set(
|
|
'json.max-document-size', DEFAULT_MAX_DOCUMENT_SIZE)
|
|
# Need the following line when executing the test against a running Valkey.
|
|
# Otherwise, data from previous test cases will interfere current test case.
|
|
client.execute_command("FLUSHDB")
|
|
|
|
# Load wikipedia sample JSONs. We use wikipedia.json as input to create a document key. Then, use
|
|
# wikipedia_compact.json, which does not have indent/space/newline, to verify correctness of serialization.
|
|
with open(DEFAULT_WIKIPEDIA_PATH, 'r') as file:
|
|
self.data_wikipedia = file.read()
|
|
with open(DEFAULT_WIKIPEDIA_COMPACT_PATH, 'r') as file:
|
|
self.data_wikipedia_compact = file.read()
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.', self.data_wikipedia)
|
|
|
|
# Create a string key to be used for verifying that JSON.GET should not operate on a non-document key.
|
|
client.execute_command(
|
|
'SET', str_key, '{"firstName":"John","lastName":"Smith"}')
|
|
|
|
def setup(self):
|
|
super(TestJsonBasic, self).setup()
|
|
self.setup_data()
|
|
|
|
def test_sanity(self):
|
|
'''
|
|
Test simple SET/GET/MGET/DEL, both legacy and JSONPath syntax.
|
|
'''
|
|
|
|
client = self.server.get_new_client()
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":"1", "b":"2", "c":"3"}')
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k2, '.', '[1,2,3,4,5]')
|
|
assert [b'{"a":"1","b":"2","c":"3"}', b'[1,2,3,4,5]'] == client.execute_command(
|
|
'JSON.MGET', k1, k2, '.')
|
|
assert b'{"a":"1","b":"2","c":"3"}' == client.execute_command(
|
|
'JSON.GET', k1)
|
|
for (key, path, exp) in [
|
|
(k1, '.', '{"a":"1","b":"2","c":"3"}'),
|
|
(k1, '.a', '"1"'),
|
|
(k2, '.', '[1,2,3,4,5]'),
|
|
(k2, '[-1]', '5')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
# pretty print
|
|
for (key, fmt, path, exp) in [
|
|
(k1, 'SPACE', '.', '{"a": "1","b": "2","c": "3"}'),
|
|
(k1, 'INDENT', '.', '{ "a":"1", "b":"2", "c":"3"}'),
|
|
(k2, 'INDENT', '.', '[ 1, 2, 3, 4, 5]'),
|
|
(k2, 'INDENT', '[-2]', '4')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, fmt, ' ', path).decode()
|
|
|
|
assert [b'[{"a":"1","b":"2","c":"3"}]', b'[[1,2,3,4,5]]'] == client.execute_command(
|
|
'JSON.MGET', k1, k2, '$')
|
|
for (key, path, exp) in [
|
|
(k1, '$', '[{"a":"1","b":"2","c":"3"}]'),
|
|
(k1, '$.*', '["1","2","3"]'),
|
|
(k2, '$', '[[1,2,3,4,5]]'),
|
|
(k2, '$.[*]', '[1,2,3,4,5]'),
|
|
(k2, '$.[-1]', '[5]'),
|
|
(k2, '$.[0:3]', '[1,2,3]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
# pretty print
|
|
for (key, fmt, path, exp) in [
|
|
(k1, 'SPACE', '$', '[{"a": "1","b": "2","c": "3"}]'),
|
|
(k1, 'INDENT', '$',
|
|
'[ { "a":"1", "b":"2", "c":"3" }]'),
|
|
(k1, 'INDENT', '$.*', '[ "1", "2", "3"]'),
|
|
(k2, 'INDENT', '$', '[ [ 1, 2, 3, 4, 5 ]]'),
|
|
(k2, 'INDENT', '$.[0:3]', '[ 1, 2, 3]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, fmt, ' ', path).decode()
|
|
|
|
assert 1 == client.execute_command('JSON.DEL', k1)
|
|
assert 1 == client.execute_command('JSON.DEL', k2)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '$', '{"a":"1", "b":"2", "c":"3"}')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k2, '$', '[1,2,3,4,5]')
|
|
for (key, val_before, del_path, del_ret, val_after) in [
|
|
(k1, '[{"a":"1","b":"2","c":"3"}]', '$.*', 3, '[{}]'),
|
|
(k2, '[[1,2,3,4,5]]', '$.[*]', 5, '[[]]')
|
|
]:
|
|
assert val_before == client.execute_command(
|
|
'JSON.GET', key, '$').decode()
|
|
assert del_ret == client.execute_command(
|
|
'JSON.DEL', key, del_path)
|
|
assert val_after == client.execute_command(
|
|
'JSON.GET', key, '$').decode()
|
|
|
|
def test_parse_invalid_json_string(self):
|
|
client = self.server.get_new_client()
|
|
for input_str in ['foo', '{"a"}', '[a]']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.firstName', input_str)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_json_set_command_supports_all_datatypes(self):
|
|
client = self.server.get_new_client()
|
|
for (path, value) in [('.address.city', '"Boston"'), # string
|
|
# number
|
|
('.age', '30'),
|
|
('.foo', '[1,2,3]'), # array
|
|
# array element access
|
|
('.phoneNumbers[0].number', '"1234567"'),
|
|
# object
|
|
('.foo', '{"a":"b"}'),
|
|
('.lastName', 'null'), # null
|
|
('.isAlive', 'false')]: # boolean
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, path, value)
|
|
assert value.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
def test_json_set_command_root_document(self):
|
|
client = self.server.get_new_client()
|
|
# path to the root document is represented as '.'
|
|
for (key, value) in [(k1, '"Boston"'), # string
|
|
(k2, '123'), # number
|
|
(k3, '["Seattle","Boston"]'), # array
|
|
(k3, '[1,2,3]'), # array
|
|
(k4, '{"a":"b"}'), # object
|
|
(k4, '{}'), # empty object
|
|
(k5, 'null'), # null
|
|
(k6, 'false')]: # boolean
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', value)
|
|
assert value.encode() == client.execute_command('JSON.GET', key)
|
|
|
|
def test_json_set_command_nx_xx_options(self):
|
|
client = self.server.get_new_client()
|
|
for (path, value, cond, exp_set_return, exp_get_return) in [
|
|
('.address.city', '"Boston"', 'NX', None, b'"New York"'),
|
|
('.address.city', '"Boston"', 'XX', b'OK', b'"Boston"'),
|
|
('.foo', '"bar"', 'NX', b'OK', b'"bar"'),
|
|
('.firstName', '"bar"', 'NX', None, b'"John"')]:
|
|
assert exp_set_return == client.execute_command(
|
|
'JSON.SET', wikipedia, path, value, cond)
|
|
assert exp_get_return == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.' '"value"', 'badword')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_json_set_command_with_error_conditions(self):
|
|
client = self.server.get_new_client()
|
|
# A new Valkey key's path must be root
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.SET', foo, '.bar', '"bar"')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
# Option XX means setting the value only if the JSON path exists, a.k.a, updating the value.
|
|
# According to API, if the value does not exist, the command should return null instead of error.
|
|
assert None == client.execute_command(
|
|
'JSON.SET', k1, '.', '"some value"', 'XX')
|
|
assert None == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '"bar"', 'XX')
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Option NX means setting the value only if the JSON path does not exists, a.k.a, inserting the value.
|
|
# According to API, if the value exists, the command should return null instead of error.
|
|
assert None == client.execute_command(
|
|
'JSON.SET', wikipedia, '.', '"some new value"', 'NX')
|
|
assert None == client.execute_command(
|
|
'JSON.SET', wikipedia, '.firstName', '"Tom"', 'NX')
|
|
assert b'"John"' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.firstName')
|
|
|
|
# syntax error: wrong option
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.', '"some new value"', 'NXXX')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.', '"some new value"', 'XXXX')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.SET', wikipedia, '.', '"bar"', 'a', 'b')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_set_ancestor_keys_should_not_be_overridden(self):
|
|
client = self.server.get_new_client()
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.firstName.a', '"some new value"')
|
|
assert self.error_class.is_write_error(str(e.value))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.age.a', '1')
|
|
assert self.error_class.is_write_error(str(e.value))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.address.a.b', '"some new value"')
|
|
assert self.error_class.is_write_error(str(e.value))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.address[0]', '"some new value"')
|
|
assert self.error_class.is_write_error(str(e.value))
|
|
|
|
def test_json_set_reject_out_of_boundary_array_index(self):
|
|
client = self.server.get_new_client()
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, 'phoneNumbers[9]', '"123"')
|
|
assert self.error_class.is_outofboundaries_error(str(e.value))
|
|
|
|
def test_json_set_insert_value(self):
|
|
client = self.server.get_new_client()
|
|
# insert is allowed if and only if the parent node is the last child in the path.
|
|
for (path, new_val) in [
|
|
('["address"]["z"]', '"z"'),
|
|
('.address.z2', '"z2"')
|
|
]:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, path, new_val)
|
|
assert new_val.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
# if the parent node is not the last child in the path, insertion is not allowed.
|
|
for (path, new_val) in [
|
|
('["address"]["foo"]["z"]', '"z"'),
|
|
('.address.foo.z', '"z"')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, path, new_val)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
def test_json_set_negative_array_index(self):
|
|
client = self.server.get_new_client()
|
|
new_val = '"1-2-3"'
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.phoneNumbers[-1].number', new_val)
|
|
assert b'"1-2-3"' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.phoneNumbers[-1].number')
|
|
assert b'["212 555-1234","1-2-3"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '$.phoneNumbers[*].number')
|
|
|
|
def test_json_set_legacy_and_v2path_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
data = '''
|
|
{"firstName":"John","lastName":"Smith","age":27,"weight":135.17,"isAlive":true,"address":
|
|
{"street":"21 2nd Street","city":"New York","state":"NY","zipcode":"10021-3100"},
|
|
"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"}],
|
|
"children":[],"spouse":null,"groups":{}}
|
|
'''
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia2, '.', data)
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia3, '.', data)
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia4, '.', data)
|
|
|
|
for (key, path, new_val, exp, path2, exp2) in [
|
|
(wikipedia, '$.address.*', '"1"',
|
|
b'["1","1","1","1"]', '$.address.*', b'["1","1","1","1"]'),
|
|
(wikipedia2, '.address.*', '"1"', b'"1"',
|
|
'$.address.*', b'["1","1","1","1"]'),
|
|
(wikipedia3, '$.phoneNumbers[*].number', '"1"', b'["1","1"]',
|
|
'$.phoneNumbers[*].number', b'["1","1"]'),
|
|
(wikipedia4, '.phoneNumbers[*].number', '"1"', b'"1"',
|
|
'$.phoneNumbers[*].number', b'["1","1"]')
|
|
]:
|
|
client.execute_command(
|
|
'JSON.SET', key, path, new_val)
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
# verify all values
|
|
assert exp2 == client.execute_command(
|
|
'JSON.GET', key, path2)
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2,3]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2, "c":3}}')
|
|
|
|
# NOTE: The expected results below account for the outcome of previous commands.
|
|
test_cases = [
|
|
(k1, '$.a[*]', '1', b'[]',
|
|
b'{"a":[],"b":[1],"c":[1,2,3]}'),
|
|
(k1, '$.b[*]', '2', b'[2]',
|
|
b'{"a":[],"b":[2],"c":[1,2,3]}'),
|
|
(k1, '$.c[*]', '4', b'[4,4,4]',
|
|
b'{"a":[],"b":[2],"c":[4,4,4]}'),
|
|
(k1, '.a[*]', '1', None, None),
|
|
(k1, '.b[*]', '3', b'3',
|
|
b'{"a":[],"b":[3],"c":[4,4,4]}'),
|
|
(k1, '.c[*]', '5', b'5',
|
|
b'{"a":[],"b":[3],"c":[5,5,5]}'),
|
|
(k2, '$.a.*', '1', b'[]',
|
|
b'{"a":{},"b":{"a":1},"c":{"a":1,"b":2,"c":3}}'),
|
|
(k2, '$.b.*', '2',
|
|
b'[2]', b'{"a":{},"b":{"a":2},"c":{"a":1,"b":2,"c":3}}'),
|
|
(k2, '$.c.*', '4',
|
|
b'[4,4,4]', b'{"a":{},"b":{"a":2},"c":{"a":4,"b":4,"c":4}}'),
|
|
(k2, '.a.*', '1', None, None),
|
|
(k2, '.b.*', '3', b'3',
|
|
b'{"a":{},"b":{"a":3},"c":{"a":4,"b":4,"c":4}}'),
|
|
(k2, '.c.*', '5', b'5',
|
|
b'{"a":{},"b":{"a":3},"c":{"a":5,"b":5,"c":5}}')
|
|
]
|
|
|
|
for (key, path, new_val, exp, exp2) in test_cases:
|
|
if exp == None:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', key, path, new_val)
|
|
client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
else:
|
|
client.execute_command(
|
|
'JSON.SET', key, path, new_val)
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
# verify entire key
|
|
assert exp2 == client.execute_command(
|
|
'JSON.GET', key, '.')
|
|
|
|
def test_json_get_command_supports_all_datatypes(self):
|
|
client = self.server.get_new_client()
|
|
for (path, value) in [('.firstName', '"John"'), # string
|
|
('.address.city', '"New York"'), # string
|
|
('.spouse', 'null'), # null
|
|
('.children', '[]'), # empy array
|
|
('.groups', '{}'), # empy object
|
|
('.isAlive', 'true'), # boolean
|
|
('.age', '27')]: # float number
|
|
assert value.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
for (path, value) in [('["weight"]', '135.17')]: # float number
|
|
assert value == client.execute_command(
|
|
'JSON.GET', wikipedia, path).decode()
|
|
|
|
def test_json_path_syntax_objectkeys(self):
|
|
client = self.server.get_new_client()
|
|
for (path, value) in [('["firstName"]', '"John"'),
|
|
('address[\'city\']', '"New York"'),
|
|
('[\'address\'][\'city\']', '"New York"'),
|
|
('["address"]["city"]', '"New York"'),
|
|
('["address"][\'city\']', '"New York"'),
|
|
('["isAlive"]', 'true'),
|
|
('[\'age\']', '27')]:
|
|
assert value.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
for (path, value) in [('["weight"]', '135.17')]:
|
|
assert value == client.execute_command(
|
|
'JSON.GET', wikipedia, path).decode()
|
|
|
|
test_cases = [
|
|
'[firstName"]',
|
|
'address["city\'',
|
|
'["address\'][[[["city"]',
|
|
'[[["address"]]]["city"]',
|
|
'"["address"][\'city\']',
|
|
'"[\'address"]["city"]',
|
|
'[""""address]["city"]',
|
|
'[address""""]',
|
|
'[\'address\']]][\'city\']',
|
|
'["address"]\'[\'"city"]',
|
|
]
|
|
|
|
# invalid json path
|
|
for path in test_cases:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_json_get_command_floating_point(self):
|
|
'''
|
|
Test special cases of floating point values.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
for value in ['0', '0.1', '0.3', '1.23456789', '-0.1', '-0.3', '-1.23456789']:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', value)
|
|
assert value == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
|
|
# max double and min double: floating points will be returned exactly as is
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '1.7976e+308')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.bar', '-1.7976e+308')
|
|
|
|
assert '1.7976e+308' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
assert '-1.7976e+308' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.bar').decode()
|
|
|
|
# 1.234567890123456789 exceeds the precision of a double but will persist regardless.
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '1.234567890123456789')
|
|
|
|
assert '1.234567890123456789' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
|
|
# trailing zeros will no longer be removed.
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '0.3000000')
|
|
|
|
assert '0.3000000' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
|
|
def test_json_get_command_with_multiple_paths(self):
|
|
client = self.server.get_new_client()
|
|
assert b'{".firstName":"John",".lastName":"Smith"}' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.firstName', '.lastName')
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', wikipedia, '.firstName', '.lastName', '.foo', '.bar')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
def test_json_get_command_returns_json_with_default_format(self):
|
|
client = self.server.get_new_client()
|
|
exp = self.data_wikipedia_compact
|
|
|
|
# default format is compact JSON string - no indent, no space and no newline
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia).decode('utf-8')
|
|
|
|
# test NOESCAPE: verify that NOESCAPE is ignored. See the API doc.
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, 'NOESCAPE').decode('utf-8')
|
|
|
|
def test_json_get_command_returns_json_with_custom_format(self):
|
|
client = self.server.get_new_client()
|
|
exp = self.data_wikipedia
|
|
|
|
# get the root document with custom indent/space/newline
|
|
ret = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
|
|
# test NOESCAPE: verify that NOESCAPE is ignored. See the API doc.
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n', 'NOESCAPE').decode('utf-8')
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, 'NOESCAPE', 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'NOESCAPE', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
|
|
# get a sub-document with custom indent/space/newline
|
|
exp_json = '{\n\t"street": "21 2nd Street",\n\t"city": "New York",\n\t"state": "NY",\n\t"zipcode": "10021-3100"\n}'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', 'SPACE', ' ', 'NEWLINE', '\n', '.address').decode('utf-8')
|
|
|
|
# INDENT: =*=*, SPACE: --, NEWLINE: \r\n
|
|
exp_json = '{\r\n=*=*"street":--"21 2nd Street",\r\n=*=*"city":--"New York",\r\n=*=*"state":--"NY",\r\n=*=*"zipcode":--"10021-3100"\r\n}'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '=*=*', 'SPACE', '--', 'NEWLINE', '\r\n', '.address').decode('utf-8')
|
|
|
|
# verify that path args do not need to be positioned at the end
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, '.', 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
exp_json = '"John"'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', '.firstName', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
|
|
exp_json = '{\n ".firstName": "John",\n ".lastName": "Smith"\n}'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE', ' ', '.firstName', '.lastName', 'NEWLINE', '\n').decode('utf-8')
|
|
exp_json = '{\n\t"street": "21 2nd Street",\n\t"city": "New York",\n\t"state": "NY",\n\t"zipcode": "10021-3100"\n}'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.address', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
exp_json = '[\n\t{\n\t\t"street": "21 2nd Street",\n\t\t"city": "New York",\n\t\t"state": "NY",\n\t\t"zipcode": "10021-3100"\n\t}\n]'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '$.address', 'SPACE', ' ', 'NEWLINE', '\n').decode('utf-8')
|
|
|
|
# check that path args can have formatting in between them
|
|
exp_json = '{\n ".firstName": "John",\n ".lastName": "Smith"\n}'
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', '.firstName', 'SPACE', ' ', '.lastName', 'NEWLINE', '\n').decode('utf-8')
|
|
assert exp_json == client.execute_command(
|
|
'JSON.GET', wikipedia, '.firstName', 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE', '\n', '.lastName').decode('utf-8')
|
|
|
|
def test_json_get_command_returns_json_with_custom_format_error_conditions(self):
|
|
client = self.server.get_new_client()
|
|
# NEWLINE is the last arg
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE', ' ', 'NEWLINE')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
# SPACE is the last arg
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', ' ', 'SPACE')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
# INDENT is the last arg
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', wikipedia, 'NEWLINE', '\n', 'SPACE', ' ', 'INDENT')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_json_get_command_with_error_conditions(self):
|
|
client = self.server.get_new_client()
|
|
# if the document key does not exist, the command should return null without throwing an error.
|
|
assert None == client.execute_command(
|
|
'JSON.GET', foo, '.firstName')
|
|
|
|
# if the key is not a document key, the command should throw an error.
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', str_key)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# if the JSON path does not exist, the command should throw an error.
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command('JSON.GET')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_number_as_member_name(self):
|
|
'''
|
|
Given a JSON that has numbers as member names, test GET and SET.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"1":1, "2":{"3":4}}')
|
|
client.execute_command('COPY', k1, k2)
|
|
client.execute_command('COPY', k1, k3)
|
|
client.execute_command('COPY', k1, k4)
|
|
client.execute_command('COPY', k1, k5)
|
|
|
|
# test GET
|
|
for (path, exp) in [
|
|
('["2"]["3"]', '4'),
|
|
('[\'2\'][\'3\']', '4'),
|
|
('.1', '1'),
|
|
('.2.3', '4'),
|
|
('$.2.3', '[4]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', k1, path).decode()
|
|
|
|
# test SET
|
|
for (key, path, val, exp_new_val) in [
|
|
(k1, '["2"]["3"]', '5', '{"1":1,"2":{"3":5}}'),
|
|
(k2, '[\'2\'][\'3\']', '5', '{"1":1,"2":{"3":5}}'),
|
|
(k3, '.2.3', '5', '{"1":1,"2":{"3":5}}'),
|
|
(k4, '.1', '2', '{"1":2,"2":{"3":4}}'),
|
|
(k5, '$.2.*', '5', '{"1":1,"2":{"3":5}}')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, path, val)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
def test_json_get_legacy_and_v2path_wildcard(self):
|
|
'''
|
|
Test two versions of path syntax - V2 JSONPath and the legacy path. A V2 JSONPath must starts with the dollar sign
|
|
that represents the root element. If a path does not start with the dollar sign, it's a legacy path.
|
|
|
|
For queries, the legacy path always returns a single value, which is the first value if multiple values are
|
|
selected. If no value is selected, the legacy path returns NONEXISTENT error. The JSONPath always returns an
|
|
array of values, which could contain zero or one or more values.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
test_cases = [
|
|
('$.address.*',
|
|
b'["21 2nd Street","New York","NY","10021-3100"]'),
|
|
('$.[\'address\'].*',
|
|
b'["21 2nd Street","New York","NY","10021-3100"]'),
|
|
('.address.*', b'"21 2nd Street"'),
|
|
('.["address"].*', b'"21 2nd Street"'),
|
|
('$.phoneNumbers.*.type', b'["home","office"]'),
|
|
('$.phoneNumbers[*].type', b'["home","office"]'),
|
|
('$.["phoneNumbers"][*].["type"]', b'["home","office"]'),
|
|
('.phoneNumbers.*.type', b'"home"'),
|
|
('.phoneNumbers[*].type', b'"home"'),
|
|
('.["phoneNumbers"][*].["type"]', b'"home"'),
|
|
('$.[ \'address\' ].*',
|
|
b'["21 2nd Street","New York","NY","10021-3100"]'),
|
|
('.[ "address" ].*', b'"21 2nd Street"'),
|
|
('$.[ "phoneNumbers" ][ * ].[ "type" ]', b'["home","office"]'),
|
|
]
|
|
|
|
for (path, exp) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b": {"a": 1}, "c": {"a": 1, "b": 2}}')
|
|
client.execute_command(
|
|
'JSON.SET', k3, '.', '{"a":[[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]]}')
|
|
client.execute_command(
|
|
'JSON.SET', k4, '.', '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h:":1}}}}}}}}')
|
|
|
|
# JSONPath always returns an array of values
|
|
# Test multiple wildcards
|
|
for (key, path, exp) in [
|
|
(k1, '$.a[*]', b'[]'),
|
|
(k1, '$.b[*]', b'[1]'),
|
|
(k1, '$.c[*]', b'[1,2]'),
|
|
(k2, '$.a.*', b'[]'),
|
|
(k2, '$.b.*', b'[1]'),
|
|
(k2, '$.c.*', b'[1,2]'),
|
|
(k3, '$.a[*]', b'[[[1,2],[3,4],[5,6]],[[7,8],[9,10],[11,12]]]'),
|
|
(k3, '$.a[*][*]', b'[[1,2],[3,4],[5,6],[7,8],[9,10],[11,12]]'),
|
|
(k3, '$.a[*][*][1]', b'[2,4,6,8,10,12]'),
|
|
(k4, '$.a.*.*', b'[{"d":{"e":{"f":{"g":{"h:":1}}}}}]'),
|
|
(k4, '$.a.*.*.*', b'[{"e":{"f":{"g":{"h:":1}}}}]'),
|
|
(k4, '$.a.*.c.*', b'[{"e":{"f":{"g":{"h:":1}}}}]'),
|
|
(k4, '$.a.*.c.*.e.*', b'[{"g":{"h:":1}}]'),
|
|
(k4, '$.a.*.c.*.e.*.g.*', b'[1]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# Legacy path always returns a single value, which is the first value.
|
|
# Test multiple wildcards
|
|
for (key, path, exp) in [
|
|
(k1, '.b[*]', b'1'),
|
|
(k1, '.c[*]', b'1'),
|
|
(k2, '.b.*', b'1'),
|
|
(k2, '.c.*', b'1'),
|
|
(k3, '.a[*]', b'[[1,2],[3,4],[5,6]]'),
|
|
(k3, '.a[*][*]', b'[1,2]'),
|
|
(k3, '.a[*][*][1]', b'2'),
|
|
(k4, '.a.*.*', b'{"d":{"e":{"f":{"g":{"h:":1}}}}}'),
|
|
(k4, '.a.*.*.*', b'{"e":{"f":{"g":{"h:":1}}}}'),
|
|
(k4, '.a.*.c.*', b'{"e":{"f":{"g":{"h:":1}}}}'),
|
|
(k4, '.a.*.c.*.e.*', b'{"g":{"h:":1}}'),
|
|
(k4, '.a.*.c.*.e.*.g.*', b'1')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# Legacy path returns non-existent error if no value is selected.
|
|
for (key, path, exp) in [
|
|
(k1, '.a[*]', None),
|
|
(k2, '.a.*', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
def test_json_get_negative_array_index_legacypath(self):
|
|
'''
|
|
Test negative array index with the legacy path syntax.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
# Negative indices do not throw error in Valkey
|
|
test_cases = [
|
|
('.phoneNumbers[-2].type', '"home"'),
|
|
('.phoneNumbers[-1].type', '"office"'),
|
|
('.phoneNumbers[ -1 ].type', '"office"'),
|
|
('.["phoneNumbers"][-2]["type"]', '"home"'),
|
|
('.["phoneNumbers"][-1]["type"]', '"office"'),
|
|
('.["phoneNumbers"][ -1]["type" ]', '"office"'),
|
|
('.["phoneNumbers"][-1 ][ "type"]', '"office"'),
|
|
('.["phoneNumbers"][ -1 ][ "type" ]', '"office"')
|
|
]
|
|
|
|
# Out of boundary test cases
|
|
oob_test_cases = [
|
|
'.phoneNumbers[2].type',
|
|
'.phoneNumbers[10].type',
|
|
'.phoneNumbers[-3].type',
|
|
]
|
|
|
|
# Legacy path always returns a single value
|
|
for (path, exp) in test_cases:
|
|
assert exp.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
# index out of bounds
|
|
for path in oob_test_cases:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
assert self.error_class.is_outofboundaries_error(str(e.value))
|
|
|
|
# test using negative index on a non-array value
|
|
for path in [
|
|
'.firstName[-1]',
|
|
'.age[-1]',
|
|
'.weight[-1]',
|
|
'.address[-1]',
|
|
'.phoneNumbers[0][-1]'
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_get_negative_array_index_v2path(self):
|
|
'''
|
|
Test negative array index with the V2 JSONPath syntax.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
test_cases = [
|
|
('$.phoneNumbers[-2].type', '["home"]'),
|
|
('$.phoneNumbers[ -2 ].type', '["home"]'),
|
|
('$.phoneNumbers[-1].type', '["office"]'),
|
|
('$.phoneNumbers[ -1].type', '["office"]'),
|
|
('$.phoneNumbers[-1 ].type', '["office"]'),
|
|
('$.["phoneNumbers"][-2]["type"]', '["home"]'),
|
|
('$.["phoneNumbers"][-1]["type"]', '["office"]'),
|
|
# index out of bounds
|
|
('$.phoneNumbers[2].type', '[]'),
|
|
('$.phoneNumbers[10].type', '[]'),
|
|
# using negative index on a non-array value
|
|
('$.firstName[-1]', '[]'),
|
|
('$.age[-1]', '[]'),
|
|
('$.weight[-1]', '[]'),
|
|
('$.weight[ -1 ]', '[]'),
|
|
('$.phoneNumbers[0][-1]', '[]'),
|
|
('$.phoneNumbers[ 0][ -1 ]', '[]'),
|
|
('$.phoneNumbers[ 0 ][ -1 ]', '[]'),
|
|
('$.[ "phoneNumbers" ][ -1 ][ "type" ]', '["office"]'),
|
|
('$.phoneNumbers[-3].type', '[]'),
|
|
('$.phoneNumbers[ -3 ].type', '[]'),
|
|
]
|
|
|
|
# JSONPath always returns an array of values
|
|
for (path, exp) in test_cases:
|
|
assert exp.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
|
|
def test_json_get_v2path_array_slice(self):
|
|
'''
|
|
Test negative array slice with the V2 JSONPath syntax.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
test_cases = [
|
|
('$[0:3]', '[0,1,2]'),
|
|
('$[ 0 : 3 ]', '[0,1,2]'),
|
|
('$[0:+3]', '[0,1,2]'),
|
|
('$[ 0 : +3 ]', '[0,1,2]'),
|
|
('$[0:-1]', '[0,1,2,3,4,5,6,7,8]'),
|
|
('$[ 0 : -1 ]', '[0,1,2,3,4,5,6,7,8]'),
|
|
('$[2:-2]', '[2,3,4,5,6,7]'),
|
|
('$[ 2 : -2 ]', '[2,3,4,5,6,7]'),
|
|
('$[+2:-2]', '[2,3,4,5,6,7]'),
|
|
('$[1:1]', '[]'),
|
|
('$[1:2]', '[1]'),
|
|
('$[+1:+2]', '[1]'),
|
|
('$[1:3]', '[1,2]'),
|
|
('$[1:0]', '[]'),
|
|
('$[5:]', '[5,6,7,8,9]'),
|
|
('$[ 5 : ]', '[5,6,7,8,9]'),
|
|
('$[:3]', '[0,1,2]'),
|
|
('$[ : 3]', '[0,1,2]'),
|
|
('$[:+3]', '[0,1,2]'),
|
|
('$[: +3]', '[0,1,2]'),
|
|
('$[:6:2]', '[0,2,4]'),
|
|
('$[ : 6 : 2]', '[0,2,4]'),
|
|
('$[:]', '[0,1,2,3,4,5,6,7,8,9]'),
|
|
('$[ : ]', '[0,1,2,3,4,5,6,7,8,9]'),
|
|
('$[::]', '[0,1,2,3,4,5,6,7,8,9]'),
|
|
('$[ : : ]', '[0,1,2,3,4,5,6,7,8,9]'),
|
|
('$[::2]', '[0,2,4,6,8]'),
|
|
('$[ : : 2 ]', '[0,2,4,6,8]'),
|
|
('$[3::2]', '[3,5,7,9]'),
|
|
('$[3 :: 2]', '[3,5,7,9]'),
|
|
('$[0::1]', '[0,1,2,3,4,5,6,7,8,9]'),
|
|
('$[0:8:2]', '[0,2,4,6]'),
|
|
('$[ 0 : 8 : 2 ]', '[0,2,4,6]'),
|
|
('$[0:+8:+2]', '[0,2,4,6]'),
|
|
('$[0 : +8 : +2]', '[0,2,4,6]'),
|
|
('$[6:0:-1]', '[6,5,4,3,2,1]'),
|
|
('$[6::-1]', '[]'),
|
|
('$[6:0:-2]', '[6,4,2]'),
|
|
('$[6::-2]', '[]'),
|
|
('$[8:0:-2]', '[8,6,4,2]')
|
|
]
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
for (path, exp) in test_cases:
|
|
assert exp.encode() == client.execute_command('JSON.GET', k1, path)
|
|
|
|
def test_json_get_v2path_array_union(self):
|
|
'''
|
|
Test array union with the V2 JSONPath syntax.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
test_cases = [
|
|
('$[0,1,2]', '[0,1,2]'),
|
|
('$[0, 1, 2]', '[0,1,2]'),
|
|
('$[0, 1, 2 ]', '[0,1,2]'),
|
|
('$[ 0, 1, 2 ]', '[0,1,2]'),
|
|
('$[ 0,1, 2 ]', '[0,1,2]'),
|
|
('$[0,1]', '[0,1]'),
|
|
('$[-1,-2]', '[9,8]'),
|
|
('$[-10,-5,-6]', '[0,5,4]'),
|
|
('$[0,1,5,0,1,2]', '[0,1,5,0,1,2]'),
|
|
('$[0, 1,5,0, 1,2]', '[0,1,5,0,1,2]'),
|
|
('$[ 0, 1, 5, 0, 1, 2 ]', '[0,1,5,0,1,2]'),
|
|
('$[ -10 , -5 , -6 ]', '[0,5,4]'),
|
|
('$[-10,-9,-8,0,1,2,-1000,1000]', '[0,1,2,0,1,2]'),
|
|
]
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
for (path, exp) in test_cases:
|
|
assert exp.encode() == client.execute_command('JSON.GET', k1, path)
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '[{"name":"name0","id":0},{"name":"name1","id":1},{"name":"name2","id":2}]')
|
|
for (path, exp) in [
|
|
('$[0,2].name', '["name0","name2"]'),
|
|
]:
|
|
assert exp.encode() == client.execute_command('JSON.GET', k2, path)
|
|
|
|
# we do not support mixing of unions and slices, nor do we support extraneous commas
|
|
for path in [
|
|
'$[0,1,2:4]',
|
|
'$[0:2,3,4]',
|
|
'$[0,,4]',
|
|
'$[0,,,4]',
|
|
'$[,4]',
|
|
'$[4,]',
|
|
'$[,4,]',
|
|
'$[,,4,,]',
|
|
'$[,]',
|
|
'$[,0,4]',
|
|
'$[,0,4,]',
|
|
'$[0,4,]',
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', k1, path)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_json_get_multipaths_legacy_and_v2path_wildcard(self):
|
|
'''
|
|
Test JSON.GET with multiple paths, legacy path or v2 JSONPath or mixed.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b": {"a": 1}, "c": {"a": 1, "b": 2}}')
|
|
|
|
test_cases = [
|
|
(k1, '.b[*]', '.c[*]', '{".b[*]":1,".c[*]":1}'),
|
|
(k2, '.b.*', '.c.*', '{".b.*":1,".c.*":1}'),
|
|
]
|
|
|
|
# if all paths are legacy path, the result conforms to the legacy path version
|
|
for (key, path1, path2, exp) in test_cases:
|
|
# 1st path returns 1 value. 2nd path returns the first one of 2 values.
|
|
assert exp.encode() == client.execute_command(
|
|
'JSON.GET', key, path1, path2)
|
|
|
|
# all paths are legacy path, the result conforms to the legacy path version.
|
|
# If one path returns 0 value, the command should fail with NONEXISTENT error.
|
|
for (key, path1, path2, exp) in [
|
|
(k1, '.a[*]', '.b[*]', None),
|
|
(k2, '.a.*', '.b.*', None),
|
|
(k2, '.foo.*', '.c.*', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path1, path2)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# if at least one path is JSONPath, the result conforms to the JSONPath version
|
|
for (key, path1, path2, path3, exp) in [
|
|
(k1, '$.a[*]', '$.b[*]', '$.c[*]',
|
|
'{"$.a[*]":[],"$.b[*]":[1],"$.c[*]":[1,2]}'),
|
|
(k1, '$.a[*]', '.b[*]', '.c[*]',
|
|
'{"$.a[*]":[],".b[*]":[1],".c[*]":[1,2]}'),
|
|
(k1, '.a[*]', '$.b[*]', '.c[*]',
|
|
'{".a[*]":[],"$.b[*]":[1],".c[*]":[1,2]}'),
|
|
(k2, '.a.*', '.b.*', '$.c.*',
|
|
'{".a.*":[],".b.*":[1],"$.c.*":[1,2]}'),
|
|
(k2, '$.a.*', '$.b.*', '.c.*',
|
|
'{"$.a.*":[],"$.b.*":[1],".c.*":[1,2]}'),
|
|
(k2, '.a.*', '$.b.*', '$.c.*',
|
|
'{".a.*":[],"$.b.*":[1],"$.c.*":[1,2]}'),
|
|
# 1st path returns 0 value. 2nd path returns 1 value. 3rd path returns 2 values.
|
|
(k2, '.foo.*', '$.b.*', '$.c.*',
|
|
'{".foo.*":[],"$.b.*":[1],"$.c.*":[1,2]}')
|
|
]:
|
|
assert exp.encode() == client.execute_command(
|
|
'JSON.GET', key, path1, path2, path3)
|
|
|
|
def test_json_mget_command(self):
|
|
client = self.server.get_new_client()
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', '{"foo":"bar1"}')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k2, '.', '{"foo":"bar2"}')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k3, '.', '{"foo":"bar3"}')
|
|
assert [b'"bar1"', b'"bar2"', b'"bar3"'] == client.execute_command(
|
|
'JSON.MGET', k1, k2, k3, '.foo')
|
|
# test the condition of JSON path does not exist
|
|
assert [None, None, None] == client.execute_command(
|
|
'JSON.MGET', k1, k2, k3, '.bar')
|
|
# test the condition of key does not exist
|
|
assert [None, None] == client.execute_command(
|
|
'JSON.MGET', baz, foo, '.')
|
|
assert [None, b'"bar2"'] == client.execute_command(
|
|
'JSON.MGET', baz, k2, '.foo')
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command('JSON.MGET')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'json.set', k4, '.', '[2,5,{"level0":[null,true,{"level0_1":[3,false]}],"level1":{"level1_0":33}}]')
|
|
assert b'OK' == client.execute_command(
|
|
'json.set', k5, '.', '[4,5,{"level0":[null,false,{"level0_1":[null,false]}],"level1":{"level1_0":[12,13]}}]')
|
|
assert b'OK' == client.execute_command(
|
|
'json.set', k6, '.', '[2,5,{"level0":[true,20,{"level0_1":[3,true]}],"level1":{"level1_0":33}}]')
|
|
for (path, exp) in [
|
|
('$..level0_1', [b"[[3,false]]",
|
|
b"[[null,false]]", b"[[3,true]]"]),
|
|
('$.[2].level1', [b'[{"level1_0":33}]',
|
|
b'[{"level1_0":[12,13]}]', b'[{"level1_0":33}]']),
|
|
('$.[2].level1.level1_0[2]', [b"[]", b"[]", b"[]"]),
|
|
('$.[2].level1.level1_0[1]', [b"[]", b"[13]", b"[]"])
|
|
]:
|
|
assert [exp[0], exp[1], exp[2]] == client.execute_command(
|
|
'JSON.MGET', k4, k5, k6, path)
|
|
|
|
def test_json_key_declaration(self):
|
|
client = self.server.get_new_client()
|
|
cmd_need_val = set(
|
|
'SET NUMMULTBY NUMINCRBY ARRAPPEND ARRINDEX STRAPPEND RESP'.split())
|
|
|
|
# These commands should only get the single key
|
|
for cmd in ('DEL', 'GET', 'SET', 'TYPE', 'NUMINCRBY', 'NUMMULTBY', 'TOGGLE', 'STRAPPEND', 'STRLEN',
|
|
'ARRAPPEND', 'ARRINDEX', 'ARRLEN', 'ARRPOP', 'CLEAR', 'OBJKEYS',
|
|
'OBJLEN', 'FORGET', 'RESP'):
|
|
if cmd not in cmd_need_val:
|
|
assert [k1] == client.execute_command(
|
|
'COMMAND GETKEYS', f'JSON.{cmd}', k1)
|
|
else:
|
|
# Dummy value in command
|
|
assert [k1] == client.execute_command(
|
|
'COMMAND GETKEYS', f'JSON.{cmd}', k1, '.', '5')
|
|
|
|
# ARRINSERT requires index
|
|
assert ['k1'] == client.execute_command(
|
|
'COMMAND GETKEYS', 'JSON.ARRINSERT', 'k1', '.', 0, '5')
|
|
# ARRINSERT requires start end
|
|
assert ['k1'] == client.execute_command(
|
|
'COMMAND GETKEYS', 'JSON.ARRTRIM', 'k1', '.', 0, 5)
|
|
|
|
debug_subcmd = set('MEMORY DEPTH'.split())
|
|
for cmd in debug_subcmd:
|
|
assert [k1] == client.execute_command(
|
|
'COMMAND GETKEYS', 'JSON.DEBUG', cmd, k1)
|
|
|
|
# JSON.MGET is the only multi-key command, so make sure it returns the right set of keys
|
|
assert [k1, k2, k3] == client.execute_command(
|
|
'COMMAND GETKEYS', 'JSON.MGET', k1, k2, k3, '.')
|
|
|
|
def __json_del_or_forget__(self, cmd):
|
|
client = self.server.get_new_client()
|
|
# delete an element
|
|
for path in ['.spouse', '.phoneNumbers']:
|
|
assert 1 == client.execute_command(
|
|
cmd, wikipedia, path)
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.GET', wikipedia, path)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# delete a doc: path not provided
|
|
assert 1 == client.execute_command(cmd, wikipedia)
|
|
assert 0 == client.execute_command('EXISTS', wikipedia)
|
|
|
|
# delete a doc: path arg provided
|
|
client.execute_command('json.set', k1, '.', '1')
|
|
assert 1 == client.execute_command(cmd, k1, '.')
|
|
assert 0 == client.execute_command('EXISTS', k1)
|
|
|
|
# return should be 0 if the document key does not exist
|
|
assert 0 == client.execute_command(
|
|
cmd, foo, '.firstName')
|
|
|
|
# return should be 0 if the path does not exist
|
|
assert 0 == client.execute_command(
|
|
cmd, wikipedia, '.foo')
|
|
|
|
# Wrong number of arguments
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
cmd, wikipedia, '.children', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_del_command(self):
|
|
self.__json_del_or_forget__('JSON.DEL')
|
|
|
|
def test_json_forget_command(self):
|
|
self.__json_del_or_forget__('JSON.FORGET')
|
|
|
|
def test_json_del_command_v2path_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
client.execute_command(
|
|
'json.set', k1, '.', '{"x": {}, "y": {"a":"a"}, "z": {"a":"", "b":"b"}}')
|
|
client.execute_command(
|
|
'json.set', k2, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
client.execute_command(
|
|
'json.set', k3, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
client.execute_command(
|
|
'json.set', k4, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
client.execute_command(
|
|
'json.set', k5, '.', '[0,1,2,3,4,5,6,7,8,9]')
|
|
|
|
# NOTE: The expected values below account for the outcome of previous commands.
|
|
for (key, path, exp_ret, exp_val) in [
|
|
(k1, '$.z.*', 2, '{"x":{},"y":{"a":"a"},"z":{}}'),
|
|
(k1, '$.*', 3, '{}'),
|
|
(k2, '$.[3:6]', 3, '[0,1,2,6,7,8,9]'),
|
|
(k2, '$.*', 7, '[]'),
|
|
]:
|
|
assert exp_ret == client.execute_command(
|
|
'JSON.DEL', key, path)
|
|
assert exp_val == client.execute_command(
|
|
'JSON.GET', key).decode()
|
|
|
|
# delete whole doc
|
|
for key in [k1, k2]:
|
|
assert 1 == client.execute_command(
|
|
'JSON.DEL', key, '$')
|
|
assert 0 == client.execute_command('EXISTS', key)
|
|
|
|
# delete with wildcard, slice, and union
|
|
assert 10 == client.execute_command(
|
|
'JSON.DEL', k3, '$[*]')
|
|
assert b'[]' == client.execute_command('JSON.GET', k3)
|
|
assert 4 == client.execute_command(
|
|
'JSON.DEL', k4, '$[3:7]')
|
|
assert b'[0,1,2,7,8,9]' == client.execute_command(
|
|
'JSON.GET', k4)
|
|
assert 4 == client.execute_command(
|
|
'JSON.DEL', k5, '$[1,5,7,8]')
|
|
assert b'[0,2,3,4,6,9]' == client.execute_command(
|
|
'JSON.GET', k5)
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'json.set', k1, '.', '[2,5,{"level0":[null,true,{"level0_1":[3,false]}],"level1":{"level1_0":33}}]')
|
|
for (key, path, exp_ret, exp_val) in [
|
|
(k1, '$[2].level0..level0_1[1]', 1,
|
|
'[2,5,{"level0":[null,true,{"level0_1":[3]}],"level1":{"level1_0":33}}]'),
|
|
(k1, '$[2].level0..level0_1', 1,
|
|
'[2,5,{"level0":[null,true,{}],"level1":{"level1_0":33}}]'),
|
|
(k1, '$..level1.level1_0', 1,
|
|
'[2,5,{"level0":[null,true,{}],"level1":{}}]'),
|
|
(k1, '$..level1', 1,
|
|
'[2,5,{"level0":[null,true,{}]}]'),
|
|
(k1, '.*', 3, '[]'),
|
|
]:
|
|
assert exp_ret == client.execute_command(
|
|
'JSON.DEL', key, path)
|
|
assert exp_val == client.execute_command(
|
|
'JSON.GET', key).decode()
|
|
if path != '.*':
|
|
assert '[]' == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
else:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
def test_json_unicode_is_supported(self):
|
|
client = self.server.get_new_client()
|
|
for unicode_str in [
|
|
'"Eat, drink, 愛"',
|
|
'"hyvää-élève"'
|
|
]:
|
|
utf8 = unicode_str.encode('utf-8')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', unicode_str)
|
|
assert utf8 == client.execute_command(
|
|
'JSON.GET', k1, 'NOESCAPE', '.')
|
|
assert 1 == client.execute_command('JSON.DEL', k1)
|
|
assert 0 == client.execute_command('EXISTS', k1)
|
|
|
|
def test_nonASCII_in_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a": {"愛": "love", "b": "b"}}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"愛": [1,2,3]}')
|
|
for (key, path1, path2, exp) in [
|
|
(k1, '.a.愛', None, b'"love"'),
|
|
(k1, '.a.愛', '.a.b', b'{".a.\xe6\x84\x9b":"love",".a.b":"b"}'),
|
|
(k2, '.愛[1]', None, b'2'),
|
|
(k2, '$.愛[*]', None, b'[1,2,3]')
|
|
]:
|
|
if path2 is None:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path1)
|
|
else:
|
|
# Valkey behavior is weird and returns dictionaries in a non-deterministic order
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path1, path2)
|
|
|
|
def test_json_number_scanner(self):
|
|
'''Test that numeric conversion gets the right types around various edge cases'''
|
|
client = self.server.get_new_client()
|
|
maxpos = (1 << 63) - 1
|
|
|
|
for v in [
|
|
(maxpos, b'integer'),
|
|
(-maxpos, b'integer'),
|
|
(-maxpos-1, b'integer')]:
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', str(v[0]))
|
|
assert v[1] == client.execute_command(
|
|
'JSON.TYPE', k1, '.'), "Value is " + str(v[0])
|
|
|
|
for v in [
|
|
(maxpos+1, b'number'),
|
|
(-maxpos-2, b'number')
|
|
]:
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', str(v[0]))
|
|
assert v[1] == client.execute_command(
|
|
'JSON.TYPE', k1, '.'), "Value is " + str(v[0])
|
|
|
|
def test_json_toggle(self):
|
|
client = self.server.get_new_client()
|
|
# Toggle back and forth
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foobool', 'false')
|
|
assert b'false' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foobool')
|
|
|
|
assert b'true' == client.execute_command(
|
|
'JSON.TOGGLE', wikipedia, '.foobool')
|
|
assert b'true' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foobool')
|
|
|
|
assert b'false' == client.execute_command(
|
|
'JSON.TOGGLE', wikipedia, '.foobool')
|
|
assert b'false' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foobool')
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.TOGGLE', wikipedia, '.foobool', 'extra')
|
|
assert self.error_class.is_wrong_number_of_arguments_error(
|
|
str(e.value))
|
|
|
|
# Wrong types
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foonum', '55')
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.TOGGLE', wikipedia, '.foonum')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foostr', '"ok"')
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.TOGGLE', wikipedia, '.foostr')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_toggle_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":true, "b":false, "c":1, "d":null, "e":"foo", "f":[], "g":{}}')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[true, false, 1, null, "foo", [], {}]')
|
|
|
|
for (key, path, exp, exp_new_val) in [
|
|
(k1, '$.*', [0, 1, None, None, None, None, None],
|
|
'{"a":false,"b":true,"c":1,"d":null,"e":"foo","f":[],"g":{}}'),
|
|
(k1, '$.*', [1, 0, None, None, None, None, None],
|
|
'{"a":true,"b":false,"c":1,"d":null,"e":"foo","f":[],"g":{}}'),
|
|
(k2, '$[*]', [0, 1, None, None, None, None, None],
|
|
'[false,true,1,null,"foo",[],{}]'),
|
|
(k2, '$[*]', [1, 0, None, None, None, None, None],
|
|
'[true,false,1,null,"foo",[],{}]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.TOGGLE', key, path)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
def test_json_numincrby(self):
|
|
client = self.server.get_new_client()
|
|
assert b'28' == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.age', '1')
|
|
assert b'38' == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.age', '10')
|
|
assert b'33' == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.age', '-5')
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '1')
|
|
assert b'1.5' == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.foo', '0.5')
|
|
assert b'2' == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.foo', '0.5')
|
|
|
|
# error condition: document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.NUMINCRBY', foo, '.age', '2')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.bar', '2')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.age', '1', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_nummultby(self):
|
|
client = self.server.get_new_client()
|
|
assert b'270' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.age', '10')
|
|
assert b'2700' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.age', '10')
|
|
assert b'27' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.age', '0.01')
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '1')
|
|
assert b'0.5' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', '0.5')
|
|
assert b'0.25' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', '0.5')
|
|
assert b'1' == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', '4')
|
|
|
|
# error condition: document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.NUMMULTBY', foo, '.age', '2')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.bar', '2')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.age', '2', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_simple_operations_remove_double_styling(self):
|
|
# Multiplying by one or adding zero will now remove styling
|
|
# and really change output for negative exponents due to decimals
|
|
client = self.server.get_new_client()
|
|
data = [
|
|
('2.50000', '2.5'),
|
|
('2e30', '2e+30'),
|
|
('2E+30', '2e+30'),
|
|
('2E30', '2e+30'),
|
|
('2E-30', '2.0000000000000002e-30'),
|
|
('2e5', '200000'),
|
|
('-2.50000', '-2.5'),
|
|
('-2e30', '-2e+30'),
|
|
('-2E+30', '-2e+30'),
|
|
('-2E30', '-2e+30'),
|
|
('-2E-30', '-2.0000000000000002e-30'),
|
|
('-2e5', '-200000'),
|
|
]
|
|
|
|
for (initial, unstyled) in data:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', initial)
|
|
assert initial == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', '1')
|
|
assert unstyled == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
|
|
for (initial, unstyled) in data:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', initial)
|
|
assert initial == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.foo', '0')
|
|
assert unstyled == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo').decode()
|
|
|
|
def test_json_double_operations_wrongtype(self):
|
|
client = self.server.get_new_client()
|
|
# None of these will these will succeed because the doubles are not valid or the field is not a double
|
|
for (cmd, key, field, arg) in [
|
|
('JSON.NUMMULTBY', wikipedia, '.age', '"2.0"'),
|
|
('JSON.NUMMULTBY', wikipedia, '.age', '2.'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0.'),
|
|
('JSON.NUMMULTBY', wikipedia, '.age', '-2.'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '-2.0.'),
|
|
('JSON.NUMMULTBY', wikipedia, '.age', '+2.'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '+2.0.'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '.2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', 'a2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '-a2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '+a2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', 'e2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '-e2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '+e2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', 'e+2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '-e-2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '+E+2.0'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0e'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0eq'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0e3q'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+a'),
|
|
('JSON.NUMINCRBY', wikipedia, '.age', '2.0e+41a'),
|
|
('JSON.NUMINCRBY', wikipedia, '.firstName', '2'),
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
cmd, key, field, arg)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_double_consistency(self):
|
|
'''
|
|
Test that double values remain consistent when going through JSON Engine.
|
|
This tests a tolerance of 2^-50 for a decent number of iterations,
|
|
but is not enough to guarantee that level of presicion to our customers.
|
|
Also verify that regular and pretty print double values have the same output.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
# arbitrarily generated hex to double to string, to send to json
|
|
random.seed(1234)
|
|
data = []
|
|
for i in range(24000):
|
|
potential_double = struct.unpack(
|
|
'<d', bytes.fromhex('%016x' % random.randrange(16**16)))[0]
|
|
# We don't want inf/-inf, NaN, or denormalized numbers
|
|
if not (isinf(potential_double) or isnan(potential_double) or frexp(potential_double)[1] == 0):
|
|
data.append(str(potential_double))
|
|
|
|
for val in data:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
v0 = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
vp = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
# verify pretty print content is the same as standard content
|
|
assert v0 is not None and vp is not None and v0 == vp
|
|
# verify content is accurate, worked up to 2^-50 when float was calculated, should work when stored as string
|
|
assert v0.decode() == val
|
|
|
|
def test_json_numincrby_large_numbers(self):
|
|
'''
|
|
Test edge cases with large values near the boundaries of int64 and double.
|
|
'''
|
|
# Although the result exceeds the range of int64, it is still a valid double number.
|
|
client = self.server.get_new_client()
|
|
for (val, incr) in [
|
|
('9223372036854775807', 1),
|
|
('9223372036854775807', 2),
|
|
('-9223372036854775808', -1),
|
|
('-9223372036854775808', -2),
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
assert val.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
# verify pretty-print gives us the same result
|
|
assert val.encode() == client.execute_command(
|
|
'JSON.GET', wikipedia, 'SPACE', ' ', 'NEWLINE', '\n', '.foo')
|
|
|
|
# The result is still within the range of int64
|
|
data = [
|
|
('-9223372036854775808', '-9223372036854775808', 0,
|
|
'-9.2233720368547758e+18', '-9223372036854775808'),
|
|
('-9223372036854775808', '-9223372036854775808', 1,
|
|
'-9.2233720368547758e+18', '-9223372036854775807'),
|
|
('-9223372036854775808', '-9223372036854775808', 2,
|
|
'-9.2233720368547758e+18', '-9223372036854775806'),
|
|
('1.79e+308', '1.79e308', 0, '1.79e+308', '1.79e308'),
|
|
('1.79e+308', '1.79e308', 1, '1.79e+308', '1.79e308'),
|
|
('1.79e+308', '1.79e308', -1, '1.79e+308', '1.79e308'),
|
|
('-1.79e+308', '-1.79e308', 0, '-1.79e+308', '-1.79e308'),
|
|
('-1.79e+308', '-1.79e308', 1, '-1.79e+308', '-1.79e308'),
|
|
('-1.79e+308', '-1.79e308', -1, '-1.79e+308', '-1.79e308'),
|
|
('9223372036854775807', '9223372036854775807', 0,
|
|
'9.2233720368547758e+18', '9223372036854775807'),
|
|
('9223372036854775807', '9223372036854775807', -1,
|
|
'9.2233720368547758e+18', '9223372036854775806'),
|
|
('9223372036854775807', '9223372036854775807', -2,
|
|
'9.2233720368547758e+18', '9223372036854775805')
|
|
]
|
|
iteration_tracker = 0
|
|
for (val, val_alt, incr, exp, exp_alt) in data:
|
|
logging.debug("1058: Iteration %d", iteration_tracker)
|
|
iteration_tracker += 1
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
v = client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.foo', incr)
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
|
|
# The result is still within the range of double
|
|
for (val, val_alt, incr, exp, exp_alt) in [
|
|
('1.79e+308', '1.79e308', 0, '1.79e+308', '1.79e308'),
|
|
('1.79e+308', '1.79e308', 1, '1.79e+308', '1.79e308'),
|
|
('1.79e+308', '1.79e308', -1, '1.79e+308', '1.79e308'),
|
|
('-1.79e+308', '-1.79e308', 0, '-1.79e+308', '-1.79e308'),
|
|
('-1.79e+308', '-1.79e308', 1, '-1.79e+308', '-1.79e308'),
|
|
('-1.79e+308', '-1.79e308', -1, '-1.79e+308', '-1.79e308')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
v = client.execute_command(
|
|
'JSON.NUMINCRBY', wikipedia, '.foo', incr)
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
|
|
def test_json_nummultby_large_numbers(self):
|
|
'''
|
|
Test edge cases with large values near the boundaries of int64 and double.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
# Multiplication causes overflow
|
|
for (val, val_alt, mult) in [
|
|
('9223372036854775807', '9223372036854775807', 1.79e+308),
|
|
('9223372036854775807', '9223372036854775807', -1.79e+308),
|
|
('1.79e+308', '1.79e308', 1.79e308),
|
|
('1.03e+300', '1.03e300', 1.03e300),
|
|
('1.79e+308', '1.79e308', -1.79e308),
|
|
('-1.03e+300', '-1.03e300', 1.03e300)
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'SPACE', ' ', 'NEWLINE', '\n', '.foo')
|
|
assert v is not None and v.decode() == val or v.decode() == val_alt
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', mult)
|
|
assert self.error_class.is_number_overflow_error(str(e.value))
|
|
|
|
# The result is still within the range of double
|
|
data = [
|
|
('9223372036854775807', '9223372036854775807', 0, '0', '0'),
|
|
('9223372036854775807', '9223372036854775807', 2,
|
|
'1.8446744073709552e+19', '18446744073709553000.0'),
|
|
('9223372036854775807', '9223372036854775807', 9223372036854775807,
|
|
'8.5070591730234616e+37', '8.5070591730234616e37'),
|
|
('9223372036854775807', '9223372036854775807', -9223372036854775807,
|
|
'-8.5070591730234616e+37', '-8.5070591730234616e37'),
|
|
('1.79e308', '1.79e+308', 0, '0', '0'),
|
|
('1.79e308', '1.79e+308', 1, '1.79e+308', '1.79e308'),
|
|
('-1.79e308', '-1.79e+308', 1, '-1.79e+308', '-1.79e308'),
|
|
('9223372036854775807', '9223372036854775807', 1,
|
|
'9.2233720368547758e+18', '9223372036854775807')
|
|
]
|
|
|
|
for (val, val_alt, mult, exp, exp_alt) in data:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', val)
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and (
|
|
v.decode() == val or v.decode() == val_alt)
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and (
|
|
v.decode() == val or v.decode() == val_alt)
|
|
v = client.execute_command(
|
|
'JSON.NUMMULTBY', wikipedia, '.foo', mult)
|
|
# logging.debug("DEBUG val: %s, mult: %f, v: %s, exp: %s" %(val, mult, v.decode(), exp))
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
# verify pretty-print gives us the same result
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, 'INDENT', '\t', '.foo')
|
|
assert v is not None and v.decode() == exp or v.decode() == exp_alt
|
|
|
|
def test_json_strlen_command(self):
|
|
client = self.server.get_new_client()
|
|
assert 2 == client.execute_command(
|
|
'JSON.STRLEN', wikipedia, '.address.state')
|
|
assert 4 == client.execute_command(
|
|
'JSON.STRLEN', wikipedia, '.firstName')
|
|
|
|
# edge case: empty string
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '""')
|
|
assert 0 == client.execute_command(
|
|
'JSON.STRLEN', wikipedia, '.foo')
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.STRLEN', foo, '.firstName')
|
|
|
|
# return error if path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRLEN', wikipedia, '.bar')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: element is not a string
|
|
for path in ['.address', '.groups', '.age', '.isAlive', '.spouse']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRLEN', wikipedia, path)
|
|
|
|
# Wrong number of arguments
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.STRLEN', wikipedia, '.firstName', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_strappend_command(self):
|
|
client = self.server.get_new_client()
|
|
for (val, new_len, new_val) in [
|
|
('"son"', 7, '"Johnson"'),
|
|
('" Junior"', 14, '"Johnson Junior"'),
|
|
('" is"', 17, '"Johnson Junior is"'),
|
|
('" my"', 20, '"Johnson Junior is my"'),
|
|
('" friend."', 28, '"Johnson Junior is my friend."'),
|
|
('""', 28, '"Johnson Junior is my friend."')
|
|
]:
|
|
assert new_len == client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, '.firstName', val)
|
|
assert new_val == client.execute_command(
|
|
'JSON.GET', wikipedia, '.firstName').decode('utf-8')
|
|
|
|
# edge case: appending to an empty string
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foo', '""')
|
|
client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, '.foo', '"abc"')
|
|
assert b'"abc"' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.foo')
|
|
|
|
# error condition: document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRAPPEND', foo, '.firstName', '"abc"')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, '.bar', '"abc"')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: attempt to append a non-string value
|
|
for val in ['123', 'true', 'false', 'null', '{}', '[]']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, '.firstName', val)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# error condition: attempt to append to a non-string element
|
|
for path in ['.address', '.groups', '.age', '.isAlive', '.spouse']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, path, '"12"')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.STRAPPEND', wikipedia, '.firstName', '"abc"', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_strlen_command_legacy_and_jsonpath_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":{"a":"a"}, "b":{"a":""}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$.a.a', [1]),
|
|
(k1, '$.a.*', [1]),
|
|
(k1, '$.b.a', [0]),
|
|
(k1, '$.b.*', [0]),
|
|
(k1, '$.c.*', [1, 2]),
|
|
(k1, '$.c.b', [2]),
|
|
(k1, '$.d.*', [None, 1, None]),
|
|
(k1, '.a.a', 1),
|
|
(k1, '.a.*', 1),
|
|
(k1, '.b.a', 0),
|
|
(k1, '.b.*', 0),
|
|
(k1, '.c.*', 1),
|
|
(k1, '.c.b', 2),
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.STRLEN', key, path)
|
|
|
|
assert 1 == client.execute_command(
|
|
'JSON.STRLEN', k1, '.d.*')
|
|
|
|
def test_json_strappend_command_legacy_and_jsonpath_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":{"a":"a"}, "b":{"a":""}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
# NOTE: The expected result below accounts for the outcome of previous commands.
|
|
for (key, path, append, exp_ret, exp_new_str, exp_whole_json) in [
|
|
(k1, '$.a.a', '"x"', [
|
|
2], '["ax"]', '{"a":{"a":"ax"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.a.a', '""', [
|
|
2], '["ax"]', '{"a":{"a":"ax"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.a.*', '"yz"', [4], '["axyz"]',
|
|
'{"a":{"a":"axyz"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '.a.a', '"a"', 5, '"axyza"',
|
|
'{"a":{"a":"axyza"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '.a.*', '"a"', 6, '"axyzaa"',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":""},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.b.a', '"a"', [
|
|
1], '["a"]', '{"a":{"a":"axyzaa"},"b":{"a":"a"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.b.*', '""', [1], '["a"]',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"a"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.b.*', '"a"', [2], '["aa"]',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '.b.a', '"a"', 3, '"aaa"',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '.b.*', '"a"', 4, '"aaaa"',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.c.*', '"a"', [2, 3], '["aa","bba"]',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aa","b":"bba"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.c.b', '"a"', [
|
|
4], '["bbaa"]', '{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"b","c":3}}'),
|
|
|
|
# The following strappend changes value at $.c value to {"a":"aaa", "b":"bbaaa"}.
|
|
# strappend returns length of the last updated value, which is 5,
|
|
# while 'json.get .c.*' returns the first selected element, which is "aaa".
|
|
(k1, '.c.*', '"a"', 5, '"aaa"',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaa"},"d":{"a":1,"b":"b","c":3}}'),
|
|
|
|
(k1, '.c.b', '"a"', 6, '"bbaaaa"',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"b","c":3}}'),
|
|
(k1, '$.d.*', '"a"', [None, 2, None], '[1,"ba",3]',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"ba","c":3}}'),
|
|
|
|
# strappend returns length of the last updated value, which is 3 ("baa"),
|
|
# while 'json.get .d.*' returns the first selected element, which is 1.
|
|
(k1, '.d.*', '"a"', 3, '1',
|
|
'{"a":{"a":"axyzaa"},"b":{"a":"aaaa"},"c":{"a":"aaa","b":"bbaaaa"},"d":{"a":1,"b":"baa","c":3}}')
|
|
]:
|
|
assert exp_ret == client.execute_command(
|
|
'JSON.STRAPPEND', key, path, append)
|
|
assert exp_new_str == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
|
|
def test_json_objectlen_command(self):
|
|
client = self.server.get_new_client()
|
|
assert 4 == client.execute_command(
|
|
'JSON.OBJLEN', wikipedia, '.address')
|
|
|
|
# edge case: empty object
|
|
assert 0 == client.execute_command(
|
|
'JSON.OBJLEN', wikipedia, '.groups')
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.OBJLEN', foo, '.address')
|
|
|
|
# return error if path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.OBJLEN', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an object
|
|
for path in ['.children', '.phoneNumbers', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.OBJLEN', wikipedia, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.OBJLEN', wikipedia, '.', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_objlen_command_jsonpath_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":{}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":{"a":3,"b":4}}, "e":1}')
|
|
|
|
test_cases = [
|
|
(k1, '$.a', [0]),
|
|
(k1, '$.a.*', []),
|
|
(k1, '.a', 0),
|
|
(k1, '$.c', [2]),
|
|
(k1, '$.c.*', [None, None]),
|
|
(k1, '.c', 2),
|
|
(k1, '$.d', [3]),
|
|
(k1, '$.d.*', [None, None, 2]),
|
|
(k1, '.d', 3),
|
|
(k1, '$.*', [0, 2, 3, None]),
|
|
(k1, '.*', 0),
|
|
(k1, '.d.*', 2),
|
|
]
|
|
|
|
for (key, path, exp) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.OBJLEN', key, path)
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.OBJLEN', k1, '.a.*')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
for (key, path) in [
|
|
(k1, '.c.*'),
|
|
(k1, '.e')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.OBJLEN', key, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_objectkeys_command(self):
|
|
client = self.server.get_new_client()
|
|
obj_keys = [b'street', b'city', b'state', b'zipcode']
|
|
assert obj_keys == client.execute_command(
|
|
'JSON.OBJKEYS', wikipedia, '.address')
|
|
|
|
# edge case: empty object
|
|
assert [] == client.execute_command(
|
|
'JSON.OBJKEYS', wikipedia, '.groups')
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.OBJKEYS', foo, '.address')
|
|
|
|
# return should be null if path does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.OBJKEYS', wikipedia, '.foo')
|
|
|
|
# error condition: the element is not an object
|
|
for path in ['.children', '.phoneNumbers', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.OBJKEYS', wikipedia, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.OBJKEYS', wikipedia, '.', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_objkeys_command_jsonpath_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":{}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":{"a":3,"b":4}}, "e":1}')
|
|
|
|
test_cases = [
|
|
(k1, '$.a', [[]]),
|
|
(k1, '$.a.*', []),
|
|
(k1, '.a', []),
|
|
(k1, '$.c', [[b"a", b"b"]]),
|
|
(k1, '.c', [b"a", b"b"]),
|
|
(k1, '$.d', [[b"a", b"b", b"c"]]),
|
|
(k1, '$.d.*', [[], [], [b"a", b"b"]]),
|
|
(k1, '.d', [b"a", b"b", b"c"]),
|
|
(k1, '.d.*', [b"a", b"b"]),
|
|
(k1, '$.*', [[], [b"a", b"b"], [b"a", b"b", b"c"], []]),
|
|
(k1, '.*', [b"a", b"b"]),
|
|
(k1, '$.c.*', [[], []]),
|
|
(k1, '$.c.*', [None, None])
|
|
]
|
|
|
|
for (key, path, exp) in [
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.OBJKEYS', key, path)
|
|
|
|
for (key, path) in [
|
|
(k1, '.c.*'),
|
|
(k1, '.e')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.OBJLEN', key, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_arrlen_command(self):
|
|
client = self.server.get_new_client()
|
|
assert 2 == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.phoneNumbers')
|
|
|
|
# edge case: empty array
|
|
assert 0 == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.children')
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.ARRLEN', foo, '.phoneNumbers')
|
|
|
|
# return error if path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.phoneNumbers', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_arrlen_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[[], \"a\", [\"a\", \"b\"], [\"a\", \"b\", \"c\"], 4]')
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$.[*]', [0, 1, 2, 3]),
|
|
(k2, '$.[*]', [0, None, 2, 3, None])
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRLEN', key, path)
|
|
|
|
def test_json_arrappend_command(self):
|
|
client = self.server.get_new_client()
|
|
# edge case: append to an empty array
|
|
assert 1 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"John"')
|
|
assert b'["John"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
|
|
# append to non-empty array
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"Mary"', '"Tom"')
|
|
assert b'["John","Mary","Tom"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.children')
|
|
|
|
# return error if document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRAPPEND', foo, '.children', '"Mary"')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.foo', '"abc"')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, path, '123')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
def test_json_arrappend_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
|
|
|
|
for (key, path, val, exp, new_val) in [
|
|
(k1, '$.[*]', '"a"', [1, 2, 3, 4],
|
|
'[[\"a\"],[\"a\",\"a\"],[\"a\",\"b\",\"a\"],[\"a\",\"b\",\"c\",\"a\"]]'),
|
|
(k2, '$.[*]', '""', [1, 2, 3, 4],
|
|
'[[\"\"],[\"a\",\"\"],[\"a\",\"b\",\"\"],[\"a\",\"b\",\"c\",\"\"]]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRAPPEND', key, path, val)
|
|
assert new_val == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
|
|
def test_json_arrpop_command(self):
|
|
client = self.server.get_new_client()
|
|
# edge case: pop an empty array
|
|
assert None == client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.children')
|
|
|
|
# populate the array
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
|
|
|
|
for (idx, popped_out, new_len, new_val) in [
|
|
(1, '"Mary"', 2, '["John","Tom"]'),
|
|
(-1, '"Tom"', 1, '["John"]'),
|
|
(0, '"John"', 0, '[]')
|
|
]:
|
|
assert popped_out == client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.children', idx).decode('utf-8')
|
|
assert new_len == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.children')
|
|
assert new_val == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children').decode('utf-8')
|
|
|
|
# edge case: pop an empty array
|
|
assert None == client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.children')
|
|
|
|
# return error if document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRPOP', foo, '.children')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, path)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# test large index: larger than int32
|
|
client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, ".children", 3000000000)
|
|
|
|
# Wrong number of arguments
|
|
res = b'{"type":"home","number":"212 555-1234"}'
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.phoneNumbers', 0, 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_arrpop_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], ["a"], ["a", "b"], ["a", "b", "c"]]')
|
|
|
|
client.execute_command('COPY', k1, k2)
|
|
client.execute_command('COPY', k1, k3)
|
|
|
|
for (key, path, index, exp, exp_new_val) in [
|
|
(k1, '$.[*]', 0, [None, b'"a"', b'"a"', b'"a"'],
|
|
'[[],[],["b"],["b","c"]]'),
|
|
(k2, '$.[*]', 1, [None, b'"a"', b'"b"', b'"b"'],
|
|
'[[],[],["a"],["a","c"]]'),
|
|
(k3, '$.[*]', -1, [None, b'"a"', b'"b"', b'"c"'],
|
|
'[[],[],["a"],["a","b"]]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRPOP', key, path, index)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
def test_json_arrinsert_command(self):
|
|
client = self.server.get_new_client()
|
|
# edge case: insert into an empty array
|
|
assert 1 == client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, '.children', 0, '"foo"')
|
|
assert b'["foo"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
assert b'"foo"' == client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.children')
|
|
|
|
# populate the array
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
|
|
assert b'["John","Mary","Tom"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
|
|
for (idx, val, new_len, new_val) in [
|
|
(0, '"Kathy"', 4, '["Kathy","John","Mary","Tom"]'),
|
|
(2, '"Rose"', 5, '["Kathy","John","Rose","Mary","Tom"]'),
|
|
(3, '"Bob"', 6, '["Kathy","John","Rose","Bob","Mary","Tom"]'),
|
|
(-1, '"Peter"', 7,
|
|
'["Kathy","John","Rose","Bob","Mary","Peter","Tom"]'),
|
|
(-1, '"Jane"', 8,
|
|
'["Kathy","John","Rose","Bob","Mary","Peter","Jane","Tom"]'),
|
|
(8, '"Grace"', 9,
|
|
'["Kathy","John","Rose","Bob","Mary","Peter","Jane","Tom","Grace"]')
|
|
]:
|
|
assert new_len == client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, '.children', idx, val)
|
|
assert new_val == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children').decode('utf-8')
|
|
|
|
# return error if document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', foo, '.children', 0, '"abc"')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, '.foo', 0, '123')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, path, 0, '1')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# error condition: index arg is out of array boundaries
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, ".children", 3000000000, '"a"')
|
|
assert self.error_class.is_outofboundaries_error(str(e.value))
|
|
|
|
# error condition: index arg is out of array boundaries
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', wikipedia, ".children", 31, '"a"')
|
|
assert self.error_class.is_outofboundaries_error(str(e.value))
|
|
|
|
def test_json_arrinsert_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], [0], [0, 1], [0, 1, 2]]')
|
|
# COPY NOT SUPPORTED by REJSON
|
|
client.execute_command('JSON.SET', k2, '.',
|
|
'[[], [0], [0, 1], [0, 1, 2]]')
|
|
client.execute_command('JSON.SET', k3, '.',
|
|
'[[], [0], [0, 1], [0, 1, 2]]')
|
|
|
|
test_cases = [
|
|
(k1, '$.[*]', 0, '3', [1, 2, 3, 4],
|
|
'[[3],[3,0],[3,0,1],[3,0,1,2]]'),
|
|
(k3, '$.[*]', -1, '3', [1, 2, 3, 4],
|
|
'[[3],[3,0],[0,3,1],[0,1,3,2]]')
|
|
]
|
|
|
|
# Negative paths beyond array length work strangely in ReJSON
|
|
for (key, path, index, val, exp, exp_new_val) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRINSERT', key, path, index, val)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
# test index out of bounds error
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINSERT', k2, "$.[*]", 1, '3')
|
|
assert self.error_class.is_outofboundaries_error(str(e.value))
|
|
|
|
def test_json_clear_command(self):
|
|
client = self.server.get_new_client()
|
|
|
|
# populate the array to be cleared
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
|
|
assert b'["John","Mary","Tom"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
|
|
# clears and counts 1, 0 remain
|
|
assert 1 == client.execute_command(
|
|
'JSON.CLEAR', wikipedia, '.children')
|
|
assert 0 == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.children')
|
|
|
|
# clears empty array
|
|
assert 0 == client.execute_command(
|
|
'JSON.CLEAR', wikipedia, '.children')
|
|
assert 0 == client.execute_command(
|
|
'JSON.ARRLEN', wikipedia, '.children')
|
|
|
|
# if path does not exist, the command should return 0
|
|
assert 0 == client.execute_command(
|
|
'JSON.CLEAR', wikipedia, '.foo')
|
|
|
|
# if the value at the path is not a container, the command should return 0
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', wikipedia, '.foobool', 'false')
|
|
assert 0 == client.execute_command(
|
|
'JSON.CLEAR', wikipedia, '.foobool')
|
|
|
|
# clear the wikipedia object entirely
|
|
wikipedia_objlen = client.execute_command(
|
|
'JSON.OBJLEN', wikipedia)
|
|
assert 0 != wikipedia_objlen
|
|
assert 1 == client.execute_command(
|
|
'JSON.CLEAR', wikipedia)
|
|
assert 0 == client.execute_command(
|
|
'JSON.OBJLEN', wikipedia)
|
|
|
|
def test_json_clear_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":{}, "b":{"a": 1, "b": null, "c": true}, "c":1, "d":true, "e":null, "f":"d"}')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[[], [0], [0,1], [0,1,2], 1, true, null, "d"]')
|
|
|
|
test_cases = [
|
|
(k1, '$.*', 4, '{"a":{},"b":{},"c":0,"d":false,"e":null,"f":""}'),
|
|
(k2, '$[*]', 6, '[[],[],[],[],0,false,null,""]')
|
|
]
|
|
|
|
for (key, path, exp, exp_new_val) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.CLEAR', key, path)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
def test_json_arrtrim_command(self):
|
|
client = self.server.get_new_client()
|
|
# edge case: empty array
|
|
assert 0 == client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, '.children', 0, 1)
|
|
|
|
# populate the array
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.children', '"John"', '"Mary"', '"Tom"')
|
|
assert b'["John","Mary","Tom"]' == client.execute_command(
|
|
'JSON.GET', wikipedia, '.children')
|
|
|
|
for (path, start, end, new_len, new_val) in [
|
|
('.children', 1, 2, 2, '["Mary","Tom"]'),
|
|
('.children', 0, 0, 1, '["Mary"]'),
|
|
('.children', -1, 5, 1, '["Mary"]'),
|
|
('.phoneNumbers', 2, 0, 0, '[]')
|
|
]:
|
|
assert new_len == client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, path, start, end)
|
|
assert new_val == client.execute_command(
|
|
'JSON.GET', wikipedia, path).decode('utf-8')
|
|
|
|
# return error if document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRTRIM', foo, '.children', 0, 1)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, '.foo', 0, 3)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, path, 0, 1)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# test large index: larger than int32
|
|
client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, ".phoneNumbers", 3000000000, 3000000001)
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.ARRTRIM', wikipedia, '.phoneNumbers', 0, 1, 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_arrtrim_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], ["a"], ["a", "b"], ["a", "b", "c"]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[[], ["a"], ["a", "b"], ["a", "b", "c"]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k3, '.',
|
|
'[[], [0], [0,1], [0,1,2], [0,1,2,3]]')
|
|
|
|
for (key, path, start, stop, exp, exp_new_val) in [
|
|
(k1, '$.[*]', 0, 1, [0, 1, 2, 2],
|
|
'[[],["a"],["a","b"],["a","b"]]'),
|
|
(k2, '$.[*]', 1, 1, [0, 0, 1, 1], '[[],[],["b"],["b"]]'),
|
|
(k3, '$.[*]', 1, 2, [0, 0, 1, 2, 2], '[[],[],[1],[1,2],[1,2]]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRTRIM', key, path, start, stop)
|
|
assert exp_new_val == client.execute_command(
|
|
'JSON.GET', key, '.').decode()
|
|
|
|
def test_json_arrindex_command(self):
|
|
# edge case: empty array
|
|
client = self.server.get_new_client()
|
|
assert -1 == client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, '.children', '"tom"')
|
|
|
|
# populate the array
|
|
assert 5 == client.execute_command('JSON.ARRAPPEND', wikipedia, '.children',
|
|
'"John"', '"Mary"', '"Tom"', '"Paul"', '"Peter"')
|
|
|
|
for (val, idx) in [
|
|
('"John"', 0),
|
|
('"Tom"', 2),
|
|
('"Peter"', 4),
|
|
('"Peter2"', -1)
|
|
]:
|
|
assert idx == client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, '.children', val)
|
|
|
|
for (val, start, stop, idx) in [
|
|
('"Tom"', 5, 0, -1),
|
|
('"Paul"', 0, 4, 3),
|
|
('"Paul"', 0, 0, 3)
|
|
]:
|
|
assert idx == client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, '.children', val, start, stop)
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', arr, '.', '[0, 1, 2, 3, 4]')
|
|
assert 1 == client.execute_command(
|
|
'JSON.ARRINDEX', arr, '.', '1')
|
|
assert - \
|
|
1 == client.execute_command(
|
|
'JSON.ARRINDEX', arr, '.', '1', '2')
|
|
|
|
# return error if document key does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINDEX', foo, '.children', 0, 5)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, '.foo', 0, 3)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# error condition: the element is not an array
|
|
for path in ['.address', '.groups', '.age', '.weight', '.isAlive', '.spouse', '.firstName']:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, path, '1')
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# test large index: larger than int32
|
|
client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, ".phoneNumbers", '1', 3000000000, 0)
|
|
|
|
def test_json_arrindex_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'[[], [\"a\"], [\"a\", \"b\"], [\"a\", \"b\", \"c\"]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[[], [0], [0,1], [0,1,2]]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k3, '.',
|
|
'[[], [0,true], [0,1,false], [0,1,2,null,true]]')
|
|
|
|
for (key, path, val, exp) in [
|
|
(k1, '$.[*]', '"a"', [-1, 0, 0, 0]),
|
|
(k1, '$.[*]', '"b"', [-1, -1, 1, 1]),
|
|
(k1, '$.[*]', '"c"', [-1, -1, -1, 2]),
|
|
(k2, '$.[*]', '1', [-1, -1, 1, 1]),
|
|
(k2, '$.[*]', '2', [-1, -1, -1, 2]),
|
|
(k2, '$.[*]', 'true', [-1, -1, -1, -1]),
|
|
(k3, '$.[*]', 'true', [-1, 1, -1, 4]),
|
|
(k3, '$.[*]', 'null', [-1, -1, -1, 3])
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRINDEX', key, path, val)
|
|
|
|
def test_json_arrindex_should_not_limit_to_scalar_value(self):
|
|
client = self.server.get_new_client()
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '[5, 6, {"a":"b"}, [99,100]]')
|
|
assert 2 == client.execute_command(
|
|
'JSON.ARRINDEX', k1, '.', '{"a":"b"}', 0, 0)
|
|
assert 3 == client.execute_command(
|
|
'JSON.ARRINDEX', k1, '.', '[99,100]', 0, 0)
|
|
assert 0 == client.execute_command(
|
|
'JSON.ARRINDEX', k1, '.', '5', 0, 0)
|
|
assert 1 == client.execute_command(
|
|
'JSON.ARRINDEX', k1, '.', '6', 0, 0)
|
|
|
|
# Wrong number of arguments
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.ARRINDEX', wikipedia, '.phoneNumbers', '1', 0, 3, 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_arrindex_complex_v2_path(self):
|
|
client = self.server.get_new_client()
|
|
|
|
json_string = '{"level0":{"level1_0":{"level2":[1,2,3, [25, [4,5,{"c":"d"}]]]},"level1_1":{"level2":[[{"a":[2,5]},true,null]]}}}'
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', json_string)
|
|
for (path, val, exp) in [
|
|
("$..level0.level1_0..", b'[4,5,{"c":"d"}]', [
|
|
None, -1, 1, -1, None]),
|
|
("$..level0.level1_0..", b'[25,[4,5,{"c":"d"}]]', [
|
|
None, 3, -1, -1, None]),
|
|
("$..level0.level1_0..", b'{"c":"d"}',
|
|
[None, -1, -1, 2, None]),
|
|
("$..level0.level1_1..", b'[{"a":[2,5]},true,null]', [
|
|
None, 0, -1, None, -1]),
|
|
("$..level0.level1_1..", b'[null,true,{"a":[2,5]}]', [
|
|
None, -1, -1, None, -1]),
|
|
("$..level0.level1_1..", b'[{"a":[2,5]},true]', [
|
|
None, -1, -1, None, -1]),
|
|
("$..level0.level1_0..", b'[4,{"c":"d"}]', [
|
|
None, -1, -1, -1, None])
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.ARRINDEX', k1, path, val)
|
|
|
|
def test_json_type_command(self):
|
|
client = self.server.get_new_client()
|
|
for (path, type) in [
|
|
('.', 'object'),
|
|
('.groups', 'object'),
|
|
('.phoneNumbers', 'array'),
|
|
('.children', 'array'),
|
|
('.isAlive', 'boolean'),
|
|
('.spouse', 'null'),
|
|
('.address.city', 'string'),
|
|
('.age', 'integer'),
|
|
('.weight', 'number')
|
|
]:
|
|
assert type == client.execute_command(
|
|
'JSON.TYPE', wikipedia, path).decode('utf-8')
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command('JSON.TYPE', foo)
|
|
|
|
# return should be null if path does not exist
|
|
assert None == client.execute_command(
|
|
'JSON.TYPE', wikipedia, '.foo')
|
|
|
|
# Wrong number of arguments
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.TYPE', wikipedia, '.phoneNumbers', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_type_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'{"a":1, "b":2.3, "c":"foo", "d":true, "e":null, "f":{}, "g":[]}')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'[1, 2.3, "foo", true, null, {}, []]')
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$.*', [b"integer", b"number", b"string",
|
|
b"boolean", b"null", b"object", b"array"]),
|
|
(k2, '$[*]', [b"integer", b"number", b"string",
|
|
b"boolean", b"null", b"object", b"array"])
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.TYPE', key, path)
|
|
|
|
def test_json_resp_command(self):
|
|
client = self.server.get_new_client()
|
|
for (path, res) in [
|
|
('.firstName', b'John'),
|
|
('.isAlive', b'true'),
|
|
('.age', 27),
|
|
('.spouse', None)
|
|
]:
|
|
assert res == client.execute_command(
|
|
'JSON.RESP', wikipedia, path)
|
|
|
|
for (path, res) in [('.weight', '135.17')]:
|
|
assert res == client.execute_command(
|
|
'JSON.RESP', wikipedia, path).decode()
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '.children')
|
|
assert 1 == len(arr)
|
|
assert arr == [b'[']
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '.address')
|
|
assert 5 == len(arr)
|
|
|
|
assert arr[0:4] == [b'{', [b'street', b'21 2nd Street'], [
|
|
b'city', b'New York'], [b'state', b'NY']]
|
|
assert b'10021-3100' == arr[4][1]
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '.phoneNumbers')
|
|
assert 3 == len(arr)
|
|
assert b'[' == arr[0]
|
|
|
|
assert 3 == len(arr[1])
|
|
assert arr[1] == [b'{', [b'type', b'home'],
|
|
[b'number', b'212 555-1234']]
|
|
assert 3 == len(arr[2])
|
|
assert arr[2] == [b'{', [b'type', b'office'],
|
|
[b'number', b'646 555-4567']]
|
|
|
|
# return should be null if document key does not exist
|
|
assert None == client.execute_command('JSON.RESP', foo)
|
|
|
|
# error condition: path does not exist
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.RESP', wikipedia, '.foo')
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Wrong number of arguments
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.RESP', wikipedia, '.phoneNumbers', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
def test_json_resp_command_jsonpath(self):
|
|
client = self.server.get_new_client()
|
|
|
|
for (path, res) in [
|
|
('$.firstName', [b'John']),
|
|
('$.isAlive', [b'true']),
|
|
('$.age', [27]),
|
|
('$.spouse', [None]),
|
|
('$.foo', [])
|
|
]:
|
|
assert res == client.execute_command(
|
|
'JSON.RESP', wikipedia, path)
|
|
|
|
for (path, res) in [('$.weight', '135.17')]:
|
|
assert res == client.execute_command(
|
|
'JSON.RESP', wikipedia, path)[0].decode()
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '$.children')
|
|
assert 1 == len(arr)
|
|
assert arr == [[b'[']]
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '$.address.*')
|
|
assert 4 == len(arr)
|
|
assert arr == [b'21 2nd Street', b'New York', b'NY', b'10021-3100']
|
|
|
|
arr = client.execute_command(
|
|
'JSON.RESP', wikipedia, '$.phoneNumbers.*')
|
|
assert 2 == len(arr)
|
|
assert arr[0] == [b'{', [b'type', b'home'],
|
|
[b'number', b'212 555-1234']]
|
|
assert arr[1] == [b'{', [b'type', b'office'],
|
|
[b'number', b'646 555-4567']]
|
|
|
|
def test_json_debug_memory(self):
|
|
# non-existent key
|
|
client = self.server.get_new_client()
|
|
|
|
assert None == client.execute_command(
|
|
'JSON.DEBUG MEMORY', nonexistentkey)
|
|
|
|
# non-existent path
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.DEBUG MEMORY', wikipedia, nonexistentpath)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# syntax error: key not provided
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.DEBUG MEMORY')
|
|
assert str(e.value).startswith('wrong number of arguments')
|
|
|
|
# syntax error: wrong subcommand
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.DEBUG MEMORY123', wikipedia)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.DEBUG M', wikipedia)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
assert None == client.execute_command(
|
|
'JSON.DEBUG MEMORY', wikipedia, '.', 'extra')
|
|
assert str(e.value).find('wrong number of arguments') >= 0
|
|
|
|
# Test shared path
|
|
no_shared_mem = client.execute_command(
|
|
'JSON.DEBUG', 'MEMORY', wikipedia)
|
|
with_shared_mem = client.execute_command(
|
|
'JSON.DEBUG', 'MEMORY', wikipedia, '.')
|
|
assert with_shared_mem > no_shared_mem
|
|
|
|
def test_json_duplicate_keys(self):
|
|
client = self.server.get_new_client()
|
|
'''Test handling of object with duplicate keys'''
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":0, "a":1}')
|
|
assert b'{"a":1}' == client.execute_command(
|
|
'JSON.GET', k1, '.')
|
|
assert [b"a"] == client.execute_command(
|
|
'JSON.OBJKEYS', k1)
|
|
assert b'1' == client.execute_command(
|
|
'JSON.GET', k1, 'a')
|
|
assert 1 == client.execute_command('JSON.OBJLEN', k1)
|
|
client.execute_command('JSON.SET', k1, 'a', '2')
|
|
assert b'{"a":2}' == client.execute_command(
|
|
'JSON.GET', k1, '.')
|
|
client.execute_command('JSON.NUMINCRBY', k1, 'a', '2')
|
|
assert b'{"a":4}' == client.execute_command(
|
|
'JSON.GET', k1, '.')
|
|
client.execute_command('JSON.NUMMULTBY', k1, 'a', '2')
|
|
assert b'{"a":8}' == client.execute_command(
|
|
'JSON.GET', k1, '.')
|
|
client.execute_command('JSON.DEL', k1, 'a')
|
|
assert b'{}' == client.execute_command(
|
|
'JSON.GET', k1, '.')
|
|
|
|
def test_json_set_command_max_depth(self):
|
|
client = self.server.get_new_client()
|
|
|
|
def json_with_depth(depth):
|
|
return '{"a":'*depth + '{}' + '}'*depth
|
|
|
|
depth_limit = 128
|
|
client.execute_command(
|
|
'config set json.max-path-limit ' + str(depth_limit))
|
|
|
|
# json not too deep: ok
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k, '.', json_with_depth(127))
|
|
|
|
# error condition: json is too deep
|
|
with pytest.raises(ResponseError) as e:
|
|
json_deep = json_with_depth(200000)
|
|
client.execute_command(
|
|
'JSON.SET', k, '.', json_deep)
|
|
assert self.error_class.is_limit_exceeded_error(str(e.value))
|
|
|
|
def test_json_set_command_max_size(self):
|
|
client = self.server.get_new_client()
|
|
|
|
def json_with_size(size):
|
|
return '"' + 'a'*(size-2) + '"'
|
|
|
|
MB = 2**20
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k, '.', json_with_size(33*MB))
|
|
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', k, '.', json_with_size(64*MB))
|
|
assert self.error_class.is_limit_exceeded_error(str(e.value))
|
|
|
|
def test_multi_exec(self):
|
|
client = self.server.get_new_client()
|
|
client.execute_command('MULTI')
|
|
client.execute_command(
|
|
'JSON.DEL', wikipedia, '.address.street')
|
|
client.execute_command(
|
|
'JSON.DEL', wikipedia, '.address.zipcode')
|
|
client.execute_command(
|
|
'JSON.SET', wikipedia, '.address.region', '"US East"')
|
|
client.execute_command('EXEC')
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.address').decode()
|
|
assert v == '{"city":"New York","state":"NY","region":"US East"}' or \
|
|
v == '{"state":"NY","city":"New York","region":"US East"}'
|
|
|
|
client.execute_command('MULTI')
|
|
client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.phoneNumbers')
|
|
client.execute_command(
|
|
'JSON.ARRPOP', wikipedia, '.phoneNumbers')
|
|
client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.phoneNumbers', '123')
|
|
client.execute_command(
|
|
'JSON.ARRAPPEND', wikipedia, '.phoneNumbers', '456')
|
|
client.execute_command('EXEC')
|
|
v = client.execute_command(
|
|
'JSON.GET', wikipedia, '.phoneNumbers').decode()
|
|
assert v == '[123,456]'
|
|
|
|
def test_escaped_member_names(self):
|
|
'''
|
|
Test accessing member names that contain escaped characters.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
assert b'OK' == client.execute_command('JSON.SET', k1, '.',
|
|
'{"a\\\\a":1, "b\\tb":2, "c\\nc":3, "d\\rd":4, "e\\be":5, "f\\"f":6, "":7, "\'":8}')
|
|
assert b'OK' == client.execute_command('JSON.SET', k2, '.',
|
|
'{"key\\u0000":"value\\u0000", "key\\u001F":"value\\u001F"}')
|
|
for (key, path, exp) in [
|
|
(k1, '$["a\\\\a"]', '[1]'),
|
|
(k1, "$['a\\a']", '[1]'),
|
|
(k1, '$["b\\tb"]', '[2]'),
|
|
(k1, "$['b\\tb']", '[2]'),
|
|
(k1, '$["c\\nc"]', '[3]'),
|
|
(k1, "$['c\\nc']", '[3]'),
|
|
(k1, '$["d\\rd"]', '[4]'),
|
|
(k1, "$['d\\rd']", '[4]'),
|
|
(k1, '$["e\\be"]', '[5]'),
|
|
(k1, "$['e\\be']", '[5]'),
|
|
(k1, '$["f\\"f"]', '[6]'),
|
|
(k1, "$['f\"f']", '[6]'),
|
|
(k1, '$[""]', '[7]'),
|
|
(k1, "$['']", '[7]'),
|
|
(k1, '$["\'"]', '[8]'),
|
|
(k1, "$['\\\'']", '[8]'),
|
|
(k2, '$["key\\u0000"]', '["value\\u0000"]'),
|
|
(k2, '$["key\\u001F"]', '["value\\u001F"]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path).decode()
|
|
|
|
def test_serializing_escaped_quotes_in_member_name(self):
|
|
client = self.server.get_new_client()
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', '{"\\"a":1, "\\"b":2}')
|
|
for (path, space, exp) in [
|
|
('.', None, '{"\\"a":1,"\\"b":2}'),
|
|
('.', ' ', '{"\\"a": 1,"\\"b": 2}')
|
|
]:
|
|
if space is None:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', k1, path).decode()
|
|
else:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', k1, 'space', space, path).decode()
|
|
|
|
def test_json_numincrby_jsonpath_and_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
|
|
client.execute_command(
|
|
'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
# JSONPath: return an array of values.
|
|
# If a value is not a number, its corresponding returned element is JSON null.
|
|
# NOTE: The expected value has accounted for the outcome of previous commands on the same key.
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMINCRBY', k1, '$.a.*', '1', '[]'),
|
|
('JSON.GET', k1, '$.a.*', None, '[]'),
|
|
('JSON.NUMINCRBY', k1, '$.b.*', '1', '[2]'),
|
|
('JSON.GET', k1, '$.b.*', None, '[2]'),
|
|
('JSON.NUMINCRBY', k1, '$.b[*]', '1', '[3]'),
|
|
('JSON.GET', k1, '$.b[*]', None, '[3]'),
|
|
('JSON.NUMINCRBY', k1, '$.d.*', '1', '[2,3,4]'),
|
|
('JSON.GET', k1, '$.d.*', None, '[2,3,4]'),
|
|
('JSON.NUMINCRBY', k1, '$.d[*]', '1', '[3,4,5]'),
|
|
('JSON.GET', k1, '$.d[*]', None, '[3,4,5]'),
|
|
('JSON.NUMINCRBY', k2, '$.a.*', '1', '[]'),
|
|
('JSON.GET', k2, '$.a.*', None, '[]'),
|
|
('JSON.NUMINCRBY', k2, '$.b.*', '1', '[2]'),
|
|
('JSON.GET', k2, '$.b.*', None, '[2]'),
|
|
('JSON.NUMINCRBY', k2, '$.d.*', '1', '[2,3,4]'),
|
|
('JSON.GET', k2, '$.d.*', None, '[2,3,4]'),
|
|
('JSON.NUMINCRBY', k3, '$.a.*', '1', '[null]'),
|
|
('JSON.GET', k3, '$.a.*', None, '["a"]'),
|
|
('JSON.NUMINCRBY', k3, '$.b.*', '1', '[null,2]'),
|
|
('JSON.GET', k3, '$.b.*', None, '["a",2]'),
|
|
('JSON.NUMINCRBY', k3, '$.c.*', '1', '[null,null]'),
|
|
('JSON.GET', k3, '$.c.*', None, '["a","b"]'),
|
|
('JSON.NUMINCRBY', k3, '$.d.*', '1', '[2,null,4]'),
|
|
('JSON.GET', k3, '$.d.*', None, '[2,"b",4]')
|
|
]:
|
|
if incr_num is not None:
|
|
assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
|
|
else:
|
|
assert exp.encode() == client.execute_command(cmd, key, path)
|
|
|
|
def test_json_numincrby_legacy_path_and_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
|
|
client.execute_command(
|
|
'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
# Legacy path: return NONEXISTENT error if no value is selected
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMINCRBY', k1, '.a.*', '1', None),
|
|
('JSON.NUMINCRBY', k2, '.a.*', '1', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
cmd, key, path, incr_num)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Legacy path: return WRONGTYPE error if no number value is selected
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMINCRBY', k3, '.a.*', '1', None),
|
|
('JSON.NUMINCRBY', k3, '.c.*', '1', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
cmd, key, path, incr_num)
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Legacy path: return a single value, which is the last updated value.
|
|
# NOTE: The expected value has accounted for the outcome of previous commands on the same key.
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMINCRBY', k1, '.b.*', '1', '2'),
|
|
('JSON.GET', k1, '.b.*', None, '2'),
|
|
('JSON.NUMINCRBY', k1, '.b[*]', '1', '3'),
|
|
('JSON.GET', k1, '.b[*]', None, '3'),
|
|
('JSON.NUMINCRBY', k1, '.d.*', '1', '4'),
|
|
('JSON.GET', k1, '.d.*', None, '2'),
|
|
('JSON.NUMINCRBY', k1, '.d[*]', '1', '5'),
|
|
('JSON.GET', k1, '.d[*]', None, '3'),
|
|
('JSON.NUMINCRBY', k2, '.b.*', '1', '2'),
|
|
('JSON.GET', k2, '.b.*', None, '2'),
|
|
('JSON.NUMINCRBY', k2, '.d.*', '1', '4'),
|
|
('JSON.GET', k2, '.d.*', None, '2'),
|
|
('JSON.NUMINCRBY', k3, '.b.*', '1', '2'),
|
|
('JSON.GET', k3, '.b.*', None, '"a"'),
|
|
('JSON.NUMINCRBY', k3, '.d.*', '1', '4'),
|
|
('JSON.GET', k3, '.d.*', None, '2')
|
|
]:
|
|
if incr_num is not None:
|
|
assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
|
|
else:
|
|
assert exp.encode() == client.execute_command(cmd, key, path)
|
|
|
|
def test_json_nummultby_jsonpath_and_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
|
|
client.execute_command(
|
|
'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
# JSONPath: return an array of values.
|
|
# If a value is not a number, its corresponding returned element is JSON null.
|
|
# NOTE: The expected value has accounted for the outcome of previous commands on the same key.
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMMULTBY', k1, '$.a.*', '2', '[]'),
|
|
('JSON.GET', k1, '$.a.*', None, '[]'),
|
|
('JSON.NUMMULTBY', k1, '$.b.*', '2', '[2]'),
|
|
('JSON.GET', k1, '$.b.*', None, '[2]'),
|
|
('JSON.NUMMULTBY', k1, '$.b[*]', '2', '[4]'),
|
|
('JSON.GET', k1, '$.b[*]', None, '[4]'),
|
|
('JSON.NUMMULTBY', k1, '$.d.*', '2', '[2,4,6]'),
|
|
('JSON.GET', k1, '$.d.*', None, '[2,4,6]'),
|
|
('JSON.NUMMULTBY', k1, '$.d[*]', '2', '[4,8,12]'),
|
|
('JSON.GET', k1, '$.d[*]', None, '[4,8,12]'),
|
|
('JSON.NUMMULTBY', k2, '$.a.*', '2', '[]'),
|
|
('JSON.GET', k2, '$.a.*', None, '[]'),
|
|
('JSON.NUMMULTBY', k2, '$.b.*', '2', '[2]'),
|
|
('JSON.GET', k2, '$.b.*', None, '[2]'),
|
|
('JSON.NUMMULTBY', k2, '$.d.*', '2', '[2,4,6]'),
|
|
('JSON.GET', k2, '$.d.*', None, '[2,4,6]'),
|
|
('JSON.NUMMULTBY', k3, '$.a.*', '2', '[null]'),
|
|
('JSON.GET', k3, '$.a.*', None, '["a"]'),
|
|
('JSON.NUMMULTBY', k3, '$.b.*', '2', '[null,2]'),
|
|
('JSON.GET', k3, '$.b.*', None, '["a",2]'),
|
|
('JSON.NUMMULTBY', k3, '$.c.*', '2', '[null,null]'),
|
|
('JSON.GET', k3, '$.c.*', None, '["a","b"]'),
|
|
('JSON.NUMMULTBY', k3, '$.d.*', '2', '[2,null,6]'),
|
|
('JSON.GET', k3, '$.d.*', None, '[2,"b",6]')
|
|
]:
|
|
if incr_num is not None:
|
|
assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
|
|
else:
|
|
assert exp.encode() == client.execute_command(cmd, key, path)
|
|
|
|
def test_json_nummultby_legacy_path_and_wildcard(self):
|
|
client = self.server.get_new_client()
|
|
|
|
client.execute_command(
|
|
'JSON.SET', k1, '.', '{"a":[], "b":[1], "c":[1,2], "d":[1,2,3]}')
|
|
client.execute_command(
|
|
'JSON.SET', k2, '.', '{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}}')
|
|
client.execute_command(
|
|
'JSON.SET', k3, '.', '{"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}}')
|
|
|
|
# Legacy path: return NONEXISTENT error if no value is selected
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMMULTBY', k1, '.a.*', '2', None),
|
|
('JSON.NUMMULTBY', k2, '.a.*', '2', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
cmd, key, path, incr_num)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
# Legacy path: return WRONGTYPE error if no number value is selected
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMMULTBY', k3, '.a.*', '2', None),
|
|
('JSON.NUMMULTBY', k3, '.c.*', '2', None)
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
assert exp == client.execute_command(
|
|
cmd, key, path, incr_num)
|
|
|
|
assert self.error_class.is_wrongtype_error(str(e.value))
|
|
|
|
# Legacy path: return a single value, which is the last updated value.
|
|
# NOTE: The expected value has accounted for the outcome of previous commands on the same key.
|
|
for (cmd, key, path, incr_num, exp) in [
|
|
('JSON.NUMMULTBY', k1, '.b.*', '2', '2'),
|
|
('JSON.GET', k1, '.b.*', None, '2'),
|
|
('JSON.NUMMULTBY', k1, '.b[*]', '2', '4'),
|
|
('JSON.GET', k1, '.b[*]', None, '4'),
|
|
('JSON.NUMMULTBY', k1, '.d.*', '2', '6'),
|
|
('JSON.GET', k1, '.d.*', None, '2'),
|
|
('JSON.NUMMULTBY', k1, '.d[*]', '2', '12'),
|
|
('JSON.GET', k1, '.d[*]', None, '4'),
|
|
('JSON.NUMMULTBY', k2, '.b.*', '2', '2'),
|
|
('JSON.GET', k2, '.b.*', None, '2'),
|
|
('JSON.NUMMULTBY', k2, '.d.*', '2', '6'),
|
|
('JSON.GET', k2, '.d.*', None, '2'),
|
|
('JSON.NUMMULTBY', k3, '.b.*', '2', '2'),
|
|
('JSON.GET', k3, '.b.*', None, '"a"'),
|
|
('JSON.NUMMULTBY', k3, '.d.*', '2', '6'),
|
|
('JSON.GET', k3, '.d.*', None, '2')
|
|
]:
|
|
if incr_num is not None:
|
|
assert exp.encode() == client.execute_command(cmd, key, path, incr_num)
|
|
else:
|
|
assert exp.encode() == client.execute_command(cmd, key, path)
|
|
|
|
def test_json_digest(self):
|
|
client = self.server.get_new_client()
|
|
orig_digest = client.debug_digest()
|
|
assert orig_digest != 0
|
|
client.execute_command("flushall")
|
|
new_digest = client.debug_digest()
|
|
assert int(new_digest) == 0
|
|
|
|
def test_big_dup(self):
|
|
client = self.server.get_new_client()
|
|
# This test is to test memory leak
|
|
|
|
for fle in glob.glob("data/*.json"):
|
|
with open(fle, 'r') as file:
|
|
self.data = file.read()
|
|
logging.debug("File %s is size %d" % (fle, len(self.data)))
|
|
b0 = client.info(JSON_INFO_METRICS_SECTION)[
|
|
JSON_INFO_NAMES['total_memory_bytes']]
|
|
try:
|
|
client.execute_command(
|
|
"json.set x .", self.data)
|
|
except:
|
|
pass
|
|
try:
|
|
client.execute_command("json.del x .")
|
|
except:
|
|
pass
|
|
b2 = client.info(JSON_INFO_METRICS_SECTION)[
|
|
JSON_INFO_NAMES['total_memory_bytes']]
|
|
assert b2 == b0
|
|
|
|
def test_jsonpath_filter_expression(self):
|
|
client = self.server.get_new_client()
|
|
|
|
store = '''
|
|
{
|
|
"store": {
|
|
"books": [
|
|
{
|
|
"category": "reference",
|
|
"author": "Nigel Rees",
|
|
"title": "Sayings of the Century",
|
|
"price": 8.95
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "Evelyn Waugh",
|
|
"title": "Sword of Honour",
|
|
"price": 12.99,
|
|
"movies": [
|
|
{
|
|
"title": "Sword of Honour",
|
|
"realisator": {
|
|
"first_name": "Bill",
|
|
"last_name": "Anderson"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "Herman Melville",
|
|
"title": "Moby Dick",
|
|
"isbn": "0-553-21311-3",
|
|
"price": 9
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "J. R. R. Tolkien",
|
|
"title": "The Lord of the Rings",
|
|
"isbn": "0-395-19395-8",
|
|
"price": 22.99
|
|
}
|
|
],
|
|
"bicycle": {
|
|
"color": "red",
|
|
"price": 19.95
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', store)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k2, '.', '[1,2,3,4,5]')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k3, '.', '[true,false,true,false,null,1,2,3,4]')
|
|
assert b'OK' == client.execute_command('JSON.SET', k4, '.',
|
|
'{"books": [{"price":5,"sold":true,"in-stock":true,"title":"foo"}, {"price":15,"sold":false,"title":"abc"}]}')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k5, '.', '[1,2,3,4,5,6,7,8,9]')
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$.store.books[?(@.isbn)].price',
|
|
b'[9,22.99]'),
|
|
(k1, '$.store.books[?( @.isbn )].price',
|
|
b'[9,22.99]'),
|
|
(k1, '$.store.books[?(@["isbn"])]["price"]',
|
|
b'[9,22.99]'),
|
|
(k1, '$.store.books[?(@[ "isbn" ])][ "price" ]',
|
|
b'[9,22.99]'),
|
|
(k1, '$.store.books[?(@[\'isbn\'])][\'price\']',
|
|
b'[9,22.99]'),
|
|
(k1,
|
|
'$.store.books[?(@.category == "reference")].price', b'[8.95]'),
|
|
(k1, '$.store.books[?(@.["category"] == "fiction")].price',
|
|
b'[12.99,9,22.99]'),
|
|
(k1, '$.store.books[?(@.price<1.0E1)].price',
|
|
b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@["price"]<1.0E1)]["price"]',
|
|
b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@.["price"]<1.0E1)]["price"]',
|
|
b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@[\'price\']<1.0E1)][\'price\']',
|
|
b'[8.95,9]'),
|
|
(k1,
|
|
'$.store.books[?(@.price>-1.23e1&&@.price<1.0E1)].price', b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@["price"]>-1.23e1&&@["price"]<1.0E1)]["price"]',
|
|
b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@.["price"]>-1.23e1&&@.["price"]<1.0E1)].["price"]', b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@["price"] > -1.23e1 && @["price"] < 1.0E1)]["price"]', b'[8.95,9]'),
|
|
(k1, '$.store.books[?(@.price==22.99)].title',
|
|
b'["The Lord of the Rings"]'),
|
|
(k1, '$.store.books[?(@["price"]==22.99)].title',
|
|
b'["The Lord of the Rings"]'),
|
|
(k1,
|
|
'$.store.books[?(@.price<10.0&&@.isbn)].price', b'[9]'),
|
|
(k1, '$.store.books[?(@.price<9||@.price>20)].price',
|
|
b'[8.95,22.99]'),
|
|
# precedence test
|
|
(k1, '$.store.books[?(@.price<9||@.price>10&&@.isbn)].price',
|
|
b'[8.95,22.99]'),
|
|
# precedence test
|
|
(k1,
|
|
'$.store.books[?((@.price<9||@.price>10)&&@.isbn)].price', b'[22.99]'),
|
|
# precedence test
|
|
(k1,
|
|
'$.store.books[?((@.price < 9 || @.price>10) && @.isbn)].price', b'[22.99]'),
|
|
# precedence test
|
|
(k1, '$.store.books[?((@["price"]<9||@["price"]>10)&&@["isbn"])]["price"]', b'[22.99]'),
|
|
# precedence test
|
|
(k1, '$.store.books[?((@["price"] < 9 || @["price"] > 10) && @["isbn"])]["price"]', b'[22.99]'),
|
|
(k2, '$.*.[?(@>2)]',
|
|
b'[3,4,5]'),
|
|
(k2, '$.*.[?(@ > 2)]',
|
|
b'[3,4,5]'),
|
|
(k2, '$.*[?(@>2)]',
|
|
b'[3,4,5]'),
|
|
(k2, '$[*][?(@>2)]',
|
|
b'[3,4,5]'),
|
|
(k2, '$[ * ][?( @ > 2 )]',
|
|
b'[3,4,5]'),
|
|
(k2, '$.*[?(@ == 3)]',
|
|
b'[3]'),
|
|
(k2, '$.*[?(@ != 3)]',
|
|
b'[1,2,4,5]'),
|
|
(k3, '$.*.[?(@==true)]',
|
|
b'[true,true]'),
|
|
(k3, '$[*][?(@==true)]',
|
|
b'[true,true]'),
|
|
(k3, '$[*][?(@ == true)]',
|
|
b'[true,true]'),
|
|
(k3, '$.*.[?(@>1)]',
|
|
b'[2,3,4]'),
|
|
(k3, '$.*.[?( @ > 1 ) ]',
|
|
b'[2,3,4]'),
|
|
(k3, '$[*][?(@>1)]',
|
|
b'[2,3,4]'),
|
|
(k3, '$[ * ][?( @ > 1 ) ]',
|
|
b'[2,3,4]'),
|
|
(k4, '$.books[?(@.price>1&&@.price<20&&@.in-stock)]',
|
|
b'[{"price":5,"sold":true,"in-stock":true,"title":"foo"}]'),
|
|
(k4, '$.books[?(@[\'price\']>1&&@.price<20&&@["in-stock"])]',
|
|
b'[{"price":5,"sold":true,"in-stock":true,"title":"foo"}]'),
|
|
(k4, '$.books[?((@.price>1&&@.price<20)&&(@.sold==false))]',
|
|
b'[{"price":15,"sold":false,"title":"abc"}]'),
|
|
(k4, '$["books"][?((@["price"]>1&&@["price"]<20)&&(@["sold"]==false))]',
|
|
b'[{"price":15,"sold":false,"title":"abc"}]'),
|
|
(k5, '$.*[?(@ > 7 || @ < 3)]',
|
|
b'[8,9,1,2]'), # order test
|
|
(k5, '$.*[?(@ < 3 || @ > 7)]',
|
|
b'[1,2,8,9]'), # order test
|
|
(k5, '$.*[?(@ > 3 && @ < 7)]',
|
|
b'[4,5,6]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '$.store.books[?(@.price<10.0)].price', '10.01')
|
|
for (key, path, exp) in [
|
|
(k1, '$.store.books[?(@.price<10.0)]', b'[]'),
|
|
(k1, '$.store.books[?(@.price<=10.02)].price',
|
|
b'[10.01,10.01]'),
|
|
(k1, '$.store.books[?(@.price <= 10.02)].price',
|
|
b'[10.01,10.01]'),
|
|
(k1, '$.store.books[?(@.price==10.01)].title',
|
|
b'["Sayings of the Century","Moby Dick"]'),
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
for (key, path, new_val, exp) in [
|
|
(k4, '$.books[?((@.price>1&&@.price<20)&&(@.sold==false))].price',
|
|
'13.13', b'[13.13]'),
|
|
(k4, '$.books[?((@.price > 1 && @.price < 20) && (@.sold == false))].price',
|
|
'13.13', b'[13.13]'),
|
|
(k4, '$["books"][?((@["price"]>1&&@["price"]<20)&&(@["sold"]==false))]["price"]',
|
|
'13.13', b'[13.13]'),
|
|
(k4, '$["books"][?((@["price"] > 1 && @["price"] < 20) && (@["sold"] == false))]["price"]', '13.13', b'[13.13]')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, path, new_val)
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# test delete with filter expression
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', store)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k2, '.', store)
|
|
for (key, path, exp) in [
|
|
(k1, '$.store.books[?(@.["category"] == "fiction")].price', 3),
|
|
(k2, '$.store.books[?((@["price"] < 9 || @["price"] > 10) && @["isbn"])]["price"]', 1)
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.DEL', key, path)
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
def test_jsonpath_recursive_descent(self):
|
|
client = self.server.get_new_client()
|
|
|
|
for (key, val) in [
|
|
(k1, '{"a":{"a":1}}'),
|
|
(k2, '{"a":{"a":{"a":{"a":1}}}}'),
|
|
(k3, '{"x": {}, "y": {"a":"a"}, "z": {"a":"", "b":"b"}}'),
|
|
(k4, '{"a":{"b":{"z":{"y":1}}, "c":{"z":{"y":2}}, "z":{"y":3}}}'),
|
|
(k5, '{"a":1, "b": {"e":[0,1,2]}, "c":{"e":[10,11,12]}}'),
|
|
(k6, '{"a":[1], "b": {"a": [2,3]}, "c": {"a": [4,5,6]}}')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', val)
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$..a', b'[{"a":1},1]'),
|
|
(k2, '$..a',
|
|
b'[{"a":{"a":{"a":1}}},{"a":{"a":1}},{"a":1},1]'),
|
|
(k2, '$..a..a', b'[{"a":{"a":1}},{"a":1},1]'),
|
|
(k2, '$..a..a..a', b'[{"a":1},1]'),
|
|
(k2, '$..a..a..a..a', b'[1]'),
|
|
(k2, '$..a..a..a..a..a', b'[]'),
|
|
(k3, '$..a', b'["a",""]'),
|
|
(k4, '$.a..z.y', b'[3,1,2]'),
|
|
(k4, '$.a..z.*', b'[3,1,2]'),
|
|
(k4, '$.a.*..y', b'[1,2,3]'),
|
|
(k5, '$..e.[*]', b'[0,1,2,10,11,12]'),
|
|
(k5, '$..e[1]', b'[1,11]'),
|
|
(k5, '$..e.[1]', b'[1,11]'),
|
|
(k5, '$..e.[1]', b'[1,11]'),
|
|
(k5, '$..e[0:2]', b'[0,1,10,11]'),
|
|
(k5, '$..["e"][1]', b'[1,11]'),
|
|
(k5, '$..["e"][1]', b'[1,11]'),
|
|
(k5, '$..["e"][0:2]', b'[0,1,10,11]'),
|
|
(k5, '$..[ "e" ][ 0 : 2 ]', b'[0,1,10,11]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# recursive ARRAPPEND
|
|
assert [2, 3, 4] == client.execute_command(
|
|
'JSON.ARRAPPEND', k6, '$..a', 0)
|
|
assert b'{"a":[1,0],"b":{"a":[2,3,0]},"c":{"a":[4,5,6,0]}}' == client.execute_command(
|
|
'JSON.GET', k6)
|
|
|
|
# recursive delete
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', k3, '$..a')
|
|
assert b'{"x":{},"y":{},"z":{"b":"b"}}' == client.execute_command(
|
|
'JSON.GET', k3)
|
|
|
|
# This is the only case that diverges from ReJSON v2. We deleted 4 elements while they deleted 1.
|
|
# The divergence comes from the order of deletion.
|
|
# Note that "JSON.GET k2 $..a" returns 4 elements.
|
|
assert 4 == client.execute_command(
|
|
'JSON.DEL', k2, '$..a')
|
|
assert b'{}' == client.execute_command('JSON.GET', k2)
|
|
|
|
def test_jsonpath_recursive_insert_update_delete(self):
|
|
'''
|
|
Test recursive insert, update and delete.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
data_store = '''
|
|
{
|
|
"store": {
|
|
"books": [
|
|
{
|
|
"category": "reference",
|
|
"author": "Nigel Rees",
|
|
"title": "Sayings of the Century",
|
|
"price": 8.95,
|
|
"in-stock": true
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "Evelyn Waugh",
|
|
"title": "Sword of Honour",
|
|
"price": 12.99,
|
|
"in-stock": true,
|
|
"movies": [
|
|
{
|
|
"title": "Sword of Honour - movie",
|
|
"realisator": {
|
|
"first_name": "Bill",
|
|
"last_name": "Anderson"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "Herman Melville",
|
|
"title": "Moby Dick",
|
|
"isbn": "0-553-21311-3",
|
|
"price": 9,
|
|
"in-stock": false
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "J. R. R. Tolkien",
|
|
"title": "The Lord of the Rings",
|
|
"isbn": "0-115-03266-2",
|
|
"price": 22.99,
|
|
"in-stock": true
|
|
},
|
|
{
|
|
"category": "reference",
|
|
"author": "William Jr. Strunk",
|
|
"title": "The Elements of Style",
|
|
"price": 6.99,
|
|
"in-stock": false
|
|
},
|
|
{
|
|
"category": "fiction",
|
|
"author": "Leo Tolstoy",
|
|
"title": "Anna Karenina",
|
|
"price": 22.99,
|
|
"in-stock": true
|
|
},
|
|
{
|
|
"category": "reference",
|
|
"author": "Sarah Janssen",
|
|
"title": "The World Almanac and Book of Facts 2021",
|
|
"isbn": "0-925-23305-2",
|
|
"price": 10.69,
|
|
"in-stock": false
|
|
},
|
|
{
|
|
"category": "reference",
|
|
"author": "Kate L. Turabian",
|
|
"title": "Manual for Writers of Research Papers",
|
|
"isbn": "0-675-16695-1",
|
|
"price": 8.59,
|
|
"in-stock": true
|
|
}
|
|
],
|
|
"bicycle": {
|
|
"color": "red",
|
|
"price": 19.64,
|
|
"in-stock": true
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
data_store2 = '''
|
|
{
|
|
"store": {
|
|
"title": "foo",
|
|
"bicycle": {
|
|
"title": "foo2",
|
|
"color": "red",
|
|
"price": 19.64,
|
|
"in-stock": true
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
for (key, val) in [
|
|
(store, data_store),
|
|
(store2, data_store2)
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', val)
|
|
|
|
# recursive delete
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', store2, '$..title')
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', store2, '$..title')
|
|
|
|
assert b'{"store":{"bicycle":{"color":"red","price":19.64,"in-stock":true}}}' == client.execute_command(
|
|
'JSON.GET', store2)
|
|
|
|
# recursive insert, update and delete
|
|
assert b'["Sayings of the Century","Sword of Honour","Sword of Honour - movie","Moby Dick","The Lord of the Rings","The Elements of Style","Anna Karenina","The World Almanac and Book of Facts 2021","Manual for Writers of Research Papers"]'\
|
|
== client.execute_command('JSON.GET', store, '$..title')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', store, '$..title', '"foo"')
|
|
assert b'["foo","foo","foo","foo","foo","foo","foo","foo","foo"]'\
|
|
== client.execute_command('JSON.GET', store, '$..title')
|
|
for (key, path, exp) in [
|
|
(store, '$.title', b'[]'),
|
|
(store, '$.store.title', b'[]'),
|
|
(store, '$.store.bicycle.title', b'[]'),
|
|
(store, '$.store.books[1].title', b'["foo"]'),
|
|
(store, '$.store.books[1].movies[0].title', b'["foo"]'),
|
|
(store, '$.store.books[1].movies[0].realisator.title', b'[]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert 9 == client.execute_command(
|
|
'JSON.DEL', store, '$..title')
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', store, '$..title')
|
|
assert True == client.execute_command('save')
|
|
|
|
def test_jsonpath_recursive_insert_update_delete2(self):
|
|
'''
|
|
Test recursive insert, update and delete.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
data_input = '''
|
|
{
|
|
"input": {
|
|
"a": 1,
|
|
"b": {
|
|
"e": [0,1,2]
|
|
},
|
|
"c": {
|
|
"e": [10,11,12]
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
data_input2 = '''
|
|
{
|
|
"input": {
|
|
"a": 1,
|
|
"b": {
|
|
"e": [0,1,2]
|
|
},
|
|
"c": {
|
|
"e": [10,11,12]
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
for (key, val) in [
|
|
(input, data_input),
|
|
(input2, data_input2)
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', val)
|
|
|
|
# recursive delete
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', input2, '$..e')
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', input2, '$..e')
|
|
assert b'{"input":{"a":1,"b":{},"c":{}}}' == client.execute_command(
|
|
'JSON.GET', input2)
|
|
|
|
# recursive insert, update and delete
|
|
assert b'[0,1,2,10,11,12]' == client.execute_command(
|
|
'JSON.GET', input, '$..e[*]')
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', input, '$..e[*]', '4')
|
|
assert b'[4,4,4,4,4,4]' == client.execute_command(
|
|
'JSON.GET', input, '$..e[*]')
|
|
assert b'[4,4,4]' == client.execute_command(
|
|
'JSON.GET', input, '$.input.b.e[*]')
|
|
for (key, path, exp) in [
|
|
(input, '$.e', b'[]'),
|
|
(input, '$.input.e', b'[]'),
|
|
(input, '$.input.a.e', b'[]'),
|
|
(input, '$.input.b.e[*]', b'[4,4,4]'),
|
|
(input, '$.input.c.e[*]', b'[4,4,4]'),
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', input, '$..e')
|
|
assert 0 == client.execute_command(
|
|
'JSON.DEL', input, '$..e')
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', input, '$..e')
|
|
assert True == client.execute_command('save')
|
|
|
|
def test_jsonpath_compatibility_invalidArrayIndex(self):
|
|
client = self.server.get_new_client()
|
|
|
|
# Array index is not integer
|
|
for (key, path) in [
|
|
(wikipedia, '.phoneNumbers[]'),
|
|
(wikipedia, '.phoneNumbers[x]'),
|
|
(wikipedia, '$.phoneNumbers[]'),
|
|
(wikipedia, '$.phoneNumbers[x]')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.GET', key, path)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_jsonpath_compatibility_unquotedMemberName(self):
|
|
client = self.server.get_new_client()
|
|
|
|
# Unquoted member name can contain any symbol except terminator characters
|
|
json = '''{
|
|
"%x22key%x22":1, "+2":2, "-3":3, "/4":4,
|
|
")5":5, "6)":6, "(7":7, "8(":8,
|
|
"]9":9, "10]":10, "[11":11, "12[":12,
|
|
">13":13, "14>":14, "<15":15, "16<":16,
|
|
"=17":17, "18=":18, "!19":19, "20!":20,
|
|
"21.21":21
|
|
}'''
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', json)
|
|
|
|
test_cases = [
|
|
(k1, '$.%x22key%x22', b'[1]'),
|
|
(k1, '$.+2', b'[2]'),
|
|
(k1, '$.-3', b'[3]'),
|
|
(k1, '$./4', b'[4]'),
|
|
# The following should return empty array because unquoted member name cannot contain terminator characters.
|
|
(k1, '$.6)', b'[]'),
|
|
(k1, '$.8(', b'[]'),
|
|
(k1, '$.10]', b'[]'),
|
|
# Bracketed/Quoted member name should work
|
|
(k1, '$["6)"]', b'[6]'),
|
|
(k1, '$["8("]', b'[8]'),
|
|
(k1, '$["10]"]', b'[10]'),
|
|
(k1, '$["12["]', b'[12]'),
|
|
(k1, '$["14>"]', b'[14]'),
|
|
(k1, '$["16<"]', b'[16]'),
|
|
(k1, '$["18="]', b'[18]'),
|
|
(k1, '$["20!"]', b'[20]'),
|
|
(k1, '$["21.21"]', b'[21]'),
|
|
(k1, '$.12[', b'[]'),
|
|
(k1, '$.14>', b'[]'),
|
|
(k1, '$.16<', b'[]'),
|
|
(k1, '$.18=', b'[]'),
|
|
(k1, '$.20!', b'[]'),
|
|
(k1, '$.21.21', b'[]'),
|
|
]
|
|
|
|
for (key, path, exp) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# Unquoted object member cannot start with a character that is a member name terminator.
|
|
for (key, path) in [
|
|
(k1, '$.)5'),
|
|
(k1, '$.]7'),
|
|
(k1, '$.]9'),
|
|
(k1, '$.[11'),
|
|
(k1, '$.>13'),
|
|
(k1, '$.<15'),
|
|
(k1, '$.=17'),
|
|
(k1, '$.!19')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.GET', key, path)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_write_commands_duplicate_values(self):
|
|
'''
|
|
Test all WRITE JSON commands for the situation that if the json path results in multiple values
|
|
with duplicates, the write operation should apply to all unique values once.
|
|
'''
|
|
client = self.server.get_new_client()
|
|
|
|
for (key, val) in [
|
|
(k1, '[0,1,2,3,4,5,6,7,8,9]'),
|
|
(k2, '[0,1,2,3,4,5,6,7,8,9]'),
|
|
(k3, '[0,1,2,3,4,5,6,7,8,9]'),
|
|
(k4, '["0","1","2","3","4","5","6","7","8","9"]'),
|
|
(k5, '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
|
|
(k6, '[true,true,true,true,true]'),
|
|
(k7, '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
|
|
(k8, '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
|
|
(k9, '[[0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]'),
|
|
(k10, '[[0,1,2,3,4],[0,1,2,3,4],[0,1,2,3,4]]'),
|
|
(k11, '[0,1,2,3,4,5,6,7,8,9]'),
|
|
(k12, '[0,1,2,3,4,5,6,7,8,9]')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', val)
|
|
|
|
# NUMINCRBY
|
|
assert b'[10]' == client.execute_command(
|
|
'JSON.NUMINCRBY', k1, '$[0,0,0,0,0]', 10)
|
|
assert b'[10,1,2,3,4,5,6,7,8,9]' == client.execute_command(
|
|
'JSON.GET', k1)
|
|
|
|
# NUMMULTBY
|
|
assert b'[18]' == client.execute_command(
|
|
'JSON.NUMMULTBY', k2, '$[9,9,9,9,9]', 2)
|
|
assert b'[0,1,2,3,4,5,6,7,8,18]' == client.execute_command(
|
|
'JSON.GET', k2)
|
|
|
|
# CLEAR
|
|
assert 1 == client.execute_command(
|
|
'JSON.CLEAR', k5, '$[0,0,0,0,0]')
|
|
assert b'[[],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
|
|
'JSON.GET', k5)
|
|
|
|
# TOGGLE
|
|
assert [0] == client.execute_command(
|
|
'JSON.TOGGLE', k6, '$[0,0,0,0]')
|
|
assert b'[false,true,true,true,true]' == client.execute_command(
|
|
'JSON.GET', k6)
|
|
|
|
# STRAPPEND
|
|
assert [4] == client.execute_command(
|
|
'JSON.STRAPPEND', k4, '$[0,0,0,0,0]', '"foo"')
|
|
assert b'["0foo","1","2","3","4","5","6","7","8","9"]' == client.execute_command(
|
|
'JSON.GET', k4)
|
|
|
|
# ARRAPPEND
|
|
assert [3] == client.execute_command(
|
|
'JSON.ARRAPPEND', k7, '$[0,0,0,0,0]', 8, 9)
|
|
assert b'[[0,8,9],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
|
|
'JSON.GET', k7)
|
|
|
|
# ARRINSERT
|
|
assert [2] == client.execute_command(
|
|
'JSON.ARRINSERT', k8, '$[0,0,0,0,0]', 0, 9)
|
|
assert b'[[9,0],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
|
|
'JSON.GET', k8)
|
|
|
|
# ARRPOP
|
|
assert [b"0"] == client.execute_command(
|
|
'JSON.ARRPOP', k9, '$[0,0,0,0,0]')
|
|
assert b'[[],[1],[2],[3],[4],[5],[6],[7],[8],[9]]' == client.execute_command(
|
|
'JSON.GET', k9)
|
|
|
|
# ARRTRIM
|
|
assert [2] == client.execute_command(
|
|
'JSON.ARRTRIM', k10, '$[0,0,0,0,0]', 0, 1)
|
|
assert b'[[0,1],[0,1,2,3,4],[0,1,2,3,4]]' == client.execute_command(
|
|
'JSON.GET', k10)
|
|
|
|
# DEL
|
|
assert 1 == client.execute_command(
|
|
'JSON.DEL', k3, '$[0,0,0,0,0]')
|
|
assert b'[1,2,3,4,5,6,7,8,9]' == client.execute_command(
|
|
'JSON.GET', k3)
|
|
|
|
# DEL: delete arbitrary elements with duplicates
|
|
assert 5 == client.execute_command(
|
|
'JSON.DEL', k12, '$[1,4,7,0,0,3,3]')
|
|
assert b'[2,5,6,8,9]' == client.execute_command(
|
|
'JSON.GET', k12)
|
|
|
|
# SET
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k11, '$[0,0,0,0,0]', 9)
|
|
assert b'[9,1,2,3,4,5,6,7,8,9]' == client.execute_command(
|
|
'JSON.GET', k11)
|
|
|
|
def test_jsonpath_compatibility_filterOnObject_and_stringComparison(self):
|
|
client = self.server.get_new_client()
|
|
|
|
json = '''
|
|
{
|
|
"key for key" : "key inside here",
|
|
"an object" : {
|
|
"weight" : 300,
|
|
"a value" : 300,
|
|
"my key" : "key inside here"
|
|
},
|
|
"another object" : {
|
|
"weight" : 400,
|
|
"a value" : 400,
|
|
"my key" : "key inside there"
|
|
},
|
|
"objects": [
|
|
{
|
|
"weight" : 100,
|
|
"a value" : 100,
|
|
"my key" : "key inside here"
|
|
},
|
|
{
|
|
"weight" : 200,
|
|
"a value" : 200,
|
|
"my key" : "key inside there"
|
|
},
|
|
{
|
|
"weight" : 300,
|
|
"a value" : 300,
|
|
"my key" : "key inside here"
|
|
},
|
|
{
|
|
"weight" : 400,
|
|
"a value" : 400,
|
|
"my key" : "key inside there"
|
|
}
|
|
]
|
|
}
|
|
'''
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '.', json)
|
|
|
|
for (key, path, exp) in [
|
|
(k1, '$["an object"].[?(@.weight > 200)].["a value"]',
|
|
b'[300]'),
|
|
(k1, '$["an object"].[?(@.weight == 300)].["a value"]',
|
|
b'[300]'),
|
|
(k1, '$["an object"].[?(@.weight > 300)].["a value"]',
|
|
b'[]'),
|
|
(k1, '$["another object"].[?(@["a value"] > 200)].weight',
|
|
b'[400]'),
|
|
(k1, '$["another object"].[?(@.["a value"] > 200)].weight',
|
|
b'[400]'),
|
|
(k1, '$["another object"].[?(@.["my key"] == "key inside there")].weight', b'[400]'),
|
|
(k1, '$["objects"].[?(@.weight > 200)].["a value"]',
|
|
b'[300,400]'),
|
|
(k1, '$["objects"].[?(@.["my key"] == "key inside there")].weight',
|
|
b'[200,400]'),
|
|
(k1, '$["objects"].[?(@.["my key"] != "key inside there")].weight',
|
|
b'[100,300]'),
|
|
(k1, '$["objects"].[?(@.["my key"] == "key inside here")].weight',
|
|
b'[100,300]'),
|
|
(k1, '$["objects"].[?(@.["my key"] != "key inside here")].weight',
|
|
b'[200,400]'),
|
|
(k1, '$["objects"].[?(@.["my key"] <= "key inside here")].weight',
|
|
b'[100,300]'),
|
|
(k1, '$["objects"].[?(@.["my key"] >= "key inside here")].weight',
|
|
b'[100,200,300,400]'),
|
|
(k1, '$["objects"].[?(@.["my key"] < "key inside herf")].weight',
|
|
b'[100,300]'),
|
|
(k1, '$["objects"].[?(@.["my key"] > "key insidd here")].weight',
|
|
b'[100,200,300,400]'),
|
|
(k1, '$["key for key"].[?(@.["a value"] > 200)]',
|
|
b'[]')
|
|
]:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
def test_jsonpath_compatibility_union_of_object_members(self):
|
|
client = self.server.get_new_client()
|
|
|
|
test_cases = [
|
|
(wikipedia, '$.["firstName","lastName"]',
|
|
b'["John","Smith"]'),
|
|
(organism,
|
|
'$.animals["2","2"].mammals..weight', b'[]'),
|
|
(organism, '$.animals["2","Junk"]',
|
|
b'[]'),
|
|
(organism, '$.animals["Junk","Junk"]',
|
|
b'[]'),
|
|
# test unique values in recursive descent
|
|
(organism, '$..[?(@.weight>400)].name',
|
|
b'["Redwood","Horse"]'),
|
|
(organism, '$..[?(@.weight>=60&&@.name=="Chimpanzee")].name',
|
|
b'["Chimpanzee"]'),
|
|
(wikipedia, '$.[ "firstName", "lastName" ]',
|
|
b'["John","Smith"]'),
|
|
(wikipedia, '$.address.[ "street", "city", "state", "zipcode" ]',
|
|
b'["21 2nd Street","New York","NY","10021-3100"]'),
|
|
]
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', organism, '.', DATA_ORGANISM)
|
|
|
|
for (key, path, exp) in test_cases:
|
|
assert exp == self.client.execute_command('JSON.GET', key, path)
|
|
|
|
def test_jsonpath_malformed_path(self):
|
|
client = self.server.get_new_client()
|
|
|
|
for (key, val) in [
|
|
(k1, '{"store":{"book":[{"price":5,"sold":true,"in-stock":true,"title":"foo","author":"me","isbn":"978-3-16-148410-0"}]}}'),
|
|
(k2, '[1,2,3,4,5,6,7,8,9,10]')
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '.', val)
|
|
|
|
test_cases = [
|
|
(k1, '$[0:2]$[0:1]$[0:2]$[0:2]$[0<2065>:2]$[0:2]', b'[]'),
|
|
(k1, '$[0,1]', b'[]'),
|
|
(k2, '$.[\"a\"].[\"b\"].[\"c\"]', b'[]'),
|
|
(k1, '$a.b.c.d', b'[]'),
|
|
(k2, '$a$b$c$d', b'[]'),
|
|
]
|
|
|
|
for (key, path, exp) in test_cases:
|
|
assert exp == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
for (key, path) in [
|
|
(k1, '.[0:2].[0:1].[0:2].[0:2].[0<2065>:2].[0:2]'),
|
|
(k1, '.[0:2]$[0:1]$[0:2]$[0:2]$[0<2065>:2]$[0:2]')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.GET', key, path)
|
|
assert self.error_class.is_wrongtype_error(
|
|
str(e.value)) or self.error_class.is_nonexistent_error(str(e.value))
|
|
|
|
for (key, path) in [
|
|
(k1, '&&$.store..price'),
|
|
(k1, '!$.store..price'),
|
|
(k1, '=$.store..price'),
|
|
(k1, '=.store..price'),
|
|
(k1, '||.store..price')
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command('JSON.GET', key, path)
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', key, path, '0')
|
|
assert self.error_class.is_syntax_error(str(e.value))
|
|
|
|
def test_v2_path_limit_recursive_descent(self):
|
|
client = self.server.get_new_client()
|
|
|
|
depth_limit = 10
|
|
client.execute_command(
|
|
'config set json.max-path-limit ' + str(depth_limit))
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '$', '{"a":0}')
|
|
|
|
# repeatedly increasing path depth by 1 till the limit is reached.
|
|
for _ in range(0, depth_limit-1):
|
|
client.execute_command(
|
|
'JSON.SET', k1, '$..a', '{"a":0}')
|
|
# verify the path depth reaches the limit
|
|
assert depth_limit == client.execute_command(
|
|
'JSON.DEBUG', 'DEPTH', k1)
|
|
|
|
# one more increase should exceed the path limit
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', k1, '$..a', '{"a":0}')
|
|
assert str(e.value).startswith("LIMIT")
|
|
|
|
def test_v2_path_limit_insert_member(self):
|
|
client = self.server.get_new_client()
|
|
|
|
depth_limit = 3
|
|
client.config_set('json.max-path-limit', depth_limit)
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '$', '{"a":{"a":{"a":0}}}')
|
|
assert depth_limit == client.execute_command(
|
|
'JSON.DEBUG DEPTH', k1)
|
|
|
|
# insert a nesting object
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'JSON.SET', k1, '$.a.a.b', '{"b":0}')
|
|
assert str(e.value).startswith("LIMIT")
|
|
|
|
# increase limit by 1
|
|
client.config_set(
|
|
'json.max-path-limit', depth_limit + 1)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', k1, '$.a.a.b', '{"b":0}')
|
|
|
|
def test_debug_command_getkeys_api(self):
|
|
client = self.server.get_new_client()
|
|
|
|
for (subcmd, res) in [
|
|
('memory k1', [b'k1']),
|
|
('fields k1', [b'k1']),
|
|
('depth k1', [b'k1']),
|
|
('memory k1', [b'k1'])
|
|
]:
|
|
assert res == client.execute_command(
|
|
'command getkeys json.debug ' + subcmd)
|
|
|
|
for subcmd in [
|
|
'',
|
|
'memory',
|
|
'mem',
|
|
'memo'
|
|
'fields'
|
|
'depth'
|
|
'help',
|
|
'max-depth-key',
|
|
'max-size-key'
|
|
]:
|
|
with pytest.raises(ResponseError) as e:
|
|
client.execute_command(
|
|
'command getkeys json.debug ' + subcmd)
|
|
assert self.error_class.is_wrong_number_of_arguments_error(str(e.value)) or \
|
|
str(e.value).lower().find("invalid command") >= 0 or \
|
|
str(e.value).lower().find(
|
|
"the command has no key arguments") >= 0
|
|
|
|
def test_debug_command_depth(self):
|
|
client = self.server.get_new_client()
|
|
for (key, val, depth) in [
|
|
(k1, '1', 0),
|
|
(k2, '"a"', 0),
|
|
(k3, 'null', 0),
|
|
(k4, 'true', 0),
|
|
(k5, '{}', 0),
|
|
(k6, '{"a":0}', 1),
|
|
(k7, '{"a":{"a":0}}', 2),
|
|
(k7, '{"a":{"a":{"a":0}}}', 3),
|
|
(k8, '[]', 0),
|
|
(k9, '[0]', 1),
|
|
(k10, '[[0]]', 2),
|
|
(k11, '[[0],[[0]]]', 3),
|
|
]:
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, '$', val)
|
|
assert depth == client.execute_command(
|
|
'JSON.DEBUG DEPTH', key)
|
|
|
|
# what if the key does not exist?
|
|
assert None == client.execute_command(
|
|
'JSON.DEBUG DEPTH', 'foobar')
|
|
|
|
def test_insert_update_delete_mode(self):
|
|
client = self.server.get_new_client()
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', organism, '.', DATA_ORGANISM)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', organism2, '.', DATA_ORGANISM)
|
|
|
|
# delete
|
|
key = organism
|
|
path = '$.animals[*].mammals[*].primates[*].apes[?(@.weight<400)].weight'
|
|
assert b'[130,300]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', key, path)
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# insert
|
|
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, path, 25)
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
# update
|
|
key = organism2
|
|
assert b'[130,300]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert b'OK' == client.execute_command(
|
|
'JSON.SET', key, path, 25)
|
|
assert b'[25,25]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
assert 2 == client.execute_command(
|
|
'JSON.DEL', key, path)
|
|
assert b'[]' == client.execute_command(
|
|
'JSON.GET', key, path)
|
|
|
|
def test_json_arity_per_command(self):
|
|
client = self.server.get_new_client()
|
|
|
|
# These commands should only get the single key
|
|
cmd_arity = [('SET', -4), ('GET', -2), ('MGET', -3), ('DEL', -2), ('FORGET', -2), ('NUMINCRBY', 4),
|
|
('NUMMULTBY', 4), ('STRLEN', -2), ('STRAPPEND', -
|
|
3), ('TOGGLE', -2), ('OBJLEN', -2), ('OBJKEYS', -2),
|
|
('ARRLEN', -2), ('ARRAPPEND', -4), ('ARRPOP', -
|
|
2), ('ARRINSERT', -5), ('ARRTRIM', 5), ('CLEAR', -2),
|
|
('ARRINDEX', -4), ('TYPE', -2), ('RESP', -2), ('DEBUG', -2)]
|
|
|
|
for cmd, arity in cmd_arity:
|
|
assert arity == client.execute_command('COMMAND', 'INFO', f'JSON.{cmd}')[
|
|
f'JSON.{cmd}']['arity']
|
|
|
|
cmd_arity = [('MEMORY', -3), ('FIELDS', -3), ('DEPTH', 3), ('HELP', 2),
|
|
('MAX-DEPTH-KEY', 2), ('MAX-SIZE-KEY',
|
|
2), ('KEYTABLE-CHECK', 2), ('KEYTABLE-CORRUPT', 3),
|
|
('KEYTABLE-DISTRIBUTION', 3)]
|
|
subcmd_dict = {f'JSON.DEBUG|{cmd}': arity for cmd, arity in cmd_arity}
|
|
|
|
output = client.execute_command(
|
|
'COMMAND', 'INFO', f'JSON.DEBUG')[f'JSON.DEBUG']['subcommands']
|
|
assert len(output) == len(subcmd_dict)
|
|
|
|
for i in range(len(output)):
|
|
assert subcmd_dict[output[i][0].decode('ascii')] == output[i][1]
|