From 589c84fd95a9c33c774f9b3e4d5264804b9e9a00 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 18 Mar 2026 23:40:32 +0100 Subject: [PATCH] Fix remaining blocking I/O calls and N+1 project query - tab_batch_ng.py: async create_batch with to_thread save/sync - tab_raw_ng.py: async do_save with to_thread, replace deepcopy with dict comprehension for display data - main.py: async create_new with to_thread save - tab_projects_ng.py: replace per-project count_data_files with single list_projects_with_file_counts JOIN query - db.py: add list_projects_with_file_counts method Zero blocking I/O calls remain in UI callbacks. Co-Authored-By: Claude Opus 4.6 --- db.py | 10 ++++++++++ main.py | 4 ++-- tab_batch_ng.py | 6 +++--- tab_projects_ng.py | 9 ++++----- tab_raw_ng.py | 15 +++++++-------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/db.py b/db.py index 9b14416..8af5ff5 100644 --- a/db.py +++ b/db.py @@ -93,6 +93,16 @@ class ProjectDB: ).fetchall() return [dict(r) for r in rows] + def list_projects_with_file_counts(self) -> list[dict]: + """List projects with data file counts in a single query.""" + rows = self.conn.execute( + "SELECT p.id, p.name, p.folder_path, p.description, p.created_at, p.updated_at, " + "COUNT(df.id) AS file_count " + "FROM projects p LEFT JOIN data_files df ON df.project_id = p.id " + "GROUP BY p.id ORDER BY p.name" + ).fetchall() + return [dict(r) for r in rows] + def get_project(self, name: str) -> dict | None: row = self.conn.execute( "SELECT id, name, folder_path, description, created_at, updated_at " diff --git a/main.py b/main.py index c98ff18..ad29c37 100644 --- a/main.py +++ b/main.py @@ -476,7 +476,7 @@ def render_sidebar(state: AppState, dual_pane: dict): with ui.expansion('Create New JSON'): new_fn_input = ui.input('Filename', placeholder='my_prompt_vace').classes('w-full') - def create_new(): + async def create_new(): fn = new_fn_input.value if not fn: return @@ -485,7 +485,7 @@ def render_sidebar(state: AppState, dual_pane: dict): path = state.current_dir / fn first_item = copy.deepcopy(DEFAULTS) first_item[KEY_SEQUENCE_NUMBER] = 1 - save_json(path, {KEY_BATCH_DATA: [first_item]}) + await asyncio.to_thread(save_json, path, {KEY_BATCH_DATA: [first_item]}) new_fn_input.set_value('') render_file_list.refresh() diff --git a/tab_batch_ng.py b/tab_batch_ng.py index c9895d5..0da999c 100644 --- a/tab_batch_ng.py +++ b/tab_batch_ng.py @@ -199,7 +199,7 @@ def render_batch_processor(state: AppState): ui.label('This is a Single file. To use Batch mode, create a copy.').classes( 'text-warning') - def create_batch(): + async def create_batch(): new_name = f'batch_{file_path.name}' new_path = file_path.parent / new_name if new_path.exists(): @@ -211,9 +211,9 @@ def render_batch_processor(state: AppState): first_item[KEY_SEQUENCE_NUMBER] = 1 new_data = {KEY_BATCH_DATA: [first_item], KEY_HISTORY_TREE: {}, KEY_PROMPT_HISTORY: []} - save_json(new_path, new_data) + await asyncio.to_thread(save_json, new_path, new_data) if state.db_enabled and state.current_project and state.db: - sync_to_db(state.db, state.current_project, new_path, new_data) + await asyncio.to_thread(sync_to_db, state.db, state.current_project, new_path, new_data) ui.notify(f'Created {new_name}', type='positive') ui.button('Create Batch Copy', icon='content_copy', on_click=create_batch) diff --git a/tab_projects_ng.py b/tab_projects_ng.py index 4dd825f..ddc1570 100644 --- a/tab_projects_ng.py +++ b/tab_projects_ng.py @@ -60,8 +60,8 @@ def render_projects_tab(state: AppState): ui.button('Create Project', icon='add', on_click=create_project).classes('w-full') # --- Active project indicator --- - # Fetch once and reuse in render_project_list - _cached_projects = state.db.list_projects() + # Fetch once with file counts and reuse in render_project_list + _cached_projects = state.db.list_projects_with_file_counts() if state.current_project: # Check if active project actually exists in the database @@ -102,7 +102,7 @@ def render_projects_tab(state: AppState): @ui.refreshable def render_project_list(): nonlocal _cached_projects - projects = state.db.list_projects() + projects = state.db.list_projects_with_file_counts() _cached_projects = projects if not projects: ui.label('No projects yet. Create one above.').classes('text-caption q-pa-md') @@ -119,8 +119,7 @@ def render_projects_tab(state: AppState): if proj['description']: ui.label(proj['description']).classes('text-caption') ui.label(f'Path: {proj["folder_path"]}').classes('text-caption') - file_count = state.db.count_data_files(proj['id']) - ui.label(f'{file_count} data file(s)').classes('text-caption') + ui.label(f'{proj["file_count"]} data file(s)').classes('text-caption') with ui.row().classes('q-gutter-xs'): if not is_active: diff --git a/tab_raw_ng.py b/tab_raw_ng.py index bdc4933..a68680c 100644 --- a/tab_raw_ng.py +++ b/tab_raw_ng.py @@ -1,4 +1,4 @@ -import copy +import asyncio import json from nicegui import ui @@ -21,11 +21,10 @@ def render_raw_editor(state: AppState): @ui.refreshable def render_editor(): - # Prepare display data + # Prepare display data — shallow copy, just pop keys if hide_history.value: - display_data = copy.deepcopy(data) - display_data.pop(KEY_HISTORY_TREE, None) - display_data.pop(KEY_PROMPT_HISTORY, None) + display_data = {k: v for k, v in data.items() + if k not in (KEY_HISTORY_TREE, KEY_PROMPT_HISTORY)} else: display_data = data @@ -40,7 +39,7 @@ def render_raw_editor(state: AppState): value=json_str, ).classes('w-full font-mono').props('outlined rows=30') - def do_save(): + async def do_save(): try: input_data = json.loads(text_area.value) @@ -51,9 +50,9 @@ def render_raw_editor(state: AppState): if KEY_PROMPT_HISTORY in data: input_data[KEY_PROMPT_HISTORY] = data[KEY_PROMPT_HISTORY] - save_json(file_path, input_data) + await asyncio.to_thread(save_json, file_path, input_data) if state.db_enabled and state.current_project and state.db: - sync_to_db(state.db, state.current_project, file_path, input_data) + await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, input_data) data.clear() data.update(input_data)