ghostbase-cli 0.1.0__tar.gz → 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghostbase-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL & MySQL
5
5
  Author-email: 4yk5 <contact.4yk5@proton.me>
6
6
  License-Expression: MIT
@@ -31,7 +31,7 @@ Dynamic: license-file
31
31
 
32
32
  ```
33
33
 
34
- # GhostBase 👻🗄️
34
+ # GhostBase 👻
35
35
 
36
36
  > **A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL, and MySQL.**
37
37
  > *No Java runtimes. No 1.5GB RAM bloat. Built-in production safety guard, instant SQL beautifier & zero-config `.env` connect.*
@@ -42,6 +42,10 @@ Dynamic: license-file
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
43
43
  [![UI: Textual](https://img.shields.io/badge/UI-Textual-bd93f9.svg)](https://github.com/Textualize/textual)
44
44
 
45
+ <p align="center">
46
+ <img src="assets/ghostbase.png" alt="GhostBase Terminal Studio" width="100%">
47
+ </p>
48
+
45
49
  ---
46
50
 
47
51
  ## ⚡ Why GhostBase?
@@ -75,10 +79,10 @@ Opening **DBeaver** or **DataGrip** just to run a quick `SELECT`, inspect an ind
75
79
  - ⚡ **1-Key Query Plan (`Ctrl + P`):**
76
80
  - Run instant execution plans (`EXPLAIN QUERY PLAN` / `EXPLAIN`) on any query to see index usage and bottlenecks.
77
81
  - 🔌 **Universal Database Support:** Connects natively to **SQLite**, **PostgreSQL**, and **MySQL**.
78
- - 🔍 **Zero-Config `.env` Auto-Connect:** Just run `ghostbase` in any project folder; it automatically picks up `DATABASE_URL` or standard database environment variables.
79
- - 🔎 **Real-time Table Search (`Ctrl + F`):** Type in the search bar to filter dozens of tables and views instantly.
82
+ - 🔑 **Zero-Config `.env` Auto-Connect:** Just run `ghostbase` in any project folder; it automatically picks up `DATABASE_URL` or standard database environment variables.
83
+ - 🔍 **Real-time Table Search (`Ctrl + F`):** Type in the search bar to filter dozens of tables and views instantly.
80
84
  - 📊 **Explorer with Tables & Views:** Browse tables and views with live row counts.
81
- - 📐 **Instant Schema & Foreign Keys:** See primary keys, column types, nullability, and foreign key references at a glance.
85
+ - 📋 **Instant Schema & Foreign Keys:** See primary keys, column types, nullability, and foreign key references at a glance.
82
86
  - 💾 **Multi-Format Exports:** Instant SQL `INSERT`s (`Ctrl + I`), Markdown (`Ctrl + M`), CSV (`Ctrl + E`), and JSON (`Ctrl + J`).
83
87
  - 📜 **Query History:** Cycle through your last 20 queries with `Ctrl + ↑` and `Ctrl + ↓`.
84
88
  - 🎨 **Clean Cyberpunk Aesthetic:** Fast, high-contrast, zero-flicker terminal interface.
@@ -153,10 +157,9 @@ ghostbase postgresql://prod-db:5432/main --read-only
153
157
 
154
158
  ---
155
159
 
156
- ## 📬 Contact & Inquiries
160
+ ## 🤝 Contributing
157
161
 
158
- For partnerships, feature requests, or business inquiries:
159
- 📧 **[contact.4yk5@proton.me](mailto:contact.4yk5@proton.me)**
162
+ Contributions, feedback, and pull requests are very welcome! Feel free to open an issue for bug reports or feature requests.
160
163
 
161
164
  ---
162
165
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  ```
9
9
 
10
- # GhostBase 👻🗄️
10
+ # GhostBase 👻
11
11
 
12
12
  > **A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL, and MySQL.**
13
13
  > *No Java runtimes. No 1.5GB RAM bloat. Built-in production safety guard, instant SQL beautifier & zero-config `.env` connect.*
