added versionstamp type to python tuple layer and updated bindingtester to test it

This commit is contained in:
Alec Grieser 2017-09-28 12:03:40 -07:00
parent 4b21da1cd6
commit bd6dabacdb
10 changed files with 275 additions and 59 deletions

View File

@ -171,6 +171,8 @@ class TestRunner(object):
if self.args.no_threads and self.args.concurrency > 1: if self.args.no_threads and self.args.concurrency > 1:
raise Exception('Not all testers support concurrency') raise Exception('Not all testers support concurrency')
# Test types should be intersection of all tester supported types
self.args.types = reduce(lambda t1, t2: filter(t1.__contains__, t2), map(lambda tester: tester.types, self.testers))
def print_test(self): def print_test(self):
test_instructions = self._generate_test() test_instructions = self._generate_test()

View File

@ -21,15 +21,18 @@
import os import os
MAX_API_VERSION = 500 MAX_API_VERSION = 500
COMMON_TYPES = [ 'null', 'bytes', 'string', 'int', 'uuid', 'bool', 'float', 'double', 'tuple' ]
ALL_TYPES = COMMON_TYPES + [ 'versionstamp' ]
class Tester: class Tester:
def __init__(self, name, cmd, max_int_bits=64, min_api_version=0, max_api_version=MAX_API_VERSION, threads_enabled=True): def __init__(self, name, cmd, max_int_bits=64, min_api_version=0, max_api_version=MAX_API_VERSION, threads_enabled=True, types=COMMON_TYPES):
self.name = name self.name = name
self.cmd = cmd self.cmd = cmd
self.max_int_bits = max_int_bits self.max_int_bits = max_int_bits
self.min_api_version = min_api_version self.min_api_version = min_api_version
self.max_api_version = max_api_version self.max_api_version = max_api_version
self.threads_enabled = threads_enabled self.threads_enabled = threads_enabled
self.types = types
def supports_api_version(self, api_version): def supports_api_version(self, api_version):
return api_version >= self.min_api_version and api_version <= self.max_api_version return api_version >= self.min_api_version and api_version <= self.max_api_version
@ -54,8 +57,8 @@ _java_completable_cmd = 'java -ea -cp %s:%s com.apple.cie.foundationdb.test.' %
# We could set min_api_version lower on some of these if the testers were updated to support them # We could set min_api_version lower on some of these if the testers were updated to support them
testers = { testers = {
'python' : Tester('python', 'python ' + _absolute_path('python/tests/tester.py'), 2040, 23, MAX_API_VERSION), 'python' : Tester('python', 'python ' + _absolute_path('python/tests/tester.py'), 2040, 23, MAX_API_VERSION, types=ALL_TYPES),
'python3' : Tester('python3', 'python3 ' + _absolute_path('python/tests/tester.py'), 2040, 23, MAX_API_VERSION), 'python3' : Tester('python3', 'python3 ' + _absolute_path('python/tests/tester.py'), 2040, 23, MAX_API_VERSION, types=ALL_TYPES),
'node' : Tester('node', _absolute_path('nodejs/tests/tester.js'), 53, 500, MAX_API_VERSION), 'node' : Tester('node', _absolute_path('nodejs/tests/tester.js'), 53, 500, MAX_API_VERSION),
'streamline' : Tester('streamline', _absolute_path('nodejs/tests/streamline_tester._js'), 53, 500, MAX_API_VERSION), 'streamline' : Tester('streamline', _absolute_path('nodejs/tests/streamline_tester._js'), 53, 500, MAX_API_VERSION),
'ruby' : Tester('ruby', _absolute_path('ruby/tests/tester.rb'), 64, 23, MAX_API_VERSION), 'ruby' : Tester('ruby', _absolute_path('ruby/tests/tester.rb'), 64, 23, MAX_API_VERSION),

View File

@ -293,6 +293,20 @@ TUPLE_PACK
stack and packs them as the tuple [item0,item1,...,itemN], and then pushes stack and packs them as the tuple [item0,item1,...,itemN], and then pushes
this single packed value onto the stack. this single packed value onto the stack.
TUPLE_PACK_WITH_VERSIONSTAMP
Pops the top item off of the stack as a byte string prefix. Pops the next item
off of the stack as N. Pops the next N items off of the stack and packs them
as the tuple [item0,item1,...,itemN], with the provided prefix and tries to
append insert the position of the first incomplete versionstamp as if the byte
string was to be used as a key in a SET_VERSIONSTAMP_KEY atomic op. If there
are no incomplete versionstamp instances, then this pushes the literal byte
string 'ERROR: NONE' to the stack. If there is more than one, then this pushes
the literal byte string 'ERROR: MULTIPLE'. If there is exactly one, then it pushes
the literal byte string 'OK' and then pushes the packed tuple. (Languages that
do not contain a 'Versionstamp' tuple-type do not have to implement this
operation.)
TUPLE_UNPACK TUPLE_UNPACK
Pops the top item off of the stack as PACKED, and then unpacks PACKED into a Pops the top item off of the stack as PACKED, and then unpacks PACKED into a

View File

@ -54,7 +54,7 @@ class ApiTest(Test):
self.generated_keys = [] self.generated_keys = []
self.outstanding_ops = [] self.outstanding_ops = []
self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version) self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version, args.types)
def add_stack_items(self, num): def add_stack_items(self, num):
self.stack_size += num self.stack_size += num
@ -148,6 +148,8 @@ class ApiTest(Test):
versions = ['GET_READ_VERSION', 'SET_READ_VERSION', 'GET_COMMITTED_VERSION'] versions = ['GET_READ_VERSION', 'SET_READ_VERSION', 'GET_COMMITTED_VERSION']
snapshot_versions = ['GET_READ_VERSION_SNAPSHOT'] snapshot_versions = ['GET_READ_VERSION_SNAPSHOT']
tuples = ['TUPLE_PACK', 'TUPLE_UNPACK', 'TUPLE_RANGE', 'TUPLE_SORT', 'SUB', 'ENCODE_FLOAT', 'ENCODE_DOUBLE', 'DECODE_DOUBLE', 'DECODE_FLOAT'] tuples = ['TUPLE_PACK', 'TUPLE_UNPACK', 'TUPLE_RANGE', 'TUPLE_SORT', 'SUB', 'ENCODE_FLOAT', 'ENCODE_DOUBLE', 'DECODE_DOUBLE', 'DECODE_FLOAT']
if 'versionstamp' in args.types:
tuples.append('TUPLE_PACK_WITH_VERSIONSTAMP')
resets = ['ON_ERROR', 'RESET', 'CANCEL'] resets = ['ON_ERROR', 'RESET', 'CANCEL']
read_conflicts = ['READ_CONFLICT_RANGE', 'READ_CONFLICT_KEY'] read_conflicts = ['READ_CONFLICT_RANGE', 'READ_CONFLICT_KEY']
write_conflicts = ['WRITE_CONFLICT_RANGE', 'WRITE_CONFLICT_KEY', 'DISABLE_WRITE_CONFLICT'] write_conflicts = ['WRITE_CONFLICT_RANGE', 'WRITE_CONFLICT_KEY', 'DISABLE_WRITE_CONFLICT']
@ -348,12 +350,12 @@ class ApiTest(Test):
key1 = self.versionstamped_values.pack((rand_str1,)) key1 = self.versionstamped_values.pack((rand_str1,))
split = random.randint(0, 70) split = random.randint(0, 70)
rand_str2 = self.random.random_string(20+split) + 'XXXXXXXXXX' + self.random.random_string(70-split) rand_str2 = self.random.random_string(20+split) + fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION + self.random.random_string(70-split)
key2 = self.versionstamped_keys.pack() + rand_str2 key2 = self.versionstamped_keys.pack() + rand_str2
index = key2.find('XXXXXXXXXX') index = key2.find(fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION)
key2 += chr(index%256)+chr(index/256) key2 += chr(index%256)+chr(index/256)
instructions.push_args(u'SET_VERSIONSTAMPED_VALUE', key1, 'XXXXXXXXXX' + rand_str2) instructions.push_args(u'SET_VERSIONSTAMPED_VALUE', key1, fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION + rand_str2)
instructions.append('ATOMIC_OP') instructions.append('ATOMIC_OP')
instructions.push_args(u'SET_VERSIONSTAMPED_KEY', key2, rand_str1) instructions.push_args(u'SET_VERSIONSTAMPED_KEY', key2, rand_str1)
@ -425,6 +427,29 @@ class ApiTest(Test):
else: else:
self.add_strings(2) self.add_strings(2)
elif op == 'TUPLE_PACK_WITH_VERSIONSTAMP':
tup = self.random.random_tuple(10, incomplete_versionstamps=True)
instructions.push_args(self.versionstamped_keys.pack(), len(tup), *tup)
instructions.append(op)
self.add_strings(1)
version_key = fdb.tuple.pack(tup, prefix=self.versionstamped_keys.pack())
first_incomplete = version_key.find(fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION)
second_incomplete = -1 if first_incomplete < 0 else version_key.find(fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION, first_incomplete + 1)
# If there is exactly one incomplete versionstamp, perform the versionstamped key operation.
if first_incomplete >= 0 and second_incomplete < 0:
rand_str = self.random.random_string(100)
instructions.push_args(rand_str)
test_util.to_front(instructions, 1)
instructions.push_args(u'SET_VERSIONSTAMPED_KEY')
instructions.append('ATOMIC_OP')
version_value_key = self.versionstamped_values.pack((rand_str,))
instructions.push_args(u'SET_VERSIONSTAMPED_VALUE', version_value_key, fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION + fdb.tuple.pack(tup))
instructions.append('ATOMIC_OP')
elif op == 'TUPLE_UNPACK': elif op == 'TUPLE_UNPACK':
tup = self.random.random_tuple(10) tup = self.random.random_tuple(10)
instructions.push_args(len(tup), *tup) instructions.push_args(len(tup), *tup)
@ -511,7 +536,7 @@ class ApiTest(Test):
for k,v in tr.get_range(begin_key, self.versionstamped_values.range().stop, limit=limit): for k,v in tr.get_range(begin_key, self.versionstamped_values.range().stop, limit=limit):
next_begin = k + '\x00' next_begin = k + '\x00'
tup = fdb.tuple.unpack(k) tup = fdb.tuple.unpack(k)
key = self.versionstamped_keys.pack() + v[10:].replace('XXXXXXXXXX', v[:10], 1) key = self.versionstamped_keys.pack() + v[10:].replace(fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION, v[:10], 1)
if tr[key] != tup[-1]: if tr[key] != tup[-1]:
incorrect_versionstamps += 1 incorrect_versionstamps += 1

