- ResultsPanel: _RowNumberProxy keeps vertical header numbers sequential in visual order (sort-stable); both views get a 48 px wide row-number header - MainWindow: _PingWorker pings each active connection every 30 s via test_connection(); status dot (green/red) shown in status bar with tooltip - TableViewer: 📌 toggle freezes first column using dual-view overlay (_frozen_view child widget) with synced scroll, row heights, and resize event-filter; column 0 is hidden in the main view while frozen - RowDialog: OK set as default button; first field focused on open - ConnectionDialog: OK set as default; Enter in password field submits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
DBClient is a desktop database client built with Python + PyQt6, supporting MySQL, PostgreSQL, SQLite, and MSSQL. Inspired by DBeaver / TablePlus.
Development Commands
Install dependencies:
pip install -r requirements.txt
Key runtime dependencies: PyQt6, sqlparse>=0.5.0, bcrypt, keyring, cryptography.
Run the application:
python main.py
There is no test suite or linter config set up.
Build distributable (Windows):
pip install pyinstaller
python build_app.py # one-folder bundle → dist/DBClient/
python build_app.py --onefile # single .exe → dist/DBClient.exe
python build_app.py --clean # wipe build/ and dist/ first
The PyInstaller spec is DBClient.spec; add new resource files / hidden imports there.
Architecture
Driver Abstraction (app/drivers/)
All database operations go through BaseDriver (app/drivers/base.py). The four concrete drivers (mysql, postgres, sqlite, mssql) each implement the same interface — the UI never imports a specific driver directly. Use the factory get_driver(db_type, config) from app/drivers/__init__.py. Adding a new DB type means subclassing BaseDriver and registering it in the factory.
Key data types defined in base.py: ColumnInfo, IndexInfo, ForeignKeyInfo, TableInfo.
Async DB Operations (app/utils/worker.py)
All database I/O runs in background threads. The pattern is:
- Create a
QueryWorker/SchemaWorker/TableDataWorker(subclasses ofQRunnable/QThread) - Connect
signals.result/signals.errorto UI slots - The worker calls the driver and emits results — never call driver methods directly from the GUI thread.
All worker errors are logged with exc_info=True (full traceback) via app.utils.logger.
Logging (app/utils/logger.py)
Call get_logger(__name__) anywhere to get a module logger. setup_logging() is called once in main.py and initialises:
TimedRotatingFileHandler→~/.dbclient/logs/dbclient.log(daily rotation, 7-day retention)StreamHandlerto stderr (WARNING+ only, for dev)sys.excepthookandthreading.excepthookto capture uncaught exceptions with full tracebacksqInstallMessageHandlerto capture Qt internal warnings
View logs inside the app via Help → View App Logs (Ctrl+L).
Signal/Slot Flow
SchemaBrowser (left sidebar) emits signals when the user double-clicks a table or selects a database. MainWindow connects these to open workspace tabs (TableViewer, TableStructure, SQLEditor). New UI interactions should follow this pattern: sidebar emits, main window routes, tabs receive.
Data Persistence
All user data lives under ~/.dbclient/:
connections.json— connection profiles (passwords omitted; stored in OS keychain viakeyring)settings.json— app-wide preferences (theme, font size, page size, timeouts)history.db— SQLite store for query history
app/config/connections.py handles profile CRUD + keyring integration. app/config/settings.py is a settings singleton.
UI Structure
MainWindow (app/main_window.py) owns a QSplitter with SchemaBrowser on the left and a QTabWidget workspace on the right. Workspace tabs are created dynamically: SQLEditor for query tabs, TableViewer for data browsing, TableStructure for DDL inspection.
The ResultTableModel (app/models/result_table_model.py) is a QAbstractTableModel — query results should always go through it rather than populating QTableWidget directly.
Styling
resources/style.qss is a Catppuccin Frappé dark theme applied globally at startup in main.py. Widget-specific overrides belong here, not as inline setStyleSheet() calls. Key palette values: base #303446, surface0 #414559, surface1 #51576d, text #c6d0f5, blue #8caaee, green #a6d189, red #e78284, peach #ef9f76, mauve #ca9ee6, sky #99d1db.
Recent Files (app/config/recent_files.py)
load_recent() / add_recent(path) / clear_recent() persist to ~/.dbclient/recent_files.json (max 10 entries, dead paths filtered on load). Called from MainWindow File menu.
Settings Cascade
PreferencesDialog emits settings_applied → MainWindow._apply_settings_to_open_tabs() iterates workspace tabs and calls widget.apply_settings() on each. SQLEditorWidget.apply_settings() cascades to each EditorTab, which cascades to CodeEditor. Add apply_settings() to any new workspace widget that reads from get_settings().
Known Quirks
TableViewer selection signals
QItemSelectionModel.selectionChanged becomes unreliable after beginResetModel/endResetModel cycles (triggered on every data load). Edit/Delete button enabling is driven by QTableView.clicked (primary, mouse) and selectionChanged (secondary, keyboard). Do not remove the clicked connection — removing it re-breaks the buttons.
NULL-aware WHERE clauses
All four drivers have a _where(where: dict) → (clause_str, params) static helper that emits col IS NULL for None values instead of col = NULL. All update_row and delete_row calls go through this helper. Do not bypass it.
Password hashing in RowDialog
RowDialog detects password-like column names (via _is_password_col()) and hashes plain-text input with bcrypt before storing. In edit mode, leaving a password field empty omits that column from the UPDATE so the existing hash is preserved.
ResultsPanel multi-tab architecture
ResultsPanel uses a QStackedWidget with two pages: page 0 = QTabWidget (result tabs), page 1 = loading spinner. Single-query results hide the tab bar; script results (show_script_results()) show named tabs. Each tab owns its own ResultTableModel + QSortFilterProxyModel. Export buttons operate on _current_model() from the active tab.
SQL formatter
EditorTab._format_sql() uses sqlparse.format(..., reindent=True, keyword_case='upper'). Requires sqlparse>=0.5.0 in requirements.txt.
Column statistics worker
_StatsWorker in app/ui/column_stats_dialog.py runs COUNT/COUNT(col)/COUNT(DISTINCT)/MIN/MAX in one query and AVG in a second (AVG silently returns None for non-numeric columns). Both queries are quoted with "col" and "table" to handle reserved words.
Implementation Status
Complete
- Connection manager (add/edit/delete, test, color coding, keyring storage)
- Schema browser (live tree: connections → databases → tables/views/functions/procedures/triggers, search, context menus, delete connection from active node)
- SQL editor (multi-tab, syntax highlighting, line numbers, Ctrl+/ comment toggle, F5/Ctrl+Enter execute)
- SQL auto-complete (
app/ui/sql_completer.py): schema-aware popup, dot-notation (schema.table, table.col), Ctrl+Space trigger - Find/Replace bar in SQL editor (
app/ui/find_bar.py): Ctrl+F find, Ctrl+H replace, wrap-around, case-sensitive toggle - SQL formatter:
≡ Formatbutton in editor toolbar using sqlparse (reindent + uppercase keywords) - Results panel (sortable
QAbstractTableModel, export CSV/JSON/SQL INSERT, pagination, execution time) - Multi-result tabs: scripts with multiple SELECTs show each result in a named sub-tab within ResultsPanel
- Table viewer (paginated grid 50/100/All, add/edit/delete rows via dialog, WHERE filter, bcrypt password hashing)
- Enhanced table context menu: "Filter by this value" (auto-populates WHERE filter), "Copy row as SQL INSERT"
- Column statistics dialog (
app/ui/column_stats_dialog.py): right-click column header → async COUNT/NULL/DISTINCT/MIN/MAX/AVG - Table structure view (columns, indexes, FKs, DDL with syntax highlighting, add/drop/rename column designer)
- Query history (auto-log with timestamp/duration/status, search, replay, persisted in
history.db) - All 4 DB drivers (MySQL, PostgreSQL, SQLite, MSSQL)
- Process list viewer (
app/ui/process_list.py) with 5 s auto-refresh and kill query - Import CSV/JSON dialog (
app/ui/import_dialog.py) with preview and progress bar - Database dump export (
app/ui/dump_dialog.py): schema/data/both, table selector, progress bar - Tools menu in MainWindow (Process List, Import CSV/JSON, Export Database Dump, User Management, Preferences)
- Preferences dialog (
app/ui/preferences_dialog.py): Ctrl+, / Tools → Preferences; font family/size/word-wrap, page size, timeout, history limit, auto-commit; live-applies to all open editors viasettings_appliedsignal - PyInstaller packaging (
DBClient.spec+build_app.py) - User & privilege management (
app/ui/user_manager.py): list/create/drop users, GRANT/REVOKE per-DB (MySQL + PostgreSQL) - EXPLAIN plan diagram (
app/ui/explain_view.py): visual node tree + raw table; opened from SQL editor "🔎 Explain" button - Application logging (
app/utils/logger.py+app/ui/log_viewer.py): rotating file logs, unhandled exception hooks, in-app log viewer (Help → View App Logs) - Keyboard shortcuts dialog (
app/ui/shortcuts_dialog.py): Help → Keyboard Shortcuts; categorised tree of all key bindings - Tab context menus: workspace tabs (Rename/Close/Close Others/Close to Right) + SQL query tabs (Rename/Duplicate/Close/Close Others)
- Smart status bar: right-side connection indicator + 60-char cell value preview (updates on cell click)
- Connection tab coloring: workspace tab labels colored by connection profile color
- File open/save/recent: Ctrl+O open SQL file, Ctrl+S save, File → Open Recent (persisted to
~/.dbclient/recent_files.json, max 10) - Ctrl+W to close current workspace tab
- Catppuccin Frappé theme: full palette upgrade from Mocha; all colors updated in
resources/style.qss, syntax highlighter, completer popup, and inline Python stylesheets - Row numbers column in results grid:
_RowNumberProxyshows sequential visual-order numbers in the vertical header (sort-stable); fixed 48 px wide header in bothResultsPanelandTableViewer - Connection ping indicator: 30-second
_PingWorker(QThread) pings each active connection viatest_connection(); status dot (● green / ● red) shown in status bar right side with per-connection tooltip - Frozen first column in
TableViewer: 📌 toggle button in toolbar; dual-view overlay (_frozen_viewchild of_table_view) with synced vertical scroll and row heights; event-filter updates geometry on resize - Keyboard navigation in dialogs: OK set as default button (Enter submits) in
RowDialogandConnectionDialog; focus jumps to first input field on open; Enter in password field submitsConnectionDialog
Not Yet Implemented
High impact:
- Auto-reconnect on dropped connection (detect broken pipe, reconnect transparently)
- Quick result filter: live filter bar above the results table (client-side, no re-query)
- SQL snippets / bookmarks: save and recall frequently used query fragments
- ERD viewer: visual entity-relationship diagram from live schema
Medium impact:
- Transaction panel: explicit BEGIN / COMMIT / ROLLBACK controls with in-flight indicator
- Global schema search: search across all tables/columns/procedures in a database
- Light theme: Catppuccin Latte or similar, toggle from Preferences
- Schema diff: compare two database schemas and show structural differences