sqlshell 0.1.4__tar.gz → 0.1.5__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.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- {sqlshell-0.1.4 → sqlshell-0.1.5}/PKG-INFO +8 -2
- {sqlshell-0.1.4 → sqlshell-0.1.5}/README.md +7 -1
- {sqlshell-0.1.4 → sqlshell-0.1.5}/pyproject.toml +1 -1
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell/__init__.py +1 -1
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell/main.py +165 -8
- sqlshell-0.1.5/sqlshell/sqlshell/create_test_databases.py +96 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/PKG-INFO +8 -2
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/SOURCES.txt +1 -1
- sqlshell-0.1.4/sqlshell/sqlshell/main.py +0 -346
- {sqlshell-0.1.4 → sqlshell-0.1.5}/setup.cfg +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/setup.py +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell/setup.py +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell/sqlshell/__init__.py +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell/sqlshell/create_test_data.py +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/dependency_links.txt +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/entry_points.txt +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/requires.txt +0 -0
- {sqlshell-0.1.4 → sqlshell-0.1.5}/sqlshell.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: sqlshell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A powerful SQL shell with GUI interface for data analysis
|
|
5
5
|
Home-page: https://github.com/yourusername/sqlshell
|
|
6
6
|
Author: SQLShell Team
|
|
@@ -29,7 +29,7 @@ Dynamic: requires-python
|
|
|
29
29
|
|
|
30
30
|
# SQL Shell
|
|
31
31
|
|
|
32
|
-
A GUI application that provides a SQL REPL interface for querying
|
|
32
|
+
A GUI application that provides a SQL REPL interface for querying Excel and parquet files (more to come!)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|