View File

@ -65,7 +65,7 @@ class DirectoryTest(Test):
def setup(self, args): def setup(self, args):
self.dir_index = 0 self.dir_index = 0
self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version) self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version, args.types)
def generate(self, args, thread_number): def generate(self, args, thread_number):
instructions = InstructionSet() instructions = InstructionSet()

View File

@ -38,7 +38,7 @@ class DirectoryHcaTest(Test):
self.next_path = 1 self.next_path = 1
def setup(self, args): def setup(self, args):
self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version) self.random = test_util.RandomGenerator(args.max_int_bits, args.api_version, args.types)
self.transactions = ['tr%d' % i for i in range(3)] # SOMEDAY: parameterize this number? self.transactions = ['tr%d' % i for i in range(3)] # SOMEDAY: parameterize this number?
self.barrier_num = 0 self.barrier_num = 0

View File

@ -29,11 +29,13 @@ import fdb.tuple
from bindingtester import util from bindingtester import util
from bindingtester import FDB_API_VERSION from bindingtester import FDB_API_VERSION
from bindingtester.known_testers import COMMON_TYPES
class RandomGenerator(object): class RandomGenerator(object):
def __init__(self, max_int_bits=64, api_version=FDB_API_VERSION): def __init__(self, max_int_bits=64, api_version=FDB_API_VERSION, types=COMMON_TYPES):
self.max_int_bits = max_int_bits self.max_int_bits = max_int_bits
self.api_version = api_version self.api_version = api_version
self.types = types
def random_unicode_str(self, length): def random_unicode_str(self, length):
return u''.join(self.random_unicode_char() for i in range(0, length)) return u''.join(self.random_unicode_char() for i in range(0, length))
@ -59,38 +61,45 @@ class RandomGenerator(object):
mantissa = random.random() mantissa = random.random()
return sign * math.pow(2, exponent) * mantissa return sign * math.pow(2, exponent) * mantissa
def random_tuple(self, max_size): def random_tuple(self, max_size, incomplete_versionstamps=False):
size = random.randint(1, max_size) size = random.randint(1, max_size)
tup = [] tup = []
for i in range(size): for i in range(size):
choice = random.randint(0, 8) choice = random.choice(self.types)
if choice == 0: if choice == 'int':
tup.append(self.random_int()) tup.append(self.random_int())
elif choice == 1: elif choice == 'null':
tup.append(None) tup.append(None)
elif choice == 2: elif choice == 'bytes':
tup.append(self.random_string(random.randint(0, 100))) tup.append(self.random_string(random.randint(0, 100)))
elif choice == 3: elif choice == 'string':
tup.append(self.random_unicode_str(random.randint(0, 100))) tup.append(self.random_unicode_str(random.randint(0, 100)))
elif choice == 4: elif choice == 'uuid':
tup.append(uuid.uuid4()) tup.append(uuid.uuid4())
elif choice == 5: elif choice == 'bool':
b = random.random() < 0.5 b = random.random() < 0.5
if self.api_version < 500: if self.api_version < 500:
tup.append(int(b)) tup.append(int(b))
else: else:
tup.append(b) tup.append(b)
elif choice == 6: elif choice == 'double':
tup.append(fdb.tuple.SingleFloat(self.random_float(8))) tup.append(fdb.tuple.SingleFloat(self.random_float(8)))
elif choice == 7: elif choice == 'float':
tup.append(self.random_float(11)) tup.append(self.random_float(11))
elif choice == 8: elif choice == 'tuple':
length = random.randint(0, max_size - size) length = random.randint(0, max_size - size)
if length == 0: if length == 0:
tup.append(()) tup.append(())
else: else:
tup.append(self.random_tuple(length)) tup.append(self.random_tuple(length))
elif choice == 'versionstamp':
if incomplete_versionstamps and random.random() < 0.5:
global_version = fdb.tuple.Versionstamp._UNSET_GLOBAL_VERSION
else:
global_version = self.random_string(10)
local_version = random.randint(0, 0xffff)
tup.append(fdb.tuple.Versionstamp(global_version, local_version))
else: else:
assert false assert false

