sqlshell 0.1.5__py3-none-any.whl → 0.1.6__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
@@ -6,120 +6,863 @@ import pandas as pd
6
6
  from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
7
7
  QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
8
8
  QLabel, QSplitter, QListWidget, QTableWidget,
9
- QTableWidgetItem, QHeaderView, QMessageBox)
10
- from PyQt6.QtCore import Qt, QAbstractTableModel
11
- from PyQt6.QtGui import QFont, QColor
9
+ QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
10
+ QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
11
+ QStyleFactory, QToolBar, QStatusBar)
12
+ from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve
13
+ from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap
12
14
  import numpy as np
13
15
  from datetime import datetime
14
16
  from sqlshell.sqlshell import create_test_data # Import from the correct location
15
17
 
18
+ class SQLSyntaxHighlighter(QSyntaxHighlighter):
19
+ def __init__(self, document):
20
+ super().__init__(document)
21
+ self.highlighting_rules = []
22
+
23
+ # SQL Keywords
24
+ keyword_format = QTextCharFormat()
25
+ keyword_format.setForeground(QColor("#0000FF")) # Blue
26
+ keyword_format.setFontWeight(QFont.Weight.Bold)
27
+ keywords = [
28
+ "\\bSELECT\\b", "\\bFROM\\b", "\\bWHERE\\b", "\\bAND\\b", "\\bOR\\b",
29
+ "\\bINNER\\b", "\\bOUTER\\b", "\\bLEFT\\b", "\\bRIGHT\\b", "\\bJOIN\\b",
30
+ "\\bON\\b", "\\bGROUP\\b", "\\bBY\\b", "\\bHAVING\\b", "\\bORDER\\b",
31
+ "\\bLIMIT\\b", "\\bOFFSET\\b", "\\bUNION\\b", "\\bEXCEPT\\b", "\\bINTERSECT\\b",
32
+ "\\bCREATE\\b", "\\bTABLE\\b", "\\bINDEX\\b", "\\bVIEW\\b", "\\bINSERT\\b",
33
+ "\\bINTO\\b", "\\bVALUES\\b", "\\bUPDATE\\b", "\\bSET\\b", "\\bDELETE\\b",
34
+ "\\bTRUNCATE\\b", "\\bALTER\\b", "\\bADD\\b", "\\bDROP\\b", "\\bCOLUMN\\b",
35
+ "\\bCONSTRAINT\\b", "\\bPRIMARY\\b", "\\bKEY\\b", "\\bFOREIGN\\b", "\\bREFERENCES\\b",
36
+ "\\bUNIQUE\\b", "\\bNOT\\b", "\\bNULL\\b", "\\bIS\\b", "\\bDISTINCT\\b",
37
+ "\\bCASE\\b", "\\bWHEN\\b", "\\bTHEN\\b", "\\bELSE\\b", "\\bEND\\b",
38
+ "\\bAS\\b", "\\bWITH\\b", "\\bBETWEEN\\b", "\\bLIKE\\b", "\\bIN\\b",
39
+ "\\bEXISTS\\b", "\\bALL\\b", "\\bANY\\b", "\\bSOME\\b", "\\bDESC\\b", "\\bASC\\b"
40
+ ]
41
+ for pattern in keywords:
42
+ regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
43
+ self.highlighting_rules.append((regex, keyword_format))
44
+
45
+ # Functions
46
+ function_format = QTextCharFormat()
47
+ function_format.setForeground(QColor("#AA00AA")) # Purple
48
+ functions = [
49
+ "\\bAVG\\b", "\\bCOUNT\\b", "\\bSUM\\b", "\\bMAX\\b", "\\bMIN\\b",
50
+ "\\bCOALESCE\\b", "\\bNVL\\b", "\\bNULLIF\\b", "\\bCAST\\b", "\\bCONVERT\\b",
51
+ "\\bLOWER\\b", "\\bUPPER\\b", "\\bTRIM\\b", "\\bLTRIM\\b", "\\bRTRIM\\b",
52
+ "\\bLENGTH\\b", "\\bSUBSTRING\\b", "\\bREPLACE\\b", "\\bCONCAT\\b",
53
+ "\\bROUND\\b", "\\bFLOOR\\b", "\\bCEIL\\b", "\\bABS\\b", "\\bMOD\\b",
54
+ "\\bCURRENT_DATE\\b", "\\bCURRENT_TIME\\b", "\\bCURRENT_TIMESTAMP\\b",
55
+ "\\bEXTRACT\\b", "\\bDATE_PART\\b", "\\bTO_CHAR\\b", "\\bTO_DATE\\b"
56
+ ]
57
+ for pattern in functions:
58
+ regex = QRegularExpression(pattern, QRegularExpression.PatternOption.CaseInsensitiveOption)
59
+ self.highlighting_rules.append((regex, function_format))
60
+
61
+ # Numbers
62
+ number_format = QTextCharFormat()
63
+ number_format.setForeground(QColor("#009900")) # Green
64
+ self.highlighting_rules.append((
65
+ QRegularExpression("\\b[0-9]+\\b"),
66
+ number_format
67
+ ))
68
+
69
+ # Single-line string literals
70
+ string_format = QTextCharFormat()
71
+ string_format.setForeground(QColor("#CC6600")) # Orange/Brown
72
+ self.highlighting_rules.append((
73
+ QRegularExpression("'[^']*'"),
74
+ string_format
75
+ ))
76
+ self.highlighting_rules.append((
77
+ QRegularExpression("\"[^\"]*\""),
78
+ string_format
79
+ ))
80
+
81
+ # Comments
82
+ comment_format = QTextCharFormat()
83
+ comment_format.setForeground(QColor("#777777")) # Gray
84
+ comment_format.setFontItalic(True)
85
+ self.highlighting_rules.append((
86
+ QRegularExpression("--[^\n]*"),
87
+ comment_format
88
+ ))
89
+
90
+ # Multi-line comments
91
+ self.comment_start_expression = QRegularExpression("/\\*")
92
+ self.comment_end_expression = QRegularExpression("\\*/")
93
+ self.multi_line_comment_format = comment_format
94
+
95
+ def highlightBlock(self, text):
96
+ # Apply regular expression highlighting rules
97
+ for pattern, format in self.highlighting_rules:
98
+ match_iterator = pattern.globalMatch(text)
99
+ while match_iterator.hasNext():
100
+ match = match_iterator.next()
101
+ self.setFormat(match.capturedStart(), match.capturedLength(), format)
102
+
103
+ # Handle multi-line comments
104
+ self.setCurrentBlockState(0)
105
+
106
+ # If previous block was inside a comment, check if this block continues it
107
+ start_index = 0
108
+ if self.previousBlockState() != 1:
109
+ # Find the start of a comment
110
+ start_match = self.comment_start_expression.match(text)
111
+ if start_match.hasMatch():
112
+ start_index = start_match.capturedStart()
113
+ else:
114
+ return
115
+
116
+ while start_index >= 0:
117
+ # Find the end of the comment
118
+ end_match = self.comment_end_expression.match(text, start_index)
119
+
120
+ # If end match found
121
+ if end_match.hasMatch():
122
+ end_index = end_match.capturedStart()
123
+ comment_length = end_index - start_index + end_match.capturedLength()
124
+ self.setFormat(start_index, comment_length, self.multi_line_comment_format)
125
+
126
+ # Look for next comment
127
+ start_match = self.comment_start_expression.match(text, start_index + comment_length)
128
+ if start_match.hasMatch():
129
+ start_index = start_match.capturedStart()
130
+ else:
131
+ start_index = -1
132
+ else:
133
+ # No end found, comment continues to next block
134
+ self.setCurrentBlockState(1) # Still inside comment
135
+ comment_length = len(text) - start_index
136
+ self.setFormat(start_index, comment_length, self.multi_line_comment_format)
137
+ start_index = -1
138
+
139
+ class LineNumberArea(QWidget):
140
+ def __init__(self, editor):
141
+ super().__init__(editor)
142
+ self.editor = editor
143
+
144
+ def sizeHint(self):
145
+ return QSize(self.editor.line_number_area_width(), 0)
146
+
147
+ def paintEvent(self, event):
148
+ self.editor.line_number_area_paint_event(event)
149
+
150
+ class SQLEditor(QPlainTextEdit):
151
+ def __init__(self, parent=None):
152
+ super().__init__(parent)
153
+ self.line_number_area = LineNumberArea(self)
154
+
155
+ # Set monospaced font
156
+ font = QFont("Consolas", 12) # Increased font size for better readability
157
+ font.setFixedPitch(True)
158
+ self.setFont(font)
159
+
160
+ # Connect signals
161
+ self.blockCountChanged.connect(self.update_line_number_area_width)
162
+ self.updateRequest.connect(self.update_line_number_area)
163
+
164
+ # Initialize
165
+ self.update_line_number_area_width(0)
166
+
167
+ # Set tab width to 4 spaces
168
+ self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
169
+
170
+ # Set placeholder text
171
+ self.setPlaceholderText("Enter your SQL query here...")
172
+
173
+ # Initialize completer
174
+ self.completer = None
175
+
176
+ # SQL Keywords for autocomplete
177
+ self.sql_keywords = [
178
+ "SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
179
+ "ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
180
+ "CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
181
+ "TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
182
+ "UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
183
+ "AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
184
+ "AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
185
+ ]
186
+
187
+ # Initialize with SQL keywords
188
+ self.set_completer(QCompleter(self.sql_keywords))
189
+
190
+ # Set modern selection color
191
+ self.selection_color = QColor("#3498DB")
192
+ self.selection_color.setAlpha(50) # Make it semi-transparent
193
+
194
+ def set_completer(self, completer):
195
+ """Set the completer for the editor"""
196
+ if self.completer:
197
+ self.completer.disconnect(self)
198
+
199
+ self.completer = completer
200
+
201
+ if not self.completer:
202
+ return
203
+
204
+ self.completer.setWidget(self)
205
+ self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
206
+ self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
207
+ self.completer.activated.connect(self.insert_completion)
208
+
209
+ def update_completer_model(self, words):
210
+ """Update the completer model with new words"""
211
+ if not self.completer:
212
+ return
213
+
214
+ # Combine SQL keywords with table/column names
215
+ all_words = self.sql_keywords + words
216
+
217
+ # Create a model with all words
218
+ model = QStringListModel()
219
+ model.setStringList(all_words)
220
+
221
+ # Set the model to the completer
222
+ self.completer.setModel(model)
223
+
224
+ def text_under_cursor(self):
225
+ """Get the text under the cursor for completion"""
226
+ tc = self.textCursor()
227
+ tc.select(QTextCursor.SelectionType.WordUnderCursor)
228
+ return tc.selectedText()
229
+
230
+ def insert_completion(self, completion):
231
+ """Insert the completion text"""
232
+ if self.completer.widget() != self:
233
+ return
234
+
235
+ tc = self.textCursor()
236
+ extra = len(completion) - len(self.completer.completionPrefix())
237
+ tc.movePosition(QTextCursor.MoveOperation.Left)
238
+ tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
239
+ tc.insertText(completion[-extra:] + " ")
240
+ self.setTextCursor(tc)
241
+
242
+ def complete(self):
243
+ """Show completion popup"""
244
+ prefix = self.text_under_cursor()
245
+
246
+ if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
247
+ if self.completer.popup().isVisible():
248
+ self.completer.popup().hide()
249
+ return
250
+
251
+ self.completer.setCompletionPrefix(prefix)
252
+
253
+ # If no completions, hide popup
254
+ if self.completer.completionCount() == 0:
255
+ self.completer.popup().hide()
256
+ return
257
+
258
+ # Get popup and position it under the current text
259
+ popup = self.completer.popup()
260
+ popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
261
+
262
+ # Calculate position for the popup
263
+ cr = self.cursorRect()
264
+ cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
265
+ self.completer.popup().verticalScrollBar().sizeHint().width())
266
+
267
+ # Show the popup
268
+ self.completer.complete(cr)
269
+
270
+ def keyPressEvent(self, event):
271
+ # Handle completer popup navigation
272
+ if self.completer and self.completer.popup().isVisible():
273
+ # Handle navigation keys for the popup
274
+ if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
275
+ Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
276
+ event.ignore()
277
+ return
278
+
279
+ # Handle special key combinations
280
+ if event.key() == Qt.Key.Key_Tab:
281
+ # Insert 4 spaces instead of a tab character
282
+ self.insertPlainText(" ")
283
+ return
284
+
285
+ # Auto-indentation for new lines
286
+ if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
287
+ cursor = self.textCursor()
288
+ block = cursor.block()
289
+ text = block.text()
290
+
291
+ # Get the indentation of the current line
292
+ indentation = ""
293
+ for char in text:
294
+ if char.isspace():
295
+ indentation += char
296
+ else:
297
+ break
298
+
299
+ # Check if line ends with an opening bracket or keywords that should increase indentation
300
+ increase_indent = ""
301
+ if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
302
+ ["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
303
+ increase_indent = " "
304
+
305
+ # Insert new line with proper indentation
306
+ super().keyPressEvent(event)
307
+ self.insertPlainText(indentation + increase_indent)
308
+ return
309
+
310
+ # Handle keyboard shortcuts
311
+ if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
312
+ if event.key() == Qt.Key.Key_Space:
313
+ # Show completion popup
314
+ self.complete()
315
+ return
316
+ elif event.key() == Qt.Key.Key_K:
317
+ # Comment/uncomment the selected lines
318
+ self.toggle_comment()
319
+ return
320
+
321
+ # For normal key presses
322
+ super().keyPressEvent(event)
323
+
324
+ # Check for autocomplete after typing
325
+ if event.text() and not event.text().isspace():
326
+ self.complete()
327
+
328
+ def paintEvent(self, event):
329
+ # Call the parent's paintEvent first
330
+ super().paintEvent(event)
331
+
332
+ # Get the current cursor
333
+ cursor = self.textCursor()
334
+
335
+ # If there's a selection, paint custom highlight
336
+ if cursor.hasSelection():
337
+ # Create a painter for this widget
338
+ painter = QPainter(self.viewport())
339
+
340
+ # Get the selection start and end positions
341
+ start = cursor.selectionStart()
342
+ end = cursor.selectionEnd()
343
+
344
+ # Create temporary cursor to get the rectangles
345
+ temp_cursor = QTextCursor(cursor)
346
+
347
+ # Move to start and get the starting position
348
+ temp_cursor.setPosition(start)
349
+ start_pos = self.cursorRect(temp_cursor)
350
+
351
+ # Move to end and get the ending position
352
+ temp_cursor.setPosition(end)
353
+ end_pos = self.cursorRect(temp_cursor)
354
+
355
+ # Set the highlight color with transparency
356
+ painter.setBrush(QBrush(self.selection_color))
357
+ painter.setPen(Qt.PenStyle.NoPen)
358
+
359
+ # Draw the highlight rectangle
360
+ if start_pos.top() == end_pos.top():
361
+ # Single line selection
362
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
363
+ end_pos.right() - start_pos.left(), start_pos.height()))
364
+ else:
365
+ # Multi-line selection
366
+ # First line
367
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
368
+ self.viewport().width() - start_pos.left(), start_pos.height()))
369
+
370
+ # Middle lines (if any)
371
+ if end_pos.top() > start_pos.top() + start_pos.height():
372
+ painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
373
+ self.viewport().width(),
374
+ end_pos.top() - (start_pos.top() + start_pos.height())))
375
+
376
+ # Last line
377
+ painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
378
+
379
+ painter.end()
380
+
381
+ def focusInEvent(self, event):
382
+ super().focusInEvent(event)
383
+ # Show temporary hint in status bar when editor gets focus
384
+ if hasattr(self.parent(), 'statusBar'):
385
+ self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
386
+
387
+ def toggle_comment(self):
388
+ cursor = self.textCursor()
389
+ if cursor.hasSelection():
390
+ # Get the selected text
391
+ start = cursor.selectionStart()
392
+ end = cursor.selectionEnd()
393
+
394
+ # Remember the selection
395
+ cursor.setPosition(start)
396
+ start_block = cursor.blockNumber()
397
+ cursor.setPosition(end)
398
+ end_block = cursor.blockNumber()
399
+
400
+ # Process each line in the selection
401
+ cursor.setPosition(start)
402
+ cursor.beginEditBlock()
403
+
404
+ for _ in range(start_block, end_block + 1):
405
+ # Move to start of line
406
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
407
+
408
+ # Check if the line is already commented
409
+ line_text = cursor.block().text().lstrip()
410
+ if line_text.startswith('--'):
411
+ # Remove comment
412
+ pos = cursor.block().text().find('--')
413
+ cursor.setPosition(cursor.block().position() + pos)
414
+ cursor.deleteChar()
415
+ cursor.deleteChar()
416
+ else:
417
+ # Add comment
418
+ cursor.insertText('--')
419
+
420
+ # Move to next line if not at the end
421
+ if not cursor.atEnd():
422
+ cursor.movePosition(cursor.MoveOperation.NextBlock)
423
+
424
+ cursor.endEditBlock()
425
+ else:
426
+ # Comment/uncomment current line
427
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
428
+ cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
429
+ line_text = cursor.selectedText().lstrip()
430
+
431
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
432
+ if line_text.startswith('--'):
433
+ # Remove comment
434
+ pos = cursor.block().text().find('--')
435
+ cursor.setPosition(cursor.block().position() + pos)
436
+ cursor.deleteChar()
437
+ cursor.deleteChar()
438
+ else:
439
+ # Add comment
440
+ cursor.insertText('--')
441
+
442
+ def line_number_area_width(self):
443
+ digits = 1
444
+ max_num = max(1, self.blockCount())
445
+ while max_num >= 10:
446
+ max_num //= 10
447
+ digits += 1
448
+
449
+ space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
450
+ return space
451
+
452
+ def update_line_number_area_width(self, _):
453
+ self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
454
+
455
+ def update_line_number_area(self, rect, dy):
456
+ if dy:
457
+ self.line_number_area.scroll(0, dy)
458
+ else:
459
+ self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
460
+
461
+ if rect.contains(self.viewport().rect()):
462
+ self.update_line_number_area_width(0)
463
+
464
+ def resizeEvent(self, event):
465
+ super().resizeEvent(event)
466
+ cr = self.contentsRect()
467
+ self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
468
+
469
+ def line_number_area_paint_event(self, event):
470
+ painter = QPainter(self.line_number_area)
471
+ painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
472
+
473
+ block = self.firstVisibleBlock()
474
+ block_number = block.blockNumber()
475
+ top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
476
+ bottom = top + round(self.blockBoundingRect(block).height())
477
+
478
+ while block.isValid() and top <= event.rect().bottom():
479
+ if block.isVisible() and bottom >= event.rect().top():
480
+ number = str(block_number + 1)
481
+ painter.setPen(QColor("#808080")) # Gray text
482
+ painter.drawText(0, top, self.line_number_area.width() - 5,
483
+ self.fontMetrics().height(),
484
+ Qt.AlignmentFlag.AlignRight, number)
485
+
486
+ block = block.next()
487
+ top = bottom
488
+ bottom = top + round(self.blockBoundingRect(block).height())
489
+ block_number += 1
490
+
16
491
  class SQLShell(QMainWindow):