|
|
@@ -50,6 +50,12 @@ A GUI application that provides a SQL REPL interface for querying DuckDB databas
|
|
|
50
50
|
pip install -r requirements.txt
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
You can also do:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install sqlshell
|
|
57
|
+
```
|
|
58
|
+
|
|
53
59
|
## Usage
|
|
54
60
|
|
|
55
61
|
1. Run the application:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SQL Shell
|
|
2
2
|
|
|
3
|
-
A GUI application that provides a SQL REPL interface for querying
|
|
3
|
+
A GUI application that provides a SQL REPL interface for querying Excel and parquet files (more to come!)
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|

|
|
@@ -21,6 +21,12 @@ A GUI application that provides a SQL REPL interface for querying DuckDB databas
|
|
|
21
21
|
pip install -r requirements.txt
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
You can also do:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install sqlshell
|
|
28
|
+
```
|
|
29
|
+
|
|
24
30
|
## Usage
|
|
25
31
|
|
|
26
32
|
1. Run the application:
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import os
|
|
3
3
|
import duckdb
|
|
4
|
+
import sqlite3
|
|
4
5
|
import pandas as pd
|
|
5
6
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
6
7
|
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
7
8
|
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
8
|
-
QTableWidgetItem, QHeaderView)
|
|
9
|
+
QTableWidgetItem, QHeaderView, QMessageBox)
|
|
9
10
|
from PyQt6.QtCore import Qt, QAbstractTableModel
|
|
10
11
|
from PyQt6.QtGui import QFont, QColor
|
|
11
12
|
import numpy as np
|
|
12
13
|
from datetime import datetime
|
|
13
|
-
import create_test_data # Import the
|
|
14
|
+
from sqlshell.sqlshell import create_test_data # Import from the correct location
|
|
14
15
|
|
|
15
16
|
class SQLShell(QMainWindow):
|
|
16
17
|
def __init__(self):
|
|
17
18
|
super().__init__()
|
|
18
|
-
self.
|
|
19
|
+
self.current_db_type = 'duckdb' # Default to DuckDB
|
|
20
|
+
self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
|
|
19
21
|
self.loaded_tables = {} # Keep track of loaded tables
|
|
20
22
|
self.init_ui()
|
|
21
23
|
|
|
@@ -32,7 +34,11 @@ class SQLShell(QMainWindow):
|
|
|
32
34
|
left_panel = QWidget()
|
|
33
35
|
left_layout = QVBoxLayout(left_panel)
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# Database info label
|
|
38
|
+
self.db_info_label = QLabel("No database connected")
|
|
39
|
+
left_layout.addWidget(self.db_info_label)
|
|
40
|
+
|
|
41
|
+
tables_label = QLabel("Tables:")
|
|
36
42
|
left_layout.addWidget(tables_label)
|
|
37
43
|
|
|
38
44
|
self.tables_list = QListWidget()
|
|
@@ -41,6 +47,8 @@ class SQLShell(QMainWindow):
|
|
|
41
47
|
|
|
42
48
|
# Buttons for table management
|
|
43
49
|
table_buttons_layout = QHBoxLayout()
|
|
50
|
+
self.open_db_btn = QPushButton('Open Database')
|
|
51
|
+
self.open_db_btn.clicked.connect(self.open_database)
|
|
44
52
|
self.browse_btn = QPushButton('Load Files')
|
|
45
53
|
self.browse_btn.clicked.connect(self.browse_files)
|
|
46
54
|
self.remove_table_btn = QPushButton('Remove Selected')
|
|
@@ -48,6 +56,7 @@ class SQLShell(QMainWindow):
|
|
|
48
56
|
self.test_btn = QPushButton('Test')
|
|
49
57
|
self.test_btn.clicked.connect(self.load_test_data)
|
|
50
58
|
|
|
59
|
+
table_buttons_layout.addWidget(self.open_db_btn)
|
|
51
60
|
table_buttons_layout.addWidget(self.browse_btn)
|
|
52
61
|
table_buttons_layout.addWidget(self.remove_table_btn)
|
|
53
62
|
table_buttons_layout.addWidget(self.test_btn)
|
|
@@ -71,8 +80,16 @@ class SQLShell(QMainWindow):
|
|
|
71
80
|
self.clear_btn = QPushButton('Clear')
|
|
72
81
|
self.clear_btn.clicked.connect(self.clear_query)
|
|
73
82
|
|
|
83
|
+
# Add export buttons
|
|
84
|
+
self.export_excel_btn = QPushButton('Export to Excel')
|
|
85
|
+
self.export_excel_btn.clicked.connect(self.export_to_excel)
|
|
86
|
+
self.export_parquet_btn = QPushButton('Export to Parquet')
|
|
87
|
+
self.export_parquet_btn.clicked.connect(self.export_to_parquet)
|
|
88
|
+
|
|
74
89
|
button_layout.addWidget(self.execute_btn)
|
|
75
90
|
button_layout.addWidget(self.clear_btn)
|
|
91
|
+
button_layout.addWidget(self.export_excel_btn)
|
|
92
|
+
button_layout.addWidget(self.export_parquet_btn)
|
|
76
93
|
button_layout.addStretch()
|
|
77
94
|
|
|
78
95
|
query_layout.addLayout(button_layout)
|
|
@@ -176,6 +193,12 @@ class SQLShell(QMainWindow):
|
|
|
176
193
|
self.row_count_label.setText(f"{len(df):,} {row_text}")
|
|
177
194
|
|
|
178
195
|
def browse_files(self):
|
|
196
|
+
if not self.conn:
|
|
197
|
+
# Create a default in-memory DuckDB connection if none exists
|
|
198
|
+
self.conn = duckdb.connect(':memory:')
|
|
199
|
+
self.current_db_type = 'duckdb'
|
|
200
|
+
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
201
|
+
|
|
179
202
|
file_names, _ = QFileDialog.getOpenFileNames(
|
|
180
203
|
self,
|
|
181
204
|
"Open Data Files",
|
|
@@ -205,8 +228,14 @@ class SQLShell(QMainWindow):
|
|
|
205
228
|
table_name = f"{original_name}_{counter}"
|
|
206
229
|
counter += 1
|
|
207
230
|
|
|
208
|
-
#
|
|
209
|
-
self.
|
|
231
|
+
# Handle table creation based on database type
|
|
232
|
+
if self.current_db_type == 'sqlite':
|
|
233
|
+
# For SQLite, create a table from the DataFrame
|
|
234
|
+
df.to_sql(table_name, self.conn, index=False, if_exists='replace')
|
|
235
|
+
else:
|
|
236
|
+
# For DuckDB, register the DataFrame as a view
|
|
237
|
+
self.conn.register(table_name, df)
|
|
238
|
+
|
|
210
239
|
self.loaded_tables[table_name] = file_name
|
|
211
240
|
|
|
212
241
|
# Update UI
|
|
@@ -251,13 +280,91 @@ class SQLShell(QMainWindow):
|
|
|
251
280
|
self.row_count_label.setText("")
|
|
252
281
|
self.results_label.setText(f"Removed table: {table_name}")
|
|
253
282
|
|
|
283
|
+
def open_database(self):
|
|
284
|
+
"""Open a database file (DuckDB or SQLite)"""
|
|
285
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
286
|
+
self,
|
|
287
|
+
"Open Database File",
|
|
288
|
+
"",
|
|
289
|
+
"Database Files (*.db);;All Files (*)"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if not file_name:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
# Try to detect database type
|
|
297
|
+
is_sqlite = self.is_sqlite_db(file_name)
|
|
298
|
+
|
|
299
|
+
# Close existing connection if any
|
|
300
|
+
if self.conn:
|
|
301
|
+
self.conn.close()
|
|
302
|
+
|
|
303
|
+
# Connect to the database
|
|
304
|
+
if is_sqlite:
|
|
305
|
+
self.conn = sqlite3.connect(file_name)
|
|
306
|
+
self.current_db_type = 'sqlite'
|
|
307
|
+
else:
|
|
308
|
+
self.conn = duckdb.connect(file_name)
|
|
309
|
+
self.current_db_type = 'duckdb'
|
|
310
|
+
|
|
311
|
+
# Clear existing tables
|
|
312
|
+
self.loaded_tables.clear()
|
|
313
|
+
self.tables_list.clear()
|
|
314
|
+
|
|
315
|
+
# Load tables
|
|
316
|
+
self.load_database_tables()
|
|
317
|
+
|
|
318
|
+
# Update UI
|
|
319
|
+
db_type = "SQLite" if is_sqlite else "DuckDB"
|
|
320
|
+
self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
|
|
321
|
+
self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
QMessageBox.critical(self, "Error", f"Failed to open database: {str(e)}")
|
|
325
|
+
self.statusBar().showMessage('Error opening database')
|
|
326
|
+
|
|
327
|
+
def is_sqlite_db(self, filename):
|
|
328
|
+
"""Check if the file is a SQLite database"""
|
|
329
|
+
try:
|
|
330
|
+
with open(filename, 'rb') as f:
|
|
331
|
+
header = f.read(16)
|
|
332
|
+
return header[:16] == b'SQLite format 3\x00'
|
|
333
|
+
except:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
def load_database_tables(self):
|
|
337
|
+
"""Load all tables from the current database"""
|
|
338
|
+
try:
|
|
339
|
+
if self.current_db_type == 'sqlite':
|
|
340
|
+
query = "SELECT name FROM sqlite_master WHERE type='table'"
|
|
341
|
+
cursor = self.conn.cursor()
|
|
342
|
+
tables = cursor.execute(query).fetchall()
|
|
343
|
+
for (table_name,) in tables:
|
|
344
|
+
self.loaded_tables[table_name] = 'database'
|
|
345
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
346
|
+
else: # duckdb
|
|
347
|
+
query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
348
|
+
result = self.conn.execute(query).fetchdf()
|
|
349
|
+
for table_name in result['table_name']:
|
|
350
|
+
self.loaded_tables[table_name] = 'database'
|
|
351
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
352
|
+
except Exception as e:
|
|
353
|
+
self.statusBar().showMessage(f'Error loading tables: {str(e)}')
|
|
354
|
+
|
|
254
355
|
def execute_query(self):
|
|
255
356
|
query = self.query_edit.toPlainText().strip()
|
|
256
357
|
if not query:
|
|
257
358
|
return
|
|
258
359
|
|
|
259
360
|
try:
|
|
260
|
-
|
|
361
|
+
if self.current_db_type == 'sqlite':
|
|
362
|
+
# Execute SQLite query and convert to DataFrame
|
|
363
|
+
result = pd.read_sql_query(query, self.conn)
|
|
364
|
+
else:
|
|
365
|
+
# Execute DuckDB query
|
|
366
|
+
result = self.conn.execute(query).fetchdf()
|
|
367
|
+
|
|
261
368
|
self.populate_table(result)
|
|
262
369
|
self.results_label.setText("Query Results:")
|
|
263
370
|
self.statusBar().showMessage('Query executed successfully')
|
|
@@ -276,7 +383,11 @@ class SQLShell(QMainWindow):
|
|
|
276
383
|
if item:
|
|
277
384
|
table_name = item.text().split(' (')[0]
|
|
278
385
|
try:
|
|
279
|
-
|
|
386
|
+
if self.current_db_type == 'sqlite':
|
|
387
|
+
preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
|
|
388
|
+
else:
|
|
389
|
+
preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
|
|
390
|
+
|
|
280
391
|
self.populate_table(preview_df)
|
|
281
392
|
self.results_label.setText(f"Preview of {table_name}:")
|
|
282
393
|
self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
|
|
@@ -332,6 +443,52 @@ class SQLShell(QMainWindow):
|
|
|
332
443
|
except Exception as e:
|
|
333
444
|
self.statusBar().showMessage(f'Error loading test data: {str(e)}')
|
|
334
445
|
|
|
446
|
+
def export_to_excel(self):
|
|
447
|
+
if self.results_table.rowCount() == 0:
|
|
448
|
+
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
file_name, _ = QFileDialog.getSaveFileName(self, "Save as Excel", "", "Excel Files (*.xlsx);;All Files (*)")
|
|
452
|
+
if not file_name:
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
# Convert table data to DataFrame
|
|
457
|
+
df = self.get_table_data_as_dataframe()
|
|
458
|
+
df.to_excel(file_name, index=False)
|
|
459
|
+
self.statusBar().showMessage(f'Data exported to {file_name}')
|
|
460
|
+
except Exception as e:
|
|
461
|
+
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
462
|
+
|
|
463
|
+
def export_to_parquet(self):
|
|
464
|
+
if self.results_table.rowCount() == 0:
|
|
465
|
+
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
file_name, _ = QFileDialog.getSaveFileName(self, "Save as Parquet", "", "Parquet Files (*.parquet);;All Files (*)")
|
|
469
|
+
if not file_name:
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
# Convert table data to DataFrame
|
|
474
|
+
df = self.get_table_data_as_dataframe()
|
|
475
|
+
df.to_parquet(file_name, index=False)
|
|
476
|
+
self.statusBar().showMessage(f'Data exported to {file_name}')
|
|
477
|
+
except Exception as e:
|
|
478
|
+
QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
|
|
479
|
+
|
|
480
|
+
def get_table_data_as_dataframe(self):
|
|
481
|
+
"""Helper function to convert table widget data to a DataFrame"""
|
|
482
|
+
headers = [self.results_table.horizontalHeaderItem(i).text() for i in range(self.results_table.columnCount())]
|
|
483
|
+
data = []
|
|
484
|
+
for row in range(self.results_table.rowCount()):
|
|
485
|
+
row_data = []
|
|
486
|
+
for column in range(self.results_table.columnCount()):
|
|
487
|
+
item = self.results_table.item(row, column)
|
|
488
|
+
row_data.append(item.text() if item else '')
|
|
489
|
+
data.append(row_data)
|
|
490
|
+
return pd.DataFrame(data, columns=headers)
|
|
491
|
+
|
|
335
492
|
def main():
|
|
336
493
|
app = QApplication(sys.argv)
|
|
337
494
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import sqlite3
|
|
3
|
+
import duckdb
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# Define paths
|
|
7
|
+
TEST_DATA_DIR = 'test_data'
|
|
8
|
+
SQLITE_DB_PATH = os.path.join(TEST_DATA_DIR, 'test.db')
|
|
9
|
+
DUCKDB_PATH = os.path.join(TEST_DATA_DIR, 'test.duckdb')
|
|
10
|
+
|
|
11
|
+
def load_source_data():
|
|
12
|
+
"""Load the source Excel and Parquet files."""
|
|
13
|
+
sales_df = pd.read_excel(os.path.join(TEST_DATA_DIR, 'sample_sales_data.xlsx'))
|
|
14
|
+
customer_df = pd.read_parquet(os.path.join(TEST_DATA_DIR, 'customer_data.parquet'))
|
|
15
|
+
product_df = pd.read_excel(os.path.join(TEST_DATA_DIR, 'product_catalog.xlsx'))
|
|
16
|
+
return sales_df, customer_df, product_df
|
|
17
|
+
|
|
18
|
+
def create_sqlite_database():
|
|
19
|
+
"""Create SQLite database with the test data."""
|
|
20
|
+
# Remove existing database if it exists
|
|
21
|
+
if os.path.exists(SQLITE_DB_PATH):
|
|
22
|
+
os.remove(SQLITE_DB_PATH)
|
|
23
|
+
|
|
24
|
+
# Load data
|
|
25
|
+
sales_df, customer_df, product_df = load_source_data()
|
|
26
|
+
|
|
27
|
+
# Create connection and write tables
|
|
28
|
+
with sqlite3.connect(SQLITE_DB_PATH) as conn:
|
|
29
|
+
sales_df.to_sql('sales', conn, index=False)
|
|
30
|
+
customer_df.to_sql('customers', conn, index=False)
|
|
31
|
+
product_df.to_sql('products', conn, index=False)
|
|
32
|
+
|
|
33
|
+
# Create indexes for better performance
|
|
34
|
+
conn.execute('CREATE INDEX idx_sales_customer ON sales(CustomerID)')
|
|
35
|
+
conn.execute('CREATE INDEX idx_sales_product ON sales(ProductID)')
|
|
36
|
+
|
|
37
|
+
print(f"Created SQLite database at {SQLITE_DB_PATH}")
|
|
38
|
+
|
|
39
|
+
def create_duckdb_database():
|
|
40
|
+
"""Create DuckDB database with the test data."""
|
|
41
|
+
# Remove existing database if it exists
|
|
42
|
+
if os.path.exists(DUCKDB_PATH):
|
|
43
|
+
os.remove(DUCKDB_PATH)
|
|
44
|
+
|
|
45
|
+
# Load data
|
|
46
|
+
sales_df, customer_df, product_df = load_source_data()
|
|
47
|
+
|
|
48
|
+
# Create connection and write tables
|
|
49
|
+
with duckdb.connect(DUCKDB_PATH) as conn:
|
|
50
|
+
conn.execute("CREATE TABLE sales AS SELECT * FROM sales_df")
|
|
51
|
+
conn.execute("CREATE TABLE customers AS SELECT * FROM customer_df")
|
|
52
|
+
conn.execute("CREATE TABLE products AS SELECT * FROM product_df")
|
|
53
|
+
|
|
54
|
+
# Create indexes for better performance
|
|
55
|
+
conn.execute('CREATE INDEX idx_sales_customer ON sales(CustomerID)')
|
|
56
|
+
conn.execute('CREATE INDEX idx_sales_product ON sales(ProductID)')
|
|
57
|
+
|
|
58
|
+
print(f"Created DuckDB database at {DUCKDB_PATH}")
|
|
59
|
+
|
|
60
|
+
def verify_databases():
|
|
61
|
+
"""Verify the databases were created correctly by running test queries."""
|
|
62
|
+
# Test SQLite
|
|
63
|
+
with sqlite3.connect(SQLITE_DB_PATH) as conn:
|
|
64
|
+
sales_count = pd.read_sql("SELECT COUNT(*) as count FROM sales", conn).iloc[0]['count']
|
|
65
|
+
print(f"\nSQLite verification:")
|
|
66
|
+
print(f"Sales records: {sales_count}")
|
|
67
|
+
|
|
68
|
+
# Test a join query
|
|
69
|
+
sample_query = """
|
|
70
|
+
SELECT
|
|
71
|
+
p.Category,
|
|
72
|
+
COUNT(*) as NumOrders,
|
|
73
|
+
ROUND(SUM(s.TotalAmount), 2) as TotalRevenue
|
|
74
|
+
FROM sales s
|
|
75
|
+
JOIN products p ON s.ProductID = p.ProductID
|
|
76
|
+
GROUP BY p.Category
|
|
77
|
+
LIMIT 3
|
|
78
|
+
"""
|
|
79
|
+
print("\nSample SQLite query result:")
|
|
80
|
+
print(pd.read_sql(sample_query, conn))
|
|
81
|
+
|
|
82
|
+
# Test DuckDB
|
|
83
|
+
with duckdb.connect(DUCKDB_PATH) as conn:
|
|
84
|
+
sales_count = conn.execute("SELECT COUNT(*) as count FROM sales").fetchone()[0]
|
|
85
|
+
print(f"\nDuckDB verification:")
|
|
86
|
+
print(f"Sales records: {sales_count}")
|
|
87
|
+
|
|
88
|
+
# Test the same join query
|
|
89
|
+
print("\nSample DuckDB query result:")
|
|
90
|
+
print(conn.execute(sample_query).df())
|
|
91
|
+
|
|
92
|
+
if __name__ == '__main__':
|
|
93
|
+
print("Creating test databases...")
|
|
94
|
+
create_sqlite_database()
|
|
95
|
+
create_duckdb_database()
|
|
96
|
+
verify_databases()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: sqlshell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A powerful SQL shell with GUI interface for data analysis
|
|
5
5
|
Home-page: https://github.com/yourusername/sqlshell
|
|
6
6
|
Author: SQLShell Team
|
|
@@ -29,7 +29,7 @@ Dynamic: requires-python
|
|
|
29
29
|
|
|
30
30
|
# SQL Shell
|
|
31
31
|
|
|
32
|
-
A GUI application that provides a SQL REPL interface for querying
|
|
32
|
+
A GUI application that provides a SQL REPL interface for querying Excel and parquet files (more to come!)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|

|
|
@@ -50,6 +50,12 @@ A GUI application that provides a SQL REPL interface for querying DuckDB databas
|
|
|
50
50
|
pip install -r requirements.txt
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
You can also do:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install sqlshell
|
|
57
|
+
```
|
|
58
|
+
|
|
53
59
|
## Usage
|
|
54
60
|
|
|
55
61
|
1. Run the application:
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import os
|
|
3
|
-
import duckdb
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
6
|
-
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
7
|
-
QLabel, QSplitter, QListWidget, QTableWidget,
|
|
8
|
-
QTableWidgetItem, QHeaderView)
|
|
9
|
-
from PyQt6.QtCore import Qt, QAbstractTableModel
|
|
10
|
-
from PyQt6.QtGui import QFont, QColor
|
|
11
|
-
import numpy as np
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
from . import create_test_data # Import from the same package
|
|
14
|
-
|
|
15
|
-
class SQLShell(QMainWindow):
|
|
16
|
-
def __init__(self):
|
|
17
|
-
super().__init__()
|
|
18
|
-
self.conn = duckdb.connect('pool.db')
|
|
19
|
-
self.loaded_tables = {} # Keep track of loaded tables
|
|
20
|
-
self.init_ui()
|
|
21
|
-
|
|
22
|
-
def init_ui(self):
|
|
23
|
-
self.setWindowTitle('SQL Shell')
|
|
24
|
-
self.setGeometry(100, 100, 1400, 800)
|
|
25
|
-
|
|
26
|
-
# Create central widget and layout
|
|
27
|
-
central_widget = QWidget()
|
|
28
|
-
self.setCentralWidget(central_widget)
|
|
29
|
-
main_layout = QHBoxLayout(central_widget)
|
|
30
|
-
|
|
31
|
-
# Left panel for table list
|
|
32
|
-
left_panel = QWidget()
|
|
33
|
-
left_layout = QVBoxLayout(left_panel)
|
|
34
|
-
|
|
35
|
-
tables_label = QLabel("Loaded Tables:")
|
|
36
|
-
left_layout.addWidget(tables_label)
|
|
37
|
-
|
|
38
|
-
self.tables_list = QListWidget()
|
|
39
|
-
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
40
|
-
left_layout.addWidget(self.tables_list)
|
|
41
|
-
|
|
42
|
-
# Buttons for table management
|
|
43
|
-
table_buttons_layout = QHBoxLayout()
|
|
44
|
-
self.browse_btn = QPushButton('Load Files')
|
|
45
|
-
self.browse_btn.clicked.connect(self.browse_files)
|
|
46
|
-
self.remove_table_btn = QPushButton('Remove Selected')
|
|
47
|
-
self.remove_table_btn.clicked.connect(self.remove_selected_table)
|
|
48
|
-
self.test_btn = QPushButton('Test')
|
|
49
|
-
self.test_btn.clicked.connect(self.load_test_data)
|
|
50
|
-
|
|
51
|
-
table_buttons_layout.addWidget(self.browse_btn)
|
|
52
|
-
table_buttons_layout.addWidget(self.remove_table_btn)
|
|
53
|
-
table_buttons_layout.addWidget(self.test_btn)
|
|
54
|
-
left_layout.addLayout(table_buttons_layout)
|
|
55
|
-
|
|
56
|
-
# Right panel for query and results
|
|
57
|
-
right_panel = QWidget()
|
|
58
|
-
right_layout = QVBoxLayout(right_panel)
|
|
59
|
-
|
|
60
|
-
# Create splitter for query and results
|
|
61
|
-
splitter = QSplitter(Qt.Orientation.Vertical)
|
|
62
|
-
|
|
63
|
-
# Top part - Query section
|
|
64
|
-
query_widget = QWidget()
|
|
65
|
-
query_layout = QVBoxLayout(query_widget)
|
|
66
|
-
|
|
67
|
-
# Button row
|
|
68
|
-
button_layout = QHBoxLayout()
|
|
69
|
-
self.execute_btn = QPushButton('Execute (Ctrl+Enter)')
|
|
70
|
-
self.execute_btn.clicked.connect(self.execute_query)
|
|
71
|
-
self.clear_btn = QPushButton('Clear')
|
|
72
|
-
self.clear_btn.clicked.connect(self.clear_query)
|
|
73
|
-
|
|
74
|
-
button_layout.addWidget(self.execute_btn)
|
|
75
|
-
button_layout.addWidget(self.clear_btn)
|
|
76
|
-
button_layout.addStretch()
|
|
77
|
-
|
|
78
|
-
query_layout.addLayout(button_layout)
|
|
79
|
-
|
|
80
|
-
# Query input
|
|
81
|
-
self.query_edit = QTextEdit()
|
|
82
|
-
self.query_edit.setPlaceholderText("Enter your SQL query here...")
|
|
83
|
-
query_layout.addWidget(self.query_edit)
|
|
84
|
-
|
|
85
|
-
# Bottom part - Results section
|
|
86
|
-
results_widget = QWidget()
|
|
87
|
-
results_layout = QVBoxLayout(results_widget)
|
|
88
|
-
|
|
89
|
-
# Results header with row count
|
|
90
|
-
results_header = QWidget()
|
|
91
|
-
results_header_layout = QHBoxLayout(results_header)
|
|
92
|
-
self.results_label = QLabel("Results:")
|
|
93
|
-
self.row_count_label = QLabel("")
|
|
94
|
-
results_header_layout.addWidget(self.results_label)
|
|
95
|
-
results_header_layout.addWidget(self.row_count_label)
|
|
96
|
-
results_header_layout.addStretch()
|
|
97
|
-
results_layout.addWidget(results_header)
|
|
98
|
-
|
|
99
|
-
# Table widget for results
|
|
100
|
-
self.results_table = QTableWidget()
|
|
101
|
-
self.results_table.setSortingEnabled(True)
|
|
102
|
-
self.results_table.setAlternatingRowColors(True)
|
|
103
|
-
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
104
|
-
self.results_table.horizontalHeader().setSectionsMovable(True)
|
|
105
|
-
self.results_table.verticalHeader().setVisible(False)
|
|
106
|
-
results_layout.addWidget(self.results_table)
|
|
107
|
-
|
|
108
|
-
# Add widgets to splitter
|
|
109
|
-
splitter.addWidget(query_widget)
|
|
110
|
-
splitter.addWidget(results_widget)
|
|
111
|
-
|
|
112
|
-
# Set initial sizes for splitter
|
|
113
|
-
splitter.setSizes([300, 500])
|
|
114
|
-
|
|
115
|
-
right_layout.addWidget(splitter)
|
|
116
|
-
|
|
117
|
-
# Add panels to main layout
|
|
118
|
-
main_layout.addWidget(left_panel, 1)
|
|
119
|
-
main_layout.addWidget(right_panel, 4)
|
|
120
|
-
|
|
121
|
-
# Status bar
|
|
122
|
-
self.statusBar().showMessage('Ready')
|
|
123
|
-
|
|
124
|
-
def format_value(self, value):
|
|
125
|
-
"""Format values for display"""
|
|
126
|
-
if pd.isna(value):
|
|
127
|
-
return 'NULL'
|
|
128
|
-
elif isinstance(value, (int, np.integer)):
|
|
129
|
-
return f"{value:,}"
|
|
130
|
-
elif isinstance(value, (float, np.floating)):
|
|
131
|
-
return f"{value:,.2f}"
|
|
132
|
-
elif isinstance(value, (datetime, pd.Timestamp)):
|
|
133
|
-
return value.strftime('%Y-%m-%d %H:%M:%S')
|
|
134
|
-
return str(value)
|
|
135
|
-
|
|
136
|
-
def populate_table(self, df):
|
|
137
|
-
"""Populate the table widget with DataFrame content"""
|
|
138
|
-
if len(df) == 0:
|
|
139
|
-
self.results_table.setRowCount(0)
|
|
140
|
-
self.results_table.setColumnCount(0)
|
|
141
|
-
self.row_count_label.setText("No results")
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
# Set dimensions
|
|
145
|
-
self.results_table.setRowCount(len(df))
|
|
146
|
-
self.results_table.setColumnCount(len(df.columns))
|
|
147
|
-
|
|
148
|
-
# Set headers
|
|
149
|
-
self.results_table.setHorizontalHeaderLabels(df.columns)
|
|
150
|
-
|
|
151
|
-
# Populate data
|
|
152
|
-
for i, (_, row) in enumerate(df.iterrows()):
|
|
153
|
-
for j, value in enumerate(row):
|
|
154
|
-
formatted_value = self.format_value(value)
|
|
155
|
-
item = QTableWidgetItem(formatted_value)
|
|
156
|
-
|
|
157
|
-
# Set alignment based on data type
|
|
158
|
-
if isinstance(value, (int, float, np.integer, np.floating)):
|
|
159
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
160
|
-
else:
|
|
161
|
-
item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
162
|
-
|
|
163
|
-
# Make cells read-only
|
|
164
|
-
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
165
|
-
|
|
166
|
-
self.results_table.setItem(i, j, item)
|
|
167
|
-
|
|
168
|
-
# Auto-adjust column widths while ensuring minimum and maximum sizes
|
|
169
|
-
self.results_table.resizeColumnsToContents()
|
|
170
|
-
for i in range(len(df.columns)):
|
|
171
|
-
width = self.results_table.columnWidth(i)
|
|
172
|
-
self.results_table.setColumnWidth(i, min(max(width, 50), 300))
|
|
173
|
-
|
|
174
|
-
# Update row count
|
|
175
|
-
row_text = "row" if len(df) == 1 else "rows"
|
|
176
|
-
self.row_count_label.setText(f"{len(df):,} {row_text}")
|
|
177
|
-
|
|
178
|
-
def browse_files(self):
|
|
179
|
-
file_names, _ = QFileDialog.getOpenFileNames(
|
|
180
|
-
self,
|
|
181
|
-
"Open Data Files",
|
|
182
|
-
"",
|
|
183
|
-
"Data Files (*.xlsx *.xls *.csv *.parquet);;Excel Files (*.xlsx *.xls);;CSV Files (*.csv);;Parquet Files (*.parquet);;All Files (*)"
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
for file_name in file_names:
|
|
187
|
-
try:
|
|
188
|
-
if file_name.endswith(('.xlsx', '.xls')):
|
|
189
|
-
df = pd.read_excel(file_name)
|
|
190
|
-
elif file_name.endswith('.csv'):
|
|
191
|
-
df = pd.read_csv(file_name)
|
|
192
|
-
elif file_name.endswith('.parquet'):
|
|
193
|
-
df = pd.read_parquet(file_name)
|
|
194
|
-
else:
|
|
195
|
-
raise ValueError("Unsupported file format")
|
|
196
|
-
|
|
197
|
-
# Generate table name from file name
|
|
198
|
-
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
199
|
-
table_name = self.sanitize_table_name(base_name)
|
|
200
|
-
|
|
201
|
-
# Ensure unique table name
|
|
202
|
-
original_name = table_name
|
|
203
|
-
counter = 1
|
|
204
|
-
while table_name in self.loaded_tables:
|
|
205
|
-
table_name = f"{original_name}_{counter}"
|
|
206
|
-
counter += 1
|
|
207
|
-
|
|
208
|
-
# Register table in DuckDB
|
|
209
|
-
self.conn.register(table_name, df)
|
|
210
|
-
self.loaded_tables[table_name] = file_name
|
|
211
|
-
|
|
212
|
-
# Update UI
|
|
213
|
-
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
214
|
-
self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
|
|
215
|
-
|
|
216
|
-
# Show preview of loaded data
|
|
217
|
-
preview_df = df.head()
|
|
218
|
-
self.populate_table(preview_df)
|
|
219
|
-
self.results_label.setText(f"Preview of {table_name}:")
|
|
220
|
-
|
|
221
|
-
except Exception as e:
|
|
222
|
-
self.statusBar().showMessage(f'Error loading file: {str(e)}')
|
|
223
|
-
self.results_table.setRowCount(0)
|
|
224
|
-
self.results_table.setColumnCount(0)
|
|
225
|
-
self.row_count_label.setText("")
|
|
226
|
-
self.results_label.setText(f"Error loading file: {str(e)}")
|
|
227
|
-
|
|
228
|
-
def sanitize_table_name(self, name):
|
|
229
|
-
# Replace invalid characters with underscores
|
|
230
|
-
import re
|
|
231
|
-
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
232
|
-
# Ensure it starts with a letter
|
|
233
|
-
if not name[0].isalpha():
|
|
234
|
-
name = 'table_' + name
|
|
235
|
-
return name.lower()
|
|
236
|
-
|
|
237
|
-
def remove_selected_table(self):
|
|
238
|
-
current_item = self.tables_list.currentItem()
|
|
239
|
-
if current_item:
|
|
240
|
-
table_name = current_item.text().split(' (')[0]
|
|
241
|
-
if table_name in self.loaded_tables:
|
|
242
|
-
# Remove from DuckDB
|
|
243
|
-
self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
|
|
244
|
-
# Remove from our tracking
|
|
245
|
-
del self.loaded_tables[table_name]
|
|
246
|
-
# Remove from list widget
|
|
247
|
-
self.tables_list.takeItem(self.tables_list.row(current_item))
|
|
248
|
-
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
249
|
-
self.results_table.setRowCount(0)
|
|
250
|
-
self.results_table.setColumnCount(0)
|
|
251
|
-
self.row_count_label.setText("")
|
|
252
|
-
self.results_label.setText(f"Removed table: {table_name}")
|
|
253
|
-
|
|
254
|
-
def execute_query(self):
|
|
255
|
-
query = self.query_edit.toPlainText().strip()
|
|
256
|
-
if not query:
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
result = self.conn.execute(query).fetchdf()
|
|
261
|
-
self.populate_table(result)
|
|
262
|
-
self.results_label.setText("Query Results:")
|
|
263
|
-
self.statusBar().showMessage('Query executed successfully')
|
|
264
|
-
except Exception as e:
|
|
265
|
-
self.results_table.setRowCount(0)
|
|
266
|
-
self.results_table.setColumnCount(0)
|
|
267
|
-
self.row_count_label.setText("")
|
|
268
|
-
self.results_label.setText(f"Error executing query: {str(e)}")
|
|
269
|
-
self.statusBar().showMessage('Error executing query')
|
|
270
|
-
|
|
271
|
-
def clear_query(self):
|
|
272
|
-
self.query_edit.clear()
|
|
273
|
-
|
|
274
|
-
def show_table_preview(self, item):
|
|
275
|
-
"""Show a preview of the selected table"""
|
|
276
|
-
if item:
|
|
277
|
-
table_name = item.text().split(' (')[0]
|
|
278
|
-
try:
|
|
279
|
-
preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
|
|
280
|
-
self.populate_table(preview_df)
|
|
281
|
-
self.results_label.setText(f"Preview of {table_name}:")
|
|
282
|
-
self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
|
|
283
|
-
except Exception as e:
|
|
284
|
-
self.results_table.setRowCount(0)
|
|
285
|
-
self.results_table.setColumnCount(0)
|
|
286
|
-
self.row_count_label.setText("")
|
|
287
|
-
self.results_label.setText(f"Error showing preview: {str(e)}")
|
|
288
|
-
self.statusBar().showMessage('Error showing table preview')
|
|
289
|
-
|
|
290
|
-
def keyPressEvent(self, event):
|
|
291
|
-
if event.key() == Qt.Key.Key_Return and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
|
292
|
-
self.execute_query()
|
|
293
|
-
else:
|
|
294
|
-
super().keyPressEvent(event)
|
|
295
|
-
|
|
296
|
-
def load_test_data(self):
|
|
297
|
-
"""Generate and load test data"""
|
|
298
|
-
try:
|
|
299
|
-
# Create test data directory if it doesn't exist
|
|
300
|
-
os.makedirs('test_data', exist_ok=True)
|
|
301
|
-
|
|
302
|
-
# Generate test data
|
|
303
|
-
sales_df = create_test_data.create_sales_data()
|
|
304
|
-
customer_df = create_test_data.create_customer_data()
|
|
305
|
-
product_df = create_test_data.create_product_data()
|
|
306
|
-
|
|
307
|
-
# Save test data
|
|
308
|
-
sales_df.to_excel('test_data/sample_sales_data.xlsx', index=False)
|
|
309
|
-
customer_df.to_parquet('test_data/customer_data.parquet', index=False)
|
|
310
|
-
product_df.to_excel('test_data/product_catalog.xlsx', index=False)
|
|
311
|
-
|
|
312
|
-
# Load the files into DuckDB
|
|
313
|
-
self.conn.register('sample_sales_data', sales_df)
|
|
314
|
-
self.conn.register('product_catalog', product_df)
|
|
315
|
-
self.conn.register('customer_data', customer_df)
|
|
316
|
-
|
|
317
|
-
# Update loaded tables tracking
|
|
318
|
-
self.loaded_tables['sample_sales_data'] = 'test_data/sample_sales_data.xlsx'
|
|
319
|
-
self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
|
|
320
|
-
self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
|
|
321
|
-
|
|
322
|
-
# Update UI
|
|
323
|
-
self.tables_list.clear()
|
|
324
|
-
for table_name, file_path in self.loaded_tables.items():
|
|
325
|
-
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
326
|
-
|
|
327
|
-
# Set the sample query
|
|
328
|
-
self.query_edit.setText("select * from sample_sales_data cd inner join product_catalog pc on pc.productid = cd.productid limit 3")
|
|
329
|
-
|
|
330
|
-
self.statusBar().showMessage('Test data loaded successfully')
|
|
331
|
-
|
|
332
|
-
except Exception as e:
|
|
333
|
-
self.statusBar().showMessage(f'Error loading test data: {str(e)}')
|
|
334
|
-
|
|
335
|
-
def main():
|
|
336
|
-
app = QApplication(sys.argv)
|
|
337
|
-
|
|
338
|
-
# Set application style
|
|
339
|
-
app.setStyle('Fusion')
|
|
340
|
-
|
|
341
|
-
sql_shell = SQLShell()
|
|
342
|
-
sql_shell.show()
|
|
343
|
-
sys.exit(app.exec())
|
|
344
|
-
|
|
345
|
-
if __name__ == '__main__':
|
|
346
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|