@@ -18,6 +18,10 @@
18
18
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
19
19
  [![UI: Textual](https://img.shields.io/badge/UI-Textual-bd93f9.svg)](https://github.com/Textualize/textual)
20
20
 
21
+ <p align="center">
22
+ <img src="assets/ghostbase.png" alt="GhostBase Terminal Studio" width="100%">
23
+ </p>
24
+
21
25
  ---
22
26
 
23
27
  ## ⚡ Why GhostBase?
@@ -51,10 +55,10 @@ Opening **DBeaver** or **DataGrip** just to run a quick `SELECT`, inspect an ind
51
55
  - ⚡ **1-Key Query Plan (`Ctrl + P`):**
52
56
  - Run instant execution plans (`EXPLAIN QUERY PLAN` / `EXPLAIN`) on any query to see index usage and bottlenecks.
53
57
  - 🔌 **Universal Database Support:** Connects natively to **SQLite**, **PostgreSQL**, and **MySQL**.
54
- - 🔍 **Zero-Config `.env` Auto-Connect:** Just run `ghostbase` in any project folder; it automatically picks up `DATABASE_URL` or standard database environment variables.
55
- - 🔎 **Real-time Table Search (`Ctrl + F`):** Type in the search bar to filter dozens of tables and views instantly.
58
+ - 🔑 **Zero-Config `.env` Auto-Connect:** Just run `ghostbase` in any project folder; it automatically picks up `DATABASE_URL` or standard database environment variables.
59
+ - 🔍 **Real-time Table Search (`Ctrl + F`):** Type in the search bar to filter dozens of tables and views instantly.
56
60
  - 📊 **Explorer with Tables & Views:** Browse tables and views with live row counts.
57
- - 📐 **Instant Schema & Foreign Keys:** See primary keys, column types, nullability, and foreign key references at a glance.
61
+ - 📋 **Instant Schema & Foreign Keys:** See primary keys, column types, nullability, and foreign key references at a glance.
58
62
  - 💾 **Multi-Format Exports:** Instant SQL `INSERT`s (`Ctrl + I`), Markdown (`Ctrl + M`), CSV (`Ctrl + E`), and JSON (`Ctrl + J`).
59
63
  - 📜 **Query History:** Cycle through your last 20 queries with `Ctrl + ↑` and `Ctrl + ↓`.
60
64
  - 🎨 **Clean Cyberpunk Aesthetic:** Fast, high-contrast, zero-flicker terminal interface.
@@ -129,10 +133,9 @@ ghostbase postgresql://prod-db:5432/main --read-only
129
133
 
130
134
  ---
131
135
 
132
- ## 📬 Contact & Inquiries
136
+ ## 🤝 Contributing
133
137
 
134
- For partnerships, feature requests, or business inquiries:
135
- 📧 **[contact.4yk5@proton.me](mailto:contact.4yk5@proton.me)**
138
+ Contributions, feedback, and pull requests are very welcome! Feel free to open an issue for bug reports or feature requests.
136
139
 
137
140
  ---
138
141
 
@@ -1,5 +1,7 @@
1
1
  import sys
2
2
  import os
3
+ import time
4
+ import re
3
5
 
4
6
  if sys.platform == "win32":
5
7
  try:
@@ -138,164 +140,127 @@ ListItem.section-header {
138
140
  #schema-title {
139
141
  color: #f0883e;
140
142
  text-style: bold;
141
- margin-bottom: 1;
142
143
  }
143
144
  #schema-content {
144
145
  color: #8b949e;
145
146
  }
146
147
 
147
- /* Query editor */
148
+ /* Content Area */
148
149
  #content-area {
149
150
  width: 1fr;
150
- padding: 0 1;
151
+ background: #0d1117;
151
152
  }
153
+
154
+ /* Query Editor */
152
155
  #query-box {
153
- height: 7;
154
- border: solid #30363d;
156
+ height: 9;
155
157
  background: #010409;
156
- margin-bottom: 0;
158
+ border-bottom: solid #30363d;
159
+ color: #e6edf3;
157
160
  }
158
161
  #query-box:focus {
159
- border: solid #388bfd;
162
+ border-bottom: solid #388bfd;
160
163
  }
161
164
 
162
- /* Actions toolbar */
165
+ /* Toolbar */
163
166
  #toolbar {
164
167
  height: 3;
168
+ background: #161b22;
169
+ border-bottom: solid #30363d;
165
170
  align: left middle;
166
- margin: 0;
167
- overflow-x: hidden;
171
+ padding: 0 1;
172
+ }
173
+ #toolbar Button {
174
+ margin-right: 1;
175
+ min-width: 8;
176
+ height: 1;
177
+ border: none;
168
178
  }
169
179
  #btn-run {
170
180
  background: #238636;
171
181
  color: #ffffff;
172
- border: none;
173
- min-width: 6;
174
- margin-right: 1;
175
182
  }
