ghostbase-cli 0.1.0__py3-none-any.whl

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.
db_engine.py ADDED
@@ -0,0 +1,396 @@
1
+ import sqlite3
2
+ import time
3
+ import urllib.parse
4
+ import re
5
+ from typing import List, Dict, Any, Tuple
6
+
7
+
8
+ class DatabaseEngine:
9
+ """Database connection and query handler for SQLite, Postgres, and MySQL."""
10
+
11
+ def __init__(self, connection_string: str = None):
12
+ self.connection_string = (connection_string or "sample.db").strip()
13
+ self.db_type = "sqlite"
14
+ self.db_name = ""
15
+ self.conn = None
16
+ self._parse_and_connect()
17
+
18
+ # --- Connection handling ---
19
+
20
+ def _parse_and_connect(self):
21
+ cs = self.connection_string
22
+ if cs.startswith(("postgresql://", "postgres://")):
23
+ self.db_type = "postgresql"
24
+ parsed = urllib.parse.urlparse(cs)
25
+ self.db_name = parsed.path.lstrip("/")
26
+ self._connect_postgres(cs)
27
+ elif cs.startswith("mysql://"):
28
+ self.db_type = "mysql"
29
+ parsed = urllib.parse.urlparse(cs)
30
+ self.db_name = parsed.path.lstrip("/")
31
+ self._connect_mysql(cs)
32
+ else:
33
+ self.db_type = "sqlite"
34
+ path = cs.removeprefix("sqlite:///")
35
+ self.db_name = path
36
+ self._connect_sqlite(path)
37
+
38
+ def _connect_sqlite(self, path: str):
39
+ self.conn = sqlite3.connect(path, check_same_thread=False)
40
+ self.conn.row_factory = sqlite3.Row
41
+
42
+ def _connect_postgres(self, url: str):
43
+ try:
44
+ import psycopg2
45
+ except ImportError:
46
+ raise RuntimeError(
47
+ "psycopg2 is not installed.\n"
48
+ "Run: pip install psycopg2-binary"
49
+ )
50
+ self.conn = psycopg2.connect(url)
51
+ self.conn.autocommit = False
52
+
53
+ def _connect_mysql(self, url: str):
54
+ try:
55
+ import pymysql
56
+ import pymysql.cursors
57
+ except ImportError:
58
+ raise RuntimeError(
59
+ "pymysql is not installed.\n"
60
+ "Run: pip install pymysql"
61
+ )
62
+ parsed = urllib.parse.urlparse(url)
63
+ self.conn = pymysql.connect(
64
+ host=parsed.hostname or "localhost",
65
+ port=parsed.port or 3306,
66
+ user=parsed.username or "root",
67
+ password=parsed.password or "",
68
+ database=parsed.path.lstrip("/"),
69
+ cursorclass=pymysql.cursors.DictCursor,
70
+ autocommit=True,
71
+ )
72
+
73
+ # --- Schema discovery ---
74
+
75
+ def get_tables(self) -> List[str]:
76
+ try:
77
+ cur = self.conn.cursor()
78
+ if self.db_type == "sqlite":
79
+ cur.execute(
80
+ "SELECT name FROM sqlite_master "
81
+ "WHERE type='table' AND name NOT LIKE 'sqlite_%' "
82
+ "ORDER BY name;"
83
+ )
84
+ return [r[0] for r in cur.fetchall()]
85
+ elif self.db_type == "postgresql":
86
+ cur.execute(
87
+ "SELECT table_name FROM information_schema.tables "
88
+ "WHERE table_schema='public' AND table_type='BASE TABLE' "
89
+ "ORDER BY table_name;"
90
+ )
91
+ return [r[0] for r in cur.fetchall()]
92
+ elif self.db_type == "mysql":
93
+ cur.execute("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE';")
94
+ rows = cur.fetchall()
95
+ return [list(r.values())[0] if isinstance(r, dict) else r[0] for r in rows]
96
+ except Exception:
97
+ return []
98
+ return []
99
+
100
+ def get_views(self) -> List[str]:
101
+ try:
102
+ cur = self.conn.cursor()
103
+ if self.db_type == "sqlite":
104
+ cur.execute(
105
+ "SELECT name FROM sqlite_master WHERE type='view' ORDER BY name;"
106
+ )
107
+ return [r[0] for r in cur.fetchall()]
108
+ elif self.db_type == "postgresql":
109
+ cur.execute(
110
+ "SELECT table_name FROM information_schema.views "
111
+ "WHERE table_schema='public' ORDER BY table_name;"
112
+ )
113
+ return [r[0] for r in cur.fetchall()]
114
+ elif self.db_type == "mysql":
115
+ cur.execute("SHOW FULL TABLES WHERE Table_type = 'VIEW';")
116
+ rows = cur.fetchall()
117
+ return [list(r.values())[0] if isinstance(r, dict) else r[0] for r in rows]
118
+ except Exception:
119
+ return []
120
+ return []
121
+
122
+ def get_table_schema(self, table_name: str) -> List[Dict[str, Any]]:
123
+ try:
124
+ cur = self.conn.cursor()
125
+ if self.db_type == "sqlite":
126
+ cur.execute(f"PRAGMA table_info([{table_name}]);")
127
+ return [
128
+ {"name": r[1], "type": r[2], "notnull": r[3],
129
+ "dflt_value": r[4], "pk": r[5]}
130
+ for r in cur.fetchall()
131
+ ]
132
+ elif self.db_type == "postgresql":
133
+ cur.execute(
134
+ """
135
+ SELECT
136
+ c.column_name,
137
+ c.data_type,
138
+ CASE WHEN c.is_nullable = 'NO' THEN 1 ELSE 0 END,
139
+ c.column_default,
140
+ CASE WHEN kcu.column_name IS NOT NULL THEN 1 ELSE 0 END
141
+ FROM information_schema.columns c
142
+ LEFT JOIN information_schema.table_constraints tc
143
+ ON tc.table_name = c.table_name
144
+ AND tc.constraint_type = 'PRIMARY KEY'
145
+ AND tc.table_schema = 'public'
146
+ LEFT JOIN information_schema.key_column_usage kcu
147
+ ON kcu.constraint_name = tc.constraint_name
148
+ AND kcu.column_name = c.column_name
149
+ WHERE c.table_name = %s AND c.table_schema = 'public'
150
+ ORDER BY c.ordinal_position;
151
+ """,
152
+ (table_name,),
153
+ )
154
+ return [
155
+ {"name": r[0], "type": r[1], "notnull": r[2],
156
+ "dflt_value": r[3], "pk": r[4]}
157
+ for r in cur.fetchall()
158
+ ]
159
+ elif self.db_type == "mysql":
160
+ cur.execute(f"DESCRIBE `{table_name}`;")
161
+ rows = cur.fetchall()
162
+ result = []
163
+ for r in rows:
164
+ d = r if isinstance(r, dict) else dict(
165
+ zip(["Field", "Type", "Null", "Key", "Default", "Extra"], r)
166
+ )
167
+ result.append({
168
+ "name": d.get("Field", ""),
169
+ "type": d.get("Type", ""),
170
+ "notnull": 1 if d.get("Null") == "NO" else 0,
171
+ "dflt_value": d.get("Default"),
172
+ "pk": 1 if d.get("Key") == "PRI" else 0,
173
+ })
174
+ return result
175
+ except Exception:
176
+ return []
177
+ return []
178
+
179
+ def get_foreign_keys(self, table_name: str) -> List[Dict[str, str]]:
180
+ try:
181
+ cur = self.conn.cursor()
182
+ if self.db_type == "sqlite":
183
+ cur.execute(f"PRAGMA foreign_key_list([{table_name}]);")
184
+ return [
185
+ {"from": r[3], "to_table": r[2], "to_col": r[4]}
186
+ for r in cur.fetchall()
187
+ ]
188
+ elif self.db_type == "postgresql":
189
+ cur.execute(
190
+ """
191
+ SELECT kcu.column_name,
192
+ ccu.table_name AS to_table,
193
+ ccu.column_name AS to_col
194
+ FROM information_schema.table_constraints tc
195
+ JOIN information_schema.key_column_usage kcu
196
+ ON tc.constraint_name = kcu.constraint_name
197
+ AND tc.table_schema = kcu.table_schema
198
+ JOIN information_schema.constraint_column_usage ccu
199
+ ON ccu.constraint_name = tc.constraint_name
200
+ AND ccu.table_schema = tc.table_schema
201
+ WHERE tc.constraint_type = 'FOREIGN KEY'
202
+ AND tc.table_name = %s;
203
+ """,
204
+ (table_name,),
205
+ )
206
+ return [
207
+ {"from": r[0], "to_table": r[1], "to_col": r[2]}
208
+ for r in cur.fetchall()
209
+ ]
210
+ elif self.db_type == "mysql":
211
+ cur.execute("SELECT DATABASE();")
212
+ row = cur.fetchone()
213
+ db = list(row.values())[0] if isinstance(row, dict) else row[0]
214
+ cur.execute(
215
+ """
216
+ SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
217
+ FROM information_schema.KEY_COLUMN_USAGE
218
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
219
+ AND REFERENCED_TABLE_NAME IS NOT NULL;
220
+ """,
221
+ (db, table_name),
222
+ )
223
+ rows = cur.fetchall()
224
+ return [
225
+ {
226
+ "from": (r.get("COLUMN_NAME") if isinstance(r, dict) else r[0]),
227
+ "to_table": (r.get("REFERENCED_TABLE_NAME") if isinstance(r, dict) else r[1]),
228
+ "to_col": (r.get("REFERENCED_COLUMN_NAME") if isinstance(r, dict) else r[2]),
229
+ }
230
+ for r in rows
231
+ ]
232
+ except Exception:
233
+ return []
234
+ return []
235
+
236
+ def get_row_count(self, table_name: str) -> int:
237
+ try:
238
+ cur = self.conn.cursor()
239
+ if self.db_type == "sqlite":
240
+ cur.execute(f"SELECT COUNT(*) FROM [{table_name}];")
241
+ elif self.db_type == "postgresql":
242
+ cur.execute(f'SELECT COUNT(*) FROM "{table_name}";')
243
+ else:
244
+ cur.execute(f"SELECT COUNT(*) FROM `{table_name}`;")
245
+ row = cur.fetchone()
246
+ val = list(row.values())[0] if isinstance(row, dict) else row[0]
247
+ return int(val)
248
+ except Exception:
249
+ return 0
250
+
251
+ # --- Query safety and formatting ---
252
+
253
+ def check_safety(self, query: str) -> Tuple[bool, str]:
254
+ """Detect destructive operations without WHERE clauses or schema drops."""
255
+ q_clean = re.sub(r"--.*$", "", query, flags=re.MULTILINE)
256
+ q_upper = q_clean.strip().upper()
257
+
258
+ if re.search(r"\bDROP\s+(TABLE|DATABASE|SCHEMA|VIEW)\b", q_upper):
259
+ return True, "DROP operation detected"
260
+ if re.search(r"\bTRUNCATE\b", q_upper):
261
+ return True, "TRUNCATE operation detected"
262
+ if re.search(r"\bDELETE\s+FROM\b", q_upper) and not re.search(r"\bWHERE\b", q_upper):
263
+ return True, "DELETE without WHERE clause (will delete all rows)"
264
+ if re.search(r"\bUPDATE\b", q_upper) and not re.search(r"\bWHERE\b", q_upper):
265
+ return True, "UPDATE without WHERE clause (will modify all rows)"
266
+ if re.search(r"\bALTER\s+TABLE\s+.*\bDROP\b", q_upper):
267
+ return True, "ALTER TABLE DROP detected"
268
+
269
+ return False, ""
270
+
271
+ def explain_query(self, query: str) -> Dict[str, Any]:
272
+ clean_q = re.sub(r"--.*$", "", query, flags=re.MULTILINE).strip().rstrip(";")
273
+ if not clean_q:
274
+ return {"success": False, "error": "Query is empty"}
275
+
276
+ if self.db_type == "sqlite":
277
+ explain_sql = f"EXPLAIN QUERY PLAN {clean_q};"
278
+ else:
279
+ explain_sql = f"EXPLAIN {clean_q};"
280
+ return self.execute_query(explain_sql)
281
+
282
+ @staticmethod
283
+ def format_sql(query: str) -> str:
284
+ """Format and uppercase SQL keywords with line indents."""
285
+ if not query or not query.strip():
286
+ return query
287
+
288
+ lines = query.strip().splitlines()
289
+ comment_lines = []
290
+ sql_lines = []
291
+ for line in lines:
292
+ if line.strip().startswith("--"):
293
+ comment_lines.append(line.strip())
294
+ else:
295
+ sql_lines.append(line)
296
+
297
+ sql_text = " ".join(sql_lines)
298
+ sql_text = re.sub(r"\s+", " ", sql_text).strip()
299
+
300
+ keywords = [
301
+ "SELECT", "FROM", "WHERE", "AND", "OR", "ORDER BY",
302
+ "GROUP BY", "HAVING", "LEFT JOIN", "RIGHT JOIN",
303
+ "INNER JOIN", "CROSS JOIN", "JOIN", "ON", "INSERT INTO",
304
+ "VALUES", "UPDATE", "SET", "DELETE FROM", "DELETE",
305
+ "LIMIT", "OFFSET", "AS", "CREATE TABLE", "CREATE VIEW",
306
+ "DROP TABLE", "DROP VIEW", "ALTER TABLE", "PRIMARY KEY",
307
+ "FOREIGN KEY", "REFERENCES", "DEFAULT", "NOT NULL",
308
+ "AUTOINCREMENT", "DESC", "ASC", "UNION ALL", "UNION",
309
+ "DISTINCT", "EXPLAIN QUERY PLAN", "EXPLAIN", "IN", "IS NULL",
310
+ "IS NOT NULL", "LIKE", "BETWEEN", "CASE", "WHEN", "THEN", "ELSE", "END",
311
+ "COUNT", "SUM", "AVG", "MIN", "MAX"
312
+ ]
313
+ keywords.sort(key=len, reverse=True)
314
+
315
+ for kw in keywords:
316
+ pattern = rf"\b{re.escape(kw)}\b"
317
+ sql_text = re.sub(pattern, kw, sql_text, flags=re.IGNORECASE)
318
+
319
+ major_clauses = [
320
+ "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "CROSS JOIN",
321
+ "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING", "LIMIT",
322
+ "OFFSET", "SET", "VALUES"
323
+ ]
324
+ for clause in major_clauses:
325
+ sql_text = re.sub(rf"\b({clause})\b", r"\n\1", sql_text)
326
+ sql_text = re.sub(r"(?<!LEFT\s)(?<!RIGHT\s)(?<!INNER\s)(?<!CROSS\s)\bJOIN\b", r"\nJOIN", sql_text)
327
+
328
+ sql_text = re.sub(r"\b(AND|OR)\b", r"\n \1", sql_text)
329
+
330
+ result_parts = []
331
+ if comment_lines:
332
+ result_parts.append("\n".join(comment_lines))
333
+ result_parts.append(sql_text.strip())
334
+ return "\n".join(result_parts)
335
+
336
+ # --- Query execution ---
337
+
338
+ def execute_query(self, query: str) -> Dict[str, Any]:
339
+ start = time.perf_counter()
340
+ try:
341
+ cur = self.conn.cursor()
342
+ cur.execute(query)
343
+ elapsed_ms = (time.perf_counter() - start) * 1000
344
+
345
+ if cur.description:
346
+ columns = [d[0] for d in cur.description]
347
+ raw = cur.fetchall()
348
+ rows = []
349
+ for r in raw:
350
+ if isinstance(r, dict):
351
+ rows.append(list(r.values()))
352
+ else:
353
+ rows.append(list(r))
354
+ return {
355
+ "success": True,
356
+ "columns": columns,
357
+ "rows": rows,
358
+ "row_count": len(rows),
359
+ "elapsed_ms": round(elapsed_ms, 2),
360
+ "is_select": True,
361
+ }
362
+ else:
363
+ if self.db_type in ("sqlite", "postgresql"):
364
+ self.conn.commit()
365
+ return {
366
+ "success": True,
367
+ "affected_rows": max(cur.rowcount, 0),
368
+ "elapsed_ms": round(elapsed_ms, 2),
369
+ "is_select": False,
370
+ }
371
+ except Exception as e:
372
+ elapsed_ms = (time.perf_counter() - start) * 1000
373
+ if self.db_type == "postgresql":
374
+ try:
375
+ self.conn.rollback()
376
+ except Exception:
377
+ pass
378
+ return {
379
+ "success": False,
380
+ "error": str(e),
381
+ "elapsed_ms": round(elapsed_ms, 2),
382
+ }
383
+
384
+ # --- Helpers ---
385
+
386
+ def db_type_label(self) -> str:
387
+ return {"sqlite": "SQLite", "postgresql": "PostgreSQL", "mysql": "MySQL"}.get(
388
+ self.db_type, self.db_type.upper()
389
+ )
390
+
391
+ def close(self):
392
+ if self.conn:
393
+ try:
394
+ self.conn.close()
395
+ except Exception:
396
+ pass
exporter.py ADDED
@@ -0,0 +1,55 @@
1
+ import csv
2
+ import json
3
+ from typing import List, Any
4
+
5
+
6
+ class Exporter:
7
+ @staticmethod
8
+ def to_csv(columns: List[str], rows: List[List[Any]], output_path: str):
9
+ with open(output_path, "w", newline="", encoding="utf-8") as f:
10
+ writer = csv.writer(f)
11
+ writer.writerow(columns)
12
+ writer.writerows(rows)
13
+
14
+ @staticmethod
15
+ def to_json(columns: List[str], rows: List[List[Any]], output_path: str):
16
+ data = []
17
+ for r in rows:
18
+ data.append(dict(zip(columns, r)))
19
+ with open(output_path, "w", encoding="utf-8") as f:
20
+ json.dump(data, f, indent=2, ensure_ascii=False)
21
+
22
+ @staticmethod
23
+ def to_markdown(columns: List[str], rows: List[List[Any]], output_path: str):
24
+ with open(output_path, "w", encoding="utf-8") as f:
25
+ # Header
26
+ f.write("| " + " | ".join(str(c) for c in columns) + " |\n")
27
+ # Separator
28
+ f.write("| " + " | ".join("---" for _ in columns) + " |\n")
29
+ # Rows
30
+ for r in rows:
31
+ clean_cells = [
32
+ (str(val).replace("|", "\\|").replace("\n", " ") if val is not None else "NULL")
33
+ for val in r
34
+ ]
35
+ f.write("| " + " | ".join(clean_cells) + " |\n")
36
+
37
+ @staticmethod
38
+ def to_sql_inserts(table_name: str, columns: List[str], rows: List[List[Any]], output_path: str):
39
+ table = table_name or "exported_table"
40
+ cols_str = ", ".join(columns)
41
+ with open(output_path, "w", encoding="utf-8") as f:
42
+ f.write(f"-- GhostBase SQL Export: {table}\n")
43
+ f.write(f"-- Total: {len(rows)} row(s)\n\n")
44
+ for r in rows:
45
+ vals = []
46
+ for v in r:
47
+ if v is None:
48
+ vals.append("NULL")
49
+ elif isinstance(v, (int, float)):
50
+ vals.append(str(v))
51
+ else:
52
+ escaped = str(v).replace("'", "''")
53
+ vals.append(f"'{escaped}'")
54
+ vals_str = ", ".join(vals)
55
+ f.write(f"INSERT INTO {table} ({cols_str}) VALUES ({vals_str});\n")
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostbase-cli
3
+ Version: 0.1.0
4
+ Summary: A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL & MySQL
5
+ Author-email: 4yk5 <contact.4yk5@proton.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/4yk5/GhostBase
8
+ Project-URL: Repository, https://github.com/4yk5/GhostBase
9
+ Project-URL: Issues, https://github.com/4yk5/GhostBase/issues
10
+ Keywords: database,sql,tui,terminal,cli,sqlite,postgres,postgresql,mysql,dbeaver,datagrip,query,developer-tools
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Database :: Front-Ends
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: textual>=8.0.0
20
+ Requires-Dist: rich>=14.0.0
21
+ Requires-Dist: psycopg2-binary>=2.9.0
22
+ Requires-Dist: pymysql>=1.1.0
23
+ Dynamic: license-file
24
+
25
+ ```
26
+ ____ _ _ ____
27
+ / ___| |__ ___ ___| |_| __ ) __ _ ___ ___
28
+ | | _| '_ \ / _ \/ __| __| _ \ / _` / __|/ _ \
29
+ | |_| | | | | (_) \__ \ |_| |_) | (_| \__ \ __/
30
+ \____|_| |_|\___/|___/\__|____/ \__,_|___/\___|
31
+
32
+ ```
33
+
34
+ # GhostBase 👻🗄️
35
+
36
+ > **A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL, and MySQL.**
37
+ > *No Java runtimes. No 1.5GB RAM bloat. Built-in production safety guard, instant SQL beautifier & zero-config `.env` connect.*
38
+
39
+ [![CI](https://github.com/4yk5/GhostBase/actions/workflows/ci.yml/badge.svg)](https://github.com/4yk5/GhostBase/actions)
40
+ [![PyPI](https://img.shields.io/pypi/v/ghostbase-cli.svg)](https://pypi.org/project/ghostbase-cli/)
41
+ [![Python: 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
43
+ [![UI: Textual](https://img.shields.io/badge/UI-Textual-bd93f9.svg)](https://github.com/Textualize/textual)
44
+
45
+ ---
46
+
47
+ ## ⚡ Why GhostBase?
48
+
49
+ Opening **DBeaver** or **DataGrip** just to run a quick `SELECT`, inspect an index, or check foreign keys is painful:
50
+ - ❌ **1.5 GB+ RAM bloat:** Heavy Java/Eclipse runtimes that spin your laptop fans.
51
+ 👉 **GhostBase idles at ~25 MB RAM.**
52
+ - ❌ **15-Second Cold Starts:** Splash screens, driver updates, and workspace indexing.
53
+ 👉 **GhostBase starts in under 120 ms directly in your terminal.**
54
+ - ❌ **Accidental Destructive Queries on Production:** Running `DELETE` or `UPDATE` without a `WHERE` clause can wipe production tables.
55
+ 👉 **GhostBase intercepts destructive queries automatically with double-confirmation and one-key Read-Only Safe Mode (`Ctrl + S`).**
56
+ - ❌ **Messy, Unformatted Queries:** Manually formatting and indenting long SQL statements.
57
+ 👉 **GhostBase auto-formats and beautifies messy SQL queries with one keypress (`Ctrl + B`).**
58
+ - ❌ **Tedious Connection Setup:** Typing credentials into multi-tab dialogs.
59
+ 👉 **GhostBase auto-detects `DATABASE_URL` from your local `.env` file.**
60
+ - ❌ **Seeding & Migrating Data:** Manually writing `INSERT INTO` statements to replicate test rows.
61
+ 👉 **GhostBase exports query results directly into executable SQL `INSERT` statements (`Ctrl + I`).**
62
+ - ❌ **Exporting for GitHub / Jira / Notion:** Exporting CSV, then finding a website to convert to Markdown.
63
+ 👉 **One-key instant Markdown table export (`Ctrl + M`) directly into clean GitHub Flavored Markdown.**
64
+
65
+ ---
66
+
67
+ ## 🚀 Killer Features
68
+
69
+ - 🛡️ **Production Safety Guard & Safe Mode (`Ctrl + S`):**
70
+ - Intercepts dangerous statements (`DELETE`/`UPDATE` without `WHERE`, `DROP TABLE`, `TRUNCATE`).
71
+ - In **Safe Mode**, all destructive operations are strictly blocked.
72
+ - In normal mode, requires explicit double-confirmation before execution.
73
+ - ✨ **1-Key SQL Beautifier (`Ctrl + B`):**
74
+ - Instantly cleans up whitespace, capitalizes SQL keywords, and indents sub-clauses (`FROM`, `WHERE`, `AND`, `OR`, `JOIN`, `ORDER BY`).
75
+ - ⚡ **1-Key Query Plan (`Ctrl + P`):**
76
+ - Run instant execution plans (`EXPLAIN QUERY PLAN` / `EXPLAIN`) on any query to see index usage and bottlenecks.
77
+ - 🔌 **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.
80
+ - 📊 **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.
82
+ - 💾 **Multi-Format Exports:** Instant SQL `INSERT`s (`Ctrl + I`), Markdown (`Ctrl + M`), CSV (`Ctrl + E`), and JSON (`Ctrl + J`).
83
+ - 📜 **Query History:** Cycle through your last 20 queries with `Ctrl + ↑` and `Ctrl + ↓`.
84
+ - 🎨 **Clean Cyberpunk Aesthetic:** Fast, high-contrast, zero-flicker terminal interface.
85
+
86
+ ---
87
+
88
+ ## 📦 Installation & Quick Start
89
+
90
+ ### Via pip
91
+
92
+ ```bash
93
+ pip install ghostbase-cli
94
+ ```
95
+
96
+ ### From Source
97
+
98
+ ```bash
99
+ git clone https://github.com/4yk5/GhostBase.git
100
+ cd GhostBase
101
+ pip install -r requirements.txt
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 💻 How to Use
107
+
108
+ ### 1. Auto-connect to current project (`.env`)
109
+ ```bash
110
+ # Automatically finds DATABASE_URL or DB_HOST in your current directory's .env
111
+ ghostbase
112
+ ```
113
+
114
+ ### 2. Connect to SQLite
115
+ ```bash
116
+ ghostbase path/to/database.db
117
+ ```
118
+
119
+ ### 3. Connect to PostgreSQL
120
+ ```bash
121
+ ghostbase postgresql://user:password@localhost:5432/my_database
122
+ ```
123
+
124
+ ### 4. Connect to MySQL
125
+ ```bash
126
+ ghostbase mysql://user:password@localhost:3306/my_database
127
+ ```
128
+
129
+ ### 5. Launch in Safe / Read-Only Mode
130
+ ```bash
131
+ ghostbase postgresql://prod-db:5432/main --read-only
132
+ ```
133
+
134
+ ---
135
+
136
+ ## ⌨️ Keyboard Shortcuts
137
+
138
+ | Shortcut | Description |
139
+ | :--- | :--- |
140
+ | `Ctrl + R` | **Execute SQL Query** (runs the query with safety validation) |
141
+ | `Ctrl + P` | **1-Key EXPLAIN** (instant query execution plan and performance inspector) |
142
+ | `Ctrl + B` | **Beautify SQL** (auto-formats, capitalizes keywords and indents clauses) |
143
+ | `Ctrl + S` | **Toggle Safe Mode** (enable/disable destructive query protection) |
144
+ | `Ctrl + N` | **New Query** (clears editor for a clean query) |
145
+ | `Ctrl + F` | **Find Table** (focuses table search filter) |
146
+ | `Ctrl + I` | **Export to SQL INSERTs** (generates executable `INSERT INTO` statements) |
147
+ | `Ctrl + M` | **Export to Markdown** (GitHub / Jira / Notion ready table) |
148
+ | `Ctrl + E` | **Export to CSV** (exports current table/query to CSV) |
149
+ | `Ctrl + J` | **Export to JSON** (exports current table/query to JSON) |
150
+ | `Ctrl + ↑` / `Ctrl + ↓` | **Query History** (cycle through previous queries) |
151
+ | `Tab` / `Shift + Tab` | Navigate between explorer, search, editor, and data table |
152
+ | `Ctrl + Q` | **Quit GhostBase** |
153
+
154
+ ---
155
+
156
+ ## 📬 Contact & Inquiries
157
+
158
+ For partnerships, feature requests, or business inquiries:
159
+ 📧 **[contact.4yk5@proton.me](mailto:contact.4yk5@proton.me)**
160
+
161
+ ---
162
+
163
+ ## 📄 License
164
+
165
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,10 @@
1
+ app.py,sha256=6ld3qVBcpzGV8TN_asTxCWN4Yky3WTHL9ygqPi1eUj0,26752
2
+ db_engine.py,sha256=ZTFwBn1ALGdZCqHijMm6HGc_Q9r1AgJn0d5sTdl4BUo,15774
3
+ exporter.py,sha256=LWP5aH5T1hfgv1S-3cUzznQe4QuWt5cPwl_A5P6h_PA,2200
4
+ main.py,sha256=Jds9cR2JS6hI4ZAHFyKcSCdqdojXybNI0ra7xMO_tS4,3949
5
+ ghostbase_cli-0.1.0.dist-info/licenses/LICENSE,sha256=bRo5mnl7qxXILhxEc0K1gp_mlFDLvbZN6ZxQyAyPtMQ,1061
6
+ ghostbase_cli-0.1.0.dist-info/METADATA,sha256=b6pkOhxKF6Fbh3tz6_58lYw2sYPnqONOmDA3IFPfqMQ,7346
7
+ ghostbase_cli-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ ghostbase_cli-0.1.0.dist-info/entry_points.txt,sha256=MU-O2U7GG76Irs1c7hQmlG8YkQ40sOlPnpU0RKI8Kzc,40
9
+ ghostbase_cli-0.1.0.dist-info/top_level.txt,sha256=AUwoRm-j29Gjq6eOpfILcrpBYifVS0Pi2JCHvoi4Ufg,28
10
+ ghostbase_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ghostbase = main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 4yk5
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ app
2
+ db_engine
3
+ exporter
4
+ main