Skip to content

Commit cb819dd

Browse files
committed
fix(vector-io): Restore resumable SQL metadata migration and adapter initialization.
Signed-off-by: Sébastien Han <seb@redhat.com>
1 parent 820a843 commit cb819dd

6 files changed

Lines changed: 137 additions & 99 deletions

File tree

src/ogx/providers/remote/vector_io/chroma/chroma.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ async def initialize(self) -> None:
281281
else:
282282
log.info(f"Connecting to Chroma local db at: {self.config.db_path}")
283283
self.client = chromadb.PersistentClient(path=self.config.db_path)
284-
self.openai_vector_stores = await self._load_openai_vector_stores()
284+
await self.initialize_openai_vector_stores()
285285

286286
async def shutdown(self) -> None:
287287
# Clean up mixin resources (file batch tasks)

src/ogx/providers/remote/vector_io/elasticsearch/elasticsearch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ async def initialize(self) -> None:
418418
vector_store, ElasticsearchIndex(self.client, vector_store.identifier), self.inference_api
419419
)
420420
self.cache[vector_store.identifier] = index
421-
self.openai_vector_stores = await self._load_openai_vector_stores()
421+
await self.initialize_openai_vector_stores()
422422

423423
async def shutdown(self) -> None:
424424
await self.client.close()

src/ogx/providers/remote/vector_io/infinispan/infinispan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ async def initialize(self) -> None:
612612
await self._load_vector_stores_from_kvstore()
613613

614614
# Initialize OpenAI vector stores
615-
self.openai_vector_stores = await self._load_openai_vector_stores()
615+
await self.initialize_openai_vector_stores()
616616

617617
async def _load_vector_stores_from_kvstore(self) -> None:
618618
"""

src/ogx/providers/remote/vector_io/qdrant/qdrant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ async def initialize(self) -> None:
366366
vector_store, QdrantIndex(self.client, vector_store.identifier), self.inference_api
367367
)
368368
self.cache[vector_store.identifier] = index
369-
self.openai_vector_stores = await self._load_openai_vector_stores()
369+
await self.initialize_openai_vector_stores()
370370

371371
async def shutdown(self) -> None:
372372
await self.client.close()

src/ogx/providers/utils/memory/openai_vector_store_mixin.py

Lines changed: 105 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
OPENAI_VECTOR_STORES_FILES_PREFIX = f"openai_vector_stores_files:{VERSION}::"
106106
OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX = f"openai_vector_stores_files_contents:{VERSION}::"
107107
OPENAI_VECTOR_STORES_FILE_BATCHES_PREFIX = f"openai_vector_stores_file_batches:{VERSION}::"
108+
OPENAI_VECTOR_STORES_SQL_MIGRATION_KEY = f"openai_vector_stores_sql_migration:{VERSION}"
108109

109110

110111
_RETRIABLE_STATUS_CODES = {429, 502, 503, 504}
@@ -205,13 +206,28 @@ async def _create_metadata_tables(self) -> None:
205206
},
206207
)
207208

209+
async def _fetch_all_metadata_rows_unfiltered(self, table: str, **kwargs: Any) -> list[dict[str, Any]]:
210+
"""Fetch rows from metadata tables without request-scoped ACL filtering.
211+
212+
Startup and migration paths run without an authenticated request user, so
213+
AuthorizedSqlStore filtering would hide tenant-owned rows. For internal
214+
provider bookkeeping we need the full table contents.
215+
"""
216+
assert self.metadata_store is not None
217+
results = await self.metadata_store.sql_store.fetch_all(table=table, **kwargs)
218+
return results.data
219+
220+
async def _fetch_one_metadata_row_unfiltered(self, table: str, **kwargs: Any) -> dict[str, Any] | None:
221+
rows = await self._fetch_all_metadata_rows_unfiltered(table=table, limit=1, **kwargs)
222+
return rows[0] if rows else None
223+
208224
async def _migrate_kvstore_to_sql(self) -> None:
209225
"""Migrate vector store metadata from KVStore to SQL on first run after upgrade.
210226
211227
When a deployment upgrades from KVStore-only storage to SQL-backed metadata_store,
212-
this method copies all existing vector store data into the new SQL tables. It runs
213-
once during initialization when both backends are configured and the SQL tables are
214-
empty.
228+
this method copies all existing vector store data into the new SQL tables. Migration
229+
completion is tracked with a KV marker key, and row-level upserts make retries safe
230+
after crashes or restarts.
215231
216232
Migrated records are inserted with owner_principal="" and access_attributes=None
217233
(the "unowned" marker), making them accessible to all authenticated users. This is
@@ -223,108 +239,111 @@ async def _migrate_kvstore_to_sql(self) -> None:
223239
assert self.metadata_store is not None
224240
assert self.kvstore is not None
225241

