Skip to content

Commit 494dda2

Browse files
committed
test: verify writes after RocksDB upgrade
1 parent 3740eee commit 494dda2

1 file changed

Lines changed: 180 additions & 30 deletions

File tree

scripts/rocksdb_patch_compat.py

Lines changed: 180 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
2222
# Validate with a separately installed source build.
2323
python scripts/rocksdb_patch_compat.py validate var/compatibility/v051
24+
25+
# Copy, query, extend, and reopen the old fixture with the source build.
26+
python scripts/rocksdb_patch_compat.py upgrade \
27+
var/compatibility/v051 var/compatibility/v051-upgraded --overwrite
2428
"""
2529

2630
from __future__ import annotations
@@ -47,6 +51,8 @@
4751
)
4852

4953
DOC_COUNT = 256
54+
UPGRADE_DOC_COUNT = 64
55+
UPGRADED_DOC_COUNT = DOC_COUNT + UPGRADE_DOC_COUNT
5056
VECTOR_DIMENSION = 8
5157
GENERATOR_VERSION = "0.5.1"
5258

@@ -57,20 +63,31 @@ def zvec_version() -> str:
5763

5864
def embedding(ordinal: int) -> list[float]:
5965
"""Return a deterministic, non-degenerate vector for an ordinal."""
66+
if ordinal >= DOC_COUNT:
67+
upgrade_ordinal = ordinal - DOC_COUNT
68+
return [
69+
-1.0 - ((upgrade_ordinal * (dimension + 3)) % 67) / 67.0
70+
for dimension in range(VECTOR_DIMENSION)
71+
]
6072
return [
6173
((ordinal * (dimension + 3)) % 257) / 257.0
6274
for dimension in range(VECTOR_DIMENSION)
6375
]
6476

6577

66-
def documents() -> list[Doc]:
78+
def documents(
79+
start: int = 0,
80+
stop: int = DOC_COUNT,
81+
extra_terms: tuple[str, ...] = (),
82+
) -> list[Doc]:
6783
docs = []
68-
for ordinal in range(DOC_COUNT):
84+
for ordinal in range(start, stop):
6985
terms = ["compatibility", "fixture", f"document{ordinal}"]
7086
if ordinal % 2 == 0:
7187
terms.append("evenmarker")
7288
if ordinal % 5 == 0:
7389
terms.append("fivemarker")
90+
terms.extend(extra_terms)
7491
docs.append(
7592
Doc(
7693
id=f"doc-{ordinal:03d}",
@@ -132,6 +149,22 @@ def sidecar_path(collection_path: Path) -> Path:
132149
return collection_path.with_name(f"{collection_path.name}.fixture.json")
133150

134151

152+
def sst_files(collection_path: Path) -> list[str]:
153+
return sorted(
154+
str(path.relative_to(collection_path))
155+
for path in collection_path.rglob("*.sst")
156+
)
157+
158+
159+
def read_metadata(collection_path: Path) -> dict:
160+
metadata = json.loads(sidecar_path(collection_path).read_text(encoding="utf-8"))
161+
if metadata["generator_version"] != GENERATOR_VERSION:
162+
raise AssertionError(
163+
f"expected a {GENERATOR_VERSION} fixture, got {metadata}"
164+
)
165+
return metadata
166+
167+
135168
def generate(collection_path: Path, overwrite: bool) -> None:
136169
if zvec_version() != GENERATOR_VERSION:
137170
raise RuntimeError(
@@ -171,11 +204,8 @@ def generate(collection_path: Path, overwrite: bool) -> None:
171204
del collection
172205
gc.collect()
173206

174-
sst_files = sorted(
175-
str(path.relative_to(collection_path))
176-
for path in collection_path.rglob("*.sst")
177-
)
178-
if not sst_files:
207+
persisted_sst_files = sst_files(collection_path)
208+
if not persisted_sst_files:
179209
raise AssertionError("fixture contains no persisted RocksDB SST files")
180210

181211
sidecar.write_text(
@@ -184,7 +214,7 @@ def generate(collection_path: Path, overwrite: bool) -> None:
184214
"generator_version": zvec_version(),
185215
"doc_count": DOC_COUNT,
186216
"vector_dimension": VECTOR_DIMENSION,
187-
"sst_files": sst_files,
217+
"sst_files": persisted_sst_files,
188218
},
189219
indent=2,
190220
sort_keys=True,
@@ -194,25 +224,15 @@ def generate(collection_path: Path, overwrite: bool) -> None:
194224
)
195225
print(
196226
f"generated {collection_path} with zvec {zvec_version()}: "
197-
f"{DOC_COUNT} docs, {len(sst_files)} SST files"
227+
f"{DOC_COUNT} docs, {len(persisted_sst_files)} SST files"
198228
)
199229

200230

201-
def validate(collection_path: Path) -> None:
202-
sidecar = sidecar_path(collection_path)
203-
metadata = json.loads(sidecar.read_text(encoding="utf-8"))
204-
if metadata["generator_version"] != GENERATOR_VERSION:
231+
def assert_collection_contents(collection, expected_doc_count: int) -> None:
232+
if collection.stats.doc_count != expected_doc_count:
205233
raise AssertionError(
206-
f"expected a {GENERATOR_VERSION} fixture, got {metadata}"
207-
)
208-
209-
collection = zvec.open(
210-
path=str(collection_path),
211-
option=CollectionOption(read_only=True, enable_mmap=True),
212-
)
213-
if collection.stats.doc_count != DOC_COUNT:
214-
raise AssertionError(
215-
f"expected {DOC_COUNT} documents, got {collection.stats.doc_count}"
234+
f"expected {expected_doc_count} documents, "
235+
f"got {collection.stats.doc_count}"
216236
)
217237

218238
fetched = collection.fetch(
@@ -223,9 +243,14 @@ def validate(collection_path: Path) -> None:
223243
if fetched["doc-127"].fields["ordinal"] != 127:
224244
raise AssertionError("scalar field changed while reopening the fixture")
225245

226-
scalar_hits = collection.query(filter="ordinal>=240", topk=DOC_COUNT)
246+
scalar_start = expected_doc_count - 16
247+
scalar_hits = collection.query(
248+
filter=f"ordinal>={scalar_start}", topk=expected_doc_count
249+
)
227250
scalar_ids = {doc.id for doc in scalar_hits}
228-
expected_scalar_ids = {f"doc-{ordinal:03d}" for ordinal in range(240, 256)}
251+
expected_scalar_ids = {
252+
f"doc-{ordinal:03d}" for ordinal in range(scalar_start, expected_doc_count)
253+
}
229254
if scalar_ids != expected_scalar_ids:
230255
raise AssertionError(
231256
f"inverted-index mismatch: expected {expected_scalar_ids}, "
@@ -236,10 +261,12 @@ def validate(collection_path: Path) -> None:
236261
queries=Query(
237262
field_name="content", fts=Fts(match_string="evenmarker")
238263
),
239-
topk=DOC_COUNT,
264+
topk=expected_doc_count,
240265
)
241266
expected_fts_ids = {
242-
f"doc-{ordinal:03d}" for ordinal in range(DOC_COUNT) if ordinal % 2 == 0
267+
f"doc-{ordinal:03d}"
268+
for ordinal in range(expected_doc_count)
269+
if ordinal % 2 == 0
243270
}
244271
fts_ids = {doc.id for doc in fts_hits}
245272
if fts_ids != expected_fts_ids:
@@ -256,18 +283,129 @@ def validate(collection_path: Path) -> None:
256283
if len(vector_hits) != 1 or vector_hits[0].id != "doc-127":
257284
raise AssertionError(f"vector-index mismatch: {vector_hits}")
258285

286+
if expected_doc_count > DOC_COUNT:
287+
upgrade_hits = collection.query(
288+
queries=Query(
289+
field_name="content", fts=Fts(match_string="upgrademark")
290+
),
291+
topk=expected_doc_count,
292+
)
293+
expected_upgrade_ids = {
294+
f"doc-{ordinal:03d}"
295+
for ordinal in range(DOC_COUNT, expected_doc_count)
296+
}
297+
if {doc.id for doc in upgrade_hits} != expected_upgrade_ids:
298+
raise AssertionError(f"upgraded FTS mismatch: {upgrade_hits}")
299+
300+
last_ordinal = expected_doc_count - 1
301+
new_vector_hits = collection.query(
302+
queries=Query(
303+
field_name="embedding", vector=embedding(last_ordinal)
304+
),
305+
topk=1,
306+
include_vector=True,
307+
)
308+
if len(new_vector_hits) != 1 or new_vector_hits[0].id != (
309+
f"doc-{last_ordinal:03d}"
310+
):
311+
raise AssertionError(f"upgraded vector-index mismatch: {new_vector_hits}")
312+
313+
314+
def validate(collection_path: Path) -> None:
315+
metadata = read_metadata(collection_path)
316+
expected_doc_count = metadata["doc_count"]
317+
collection = zvec.open(
318+
path=str(collection_path),
319+
option=CollectionOption(read_only=True, enable_mmap=True),
320+
)
321+
assert_collection_contents(collection, expected_doc_count)
322+
259323
del collection
260324
gc.collect()
261325
print(
262326
f"validated zvec {metadata['generator_version']} fixture with "
263-
f"zvec {zvec_version()}: fetch, inverted, FTS, and vector indexes pass"
327+
f"zvec {zvec_version()}: {expected_doc_count} docs; fetch, inverted, "
328+
"FTS, and vector indexes pass"
329+
)
330+
331+
332+
def upgrade(source_path: Path, destination_path: Path, overwrite: bool) -> None:
333+
if zvec_version() == GENERATOR_VERSION:
334+
raise RuntimeError(
335+
"upgrade requires the new source build, not the 0.5.1 generator"
336+
)
337+
if source_path == destination_path:
338+
raise ValueError("upgrade destination must differ from the source fixture")
339+
340+
metadata = read_metadata(source_path)
341+
if metadata["doc_count"] != DOC_COUNT:
342+
raise AssertionError("upgrade source must be the untouched 0.5.1 fixture")
343+
344+
destination_sidecar = sidecar_path(destination_path)
345+
if destination_path.exists():
346+
if not overwrite:
347+
raise FileExistsError(
348+
f"{destination_path} already exists; pass --overwrite to replace it"
349+
)
350+
shutil.rmtree(destination_path)
351+
if destination_sidecar.exists() and overwrite:
352+
destination_sidecar.unlink()
353+
354+
destination_path.parent.mkdir(parents=True, exist_ok=True)
355+
shutil.copytree(source_path, destination_path)
356+
shutil.copy2(sidecar_path(source_path), destination_sidecar)
357+
358+
collection = zvec.open(
359+
path=str(destination_path),
360+
option=CollectionOption(read_only=False, enable_mmap=True),
361+
)
362+
363+
# Query the release-created indexes before changing anything.
364+
assert_collection_contents(collection, DOC_COUNT)
365+
366+
require_ok(
367+
collection.insert(
368+
documents(
369+
DOC_COUNT,
370+
UPGRADED_DOC_COUNT,
371+
extra_terms=("upgrademark",),
372+
)
373+
)
374+
)
375+
collection.flush()
376+
collection.optimize()
377+
collection.flush()
378+
379+
# Query both old and newly inserted data through every index type.
380+
assert_collection_contents(collection, UPGRADED_DOC_COUNT)
381+
del collection
382+
gc.collect()
383+
384+
metadata.update(
385+
{
386+
"doc_count": UPGRADED_DOC_COUNT,
387+
"sst_files": sst_files(destination_path),
388+
"upgraded_with_version": zvec_version(),
389+
}
390+
)
391+
destination_sidecar.write_text(
392+
json.dumps(metadata, indent=2, sort_keys=True) + "\n",
393+
encoding="utf-8",
394+
)
395+
396+
# Reopen read-only to prove the mixed-version collection persisted cleanly.
397+
validate(destination_path)
398+
print(
399+
f"upgraded a {GENERATOR_VERSION} fixture with zvec {zvec_version()}: "
400+
f"inserted {UPGRADE_DOC_COUNT} docs and persisted {UPGRADED_DOC_COUNT}"
264401
)
265402

266403

267404
def parse_args() -> argparse.Namespace:
268405
parser = argparse.ArgumentParser(description=__doc__)
269-
parser.add_argument("mode", choices=("generate", "validate"))
406+
parser.add_argument("mode", choices=("generate", "validate", "upgrade"))
270407
parser.add_argument("collection_path", type=Path)
408+
parser.add_argument("destination_path", nargs="?", type=Path)
271409
parser.add_argument(
272410
"--overwrite",
273411
action="store_true",
@@ -279,9 +417,21 @@ def parse_args() -> argparse.Namespace:
279417
def main() -> None:
280418
args = parse_args()
281419
if args.mode == "generate":
420+
if args.destination_path is not None:
421+
raise ValueError("generate accepts one collection path")
282422
generate(args.collection_path.resolve(), args.overwrite)
283-
else:
423+
elif args.mode == "validate":
424+
if args.destination_path is not None:
425+
raise ValueError("validate accepts one collection path")
284426
validate(args.collection_path.resolve())
427+
else:
428+
if args.destination_path is None:
429+
raise ValueError("upgrade requires source and destination paths")
430+
upgrade(
431+
args.collection_path.resolve(),
432+
args.destination_path.resolve(),
433+
args.overwrite,
434+
)
285435

286436

287437
if __name__ == "__main__":

0 commit comments

Comments
 (0)