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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sqlshell
3
- Version: 0.1.4
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 DuckDB databases and Excel files.
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
  ![SQLShell Interface](sqlshell_demo.png)
@@ -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 DuckDB databases and Excel files.
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
  ![SQLShell Interface](sqlshell_demo.png)
@@ -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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sqlshell"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A powerful SQL shell with GUI interface for data analysis"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -2,5 +2,5 @@
2
2
  SQLShell - A powerful SQL shell with GUI interface for data analysis
3
3
  """
4
4
 
5
- __version__ = "0.1.1"
5
+ __version__ = "0.1.5"
6
6
  __author__ = "SQLShell Team"
@@ -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 test data generation module
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.conn = duckdb.connect('pool.db')
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
- tables_label = QLabel("Loaded Tables:")
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
- # Register table in DuckDB
209
- self.conn.register(table_name, df)
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
- result = self.conn.execute(query).fetchdf()
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
- preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
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.4
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 DuckDB databases and Excel files.
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
  ![SQLShell Interface](sqlshell_demo.png)
@@ -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:
@@ -12,4 +12,4 @@ sqlshell.egg-info/requires.txt
12
12
  sqlshell.egg-info/top_level.txt
13
13
  sqlshell/sqlshell/__init__.py
14
14
  sqlshell/sqlshell/create_test_data.py
15
- sqlshell/sqlshell/main.py
15
+ sqlshell/sqlshell/create_test_databases.py
@@ -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