176
- #btn-run:hover { background: #2ea043; }
177
-
183
+ #btn-run:hover {
184
+ background: #2ea043;
185
+ }
178
186
  #btn-explain {
179
- background: #388bfd;
187
+ background: #1f6feb;
180
188
  color: #ffffff;
181
- border: none;
182
- min-width: 9;
183
- margin-right: 1;
184
189
  }
185
- #btn-explain:hover { background: #58a6ff; }
186
-
190
+ #btn-explain:hover {
191
+ background: #388bfd;
192
+ }
187
193
  #btn-format {
188
- background: #1f6feb;
189
- color: #ffffff;
190
- border: none;
191
- min-width: 8;
192
- margin-right: 1;
194
+ background: #21262d;
195
+ color: #c9d1d9;
196
+ }
197
+ #btn-format:hover {
198
+ background: #30363d;
193
199
  }
194
- #btn-format:hover { background: #388bfd; }
195
-
196
200
  #btn-clear {
197
201
  background: #21262d;
198
202
  color: #8b949e;
199
- border: none;
200
- min-width: 7;
201
- margin-right: 1;
202
203
  }
203
- #btn-clear:hover { background: #30363d; color: #c9d1d9; }
204
-
204
+ #btn-clear:hover {
205
+ background: #30363d;
206
+ color: #f85149;
207
+ }
205
208
  #btn-safe {
209
+ background: #21262d;
210
+ color: #3fb950;
211
+ }
212
+ #btn-safe:hover {
206
213
  background: #30363d;
207
- color: #c9d1d9;
208
- border: none;
209
- min-width: 6;
210
- margin-right: 1;
211
214
  }
212
- #btn-safe:hover { background: #484f58; }
213
-
214
215
  #status-label {
216
+ margin-left: 2;
215
217
  color: #8b949e;
216
- text-overflow: ellipsis;
217
- overflow: hidden;
218
218
  }
219
219
 
220
- /* Results & export bar */
220
+ /* Results Bar */
221
221
  #results-bar {
222
222
  height: 3;
223
+ background: #161b22;
224
+ border-bottom: solid #30363d;
223
225
  align: left middle;
224
226
  padding: 0 1;
225
- background: #161b22;
226
- border: solid #30363d;
227
- border-bottom: none;
228
- overflow-x: hidden;
229
227
  }
230
228
  #results-title {
231
- color: #79c0ff;
229
+ color: #58a6ff;
232
230
  text-style: bold;
233
- margin-right: 1;
234
231
  }
235
232
  #results-count {
236
- color: #8b949e;
237
- margin-right: 1;
233
+ color: #484f58;
234
+ margin-left: 1;
238
235
  }
239
236
  .export-sep {
240
237
  color: #30363d;
241
- margin-right: 1;
238
+ margin: 0 1;
242
239
  }
243
240
  .export-label {
244
241
  color: #484f58;
245
242
  margin-right: 1;
246
243
  }
247
- #btn-csv {
248
- background: #0d419d;
249
- color: #79c0ff;
250
- border: none;
251
- min-width: 5;
244
+ #results-bar Button {
252
245
  margin-right: 1;
253
- }
254
- #btn-csv:hover { background: #1158c7; }
255
-
256
- #btn-json {
257
- background: #5a1e02;
258
- color: #ffa657;
259
- border: none;
260
246
  min-width: 6;
261
- margin-right: 1;
262
- }
263
- #btn-json:hover { background: #7d2d00; }
264
-
265
- #btn-md {
266
- background: #232a35;
267
- color: #a5d6ff;
247
+ height: 1;
268
248
  border: none;
269
- min-width: 4;
270
- margin-right: 1;
249
+ background: #21262d;
250
+ color: #8b949e;
271
251
  }
272
- #btn-md:hover { background: #30363d; }
273
-
274
- #btn-sql {
275
- background: #3b2300;
276
- color: #ffc680;
277
- border: none;
278
- min-width: 5;
279
- margin-right: 1;
252
+ #results-bar Button:hover {
253
+ background: #30363d;
254
+ color: #c9d1d9;
280
255
  }
281
- #btn-sql:hover { background: #5a3500; }
282
256
 
283
- /* Results table */
257
+ /* Data Table */
284
258
  #data-table {
285
259
  height: 1fr;
286
- border: solid #30363d;
287
260
  background: #0d1117;
288
261
  }