17
492
  def __init__(self):
18
493
  super().__init__()
19
494
  self.current_db_type = 'duckdb' # Default to DuckDB
20
495
  self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
21
496
  self.loaded_tables = {} # Keep track of loaded tables
497
+ self.table_columns = {} # Keep track of table columns
498
+
499
+ # Define color scheme
500
+ self.colors = {
501
+ 'primary': "#2C3E50", # Dark blue-gray
502
+ 'secondary': "#3498DB", # Bright blue
503
+ 'accent': "#1ABC9C", # Teal
504
+ 'background': "#ECF0F1", # Light gray
505
+ 'text': "#2C3E50", # Dark blue-gray
506
+ 'text_light': "#7F8C8D", # Medium gray
507
+ 'success': "#2ECC71", # Green
508
+ 'warning': "#F39C12", # Orange
509
+ 'error': "#E74C3C", # Red
510
+ 'dark_bg': "#34495E", # Darker blue-gray
511
+ 'light_bg': "#F5F5F5", # Very light gray
512
+ 'border': "#BDC3C7" # Light gray border
513
+ }
514
+
22
515
  self.init_ui()
516
+ self.apply_stylesheet()
517
+
518
+ def apply_stylesheet(self):
519
+ """Apply custom stylesheet to the application"""
520
+ self.setStyleSheet(f"""
521
+ QMainWindow {{
522
+ background-color: {self.colors['background']};
523
+ }}
524
+
525
+ QWidget {{
526
+ color: {self.colors['text']};
527
+ font-family: 'Segoe UI', 'Arial', sans-serif;
528
+ }}
529
+
530
+ QLabel {{
531
+ font-size: 13px;
532
+ padding: 2px;
533
+ }}
534
+
535
+ QLabel#header_label {{
536
+ font-size: 16px;
537
+ font-weight: bold;
538
+ color: {self.colors['primary']};
539
+ padding: 8px 0;
540
+ }}
541
+
542
+ QPushButton {{
543
+ background-color: {self.colors['secondary']};
544
+ color: white;
545
+ border: none;
546
+ border-radius: 4px;
547
+ padding: 8px 16px;
548
+ font-weight: bold;
549
+ font-size: 13px;
550
+ min-height: 30px;
551
+ }}
552
+
553
+ QPushButton:hover {{
554
+ background-color: #2980B9;
555
+ }}
556
+
557
+ QPushButton:pressed {{
558
+ background-color: #1F618D;
559
+ }}
560
+
561
+ QPushButton#primary_button {{
562
+ background-color: {self.colors['accent']};
563
+ }}
564
+
565
+ QPushButton#primary_button:hover {{
566
+ background-color: #16A085;
567
+ }}
568
+
569
+ QPushButton#primary_button:pressed {{
570
+ background-color: #0E6655;
571
+ }}
572
+
573
+ QPushButton#danger_button {{
574
+ background-color: {self.colors['error']};
575
+ }}
576
+
577
+ QPushButton#danger_button:hover {{
578
+ background-color: #CB4335;
579
+ }}
580
+
581
+ QToolButton {{
582
+ background-color: transparent;
583
+ border: none;
584
+ border-radius: 4px;
585
+ padding: 4px;
586
+ }}
587
+
588
+ QToolButton:hover {{
589
+ background-color: rgba(52, 152, 219, 0.2);
590
+ }}
591
+
592
+ QFrame#sidebar {{
593
+ background-color: {self.colors['primary']};
594
+ border-radius: 0px;
595
+ }}
596
+
597
+ QFrame#content_panel {{
598
+ background-color: white;
599
+ border-radius: 8px;
600
+ border: 1px solid {self.colors['border']};
601
+ }}
602
+
603
+ QListWidget {{
604
+ background-color: white;
605
+ border-radius: 4px;
606
+ border: 1px solid {self.colors['border']};
607
+ padding: 4px;
608
+ outline: none;
609
+ }}
610
+
611
+ QListWidget::item {{
612
+ padding: 8px;
613
+ border-radius: 4px;
614
+ }}
615
+
616
+ QListWidget::item:selected {{
617
+ background-color: {self.colors['secondary']};
618
+ color: white;
619
+ }}
620
+
621
+ QListWidget::item:hover:!selected {{
622
+ background-color: #E3F2FD;
623
+ }}
624
+
625
+ QTableWidget {{
626
+ background-color: white;
627
+ alternate-background-color: #F8F9FA;
628
+ border-radius: 4px;
629
+ border: 1px solid {self.colors['border']};
630
+ gridline-color: #E0E0E0;
631
+ outline: none;
632
+ }}
633
+
634
+ QTableWidget::item {{
635
+ padding: 4px;
636
+ }}
637
+
638
+ QTableWidget::item:selected {{
639
+ background-color: rgba(52, 152, 219, 0.2);
640
+ color: {self.colors['text']};
641
+ }}
642
+
643
+ QHeaderView::section {{
644
+ background-color: {self.colors['primary']};
645
+ color: white;
646
+ padding: 8px;
647
+ border: none;
648
+ font-weight: bold;
649
+ }}
650
+
651
+ QSplitter::handle {{
652
+ background-color: {self.colors['border']};
653
+ }}
654
+
655
+ QStatusBar {{
656
+ background-color: {self.colors['primary']};
657
+ color: white;
658
+ padding: 8px;
659
+ }}
660
+
661
+ QPlainTextEdit, QTextEdit {{
662
+ background-color: white;
663
+ border-radius: 4px;
664
+ border: 1px solid {self.colors['border']};
665
+ padding: 8px;
666
+ selection-background-color: #BBDEFB;
667
+ selection-color: {self.colors['text']};
668
+ font-family: 'Consolas', 'Courier New', monospace;
669
+ font-size: 14px;
670
+ }}
671
+ """)
23
672
 
