shoveler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shoveler-0.1.0/.gitignore +34 -0
- shoveler-0.1.0/.python-version +1 -0
- shoveler-0.1.0/PKG-INFO +90 -0
- shoveler-0.1.0/README.md +79 -0
- shoveler-0.1.0/pyproject.toml +35 -0
- shoveler-0.1.0/shoveler.spec +57 -0
- shoveler-0.1.0/src/shoveler/__init__.py +7 -0
- shoveler-0.1.0/src/shoveler/__main__.py +20 -0
- shoveler-0.1.0/src/shoveler/db.py +112 -0
- shoveler-0.1.0/src/shoveler/db_status_widget.py +123 -0
- shoveler-0.1.0/src/shoveler/editor.py +208 -0
- shoveler-0.1.0/src/shoveler/main_window.py +246 -0
- shoveler-0.1.0/src/shoveler/query_tab.py +68 -0
- shoveler-0.1.0/src/shoveler/results_panel.py +125 -0
- shoveler-0.1.0/src/shoveler/schema_panel.py +75 -0
- shoveler-0.1.0/tests/test_db.py +172 -0
- shoveler-0.1.0/tests/test_editor.py +66 -0
- shoveler-0.1.0/uv.lock +276 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*.pyo
|
|
8
|
+
|
|
9
|
+
# PyInstaller output — commit the .spec file, not the output
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
|
|
13
|
+
# uv — commit uv.lock to the repo, ignore the venv
|
|
14
|
+
.venv/
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
*.swp
|
|
25
|
+
|
|
26
|
+
# Local agent instructions
|
|
27
|
+
.github/copilot-instructions.md
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
|
|
33
|
+
# Github
|
|
34
|
+
.github/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
shoveler-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shoveler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shoveler: A minimal desktop SQL workbench for DuckDB
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: duckdb,education,sql,workbench
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: duckdb>=0.10
|
|
9
|
+
Requires-Dist: pyside6>=6.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# DuckDB Workbench
|
|
13
|
+
|
|
14
|
+
A minimal desktop SQL workbench for DuckDB, built with Python and PySide6.
|
|
15
|
+
|
|
16
|
+
## Setup (uv)
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
git clone https://github.com/mrmatho/shoveler
|
|
20
|
+
cd shoveler
|
|
21
|
+
uv sync
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`uv sync` creates a `.venv`, installs all dependencies including the dev group,
|
|
25
|
+
and respects `uv.lock` if it exists. Run it again after pulling changes.
|
|
26
|
+
|
|
27
|
+
## Running
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
uv run python -m shoveler
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Testing
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
uv run pytest
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Managing dependencies
|
|
40
|
+
|
|
41
|
+
```shell
|
|
42
|
+
uv add somepackage # add a runtime dependency
|
|
43
|
+
uv add --dev somepackage # add to the dev group (not published to PyPI)
|
|
44
|
+
uv remove somepackage # remove a dependency
|
|
45
|
+
uv lock # regenerate uv.lock without installing
|
|
46
|
+
uv sync # install from uv.lock
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Commit `uv.lock` to the repository. It pins exact versions for reproducible installs.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
- **Open File** — connect to an existing `.duckdb` or `.db` file (green indicator)
|
|
54
|
+
- **New In-Memory** — scratch database; data is lost when closed (amber indicator)
|
|
55
|
+
- **File > Save Database As...** — save current database state to a `.duckdb` file
|
|
56
|
+
- **Checkpoint** — flush the write-ahead log to disk (file databases only)
|
|
57
|
+
- **F5 or Ctrl+Enter** — run query
|
|
58
|
+
- Select part of your SQL to run only that selection
|
|
59
|
+
- Double-click a table name in the schema panel to insert it into the editor
|
|
60
|
+
- **View > Syntax highlighting** — toggle SQL highlighting on or off; enabled by default and remembered between launches
|
|
61
|
+
|
|
62
|
+
## Building a standalone Windows executable
|
|
63
|
+
|
|
64
|
+
The project includes `shoveler.spec` with the correct PyInstaller
|
|
65
|
+
configuration. Use this rather than running `pyinstaller` with flags directly —
|
|
66
|
+
the spec handles a non-obvious issue with DuckDB's compiled extension.
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
uv run pyinstaller shoveler.spec
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output is in `dist/Shoveler/`. Distribute that folder as a zip.
|
|
73
|
+
|
|
74
|
+
**Why --onedir and not --onefile?**
|
|
75
|
+
`--onefile` extracts everything to a temp directory on every launch, which makes
|
|
76
|
+
startup noticeably slow for a PySide6 app. `--onedir` is faster and easier to
|
|
77
|
+
debug if something goes wrong.
|
|
78
|
+
|
|
79
|
+
**Known PyInstaller issue with DuckDB:**
|
|
80
|
+
DuckDB's compiled core (`_duckdb.pyd` on Windows) lives outside the `duckdb/`
|
|
81
|
+
package directory. PyInstaller does not find it automatically. The spec file
|
|
82
|
+
uses `collect_all('duckdb')` which handles this correctly. If you ever regenerate
|
|
83
|
+
the spec from scratch, make sure this is included or the packaged app will fail
|
|
84
|
+
to start.
|
|
85
|
+
|
|
86
|
+
## Extending
|
|
87
|
+
|
|
88
|
+
|Feature|Where to add it|
|
|
89
|
+
|---|---|
|
|
90
|
+
|More export formats|Add menu items in `ResultsPanel._show_export_menu()` — raw data is in `_last_rows` / `_last_columns`|
|
shoveler-0.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# DuckDB Workbench
|
|
2
|
+
|
|
3
|
+
A minimal desktop SQL workbench for DuckDB, built with Python and PySide6.
|
|
4
|
+
|
|
5
|
+
## Setup (uv)
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
git clone https://github.com/mrmatho/shoveler
|
|
9
|
+
cd shoveler
|
|
10
|
+
uv sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`uv sync` creates a `.venv`, installs all dependencies including the dev group,
|
|
14
|
+
and respects `uv.lock` if it exists. Run it again after pulling changes.
|
|
15
|
+
|
|
16
|
+
## Running
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
uv run python -m shoveler
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Testing
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
uv run pytest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Managing dependencies
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
uv add somepackage # add a runtime dependency
|
|
32
|
+
uv add --dev somepackage # add to the dev group (not published to PyPI)
|
|
33
|
+
uv remove somepackage # remove a dependency
|
|
34
|
+
uv lock # regenerate uv.lock without installing
|
|
35
|
+
uv sync # install from uv.lock
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Commit `uv.lock` to the repository. It pins exact versions for reproducible installs.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
- **Open File** — connect to an existing `.duckdb` or `.db` file (green indicator)
|
|
43
|
+
- **New In-Memory** — scratch database; data is lost when closed (amber indicator)
|
|
44
|
+
- **File > Save Database As...** — save current database state to a `.duckdb` file
|
|
45
|
+
- **Checkpoint** — flush the write-ahead log to disk (file databases only)
|
|
46
|
+
- **F5 or Ctrl+Enter** — run query
|
|
47
|
+
- Select part of your SQL to run only that selection
|
|
48
|
+
- Double-click a table name in the schema panel to insert it into the editor
|
|
49
|
+
- **View > Syntax highlighting** — toggle SQL highlighting on or off; enabled by default and remembered between launches
|
|
50
|
+
|
|
51
|
+
## Building a standalone Windows executable
|
|
52
|
+
|
|
53
|
+
The project includes `shoveler.spec` with the correct PyInstaller
|
|
54
|
+
configuration. Use this rather than running `pyinstaller` with flags directly —
|
|
55
|
+
the spec handles a non-obvious issue with DuckDB's compiled extension.
|
|
56
|
+
|
|
57
|
+
```shell
|
|
58
|
+
uv run pyinstaller shoveler.spec
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Output is in `dist/Shoveler/`. Distribute that folder as a zip.
|
|
62
|
+
|
|
63
|
+
**Why --onedir and not --onefile?**
|
|
64
|
+
`--onefile` extracts everything to a temp directory on every launch, which makes
|
|
65
|
+
startup noticeably slow for a PySide6 app. `--onedir` is faster and easier to
|
|
66
|
+
debug if something goes wrong.
|
|
67
|
+
|
|
68
|
+
**Known PyInstaller issue with DuckDB:**
|
|
69
|
+
DuckDB's compiled core (`_duckdb.pyd` on Windows) lives outside the `duckdb/`
|
|
70
|
+
package directory. PyInstaller does not find it automatically. The spec file
|
|
71
|
+
uses `collect_all('duckdb')` which handles this correctly. If you ever regenerate
|
|
72
|
+
the spec from scratch, make sure this is included or the packaged app will fail
|
|
73
|
+
to start.
|
|
74
|
+
|
|
75
|
+
## Extending
|
|
76
|
+
|
|
77
|
+
|Feature|Where to add it|
|
|
78
|
+
|---|---|
|
|
79
|
+
|More export formats|Add menu items in `ResultsPanel._show_export_menu()` — raw data is in `_last_rows` / `_last_columns`|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shoveler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shoveler: A minimal desktop SQL workbench for DuckDB"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["duckdb", "sql", "workbench", "education"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"PySide6>=6.6",
|
|
15
|
+
"duckdb>=0.10",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# gui-scripts suppresses the Windows console window; use this instead of scripts
|
|
19
|
+
[project.gui-scripts]
|
|
20
|
+
shoveler = "shoveler.__main__:main"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/shoveler"]
|
|
24
|
+
|
|
25
|
+
# dependency-groups are uv/PEP 735 dev-only deps.
|
|
26
|
+
# They are NOT published to PyPI — use [project.optional-dependencies] for that.
|
|
27
|
+
# Install with: uv sync (dev group is included by default)
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"pyinstaller>=6.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# shoveler.spec
|
|
2
|
+
#
|
|
3
|
+
# Build with: uv run pyinstaller shoveler.spec
|
|
4
|
+
# Output: dist/Shoveler/ (--onedir, recommended)
|
|
5
|
+
#
|
|
6
|
+
# Why --onedir and not --onefile:
|
|
7
|
+
# --onefile extracts everything to a temp directory on every launch,
|
|
8
|
+
# making startup slow. --onedir is faster and easier to debug.
|
|
9
|
+
# Distribute the dist/DuckDB Workbench/ folder as a zip.
|
|
10
|
+
|
|
11
|
+
from PyInstaller.utils.hooks import collect_all
|
|
12
|
+
|
|
13
|
+
# collect_all picks up:
|
|
14
|
+
# - the duckdb Python package
|
|
15
|
+
# - _duckdb.pyd (the compiled extension, which lives outside the package dir)
|
|
16
|
+
# - any data files shipped with duckdb
|
|
17
|
+
# Without this, the packaged app fails at runtime with an ImportError on _duckdb.
|
|
18
|
+
duckdb_datas, duckdb_binaries, duckdb_hiddenimports = collect_all("duckdb")
|
|
19
|
+
|
|
20
|
+
a = Analysis(
|
|
21
|
+
["src/shoveler/__main__.py"],
|
|
22
|
+
pathex=[],
|
|
23
|
+
binaries=duckdb_binaries,
|
|
24
|
+
datas=duckdb_datas,
|
|
25
|
+
hiddenimports=duckdb_hiddenimports,
|
|
26
|
+
hookspath=[],
|
|
27
|
+
hooksconfig={},
|
|
28
|
+
runtime_hooks=[],
|
|
29
|
+
excludes=[],
|
|
30
|
+
noarchive=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
pyz = PYZ(a.pure)
|
|
34
|
+
|
|
35
|
+
exe = EXE(
|
|
36
|
+
pyz,
|
|
37
|
+
a.scripts,
|
|
38
|
+
[],
|
|
39
|
+
exclude_binaries=True, # required for COLLECT (onedir mode)
|
|
40
|
+
name="Shoveler",
|
|
41
|
+
debug=False,
|
|
42
|
+
bootloader_ignore_signals=False,
|
|
43
|
+
strip=False,
|
|
44
|
+
upx=True,
|
|
45
|
+
console=False, # no console window — same effect as gui-scripts in pyproject.toml
|
|
46
|
+
disable_windowed_traceback=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
coll = COLLECT(
|
|
50
|
+
exe,
|
|
51
|
+
a.binaries,
|
|
52
|
+
a.datas,
|
|
53
|
+
strip=False,
|
|
54
|
+
upx=True,
|
|
55
|
+
upx_exclude=[],
|
|
56
|
+
name="Shoveler",
|
|
57
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from PySide6.QtWidgets import QApplication
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from .main_window import MainWindow
|
|
6
|
+
except ImportError:
|
|
7
|
+
# PyInstaller may execute this file as a top-level script during startup.
|
|
8
|
+
from shoveler.main_window import MainWindow
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
app = QApplication(sys.argv)
|
|
13
|
+
app.setStyle("Fusion")
|
|
14
|
+
window = MainWindow()
|
|
15
|
+
window.show()
|
|
16
|
+
sys.exit(app.exec())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import duckdb
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Database:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.conn = None
|
|
10
|
+
self.mode: Literal["file", "memory"] | None = None
|
|
11
|
+
self.path: str | None = None
|
|
12
|
+
|
|
13
|
+
def open_file(self, path: str):
|
|
14
|
+
self._close_existing()
|
|
15
|
+
self.conn = duckdb.connect(path)
|
|
16
|
+
self.mode = "file"
|
|
17
|
+
self.path = path
|
|
18
|
+
|
|
19
|
+
def new_memory(self):
|
|
20
|
+
self._close_existing()
|
|
21
|
+
self.conn = duckdb.connect(":memory:")
|
|
22
|
+
self.mode = "memory"
|
|
23
|
+
self.path = None
|
|
24
|
+
|
|
25
|
+
def checkpoint(self):
|
|
26
|
+
if self.mode == "file" and self.conn:
|
|
27
|
+
self.conn.execute("CHECKPOINT")
|
|
28
|
+
|
|
29
|
+
def save_as(self, path: str) -> str:
|
|
30
|
+
if not self.conn:
|
|
31
|
+
raise RuntimeError("No database connected.")
|
|
32
|
+
|
|
33
|
+
target_path = os.path.abspath(path)
|
|
34
|
+
if self.mode == "file" and self.path and os.path.abspath(self.path) == target_path:
|
|
35
|
+
self.checkpoint()
|
|
36
|
+
return target_path
|
|
37
|
+
|
|
38
|
+
source_catalog = self.conn.execute("PRAGMA database_list").fetchone()[1]
|
|
39
|
+
source_catalog_escaped = source_catalog.replace('"', '""')
|
|
40
|
+
target_path_escaped = target_path.replace("'", "''")
|
|
41
|
+
target_catalog = "shoveler_save_target"
|
|
42
|
+
|
|
43
|
+
self.conn.execute(f"ATTACH '{target_path_escaped}' AS {target_catalog}")
|
|
44
|
+
try:
|
|
45
|
+
self.conn.execute(
|
|
46
|
+
f'COPY FROM DATABASE "{source_catalog_escaped}" TO {target_catalog}'
|
|
47
|
+
)
|
|
48
|
+
finally:
|
|
49
|
+
self.conn.execute(f"DETACH {target_catalog}")
|
|
50
|
+
|
|
51
|
+
self.open_file(target_path)
|
|
52
|
+
return target_path
|
|
53
|
+
|
|
54
|
+
def execute(self, sql: str) -> dict:
|
|
55
|
+
"""Returns dict: columns, rows, elapsed, error"""
|
|
56
|
+
if not self.conn:
|
|
57
|
+
return {
|
|
58
|
+
"columns": [],
|
|
59
|
+
"rows": [],
|
|
60
|
+
"elapsed": 0.0,
|
|
61
|
+
"error": "No database connected.",
|
|
62
|
+
}
|
|
63
|
+
start = time.perf_counter()
|
|
64
|
+
try:
|
|
65
|
+
result = self.conn.execute(sql)
|
|
66
|
+
elapsed = time.perf_counter() - start
|
|
67
|
+
if result.description:
|
|
68
|
+
columns = [desc[0] for desc in result.description]
|
|
69
|
+
rows = result.fetchall()
|
|
70
|
+
else:
|
|
71
|
+
columns = []
|
|
72
|
+
rows = []
|
|
73
|
+
return {"columns": columns, "rows": rows, "elapsed": elapsed, "error": None}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
elapsed = time.perf_counter() - start
|
|
76
|
+
return {"columns": [], "rows": [], "elapsed": elapsed, "error": str(e)}
|
|
77
|
+
|
|
78
|
+
def get_tables(self) -> list[str]:
|
|
79
|
+
if not self.conn:
|
|
80
|
+
return []
|
|
81
|
+
try:
|
|
82
|
+
result = self.conn.execute("SHOW TABLES").fetchall()
|
|
83
|
+
return [row[0] for row in result]
|
|
84
|
+
except Exception:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def get_columns(self, table: str) -> list[tuple[str, str]]:
|
|
88
|
+
if not self.conn:
|
|
89
|
+
return []
|
|
90
|
+
try:
|
|
91
|
+
# DESCRIBE returns: column_name, column_type, null, key, default, extra
|
|
92
|
+
result = self.conn.execute(f"DESCRIBE {table}").fetchall()
|
|
93
|
+
return [(row[0], row[1]) for row in result]
|
|
94
|
+
except Exception:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
def _close_existing(self):
|
|
98
|
+
if self.conn:
|
|
99
|
+
try:
|
|
100
|
+
self.conn.close()
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
self.conn = None
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
self._close_existing()
|
|
107
|
+
self.mode = None
|
|
108
|
+
self.path = None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_connected(self) -> bool:
|
|
112
|
+
return self.conn is not None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import (
|
|
4
|
+
QWidget,
|
|
5
|
+
QHBoxLayout,
|
|
6
|
+
QLabel,
|
|
7
|
+
QPushButton,
|
|
8
|
+
QFileDialog,
|
|
9
|
+
QFrame,
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import Signal, Qt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Colour constants — change here to restyle the indicator globally
|
|
15
|
+
COLOUR_FILE = "#2a9d2a" # green → file-backed database
|
|
16
|
+
COLOUR_MEMORY = "#e6a817" # amber → in-memory (unsaved)
|
|
17
|
+
COLOUR_NONE = "#999999" # grey → nothing connected
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatabaseStatusWidget(QFrame):
|
|
21
|
+
"""
|
|
22
|
+
Toolbar-level widget showing connection state with a colour-coded dot.
|
|
23
|
+
|
|
24
|
+
Signals:
|
|
25
|
+
file_opened(path) — user selected a .duckdb file
|
|
26
|
+
memory_requested() — user clicked New In-Memory
|
|
27
|
+
checkpoint_requested() — user clicked Checkpoint
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
file_opened = Signal(str)
|
|
31
|
+
memory_requested = Signal()
|
|
32
|
+
checkpoint_requested = Signal()
|
|
33
|
+
|
|
34
|
+
def __init__(self, parent=None):
|
|
35
|
+
super().__init__(parent)
|
|
36
|
+
self.setFrameShape(QFrame.Shape.StyledPanel)
|
|
37
|
+
self.setFixedHeight(38)
|
|
38
|
+
|
|
39
|
+
layout = QHBoxLayout(self)
|
|
40
|
+
layout.setContentsMargins(10, 0, 10, 0)
|
|
41
|
+
layout.setSpacing(10)
|
|
42
|
+
|
|
43
|
+
# Colour dot
|
|
44
|
+
self.dot = QLabel("●")
|
|
45
|
+
self.dot.setFixedWidth(16)
|
|
46
|
+
self.dot.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
|
47
|
+
layout.addWidget(self.dot)
|
|
48
|
+
|
|
49
|
+
# Status text
|
|
50
|
+
self.db_label = QLabel()
|
|
51
|
+
self.db_label.setStyleSheet("font-size: 12px;")
|
|
52
|
+
layout.addWidget(self.db_label)
|
|
53
|
+
|
|
54
|
+
layout.addStretch()
|
|
55
|
+
|
|
56
|
+
# Buttons
|
|
57
|
+
self.open_btn = QPushButton("Open File…")
|
|
58
|
+
self.open_btn.setFixedWidth(95)
|
|
59
|
+
self.open_btn.clicked.connect(self._on_open_file)
|
|
60
|
+
layout.addWidget(self.open_btn)
|
|
61
|
+
|
|
62
|
+
self.memory_btn = QPushButton("New In-Memory")
|
|
63
|
+
self.memory_btn.setFixedWidth(115)
|
|
64
|
+
self.memory_btn.clicked.connect(self.memory_requested.emit)
|
|
65
|
+
layout.addWidget(self.memory_btn)
|
|
66
|
+
|
|
67
|
+
self.checkpoint_btn = QPushButton("Checkpoint")
|
|
68
|
+
self.checkpoint_btn.setFixedWidth(95)
|
|
69
|
+
self.checkpoint_btn.setToolTip(
|
|
70
|
+
"Flush the write-ahead log to disk (file databases only)"
|
|
71
|
+
)
|
|
72
|
+
self.checkpoint_btn.clicked.connect(self.checkpoint_requested.emit)
|
|
73
|
+
layout.addWidget(self.checkpoint_btn)
|
|
74
|
+
|
|
75
|
+
self._set_state_disconnected()
|
|
76
|
+
|
|
77
|
+
# ── Public state setters ────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def set_file_mode(self, path: str):
|
|
80
|
+
filename = os.path.basename(path)
|
|
81
|
+
self._apply(
|
|
82
|
+
colour=COLOUR_FILE,
|
|
83
|
+
text=f"<b>{filename}</b> <span style='color:#888'>({path})</span>",
|
|
84
|
+
checkpoint_enabled=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def set_memory_mode(self):
|
|
88
|
+
self._apply(
|
|
89
|
+
colour=COLOUR_MEMORY,
|
|
90
|
+
text="<b>In-memory</b> <span style='color:#888'>(unsaved — data is lost when closed)</span>",
|
|
91
|
+
checkpoint_enabled=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def notify_checkpoint_ok(self):
|
|
95
|
+
"""Brief visual confirmation after a successful checkpoint."""
|
|
96
|
+
current = self.db_label.text()
|
|
97
|
+
# Append a transient tick — main window resets this on next open
|
|
98
|
+
if "✓" not in current:
|
|
99
|
+
self.db_label.setText(current + " ✓")
|
|
100
|
+
|
|
101
|
+
# ── Internals ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def _set_state_disconnected(self):
|
|
104
|
+
self._apply(
|
|
105
|
+
colour=COLOUR_NONE,
|
|
106
|
+
text="No database connected",
|
|
107
|
+
checkpoint_enabled=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _apply(self, colour: str, text: str, checkpoint_enabled: bool):
|
|
111
|
+
self.dot.setStyleSheet(f"font-size: 18px; color: {colour};")
|
|
112
|
+
self.db_label.setText(text)
|
|
113
|
+
self.checkpoint_btn.setEnabled(checkpoint_enabled)
|
|
114
|
+
|
|
115
|
+
def _on_open_file(self):
|
|
116
|
+
path, _ = QFileDialog.getOpenFileName(
|
|
117
|
+
self,
|
|
118
|
+
"Open DuckDB Database",
|
|
119
|
+
"",
|
|
120
|
+
"DuckDB Files (*.duckdb *.db);;All Files (*)",
|
|
121
|
+
)
|
|
122
|
+
if path:
|
|
123
|
+
self.file_opened.emit(path)
|