sqlshell 0.1.4__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
@@ -1,74 +1,811 @@
1
1
  import sys
2
2
  import os
3
3
  import duckdb
4
+ import sqlite3
4
5
  import pandas as pd
5
6
  from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
6
7
  QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
7
8
  QLabel, QSplitter, QListWidget, QTableWidget,
8
- QTableWidgetItem, QHeaderView)
9
- from PyQt6.QtCore import Qt, QAbstractTableModel
10
- 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
11
14
  import numpy as np
12
15
  from datetime import datetime
13
- import create_test_data # Import the test data generation module
16
+ from sqlshell.sqlshell import create_test_data # Import from the correct location
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
14
490
 
15
491
  class SQLShell(QMainWindow):
16
492
  def __init__(self):
17
493
  super().__init__()
18
- self.conn = duckdb.connect('pool.db')
494
+ self.current_db_type = 'duckdb' # Default to DuckDB
495
+ self.conn = duckdb.connect(':memory:') # Create in-memory DuckDB connection by default
19
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
+
20
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
+ """)
21
672
 
22
673
  def init_ui(self):
23
674
  self.setWindowTitle('SQL Shell')
24
675
  self.setGeometry(100, 100, 1400, 800)
25
-
676
+
677
+ # Create custom status bar
678
+ status_bar = QStatusBar()
679
+ self.setStatusBar(status_bar)
680
+
26
681
  # Create central widget and layout
27
682
  central_widget = QWidget()
28
683
  self.setCentralWidget(central_widget)
29
684
  main_layout = QHBoxLayout(central_widget)
685
+ main_layout.setContentsMargins(0, 0, 0, 0)
686
+ main_layout.setSpacing(0)
30
687
 
31
688
  # Left panel for table list
32
- left_panel = QWidget()
689
+ left_panel = QFrame()
690
+ left_panel.setObjectName("sidebar")
691
+ left_panel.setMinimumWidth(300)
692
+ left_panel.setMaximumWidth(400)
33
693
  left_layout = QVBoxLayout(left_panel)
694
+ left_layout.setContentsMargins(16, 16, 16, 16)
695
+ left_layout.setSpacing(12)
34
696
 
35
- tables_label = QLabel("Loaded Tables:")
36
- left_layout.addWidget(tables_label)
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)
37
702
 
38
- self.tables_list = QListWidget()
39
- self.tables_list.itemClicked.connect(self.show_table_preview)
40
- left_layout.addWidget(self.tables_list)
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;")
705
+ left_layout.addWidget(self.db_info_label)
706
+
707
+ # Database action buttons
708
+ db_buttons_layout = QHBoxLayout()
709
+ db_buttons_layout.setSpacing(8)
710
+
711
+ self.open_db_btn = QPushButton('Open Database')
712
+ self.open_db_btn.setIcon(QIcon.fromTheme("document-open"))
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)
41
731
 
42
- # Buttons for table management
43
- table_buttons_layout = QHBoxLayout()
44
732
  self.browse_btn = QPushButton('Load Files')
733
+ self.browse_btn.setIcon(QIcon.fromTheme("document-new"))
45
734
  self.browse_btn.clicked.connect(self.browse_files)
46
- 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"))
47
739
  self.remove_table_btn.clicked.connect(self.remove_selected_table)
48
- self.test_btn = QPushButton('Test')
49
- self.test_btn.clicked.connect(self.load_test_data)
50
740
 
51
- table_buttons_layout.addWidget(self.browse_btn)
52
- table_buttons_layout.addWidget(self.remove_table_btn)
53
- table_buttons_layout.addWidget(self.test_btn)
54
- left_layout.addLayout(table_buttons_layout)
55
-
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
+
56
767
  # Right panel for query and results
57
- right_panel = QWidget()
768
+ right_panel = QFrame()
769
+ right_panel.setObjectName("content_panel")
58
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)
59
778
 
60
779
  # Create splitter for query and results
61
780
  splitter = QSplitter(Qt.Orientation.Vertical)
781
+ splitter.setHandleWidth(8)
782
+ splitter.setChildrenCollapsible(False)
62
783
 
63
784
  # Top part - Query section
64
- query_widget = QWidget()
785
+ query_widget = QFrame()
786
+ query_widget.setObjectName("content_panel")
65
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)
66
796
 
67
797
  # Button row
68
798
  button_layout = QHBoxLayout()
69
- 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"))
70
804
  self.execute_btn.clicked.connect(self.execute_query)
805
+ self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
806
+
71
807
  self.clear_btn = QPushButton('Clear')
808
+ self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
72
809
  self.clear_btn.clicked.connect(self.clear_query)
73
810
 
74
811
  button_layout.addWidget(self.execute_btn)
@@ -76,33 +813,56 @@ class SQLShell(QMainWindow):
76
813
  button_layout.addStretch()
77
814
 
78
815
  query_layout.addLayout(button_layout)
79
-
80
- # Query input
81
- self.query_edit = QTextEdit()
82
- self.query_edit.setPlaceholderText("Enter your SQL query here...")
83
- query_layout.addWidget(self.query_edit)
84
-
816
+
85
817
  # Bottom part - Results section
86
- results_widget = QWidget()
818
+ results_widget = QFrame()
819
+ results_widget.setObjectName("content_panel")
87
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")
88
829
 
89
- # Results header with row count
90
- results_header = QWidget()
91
- results_header_layout = QHBoxLayout(results_header)
92
- self.results_label = QLabel("Results:")
93
830
  self.row_count_label = QLabel("")
94
- 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)
95
834
  results_header_layout.addWidget(self.row_count_label)
96
835
  results_header_layout.addStretch()
97
- results_layout.addWidget(results_header)
98
836
 
99
- # 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
100
856
  self.results_table = QTableWidget()
101
857
  self.results_table.setSortingEnabled(True)
102
858
  self.results_table.setAlternatingRowColors(True)
103
859
  self.results_table.horizontalHeader().setStretchLastSection(True)
104
860
  self.results_table.horizontalHeader().setSectionsMovable(True)
105
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
+
106
866
  results_layout.addWidget(self.results_table)
107
867
 
108
868
  # Add widgets to splitter
@@ -119,7 +879,16 @@ class SQLShell(QMainWindow):
119
879
  main_layout.addWidget(right_panel, 4)
120
880
 
121
881
  # Status bar
122
- 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
+ )
123
892
 
124
893
  def format_value(self, value):
125
894
  """Format values for display"""
@@ -163,19 +932,43 @@ class SQLShell(QMainWindow):
163
932
  # Make cells read-only
164
933
  item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
165
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
+
166
940
  self.results_table.setItem(i, j, item)
167
941
 
168
942
  # Auto-adjust column widths while ensuring minimum and maximum sizes
169
943
  self.results_table.resizeColumnsToContents()
170
944
  for i in range(len(df.columns)):
171
945
  width = self.results_table.columnWidth(i)
172
- self.results_table.setColumnWidth(i, min(max(width, 50), 300))
946
+ self.results_table.setColumnWidth(i, min(max(width, 80), 300))
173
947
 
174
948
  # Update row count
175
949
  row_text = "row" if len(df) == 1 else "rows"
176
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"))
177
964
 
178
965
  def browse_files(self):
966
+ if not self.conn:
967
+ # Create a default in-memory DuckDB connection if none exists
968
+ self.conn = duckdb.connect(':memory:')
969
+ self.current_db_type = 'duckdb'
970
+ self.db_info_label.setText("Connected to: in-memory DuckDB")
971
+
179
972
  file_names, _ = QFileDialog.getOpenFileNames(
180
973
  self,
181
974
  "Open Data Files",
@@ -205,10 +998,19 @@ class SQLShell(QMainWindow):
205
998
  table_name = f"{original_name}_{counter}"
206
999
  counter += 1
207
1000
 
208
- # Register table in DuckDB
209
- self.conn.register(table_name, df)
1001
+ # Handle table creation based on database type
1002
+ if self.current_db_type == 'sqlite':
1003
+ # For SQLite, create a table from the DataFrame
1004
+ df.to_sql(table_name, self.conn, index=False, if_exists='replace')
1005
+ else:
1006
+ # For DuckDB, register the DataFrame as a view
1007
+ self.conn.register(table_name, df)
1008
+
210
1009
  self.loaded_tables[table_name] = file_name
211
1010
 
1011
+ # Store column names
1012
+ self.table_columns[table_name] = df.columns.tolist()
1013
+
212
1014
  # Update UI
213
1015
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
214
1016
  self.statusBar().showMessage(f'Loaded {file_name} as table "{table_name}"')
@@ -216,14 +1018,22 @@ class SQLShell(QMainWindow):
216
1018
  # Show preview of loaded data
217
1019
  preview_df = df.head()
218
1020
  self.populate_table(preview_df)
219
- 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()
220
1029
 
221
1030
  except Exception as e:
222
- 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)
223
1034
  self.results_table.setRowCount(0)
224
1035
  self.results_table.setColumnCount(0)
225
1036
  self.row_count_label.setText("")
226
- self.results_label.setText(f"Error loading file: {str(e)}")
227
1037
 
228
1038
  def sanitize_table_name(self, name):
229
1039
  # Replace invalid characters with underscores
@@ -243,59 +1053,229 @@ class SQLShell(QMainWindow):
243
1053
  self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
244
1054
  # Remove from our tracking
245
1055
  del self.loaded_tables[table_name]
1056
+ if table_name in self.table_columns:
1057
+ del self.table_columns[table_name]
246
1058
  # Remove from list widget
247
1059
  self.tables_list.takeItem(self.tables_list.row(current_item))
248
1060
  self.statusBar().showMessage(f'Removed table "{table_name}"')
249
1061
  self.results_table.setRowCount(0)
250
1062
  self.results_table.setColumnCount(0)
251
1063
  self.row_count_label.setText("")
252
- self.results_label.setText(f"Removed table: {table_name}")
1064
+
1065
+ # Update completer
1066
+ self.update_completer()
1067
+
1068
+ def open_database(self):
1069
+ """Open a database file (DuckDB or SQLite)"""
1070
+ file_name, _ = QFileDialog.getOpenFileName(
1071
+ self,
1072
+ "Open Database File",
1073
+ "",
1074
+ "Database Files (*.db);;All Files (*)"
1075
+ )
1076
+
1077
+ if not file_name:
1078
+ return
1079
+
1080
+ try:
1081
+ # Try to detect database type
1082
+ is_sqlite = self.is_sqlite_db(file_name)
1083
+
1084
+ # Close existing connection if any
1085
+ if self.conn:
1086
+ self.conn.close()
1087
+
1088
+ # Connect to the database
1089
+ if is_sqlite:
1090
+ self.conn = sqlite3.connect(file_name)
1091
+ self.current_db_type = 'sqlite'
1092
+ else:
1093
+ self.conn = duckdb.connect(file_name)
1094
+ self.current_db_type = 'duckdb'
1095
+
1096
+ # Clear existing tables
1097
+ self.loaded_tables.clear()
1098
+ self.tables_list.clear()
1099
+
1100
+ # Load tables
1101
+ self.load_database_tables()
1102
+
1103
+ # Update UI
1104
+ db_type = "SQLite" if is_sqlite else "DuckDB"
1105
+ self.db_info_label.setText(f"Connected to: {os.path.basename(file_name)} ({db_type})")
1106
+ self.statusBar().showMessage(f'Successfully opened {db_type} database: {file_name}')
1107
+
1108
+ except Exception as e:
1109
+ QMessageBox.critical(self, "Error", f"Failed to open database: {str(e)}")
1110
+ self.statusBar().showMessage('Error opening database')
1111
+
1112
+ def is_sqlite_db(self, filename):
1113
+ """Check if the file is a SQLite database"""
1114
+ try:
1115
+ with open(filename, 'rb') as f:
1116
+ header = f.read(16)
1117
+ return header[:16] == b'SQLite format 3\x00'
1118
+ except:
1119
+ return False
1120
+
1121
+ def load_database_tables(self):
1122
+ """Load all tables from the current database"""
1123
+ try:
1124
+ if self.current_db_type == 'sqlite':
1125
+ query = "SELECT name FROM sqlite_master WHERE type='table'"
1126
+ cursor = self.conn.cursor()
1127
+ tables = cursor.execute(query).fetchall()
1128
+ for (table_name,) in tables:
1129
+ self.loaded_tables[table_name] = 'database'
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] = []
1139
+ else: # duckdb
1140
+ query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
1141
+ result = self.conn.execute(query).fetchdf()
1142
+ for table_name in result['table_name']:
1143
+ self.loaded_tables[table_name] = 'database'
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()
1156
+ except Exception as e:
1157
+ self.statusBar().showMessage(f'Error loading tables: {str(e)}')
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)
253
1171
 
254
1172
  def execute_query(self):
255
1173
  query = self.query_edit.toPlainText().strip()
256
1174
  if not query:
257
1175
  return
258
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
+
259
1186
  try:
260
- result = self.conn.execute(query).fetchdf()
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
+
1192
+ if self.current_db_type == 'sqlite':
1193
+ # Execute SQLite query and convert to DataFrame
1194
+ result = pd.read_sql_query(query, self.conn)
1195
+ else:
1196
+ # Execute DuckDB query
1197
+ result = self.conn.execute(query).fetchdf()
1198
+
261
1199
  self.populate_table(result)
262
- self.results_label.setText("Query Results:")
263
- 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
+
264
1206
  except Exception as e:
265
1207
  self.results_table.setRowCount(0)
266
1208
  self.results_table.setColumnCount(0)
267
- self.row_count_label.setText("")
268
- self.results_label.setText(f"Error executing query: {str(e)}")
1209
+ self.row_count_label.setText("Error")
269
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()
270
1227
 
271
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
272
1236
  self.query_edit.clear()
1237
+
1238
+ # Show success message
1239
+ self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
273
1240
 
274
1241
  def show_table_preview(self, item):
275
1242
  """Show a preview of the selected table"""
276
1243
  if item:
277
1244
  table_name = item.text().split(' (')[0]
278
1245
  try:
279
- preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
1246
+ if self.current_db_type == 'sqlite':
1247
+ preview_df = pd.read_sql_query(f'SELECT * FROM "{table_name}" LIMIT 5', self.conn)
1248
+ else:
1249
+ preview_df = self.conn.execute(f'SELECT * FROM {table_name} LIMIT 5').fetchdf()
1250
+
280
1251
  self.populate_table(preview_df)
281
- self.results_label.setText(f"Preview of {table_name}:")
282
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
+
283
1259
  except Exception as e:
284
1260
  self.results_table.setRowCount(0)
285
1261
  self.results_table.setColumnCount(0)
286
1262
  self.row_count_label.setText("")
287
- self.results_label.setText(f"Error showing preview: {str(e)}")
288
1263
  self.statusBar().showMessage('Error showing table preview')
289
-
290
- def keyPressEvent(self, event):
291
- if event.key() == Qt.Key.Key_Return and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
292
- self.execute_query()
293
- else:
294
- super().keyPressEvent(event)
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
+ )
295
1272
 
296
1273
  def load_test_data(self):
297
1274
  """Generate and load test data"""
298
1275
  try:
1276
+ # Show loading indicator
1277
+ self.statusBar().showMessage('Generating test data...')
1278
+
299
1279
  # Create test data directory if it doesn't exist
300
1280
  os.makedirs('test_data', exist_ok=True)
301
1281
 
@@ -319,18 +1299,134 @@ class SQLShell(QMainWindow):
319
1299
  self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
320
1300
  self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
321
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
+
322
1307
  # Update UI
323
1308
  self.tables_list.clear()
324
1309
  for table_name, file_path in self.loaded_tables.items():
325
1310
  self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
326
1311
 
327
1312
  # Set the sample query
328
- self.query_edit.setText("select * from sample_sales_data cd inner join product_catalog pc on pc.productid = cd.productid limit 3")
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()
329
1334
 
1335
+ # Show success message
330
1336
  self.statusBar().showMessage('Test data loaded successfully')
331
1337
 
1338
+ # Show a preview of the sales data
1339
+ self.show_table_preview(self.tables_list.item(0))
1340
+
332
1341
  except Exception as e:
333
1342
  self.statusBar().showMessage(f'Error loading test data: {str(e)}')
1343
+ QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
1344
+
1345
+ def export_to_excel(self):
1346
+ if self.results_table.rowCount() == 0:
1347
+ QMessageBox.warning(self, "No Data", "There is no data to export.")
1348
+ return
1349
+
1350
+ file_name, _ = QFileDialog.getSaveFileName(self, "Save as Excel", "", "Excel Files (*.xlsx);;All Files (*)")
1351
+ if not file_name:
1352
+ return
1353
+
1354
+ try:
1355
+ # Show loading indicator
1356
+ self.statusBar().showMessage('Exporting data to Excel...')
1357
+
1358
+ # Convert table data to DataFrame
1359
+ df = self.get_table_data_as_dataframe()
1360
+ df.to_excel(file_name, index=False)
1361
+
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
+ )
1371
+ except Exception as e:
1372
+ QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1373
+ self.statusBar().showMessage('Error exporting data')
1374
+
1375
+ def export_to_parquet(self):
1376
+ if self.results_table.rowCount() == 0:
1377
+ QMessageBox.warning(self, "No Data", "There is no data to export.")
1378
+ return
1379
+
1380
+ file_name, _ = QFileDialog.getSaveFileName(self, "Save as Parquet", "", "Parquet Files (*.parquet);;All Files (*)")
1381
+ if not file_name:
1382
+ return
1383
+
1384
+ try:
1385
+ # Show loading indicator
1386
+ self.statusBar().showMessage('Exporting data to Parquet...')
1387
+
1388
+ # Convert table data to DataFrame
1389
+ df = self.get_table_data_as_dataframe()
1390
+ df.to_parquet(file_name, index=False)
1391
+
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
+ )
1401
+ except Exception as e:
1402
+ QMessageBox.critical(self, "Error", f"Failed to export data: {str(e)}")
1403
+ self.statusBar().showMessage('Error exporting data')
1404
+
1405
+ def get_table_data_as_dataframe(self):
1406
+ """Helper function to convert table widget data to a DataFrame"""
1407
+ headers = [self.results_table.horizontalHeaderItem(i).text() for i in range(self.results_table.columnCount())]
1408
+ data = []
1409
+ for row in range(self.results_table.rowCount()):
1410
+ row_data = []
1411
+ for column in range(self.results_table.columnCount()):
1412
+ item = self.results_table.item(row, column)
1413
+ row_data.append(item.text() if item else '')
1414
+ data.append(row_data)
1415
+ return pd.DataFrame(data, columns=headers)
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)
334
1430
 
335
1431
  def main():
336
1432
  app = QApplication(sys.argv)
@@ -338,7 +1434,38 @@ def main():
338
1434
  # Set application style
339
1435
  app.setStyle('Fusion')
340
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
341
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
+
342
1469
  sql_shell.show()
343
1470
  sys.exit(app.exec())
344
1471