242+
migration_complete = await self.kvstore.get(OPENAI_VECTOR_STORES_SQL_MIGRATION_KEY)
243+
if migration_complete == "1":
244+
return
245+
226246
sql_store = self.metadata_store.sql_store
227247

228248
stores_data = await self.kvstore.values_in_range(
229249
OPENAI_VECTOR_STORES_PREFIX, f"{OPENAI_VECTOR_STORES_PREFIX}\xff"
230250
)
231251
if not stores_data:
252+
await self.kvstore.set(key=OPENAI_VECTOR_STORES_SQL_MIGRATION_KEY, value="1")
232253
return
233254

234255
migrated_stores = 0
235256
migrated_files = 0
236257
migrated_chunks = 0
237258
migrated_batches = 0
238259

239-
# Per-table migration: each table is checked independently so a crash
240-
# mid-migration doesn't skip remaining tables on the next boot.
241-
existing_stores = await sql_store.fetch_all(table=TABLE_VECTOR_STORES, limit=1)
242-
if not existing_stores.data:
243-
logger.info(
244-
"Starting KVStore to SQL migration for vector store metadata",
245-
store_count=len(stores_data),
260+
logger.info(
261+
"Starting KVStore to SQL migration for vector store metadata",
262+
store_count=len(stores_data),
263+
)
264+
265+
for raw in stores_data:
266+
info = json.loads(raw)
267+
store_id = info["id"]
268+
await sql_store.upsert(
269+
table=TABLE_VECTOR_STORES,
270+
data={
271+
"id": store_id,
272+
"store_data": info,
273+
"owner_principal": "",
274+
"access_attributes": None,
275+
},
276+
conflict_columns=["id"],
277+
update_columns=["store_data"],
278+
)
279+
migrated_stores += 1
280+
281+
file_keys = await self.kvstore.keys_in_range(
282+
f"{OPENAI_VECTOR_STORES_FILES_PREFIX}{store_id}:",
283+
f"{OPENAI_VECTOR_STORES_FILES_PREFIX}{store_id}:\xff",
246284
)
247-
for raw in stores_data:
248-
info = json.loads(raw)
249-
store_id = info["id"]
250-
await sql_store.insert(
251-
table=TABLE_VECTOR_STORES,
285+
for file_key in file_keys:
286+
suffix = file_key[len(OPENAI_VECTOR_STORES_FILES_PREFIX) :]
287+
file_id = suffix.split(":", 1)[1] if ":" in suffix else suffix
288+
raw_file = await self.kvstore.get(file_key)
289+
if not raw_file:
290+
continue
291+
file_info = json.loads(raw_file)
292+
await sql_store.upsert(
293+
table=TABLE_VECTOR_STORE_FILES,
252294
data={
253-
"id": store_id,
254-
"store_data": info,
295+
"id": f"{store_id}:{file_id}",
296+
"store_id": store_id,
297+
"file_id": file_id,
298+
"file_data": file_info,
255299
"owner_principal": "",
256300
"access_attributes": None,
257301
},
302+
conflict_columns=["id"],
303+
update_columns=["store_id", "file_id", "file_data"],
258304
)
259-
migrated_stores += 1
260-
261-
existing_files = await sql_store.fetch_all(table=TABLE_VECTOR_STORE_FILES, limit=1)
262-
if not existing_files.data:
263-
for raw in stores_data:
264-
info = json.loads(raw)
265-
store_id = info["id"]
266-
file_keys = await self.kvstore.keys_in_range(
267-
f"{OPENAI_VECTOR_STORES_FILES_PREFIX}{store_id}:",
268-
f"{OPENAI_VECTOR_STORES_FILES_PREFIX}{store_id}:\xff",
269-
)
270-
for file_key in file_keys:
271-
suffix = file_key[len(OPENAI_VECTOR_STORES_FILES_PREFIX) :]
272-
file_id = suffix.split(":", 1)[1] if ":" in suffix else suffix
273-
raw_file = await self.kvstore.get(file_key)
274-
if not raw_file:
275-
continue
276-
file_info = json.loads(raw_file)
277-
await sql_store.insert(
278-
table=TABLE_VECTOR_STORE_FILES,
305+
migrated_files += 1
306+
307+
chunk_prefix = f"{OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX}{store_id}:{file_id}:"
308+
chunk_values = await self.kvstore.values_in_range(chunk_prefix, f"{chunk_prefix}\xff")
309+
for idx, raw_chunk in enumerate(chunk_values):
310+
chunk = json.loads(raw_chunk)
311+
await sql_store.upsert(
312+
table=TABLE_VECTOR_STORE_FILE_CONTENTS,
279313
data={
280-
"id": f"{store_id}:{file_id}",
314+
"id": f"{store_id}:{file_id}:{idx}",
281315
"store_id": store_id,
282316
"file_id": file_id,
283-
"file_data": file_info,
317+
"chunk_index": idx,
318+
"chunk_data": chunk,
284319
"owner_principal": "",
285320
"access_attributes": None,
286321
},
322+
conflict_columns=["id"],
323+
update_columns=["store_id", "file_id", "chunk_index", "chunk_data"],
287324
)
288-
migrated_files += 1
289-
290-
chunk_prefix = f"{OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX}{store_id}:{file_id}:"
291-
chunk_values = await self.kvstore.values_in_range(chunk_prefix, f"{chunk_prefix}\xff")
292-
for idx, raw_chunk in enumerate(chunk_values):
293-
chunk = json.loads(raw_chunk)
294-
await sql_store.insert(
295-
table=TABLE_VECTOR_STORE_FILE_CONTENTS,
296-
data={
297-
"id": f"{store_id}:{file_id}:{idx}",
298-
"store_id": store_id,
299-
"file_id": file_id,
300-
"chunk_index": idx,
301-
"chunk_data": chunk,
302-
"owner_principal": "",
303-
"access_attributes": None,
304-
},
305-
)
306-
migrated_chunks += 1
325+
migrated_chunks += 1
307326

308-
existing_batches = await sql_store.fetch_all(table=TABLE_VECTOR_STORE_FILE_BATCHES, limit=1)
309-
if not existing_batches.data:
310-
batch_data = await self.kvstore.values_in_range(
311-
OPENAI_VECTOR_STORES_FILE_BATCHES_PREFIX, f"{OPENAI_VECTOR_STORES_FILE_BATCHES_PREFIX}\xff"
327+
batch_data = await self.kvstore.values_in_range(
328+
OPENAI_VECTOR_STORES_FILE_BATCHES_PREFIX, f"{OPENAI_VECTOR_STORES_FILE_BATCHES_PREFIX}\xff"
329+
)
330+
for raw_batch in batch_data:
331+
batch_info = json.loads(raw_batch)
332+
batch_id = batch_info["id"]
333+
await sql_store.upsert(
334+
table=TABLE_VECTOR_STORE_FILE_BATCHES,
335+
data={
336+
"id": batch_id,
337+
"store_id": batch_info.get("vector_store_id", ""),
338+
"batch_data": batch_info,
339+
"expires_at": batch_info.get("expires_at", 0),
340+
"owner_principal": "",
341+
"access_attributes": None,
342+
},
343+
conflict_columns=["id"],
344+
update_columns=["store_id", "batch_data", "expires_at"],
312345
)
313-
for raw_batch in batch_data:
314-
batch_info = json.loads(raw_batch)
315-
batch_id = batch_info["id"]
316-
await sql_store.insert(
317-
table=TABLE_VECTOR_STORE_FILE_BATCHES,
318-
data={
319-
"id": batch_id,
320-
"store_id": batch_info.get("vector_store_id", ""),
321-
"batch_data": batch_info,
322-
"expires_at": batch_info.get("expires_at", 0),
323-
"owner_principal": "",
324-
"access_attributes": None,
325-
},
326-
)
327-
migrated_batches += 1
346+
migrated_batches += 1
328347

329348
if migrated_stores or migrated_files or migrated_chunks or migrated_batches:
330349
logger.info(
@@ -335,6 +354,8 @@ async def _migrate_kvstore_to_sql(self) -> None:
335354
batches=migrated_batches,
336355
)
337356

357+
await self.kvstore.set(key=OPENAI_VECTOR_STORES_SQL_MIGRATION_KEY, value="1")
358+
338359
async def _save_openai_vector_store(self, store_id: str, store_info: dict[str, Any]) -> None:
339360
"""Save vector store metadata to persistent storage."""
340361
if self.metadata_store:
@@ -386,9 +407,9 @@ async def _ensure_openai_metadata_exists(self, vector_store: VectorStore, name:
386407
async def _load_openai_vector_stores(self) -> dict[str, dict[str, Any]]:
387408
"""Load all vector store metadata from persistent storage."""
388409
if self.metadata_store:
389-
results = await self.metadata_store.fetch_all(table=TABLE_VECTOR_STORES)
390410
stores: dict[str, dict[str, Any]] = {}
391-
for row in results.data:
411+
rows = await self._fetch_all_metadata_rows_unfiltered(table=TABLE_VECTOR_STORES)
412+
for row in rows:
392413
info = row["store_data"]
393414
stores[info["id"]] = info
394415
return stores
@@ -466,7 +487,7 @@ async def _save_openai_vector_store_file(
466487
async def _load_openai_vector_store_file(self, store_id: str, file_id: str) -> dict[str, Any]:
467488
"""Load vector store file metadata from persistent storage."""
468489
if self.metadata_store:
469-
row = await self.metadata_store.fetch_one(
490+
row = await self._fetch_one_metadata_row_unfiltered(
470491
table=TABLE_VECTOR_STORE_FILES,
471492
where={"store_id": store_id, "file_id": file_id},
472493
)
@@ -480,12 +501,12 @@ async def _load_openai_vector_store_file(self, store_id: str, file_id: str) -> d
480501
async def _load_openai_vector_store_file_contents(self, store_id: str, file_id: str) -> list[dict[str, Any]]:
481502
"""Load vector store file contents from persistent storage."""
482503
if self.metadata_store:
483-
results = await self.metadata_store.fetch_all(
504+
rows = await self._fetch_all_metadata_rows_unfiltered(
484505
table=TABLE_VECTOR_STORE_FILE_CONTENTS,
485506
where={"store_id": store_id, "file_id": file_id},
486507
order_by=[("chunk_index", "asc")],
487508
)
488-
return [row["chunk_data"] for row in results.data]
509+
return [row["chunk_data"] for row in rows]
489510
else:
490511
assert self.kvstore
491512
prefix = f"{OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX}{store_id}:{file_id}:"
@@ -548,9 +569,9 @@ async def _save_openai_vector_store_file_batch(self, batch_id: str, batch_info:
548569
async def _load_openai_vector_store_file_batches(self) -> dict[str, dict[str, Any]]:
549570
"""Load all file batch metadata from persistent storage."""
550571
if self.metadata_store:
551-
results = await self.metadata_store.fetch_all(table=TABLE_VECTOR_STORE_FILE_BATCHES)
552572
batches: dict[str, dict[str, Any]] = {}
553-
for row in results.data:
573+
rows = await self._fetch_all_metadata_rows_unfiltered(table=TABLE_VECTOR_STORE_FILE_BATCHES)
574+
for row in rows:
554575
info = row["batch_data"]
555576
batches[info["id"]] = info
556577
return batches

0 commit comments

Comments
 (0)