View File

@ -25,7 +25,7 @@ import fdb.tuple
class Subspace (object): class Subspace (object):
def __init__(self, prefixTuple=tuple(), rawPrefix=b''): def __init__(self, prefixTuple=tuple(), rawPrefix=b''):
self.rawPrefix = rawPrefix + fdb.tuple.pack(prefixTuple) self.rawPrefix = fdb.tuple.pack(prefixTuple, prefix=rawPrefix)
def __repr__(self): def __repr__(self):
return 'Subspace(rawPrefix=' + repr(self.rawPrefix) + ')' return 'Subspace(rawPrefix=' + repr(self.rawPrefix) + ')'
@ -37,13 +37,13 @@ class Subspace (object):
return self.rawPrefix return self.rawPrefix
def pack(self, t=tuple()): def pack(self, t=tuple()):
return self.rawPrefix + fdb.tuple.pack(t) return fdb.tuple.pack(t, prefix=self.rawPrefix)
def unpack(self, key): def unpack(self, key):
if not self.contains(key): if not self.contains(key):
raise ValueError('Cannot unpack key that is not in subspace.') raise ValueError('Cannot unpack key that is not in subspace.')
return fdb.tuple.unpack(key[len(self.rawPrefix):]) return fdb.tuple.unpack(key, prefix_len=len(self.rawPrefix))
def range(self, t=tuple()): def range(self, t=tuple()):
p = fdb.tuple.range(t) p = fdb.tuple.range(t)

