105105OPENAI_VECTOR_STORES_FILES_PREFIX = f"openai_vector_stores_files:{ VERSION } ::"
106106OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX = f"openai_vector_stores_files_contents:{ VERSION } ::"
107107OPENAI_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