24
673
  def init_ui(self):
25
674
  self.setWindowTitle('SQL Shell')
26
675
  self.setGeometry(100, 100, 1400, 800)
27
-
676
+
677
+ # Create custom status bar
678
+ status_bar = QStatusBar()
679
+ self.setStatusBar(status_bar)
680
+
28
681
  # Create central widget and layout
29
682
  central_widget = QWidget()
30
683
  self.setCentralWidget(central_widget)
31
684
  main_layout = QHBoxLayout(central_widget)
685
+ main_layout.setContentsMargins(0, 0, 0, 0)
686
+ main_layout.setSpacing(0)
32
687
 
33
688
  # Left panel for table list
34
- left_panel = QWidget()
689
+ left_panel = QFrame()
690
+ left_panel.setObjectName("sidebar")
691
+ left_panel.setMinimumWidth(300)
692
+ left_panel.setMaximumWidth(400)
35
693
  left_layout = QVBoxLayout(left_panel)
694
+ left_layout.setContentsMargins(16, 16, 16, 16)
695
+ left_layout.setSpacing(12)
696
+
697
+ # Database info section
698
+ db_header = QLabel("DATABASE")
699
+ db_header.setObjectName("header_label")
700
+ db_header.setStyleSheet("color: white;")
701
+ left_layout.addWidget(db_header)
36
702
 
