sqlshell 0.1.9__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of sqlshell might be problematic. Click here for more details.

sqlshell/main.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import sys
2
2
  import os
3
3
  import json
4
+ import argparse
5
+ from pathlib import Path
6
+ import tempfile
4
7
 
5
8
  # Ensure proper path setup for resources when running directly
6
9
  if __name__ == "__main__":
@@ -15,7 +18,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
15
18
  QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
16
19
  QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
17
20
  QCheckBox, QWidgetAction, QMenuBar, QInputDialog, QProgressDialog,
18
- QListWidgetItem)
21
+ QListWidgetItem, QDialog, QGraphicsDropShadowEffect, QTreeWidgetItem)
19
22
  from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint, QMimeData
20
23
  from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath, QDrag
21
24
  import numpy as np
@@ -28,111 +31,12 @@ from sqlshell.editor import LineNumberArea, SQLEditor
28
31
  from sqlshell.ui import FilterHeader, BarChartDelegate
29
32
  from sqlshell.db import DatabaseManager
30
33
  from sqlshell.query_tab import QueryTab
31
-
32
- class DraggableTablesList(QListWidget):
33
- """Custom QListWidget that provides better drag functionality for table names."""
34
-
35
- def __init__(self, parent=None):
36
- super().__init__(parent)
37
- self.parent = parent
38
- self.setDragEnabled(True)
39
- self.setDragDropMode(QListWidget.DragDropMode.DragOnly)
40
-
41
- # Apply custom styling
42
- self.setStyleSheet("""
43
- QListWidget {
44
- background-color: rgba(255, 255, 255, 0.1);
45
- border: none;
46
- border-radius: 4px;
47
- color: white;
48
- }
49
- QListWidget::item:selected {
50
- background-color: rgba(255, 255, 255, 0.2);
51
- }
52
- QListWidget::item:hover:!selected {
53
- background-color: rgba(255, 255, 255, 0.1);
54
- }
55
- """)
56
-
57
- def startDrag(self, supportedActions):
58
- """Override startDrag to customize the drag data."""
59
- item = self.currentItem()
60
- if not item:
61
- return
62
-
63
- # Extract the table name without the file info in parentheses
64
- table_name = item.text().split(' (')[0]
65
-
66
- # Create mime data with the table name
67
- mime_data = QMimeData()
68
- mime_data.setText(table_name)
69
-
70
- # Create drag object
71
- drag = QDrag(self)
72
- drag.setMimeData(mime_data)
73
-
74
- # Create a visually appealing drag pixmap
75
- font = self.font()
76
- font.setBold(True)
77
- metrics = self.fontMetrics()
78
- text_width = metrics.horizontalAdvance(table_name)
79
- text_height = metrics.height()
80
-
81
- # Make the pixmap large enough for the text plus padding and a small icon
82
- padding = 10
83
- pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
84
- pixmap.fill(Qt.GlobalColor.transparent)
85
-
86
- # Begin painting
87
- painter = QPainter(pixmap)
88
- painter.setRenderHint(QPainter.RenderHint.Antialiasing)
89
-
90
- # Draw a nice rounded rectangle background
91
- bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
92
- painter.setBrush(QBrush(bg_color))
93
- painter.setPen(Qt.PenStyle.NoPen)
94
- painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
95
-
96
- # Draw text
97
- painter.setPen(Qt.GlobalColor.white)
98
- painter.setFont(font)
99
- painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), table_name)
100
-
101
- # Draw a small database icon (simulated)
102
- icon_x = padding / 2
103
- icon_y = (pixmap.height() - 12) / 2
104
-
105
- # Draw a simple database icon as a blue circle with lines
106
- table_icon_color = QColor("#3498DB")
107
- painter.setBrush(QBrush(table_icon_color))
108
- painter.setPen(Qt.GlobalColor.white)
109
- painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
110
-
111
- # Draw "table" lines inside the circle
112
- painter.setPen(Qt.GlobalColor.white)
113
- painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
114
- painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
115
- painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
116
-
117
- painter.end()
118
-
119
- # Set the drag pixmap
120
- drag.setPixmap(pixmap)
121
-
122
- # Set hotspot to be at the top-left corner of the text
123
- drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
124
-
125
- # Execute drag operation
126
- result = drag.exec(supportedActions)
127
-
128
- # Optional: add a highlight effect after dragging
129
- if result == Qt.DropAction.CopyAction and item:
130
- # Briefly highlight the dragged item
131
- orig_bg = item.background()
132
- item.setBackground(QBrush(QColor(26, 188, 156, 100))) # Light green highlight
133
-
134
- # Reset after a short delay
135
- QTimer.singleShot(300, lambda: item.setBackground(orig_bg))
34
+ from sqlshell.styles import (get_application_stylesheet, get_tab_corner_stylesheet,
35
+ get_context_menu_stylesheet,
36
+ get_header_label_stylesheet, get_db_info_label_stylesheet,
37
+ get_tables_header_stylesheet, get_row_count_label_stylesheet)
38
+ from sqlshell.menus import setup_menubar
39
+ from sqlshell.table_list import DraggableTablesList
136
40
 
137
41
  class SQLShell(QMainWindow):
138
42
  def __init__(self):
@@ -145,9 +49,20 @@ class SQLShell(QMainWindow):
145
49
  self.max_recent_projects = 10 # Maximum number of recent projects to track
146
50
  self.tabs = [] # Store list of all tabs
147
51
 
52
+ # User preferences
53
+ self.auto_load_recent_project = True # Default to auto-loading most recent project
54
+
55
+ # File tracking for quick access
56
+ self.recent_files = [] # Store list of recently opened files
57
+ self.frequent_files = {} # Store file paths with usage counts
58
+ self.max_recent_files = 15 # Maximum number of recent files to track
59
+
148
60
  # Load recent projects from settings
149
61
  self.load_recent_projects()
150
62
 
63
+ # Load recent and frequent files from settings
64
+ self.load_recent_files()
65
+
151
66
  # Define color scheme
