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
2630from __future__ import annotations
4751)
4852
4953DOC_COUNT = 256
54+ UPGRADE_DOC_COUNT = 64
55+ UPGRADED_DOC_COUNT = DOC_COUNT + UPGRADE_DOC_COUNT
5056VECTOR_DIMENSION = 8
5157GENERATOR_VERSION = "0.5.1"
5258
@@ -57,20 +63,31 @@ def zvec_version() -> str:
5763
5864def 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+
135168def 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
267404def 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:
279417def 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
287437if __name__ == "__main__" :
0 commit comments