37
- # Database info label
38
703
  self.db_info_label = QLabel("No database connected")
704
+ self.db_info_label.setStyleSheet("color: white; background-color: rgba(255, 255, 255, 0.1); padding: 8px; border-radius: 4px;")
39
705
  left_layout.addWidget(self.db_info_label)
40
706
 
41
- tables_label = QLabel("Tables:")
42
- left_layout.addWidget(tables_label)
43
-
44
- self.tables_list = QListWidget()
45
- self.tables_list.itemClicked.connect(self.show_table_preview)
46
- left_layout.addWidget(self.tables_list)
707
+ # Database action buttons
708
+ db_buttons_layout = QHBoxLayout()
709
+ db_buttons_layout.setSpacing(8)
47
710
 
48
- # Buttons for table management
49
- table_buttons_layout = QHBoxLayout()
50
711
  self.open_db_btn = QPushButton('Open Database')
712
+ self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
51
713
  self.open_db_btn.clicked.connect(self.open_database)
714
+
715
+ self.test_btn = QPushButton('Load Test Data')
716
+ self.test_btn.clicked.connect(self.load_test_data)
717
+
718
+ db_buttons_layout.addWidget(self.open_db_btn)
719
+ db_buttons_layout.addWidget(self.test_btn)
720
+ left_layout.addLayout(db_buttons_layout)
721
+
722
+ # Tables section
723
+ tables_header = QLabel("TABLES")
724
+ tables_header.setObjectName("header_label")
725
+ tables_header.setStyleSheet("color: white; margin-top: 16px;")
726
+ left_layout.addWidget(tables_header)
727
+
728
+ # Table actions
729
+ table_actions_layout = QHBoxLayout()
730
+ table_actions_layout.setSpacing(8)
731
+
52
732
  self.browse_btn = QPushButton('Load Files')