View File

@ -29,18 +29,19 @@ import fdb
_size_limits = tuple( (1 << (i*8))-1 for i in range(9) ) _size_limits = tuple( (1 << (i*8))-1 for i in range(9) )
# Define type codes: # Define type codes:
NULL_CODE = 0x00 NULL_CODE = 0x00
BYTES_CODE = 0x01 BYTES_CODE = 0x01
STRING_CODE = 0x02 STRING_CODE = 0x02
NESTED_CODE = 0x05 NESTED_CODE = 0x05
INT_ZERO_CODE = 0x14 INT_ZERO_CODE = 0x14
POS_INT_END = 0x1d POS_INT_END = 0x1d
NEG_INT_START = 0x0b NEG_INT_START = 0x0b
FLOAT_CODE = 0x20 FLOAT_CODE = 0x20
DOUBLE_CODE = 0x21 DOUBLE_CODE = 0x21
FALSE_CODE = 0x26 FALSE_CODE = 0x26
TRUE_CODE = 0x27 TRUE_CODE = 0x27
UUID_CODE = 0x30 UUID_CODE = 0x30
VERSIONSTAMP_CODE = 0x33
# Reserved: Codes 0x03, 0x04, 0x23, and 0x24 are reserved for historical reasons. # Reserved: Codes 0x03, 0x04, 0x23, and 0x24 are reserved for historical reasons.
@ -115,6 +116,104 @@ class SingleFloat(object):
def __nonzero__(self): def __nonzero__(self):
return bool(self.value) return bool(self.value)
class Versionstamp(object):
_GLOBAL_VERSION_LEN = 10
_MAX_LOCAL_VERSION = (1 << 16) - 1
_UNSET_GLOBAL_VERSION = 10 * six.int2byte(0xff)
_STRUCT_FORMAT_STRING = '>' + str(_GLOBAL_VERSION_LEN) + 'sH'
@classmethod
def validate_global_version(cls, global_version):
if global_version is None:
return
if not isinstance(global_version, bytes):
raise TypeError("Global version has illegal type " + str(type(global_version)) + " (requires bytes)")
elif len(global_version) != cls._GLOBAL_VERSION_LEN:
raise ValueError("Global version has incorrect length " + str(len(global_version)) + " (requires " + str(cls._GLOBAL_VERSION_LEN) + ")")
@classmethod
def validate_local_version(cls, local_version):
if not isinstance(local_version, six.integer_types):
raise TypeError("Local version has illegal type " + str(type(local_version)) + " (requires integer type)")
elif local_version < 0 or local_version > cls._MAX_LOCAL_VERSION:
raise ValueError("Local version has value " + str(local_version) + " which is out of range")
def __init__(self, global_version=None, local_version=0):
Versionstamp.validate_global_version(global_version)
Versionstamp.validate_local_version(local_version)
self.global_version = global_version
self.local_version = local_version
@classmethod
def from_bytes(cls, v, start=0):
if not isinstance(v, bytes):
raise TypeError("Cannot parse versionstamp from non-byte string")
elif len(v) - start < cls._GLOBAL_VERSION_LEN + 2:
raise ValueError("Versionstamp byte string is too short (only " + str(len(v) - start) + " bytes to read from")
else:
global_version = v[start:start + cls._GLOBAL_VERSION_LEN]
if global_version == cls._UNSET_GLOBAL_VERSION:
global_version = None
local_version = six.indexbytes(v, start + cls._GLOBAL_VERSION_LEN) * (1 << 8) + six.indexbytes(v, start + cls._GLOBAL_VERSION_LEN + 1)
return Versionstamp(global_version, local_version)
def is_complete(self):
return self.global_version is not None
def __repr__(self):
return "fdb.tuple.Versionstamp(" + repr(self.global_version) + ", " + repr(self.local_version) + ")"
def __str__(self):
return "Versionstamp(" + str(self.global_version) + ", " + str(self.local_version) + ")"
def to_bytes(self):
return struct.pack(self._STRUCT_FORMAT_STRING,
self.global_version if self.is_complete() else self._UNSET_GLOBAL_VERSION,
self.local_version)
def completed(self, new_global_version):
if self.is_complete():
raise RuntimeError("Cannot complete Versionstamp twice")
else:
return Versionstamp(new_global_version, self.local_version)
# Comparisons
def __eq__(self, other):
if isinstance(other, Versionstamp):
return self.global_version == other.global_version and self.local_version == other.local_version
else:
return False
def __ne__(self, other):
return not (self == other)
def __cmp__(self, other):
if self.is_complete():
if other.is_complete():
if self.global_version == other.global_version:
return cmp(self.local_version, other.local_version)
else:
return cmp(self.global_version, other.global_version)
else:
# All complete are less than all incomplete.
return -1
else:
if other.is_complete():
# All incomplete are greater than all complete
return 1
else:
return cmp(self.local_version, other.local_version)
def __hash__(self):
if self.global_version is None:
return hash(self.local_version)
else:
return hash(self.global_version) * 37 ^ hash(self.local_version)
def __nonzero__(self):
return bool(self.global_version) or bool(self.local_version)
def _decode(v, pos): def _decode(v, pos):
code = six.indexbytes(v, pos) code = six.indexbytes(v, pos)
if code == NULL_CODE: if code == NULL_CODE:
@ -161,6 +260,8 @@ def _decode(v, pos):
if hasattr(fdb, "_version") and fdb._version < 500: if hasattr(fdb, "_version") and fdb._version < 500:
raise ValueError("Invalid API version " + str(fdb._version) + " for boolean types") raise ValueError("Invalid API version " + str(fdb._version) + " for boolean types")
return True, pos+1 return True, pos+1
elif code == VERSIONSTAMP_CODE:
return Versionstamp.from_bytes(v, pos+1), pos + 13
elif code == NESTED_CODE: elif code == NESTED_CODE:
ret = [] ret = []
end_pos = pos+1 end_pos = pos+1
@ -178,32 +279,45 @@ def _decode(v, pos):
else: else:
raise ValueError("Unknown data type in DB: " + repr(v)) raise ValueError("Unknown data type in DB: " + repr(v))
def _reduce_children(child_values):
version_pos = -1
len_so_far = 0
bytes_list = []
for child_bytes, child_pos in child_values:
if child_pos >= 0:
if version_pos >= 0:
raise ValueError("Multiple unset versionstamps included in tuple")
version_pos = len_so_far + child_pos
len_so_far += len(child_bytes)
bytes_list.append(child_bytes)
return bytes_list, version_pos
def _encode(value, nested=False): def _encode(value, nested=False):
# returns [code][data] (code != 0xFF) # returns [code][data] (code != 0xFF)
# encoded values are self-terminating # encoded values are self-terminating
# sorting need to work too! # sorting need to work too!
if value == None: # ==, not is, because some fdb.impl.Value are equal to None if value == None: # ==, not is, because some fdb.impl.Value are equal to None
if nested: if nested:
return b''.join([six.int2byte(NULL_CODE), six.int2byte(0xff)]) return b''.join([six.int2byte(NULL_CODE), six.int2byte(0xff)]), -1
else: else:
return b''.join([six.int2byte(NULL_CODE)]) return b''.join([six.int2byte(NULL_CODE)]), -1
elif isinstance(value, bytes): # also gets non-None fdb.impl.Value elif isinstance(value, bytes): # also gets non-None fdb.impl.Value
return six.int2byte(BYTES_CODE) + value.replace(b'\x00', b'\x00\xFF') + b'\x00' return six.int2byte(BYTES_CODE) + value.replace(b'\x00', b'\x00\xFF') + b'\x00', -1
elif isinstance(value, six.text_type): elif isinstance(value, six.text_type):
return six.int2byte(STRING_CODE) + value.encode('utf-8').replace(b'\x00', b'\x00\xFF') + b'\x00' return six.int2byte(STRING_CODE) + value.encode('utf-8').replace(b'\x00', b'\x00\xFF') + b'\x00', -1
elif isinstance(value, six.integer_types) and (not isinstance(value, bool) or (hasattr(fdb, '_version') and fdb._version < 500)): elif isinstance(value, six.integer_types) and (not isinstance(value, bool) or (hasattr(fdb, '_version') and fdb._version < 500)):
if value == 0: if value == 0:
return b''.join([six.int2byte(INT_ZERO_CODE)]) return b''.join([six.int2byte(INT_ZERO_CODE)]), -1
elif value > 0: elif value > 0:
if value >= _size_limits[-1]: if value >= _size_limits[-1]:
length = (value.bit_length()+7)//8 length = (value.bit_length()+7)//8
data = [six.int2byte(POS_INT_END), six.int2byte(length)] data = [six.int2byte(POS_INT_END), six.int2byte(length)]
for i in _range(length-1,-1,-1): for i in _range(length-1,-1,-1):
data.append(six.int2byte( (value>>(8*i))&0xff )) data.append(six.int2byte( (value>>(8*i))&0xff ))
return b''.join(data) return b''.join(data), -1
n = bisect_left( _size_limits, value ) n = bisect_left( _size_limits, value )
return six.int2byte(INT_ZERO_CODE + n) + struct.pack( ">Q", value )[-n:] return six.int2byte(INT_ZERO_CODE + n) + struct.pack( ">Q", value )[-n:], -1
else: else:
if -value >= _size_limits[-1]: if -value >= _size_limits[-1]:
length = (value.bit_length()+7)//8 length = (value.bit_length()+7)//8
@ -211,38 +325,72 @@ def _encode(value, nested=False):
data = [six.int2byte(NEG_INT_START), six.int2byte(length^0xff)] data = [six.int2byte(NEG_INT_START), six.int2byte(length^0xff)]
for i in _range(length-1,-1,-1): for i in _range(length-1,-1,-1):
data.append(six.int2byte( (value>>(8*i))&0xff )) data.append(six.int2byte( (value>>(8*i))&0xff ))
return b''.join(data) return b''.join(data), -1
n = bisect_left( _size_limits, -value ) n = bisect_left( _size_limits, -value )
maxv = _size_limits[n] maxv = _size_limits[n]
return six.int2byte(INT_ZERO_CODE - n) + struct.pack( ">Q", maxv+value)[-n:] return six.int2byte(INT_ZERO_CODE - n) + struct.pack( ">Q", maxv+value)[-n:], -1
elif isinstance(value, ctypes.c_float) or isinstance(value, SingleFloat): elif isinstance(value, ctypes.c_float) or isinstance(value, SingleFloat):
return six.int2byte(FLOAT_CODE) + _float_adjust(struct.pack(">f", value.value), True) return six.int2byte(FLOAT_CODE) + _float_adjust(struct.pack(">f", value.value), True), -1
elif isinstance(value, ctypes.c_double): elif isinstance(value, ctypes.c_double):
return six.int2byte(DOUBLE_CODE) + _float_adjust(struct.pack(">d", value.value), True) return six.int2byte(DOUBLE_CODE) + _float_adjust(struct.pack(">d", value.value), True), -1
elif isinstance(value, float): elif isinstance(value, float):
return six.int2byte(DOUBLE_CODE) + _float_adjust(struct.pack(">d", value), True) return six.int2byte(DOUBLE_CODE) + _float_adjust(struct.pack(">d", value), True), -1
elif isinstance(value, uuid.UUID): elif isinstance(value, uuid.UUID):
return six.int2byte(UUID_CODE) + value.bytes return six.int2byte(UUID_CODE) + value.bytes, -1
elif isinstance(value, bool): elif isinstance(value, bool):
if value: if value:
return b''.join([six.int2byte(TRUE_CODE)]) return b''.join([six.int2byte(TRUE_CODE)]), -1
else: else:
return b''.join([six.int2byte(FALSE_CODE)]) return b''.join([six.int2byte(FALSE_CODE)]), -1
elif isinstance(value, Versionstamp):
version_pos = -1 if value.is_complete() else 1
return six.int2byte(VERSIONSTAMP_CODE) + value.to_bytes(), version_pos
elif isinstance(value, tuple) or isinstance(value, list): elif isinstance(value, tuple) or isinstance(value, list):
return b''.join([six.int2byte(NESTED_CODE)] + list(map(lambda x: _encode(x, True), value)) + [six.int2byte(0x00)]) child_bytes, version_pos = _reduce_children(map(lambda x: _encode(x, True), value))
new_version_pos = -1 if version_pos < 0 else version_pos + 1
return b''.join([six.int2byte(NESTED_CODE)] + child_bytes + [six.int2byte(0x00)]), version_pos
else: else:
raise ValueError("Unsupported data type: " + str(type(value))) raise ValueError("Unsupported data type: " + str(type(value)))
# packs the specified tuple into a key # packs the specified tuple into that may be used for versionstamp operations but may be used for regular ops
def pack(t): def pack_maybe_with_versionstamp(t, prefix=None, prefix_len=0):
if prefix is not None and prefix_len > 0 and len(prefix) != prefix_len:
raise ValueError("Inconsistent values specified for prefix and prefix_len")
if prefix_len < 0:
raise ValueError("Illegal prefix_len " + str(prefix_len) + " specified")
if not isinstance(t, tuple): if not isinstance(t, tuple):
raise Exception("fdbtuple pack() expects a tuple, got a " + str(type(t))) raise Exception("fdbtuple pack() expects a tuple, got a " + str(type(t)))
return b''.join([_encode(x) for x in t])
bytes_list = [prefix] if prefix is not None else []
child_bytes, version_pos = _reduce_children(map(_encode, t))
if version_pos >= 0:
version_pos += len(prefix) if prefix is not None else prefix_len
bytes_list.extend(child_bytes)
bytes_list.append(struct.pack('<H', version_pos))
else:
bytes_list.extend(child_bytes)
return b''.join(bytes_list), version_pos
# packs the specified tuple into a key
def pack(t, prefix=None):
res, version_pos = pack_maybe_with_versionstamp(t, prefix)
if version_pos >= 0:
raise ValueError("Incomplete versionstamp included in vanilla tuple pack")
return res
# packs the specified tuple into a key for versionstamp operations
def pack_with_versionstamp(t, prefix=None, prefix_len=0):
res, version_pos = pack_maybe_with_versionstamp(t, prefix, prefix_len)
if version_pos < 0:
raise ValueError("No incomplete versionstamp included in tuple pack with versionstamp")
return res
# unpacks the specified key into a tuple # unpacks the specified key into a tuple
def unpack(key): def unpack(key, prefix_len=0):
pos = 0 pos = prefix_len
res = [] res = []
while pos < len(key): while pos < len(key):
r, pos = _decode(key, pos) r, pos = _decode(key, pos)
@ -282,6 +430,8 @@ def _code_for(value):
return DOUBLE_CODE return DOUBLE_CODE
elif isinstance(value, uuid.UUID): elif isinstance(value, uuid.UUID):
return UUID_CODE return UUID_CODE
elif isinstance(value, Versionstamp):
return VERSIONSTAMP_CODE
elif isinstance(value, tuple) or isinstance(value, list): elif isinstance(value, tuple) or isinstance(value, list):
return NESTED_CODE return NESTED_CODE
else: else:
@ -334,7 +484,7 @@ def _compare_values(value1, value2):
elif code1 == NESTED_CODE: elif code1 == NESTED_CODE:
return compare(value1, value2) return compare(value1, value2)
else: else:
# Booleans, UUIDs, and integers can just use standard comparison. # Booleans, UUIDs, integers, and Versionstamps can just use standard comparison.
return -1 if value1 < value2 else 0 if value1 == value2 else 1 return -1 if value1 < value2 else 0 if value1 == value2 else 1
# compare element by element and return -1 if t1 < t2 or 1 if t1 > t2 or 0 if t1 == t2 # compare element by element and return -1 if t1 < t2 or 1 if t1 > t2 or 0 if t1 == t2

View File

@ -453,6 +453,19 @@ class Tester:
count = inst.pop() count = inst.pop()
items = inst.pop(count) items = inst.pop(count)
inst.push(fdb.tuple.pack(tuple(items))) inst.push(fdb.tuple.pack(tuple(items)))
elif inst.op == six.u("TUPLE_PACK_WITH_VERSIONSTAMP"):
prefix = inst.pop()
count = inst.pop()
items = inst.pop(count)
try:
packed = fdb.tuple.pack_with_versionstamp(tuple(items), prefix=prefix)
inst.push(b"OK")
inst.push(packed)
except ValueError as e:
if str(e).startswith("No incomplete"):
inst.push(b"ERROR: NONE")
else:
inst.push(b"ERROR: MULTIPLE")
elif inst.op == six.u("TUPLE_UNPACK"): elif inst.op == six.u("TUPLE_UNPACK"):
for i in fdb.tuple.unpack( inst.pop() ): for i in fdb.tuple.unpack( inst.pop() ):
inst.push(fdb.tuple.pack((i,))) inst.push(fdb.tuple.pack((i,)))