289
- DataTable > .datatable--header {
290
- background: #161b22;
291
- color: #79c0ff;
292
- text-style: bold;
293
- }
294
- DataTable > .datatable--cursor {
295
- background: #1c2128;
296
- }
297
262
 
298
- /* Status bar */
263
+ /* Status Bar */
299
264
  #status-bar {
300
265
  height: 1;
301
266
  dock: bottom;
@@ -344,18 +309,22 @@ class GhostBaseApp(App):
344
309
  selected_table: reactive[str] = reactive("")
345
310
  safe_mode: reactive[bool] = reactive(False)
346
311
 
347
- def __init__(self, db_path: str = None, read_only: bool = False):
312
+ def __init__(self, db_path: str = None, read_only: bool = False, populate_sample: bool = False):
348
313
  super().__init__()
349
314
  self.db_path = db_path or "sample.db"
315
+ self.populate_sample = populate_sample
350
316
  self.db = DatabaseEngine(self.db_path)
351
317
  self.safe_mode = read_only
352
318
  self.last_columns: list = []
353
319
  self.last_rows: list = []
320
+ self.last_query_table: str = None
354
321
  self.query_history: list = []
355
322
  self.history_index: int = -1
356
323
  self.all_tables: list = []
357
324
  self.all_views: list = []
325
+ self._row_count_cache: dict = {}
358
326
  self._pending_destructive_query = None
327
+ self._pending_destructive_time = 0.0
359
328
 
360
329
  # Layout
361
330
 
@@ -373,7 +342,7 @@ class GhostBaseApp(App):
373
342
  yield Label(f"[{self.db.db_type_label()}]", id="db-type-label")
374
343
  yield Label(db_label, id="db-name-label")
375
344
  yield Label(safe_text, id="safe-badge", classes=safe_class)
376
- yield Label("v0.1.0", id="version-label")
345
+ yield Label("v0.1.1", id="version-label")
377
346
 
378
347
  with Horizontal(id="main-container"):
379
348
  with Vertical(id="sidebar"):
@@ -421,12 +390,19 @@ class GhostBaseApp(App):
421
390
 
422
391
  async def on_mount(self):
423
392
  tables = self.db.get_tables()
424
- if not tables and self.db.db_type == "sqlite":
393
+ # Only populate demo tables when explicitly requested, in-memory, or default sample.db
394
+ is_default_sample = (
395
+ self.populate_sample
396
+ or self.db_path == ":memory:"
397
+ or os.path.basename(os.path.abspath(self.db_path)) == "sample.db"
398
+ )
399
+ if not tables and self.db.db_type == "sqlite" and is_default_sample:
425
400
  self._create_sample_data()
426
401
  tables = self.db.get_tables()
427
402
 
428
403
  self.all_tables = tables
429
404
  self.all_views = self.db.get_views()
405
+ self._refresh_row_counts()
430
406
  self._refresh_sidebar(self.all_tables, self.all_views)
431
407
 
432
408
  if self.all_tables:
@@ -437,9 +413,20 @@ class GhostBaseApp(App):
437
413
  )
438
414
  self.action_run_query()
439
415
  self._show_schema(first)
416
+ else:
417
+ self.query_one("#query-box", TextArea).text = (
418
+ "-- Connected to database.\n-- Write a query and press Ctrl+R to execute.\n"
419
+ )
440
420
 
441
421
  self._update_status_bar()
442
422
 
423
+ # Row count caching
424
+
425
+ def _refresh_row_counts(self):
426
+ self._row_count_cache = {}
427
+ for t in self.all_tables:
428
+ self._row_count_cache[t] = self.db.get_row_count(t)
429
+
443
430
  # Sample schema
444
431
 
445
432
  def _create_sample_data(self):
@@ -500,7 +487,7 @@ class GhostBaseApp(App):
500
487
  lv.append(_sep_tables)
501
488
 
502
489
  for t in tables:
503
- rc = self.db.get_row_count(t)
490
+ rc = self._row_count_cache.get(t, 0)
504
491
  item = ListItem(Label(f" {t} ({rc})"))
505
492
  item.table_name = t # type: ignore
506
493
  item.is_view = False # type: ignore
@@ -533,6 +520,11 @@ class GhostBaseApp(App):
533
520
  filtered_views = [v for v in self.all_views if q in v.lower()]
534
521
  self._refresh_sidebar(filtered_tables, filtered_views)
535
522
 