733
+ self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
53
734
  self.browse_btn.clicked.connect(self.browse_files)
54
- self.remove_table_btn = QPushButton('Remove Selected')
735
+
736
+ self.remove_table_btn = QPushButton('Remove')
737
+ self.remove_table_btn.setObjectName("danger_button")
738
+ self.remove_table_btn.setIcon(QIcon.fromTheme("edit-delete"))
55
739
  self.remove_table_btn.clicked.connect(self.remove_selected_table)
56
- self.test_btn = QPushButton('Test')
57
- self.test_btn.clicked.connect(self.load_test_data)
58
740
 
59
- table_buttons_layout.addWidget(self.open_db_btn)
60
- table_buttons_layout.addWidget(self.browse_btn)
61
- table_buttons_layout.addWidget(self.remove_table_btn)
62
- table_buttons_layout.addWidget(self.test_btn)
63
- left_layout.addLayout(table_buttons_layout)
64
-
741
+ table_actions_layout.addWidget(self.browse_btn)
742
+ table_actions_layout.addWidget(self.remove_table_btn)
743
+ left_layout.addLayout(table_actions_layout)
744
+
745
+ # Tables list with custom styling
746
+ self.tables_list = QListWidget()
747
+ self.tables_list.setStyleSheet("""
748
+ QListWidget {
749
+ background-color: rgba(255, 255, 255, 0.1);
750
+ border: none;
751
+ border-radius: 4px;
752
+ color: white;
753
+ }
754
+ QListWidget::item:selected {
755
+ background-color: rgba(255, 255, 255, 0.2);
756
+ }
757
+ QListWidget::item:hover:!selected {
758
+ background-color: rgba(255, 255, 255, 0.1);
759
+ }
760
+ """)
761
+ self.tables_list.itemClicked.connect(self.show_table_preview)
762
+ left_layout.addWidget(self.tables_list)
763
+
764
+ # Add spacer at the bottom
765
+ left_layout.addStretch()
766
+
65
767
  # Right panel for query and results
66
- right_panel = QWidget()
768
+ right_panel = QFrame()
769
+ right_panel.setObjectName("content_panel")
67
770
  right_layout = QVBoxLayout(right_panel)
771
+ right_layout.setContentsMargins(16, 16, 16, 16)
772
+ right_layout.setSpacing(16)
773
+
774
+ # Query section header
775
+ query_header = QLabel("SQL QUERY")
776
+ query_header.setObjectName("header_label")
777
+ right_layout.addWidget(query_header)
68
778
 
69
779
  # Create splitter for query and results
70
780
  splitter = QSplitter(Qt.Orientation.Vertical)
781
+ splitter.setHandleWidth(8)
782
+ splitter.setChildrenCollapsible(False)
71
783
 
72
784
  # Top part - Query section
73
- query_widget = QWidget()
785
+ query_widget = QFrame()
786
+ query_widget.setObjectName("content_panel")
74
787
  query_layout = QVBoxLayout(query_widget)
788
+ query_layout.setContentsMargins(16, 16, 16, 16)
789
+ query_layout.setSpacing(12)
790
+
791
+ # Query input
792
+ self.query_edit = SQLEditor()
793
+ # Apply syntax highlighting to the query editor
794
+ self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
795
+ query_layout.addWidget(self.query_edit)
75
796
 
76
797
  # Button row
77
798
  button_layout = QHBoxLayout()
78
- self.execute_btn = QPushButton('Execute (Ctrl+Enter)')
799
+ button_layout.setSpacing(8)
800
+
801
+ self.execute_btn = QPushButton('Execute Query')
802
+ self.execute_btn.setObjectName("primary_button")
803
+ self.execute_btn.setIcon(QIcon.fromTheme("media-playback-start"))
79
804
  self.execute_btn.clicked.connect(self.execute_query)
805
+ self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
806
+
80
807
  self.clear_btn = QPushButton('Clear')
808
+ self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
81
809
  self.clear_btn.clicked.connect(self.clear_query)
82
810
 
83
- # Add export buttons
84
- self.export_excel_btn = QPushButton('Export to Excel')
85
- self.export_excel_btn.clicked.connect(self.export_to_excel)
86
- self.export_parquet_btn = QPushButton('Export to Parquet')
87
- self.export_parquet_btn.clicked.connect(self.export_to_parquet)
88
-
89
811
  button_layout.addWidget(self.execute_btn)
90
812
  button_layout.addWidget(self.clear_btn)
91
- button_layout.addWidget(self.export_excel_btn)
92
- button_layout.addWidget(self.export_parquet_btn)
93
813
  button_layout.addStretch()
94
814
 
95
815
  query_layout.addLayout(button_layout)
96
-
97
- # Query input
98
- self.query_edit = QTextEdit()
99
- self.query_edit.setPlaceholderText("Enter your SQL query here...")
100
- query_layout.addWidget(self.query_edit)
101
-
816
+
102
817
  # Bottom part - Results section
103
- results_widget = QWidget()
818
+ results_widget = QFrame()
819
+ results_widget.setObjectName("content_panel")
104
820
  results_layout = QVBoxLayout(results_widget)
821
+ results_layout.setContentsMargins(16, 16, 16, 16)
822
+ results_layout.setSpacing(12)
823
+
824
+ # Results header with row count and export options
825
+ results_header_layout = QHBoxLayout()
826
+
827
+ results_title = QLabel("RESULTS")
828
+ results_title.setObjectName("header_label")
105
829
 
106
- # Results header with row count
107
- results_header = QWidget()
108
- results_header_layout = QHBoxLayout(results_header)
109
- self.results_label = QLabel("Results:")
110
830
  self.row_count_label = QLabel("")