152
67
  self.colors = {
153
68
  'primary': "#2C3E50", # Dark blue-gray
@@ -169,199 +84,14 @@ class SQLShell(QMainWindow):
169
84
 
170
85
  # Create initial tab
171
86
  self.add_tab()
87
+
88
+ # Load most recent project if enabled and available
89
+ if self.auto_load_recent_project:
90
+ self.load_most_recent_project()
172
91
 
173
92
  def apply_stylesheet(self):
174
93
  """Apply custom stylesheet to the application"""
175
- self.setStyleSheet(f"""
176
- QMainWindow {{
177
- background-color: {self.colors['background']};
178
- }}
179
-
180
- QWidget {{
181
- color: {self.colors['text']};
182
- font-family: 'Segoe UI', 'Arial', sans-serif;
183
- }}
184
-
185
- QLabel {{
186
- font-size: 13px;
187
- padding: 2px;
188
- }}
189
-
190
- QLabel#header_label {{
191
- font-size: 16px;
192
- font-weight: bold;
193
- color: {self.colors['primary']};
194
- padding: 8px 0;
195
- }}
196
-
197
- QPushButton {{
198
- background-color: {self.colors['secondary']};
199
- color: white;
200
- border: none;
201
- border-radius: 4px;
202
- padding: 8px 16px;
203
- font-weight: bold;
204
- font-size: 13px;
205
- min-height: 30px;
206
- }}
207
-
208
- QPushButton:hover {{
209
- background-color: #2980B9;
210
- }}
211
-
212
- QPushButton:pressed {{
213
- background-color: #1F618D;
214
- }}
215
-
216
- QPushButton#primary_button {{
217
- background-color: {self.colors['accent']};
218
- }}
219
-
220
- QPushButton#primary_button:hover {{
221
- background-color: #16A085;
222
- }}
223
-
224
- QPushButton#primary_button:pressed {{
225
- background-color: #0E6655;
226
- }}
227
-
228
- QPushButton#danger_button {{
229
- background-color: {self.colors['error']};
230
- }}
231
-
232
- QPushButton#danger_button:hover {{
233
- background-color: #CB4335;
234
- }}
235
-
236
- QToolButton {{
237
- background-color: transparent;
238
- border: none;
239
- border-radius: 4px;
240
- padding: 4px;
241
- }}
242
-
243
- QToolButton:hover {{
244
- background-color: rgba(52, 152, 219, 0.2);
245
- }}
246
-
247
- QFrame#sidebar {{
248
- background-color: {self.colors['primary']};
249
- border-radius: 0px;
250
- }}
251
-
252
- QFrame#content_panel {{
253
- background-color: white;
254
- border-radius: 8px;
255
- border: 1px solid {self.colors['border']};
256
- }}
257
-
258
- QListWidget {{
259
- background-color: white;
260
- border-radius: 4px;
261
- border: 1px solid {self.colors['border']};
262
- padding: 4px;
263
- outline: none;
264
- }}
265
-
266
- QListWidget::item {{
267
- padding: 8px;
268
- border-radius: 4px;
269
- }}
270
-
271
- QListWidget::item:selected {{
272
- background-color: {self.colors['secondary']};
273
- color: white;
274
- }}
275
-
276
- QListWidget::item:hover:!selected {{
277
- background-color: #E3F2FD;
278
- }}
279
-
280
- QTableWidget {{
281
- background-color: white;
282
- alternate-background-color: #F8F9FA;
283
- border-radius: 4px;
284
- border: 1px solid {self.colors['border']};
285
- gridline-color: #E0E0E0;
286
- outline: none;
287
- }}
288
-
289
- QTableWidget::item {{
290
- padding: 4px;
291
- }}
292
-
293
- QTableWidget::item:selected {{
294
- background-color: rgba(52, 152, 219, 0.2);
295
- color: {self.colors['text']};
296
- }}
297
-
298
- QHeaderView::section {{
299
- background-color: {self.colors['primary']};
300
- color: white;
301
- padding: 8px;
302
- border: none;
303
- font-weight: bold;
304
- }}
305
-
306
- QSplitter::handle {{
307
- background-color: {self.colors['border']};
308
- }}
309
-
310
- QStatusBar {{
311
- background-color: {self.colors['primary']};
312
- color: white;
313
- padding: 8px;
314
- }}
315
-
316
- QTabWidget::pane {{
317
- border: 1px solid {self.colors['border']};
318
- border-radius: 4px;
319
- top: -1px;
320
- background-color: white;
321
- }}
322
-
323
- QTabBar::tab {{
324
- background-color: {self.colors['light_bg']};
325
- color: {self.colors['text']};
326
- border: 1px solid {self.colors['border']};
327
- border-bottom: none;
328
- border-top-left-radius: 4px;
329
- border-top-right-radius: 4px;
330
- padding: 8px 12px;
331
- margin-right: 2px;
332
- min-width: 80px;
333
- }}
334
-
335
- QTabBar::tab:selected {{
336
- background-color: white;
337
- border-bottom: 1px solid white;
338
- }}
339
-
340
- QTabBar::tab:hover:!selected {{
341
- background-color: #E3F2FD;
342
- }}
343
-
344
- QTabBar::close-button {{
345
- image: url(close.png);
346
- subcontrol-position: right;
347
- }}
348
-
349
- QTabBar::close-button:hover {{
350
- background-color: rgba(255, 0, 0, 0.2);
351
- border-radius: 2px;
352
- }}
353
-
354
- QPlainTextEdit, QTextEdit {{
355
- background-color: white;
356
- border-radius: 4px;
357
- border: 1px solid {self.colors['border']};
358
- padding: 8px;
359
- selection-background-color: #BBDEFB;
360
- selection-color: {self.colors['text']};
361
- font-family: 'Consolas', 'Courier New', monospace;
362
- font-size: 14px;
363
- }}
364
- """)
94
+ self.setStyleSheet(get_application_stylesheet(self.colors))
365
95
 
366
96
  def init_ui(self):
367
97
  self.setWindowTitle('SQL Shell')
@@ -399,78 +129,12 @@ class SQLShell(QMainWindow):
399
129
  if os.path.exists(main_logo_path):
400
130
  self.setWindowIcon(QIcon(main_logo_path))
401
131
 
402
- # Create menu bar
403
- menubar = self.menuBar()
404
- file_menu = menubar.addMenu('&File')
405
-
406
- # Project management actions
407
- new_project_action = file_menu.addAction('New Project')
408
- new_project_action.setShortcut('Ctrl+N')
409
- new_project_action.triggered.connect(self.new_project)
410
-
411
- open_project_action = file_menu.addAction('Open Project...')
412
- open_project_action.setShortcut('Ctrl+O')
413
- open_project_action.triggered.connect(self.open_project)
414
-
415
- # Add Recent Projects submenu
416
- self.recent_projects_menu = file_menu.addMenu('Recent Projects')
417
- self.update_recent_projects_menu()
418
-
419
- save_project_action = file_menu.addAction('Save Project')
420
- save_project_action.setShortcut('Ctrl+S')
421
- save_project_action.triggered.connect(self.save_project)
422
-
423
- save_project_as_action = file_menu.addAction('Save Project As...')
424
- save_project_as_action.setShortcut('Ctrl+Shift+S')
425
- save_project_as_action.triggered.connect(self.save_project_as)
426
-
427
- file_menu.addSeparator()
428
-
429
- exit_action = file_menu.addAction('Exit')
430
- exit_action.setShortcut('Ctrl+Q')
431
- exit_action.triggered.connect(self.close)
432
-
433
- # Add View menu for window management
434
- view_menu = menubar.addMenu('&View')
435
-
436
- # Maximized window option
437
- maximize_action = view_menu.addAction('Maximize Window')
438
- maximize_action.setShortcut('F11')
439
- maximize_action.triggered.connect(self.toggle_maximize_window)
440
-
441
- # Zoom submenu
442
- zoom_menu = view_menu.addMenu('Zoom')
443
-
444
- zoom_in_action = zoom_menu.addAction('Zoom In')
445
- zoom_in_action.setShortcut('Ctrl++')
446
- zoom_in_action.triggered.connect(lambda: self.change_zoom(1.1))
132
+ # Setup menus
133
+ setup_menubar(self)
447
134
 
448
- zoom_out_action = zoom_menu.addAction('Zoom Out')
449
- zoom_out_action.setShortcut('Ctrl+-')
450
- zoom_out_action.triggered.connect(lambda: self.change_zoom(0.9))
451
-
452
- reset_zoom_action = zoom_menu.addAction('Reset Zoom')
453
- reset_zoom_action.setShortcut('Ctrl+0')
454
- reset_zoom_action.triggered.connect(lambda: self.reset_zoom())
455
-
456
- # Add Tab menu
457
- tab_menu = menubar.addMenu('&Tab')
458
-
459
- new_tab_action = tab_menu.addAction('New Tab')
460
- new_tab_action.setShortcut('Ctrl+T')
461
- new_tab_action.triggered.connect(self.add_tab)
462
-
463
- duplicate_tab_action = tab_menu.addAction('Duplicate Current Tab')
464
- duplicate_tab_action.setShortcut('Ctrl+D')
465
- duplicate_tab_action.triggered.connect(self.duplicate_current_tab)
466
-
467
- rename_tab_action = tab_menu.addAction('Rename Current Tab')
468
- rename_tab_action.setShortcut('Ctrl+R')
469
- rename_tab_action.triggered.connect(self.rename_current_tab)
470
-
471
- close_tab_action = tab_menu.addAction('Close Current Tab')
472
- close_tab_action.setShortcut('Ctrl+W')
473
- close_tab_action.triggered.connect(self.close_current_tab)
135
+ # Update quick access menu
136
+ if hasattr(self, 'quick_access_menu'):
137
+ self.update_quick_access_menu()
474
138
 
475
139
  # Create custom status bar
476
140
  status_bar = QStatusBar()
@@ -495,53 +159,37 @@ class SQLShell(QMainWindow):
495
159
  # Database info section
496
160
  db_header = QLabel("DATABASE")
497
161
  db_header.setObjectName("header_label")
498
- db_header.setStyleSheet("color: white;")
162
+ db_header.setStyleSheet(get_header_label_stylesheet())
499
163
  left_layout.addWidget(db_header)
500
164
 
501
165
  self.db_info_label = QLabel("No database connected")
502
- self.db_info_label.setStyleSheet("color: white; background-color: rgba(255, 255, 255, 0.1); padding: 8px; border-radius: 4px;")
166
+ self.db_info_label.setStyleSheet(get_db_info_label_stylesheet())
503
167
  left_layout.addWidget(self.db_info_label)
504
168
 
505
169
  # Database action buttons
506
170
  db_buttons_layout = QHBoxLayout()
507
171
  db_buttons_layout.setSpacing(8)
508
172
 
509
- self.open_db_btn = QPushButton('Open Database')
510
- self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
511
- self.open_db_btn.clicked.connect(self.open_database)
173
+ self.load_btn = QPushButton('Load')
174
+ self.load_btn.setIcon(QIcon.fromTheme("document-open"))
175
+ self.load_btn.clicked.connect(self.show_load_dialog)
512
176
 
513
- self.test_btn = QPushButton('Load Test Data')
514
- self.test_btn.clicked.connect(self.load_test_data)
177
+ self.quick_access_btn = QPushButton('Quick Access')
178
+ self.quick_access_btn.setIcon(QIcon.fromTheme("document-open-recent"))
179
+ self.quick_access_btn.clicked.connect(self.show_quick_access_menu)
515
180
 
516
- db_buttons_layout.addWidget(self.open_db_btn)
517
- db_buttons_layout.addWidget(self.test_btn)
181
+ db_buttons_layout.addWidget(self.load_btn)
182
+ db_buttons_layout.addWidget(self.quick_access_btn)
518
183
  left_layout.addLayout(db_buttons_layout)
519
184
 
520
185
  # Tables section
521
186
  tables_header = QLabel("TABLES")
522
187
  tables_header.setObjectName("header_label")
523
- tables_header.setStyleSheet("color: white; margin-top: 16px;")
188
+ tables_header.setStyleSheet(get_tables_header_stylesheet())
524
189
  left_layout.addWidget(tables_header)
525
190
 
526
- # Table actions
527
- table_actions_layout = QHBoxLayout()
528
- table_actions_layout.setSpacing(8)
529
-
530
- self.browse_btn = QPushButton('Load Files')
531
- self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
532
- self.browse_btn.clicked.connect(self.browse_files)
533
-
534
- self.remove_table_btn = QPushButton('Remove')
535
- self.remove_table_btn.setObjectName("danger_button")
536
- self.remove_table_btn.setIcon(QIcon.fromTheme("edit-delete"))
537
- self.remove_table_btn.clicked.connect(self.remove_selected_table)
538
-
539
- table_actions_layout.addWidget(self.browse_btn)
540
- table_actions_layout.addWidget(self.remove_table_btn)
541
- left_layout.addLayout(table_actions_layout)
542
-
543
191
  # Tables list with custom styling
544
- self.tables_list = DraggableTablesList()
192
+ self.tables_list = DraggableTablesList(self)
545
193
  self.tables_list.itemClicked.connect(self.show_table_preview)
546
194
  self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
547
195
  self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
@@ -581,7 +229,7 @@ class SQLShell(QMainWindow):
581
229
  main_layout.addWidget(right_panel, 4)
582
230
 
583
231
  # Status bar
584
- self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab')
232
+ self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab | Ctrl+Shift+O: Quick Access Files')
585
233
 
586
234
  def create_tab_corner_widget(self):
587
235
  """Create a corner widget with a + button to add new tabs"""
@@ -593,23 +241,7 @@ class SQLShell(QMainWindow):
593
241
  add_tab_btn = QToolButton()
594
242
  add_tab_btn.setText("+")
595
243
  add_tab_btn.setToolTip("Add new tab (Ctrl+T)")
596
- add_tab_btn.setStyleSheet("""
597
- QToolButton {
598
- background-color: transparent;
599
- border: none;
600
- border-radius: 4px;
601
- padding: 4px;
602
- font-weight: bold;
603
- font-size: 16px;
604
- color: #3498DB;
605
- }
606
- QToolButton:hover {
607
- background-color: rgba(52, 152, 219, 0.2);
608
- }
609
- QToolButton:pressed {
610
- background-color: rgba(52, 152, 219, 0.4);
611
- }
612
- """)
244
+ add_tab_btn.setStyleSheet(get_tab_corner_stylesheet())
613
245
  add_tab_btn.clicked.connect(self.add_tab)
614
246
 
615
247
  layout.addWidget(add_tab_btn)
@@ -743,11 +375,16 @@ class SQLShell(QMainWindow):
743
375
  elif isinstance(value, (float, np.floating)):
744
376
  if value.is_integer():
745
377
  return str(int(value))
746
- return f"{value:.6g}" # Use general format with up to 6 significant digits
378
+ # Display full number without scientific notation by using 'f' format
379
+ # Format large numbers with commas for better readability
380
+ if abs(value) >= 1000000:
381
+ return f"{value:,.2f}" # Format with commas and 2 decimal places
382
+ return f"{value:.6f}" # Use fixed-point notation with 6 decimal places
747
383
  elif isinstance(value, (pd.Timestamp, datetime)):
748
384
  return value.strftime("%Y-%m-%d %H:%M:%S")
749
385
  elif isinstance(value, (np.integer, int)):
750
- return str(value)
386
+ # Format large integers with commas for better readability
387
+ return f"{value:,}"
751
388
  elif isinstance(value, bool):
752
389
  return str(value)
753
390
  elif isinstance(value, (bytes, bytearray)):
@@ -769,11 +406,14 @@ class SQLShell(QMainWindow):
769
406
 
770
407
  for file_name in file_names:
771
408
  try:
409
+ # Add to recent files
410
+ self.add_recent_file(file_name)
411
+
772
412
  # Use the database manager to load the file
773
413
  table_name, df = self.db_manager.load_file(file_name)
774
414
 
775
- # Update UI
776
- self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
415
+ # Update UI using new method
416
+ self.tables_list.add_table_item(table_name, os.path.basename(file_name))
777
417
  self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
778
418
 
779
419
  # Show preview of loaded data
@@ -798,18 +438,34 @@ class SQLShell(QMainWindow):
798
438
 
799
439
  def remove_selected_table(self):
800
440
  current_item = self.tables_list.currentItem()
801
- if current_item:
802
- table_name = current_item.text().split(' (')[0]
803
- if self.db_manager.remove_table(table_name):
804
- # Remove from list widget
805
- self.tables_list.takeItem(self.tables_list.row(current_item))
806
- self.statusBar().showMessage(f'Removed table "{table_name}"')
807
- self.results_table.setRowCount(0)
808
- self.results_table.setColumnCount(0)
809
- self.row_count_label.setText("")
810
-
811
- # Update completer
812
- self.update_completer()
441
+ if not current_item or self.tables_list.is_folder_item(current_item):
442
+ return
443
+
444
+ table_name = self.tables_list.get_table_name_from_item(current_item)
445
+ if not table_name:
446
+ return
447
+
448
+ if self.db_manager.remove_table(table_name):
449
+ # Remove from tree widget
450
+ parent = current_item.parent()
451
+ if parent:
452
+ parent.removeChild(current_item)
453
+ else:
454
+ index = self.tables_list.indexOfTopLevelItem(current_item)
455
+ if index >= 0:
456
+ self.tables_list.takeTopLevelItem(index)
457
+
458
+ self.statusBar().showMessage(f'Removed table "{table_name}"')
459
+
460
+ # Get the current tab and clear its results table
461
+ current_tab = self.get_current_tab()
462
+ if current_tab:
463
+ current_tab.results_table.setRowCount(0)
464
+ current_tab.results_table.setColumnCount(0)
465
+ current_tab.row_count_label.setText("")
466
+
467
+ # Update completer
468
+ self.update_completer()
813
469
 
814
470
  def open_database(self):
815
471
  """Open a database connection with proper error handling and resource management"""
@@ -823,19 +479,22 @@ class SQLShell(QMainWindow):
823
479
 
824
480
  if filename:
825
481
  try:
482
+ # Add to recent files
483
+ self.add_recent_file(filename)
484
+
826
485
  # Clear existing database tables from the list widget
827
- for i in range(self.tables_list.count() - 1, -1, -1):
828
- item = self.tables_list.item(i)
829
- if item and item.text().endswith('(database)'):
830
- self.tables_list.takeItem(i)
486
+ for i in range(self.tables_list.topLevelItemCount() - 1, -1, -1):
487
+ item = self.tables_list.topLevelItem(i)
488
+ if item and item.text(0).endswith('(database)'):
489
+ self.tables_list.takeTopLevelItem(i)
831
490
 
832
491
  # Use the database manager to open the database
833
- self.db_manager.open_database(filename)
492
+ self.db_manager.open_database(filename, load_all_tables=True)
834
493
 
835
494
  # Update UI with tables from the database
836
495
  for table_name, source in self.db_manager.loaded_tables.items():
837
496
  if source == 'database':
838
- self.tables_list.addItem(f"{table_name} (database)")
497
+ self.tables_list.add_table_item(table_name, "database")
839
498
 
840
499
  # Update the completer with table and column names
841
500
  self.update_completer()
@@ -861,6 +520,12 @@ class SQLShell(QMainWindow):
861
520
  if self.tab_widget.count() == 0:
862
521
  return
863
522
 
523
+ # Import the suggestion manager
524
+ from sqlshell.suggester_integration import get_suggestion_manager
525
+
526
+ # Get the suggestion manager singleton
527
+ suggestion_mgr = get_suggestion_manager()
528
+
864
529
  # Start a background update with a timer
865
530
  self.statusBar().showMessage("Updating auto-completion...", 2000)
866
531
 
@@ -869,7 +534,34 @@ class SQLShell(QMainWindow):
869
534
  self.query_history = []
870
535
  self.completion_usage = {} # Track usage frequency
871
536
 
872
- # Get completion words from the database manager
537
+ # Get schema information from the database manager
538
+ try:
539
+ # Get table and column information
540
+ tables = set(self.db_manager.loaded_tables.keys())
541
+ table_columns = self.db_manager.table_columns
542
+
543
+ # Get column data types if available
544
+ column_types = {}
545
+ for table, columns in self.db_manager.table_columns.items():
546
+ for col in columns:
547
+ qualified_name = f"{table}.{col}"
548
+ # Try to infer type from sample data
549
+ if hasattr(self.db_manager, 'sample_data') and table in self.db_manager.sample_data:
550
+ sample = self.db_manager.sample_data[table]
551
+ if col in sample.columns:
552
+ # Get data type from pandas
553
+ col_dtype = str(sample[col].dtype)
554
+ column_types[qualified_name] = col_dtype
555
+ # Also store unqualified name
556
+ column_types[col] = col_dtype
557
+
558
+ # Update the suggestion manager with schema information
559
+ suggestion_mgr.update_schema(tables, table_columns, column_types)
560
+
561
+ except Exception as e:
562
+ self.statusBar().showMessage(f"Error getting completions: {str(e)}", 2000)
563
+
564
+ # Get all completion words from basic system (for backward compatibility)
873
565
  try:
874
566
  completion_words = self.db_manager.get_all_table_columns()
875
567
  except Exception as e:
@@ -886,141 +578,45 @@ class SQLShell(QMainWindow):
886
578
  )[:100]
887
579
 
888
580
  # Add these to our completion words
889
- for term, _ in frequent_terms:
581
+ for term, count in frequent_terms:
582
+ suggestion_mgr.suggester.usage_counts[term] = count
890
583
  if term not in completion_words:
891
584
  completion_words.append(term)
892
585
 
893
- # Limit to a reasonable number of items to prevent performance issues
894
- MAX_COMPLETION_ITEMS = 2000 # Increased from 1000 to accommodate more smart suggestions
895
- if len(completion_words) > MAX_COMPLETION_ITEMS:
896
- # Create a more advanced prioritization strategy
897
- prioritized_words = []
898
-
899
- # First, include all table names
900
- tables = list(self.db_manager.loaded_tables.keys())
901
- prioritized_words.extend(tables)
902
-
903
- # Then add most common SQL keywords and patterns
904
- sql_keywords = [w for w in completion_words if w.isupper() and len(w) > 1]
905
- prioritized_words.extend(sql_keywords[:200]) # Cap at 200 keywords
906
-
907
- # Add frequently used items
908
- if hasattr(self, 'completion_usage'):
909
- frequent_items = [
910
- item for item, _ in sorted(
911
- self.completion_usage.items(),
912
- key=lambda x: x[1],
913
- reverse=True
914
- )[:100] # Top 100 most used
915
- ]
916
- prioritized_words.extend(frequent_items)
917
-
918
- # Add table.column patterns which are very useful
919
- qualified_columns = [w for w in completion_words if '.' in w and w.split('.')[0] in tables]
920
- prioritized_words.extend(qualified_columns[:300]) # Cap at 300 qualified columns
921
-
922
- # Add common completion patterns
923
- patterns = [w for w in completion_words if ' ' in w] # Spaces indicate phrases/patterns
924
- prioritized_words.extend(patterns[:200]) # Cap at 200 patterns
925
-
926
- # Finally add other columns
927
- remaining_slots = MAX_COMPLETION_ITEMS - len(prioritized_words)
928
- remaining_words = [
929
- w for w in completion_words
930
- if w not in prioritized_words
931
- and not w.isupper()
932
- and '.' not in w
933
- and ' ' not in w
934
- ]
935
- prioritized_words.extend(remaining_words[:remaining_slots])
936
-
937
- # Remove duplicates while preserving order
938
- seen = set()
939
- completion_words = []
940
- for item in prioritized_words:
941
- if item not in seen:
942
- seen.add(item)
943
- completion_words.append(item)
944
-
945
- # Ensure we don't exceed the maximum
946
- completion_words = completion_words[:MAX_COMPLETION_ITEMS]
947
-
948
- # Use a single model for all tabs to save memory and improve performance
586
+ # Create a single shared model for all tabs to save memory
949
587
  model = QStringListModel(completion_words)