523
+ def on_text_area_changed(self, event: TextArea.Changed):
524
+ if event.text_area.id == "query-box" and self._pending_destructive_query is not None:
525
+ self._pending_destructive_query = None
526
+ self._pending_destructive_time = 0.0
527
+
536
528
  # Schema panel
537
529
 
538
530
  def _show_schema(self, table_name: str, is_view: bool = False):
@@ -591,6 +583,9 @@ class GhostBaseApp(App):
591
583
  return
592
584
  is_view = getattr(item, "is_view", False)
593
585
  self.selected_table = t
586
+ self._pending_destructive_query = None
587
+ self._pending_destructive_time = 0.0
588
+
594
589
  self.query_one("#query-box", TextArea).text = (
595
590
  f"SELECT * FROM {t} LIMIT 50;"
596
591
  )
@@ -626,26 +621,32 @@ class GhostBaseApp(App):
626
621
  status = self.query_one("#status-label", Label)
627
622
  res_cnt = self.query_one("#results-count", Label)
628
623
 
629
- # Safety guard check
630
- is_destructive, reason = self.db.check_safety(query)
624
+ # Safety guard check (strict read-only in safe mode, destructive guard in normal mode)
625
+ is_destructive, reason = self.db.check_safety(query, is_safe_mode=self.safe_mode)
631
626
  if is_destructive:
632
627
  if self.safe_mode:
633
628
  self.notify(f"BLOCKED BY SAFE MODE: {reason}", severity="error", timeout=5)
634
629
  status.update(f"[bold red]BLOCKED BY SAFE MODE:[/] {reason}")
635
630
  return
636
631
 
637
- if self._pending_destructive_query != query:
632
+ now = time.time()
633
+ if self._pending_destructive_query == query and (now - self._pending_destructive_time) <= 10.0:
634
+ # Double confirmation received within 10s window
635
+ self._pending_destructive_query = None
636
+ self._pending_destructive_time = 0.0
637
+ else:
638
638
  self._pending_destructive_query = query
639
+ self._pending_destructive_time = now
639
640
  self.notify(
640
- f"DANGER: {reason}!\nPress Ctrl+R again to confirm.",
641
+ f"DANGER: {reason}!\nPress Ctrl+R within 10s to confirm.",
641
642
  severity="warning",
642
643
  timeout=5,
643
644
  )
644
645
  status.update(f"[bold red]DANGER:[/] {reason} -> Press Ctrl+R to CONFIRM")
645
646
  return
646
-
647
- # Double confirmation received
647
+ else:
648
648
  self._pending_destructive_query = None
649
+ self._pending_destructive_time = 0.0
649
650
 
650
651
  if not self.query_history or self.query_history[-1] != query:
651
652
  self.query_history.append(query)
@@ -658,6 +659,10 @@ class GhostBaseApp(App):
658
659
  dt.clear(columns=True)
659
660
 
660
661
  if not res["success"]:
662
+ # Clear export cache on error
663
+ self.last_columns = []
664
+ self.last_rows = []
665
+ self.last_query_table = None
661
666
  err = res.get("error", "Unknown error")
662
667
  status.update(f"[bold red]ERROR[/] {err}")
663
668
  res_cnt.update("(Error)")
@@ -667,6 +672,11 @@ class GhostBaseApp(App):
667
672
  if res["is_select"]:
668
673
  self.last_columns = res["columns"]
669
674
  self.last_rows = res["rows"]
675
+
676
+ # Detect target table for export if simple SELECT FROM table
677
+ m = re.search(r"\bFROM\s+[`\"\[]?([a-zA-Z0-9_\.]+)[`\"\]]?", query, flags=re.IGNORECASE)
678
+ self.last_query_table = m.group(1).split(".")[-1] if m else (self.selected_table or "query_result")
679
+
670
680
  dt.add_columns(*self.last_columns)
671
681
  for row in self.last_rows:
672
682
  styled = []
@@ -685,16 +695,29 @@ class GhostBaseApp(App):
685
695
  rc = res["row_count"]
686
696
  ms = res["elapsed_ms"]
687
697
  status.update(f"[bold green]OK[/] ({ms}ms)")
688
- res_cnt.update(f"({rc} rows)")
698
+
699
+ if res.get("truncated"):
700
+ res_cnt.update(f"({rc}+ rows, capped at 1,000)")
701
+ self.notify("Displaying first 1,000 rows (use LIMIT / OFFSET for more)", severity="information", timeout=4)
702
+ else:
703
+ res_cnt.update(f"({rc} rows)")
704
+
689
705
  self._update_status_bar(row_count=rc, ms=ms)
