@@ -473,54 +473,77 @@ export async function restoreKnowledgeBase(
473473 }
474474 }
475475
476- const newName = await generateRestoreName ( kb . name , async ( candidate ) => {
477- if ( ! kb . workspaceId ) return false
478- const [ match ] = await db
479- . select ( { id : knowledgeBase . id } )
480- . from ( knowledgeBase )
481- . where (
482- and (
483- eq ( knowledgeBase . workspaceId , kb . workspaceId ) ,
484- eq ( knowledgeBase . name , candidate ) ,
485- isNull ( knowledgeBase . deletedAt )
486- )
487- )
488- . limit ( 1 )
489- return ! ! match
490- } )
476+ /**
477+ * A concurrent create/rename can commit the same active name after `generateRestoreName`'s check
478+ * (MVCC) and before this transaction commits. Retries pick a new random suffix; 23505 is still
479+ * mapped to {@link KnowledgeBaseConflictError} if exhaustion occurs.
480+ */
481+ const maxUniqueViolationRetries = 8
482+ let attemptedRestoreName = ''
483+
484+ for ( let attempt = 0 ; attempt < maxUniqueViolationRetries ; attempt ++ ) {
485+ attemptedRestoreName = ''
486+ try {
487+ await db . transaction ( async ( tx ) => {
488+ await tx . execute ( sql `SELECT 1 FROM knowledge_base WHERE id = ${ knowledgeBaseId } FOR UPDATE` )
489+
490+ attemptedRestoreName = await generateRestoreName ( kb . name , async ( candidate ) => {
491+ if ( ! kb . workspaceId ) return false
492+ const [ match ] = await tx
493+ . select ( { id : knowledgeBase . id } )
494+ . from ( knowledgeBase )
495+ . where (
496+ and (
497+ eq ( knowledgeBase . workspaceId , kb . workspaceId ) ,
498+ eq ( knowledgeBase . name , candidate ) ,
499+ isNull ( knowledgeBase . deletedAt )
500+ )
501+ )
502+ . limit ( 1 )
503+ return ! ! match
504+ } )
491505
492- const now = new Date ( )
506+ const now = new Date ( )
493507
494- await db . transaction ( async ( tx ) => {
495- await tx . execute ( sql `SELECT 1 FROM knowledge_base WHERE id = ${ knowledgeBaseId } FOR UPDATE` )
508+ await tx
509+ . update ( knowledgeBase )
510+ . set ( { deletedAt : null , updatedAt : now , name : attemptedRestoreName } )
511+ . where ( eq ( knowledgeBase . id , knowledgeBaseId ) )
496512
497- await tx
498- . update ( knowledgeBase )
499- . set ( { deletedAt : null , updatedAt : now , name : newName } )
500- . where ( eq ( knowledgeBase . id , knowledgeBaseId ) )
501-
502- await tx
503- . update ( document )
504- . set ( { archivedAt : null } )
505- . where (
506- and (
507- eq ( document . knowledgeBaseId , knowledgeBaseId ) ,
508- isNotNull ( document . archivedAt ) ,
509- isNull ( document . deletedAt )
510- )
511- )
513+ await tx
514+ . update ( document )
515+ . set ( { archivedAt : null } )
516+ . where (
517+ and (
518+ eq ( document . knowledgeBaseId , knowledgeBaseId ) ,
519+ isNotNull ( document . archivedAt ) ,
520+ isNull ( document . deletedAt )
521+ )
522+ )
512523
513- await tx
514- . update ( knowledgeConnector )
515- . set ( { archivedAt : null , status : 'active' , updatedAt : now } )
516- . where (
517- and (
518- eq ( knowledgeConnector . knowledgeBaseId , knowledgeBaseId ) ,
519- isNotNull ( knowledgeConnector . archivedAt ) ,
520- isNull ( knowledgeConnector . deletedAt )
521- )
522- )
523- } )
524+ await tx
525+ . update ( knowledgeConnector )
526+ . set ( { archivedAt : null , status : 'active' , updatedAt : now } )
527+ . where (
528+ and (
529+ eq ( knowledgeConnector . knowledgeBaseId , knowledgeBaseId ) ,
530+ isNotNull ( knowledgeConnector . archivedAt ) ,
531+ isNull ( knowledgeConnector . deletedAt )
532+ )
533+ )
534+ } )
535+ break
536+ } catch ( error : unknown ) {
537+ if ( getPostgresErrorCode ( error ) !== '23505' ) {
538+ throw error
539+ }
540+ if ( attempt === maxUniqueViolationRetries - 1 ) {
541+ throw new KnowledgeBaseConflictError ( attemptedRestoreName || kb . name )
542+ }
543+ }
544+ }
524545
525- logger . info ( `[${ requestId } ] Restored knowledge base: ${ knowledgeBaseId } as "${ newName } "` )
546+ logger . info (
547+ `[${ requestId } ] Restored knowledge base: ${ knowledgeBaseId } as "${ attemptedRestoreName } "`
548+ )
526549}
0 commit comments