diff --git a/flake.nix b/flake.nix index 8d21398..b58b9a7 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ let buildSystems = { rocksdb = [ "setuptools" "cython" "pkgconfig" ]; - cprotobuf = [ "setuptools" ]; + cprotobuf = [ "setuptools" "cython" ]; }; in lib.mapAttrs diff --git a/iavl/cli.py b/iavl/cli.py index d7c8ecf..7ce1437 100644 --- a/iavl/cli.py +++ b/iavl/cli.py @@ -1,6 +1,7 @@ import binascii import hashlib import json +import mmap import sys from pathlib import Path from typing import List, Optional @@ -9,7 +10,7 @@ from hexbytes import HexBytes from . import dbm, diff -from .iavl import NodeDB, Tree, delete_version +from .iavl import DEFAULT_CACHE_SIZE, NodeDB, Tree, delete_version from .utils import ( decode_fast_node, diff_iterators, @@ -376,7 +377,14 @@ def visualize(db, version, store=None, include_prev_version=False): type=click.Path(exists=True), required=True, ) -def dump_changesets(db, start_version, end_version, store: Optional[str], out_dir: str): +@click.option( + "--cache-size", + help="the output directory to save the data files", + default=DEFAULT_CACHE_SIZE, +) +def dump_changesets( + db, start_version, end_version, store: Optional[str], out_dir: str, cache_size: int +): """ extract changeset by comparing iavl versions and save in files with compatible format with file streamer. @@ -384,22 +392,56 @@ def dump_changesets(db, start_version, end_version, store: Optional[str], out_di """ db = dbm.open(str(db), read_only=True) prefix = store_prefix(store) if store is not None else b"" - ndb = NodeDB(db, prefix=prefix) - for _, v, _, changeset in iter_state_changes( - db, ndb, start_version=start_version, end_version=end_version, prefix=prefix - ): - with (Path(out_dir) / f"block-{v}-data").open("wb") as fp: - diff.write_change_set(fp, changeset) + ndb = NodeDB(db, prefix=prefix, cache_size=cache_size) + + last_version = None + offset = 0 + output = Path(out_dir) / f"block-{start_version}" + if output.exists(): + with output.open("rb") as fp: + last_version, offset = diff.seek_last_version(fp) + + with output.open("ab") as fp: + fp.seek(offset) + fp.truncate() + if offset == 0: + fp.write(diff.VERSIONDB_MAGIC) + if last_version is not None: + start_version = last_version + 1 + print("continue from", start_version) + else: + print("start from", start_version) + for _, v, _, changeset in iter_state_changes( + db, ndb, start_version=start_version, end_version=end_version, prefix=prefix + ): + diff.append_change_set(fp, v, changeset) @cli.command() @click.argument("file", type=click.Path(exists=True)) -def print_changeset(file): +@click.option( + "--parse-kv-pairs/--no-parse-kv-pairs", + default=True, + help="if parse the changeset kv pairs", +) +def print_changesets(file, parse_kv_pairs): """ decode and print the content of changeset files """ - for item in diff.parse_change_set(Path(file).read_bytes()): - print(json.dumps(item.as_json())) + with Path(file).open("rb") as fp: + with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as data: + if parse_kv_pairs: + data.madvise(mmap.MADV_NORMAL) + else: + data.madvise(mmap.MADV_RANDOM) + for version, items in diff.parse_change_set( + memoryview(data), parse_kv_pairs + ): + print("version:", version) + if items is None: + continue + for item in items: + print(json.dumps(item.as_json())) @cli.command() diff --git a/iavl/diff.py b/iavl/diff.py index fbd1d18..58e6ff2 100644 --- a/iavl/diff.py +++ b/iavl/diff.py @@ -2,63 +2,60 @@ tree diff algorithm between two versions """ import binascii -from enum import IntEnum -from typing import List, Tuple - -from cprotobuf import Field, ProtoEntity, decode_primitive, encode_primitive +import mmap +from typing import List, NamedTuple + +from cprotobuf import ( + Field, + ProtoEntity, + decode_primitive, + encode_data, + encode_primitive, +) +from cprotobuf.internal import InternalDecodeError from .iavl import PersistedNode, Tree from .utils import GetNode, visit_iavl_nodes +VERSIONDB_MAGIC = b"VERDB000" -class Op(IntEnum): - Update, Delete, Insert = range(3) +class KVPair(NamedTuple): + delete: bool = False + key: bytes = None + value: bytes = None -Change = Tuple[bytes, Op, object] -ChangeSet = List[Change] + def as_json(self): + d = {"key": binascii.hexlify(self.key).decode()} + if self.value: + d["value"] = binascii.hexlify(self.value).decode() + if self.delete: + d["delete"] = True + return d -def split_operations(nodes1, nodes2) -> ChangeSet: - """ - Contract: input nodes are all leaf nodes, sorted by node.key +class StoreKVPair(ProtoEntity): + delete = Field("bool", 1) + key = Field("bytes", 2) + value = Field("bytes", 3) + + def as_json(self): + d = {"key": binascii.hexlify(self.key).decode()} + if self.value: + d["value"] = binascii.hexlify(self.value).decode() + if self.delete: + d["delete"] = True + return d - return: [(key, op, arg)] - arg: original value if op==Delete - new value if op==Insert - (original value, new value) if op==Update - """ - i1 = i2 = 0 - result = [] - while True: - if i1 > len(nodes1) - 1: - for n in nodes2[i2:]: - result.append((n.key, Op.Insert, n.value)) - break - if i2 > len(nodes2) - 1: - for n in nodes1[i1:]: - result.append((n.key, Op.Delete, n.value)) - break - n1 = nodes1[i1] - n2 = nodes2[i2] - k1 = n1.key - k2 = n2.key - if k1 == k2: - result.append((k1, Op.Update, (n1.value, n2.value))) - i1 += 1 - i2 += 1 - elif k1 < k2: - # proceed to next node in nodes1 until catch up with nodes2 - result.append((n1.key, Op.Delete, n1.value)) - i1 += 1 - else: - # proceed to next node in nodes2 until catch up with nodes1 - result.append((n2.key, Op.Insert, n2.value)) - i2 += 1 - return result +class StoreChangeSet(ProtoEntity): + pairs = Field(StoreKVPair, 1, repeated=True) -def state_changes(get_node: GetNode, version, root, successor_root): + +ChangeSet = List[KVPair] + + +def state_changes(get_node: GetNode, version, root, successor_root) -> ChangeSet: """ extract state changes from two versions of the iavl tree. @@ -66,14 +63,12 @@ def state_changes(get_node: GetNode, version, root, successor_root): and new leaf nodes, then traverse the target version to find the orphaned leaf nodes, then extract kv pair operations from it. - return: [(key, op, arg)] - arg: original value if op==Delete - new value if op==Insert - (original value, new value) if op==Update + return: [KVPair] """ shared = set() - new = [] + new = [] # update and inserts + new_keys = set() if successor_root: def successor_prune(n: PersistedNode) -> (bool, bool): @@ -84,102 +79,118 @@ def successor_prune(n: PersistedNode) -> (bool, bool): if n.version <= version: shared.add(n.hash) elif n.is_leaf(): - new.append(n) + new.append(KVPair(key=n.key, value=n.value)) + new_keys.add(n.key) def prune(n: PersistedNode) -> (bool, bool): b = n.hash in shared return b, b if root: - orphaned = [ - n + deleted = [ + KVPair(delete=True, key=n.key) for n in visit_iavl_nodes(get_node, prune, root) - if n.is_leaf() and n.hash not in shared + if n.is_leaf() and n.hash not in shared and n.key not in new_keys ] else: - orphaned = [] + deleted = [] - return split_operations(orphaned, new) + changeset = new + deleted + changeset.sort(key=lambda n: n.key) + return changeset def apply_change_set(tree: Tree, changeset: ChangeSet): """ changeset: the result of `state_changes` """ - for key, op, arg in changeset: - if op == Op.Insert: - tree.set(key, arg) - elif op == Op.Update: - _, value = arg - tree.set(key, value) - elif op == Op.Delete: - tree.remove(key) + for pair in changeset: + if pair.delete: + tree.remove(pair.key) else: - raise NotImplementedError(f"unknown op {op}") + tree.set(pair.key, pair.value) -class StoreKVPairs(ProtoEntity): - """ - protobuf format compatible with file streamer output - store an additional original value, it's empty for insert operation. +def append_change_set(fp, version: int, changeset: ChangeSet): """ + write change set to file, file format: - # the store key for the KVStore this pair originates from - store_key = Field("string", 1) - # true indicates a delete operation - delete = Field("bool", 2) - key = Field("bytes", 3) - value = Field("bytes", 4) - original = Field("bytes", 5) - - def as_json(self): - d = {"key": binascii.hexlify(self.key).decode()} - if self.store_key: - d["store_key"] = self.store_key - if self.value: - d["value"] = binascii.hexlify(self.value).decode() - if self.original: - d["original"] = binascii.hexlify(self.original).decode() - if self.delete: - d["delete"] = True - return d - - -def write_change_set(fp, changeset: ChangeSet, store=""): + ``` + version: varint + size: varint # the total size of kv-pairs, so we can skip faster + kv-pairs: length prefixed proto msg + ``` """ - write change set to file, compatible with the file streamer output. - """ - chunks = [] - for key, op, arg in changeset: - kv = StoreKVPairs(store_key=store, key=key) - if op == Op.Delete: - kv.delete = True - kv.original = arg - elif op == Op.Update: - kv.original, kv.value = arg - elif op == Op.Insert: - kv.value = arg - item = kv.SerializeToString() - chunks.append(encode_primitive("uint64", len(item))) - chunks.append(item) - data = b"".join(chunks) - fp.write(len(data).to_bytes(8, "big")) + data = encode_data(StoreChangeSet, {"pairs": [kv._asdict() for kv in changeset]}) + fp.write(encode_primitive("uint64", version)) + fp.write(encode_primitive("uint64", len(data))) fp.write(data) -def parse_change_set(data): +def parse_change_set(data, parse_body=True): """ - return list of StoreKVPairs + data is the bytes slice of a change set file, + could be mmapped from the disk file. + + yield (version, [KVPair]) """ - size = int.from_bytes(data[:8], "big") - assert len(data) == size + 8 + assert data[:8] == VERSIONDB_MAGIC offset = 8 - items = [] + while offset < len(data): + version, n = decode_primitive(data[offset:], "uint64") + offset += n size, n = decode_primitive(data[offset:], "uint64") offset += n - item = StoreKVPairs() - item.ParseFromString(data[offset : offset + size]) - items.append(item) + + if offset + size > len(data): + # incomplete file + break + + body = None + if parse_body: + changeSet = StoreChangeSet() + changeSet.ParseFromString(data[offset : offset + size]) + body = changeSet.pairs offset += size - return items + yield version, body + + +def _seek_last_version(data): + """ + find the last complete version and return the offset of the end, + which will be used to truncate the file + """ + assert data[:8] == VERSIONDB_MAGIC + offset = 8 + + version = None + tmp = offset + while offset < len(data): + try: + tmp_version, n = decode_primitive(data[tmp:], "uint64") + tmp += n + size, n = decode_primitive(data[tmp:], "uint64") + tmp += n + except InternalDecodeError: + # corrupted version + break + tmp += size + if tmp > len(data): + # corrupted version + break + offset = tmp + version = tmp_version + return version, offset + + +def seek_last_version(fp): + """ + try to truncate the corrupted version data at the end of change set file, + and return the last completed version, return None if none. + """ + with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as data: + if len(data) < 8: + return None, 0 + data.madvise(mmap.MADV_RANDOM) + return _seek_last_version(memoryview(data)) diff --git a/iavl/iavl.py b/iavl/iavl.py index d6166e3..24ba481 100644 --- a/iavl/iavl.py +++ b/iavl/iavl.py @@ -20,6 +20,8 @@ NodeRef = Union[bytes, "Node"] +DEFAULT_CACHE_SIZE = 500000 + class NodeDB: """ @@ -30,13 +32,22 @@ class NodeDB: batch: rocksdb.WriteBatch cache: Dict[bytes, PersistedNode] prefix: bytes + cache_size: int - def __init__(self, db, prefix=b""): + def __init__(self, db, prefix=b"", cache_size=DEFAULT_CACHE_SIZE): self.db = db self.batch = None self.cache = {} + self.cache_size = cache_size self.prefix = prefix + def _set_cache(self, hash, node): + if self.cache_size == 0: + return + if len(self.cache) >= self.cache_size: + self.cache = {} + self.cache[hash] = node + def get(self, hash: bytes) -> Optional[PersistedNode]: try: return self.cache[hash] @@ -45,7 +56,7 @@ def get(self, hash: bytes) -> Optional[PersistedNode]: if bz is None: return node = PersistedNode.decode(bz, hash) - self.cache[hash] = node + self._set_cache(hash, node) return node def resolve_node(self, ref: NodeRef) -> Union["Node", PersistedNode, None]: @@ -69,7 +80,7 @@ def batch_remove_root_hash(self, version: int): def batch_set_node(self, hash: bytes, node: PersistedNode): if self.batch is None: self.batch = rocksdb.WriteBatch() - self.cache[hash] = node + self._set_cache(hash, node) self.batch.put(node_key(hash), node.encode()) def batch_set_root_hash(self, version: int, hash: bytes): diff --git a/poetry.lock b/poetry.lock index b10915d..76cd9c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,6 +52,13 @@ description = "pythonic and high performance protocol buffer implementation." category = "main" optional = false python-versions = "*" +develop = false + +[package.source] +type = "git" +url = "https://github.com/yihuang/cprotobuf.git" +reference = "master" +resolved_reference = "32283bc02f8e0d2a63fd67e568c2b1e9ce3b9152" [[package]] name = "graphviz" @@ -203,6 +210,14 @@ url = "https://github.com/HathorNetwork/python-rocksdb.git" reference = "master" resolved_reference = "947f68a80d97c4a5621ee681ae01602ebd883f3a" +[[package]] +name = "rocksdict" +version = "0.3.5" +description = "Rocksdb Python Binding" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "setuptools" version = "65.5.0" @@ -227,7 +242,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "91aebba68238c547c34a3d88c02d226c6a711185c441f49abca9f46201391a70" +content-hash = "efa0c631dd3880293f507dd983a9acf2653a5a16d09a59f10c64fe57cc78c190" [metadata.files] attrs = [ @@ -294,9 +309,7 @@ coverage = [ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] -cprotobuf = [ - {file = "cprotobuf-0.1.11.tar.gz", hash = "sha256:d2d88c8de840275205e64e530052c653dd25a0fb9e5cd9f7e39ce8f762d7c0a4"}, -] +cprotobuf = [] graphviz = [ {file = "graphviz-0.20.1-py3-none-any.whl", hash = "sha256:587c58a223b51611c0cf461132da386edd896a029524ca61a1462b880bf97977"}, {file = "graphviz-0.20.1.zip", hash = "sha256:8c58f14adaa3b947daf26c19bc1e98c4e0702cdc31cf99153e6f06904d492bf8"}, @@ -392,6 +405,38 @@ python-snappy = [ {file = "python_snappy-0.6.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:03bb511380fca2a13325b6f16fe8234c8e12da9660f0258cd45d9a02ffc916af"}, ] rocksdb = [] +rocksdict = [ + {file = "rocksdict-0.3.5-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:de51f8a07b245e6eded8ebd59153a65b60ae854676b61d81b104cf1e7134024c"}, + {file = "rocksdict-0.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c013c0b9560fda896f8d62aa99f4ba99827ad7b0f76ded796c504df839912b"}, + {file = "rocksdict-0.3.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4c3a9b93a5e63ebe08d7e3e13a058ad78b7d3301d5acf6230131e66163c75d39"}, + {file = "rocksdict-0.3.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f5b18bec9a43b826ce5c20405d479638a53d668e19882c9746da9638b4f78a50"}, + {file = "rocksdict-0.3.5-cp310-none-win32.whl", hash = "sha256:68cc92a30e51948b9fca3febdce692d70a5eb60aaf54c1e51e2e75a754415317"}, + {file = "rocksdict-0.3.5-cp310-none-win_amd64.whl", hash = "sha256:0fc7c9333170557c8f9a5cac1ca149f989b76042401595500ec2af5acc7fe381"}, + {file = "rocksdict-0.3.5-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:aab989be1f3605dbec5a19ddccf6d29219e891288521a75e41f2625f920f5d9e"}, + {file = "rocksdict-0.3.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2875d60706de522259001a60dfa00a8a4366c9ef3dcf3eb38c4ab2208ac5d6"}, + {file = "rocksdict-0.3.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e3f3a281c98043f3f00e41bb85b44733707c27d6c288ad7f33f9c580db10480"}, + {file = "rocksdict-0.3.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0c23d54ff3397b0f866180cda96d50458b95cf5b12287302fe63a011e82e7616"}, + {file = "rocksdict-0.3.5-cp311-none-win32.whl", hash = "sha256:ca7b5df80d589ec51bdeb12c2fb49d3c090281df025ef5d88928875337a29f69"}, + {file = "rocksdict-0.3.5-cp311-none-win_amd64.whl", hash = "sha256:9a085b52b49e799b496a6c92bb7cb1988fcf48da185dd83fc9040bf47325b04c"}, + {file = "rocksdict-0.3.5-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b4a3dda682a4a5368753c788f868a039257bb444933fb023e01fd2718abad93d"}, + {file = "rocksdict-0.3.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6cae900f197922a23114f518eeee6261e798e953ee400c549f912602a2d9bc"}, + {file = "rocksdict-0.3.5-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:8769635a59db155f2a5d6ce4e624a4bd05a9c88281f486a629c5321243bec3af"}, + {file = "rocksdict-0.3.5-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:9b69dadb325d2b8846a1b85b13507e107ae5354419def11ee7314ad36f52a30a"}, + {file = "rocksdict-0.3.5-cp37-none-win32.whl", hash = "sha256:2a6d78ca75b6faf9331336ab33fc0c311ccfe8bbe98afd75ff54c2fc500fd0b1"}, + {file = "rocksdict-0.3.5-cp37-none-win_amd64.whl", hash = "sha256:9e9ff5e7f2e0f9480f74be3b3690489dfe4e957e9dda1636fb3e91679440bd12"}, + {file = "rocksdict-0.3.5-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:3c92afd754532f8634fa16fff8187851de7326501429ee680f7058b1b4387653"}, + {file = "rocksdict-0.3.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4302e7e2eadfc7f16d3c91b8b2542816d36f35a51bf88c7f83607b2cbb797f"}, + {file = "rocksdict-0.3.5-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e834e8665c6d5e26750a6b9c0ae25377f9303543ccd9a5c00bf22e1687dd8af4"}, + {file = "rocksdict-0.3.5-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:29ddc56d68f84ffa166664ff5906e7bafe1e927494f231822c493ca15e60b06e"}, + {file = "rocksdict-0.3.5-cp38-none-win32.whl", hash = "sha256:8c7f6de33b453536af0debefa6986150f06ebbe160687e18530287a5f7943b4f"}, + {file = "rocksdict-0.3.5-cp38-none-win_amd64.whl", hash = "sha256:274e9742d57f112d10fb672658d9fc95f0e261884a0dea09af3ac8ca9f05a8f0"}, + {file = "rocksdict-0.3.5-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:96a3cb556abc06833b6fa2e7c4452af5312f19759df6f6643c4dc4cf819e270e"}, + {file = "rocksdict-0.3.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c73c25235c2b3cb8b9dbac8c86fa2b1cc2ba1c7e9eaac6e2b99b2d01744ba9"}, + {file = "rocksdict-0.3.5-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:4c509921f86f9ff6cfa96be115faa651708cd11d4d6cff85b4e2a6ee52011901"}, + {file = "rocksdict-0.3.5-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:02bee8d83d00a0df6f7f5a02f3bc047a7fd1acb6570511f680ad44997fa97b91"}, + {file = "rocksdict-0.3.5-cp39-none-win32.whl", hash = "sha256:b95522fd690b68578f22dfc0aab645caf7a99d83670367ee840248f63225ee56"}, + {file = "rocksdict-0.3.5-cp39-none-win_amd64.whl", hash = "sha256:523bbda529839df671593092ba03067c13e5366ce2b7f7fd1d7f6466a37558bc"}, +] setuptools = [ {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, diff --git a/pyproject.toml b/pyproject.toml index 9cfb38e..2fbda94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,12 @@ authors = ["HuangYi "] [tool.poetry.dependencies] python = "^3.10" -cprotobuf = "^0.1.11" +cprotobuf = { git = "https://github.com/yihuang/cprotobuf.git", branch = "master" } click = "^8.1.3" hexbytes = "^0.3.0" python-snappy = "^0.6.1" graphviz = "^0.20.1" +rocksdict = "^0.3.5" [tool.poetry.group.rocksdb] optional = true diff --git a/tests/test_iavl.py b/tests/test_iavl.py index 07b36db..3229826 100644 --- a/tests/test_iavl.py +++ b/tests/test_iavl.py @@ -2,7 +2,7 @@ import rocksdb from hexbytes import HexBytes -from iavl.diff import Op, apply_change_set +from iavl.diff import KVPair, apply_change_set from iavl.iavl import NodeDB, Tree @@ -41,16 +41,22 @@ class ExpResult(NamedTuple): ChangeSets = [ - [(b"hello", Op.Insert, b"world")], - [(b"hello", Op.Update, (b"world", b"world1")), (b"hello1", Op.Insert, b"world1")], - [(b"hello2", Op.Insert, b"world1"), (b"hello3", Op.Insert, b"world1")], - [(b"hello%02d" % i, Op.Insert, b"world1") for i in range(20)], - [(b"hello", Op.Delete, b"world1"), (b"hello19", Op.Delete, b"world1")], + [KVPair(key=b"hello", value=b"world")], + [ + KVPair(key=b"hello", value=b"world1"), + KVPair(key=b"hello1", value=b"world1"), + ], + [ + KVPair(key=b"hello2", value=b"world1"), + KVPair(key=b"hello3", value=b"world1"), + ], + [KVPair(key=b"hello%02d" % i, value=b"world1") for i in range(20)], + [KVPair(key=b"hello", delete=True), KVPair(key=b"hello19", delete=True)], # try to cover all balancing cases - [(b"aello%02d" % i, Op.Insert, b"world1") for i in range(21)], + [KVPair(key=b"aello%02d" % i, value=b"world1") for i in range(21)], # remove most of the values - [(b"aello%02d" % i, Op.Delete, b"world1") for i in range(21)] - + [(b"hello%02d" % i, Op.Delete, b"world1") for i in range(19)], + [KVPair(key=b"aello%02d" % i, delete=True) for i in range(21)] + + [KVPair(key=b"hello%02d" % i, delete=True) for i in range(19)], ]