950
588
 
951
589
  # Keep a reference to the model to prevent garbage collection
952
590
  self._current_completer_model = model
953
591
 
954
- # Only update the current tab immediately
955
- current_index = self.tab_widget.currentIndex()
956
- if current_index >= 0:
957
- current_tab = self.tab_widget.widget(current_index)
958
- if current_tab and hasattr(current_tab, 'query_edit'):
592
+ # First unregister all existing editors to avoid duplicates
593
+ existing_editors = suggestion_mgr._editors.copy()
594
+ for editor_id in existing_editors:
595
+ suggestion_mgr.unregister_editor(editor_id)
596
+
597
+ # Register editors with the suggestion manager and update their completer models
598
+ for i in range(self.tab_widget.count()):
599
+ tab = self.tab_widget.widget(i)
600
+ if tab and hasattr(tab, 'query_edit'):
601
+ # Register this editor with the suggestion manager using a unique ID
602
+ editor_id = f"tab_{i}_{id(tab.query_edit)}"
603
+ suggestion_mgr.register_editor(tab.query_edit, editor_id)
604
+
605
+ # Update the basic completer model for backward compatibility
959
606
  try:
960
- current_tab.query_edit.update_completer_model(model)
607
+ tab.query_edit.update_completer_model(model)
961
608
  except Exception as e:
962
- self.statusBar().showMessage(f"Error updating current tab completer: {str(e)}", 2000)
963
-
964
- # Only schedule updates for additional tabs if we have more than 3 tabs
965
- # This reduces overhead for common usage patterns
966
- if self.tab_widget.count() > 1:
967
- # Calculate a reasonable maximum delay (ms)
968
- max_delay = min(500, 50 * self.tab_widget.count())
969
-
970
- # Store timers to prevent garbage collection
971
- if not hasattr(self, '_completer_timers'):
972
- self._completer_timers = []
973
-
974
- # Clear old timers
975
- for timer in self._completer_timers:
976
- if timer.isActive():
977
- timer.stop()
978
- self._completer_timers = []
979
-
980
- # Schedule updates for other tabs with increasing delays
981
- for i in range(self.tab_widget.count()):
982
- if i != current_index:
983
- tab = self.tab_widget.widget(i)
984
- if tab and not tab.isHidden() and hasattr(tab, 'query_edit'):
985
- delay = int((i + 1) / self.tab_widget.count() * max_delay)
986
-
987
- timer = QTimer()
988
- timer.setSingleShot(True)
989
- # Store tab and model as local variables for the lambda
990
- # to avoid closure issues
991
- tab_ref = tab
992
- model_ref = model
993
- timer.timeout.connect(
994
- lambda t=tab_ref, m=model_ref: self._update_tab_completer(t, m))
995
- self._completer_timers.append(timer)
996
- timer.start(delay)
609
+ self.statusBar().showMessage(f"Error updating completer for tab {i}: {str(e)}", 2000)
997
610
 
998
611
  # Process events to keep UI responsive
999
612
  QApplication.processEvents()
1000
613
 
1001
- # Return True to indicate success
1002
614
  return True
1003
615
 
1004
616
  except Exception as e:
1005
617
  # Catch any errors to prevent hanging
1006
618
  self.statusBar().showMessage(f"Auto-completion update error: {str(e)}", 2000)
1007
619
  return False
1008
-
1009
- def _update_tab_completer(self, tab, model):
1010
- """Helper method to update a tab's completer with the given model"""
1011
- if tab and not tab.isHidden() and hasattr(tab, 'query_edit'): # Only update visible tabs with query editors
1012
- try:
1013
- tab.query_edit.update_completer_model(model)
1014
- QApplication.processEvents() # Keep UI responsive
1015
- except Exception as e:
1016
- print(f"Error updating tab completer: {e}")
1017
- # Try a simpler approach as fallback
1018
- try:
1019
- if hasattr(tab.query_edit, 'all_sql_keywords'):
1020
- fallback_model = QStringListModel(tab.query_edit.all_sql_keywords)
1021
- tab.query_edit.completer.setModel(fallback_model)
1022
- except Exception:
1023
- pass # Last resort: ignore errors to prevent crashes
1024
620
 
1025
621
  def execute_query(self):
1026
622
  try:
@@ -1044,7 +640,16 @@ class SQLShell(QMainWindow):
1044
640
  self.populate_table(result)
1045
641
  self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
1046
642
 
1047
- # Record query in history and update completion usage
643
+ # Record query for context-aware suggestions
644
+ try:
645
+ from sqlshell.suggester_integration import get_suggestion_manager
646
+ suggestion_mgr = get_suggestion_manager()
647
+ suggestion_mgr.record_query(query)
648
+ except Exception as e:
649
+ # Don't let suggestion errors affect query execution
650
+ print(f"Error recording query for suggestions: {e}")
651
+
652
+ # Record query in history and update completion usage (legacy)
1048
653
  self._update_query_history(query)
1049
654
 
1050
655
  except SyntaxError as e:
@@ -1145,7 +750,7 @@ class SQLShell(QMainWindow):
1145
750
 
1146
751
  def show_table_preview(self, item):
1147
752
  """Show a preview of the selected table"""
1148
- if not item:
753
+ if not item or self.tables_list.is_folder_item(item):
1149
754
  return
1150
755
 
1151
756
  # Get the current tab
@@ -1153,7 +758,15 @@ class SQLShell(QMainWindow):
1153
758
  if not current_tab:
1154
759
  return
1155
760
 
1156
- table_name = item.text().split(' (')[0]
761
+ table_name = self.tables_list.get_table_name_from_item(item)
762
+ if not table_name:
763
+ return
764
+
765
+ # Check if this table needs to be reloaded first
766
+ if table_name in self.tables_list.tables_needing_reload:
767
+ # Reload the table immediately without asking
768
+ self.reload_selected_table(table_name)
769
+
1157
770
  try:
1158
771
  # Use the database manager to get a preview of the table
1159
772
  preview_df = self.db_manager.get_table_preview(table_name)
@@ -1189,40 +802,59 @@ class SQLShell(QMainWindow):
1189
802
  # Show loading indicator
1190
803
  self.statusBar().showMessage('Generating test data...')
1191
804
 
1192
- # Create test data directory if it doesn't exist
1193
- os.makedirs('test_data', exist_ok=True)
805
+ # Create temporary directory for test data
806
+ temp_dir = tempfile.mkdtemp(prefix='sqlshell_test_')
1194
807
 
1195
808
  # Generate test data
1196
809
  sales_df = create_test_data.create_sales_data()
1197
810
  customer_df = create_test_data.create_customer_data()
1198
811
  product_df = create_test_data.create_product_data()
812
+ large_numbers_df = create_test_data.create_large_numbers_data()
1199
813
 
1200
- # Save test data
1201
- sales_df.to_excel('test_data/sample_sales_data.xlsx', index=False)
1202
- customer_df.to_parquet('test_data/customer_data.parquet', index=False)
1203
- product_df.to_excel('test_data/product_catalog.xlsx', index=False)
814
+ # Save test data to temporary directory
815
+ sales_path = os.path.join(temp_dir, 'sample_sales_data.xlsx')
816
+ customer_path = os.path.join(temp_dir, 'customer_data.parquet')
817
+ product_path = os.path.join(temp_dir, 'product_catalog.xlsx')
818
+ large_numbers_path = os.path.join(temp_dir, 'large_numbers.xlsx')
819
+
820
+ sales_df.to_excel(sales_path, index=False)
821
+ customer_df.to_parquet(customer_path, index=False)
822
+ product_df.to_excel(product_path, index=False)
823
+ large_numbers_df.to_excel(large_numbers_path, index=False)
1204
824
 
1205
825
  # Register the tables in the database manager
1206
- self.db_manager.register_dataframe(sales_df, 'sample_sales_data', 'test_data/sample_sales_data.xlsx')
1207
- self.db_manager.register_dataframe(product_df, 'product_catalog', 'test_data/product_catalog.xlsx')
1208
- self.db_manager.register_dataframe(customer_df, 'customer_data', 'test_data/customer_data.parquet')
826
+ self.db_manager.register_dataframe(sales_df, 'sample_sales_data', sales_path)
827
+ self.db_manager.register_dataframe(product_df, 'product_catalog', product_path)
828
+ self.db_manager.register_dataframe(customer_df, 'customer_data', customer_path)
829
+ self.db_manager.register_dataframe(large_numbers_df, 'large_numbers', large_numbers_path)
1209
830
 
1210
831
  # Update UI
1211
832
  self.tables_list.clear()
1212
833
  for table_name, file_path in self.db_manager.loaded_tables.items():
1213
- self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
834
+ # Use the new add_table_item method
835
+ self.tables_list.add_table_item(table_name, os.path.basename(file_path))
1214
836
 
1215
837
  # Set the sample query in the current tab
1216
838
  current_tab = self.get_current_tab()
1217
839
  if current_tab:
1218
840
  sample_query = """
841
+ -- Example query with tables containing large numbers
1219
842
  SELECT
1220
- DISTINCT
1221
- c.customername
843
+ ln.ID,
844
+ ln.Category,
845
+ ln.MediumValue,
846
+ ln.LargeValue,
847
+ ln.VeryLargeValue,
848
+ ln.MassiveValue,
849
+ ln.ExponentialValue,
850
+ ln.Revenue,
851
+ ln.Budget
1222
852
  FROM
1223
- sample_sales_data s
1224
- INNER JOIN customer_data c ON c.customerid = s.customerid
1225
- INNER JOIN product_catalog p ON p.productid = s.productid
853
+ large_numbers ln
854
+ WHERE
855
+ ln.LargeValue > 5000000000000
856
+ ORDER BY
857
+ ln.MassiveValue DESC
1226
858
  LIMIT 10
1227
859
  """
1228
860
  current_tab.set_query_text(sample_query.strip())
@@ -1233,8 +865,10 @@ LIMIT 10
1233
865
  # Show success message
1234
866
  self.statusBar().showMessage('Test data loaded successfully')
1235
867
 
1236
- # Show a preview of the sales data
1237
- self.show_table_preview(self.tables_list.item(0))
868
+ # Show a preview of the large numbers data
869
+ large_numbers_item = self.tables_list.find_table_item("large_numbers")
870
+ if large_numbers_item:
871
+ self.show_table_preview(large_numbers_item)
1238
872
 
1239
873
  except Exception as e:
1240
874
  self.statusBar().showMessage(f'Error loading test data: {str(e)}')
@@ -1280,8 +914,8 @@ LIMIT 10
1280
914
  self.db_manager.loaded_tables[table_name] = file_name
1281
915
  self.db_manager.table_columns[table_name] = df.columns.tolist()
1282
916
 
1283
- # Update UI
1284
- self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
917
+ # Update UI using new method
918
+ self.tables_list.add_table_item(table_name, os.path.basename(file_name))
1285
919
  self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
1286
920
 
1287
921
  # Update completer with new table and column names
@@ -1338,8 +972,8 @@ LIMIT 10
1338
972
  self.db_manager.loaded_tables[table_name] = file_name
1339
973
  self.db_manager.table_columns[table_name] = df.columns.tolist()
1340
974
 
1341
- # Update UI
1342
- self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
975
+ # Update UI using new method
976
+ self.tables_list.add_table_item(table_name, os.path.basename(file_name))
1343
977
  self.statusBar().showMessage(f'Data exported to {file_name} and loaded as table "{table_name}"')
1344
978
 
1345
979
  # Update completer with new table and column names
@@ -1357,7 +991,7 @@ LIMIT 10
1357
991
  self.statusBar().showMessage('Error exporting data')
1358
992
 
1359
993
  def get_table_data_as_dataframe(self):
1360
- """Helper function to convert table widget data to a DataFrame"""
994
+ """Helper function to convert table widget data to a DataFrame with proper data types"""
1361
995
  # Get the current tab
1362
996
  current_tab = self.get_current_tab()
1363
997
  if not current_tab:
@@ -1371,7 +1005,81 @@ LIMIT 10
1371
1005
  item = current_tab.results_table.item(row, column)
1372
1006
  row_data.append(item.text() if item else '')
1373
1007
  data.append(row_data)
1374
- return pd.DataFrame(data, columns=headers)
1008
+
1009
+ # Create DataFrame from raw string data
1010
+ df_raw = pd.DataFrame(data, columns=headers)
1011
+
1012
+ # Try to use the original dataframe's dtypes if available
1013
+ if hasattr(current_tab, 'current_df') and current_tab.current_df is not None:
1014
+ original_df = current_tab.current_df
1015
+ # Since we might have filtered rows, we can't just return the original DataFrame
1016
+ # But we can use its column types to convert our string data appropriately
1017
+
1018
+ # Create a new DataFrame with appropriate types
1019
+ df_typed = pd.DataFrame()
1020
+
1021
+ for col in df_raw.columns:
1022
+ if col in original_df.columns:
1023
+ # Get the original column type
1024
+ orig_type = original_df[col].dtype
1025
+
1026
+ # Special handling for different data types
1027
+ if pd.api.types.is_numeric_dtype(orig_type):
1028
+ # Handle numeric columns (int or float)
1029
+ try:
1030
+ # First try to convert to numeric type
1031
+ # Remove commas used for thousands separators
1032
+ numeric_col = pd.to_numeric(df_raw[col].str.replace(',', '').replace('NULL', np.nan))
1033
+ df_typed[col] = numeric_col
1034
+ except:
1035
+ # If that fails, keep the original string
1036
+ df_typed[col] = df_raw[col]
1037
+ elif pd.api.types.is_datetime64_dtype(orig_type):
1038
+ # Handle datetime columns
1039
+ try:
1040
+ df_typed[col] = pd.to_datetime(df_raw[col].replace('NULL', np.nan))
1041
+ except:
1042
+ df_typed[col] = df_raw[col]
1043
+ elif pd.api.types.is_bool_dtype(orig_type):
1044
+ # Handle boolean columns
1045
+ try:
1046
+ df_typed[col] = df_raw[col].map({'True': True, 'False': False}).replace('NULL', np.nan)
1047
+ except:
1048
+ df_typed[col] = df_raw[col]
1049
+ else:
1050
+ # For other types, keep as is
1051
+ df_typed[col] = df_raw[col]
1052
+ else:
1053
+ # For columns not in the original dataframe, infer type
1054
+ df_typed[col] = df_raw[col]
1055
+
1056
+ return df_typed
1057
+
1058
+ else:
1059
+ # If we don't have the original dataframe, try to infer types
1060
+ # First replace 'NULL' with actual NaN
1061
+ df_raw.replace('NULL', np.nan, inplace=True)
1062
+
1063
+ # Try to convert each column to numeric if possible
1064
+ for col in df_raw.columns:
1065
+ try:
1066
+ # First try to convert to numeric by removing commas
1067
+ df_raw[col] = pd.to_numeric(df_raw[col].str.replace(',', ''))
1068
+ except:
1069
+ # If that fails, try to convert to datetime
1070
+ try:
1071
+ df_raw[col] = pd.to_datetime(df_raw[col])
1072
+ except:
1073
+ # If both numeric and datetime conversions fail,
1074
+ # try boolean conversion for True/False strings
1075
+ try:
1076
+ if df_raw[col].dropna().isin(['True', 'False']).all():
1077
+ df_raw[col] = df_raw[col].map({'True': True, 'False': False})
1078
+ except:
1079
+ # Otherwise, keep as is
1080
+ pass
1081
+
1082
+ return df_raw
1375
1083
 