111
- results_header_layout.addWidget(self.results_label)
831
+ self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
832
+
833
+ results_header_layout.addWidget(results_title)
112
834
  results_header_layout.addWidget(self.row_count_label)
113
835
  results_header_layout.addStretch()
114
- results_layout.addWidget(results_header)
115
836
 
116
- # Table widget for results
837
+ # Export buttons
838
+ export_layout = QHBoxLayout()
839
+ export_layout.setSpacing(8)
840
+
841
+ self.export_excel_btn = QPushButton('Export to Excel')
842
+ self.export_excel_btn.setIcon(QIcon.fromTheme("x-office-spreadsheet"))
843
+ self.export_excel_btn.clicked.connect(self.export_to_excel)
844
+
845
+ self.export_parquet_btn = QPushButton('Export to Parquet')
846
+ self.export_parquet_btn.setIcon(QIcon.fromTheme("document-save"))
847
+ self.export_parquet_btn.clicked.connect(self.export_to_parquet)
848
+
849
+ export_layout.addWidget(self.export_excel_btn)
850
+ export_layout.addWidget(self.export_parquet_btn)
851
+
852
+ results_header_layout.addLayout(export_layout)
853
+ results_layout.addLayout(results_header_layout)
854
+
855
+ # Table widget for results with modern styling
117
856
  self.results_table = QTableWidget()
118
857
  self.results_table.setSortingEnabled(True)
119
858
  self.results_table.setAlternatingRowColors(True)
120
859
  self.results_table.horizontalHeader().setStretchLastSection(True)
121
860
  self.results_table.horizontalHeader().setSectionsMovable(True)
122
861
  self.results_table.verticalHeader().setVisible(False)
862
+ self.results_table.setShowGrid(True)
863
+ self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
864
+ self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
865
+
123
866
  results_layout.addWidget(self.results_table)
124
867
 
125
868
  # Add widgets to splitter
@@ -136,7 +879,16 @@ class SQLShell(QMainWindow):
136
879
  main_layout.addWidget(right_panel, 4)
137
880
 
138
881
  # Status bar
139
- self.statusBar().showMessage('Ready')
882
+ self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment')
883
+
884
+ # Show keyboard shortcuts in a tooltip for the query editor
885
+ self.query_edit.setToolTip(
886
+ "Keyboard Shortcuts:\n"
887
+ "Ctrl+Enter: Execute Query\n"
888
+ "Ctrl+K: Toggle Comment\n"
889
+ "Tab: Insert 4 spaces\n"
890
+ "Ctrl+Space: Show autocomplete"
891
+ )
140
892
 
141
893
  def format_value(self, value):
142
894
  """Format values for display"""
@@ -180,17 +932,35 @@ class SQLShell(QMainWindow):
180
932
  # Make cells read-only
181
933
  item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
182
934
 
935
+ # Apply special styling for NULL values
936
+ if pd.isna(value):
937
+ item.setForeground(QColor(self.colors['text_light']))
938
+ item.setBackground(QColor("#F8F9FA"))
939
+
183
940
  self.results_table.setItem(i, j, item)
184
941
 
185
942
  # Auto-adjust column widths while ensuring minimum and maximum sizes
186
943
  self.results_table.resizeColumnsToContents()
187
944
  for i in range(len(df.columns)):
188
945
  width = self.results_table.columnWidth(i)
189
- self.results_table.setColumnWidth(i, min(max(width, 50), 300))
946
+ self.results_table.setColumnWidth(i, min(max(width, 80), 300))
190
947
 
191
948
  # Update row count
192
949
  row_text = "row" if len(df) == 1 else "rows"
193
950
  self.row_count_label.setText(f"{len(df):,} {row_text}")
951
+
952
+ # Apply zebra striping
953
+ for i in range(len(df)):
954
+ if i % 2 == 0:
955
+ for j in range(len(df.columns)):
956
+ item = self.results_table.item(i, j)
957
+ if item and not pd.isna(df.iloc[i, j]):
958
+ item.setBackground(QColor("#FFFFFF"))
959
+ else:
960
+ for j in range(len(df.columns)):
961
+ item = self.results_table.item(i, j)
962
+ if item and not pd.isna(df.iloc[i, j]):
963
+ item.setBackground(QColor("#F8F9FA"))
194
964
 
195
965
  def browse_files(self):
196
966
  if not self.conn:
@@ -238,6 +1008,9 @@ class SQLShell(QMainWindow):
238
1008
 
239
1009
  self.loaded_tables[table_name] = file_name
240
1010
 
1011
+ # Store column names
1012
+ self.table_columns[table_name] = df.columns.tolist()
1013
+
241
1014
  # Update UI
242
1015
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
243
1016
  self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
@@ -245,14 +1018,22 @@ class SQLShell(QMainWindow):
245
1018
  # Show preview of loaded data
246
1019
  preview_df = df.head()
247
1020
  self.populate_table(preview_df)
248
- self.results_label.setText(f"Preview of {table_name}:")
1021
+
1022
+ # Update results title to show preview
1023
+ results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1024
+ if results_title and results_title.text() == "RESULTS":
1025
+ results_title.setText(f"PREVIEW: {table_name}")
1026
+
1027
+ # Update completer with new table and column names
1028
+ self.update_completer()
249
1029
 
250
1030
  except Exception as e:
251
- self.statusBar().showMessage(f'Error loading file: {str(e)}')
1031
+ error_msg = f'Error loading file {os.path.basename(file_name)}: {str(e)}'
1032
+ self.statusBar().showMessage(error_msg)
1033
+ QMessageBox.critical(self, "Error", error_msg)
252
1034
  self.results_table.setRowCount(0)
253
1035
  self.results_table.setColumnCount(0)
254
1036
  self.row_count_label.setText("")
255
- self.results_label.setText(f"Error loading file: {str(e)}")
256
1037
 
257
1038
  def sanitize_table_name(self, name):
258
1039
  # Replace invalid characters with underscores
@@ -272,13 +1053,17 @@ class SQLShell(QMainWindow):
272
1053
  self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
273
1054
  # Remove from our tracking
274
1055
  del self.loaded_tables[table_name]
1056
+ if table_name in self.table_columns:
1057
+ del self.table_columns[table_name]
275
1058
  # Remove from list widget
276
1059
  self.tables_list.takeItem(self.tables_list.row(current_item))
277
1060
  self.statusBar().showMessage(f'Removed table "{table_name}"')
278
1061
  self.results_table.setRowCount(0)
279
1062
  self.results_table.setColumnCount(0)
280
1063
  self.row_count_label.setText("")
281
- self.results_label.setText(f"Removed table: {table_name}")
1064
+
1065
+ # Update completer
1066
+ self.update_completer()
282
1067
 
283
1068
  def open_database(self):
284
1069
  """Open a database file (DuckDB or SQLite)"""
@@ -343,21 +1128,67 @@ class SQLShell(QMainWindow):
343
1128
  for (table_name,) in tables:
344
1129
  self.loaded_tables[table_name] = 'database'
345
1130
  self.tables_list.addItem(f"{table_name} (database)")
1131
+
1132
+ # Get column names for each table
1133
+ try:
1134
+ column_query = f"PRAGMA table_info({table_name})"
1135
+ columns = cursor.execute(column_query).fetchall()
1136
+ self.table_columns[table_name] = [col[1] for col in columns] # Column name is at index 1
1137
+ except Exception:
1138
+ self.table_columns[table_name] = []
346
1139
  else: # duckdb
347
1140
  query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
348
1141
  result = self.conn.execute(query).fetchdf()
349
1142
  for table_name in result['table_name']:
350
1143
  self.loaded_tables[table_name] = 'database'
351
1144
  self.tables_list.addItem(f"{table_name} (database)")
1145
+
1146
+ # Get column names for each table
1147
+ try:
1148
+ column_query = f"SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' AND table_schema='main'"
1149
+ columns = self.conn.execute(column_query).fetchdf()
1150
+ self.table_columns[table_name] = columns['column_name'].tolist()
1151
+ except Exception:
1152
+ self.table_columns[table_name] = []
1153
+
1154
+ # Update the completer with table and column names
1155
+ self.update_completer()
352
1156
  except Exception as e:
353
1157
  self.statusBar().showMessage(f'Error loading tables: {str(e)}')
354
1158
 
1159
+ def update_completer(self):
1160
+ """Update the completer with table and column names"""
1161
+ # Collect all table names and column names
1162
+ completion_words = list(self.loaded_tables.keys())
1163
+
1164
+ # Add column names with table name prefix (for joins)
1165
+ for table, columns in self.table_columns.items():
1166
+ completion_words.extend(columns)
1167
+ completion_words.extend([f"{table}.{col}" for col in columns])
1168
+
1169
+ # Update the completer in the query editor
1170
+ self.query_edit.update_completer_model(completion_words)
1171
+
355
1172
  def execute_query(self):
356
1173
  query = self.query_edit.toPlainText().strip()
357
1174
  if not query:
358
1175
  return
359
1176
 
1177
+ if not self.conn:
1178
+ QMessageBox.warning(self, "No Connection", "No database connection available. Creating an in-memory DuckDB database.")
1179
+ self.conn = duckdb.connect(':memory:')
1180
+ self.current_db_type = 'duckdb'
1181
+ self.db_info_label.setText("Connected to: in-memory DuckDB")
1182
+
1183
+ # Show loading indicator in status bar
1184
+ self.statusBar().showMessage('Executing query...')
1185
+
360
1186
  try:
1187
+ # Reset results title
1188
+ results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1189
+ if results_title:
1190
+ results_title.setText("RESULTS")
1191
+
361
1192
  if self.current_db_type == 'sqlite':
362
1193
  # Execute SQLite query and convert to DataFrame
363
1194
  result = pd.read_sql_query(query, self.conn)
@@ -366,17 +1197,46 @@ class SQLShell(QMainWindow):
366
1197
  result = self.conn.execute(query).fetchdf()
367
1198
 
368
1199
  self.populate_table(result)
369
- self.results_label.setText("Query Results:")
370
- self.statusBar().showMessage('Query executed successfully')
1200
+
1201
+ # Show success message with query stats
1202
+ execution_time = datetime.now().strftime("%H:%M:%S")
1203
+ row_count = len(result)
1204
+ self.statusBar().showMessage(f'Query executed successfully at {execution_time} - {row_count:,} rows returned')
1205
+
371
1206
  except Exception as e:
372
1207
  self.results_table.setRowCount(0)
373
1208
  self.results_table.setColumnCount(0)
374
- self.row_count_label.setText("")
375
- self.results_label.setText(f"Error executing query: {str(e)}")
1209
+ self.row_count_label.setText("Error")
376
1210
  self.statusBar().showMessage('Error executing query')
1211
+
1212
+ # Show error message with modern styling
1213
+ error_msg = str(e)
1214
+ if "not found" in error_msg.lower():
1215
+ error_msg += "\nMake sure the table name is correct and the table is loaded."
1216
+ elif "syntax error" in error_msg.lower():
1217
+ error_msg += "\nPlease check your SQL syntax."
1218
+
1219
+ error_box = QMessageBox(self)
1220
+ error_box.setIcon(QMessageBox.Icon.Critical)
1221
+ error_box.setWindowTitle("Query Error")
1222
+ error_box.setText("Error executing query")
1223
+ error_box.setInformativeText(error_msg)
1224
+ error_box.setDetailedText(f"Query:\n{query}")
1225
+ error_box.setStandardButtons(QMessageBox.StandardButton.Ok)
1226
+ error_box.exec()
377
1227
 
378
1228
  def clear_query(self):
1229
+ """Clear the query editor with animation"""
1230
+ # Save current text for animation
1231
+ current_text = self.query_edit.toPlainText()
1232
+ if not current_text:
1233
+ return
1234
+
1235
+ # Clear the editor
379
1236
  self.query_edit.clear()
1237
+
1238
+ # Show success message
1239
+ self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
380
1240
 
381
1241
  def show_table_preview(self, item):
382
1242
  """Show a preview of the selected table"""
@@ -389,24 +1249,33 @@ class SQLShell(QMainWindow):
389
1249
  preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
390
1250
 
391
1251
  self.populate_table(preview_df)
392
- self.results_label.setText(f"Preview of {table_name}:")
393
1252
  self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
1253
+
1254
+ # Update the results title to show which table is being previewed
1255
+ results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
1256
+ if results_title and results_title.text() == "RESULTS":
1257
+ results_title.setText(f"PREVIEW: {table_name}")
1258
+
394
1259
  except Exception as e:
395
1260
  self.results_table.setRowCount(0)
396
1261
  self.results_table.setColumnCount(0)
397
1262
  self.row_count_label.setText("")
398
- self.results_label.setText(f"Error showing preview: {str(e)}")
399
1263
  self.statusBar().showMessage('Error showing table preview')
400
-
401
- def keyPressEvent(self, event):
402
- if event.key() == Qt.Key.Key_Return and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
403
- self.execute_query()
404
- else:
405
- super().keyPressEvent(event)
1264
+
1265
+ # Show error message with modern styling
1266
+ QMessageBox.critical(
1267
+ self,
1268
+ "Error",
1269
+ f"Error showing preview: {str(e)}",
1270
+ QMessageBox.StandardButton.Ok
1271
+ )
406
1272
 
407
1273
  def load_test_data(self):
408
1274
  """Generate and load test data"""
409
1275
  try:
1276
+ # Show loading indicator
1277
+ self.statusBar().showMessage('Generating test data...')
1278
+
410
1279
  # Create test data directory if it doesn't exist
411
1280
  os.makedirs('test_data', exist_ok=True)
412
1281
 