690
706
  else:
707
+ # Clear export cache so previous SELECT results are not mistakenly exported
708
+ self.last_columns = []
709
+ self.last_rows = []
710
+ self.last_query_table = None
711
+
691
712
  aff = res.get("affected_rows", 0)
692
713
  ms = res["elapsed_ms"]
693
714
  status.update(f"[bold green]OK[/] ({ms}ms)")
694
715
  res_cnt.update(f"({aff} rows affected)")
695
716
  self.notify(f"Done — {aff} row(s) affected")
717
+
696
718
  self.all_tables = self.db.get_tables()
697
719
  self.all_views = self.db.get_views()
720
+ self._refresh_row_counts()
698
721
  self._refresh_sidebar(self.all_tables, self.all_views)
699
722
  self._update_status_bar(ms=ms)
700
723
 
@@ -745,21 +768,26 @@ class GhostBaseApp(App):
745
768
  def action_toggle_safe_mode(self):
746
769
  """Toggle Production Safe Mode (Ctrl+S)"""
747
770
  self.safe_mode = not self.safe_mode
771
+ self._pending_destructive_query = None
772
+ self._pending_destructive_time = 0.0
773
+
748
774
  badge = self.query_one("#safe-badge", Label)
749
775
  if self.safe_mode:
750
776
  badge.update("[SAFE: ON]")
751
777
  badge.remove_class("unsafe")
752
- self.notify("Safe Mode ENABLED: Destructive queries blocked", timeout=3)
778
+ self.notify("Safe Mode ENABLED: Modifications strictly blocked (Read-Only)", timeout=3)
753
779
  else:
754
780
  badge.update("[SAFE: OFF]")
755
781
  badge.add_class("unsafe")
756
- self.notify("Safe Mode DISABLED: Destructive queries allowed with confirmation", severity="warning", timeout=3)
782
+ self.notify("Safe Mode DISABLED: Destructive queries require confirmation", severity="warning", timeout=3)
757
783
  self._update_status_bar()
758
784
 
759
785
  def action_focus_filter(self):
760
786
  self.query_one("#table-search", Input).focus()
761
787
 
762
788
  def action_clear_query(self):
789
+ self._pending_destructive_query = None
790
+ self._pending_destructive_time = 0.0
763
791
  self.query_one("#query-box", TextArea).text = ""
764
792
  self.query_one("#status-label", Label).update("Ready")
765
793
  self.query_one("#query-box", TextArea).focus()
@@ -782,7 +810,7 @@ class GhostBaseApp(App):
782
810
 
783
811
  def action_export_csv(self):
784
812
  if not self.last_columns:
785
- self.notify("Run a query first", severity="warning")
813
+ self.notify("Run a SELECT query first", severity="warning")
786
814
  return
787
815
  db_dir = (
788
816
  os.path.dirname(os.path.abspath(self.db_path))
@@ -795,7 +823,7 @@ class GhostBaseApp(App):
795
823
 
796
824
  def action_export_json(self):
797
825
  if not self.last_columns:
798
- self.notify("Run a query first", severity="warning")
826
+ self.notify("Run a SELECT query first", severity="warning")
799
827
  return
800
828
  db_dir = (
801
829
  os.path.dirname(os.path.abspath(self.db_path))
@@ -808,7 +836,7 @@ class GhostBaseApp(App):
808
836
 
809
837
  def action_export_markdown(self):
810
838
  if not self.last_columns:
811
- self.notify("Run a query first", severity="warning")
839
+ self.notify("Run a SELECT query first", severity="warning")
812
840
  return
813
841
  db_dir = (
814
842
  os.path.dirname(os.path.abspath(self.db_path))
@@ -821,9 +849,9 @@ class GhostBaseApp(App):
821
849
 
822
850
  def action_export_sql(self):
823
851
  if not self.last_columns:
824
- self.notify("Run a query first", severity="warning")
852
+ self.notify("Run a SELECT query first", severity="warning")
825
853
  return
826
- table_name = self.selected_table or "exported_table"
854
+ table_name = getattr(self, "last_query_table", None) or self.selected_table or "exported_table"
827
855
  db_dir = (
828
856
  os.path.dirname(os.path.abspath(self.db_path))
829
857
  if (self.db.db_type == "sqlite" and self.db_path != ":memory:")