feat: enhance error handling in local folder indexing by adding rollback and refresh on IntegrityError

This commit is contained in:
Anish Sarkar 2026-04-03 09:29:59 +05:30
parent 9a65163fe4
commit e2ba509314
5 changed files with 27 additions and 10 deletions

View file

@ -19,7 +19,7 @@ from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config from app.config import config
@ -732,7 +732,12 @@ async def index_local_folder(
document.folder_id = folder_mapping.get( document.folder_id = folder_mapping.get(
parent_dir, folder_mapping.get("") parent_dir, folder_mapping.get("")
) )
try:
await session.commit() await session.commit()
except IntegrityError:
await session.rollback()
for document in documents:
await session.refresh(document)
llm = await get_user_long_context_llm(session, user_id, search_space_id) llm = await get_user_long_context_llm(session, user_id, search_space_id)
@ -905,10 +910,14 @@ async def _index_single_file(
# Assign folder_id before indexing so the doc appears in the # Assign folder_id before indexing so the doc appears in the
# correct folder while still pending/processing. # correct folder while still pending/processing.
if root_folder_id: if root_folder_id:
try:
db_doc.folder_id = await _resolve_folder_for_file( db_doc.folder_id = await _resolve_folder_for_file(
session, rel_path, root_folder_id, search_space_id, user_id session, rel_path, root_folder_id, search_space_id, user_id
) )
await session.commit() await session.commit()
except IntegrityError:
await session.rollback()
await session.refresh(db_doc)
await pipeline.index(db_doc, connector_doc, llm) await pipeline.index(db_doc, connector_doc, llm)

View file

@ -237,7 +237,7 @@ export const DocumentNode = React.memo(function DocumentNode({
</DropdownMenuItem> </DropdownMenuItem>
{onExport && ( {onExport && (
<DropdownMenuSub> <DropdownMenuSub>
<DropdownMenuSubTrigger> <DropdownMenuSubTrigger disabled={isProcessing}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Export Export
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
@ -277,7 +277,7 @@ export const DocumentNode = React.memo(function DocumentNode({
</ContextMenuItem> </ContextMenuItem>
{onExport && ( {onExport && (
<ContextMenuSub> <ContextMenuSub>
<ContextMenuSubTrigger> <ContextMenuSubTrigger disabled={isProcessing}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Export Export
</ContextMenuSubTrigger> </ContextMenuSubTrigger>

View file

@ -358,6 +358,14 @@ export function DocumentsSidebar({
const handleDeleteFolder = useCallback(async (folder: FolderDisplay) => { const handleDeleteFolder = useCallback(async (folder: FolderDisplay) => {
if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return;
try { try {
const api = window.electronAPI;
if (api) {
const watchedFolders = await api.getWatchedFolders();
const matched = watchedFolders.find((wf) => wf.rootFolderId === folder.id);
if (matched) {
await api.removeWatchedFolder(matched.path);
}
}
await foldersApiService.deleteFolder(folder.id); await foldersApiService.deleteFolder(folder.id);
toast.success("Folder deleted"); toast.success("Folder deleted");
} catch (e: unknown) { } catch (e: unknown) {

View file

@ -47,7 +47,7 @@ function ContextMenuSubTrigger({
data-slot="context-menu-sub-trigger" data-slot="context-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8",
className className
)} )}
{...props} {...props}

View file

@ -182,7 +182,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-neutral-200 focus:text-accent-foreground dark:focus:bg-neutral-700 data-[state=open]:bg-neutral-200 data-[state=open]:text-accent-foreground dark:data-[state=open]:bg-neutral-700 [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-neutral-200 focus:text-accent-foreground dark:focus:bg-neutral-700 data-[state=open]:bg-neutral-200 data-[state=open]:text-accent-foreground dark:data-[state=open]:bg-neutral-700 [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}