1376
1084
  def keyPressEvent(self, event):
1377
1085
  """Handle global keyboard shortcuts"""
@@ -1400,6 +1108,13 @@ LIMIT 10
1400
1108
  self.rename_current_tab()
1401
1109
  return
1402
1110
 
1111
+ # Show quick access menu with Ctrl+Shift+O
1112
+ if (event.key() == Qt.Key.Key_O and
1113
+ (event.modifiers() & Qt.KeyboardModifier.ControlModifier) and
1114
+ (event.modifiers() & Qt.KeyboardModifier.ShiftModifier)):
1115
+ self.show_quick_access_menu()
1116
+ return
1117
+
1403
1118
  super().keyPressEvent(event)
1404
1119
 
1405
1120
  def closeEvent(self, event):
@@ -1489,7 +1204,9 @@ LIMIT 10
1489
1204
  def show_tables_context_menu(self, position):
1490
1205
  """Show context menu for tables list"""
1491
1206
  item = self.tables_list.itemAt(position)
1492
- if not item:
1207
+
1208
+ # If no item or it's a folder, let the tree widget handle it
1209
+ if not item or self.tables_list.is_folder_item(item):
1493
1210
  return
1494
1211
 
1495
1212
  # Get current tab
@@ -1498,28 +1215,54 @@ LIMIT 10
1498
1215
  return
1499
1216
 
1500
1217
  # Get table name without the file info in parentheses
1501
- table_name = item.text().split(' (')[0]
1218
+ table_name = self.tables_list.get_table_name_from_item(item)
1219
+ if not table_name:
1220
+ return
1502
1221
 
1503
1222
  # Create context menu
1504
1223
  context_menu = QMenu(self)
1505
- context_menu.setStyleSheet("""
1506
- QMenu {
1507
- background-color: white;
1508
- border: 1px solid #BDC3C7;
1509
- padding: 5px;
1510
- }
1511
- QMenu::item {
1512
- padding: 5px 20px;
1513
- }
1514
- QMenu::item:selected {
1515
- background-color: #3498DB;
1516
- color: white;
1517
- }
1518
- """)
1224
+ context_menu.setStyleSheet(get_context_menu_stylesheet())
1519
1225
 
1520
1226
  # Add menu actions
1521
1227
  select_from_action = context_menu.addAction("Select from")
1522
1228
  add_to_editor_action = context_menu.addAction("Just add to editor")
1229
+
1230
+ # Add entropy profiler action
1231
+ context_menu.addSeparator()
1232
+ analyze_entropy_action = context_menu.addAction("Analyze Column Importance")
1233
+ analyze_entropy_action.setIcon(QIcon.fromTheme("system-search"))
1234
+
1235
+ # Add table profiler action
1236
+ profile_table_action = context_menu.addAction("Profile Table Structure")
1237
+ profile_table_action.setIcon(QIcon.fromTheme("edit-find"))
1238
+
1239
+ # Check if table needs reloading and add appropriate action
1240
+ if table_name in self.tables_list.tables_needing_reload:
1241
+ reload_action = context_menu.addAction("Reload Table")
1242
+ reload_action.setIcon(QIcon.fromTheme("view-refresh"))
1243
+ else:
1244
+ reload_action = context_menu.addAction("Refresh")
1245
+ reload_action.setIcon(QIcon.fromTheme("view-refresh"))
1246
+
1247
+ # Add move to folder submenu
1248
+ move_menu = context_menu.addMenu("Move to Folder")
1249
+ move_menu.setIcon(QIcon.fromTheme("folder"))
1250
+
1251
+ # Add "New Folder" option to move menu
1252
+ new_folder_action = move_menu.addAction("New Folder...")
1253
+ move_menu.addSeparator()
1254
+
1255
+ # Add folders to the move menu
1256
+ for i in range(self.tables_list.topLevelItemCount()):
1257
+ top_item = self.tables_list.topLevelItem(i)
1258
+ if self.tables_list.is_folder_item(top_item):
1259
+ folder_action = move_menu.addAction(top_item.text(0))
1260
+ folder_action.setData(top_item)
1261
+
1262
+ # Add root option
1263
+ move_menu.addSeparator()
1264
+ root_action = move_menu.addAction("Root (No Folder)")
1265
+
1523
1266
  context_menu.addSeparator()
1524
1267
  rename_action = context_menu.addAction("Rename table...")
1525
1268
  delete_action = context_menu.addAction("Delete table")
@@ -1529,6 +1272,11 @@ LIMIT 10
1529
1272
  action = context_menu.exec(self.tables_list.mapToGlobal(position))
1530
1273
 
1531
1274
  if action == select_from_action:
1275
+ # Check if table needs reloading first
1276
+ if table_name in self.tables_list.tables_needing_reload:
1277
+ # Reload the table immediately without asking
1278
+ self.reload_selected_table(table_name)
1279
+
1532
1280
  # Insert "SELECT * FROM table_name" at cursor position
1533
1281
  cursor = current_tab.query_edit.textCursor()
1534
1282
  cursor.insertText(f"SELECT * FROM {table_name}")
@@ -1538,6 +1286,14 @@ LIMIT 10
1538
1286
  cursor = current_tab.query_edit.textCursor()
1539
1287
  cursor.insertText(table_name)
1540
1288
  current_tab.query_edit.setFocus()
1289
+ elif action == reload_action:
1290
+ self.reload_selected_table(table_name)
1291
+ elif action == analyze_entropy_action:
1292
+ # Call the entropy analysis method
1293
+ self.analyze_table_entropy(table_name)
1294
+ elif action == profile_table_action:
1295
+ # Call the table profile method
1296
+ self.profile_table_structure(table_name)
1541
1297
  elif action == rename_action:
1542
1298
  # Show rename dialog
1543
1299
  new_name, ok = QInputDialog.getText(
@@ -1550,8 +1306,8 @@ LIMIT 10
1550
1306
  if ok and new_name:
1551
1307
  if self.rename_table(table_name, new_name):
1552
1308
  # Update the item text
1553
- source = item.text().split(' (')[1][:-1] # Get the source part
1554
- item.setText(f"{new_name} ({source})")
1309
+ source = item.text(0).split(' (')[1][:-1] # Get the source part
1310
+ item.setText(0, f"{new_name} ({source})")
1555
1311
  self.statusBar().showMessage(f'Table renamed to "{new_name}"')
1556
1312
  elif action == delete_action:
1557
1313
  # Show confirmation dialog
@@ -1563,17 +1319,91 @@ LIMIT 10
1563
1319
  )
1564
1320
  if reply == QMessageBox.StandardButton.Yes:
1565
1321
  self.remove_selected_table()
1322
+ elif action == new_folder_action:
1323
+ # Create a new folder and move the table there
1324
+ folder_name, ok = QInputDialog.getText(
1325
+ self,
1326
+ "New Folder",
1327
+ "Enter folder name:",
1328
+ QLineEdit.EchoMode.Normal
1329
+ )
1330
+ if ok and folder_name:
1331
+ folder = self.tables_list.create_folder(folder_name)
1332
+ self.tables_list.move_item_to_folder(item, folder)
1333
+ self.statusBar().showMessage(f'Moved table "{table_name}" to folder "{folder_name}"')
1334
+ elif action == root_action:
1335
+ # Move table to root (remove from any folder)
1336
+ parent = item.parent()
1337
+ if parent and self.tables_list.is_folder_item(parent):
1338
+ # Create a clone at root level
1339
+ source = item.text(0).split(' (')[1][:-1] # Get the source part
1340
+ needs_reload = table_name in self.tables_list.tables_needing_reload
1341
+ # Remove from current parent
1342
+ parent.removeChild(item)
1343
+ # Add to root
1344
+ self.tables_list.add_table_item(table_name, source, needs_reload)
1345
+ self.statusBar().showMessage(f'Moved table "{table_name}" to root')
1346
+ elif action and action.parent() == move_menu:
1347
+ # Move to selected folder
1348
+ target_folder = action.data()
1349
+ if target_folder:
1350
+ self.tables_list.move_item_to_folder(item, target_folder)
1351
+ self.statusBar().showMessage(f'Moved table "{table_name}" to folder "{target_folder.text(0)}"')
1566
1352
 
1567
- def new_project(self):
1353
+ def reload_selected_table(self, table_name=None):
1354
+ """Reload the data for a table from its source file"""
1355
+ try:
1356
+ # If table_name is not provided, get it from the selected item
1357
+ if not table_name:
1358
+ current_item = self.tables_list.currentItem()
1359
+ if not current_item:
1360
+ return
1361
+ table_name = self.tables_list.get_table_name_from_item(current_item)
1362
+
1363
+ # Show a loading indicator
1364
+ self.statusBar().showMessage(f'Reloading table "{table_name}"...')
1365
+
1366
+ # Use the database manager to reload the table
1367
+ success, message = self.db_manager.reload_table(table_name)
1368
+
1369
+ if success:
1370
+ # Show success message
1371
+ self.statusBar().showMessage(message)
1372
+
1373
+ # Update completer with any new column names
1374
+ self.update_completer()
1375
+
1376
+ # Mark the table as reloaded (remove the reload icon)
1377
+ self.tables_list.mark_table_reloaded(table_name)
1378
+
1379
+ # Show a preview of the reloaded table
1380
+ table_item = self.tables_list.find_table_item(table_name)
1381
+ if table_item:
1382
+ self.show_table_preview(table_item)
1383
+ else:
1384
+ # Show error message
1385
+ QMessageBox.warning(self, "Reload Failed", message)
1386
+ self.statusBar().showMessage(f'Failed to reload table: {message}')
1387
+
1388
+ except Exception as e:
1389
+ QMessageBox.critical(self, "Error",
1390
+ f"Error reloading table:\n\n{str(e)}")
1391
+ self.statusBar().showMessage('Error reloading table')
1392
+
1393
+ def new_project(self, skip_confirmation=False):
1568
1394
  """Create a new project by clearing current state"""
