6.4 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
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 Mocha dark theme applied globally at startup in main.py. Widget-specific overrides belong here, not as inline setStyleSheet() calls.
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.
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)
- Results panel (sortable
QAbstractTableModel, export CSV/JSON/SQL INSERT, pagination, execution time) - Table viewer (paginated grid 50/100/All, add/edit/delete rows via dialog, WHERE filter, bcrypt password hashing)
- 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)
- 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)
Not Yet Implemented
- (All planned features are now complete)