@@ -430,18 +1299,48 @@ class SQLShell(QMainWindow):
430
1299
  self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
431
1300
  self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
432
1301
 
1302
+ # Store column names
1303
+ self.table_columns['sample_sales_data'] = sales_df.columns.tolist()
1304
+ self.table_columns['product_catalog'] = product_df.columns.tolist()
1305
+ self.table_columns['customer_data'] = customer_df.columns.tolist()
1306
+
433
1307
  # Update UI
434
1308
  self.tables_list.clear()
435
1309
  for table_name, file_path in self.loaded_tables.items():
436
1310
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
437
1311
 
438
1312
  # Set the sample query
439
- self.query_edit.setText("select * from sample_sales_data cd inner join product_catalog pc on pc.productid = cd.productid limit 3")
1313
+ sample_query = """
1314
+ SELECT
1315
+ s.orderid,
1316
+ s.orderdate,
1317
+ c.customername,
1318
+ p.productname,
1319
+ s.quantity,
1320
+ s.unitprice,
1321
+ (s.quantity * s.unitprice) AS total_amount
1322
+ FROM
1323
+ sample_sales_data s
1324
+ INNER JOIN customer_data c ON c.customerid = s.customerid
1325
+ INNER JOIN product_catalog p ON p.productid = s.productid
1326
+ ORDER BY
1327
+ s.orderdate DESC
1328
+ LIMIT 10
1329
+ """
1330
+ self.query_edit.setPlainText(sample_query.strip())
1331
+
1332
+ # Update completer
1333
+ self.update_completer()
440
1334
 
1335
+ # Show success message
441
1336
  self.statusBar().showMessage('Test data loaded successfully')
442
1337
 
1338
+ # Show a preview of the sales data
1339
+ self.show_table_preview(self.tables_list.item(0))
1340
+
443
1341
  except Exception as e:
444
1342
  self.statusBar().showMessage(f'Error loading test data: {str(e)}')
1343
+ QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
445
1344
 
446
1345
  def export_to_excel(self):
447
1346
  if self.results_table.rowCount() == 0:
@@ -453,12 +1352,25 @@ class SQLShell(QMainWindow):
453
1352
  return
454
1353
 
455
1354
  try:
1355
+ # Show loading indicator
1356
+ self.statusBar().showMessage('Exporting data to Excel...')
1357
+
456
1358
  # Convert table data to DataFrame
457
1359
  df = self.get_table_data_as_dataframe()
458
1360
  df.to_excel(file_name, index=False)
1361
+
459
1362
  self.statusBar().showMessage(f'Data exported to {file_name}')
1363
+
1364
+ # Show success message
1365
+ QMessageBox.information(
1366
+ self,
1367
+ "Export Successful",
1368
+ f"Data has been exported to:\n{file_name}",
1369
+ QMessageBox.StandardButton.Ok
1370
+ )
460
1371
  except Exception as e:
461
1372
  QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1373
+ self.statusBar().showMessage('Error exporting data')
462
1374
 
463
1375
  def export_to_parquet(self):
464
1376
  if self.results_table.rowCount() == 0:
@@ -470,12 +1382,25 @@ class SQLShell(QMainWindow):
470
1382
  return
471
1383
 
472
1384
  try:
1385
+ # Show loading indicator
1386
+ self.statusBar().showMessage('Exporting data to Parquet...')
1387
+
473
1388
  # Convert table data to DataFrame
474
1389
  df = self.get_table_data_as_dataframe()
475
1390
  df.to_parquet(file_name, index=False)
1391
+
476
1392
  self.statusBar().showMessage(f'Data exported to {file_name}')
1393
+
1394
+ # Show success message
1395
+ QMessageBox.information(
1396
+ self,
1397
+ "Export Successful",
1398
+ f"Data has been exported to:\n{file_name}",
1399
+ QMessageBox.StandardButton.Ok
1400
+ )
477
1401
  except Exception as e:
478
1402
  QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1403
+ self.statusBar().showMessage('Error exporting data')
479
1404
 
480
1405
  def get_table_data_as_dataframe(self):
481
1406
  """Helper function to convert table widget data to a DataFrame"""
@@ -489,13 +1414,58 @@ class SQLShell(QMainWindow):
489
1414
  data.append(row_data)
490
1415
  return pd.DataFrame(data, columns=headers)
491
1416
 
1417
+ def keyPressEvent(self, event):
1418
+ """Handle global keyboard shortcuts"""
1419
+ # Execute query with Ctrl+Enter or Cmd+Enter (for Mac)
1420
+ if event.key() == Qt.Key.Key_Return and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
1421
+ self.execute_btn.click() # Simply click the button instead of animating
1422
+ return
1423
+
1424
+ # Clear query with Ctrl+L
1425
+ if event.key() == Qt.Key.Key_L and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
1426
+ self.clear_btn.click() # Simply click the button instead of animating
1427
+ return
1428
+
1429
+ super().keyPressEvent(event)
1430
+
492
1431
  def main():
493
1432
  app = QApplication(sys.argv)
494
1433
 
495
1434
  # Set application style
496
1435
  app.setStyle('Fusion')
497
1436
 
1437
+ # Apply custom palette for the entire application
1438
+ palette = QPalette()
1439
+ palette.setColor(QPalette.ColorRole.Window, QColor("#ECF0F1"))
1440
+ palette.setColor(QPalette.ColorRole.WindowText, QColor("#2C3E50"))
1441
+ palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
1442
+ palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F5F5F5"))
1443
+ palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#2C3E50"))
1444
+ palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#FFFFFF"))
1445
+ palette.setColor(QPalette.ColorRole.Text, QColor("#2C3E50"))
1446
+ palette.setColor(QPalette.ColorRole.Button, QColor("#3498DB"))
1447
+ palette.setColor(QPalette.ColorRole.ButtonText, QColor("#FFFFFF"))
1448
+ palette.setColor(QPalette.ColorRole.BrightText, QColor("#FFFFFF"))
1449
+ palette.setColor(QPalette.ColorRole.Link, QColor("#3498DB"))
1450
+ palette.setColor(QPalette.ColorRole.Highlight, QColor("#3498DB"))
1451
+ palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
1452
+ app.setPalette(palette)
1453
+
1454
+ # Set default font
1455
+ default_font = QFont("Segoe UI", 10)
1456
+ app.setFont(default_font)
1457
+
1458
+ # Create and show the application
498
1459
  sql_shell = SQLShell()
1460
+
1461
+ # Set application icon (if available)
1462
+ try:
1463
+ app_icon = QIcon("sqlshell/resources/icon.png")
1464
+ sql_shell.setWindowIcon(app_icon)
1465
+ except:
1466
+ # If icon not found, continue without it
1467
+ pass
1468
+
499
1469
  sql_shell.show()
500
1470
  sys.exit(app.exec())
501
1471