1569
- if self.db_manager.is_connected():
1395
+ if self.db_manager.is_connected() and not skip_confirmation:
1570
1396
  reply = QMessageBox.question(self, 'New Project',
1571
- 'Are you sure you want to start a new project? All unsaved changes will be lost.',
1572
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
1397
+ 'Are you sure you want to start a new project? All unsaved changes will be lost.',
1398
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
1573
1399
  if reply == QMessageBox.StandardButton.Yes:
1574
1400
  # Close existing connection
1575
1401
  self.db_manager.close_connection()
1576
1402
 
1403
+ # Clear all database tracking
1404
+ self.db_manager.loaded_tables = {}
1405
+ self.db_manager.table_columns = {}
1406
+
1577
1407
  # Reset state
1578
1408
  self.tables_list.clear()
1579
1409
 
@@ -1594,6 +1424,34 @@ LIMIT 10
1594
1424
  self.setWindowTitle('SQL Shell')
1595
1425
  self.db_info_label.setText("No database connected")
1596
1426
  self.statusBar().showMessage('New project created')
1427
+ elif skip_confirmation:
1428
+ # Skip confirmation and just clear everything
1429
+ if self.db_manager.is_connected():
1430
+ self.db_manager.close_connection()
1431
+
1432
+ # Clear all database tracking
1433
+ self.db_manager.loaded_tables = {}
1434
+ self.db_manager.table_columns = {}
1435
+
1436
+ # Reset state
1437
+ self.tables_list.clear()
1438
+
1439
+ # Clear all tabs except one
1440
+ while self.tab_widget.count() > 1:
1441
+ self.close_tab(1) # Always close tab at index 1 to keep at least one tab
1442
+
1443
+ # Clear the remaining tab
1444
+ first_tab = self.get_tab_at_index(0)
1445
+ if first_tab:
1446
+ first_tab.set_query_text("")
1447
+ first_tab.results_table.setRowCount(0)
1448
+ first_tab.results_table.setColumnCount(0)
1449
+ first_tab.row_count_label.setText("")
1450
+ first_tab.results_title.setText("RESULTS")
1451
+
1452
+ self.current_project_file = None
1453
+ self.setWindowTitle('SQL Shell')
1454
+ self.db_info_label.setText("No database connected")
1597
1455
 
1598
1456
  def save_project(self):
1599
1457
  """Save the current project"""
@@ -1634,6 +1492,7 @@ LIMIT 10
1634
1492
 
1635
1493
  project_data = {
1636
1494
  'tables': {},
1495
+ 'folders': {},
1637
1496
  'tabs': tabs_data,
1638
1497
  'connection_type': self.db_manager.connection_type,
1639
1498
  'database_path': None # Initialize to None
@@ -1643,8 +1502,51 @@ LIMIT 10
1643
1502
  if self.db_manager.is_connected() and hasattr(self.db_manager, 'database_path'):
1644
1503
  project_data['database_path'] = self.db_manager.database_path
1645
1504
 
1646
- # Save table information
1647
- for table_name, file_path in self.db_manager.loaded_tables.items():
1505
+ # Helper function to recursively save folder structure
1506
+ def save_folder_structure(parent_item, parent_path=""):
1507
+ if parent_item is None:
1508
+ # Handle top-level items
1509
+ for i in range(self.tables_list.topLevelItemCount()):
1510
+ item = self.tables_list.topLevelItem(i)
1511
+ if self.tables_list.is_folder_item(item):
1512
+ # It's a folder - add to folders and process its children
1513
+ folder_name = item.text(0)
1514
+ folder_id = f"folder_{i}"
1515
+ project_data['folders'][folder_id] = {
1516
+ 'name': folder_name,
1517
+ 'parent': None,
1518
+ 'expanded': item.isExpanded()
1519
+ }
1520
+ save_folder_structure(item, folder_id)
1521
+ else:
1522
+ # It's a table - add to tables at root level
1523
+ save_table_item(item)
1524
+ else:
1525
+ # Process children of this folder
1526
+ for i in range(parent_item.childCount()):
1527
+ child = parent_item.child(i)
1528
+ if self.tables_list.is_folder_item(child):
1529
+ # It's a subfolder
1530
+ folder_name = child.text(0)
1531
+ folder_id = f"{parent_path}_sub_{i}"
1532
+ project_data['folders'][folder_id] = {
1533
+ 'name': folder_name,
1534
+ 'parent': parent_path,
1535
+ 'expanded': child.isExpanded()
1536
+ }
1537
+ save_folder_structure(child, folder_id)
1538
+ else:
1539
+ # It's a table in this folder
1540
+ save_table_item(child, parent_path)
1541
+
1542
+ # Helper function to save table item
1543
+ def save_table_item(item, folder_id=None):
1544
+ table_name = self.tables_list.get_table_name_from_item(item)
1545
+ if not table_name or table_name not in self.db_manager.loaded_tables:
1546
+ return
1547
+
1548
+ file_path = self.db_manager.loaded_tables[table_name]
1549
+
1648
1550
  # For database tables and query results, store the special identifier
1649
1551
  if file_path in ['database', 'query_result']:
1650
1552
  source_path = file_path
@@ -1654,9 +1556,13 @@ LIMIT 10
1654
1556
 
1655
1557
  project_data['tables'][table_name] = {
1656
1558
  'file_path': source_path,
1657
- 'columns': self.db_manager.table_columns.get(table_name, [])
1559
+ 'columns': self.db_manager.table_columns.get(table_name, []),
1560
+ 'folder': folder_id
1658
1561
  }
1659
1562
 
1563
+ # Save the folder structure
1564
+ save_folder_structure(None)
1565
+
1660
1566
  with open(file_name, 'w') as f:
1661
1567
  json.dump(project_data, f, indent=4)
1662
1568
 
@@ -1672,6 +1578,20 @@ LIMIT 10
1672
1578
  def open_project(self, file_name=None):
1673
1579
  """Open a project file"""
1674
1580
  if not file_name:
1581
+ # Check for unsaved changes before showing file dialog
1582
+ if self.has_unsaved_changes():
1583
+ reply = QMessageBox.question(self, 'Save Changes',
1584
+ 'Do you want to save your changes before opening another project?',
1585
+ QMessageBox.StandardButton.Save |
1586
+ QMessageBox.StandardButton.Discard |
1587
+ QMessageBox.StandardButton.Cancel)
1588
+
1589
+ if reply == QMessageBox.StandardButton.Save:
1590
+ self.save_project()
1591
+ elif reply == QMessageBox.StandardButton.Cancel:
1592
+ return
1593
+
1594
+ # Show file dialog after handling save prompt
1675
1595
  file_name, _ = QFileDialog.getOpenFileName(
1676
1596
  self,
1677
1597
  "Open Project",
@@ -1697,46 +1617,53 @@ LIMIT 10
1697
1617
  QApplication.processEvents()
1698
1618
 
1699
1619
  # Start fresh
1700
- self.new_project()
1620
+ self.new_project(skip_confirmation=True)
1701
1621
  progress.setValue(15)
1702
1622
  QApplication.processEvents()
1703
1623
 
1624
+ # Make sure all database tables are cleared from tracking
1625
+ self.db_manager.loaded_tables = {}
1626
+ self.db_manager.table_columns = {}
1627
+
1704
1628
  # Check if there's a database path in the project
1705
1629
  has_database_path = 'database_path' in project_data and project_data['database_path']
1706
1630
  has_database_tables = any(table_info.get('file_path') == 'database'
1707
1631
  for table_info in project_data.get('tables', {}).values())
1708
1632
 
1709
- # Set a flag to track if database tables are loaded
1633
+ # Connect to database if needed
1634
+ progress.setLabelText("Connecting to database...")
1710
1635
  database_tables_loaded = False
1636
+ database_connection_message = None
1711
1637
 
1712
- # If the project contains database tables and a database path, try to connect to it
1713
- progress.setLabelText("Connecting to database...")
1714
1638
  if has_database_path and has_database_tables:
1715
1639
  database_path = project_data['database_path']
1716
1640
  try:
1717
1641
  if os.path.exists(database_path):
1718
1642
  # Connect to the database
1719
- self.db_manager.open_database(database_path)
1643
+ self.db_manager.open_database(database_path, load_all_tables=False)
1720
1644
  self.db_info_label.setText(self.db_manager.get_connection_info())
1721
1645
  self.statusBar().showMessage(f"Connected to database: {database_path}")
1722
1646
 
1723
- # Add all database tables to the tables list
1724
- for table_name, source in self.db_manager.loaded_tables.items():
1725
- if source == 'database':
1726
- self.tables_list.addItem(f"{table_name} (database)")
1727
-
1728
1647
  # Mark database tables as loaded
1729
1648
  database_tables_loaded = True
1730
1649
  else:
1731
1650
  database_tables_loaded = False
1732
- QMessageBox.warning(self, "Database Not Found",
1651
+ # Store the message instead of showing immediately
1652
+ database_connection_message = (
1653
+ "Database Not Found",
1733
1654
  f"The project's database file was not found at:\n{database_path}\n\n"
1734
- "Database tables will be shown but not accessible until you reconnect to the database.")
1655
+ "Database tables will be shown but not accessible until you reconnect to the database.\n\n"
1656
+ "Use the 'Open Database' button to connect to your database file."
1657
+ )
1735
1658
  except Exception as e:
1736
1659
  database_tables_loaded = False
1737
- QMessageBox.warning(self, "Database Connection Error",
1660
+ # Store the message instead of showing immediately
1661
+ database_connection_message = (
1662
+ "Database Connection Error",
1738
1663
  f"Failed to connect to the project's database:\n{str(e)}\n\n"
1739
- "Database tables will be shown but not accessible until you reconnect to the database.")
1664
+ "Database tables will be shown but not accessible until you reconnect to the database.\n\n"
1665
+ "Use the 'Open Database' button to connect to your database file."
1666
+ )
1740
1667
  else:
1741
1668
  # Create connection if needed (we don't have a specific database to connect to)
1742
1669
  database_tables_loaded = False
@@ -1745,11 +1672,52 @@ LIMIT 10
1745
1672
  self.db_info_label.setText(connection_info)
1746
1673
  elif 'connection_type' in project_data and project_data['connection_type'] != self.db_manager.connection_type:
1747
1674
  # If connected but with a different database type than what was saved in the project
1748
- QMessageBox.warning(self, "Database Type Mismatch",
1675
+ # Store the message instead of showing immediately
1676
+ database_connection_message = (
1677
+ "Database Type Mismatch",
1749
1678
  f"The project was saved with a {project_data['connection_type']} database, but you're currently using {self.db_manager.connection_type}.\n\n"
1750
- "Some database-specific features may not work correctly. Consider reconnecting to the correct database type.")
1679
+ "Some database-specific features may not work correctly. Consider reconnecting to the correct database type."
1680
+ )
1751
1681
 
1752
- progress.setValue(30)
1682
+ progress.setValue(20)
1683
+ QApplication.processEvents()
1684
+
1685
+ # First, recreate the folder structure
1686
+ folder_items = {} # Store folder items by ID
1687
+
1688
+ # Create folders first
1689
+ if 'folders' in project_data:
1690
+ progress.setLabelText("Creating folders...")
1691
+ # First pass: create top-level folders
1692
+ for folder_id, folder_info in project_data['folders'].items():
1693
+ if folder_info.get('parent') is None:
1694
+ # Create top-level folder
1695
+ folder = self.tables_list.create_folder(folder_info['name'])
1696
+ folder_items[folder_id] = folder
1697
+ # Set expanded state
1698
+ folder.setExpanded(folder_info.get('expanded', True))
1699
+
1700
+ # Second pass: create subfolders
1701
+ for folder_id, folder_info in project_data['folders'].items():
1702
+ parent_id = folder_info.get('parent')
1703
+ if parent_id is not None and parent_id in folder_items:
1704
+ # Create subfolder under parent
1705
+ parent_folder = folder_items[parent_id]
1706
+ subfolder = QTreeWidgetItem(parent_folder)
1707
+ subfolder.setText(0, folder_info['name'])
1708
+ subfolder.setIcon(0, QIcon.fromTheme("folder"))
1709
+ subfolder.setData(0, Qt.ItemDataRole.UserRole, "folder")
1710
+ # Make folder text bold
1711
+ font = subfolder.font(0)
1712
+ font.setBold(True)
1713
+ subfolder.setFont(0, font)
1714
+ # Set folder flags
1715
+ subfolder.setFlags(subfolder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
1716
+ # Set expanded state
1717
+ subfolder.setExpanded(folder_info.get('expanded', True))
1718
+ folder_items[folder_id] = subfolder
1719
+
1720
+ progress.setValue(25)
1753
1721
  QApplication.processEvents()
1754
1722
 
1755
1723
  # Calculate progress steps for loading tables
@@ -1764,59 +1732,113 @@ LIMIT 10
1764
1732
  if progress.wasCanceled():
1765
1733
  break
1766
1734
 
1767
- progress.setLabelText(f"Loading table: {table_name}")
1735
+ progress.setLabelText(f"Processing table: {table_name}")
1768
1736
  file_path = table_info['file_path']
1737
+ self.statusBar().showMessage(f"Processing table: {table_name} from {file_path}")
1738
+
1769
1739
  try:
1740
+ # Determine folder placement
1741
+ folder_id = table_info.get('folder')
1742
+ parent_folder = folder_items.get(folder_id) if folder_id else None
1743
+
1770
1744
  if file_path == 'database':
1771
- # Skip if we already loaded database tables by connecting to the database
1745
+ # Different handling based on whether database connection is active
1772
1746
  if database_tables_loaded:
1773
- continue
1747
+ # Store table info without loading data
1748
+ self.db_manager.loaded_tables[table_name] = 'database'
1749
+ if 'columns' in table_info:
1750
+ self.db_manager.table_columns[table_name] = table_info['columns']
1751
+
1752
+ # Create item without reload icon
1753
+ if parent_folder:
1754
+ # Add to folder
1755
+ item = QTreeWidgetItem(parent_folder)
1756
+ item.setText(0, f"{table_name} (database)")
1757
+ item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
1758
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
1759
+ else:
1760
+ # Add to root
1761
+ self.tables_list.add_table_item(table_name, "database", needs_reload=False)
1762
+ else:
1763
+ # No active database connection, just register the table name
1764
+ self.db_manager.loaded_tables[table_name] = 'database'
1765
+ if 'columns' in table_info:
1766
+ self.db_manager.table_columns[table_name] = table_info['columns']
1774
1767
 
1775
- # For database tables, we need to check if the original database is connected
1776
- # Don't try to SELECT from non-existent tables
1777
- # Instead just register the table name for UI display
1778
- self.db_manager.loaded_tables[table_name] = 'database'
1779
-
1780
- # If we have column information, use it
1781
- if 'columns' in table_info:
1782
- self.db_manager.table_columns[table_name] = table_info['columns']
1783
-
1784
- # Add to the UI list
1785
- self.tables_list.addItem(f"{table_name} (database)")
1768
+ # Create item with reload icon
1769
+ if parent_folder:
1770
+ # Add to folder
1771
+ item = QTreeWidgetItem(parent_folder)
1772
+ item.setText(0, f"{table_name} (database)")
1773
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
1774
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
1775
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
1776
+ self.tables_list.tables_needing_reload.add(table_name)
1777
+ else:
1778
+ # Add to root
1779
+ self.tables_list.add_table_item(table_name, "database", needs_reload=True)
1786
1780
  elif file_path == 'query_result':
1787
- # For tables from query results, we'll need to re-run the query
1788
- # For now, just note it as a query result table
1781
+ # For tables from query results, just note it as a query result table
1789
1782
  self.db_manager.loaded_tables[table_name] = 'query_result'
1790
- self.tables_list.addItem(f"{table_name} (query result)")
1783
+
1784
+ # Create item with reload icon
1785
+ if parent_folder:
1786
+ # Add to folder
1787
+ item = QTreeWidgetItem(parent_folder)
1788
+ item.setText(0, f"{table_name} (query result)")
1789
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
1790
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
1791
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
1792
+ self.tables_list.tables_needing_reload.add(table_name)
1793
+ else:
1794
+ # Add to root
1795
+ self.tables_list.add_table_item(table_name, "query result", needs_reload=True)
1791
1796
  elif os.path.exists(file_path):
1792
- # Use the database manager to load the file
1793
- try:
1794
- loaded_table_name, df = self.db_manager.load_file(file_path)
1795
- self.tables_list.addItem(f"{loaded_table_name} ({os.path.basename(file_path)})")
1796
- except Exception as e:
1797
- QMessageBox.warning(self, "Warning",
1798
- f"Failed to load file for table {table_name}:\n{str(e)}")
1799
- continue
1797
+ # Register the file as a table source but don't load data yet
1798
+ self.db_manager.loaded_tables[table_name] = file_path
1799
+ if 'columns' in table_info:
1800
+ self.db_manager.table_columns[table_name] = table_info['columns']
1801
+
1802
+ # Create item with reload icon
1803
+ if parent_folder:
1804
+ # Add to folder
1805
+ item = QTreeWidgetItem(parent_folder)
1806
+ item.setText(0, f"{table_name} ({os.path.basename(file_path)})")
1807
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
1808
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
1809
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
1810
+ self.tables_list.tables_needing_reload.add(table_name)
1811
+ else:
1812
+ # Add to root
1813
+ self.tables_list.add_table_item(table_name, os.path.basename(file_path), needs_reload=True)
1800
1814
  else:
1801
- QMessageBox.warning(self, "Warning",
1802
- f"Could not find file for table {table_name}: {file_path}")
1803
- continue
1815
+ # File doesn't exist, but add to list with warning
1816
+ self.db_manager.loaded_tables[table_name] = file_path
1817
+ if 'columns' in table_info:
1818
+ self.db_manager.table_columns[table_name] = table_info['columns']
1819
+
1820
+ # Create item with reload icon and missing warning
1821
+ if parent_folder:
1822
+ # Add to folder
1823
+ item = QTreeWidgetItem(parent_folder)
1824
+ item.setText(0, f"{table_name} ({os.path.basename(file_path)} (missing))")
1825
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
1826
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
1827
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
1828
+ self.tables_list.tables_needing_reload.add(table_name)
1829
+ else:
1830
+ # Add to root
1831
+ self.tables_list.add_table_item(table_name, f"{os.path.basename(file_path)} (missing)", needs_reload=True)
1804
1832
 
1805
1833
  except Exception as e:
1806
1834
  QMessageBox.warning(self, "Warning",
1807
- f"Failed to load table {table_name}:\n{str(e)}")
1835
+ f"Failed to process table {table_name}:\n{str(e)}")
1808
1836
 
1809
1837
  # Update progress for this table
1810
1838
  current_progress += table_progress_step
1811
1839
  progress.setValue(int(current_progress))
1812
1840
  QApplication.processEvents() # Keep UI responsive
1813
1841
 
1814
- # If the project had database tables but we couldn't connect automatically, notify the user
1815
- if has_database_tables and not database_tables_loaded:
1816
- QMessageBox.information(self, "Database Connection Required",
1817
- "This project contains database tables. You need to reconnect to the database to use them.\n\n"
1818
- "Use the 'Open Database' button to connect to your database file.")
1819
-
1820
1842
  # Check if the operation was canceled
1821
1843
  if progress.wasCanceled():
1822
1844
  self.statusBar().showMessage("Project loading was canceled")
@@ -1919,9 +1941,25 @@ LIMIT 10
1919
1941
  progress.setValue(100)
1920
1942
  QApplication.processEvents()
1921
1943
 
1922
- self.statusBar().showMessage(f'Project loaded from {file_name}')
1944
+ # Show message about tables needing reload
1945
+ reload_count = len(self.tables_list.tables_needing_reload)
1946
+ if reload_count > 0:
1947
+ self.statusBar().showMessage(
1948
+ f'Project loaded from {file_name} with {table_count} tables. {reload_count} tables need to be reloaded (click reload icon).'
1949
+ )
1950
+ else:
1951
+ self.statusBar().showMessage(
1952
+ f'Project loaded from {file_name} with {table_count} tables.'
1953
+ )
1954
+
1955
+ # Close progress dialog before showing message boxes
1923
1956
  progress.close()
1924
1957
 
1958
+ # Now show any database connection message we stored earlier
1959
+ if database_connection_message and not database_tables_loaded and has_database_tables:
1960
+ title, message = database_connection_message
1961
+ QMessageBox.warning(self, title, message)
1962
+
1925
1963
  except Exception as e:
1926
1964
  QMessageBox.critical(self, "Error",
1927
1965
  f"Failed to open project:\n\n{str(e)}")
@@ -1952,6 +1990,10 @@ LIMIT 10
1952
1990
  settings = json.load(f)
1953
1991
  self.recent_projects = settings.get('recent_projects', [])
1954
1992
 
1993
+ # Load user preferences
1994
+ preferences = settings.get('preferences', {})
1995
+ self.auto_load_recent_project = preferences.get('auto_load_recent_project', True)
1996
+
1955
1997
  # Load window settings if available
1956
1998
  window_settings = settings.get('window', {})
1957
1999
  if window_settings:
@@ -1969,10 +2011,19 @@ LIMIT 10
1969
2011
  settings = json.load(f)
1970
2012
  settings['recent_projects'] = self.recent_projects
1971
2013
 
2014
+ # Save user preferences
2015
+ if 'preferences' not in settings:
2016
+ settings['preferences'] = {}
2017
+ settings['preferences']['auto_load_recent_project'] = self.auto_load_recent_project
2018
+
1972
2019
  # Save window settings
1973
2020
  window_settings = self.save_window_state()
1974
2021
  settings['window'] = window_settings
1975
2022
 
2023
+ # Also save recent and frequent files data
2024
+ settings['recent_files'] = self.recent_files
2025
+ settings['frequent_files'] = self.frequent_files
2026
+
1976
2027
  with open(settings_file, 'w') as f:
1977
2028
  json.dump(settings, f, indent=4)
1978
2029
  except Exception as e:
@@ -2064,6 +2115,20 @@ LIMIT 10
2064
2115
  def open_recent_project(self, project_path):
2065
2116
  """Open a project from the recent projects list"""
2066
2117
  if os.path.exists(project_path):
2118
+ # Check if current project has unsaved changes before loading the new one
2119
+ if self.has_unsaved_changes():
2120
+ reply = QMessageBox.question(self, 'Save Changes',
2121
+ 'Do you want to save your changes before loading another project?',
2122
+ QMessageBox.StandardButton.Save |
2123
+ QMessageBox.StandardButton.Discard |
2124
+ QMessageBox.StandardButton.Cancel)
2125
+
2126
+ if reply == QMessageBox.StandardButton.Save:
2127
+ self.save_project()
2128
+ elif reply == QMessageBox.StandardButton.Cancel:
2129
+ return
2130
+
2131
+ # Now proceed with loading the project
2067
2132
  self.current_project_file = project_path
2068
2133
  self.open_project(project_path)
2069
2134
  else:
@@ -2079,6 +2144,246 @@ LIMIT 10
2079
2144
  self.save_recent_projects()
2080
2145
  self.update_recent_projects_menu()
2081
2146
 
2147
+ def load_recent_files(self):
2148
+ """Load recent and frequent files from settings file"""
2149
+ try:
2150
+ settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
2151
+ if os.path.exists(settings_file):
2152
+ with open(settings_file, 'r') as f:
2153
+ settings = json.load(f)
2154
+ self.recent_files = settings.get('recent_files', [])
2155
+ self.frequent_files = settings.get('frequent_files', {})
2156
+ except Exception:
2157
+ self.recent_files = []
2158
+ self.frequent_files = {}
2159
+
2160
+ def save_recent_files(self):
2161
+ """Save recent and frequent files to settings file"""
2162
+ try:
2163
+ settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
2164
+ settings = {}
2165
+ if os.path.exists(settings_file):
2166
+ with open(settings_file, 'r') as f:
2167
+ settings = json.load(f)
2168
+ settings['recent_files'] = self.recent_files
2169
+ settings['frequent_files'] = self.frequent_files
2170
+
2171
+ with open(settings_file, 'w') as f:
2172
+ json.dump(settings, f, indent=4)
2173
+ except Exception as e:
2174
+ print(f"Error saving recent files: {e}")
2175
+
2176
+ def add_recent_file(self, file_path):
2177
+ """Add a file to recent files list and update frequent files count"""
2178
+ file_path = os.path.abspath(file_path)
2179
+
2180
+ # Update recent files
2181
+ if file_path in self.recent_files:
2182
+ self.recent_files.remove(file_path)
2183
+ self.recent_files.insert(0, file_path)
2184
+ self.recent_files = self.recent_files[:self.max_recent_files]
2185
+
2186
+ # Update frequency count
2187
+ if file_path in self.frequent_files:
2188
+ self.frequent_files[file_path] += 1
2189
+ else:
2190
+ self.frequent_files[file_path] = 1
2191
+
2192
+ # Save to settings
2193
+ self.save_recent_files()
2194
+
2195
+ # Update the quick access menu if it exists
2196
+ if hasattr(self, 'quick_access_menu'):
2197
+ self.update_quick_access_menu()
2198
+
2199
+ def get_frequent_files(self, limit=10):
2200
+ """Get the most frequently used files"""
2201
+ sorted_files = sorted(
2202
+ self.frequent_files.items(),
2203
+ key=lambda item: item[1],
2204
+ reverse=True
2205
+ )
2206
+ return [path for path, count in sorted_files[:limit] if os.path.exists(path)]
2207
+
2208
+ def clear_recent_files(self):
2209
+ """Clear the list of recent files"""
2210
+ self.recent_files.clear()
2211
+ self.save_recent_files()
2212
+ if hasattr(self, 'quick_access_menu'):
2213
+ self.update_quick_access_menu()
2214
+
2215
+ def clear_frequent_files(self):
2216
+ """Clear the list of frequent files"""
2217
+ self.frequent_files.clear()
2218
+ self.save_recent_files()
2219
+ if hasattr(self, 'quick_access_menu'):
2220
+ self.update_quick_access_menu()
2221
+
2222
+ def update_quick_access_menu(self):
2223
+ """Update the quick access menu with recent and frequent files"""
2224
+ if not hasattr(self, 'quick_access_menu'):
2225
+ return
2226
+
2227
+ self.quick_access_menu.clear()
2228
+
2229
+ # Add "Recent Files" section
2230
+ if self.recent_files:
2231
+ recent_section = self.quick_access_menu.addSection("Recent Files")
2232
+
2233
+ for file_path in self.recent_files[:10]: # Show top 10 recent files
2234
+ if os.path.exists(file_path):
2235
+ file_name = os.path.basename(file_path)
2236
+ action = self.quick_access_menu.addAction(file_name)
2237
+ action.setData(file_path)
2238
+ action.setToolTip(file_path)
2239
+ action.triggered.connect(lambda checked, path=file_path: self.quick_open_file(path))
2240
+
2241
+ # Add "Frequently Used Files" section
2242
+ frequent_files = self.get_frequent_files(10) # Get top 10 frequent files
2243
+ if frequent_files:
2244
+ self.quick_access_menu.addSeparator()
2245
+ freq_section = self.quick_access_menu.addSection("Frequently Used Files")
2246
+
2247
+ for file_path in frequent_files:
2248
+ file_name = os.path.basename(file_path)
2249
+ count = self.frequent_files.get(file_path, 0)
2250
+ action = self.quick_access_menu.addAction(f"{file_name} ({count} uses)")
2251
+ action.setData(file_path)
2252
+ action.setToolTip(file_path)
2253
+ action.triggered.connect(lambda checked, path=file_path: self.quick_open_file(path))
2254
+
2255
+ # Add management options if we have any files
2256
+ if self.recent_files or self.frequent_files:
2257
+ self.quick_access_menu.addSeparator()
2258
+ clear_recent = self.quick_access_menu.addAction("Clear Recent Files")
2259
+ clear_recent.triggered.connect(self.clear_recent_files)
2260
+
2261
+ clear_frequent = self.quick_access_menu.addAction("Clear Frequent Files")
2262
+ clear_frequent.triggered.connect(self.clear_frequent_files)
2263
+ else:
2264
+ # No files placeholder
2265
+ no_files = self.quick_access_menu.addAction("No Recent Files")
2266
+ no_files.setEnabled(False)
2267
+
2268
+ def quick_open_file(self, file_path):
2269
+ """Open a file from the quick access menu"""
2270
+ if not os.path.exists(file_path):
2271
+ QMessageBox.warning(self, "File Not Found",
2272
+ f"The file no longer exists:\n{file_path}")
2273
+
2274
+ # Remove from tracking
2275
+ if file_path in self.recent_files:
2276
+ self.recent_files.remove(file_path)
2277
+ if file_path in self.frequent_files:
2278
+ del self.frequent_files[file_path]
2279
+ self.save_recent_files()
2280
+ self.update_quick_access_menu()
2281
+ return
2282
+
2283
+ try:
2284
+ # Determine file type
2285
+ file_ext = os.path.splitext(file_path)[1].lower()
2286
+
2287
+ # Check if this is a Delta table directory
2288
+ is_delta_table = False
2289
+ if os.path.isdir(file_path):
2290
+ delta_path = Path(file_path)
2291
+ delta_log_path = delta_path / '_delta_log'
2292
+ if delta_log_path.exists():
2293
+ is_delta_table = True
2294
+
2295
+ if is_delta_table:
2296
+ # Delta table directory
2297
+ if not self.db_manager.is_connected():
2298
+ # Create a default in-memory DuckDB connection if none exists
2299
+ connection_info = self.db_manager.create_memory_connection()
2300
+ self.db_info_label.setText(connection_info)
2301
+
2302
+ # Use the database manager to load the Delta table
2303
+ table_name, df = self.db_manager.load_file(file_path)
2304
+
2305
+ # Update UI using new method
2306
+ self.tables_list.add_table_item(table_name, os.path.basename(file_path))
2307
+ self.statusBar().showMessage(f'Loaded Delta table from {file_path} as "{table_name}"')
2308
+
2309
+ # Show preview of loaded data
2310
+ preview_df = df.head()
2311
+ current_tab = self.get_current_tab()
2312
+ if current_tab:
2313
+ self.populate_table(preview_df)
2314
+ current_tab.results_title.setText(f"PREVIEW: {table_name}")
2315
+
2316
+ # Update completer with new table and column names
2317
+ self.update_completer()
2318
+ elif file_ext in ['.db', '.sqlite', '.sqlite3']:
2319
+ # Database file
2320
+ # Clear existing database tables from the list widget
2321
+ for i in range(self.tables_list.topLevelItemCount() - 1, -1, -1):
2322
+ item = self.tables_list.topLevelItem(i)
2323
+ if item and item.text(0).endswith('(database)'):
2324
+ self.tables_list.takeTopLevelItem(i)
2325
+
2326
+ # Use the database manager to open the database
2327
+ self.db_manager.open_database(file_path)
2328
+
2329
+ # Update UI with tables from the database using new method
2330
+ for table_name, source in self.db_manager.loaded_tables.items():
2331
+ if source == 'database':
2332
+ self.tables_list.add_table_item(table_name, "database")
2333
+
2334
+ # Update the completer with table and column names
2335
+ self.update_completer()
2336
+
2337
+ # Update status bar
2338
+ self.statusBar().showMessage(f"Connected to database: {file_path}")
2339
+ self.db_info_label.setText(self.db_manager.get_connection_info())
2340
+
2341
+ elif file_ext in ['.xlsx', '.xls', '.csv', '.parquet']:
2342
+ # Data file
2343
+ if not self.db_manager.is_connected():
2344
+ # Create a default in-memory DuckDB connection if none exists
2345
+ connection_info = self.db_manager.create_memory_connection()
2346
+ self.db_info_label.setText(connection_info)
2347
+
2348
+ # Use the database manager to load the file
2349
+ table_name, df = self.db_manager.load_file(file_path)
2350
+
2351
+ # Update UI using new method
2352
+ self.tables_list.add_table_item(table_name, os.path.basename(file_path))
2353
+ self.statusBar().showMessage(f'Loaded {file_path} as table "{table_name}"')
2354
+
2355
+ # Show preview of loaded data
2356
+ preview_df = df.head()
2357
+ current_tab = self.get_current_tab()
2358
+ if current_tab:
2359
+ self.populate_table(preview_df)
2360
+ current_tab.results_title.setText(f"PREVIEW: {table_name}")
2361
+
2362
+ # Update completer with new table and column names
2363
+ self.update_completer()
2364
+ else:
2365
+ QMessageBox.warning(self, "Unsupported File Type",
2366
+ f"The file type {file_ext} is not supported.")
2367
+ return
2368
+
2369
+ # Update tracking - increment usage count
2370
+ self.add_recent_file(file_path)
2371
+
2372
+ except Exception as e:
2373
+ QMessageBox.critical(self, "Error",
2374
+ f"Failed to open file:\n\n{str(e)}")
2375
+ self.statusBar().showMessage(f"Error opening file: {os.path.basename(file_path)}")
2376
+
2377
+ def show_quick_access_menu(self):
2378
+ """Display the quick access menu when the button is clicked"""
2379
+ # First, make sure the menu is up to date
2380
+ self.update_quick_access_menu()
2381
+
2382
+ # Show the menu below the quick access button
2383
+ if hasattr(self, 'quick_access_menu') and hasattr(self, 'quick_access_btn'):
2384
+ self.quick_access_menu.popup(self.quick_access_btn.mapToGlobal(
2385
+ QPoint(0, self.quick_access_btn.height())))
2386
+
2082
2387
  def add_tab(self, title="Query 1"):
2083
2388
  """Add a new query tab"""
2084
2389
  # Ensure title is a string
@@ -2123,6 +2428,24 @@ LIMIT 10
2123
2428
  # Process events to keep UI responsive
2124
2429
  QApplication.processEvents()
2125
2430
 
2431
+ # Update completer for the new tab
2432
+ try:
2433
+ from sqlshell.suggester_integration import get_suggestion_manager
2434
+
2435
+ # Get the suggestion manager singleton
2436
+ suggestion_mgr = get_suggestion_manager()
2437
+
2438
+ # Register the new editor with a unique ID
2439
+ editor_id = f"tab_{index}_{id(tab.query_edit)}"
2440
+ suggestion_mgr.register_editor(tab.query_edit, editor_id)
2441
+
2442
+ # Apply the current completer model if available
2443
+ if hasattr(self, '_current_completer_model'):
2444
+ tab.query_edit.update_completer_model(self._current_completer_model)
2445
+ except Exception as e:
2446
+ # Don't let autocomplete errors affect tab creation
2447
+ print(f"Error setting up autocomplete for new tab: {e}")
2448
+
2126
2449
  return tab
2127
2450
 
2128
2451
  def duplicate_current_tab(self):
@@ -2199,11 +2522,27 @@ LIMIT 10
2199
2522
  tab.results_table.setColumnCount(0)
2200
2523
  return
2201
2524
 
2525
+ # Get the widget before removing the tab
2526
+ widget = self.tab_widget.widget(index)
2527
+
2528
+ # Unregister the editor from the suggestion manager before closing
2529
+ try:
2530
+ from sqlshell.suggester_integration import get_suggestion_manager
2531
+ suggestion_mgr = get_suggestion_manager()
2532
+
2533
+ # Find and unregister this editor
2534
+ for editor_id in list(suggestion_mgr._editors.keys()):
2535
+ if editor_id.startswith(f"tab_{index}_") or (hasattr(widget, 'query_edit') and
2536
+ str(id(widget.query_edit)) in editor_id):
2537
+ suggestion_mgr.unregister_editor(editor_id)
2538
+ except Exception as e:
2539
+ # Don't let errors affect tab closing
2540
+ print(f"Error unregistering editor from suggestion manager: {e}")
2541
+
2202
2542
  # Block signals temporarily to improve performance when removing multiple tabs
2203
2543
  was_blocked = self.tab_widget.blockSignals(True)
2204
2544
 
2205
2545
  # Remove the tab
2206
- widget = self.tab_widget.widget(index)
2207
2546
  self.tab_widget.removeTab(index)
2208
2547
 
2209
2548
  # Restore signals
@@ -2218,6 +2557,36 @@ LIMIT 10
2218
2557
 
2219
2558
  # Process events to keep UI responsive
2220
2559
  QApplication.processEvents()
2560
+
2561
+ # Update tab indices in the suggestion manager
2562
+ QTimer.singleShot(100, self.update_tab_indices_in_suggestion_manager)
2563
+
2564
+ def update_tab_indices_in_suggestion_manager(self):
2565
+ """Update tab indices in the suggestion manager after tab removal"""
2566
+ try:
2567
+ from sqlshell.suggester_integration import get_suggestion_manager
2568
+ suggestion_mgr = get_suggestion_manager()
2569
+
2570
+ # Get current editors
2571
+ old_editors = suggestion_mgr._editors.copy()
2572
+ old_completers = suggestion_mgr._completers.copy()
2573
+
2574
+ # Clear current registrations
2575
+ suggestion_mgr._editors.clear()
2576
+ suggestion_mgr._completers.clear()
2577
+
2578
+ # Re-register with updated indices
2579
+ for i in range(self.tab_widget.count()):
2580
+ tab = self.tab_widget.widget(i)
2581
+ if tab and hasattr(tab, 'query_edit'):
2582
+ # Register with new index
2583
+ editor_id = f"tab_{i}_{id(tab.query_edit)}"
2584
+ suggestion_mgr._editors[editor_id] = tab.query_edit
2585
+ if hasattr(tab.query_edit, 'completer') and tab.query_edit.completer:
2586
+ suggestion_mgr._completers[editor_id] = tab.query_edit.completer
2587
+ except Exception as e:
2588
+ # Don't let errors affect application
2589
+ print(f"Error updating tab indices in suggestion manager: {e}")
2221
2590
 
2222
2591
  def close_current_tab(self):
2223
2592
  """Close the current tab"""
@@ -2317,7 +2686,544 @@ LIMIT 10
2317
2686
  except Exception as e:
2318
2687
  self.statusBar().showMessage(f"Error resetting zoom: {str(e)}", 2000)
2319
2688
 
2689
+ def load_most_recent_project(self):
2690
+ """Load the most recent project if available"""
2691
+ if self.recent_projects:
2692
+ most_recent_project = self.recent_projects[0]
2693
+ if os.path.exists(most_recent_project):
2694
+ self.open_project(most_recent_project)
2695
+ self.statusBar().showMessage(f"Auto-loaded most recent project: {os.path.basename(most_recent_project)}")
2696
+ else:
2697
+ # Remove the non-existent project from the list
2698
+ self.recent_projects.remove(most_recent_project)
2699
+ self.save_recent_projects()
2700
+ # Try the next project if available
2701
+ if self.recent_projects:
2702
+ self.load_most_recent_project()
2703
+
2704
+ def load_delta_table(self):
2705
+ """Load a Delta table from a directory"""
2706
+ if not self.db_manager.is_connected():
2707
+ # Create a default in-memory DuckDB connection if none exists
2708
+ connection_info = self.db_manager.create_memory_connection()
2709
+ self.db_info_label.setText(connection_info)
2710
+
2711
+ # Get directory containing the Delta table
2712
+ delta_dir = QFileDialog.getExistingDirectory(
2713
+ self,
2714
+ "Select Delta Table Directory",
2715
+ "",
2716
+ QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
2717
+ )
2718
+
2719
+ if not delta_dir:
2720
+ return
2721
+
2722
+ # Check if this is a valid Delta table directory
2723
+ delta_path = Path(delta_dir)
2724
+ delta_log_path = delta_path / '_delta_log'
2725
+
2726
+ if not delta_log_path.exists():
2727
+ # Ask if they want to select a subdirectory
2728
+ subdirs = [d for d in delta_path.iterdir() if d.is_dir() and (d / '_delta_log').exists()]
2729
+
2730
+ if subdirs:
2731
+ # There are subdirectories with Delta tables
2732
+ msg = QMessageBox()
2733
+ msg.setIcon(QMessageBox.Icon.Information)
2734
+ msg.setWindowTitle("Select Subdirectory")
2735
+ msg.setText(f"The selected directory does not contain a Delta table, but it contains {len(subdirs)} subdirectories with Delta tables.")
2736
+ msg.setInformativeText("Would you like to select one of these subdirectories?")
2737
+ msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
2738
+ msg.setDefaultButton(QMessageBox.StandardButton.Yes)
2739
+
2740
+ if msg.exec() == QMessageBox.StandardButton.Yes:
2741
+ # Create a dialog to select a subdirectory
2742
+ subdir_names = [d.name for d in subdirs]
2743
+ subdir, ok = QInputDialog.getItem(
2744
+ self,
2745
+ "Select Delta Subdirectory",
2746
+ "Choose a subdirectory containing a Delta table:",
2747
+ subdir_names,
2748
+ 0,
2749
+ False
2750
+ )
2751
+
2752
+ if not ok or not subdir:
2753
+ return
2754
+
2755
+ delta_dir = str(delta_path / subdir)
2756
+ delta_path = Path(delta_dir)
2757
+ else:
2758
+ # Show error and return
2759
+ QMessageBox.critical(self, "Invalid Delta Table",
2760
+ "The selected directory does not contain a Delta table (_delta_log directory not found).")
2761
+ return
2762
+ else:
2763
+ # No Delta tables found
2764
+ QMessageBox.critical(self, "Invalid Delta Table",
2765
+ "The selected directory does not contain a Delta table (_delta_log directory not found).")
2766
+ return
2767
+
2768
+ try:
2769
+ # Add to recent files
2770
+ self.add_recent_file(delta_dir)
2771
+
2772
+ # Use the database manager to load the Delta table
2773
+ import os
2774
+ table_name, df = self.db_manager.load_file(delta_dir)
2775
+
2776
+ # Update UI using new method
2777
+ self.tables_list.add_table_item(table_name, os.path.basename(delta_dir))
2778
+ self.statusBar().showMessage(f'Loaded Delta table from {delta_dir} as "{table_name}"')
2779
+
2780
+ # Show preview of loaded data
2781
+ preview_df = df.head()
2782
+ self.populate_table(preview_df)
2783
+
2784
+ # Update results title to show preview
2785
+ current_tab = self.get_current_tab()
2786
+ if current_tab:
2787
+ current_tab.results_title.setText(f"PREVIEW: {table_name}")
2788
+
2789
+ # Update completer with new table and column names
2790
+ self.update_completer()
2791
+
2792
+ except Exception as e:
2793
+ error_msg = f'Error loading Delta table from {os.path.basename(delta_dir)}: {str(e)}'
2794
+ self.statusBar().showMessage(error_msg)
2795
+ QMessageBox.critical(self, "Error", error_msg)
2796
+
2797
+ current_tab = self.get_current_tab()
2798
+ if current_tab:
2799
+ current_tab.results_table.setRowCount(0)
2800
+ current_tab.results_table.setColumnCount(0)
2801
+ current_tab.row_count_label.setText("")
2802
+
2803
+ def show_load_dialog(self):
2804
+ """Show a modern dialog with options to load different types of data"""
2805
+ # Create the dialog
2806
+ dialog = QDialog(self)
2807
+ dialog.setWindowTitle("Load Data")
2808
+ dialog.setMinimumWidth(450)
2809
+ dialog.setMinimumHeight(520)
2810
+
2811
+ # Create a layout for the dialog
2812
+ layout = QVBoxLayout(dialog)
2813
+ layout.setSpacing(24)
2814
+ layout.setContentsMargins(30, 30, 30, 30)
2815
+
2816
+ # Header section with title and logo
2817
+ header_layout = QHBoxLayout()
2818
+
2819
+ # Title label with gradient effect
2820
+ title_label = QLabel("Load Data")
2821
+ title_font = QFont()
2822
+ title_font.setPointSize(20)
2823
+ title_font.setBold(True)
2824
+ title_label.setFont(title_font)
2825
+ title_label.setStyleSheet("""
2826
+ font-weight: bold;
2827
+ background: -webkit-linear-gradient(#2C3E50, #3498DB);
2828
+ -webkit-background-clip: text;
2829
+ -webkit-text-fill-color: transparent;
2830
+ """)
2831
+ header_layout.addWidget(title_label, 1)
2832
+
2833
+ # Try to add a small logo image
2834
+ try:
2835
+ icon_path = os.path.join(os.path.dirname(__file__), "resources", "icon.png")
2836
+ if os.path.exists(icon_path):
2837
+ logo_label = QLabel()
2838
+ logo_pixmap = QPixmap(icon_path).scaled(48, 48, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
2839
+ logo_label.setPixmap(logo_pixmap)
2840
+ header_layout.addWidget(logo_label)
2841
+ except Exception:
2842
+ pass # Skip logo if any issues
2843
+
2844
+ layout.addLayout(header_layout)
2845
+
2846
+ # Description with clearer styling
2847
+ desc_label = QLabel("Choose a data source to load into SQLShell")
2848
+ desc_label.setStyleSheet("color: #7F8C8D; font-size: 14px; margin: 4px 0 12px 0;")
2849
+ layout.addWidget(desc_label)
2850
+
2851
+ # Add separator line
2852
+ separator = QFrame()
2853
+ separator.setFrameShape(QFrame.Shape.HLine)
2854
+ separator.setFrameShadow(QFrame.Shadow.Sunken)
2855
+ separator.setStyleSheet("background-color: #E0E0E0; min-height: 1px; max-height: 1px;")
2856
+ layout.addWidget(separator)
2857
+
2858
+ # Create option cards with icons, titles and descriptions
2859
+ options_layout = QVBoxLayout()
2860
+ options_layout.setSpacing(16)
2861
+ options_layout.setContentsMargins(0, 10, 0, 10)
2862
+
2863
+ # Store animation references to prevent garbage collection
2864
+ animations = []
2865
+
2866
+ # Function to create hover animations for cards
2867
+ def create_hover_animations(card):
2868
+ # Store original stylesheet
2869
+ original_style = card.styleSheet()
2870
+ hover_style = """
2871
+ background-color: #F8F9FA;
2872
+ border: 1px solid #3498DB;
2873
+ border-radius: 8px;
2874
+ """
2875
+
2876
+ # Function to handle enter event with animation
2877
+ def enterEvent(event):
2878
+ # Create and configure animation
2879
+ anim = QPropertyAnimation(card, b"geometry")
2880
+ anim.setDuration(150)
2881
+ current_geo = card.geometry()
2882
+ target_geo = QRect(
2883
+ current_geo.x() - 3, # Slight shift to left for effect
2884
+ current_geo.y(),
2885
+ current_geo.width() + 6, # Slight growth in width
2886
+ current_geo.height()
2887
+ )
2888
+ anim.setStartValue(current_geo)
2889
+ anim.setEndValue(target_geo)
2890
+ anim.setEasingCurve(QEasingCurve.Type.OutCubic)
2891
+
2892
+ # Set hover style
2893
+ card.setStyleSheet(hover_style)
2894
+ # Start animation
2895
+ anim.start()
2896
+ # Keep reference to prevent garbage collection
2897
+ animations.append(anim)
2898
+
2899
+ # Call original enter event if it exists
2900
+ original_enter = getattr(card, "_original_enterEvent", None)
2901
+ if original_enter:
2902
+ original_enter(event)
2903
+
2904
+ # Function to handle leave event with animation
2905
+ def leaveEvent(event):
2906
+ # Create and configure animation to return to original state
2907
+ anim = QPropertyAnimation(card, b"geometry")
2908
+ anim.setDuration(200)
2909
+ current_geo = card.geometry()
2910
+ original_geo = QRect(
2911
+ current_geo.x() + 3, # Shift back to original position
2912
+ current_geo.y(),
2913
+ current_geo.width() - 6, # Shrink back to original width
2914
+ current_geo.height()
2915
+ )
2916
+ anim.setStartValue(current_geo)
2917
+ anim.setEndValue(original_geo)
2918
+ anim.setEasingCurve(QEasingCurve.Type.OutCubic)
2919
+
2920
+ # Restore original style
2921
+ card.setStyleSheet(original_style)
2922
+ # Start animation
2923
+ anim.start()
2924
+ # Keep reference to prevent garbage collection
2925
+ animations.append(anim)
2926
+
2927
+ # Call original leave event if it exists
2928
+ original_leave = getattr(card, "_original_leaveEvent", None)
2929
+ if original_leave:
2930
+ original_leave(event)
2931
+
2932
+ # Store original event handlers and set new ones
2933
+ card._original_enterEvent = card.enterEvent
2934
+ card._original_leaveEvent = card.leaveEvent
2935
+ card.enterEvent = enterEvent
2936
+ card.leaveEvent = leaveEvent
2937
+
2938
+ return card
2939
+
2940
+ # Function to create styled option buttons with descriptions
2941
+ def create_option_button(title, description, icon_name, option_type, accent_color="#3498DB"):
2942
+ # Create container frame
2943
+ container = QFrame()
2944
+ container.setObjectName("optionCard")
2945
+ container.setCursor(Qt.CursorShape.PointingHandCursor)
2946
+ container.setProperty("optionType", option_type)
2947
+
2948
+ # Set frame style
2949
+ container.setFrameShape(QFrame.Shape.StyledPanel)
2950
+ container.setLineWidth(1)
2951
+ container.setMinimumHeight(90)
2952
+ container.setStyleSheet(f"""
2953
+ background-color: #FFFFFF;
2954
+ border-radius: 10px;
2955
+ border: 1px solid #E0E0E0;
2956
+ """)
2957
+
2958
+ # Create layout for the container
2959
+ card_layout = QHBoxLayout(container)
2960
+ card_layout.setContentsMargins(20, 16, 20, 16)
2961
+
2962
+ # Add icon with colored circle background
2963
+ icon_container = QFrame()
2964
+ icon_container.setFixedSize(QSize(50, 50))
2965
+ icon_container.setStyleSheet(f"""
2966
+ background-color: {accent_color}20; /* 20% opacity */
2967
+ border-radius: 25px;
2968
+ border: none;
2969
+ """)
2970
+
2971
+ icon_layout = QHBoxLayout(icon_container)
2972
+ icon_layout.setContentsMargins(0, 0, 0, 0)
2973
+
2974
+ icon_label = QLabel()
2975
+ icon = QIcon.fromTheme(icon_name)
2976
+ icon_pixmap = icon.pixmap(QSize(24, 24))
2977
+ icon_label.setPixmap(icon_pixmap)
2978
+ icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
2979
+ icon_layout.addWidget(icon_label)
2980
+
2981
+ card_layout.addWidget(icon_container)
2982
+
2983
+ # Add text section
2984
+ text_layout = QVBoxLayout()
2985
+ text_layout.setSpacing(4)
2986
+ text_layout.setContentsMargins(12, 0, 0, 0)
2987
+
2988
+ # Add title
2989
+ title_label = QLabel(title)
2990
+ title_font = QFont()
2991
+ title_font.setBold(True)
2992
+ title_font.setPointSize(12)
2993
+ title_label.setFont(title_font)
2994
+ text_layout.addWidget(title_label)
2995
+
2996
+ # Add description
2997
+ desc_label = QLabel(description)
2998
+ desc_label.setWordWrap(True)
2999
+ desc_label.setStyleSheet("color: #7F8C8D; font-size: 11px;")
3000
+ text_layout.addWidget(desc_label)
3001
+
3002
+ card_layout.addLayout(text_layout, 1)
3003
+
3004
+ # Add arrow icon to suggest clickable
3005
+ arrow_label = QLabel("→")
3006
+ arrow_label.setStyleSheet(f"color: {accent_color}; font-size: 16px; font-weight: bold;")
3007
+ card_layout.addWidget(arrow_label)
3008
+
3009
+ # Connect click event
3010
+ container.mousePressEvent = lambda e: self.handle_load_option(dialog, option_type)
3011
+
3012
+ # Apply hover animations
3013
+ container = create_hover_animations(container)
3014
+
3015
+ return container
3016
+
3017
+ # Database option
3018
+ db_option = create_option_button(
3019
+ "Database",
3020
+ "Load SQL database files (SQLite, etc.) to query and analyze.",
3021
+ "database",
3022
+ "database",
3023
+ "#2980B9" # Blue accent
3024
+ )
3025
+ options_layout.addWidget(db_option)
3026
+
3027
+ # Files option
3028
+ files_option = create_option_button(
3029
+ "Data Files",
3030
+ "Load Excel, CSV, Parquet and other data file formats.",
3031
+ "document-new",
3032
+ "files",
3033
+ "#27AE60" # Green accent
3034
+ )
3035
+ options_layout.addWidget(files_option)
3036
+
3037
+ # Delta Table option
3038
+ delta_option = create_option_button(
3039
+ "Delta Table",
3040
+ "Load data from Delta Lake format directories.",
3041
+ "folder-open",
3042
+ "delta",
3043
+ "#8E44AD" # Purple accent
3044
+ )
3045
+ options_layout.addWidget(delta_option)
3046
+
3047
+ # Test Data option
3048
+ test_option = create_option_button(
3049
+ "Test Data",
3050
+ "Generate and load sample data for testing and exploration.",
3051
+ "system-run",
3052
+ "test",
3053
+ "#E67E22" # Orange accent
3054
+ )
3055
+ options_layout.addWidget(test_option)
3056
+
3057
+ layout.addLayout(options_layout)
3058
+
3059
+ # Add spacer
3060
+ layout.addStretch()
3061
+
3062
+ # Add separator line before buttons
3063
+ bottom_separator = QFrame()
3064
+ bottom_separator.setFrameShape(QFrame.Shape.HLine)
3065
+ bottom_separator.setFrameShadow(QFrame.Shadow.Sunken)
3066
+ bottom_separator.setStyleSheet("background-color: #E0E0E0; min-height: 1px; max-height: 1px;")
3067
+ layout.addWidget(bottom_separator)
3068
+
3069
+ # Add cancel button
3070
+ button_layout = QHBoxLayout()
3071
+ button_layout.setSpacing(12)
3072
+ button_layout.setContentsMargins(0, 16, 0, 0)
3073
+ button_layout.addStretch()
3074
+
3075
+ cancel_btn = QPushButton("Cancel")
3076
+ cancel_btn.setFixedWidth(100)
3077
+ cancel_btn.setStyleSheet("""
3078
+ background-color: #F5F5F5;
3079
+ border: 1px solid #E0E0E0;
3080
+ border-radius: 6px;
3081
+ padding: 8px 16px;
3082
+ color: #7F8C8D;
3083
+ font-weight: bold;
3084
+ """)
3085
+ cancel_btn.clicked.connect(dialog.reject)
3086
+ button_layout.addWidget(cancel_btn)
3087
+
3088
+ layout.addLayout(button_layout)
3089
+
3090
+ # Apply modern drop shadow effect to the dialog
3091
+ try:
3092
+ dialog.setGraphicsEffect(None) # Clear any existing effects
3093
+ shadow = QGraphicsDropShadowEffect(dialog)
3094
+ shadow.setBlurRadius(20)
3095
+ shadow.setColor(QColor(0, 0, 0, 50)) # Semi-transparent black
3096
+ shadow.setOffset(0, 0)
3097
+ dialog.setGraphicsEffect(shadow)
3098
+ except Exception:
3099
+ pass # Skip shadow if there are any issues
3100
+
3101
+ # Add custom styling to make the dialog look modern
3102
+ dialog.setStyleSheet("""
3103
+ QDialog {
3104
+ background-color: #FFFFFF;
3105
+ border-radius: 12px;
3106
+ }
3107
+ QLabel {
3108
+ color: #2C3E50;
3109
+ }
3110
+ """)
3111
+
3112
+ # Store dialog animation references in the instance to prevent garbage collection
3113
+ dialog._animations = animations
3114
+
3115
+ # Center the dialog on the parent window
3116
+ if self.geometry().isValid():
3117
+ dialog.move(
3118
+ self.geometry().center().x() - dialog.width() // 2,
3119
+ self.geometry().center().y() - dialog.height() // 2
3120
+ )
3121
+
3122
+ # Show the dialog
3123
+ dialog.exec()
3124
+
3125
+ def handle_load_option(self, dialog, option):
3126
+ """Handle the selected load option"""
3127
+ # Close the dialog
3128
+ dialog.accept()
3129
+
3130
+ # Call the appropriate function based on the selected option
3131
+ if option == "database":
3132
+ self.open_database()
3133
+ elif option == "files":
3134
+ self.browse_files()
3135
+ elif option == "delta":
3136
+ self.load_delta_table()
3137
+ elif option == "test":
3138
+ self.load_test_data()
3139
+
3140
+ def analyze_table_entropy(self, table_name):
3141
+ """Analyze a table with the entropy profiler to identify important columns"""
3142
+ try:
3143
+ # Show a loading indicator
3144
+ self.statusBar().showMessage(f'Analyzing table "{table_name}" columns...')
3145
+
3146
+ # Get the table data
3147
+ if table_name in self.db_manager.loaded_tables:
3148
+ # Check if table needs reloading first
3149
+ if table_name in self.tables_list.tables_needing_reload:
3150
+ # Reload the table immediately
3151
+ self.reload_selected_table(table_name)
3152
+
3153
+ # Get the data as a dataframe
3154
+ query = f'SELECT * FROM "{table_name}"'
3155
+ df = self.db_manager.execute_query(query)
3156
+
3157
+ if df is not None and not df.empty:
3158
+ # Import the entropy profiler
3159
+ from sqlshell.utils.profile_entropy import visualize_profile
3160
+
3161
+ # Create and show the visualization
3162
+ self.statusBar().showMessage(f'Generating entropy profile for "{table_name}"...')
3163
+ vis = visualize_profile(df)
3164
+
3165
+ # Store a reference to prevent garbage collection
3166
+ self._entropy_window = vis
3167
+
3168
+ self.statusBar().showMessage(f'Entropy profile generated for "{table_name}"')
3169
+ else:
3170
+ QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
3171
+ self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
3172
+ else:
3173
+ QMessageBox.warning(self, "Table Not Found", f"Table '{table_name}' not found.")
3174
+ self.statusBar().showMessage(f'Table "{table_name}" not found')
3175
+
3176
+ except Exception as e:
3177
+ QMessageBox.critical(self, "Analysis Error", f"Error analyzing table:\n\n{str(e)}")
3178
+ self.statusBar().showMessage(f'Error analyzing table: {str(e)}')
3179
+
3180
+ def profile_table_structure(self, table_name):
3181
+ """Analyze a table's structure to identify candidate keys and functional dependencies"""
3182
+ try:
3183
+ # Show a loading indicator
3184
+ self.statusBar().showMessage(f'Profiling table structure for "{table_name}"...')
3185
+
3186
+ # Get the table data
3187
+ if table_name in self.db_manager.loaded_tables:
3188
+ # Check if table needs reloading first
3189
+ if table_name in self.tables_list.tables_needing_reload:
3190
+ # Reload the table immediately
3191
+ self.reload_selected_table(table_name)
3192
+
3193
+ # Get the data as a dataframe
3194
+ query = f'SELECT * FROM "{table_name}"'
3195
+ df = self.db_manager.execute_query(query)
3196
+
3197
+ if df is not None and not df.empty:
3198
+ # Import the key profiler
3199
+ from sqlshell.utils.profile_keys import visualize_profile
3200
+
3201
+ # Create and show the visualization
3202
+ self.statusBar().showMessage(f'Generating table profile for "{table_name}"...')
3203
+ vis = visualize_profile(df)
3204
+
3205
+ # Store a reference to prevent garbage collection
3206
+ self._keys_profile_window = vis
3207
+
3208
+ self.statusBar().showMessage(f'Table structure profile generated for "{table_name}"')
3209
+ else:
3210
+ QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
3211
+ self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
3212
+ else:
3213
+ QMessageBox.warning(self, "Table Not Found", f"Table '{table_name}' not found.")
3214
+ self.statusBar().showMessage(f'Table "{table_name}" not found')
3215
+
3216
+ except Exception as e:
3217
+ QMessageBox.critical(self, "Profile Error", f"Error profiling table structure:\n\n{str(e)}")
3218
+ self.statusBar().showMessage(f'Error profiling table: {str(e)}')
3219
+
2320
3220
  def main():
3221
+ # Parse command line arguments
3222
+ parser = argparse.ArgumentParser(description='SQL Shell - SQL Query Tool')
3223
+ parser.add_argument('--no-auto-load', action='store_true',
3224
+ help='Disable auto-loading the most recent project at startup')
3225
+ args = parser.parse_args()
3226
+
2321
3227
  app = QApplication(sys.argv)
2322
3228
  app.setStyle(QStyleFactory.create('Fusion'))
2323
3229
 
@@ -2358,6 +3264,10 @@ def main():
2358
3264
  print("Initializing main application...")
2359
3265
  window = SQLShell()
2360
3266
 
3267
+ # Override auto-load setting if command-line argument is provided
3268
+ if args.no_auto_load:
3269
+ window.auto_load_recent_project = False
3270
+
2361
3271
  # Define the function to show main window and hide splash
2362
3272
  def show_main_window():
2363
3273
  # Properly finish the splash screen