Files
DBClient/CLAUDE.md
T

163 lines
12 KiB
Markdown

# 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:**
```bash
pip install -r requirements.txt
```
Key runtime dependencies: `PyQt6`, `sqlparse>=0.5.0`, `bcrypt`, `keyring`, `cryptography`.
**Run the application:**
```bash
python main.py
```
There is no test suite or linter config set up.
**Build distributable (Windows):**
```bash
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:
1. Create a `QueryWorker` / `SchemaWorker` / `TableDataWorker` (subclasses of `QRunnable`/`QThread`)
2. Connect `signals.result` / `signals.error` to UI slots
3. 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)
- `StreamHandler` to stderr (WARNING+ only, for dev)
- `sys.excepthook` and `threading.excepthook` to capture uncaught exceptions with full tracebacks
- `qInstallMessageHandler` to 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 via `keyring`)
- `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
Theming uses a single `resources/style_template.qss` with `@{token}` placeholders. `app/config/theme.py` defines seven palette dicts (`ALL_THEMES`) and `apply_qss(app)` substitutes tokens at runtime via regex — no duplicate `.qss` files. Available themes: `dark` (Catppuccin Frappé), `light` (Catppuccin Latte), `one_dark`, `nord`, `tokyo_night`, `dracula`, `github_light`. To add a new theme, add a palette dict to `ALL_THEMES` and register it in `_THEMES` in `preferences_dialog.py`. Widget-specific color overrides must call `get_palette()` at render time (not at import time) so they react to live theme switches. `is_dark()` returns True for all dark-family themes.
### 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). Identifiers are quoted via `driver.quote_identifier(name)` — backticks for MySQL, brackets for MSSQL, double-quotes for PostgreSQL/SQLite — defined on `BaseDriver` with per-driver overrides.
### Identifier quoting
`BaseDriver.quote_identifier(name)` returns `"name"` by default. `MySQLDriver` overrides to `` `name` `` and `MSSQLDriver` to `[name]`. Always use this method when building dynamic SQL with column/table names — never hardcode a quote style.
---
## 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: `≡ Format` button 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/SQL dialog (`app/ui/import_dialog.py`): CSV/JSON inserts rows into a target table; SQL dump executes statements via `_SqlImportWorker` (sqlparse split, stop-on-error option, per-statement error summary)
- 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 via `settings_applied` signal
- 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: `_RowNumberProxy` shows sequential visual-order numbers in the vertical header (sort-stable); fixed 48 px wide header in both `ResultsPanel` and `TableViewer`
- Connection ping indicator: 30-second `_PingWorker` (QThread) pings each active connection via `test_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_view` child 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 `RowDialog` and `ConnectionDialog`; focus jumps to first input field on open; Enter in password field submits `ConnectionDialog`
- Multi-theme support: 7 themes (Frappé, Latte, One Dark Pro, Nord, Tokyo Night, Dracula, GitHub Light) via `resources/style_template.qss` token substitution; select in Preferences → Color theme; Ctrl+Shift+T toggles dark↔light; syntax highlighter, completer popup, and model colors all update live via `get_palette()`
### 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
- Schema diff: compare two database schemas and show structural differences