sqlshell 0.2.3__py3-none-any.whl → 0.3.0__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/__init__.py +34 -4
- sqlshell/db/__init__.py +2 -1
- sqlshell/db/database_manager.py +336 -23
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/main.py +570 -140
- sqlshell/query_tab.py +592 -7
- sqlshell/ui/filter_header.py +22 -1
- sqlshell/utils/profile_column.py +1586 -170
- sqlshell/utils/profile_foreign_keys.py +103 -11
- sqlshell/utils/profile_ohe.py +631 -0
- {sqlshell-0.2.3.dist-info → sqlshell-0.3.0.dist-info}/METADATA +126 -7
- {sqlshell-0.2.3.dist-info → sqlshell-0.3.0.dist-info}/RECORD +17 -13
- {sqlshell-0.2.3.dist-info → sqlshell-0.3.0.dist-info}/WHEEL +1 -1
- {sqlshell-0.2.3.dist-info → sqlshell-0.3.0.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.2.3.dist-info → sqlshell-0.3.0.dist-info}/top_level.txt +0 -0
sqlshell/query_tab.py
CHANGED
|
@@ -2,13 +2,17 @@ import os
|
|
|
2
2
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
3
3
|
QPushButton, QFrame, QHeaderView, QTableWidget, QSplitter, QApplication,
|
|
4
4
|
QToolButton, QMenu)
|
|
5
|
-
from PyQt6.QtCore import Qt
|
|
5
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
6
6
|
from PyQt6.QtGui import QIcon
|
|
7
|
+
import re
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
7
10
|
|
|
8
11
|
from sqlshell.editor import SQLEditor
|
|
9
12
|
from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
|
|
10
13
|
from sqlshell.ui import FilterHeader
|
|
11
14
|
from sqlshell.styles import get_row_count_label_stylesheet
|
|
15
|
+
from sqlshell.editor_integration import integrate_execution_functionality
|
|
12
16
|
|
|
13
17
|
class QueryTab(QWidget):
|
|
14
18
|
def __init__(self, parent, results_title="RESULTS"):
|
|
@@ -43,6 +47,12 @@ class QueryTab(QWidget):
|
|
|
43
47
|
# Apply syntax highlighting to the query editor
|
|
44
48
|
self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
|
|
45
49
|
|
|
50
|
+
# Integrate F5/F9 execution functionality
|
|
51
|
+
self.execution_integration = integrate_execution_functionality(
|
|
52
|
+
self.query_edit,
|
|
53
|
+
self._execute_query_callback
|
|
54
|
+
)
|
|
55
|
+
|
|
46
56
|
# Ensure a default completer is available
|
|
47
57
|
if not self.query_edit.completer:
|
|
48
58
|
from PyQt6.QtCore import QStringListModel
|
|
@@ -69,10 +79,21 @@ class QueryTab(QWidget):
|
|
|
69
79
|
self.execute_btn.setIcon(QIcon.fromTheme("media-playback-start"))
|
|
70
80
|
self.execute_btn.clicked.connect(self.execute_query)
|
|
71
81
|
|
|
82
|
+
# Add F5/F9 buttons for clarity
|
|
83
|
+
self.execute_all_btn = QPushButton('F5 - Execute All')
|
|
84
|
+
self.execute_all_btn.setToolTip('Execute all statements (F5)')
|
|
85
|
+
self.execute_all_btn.clicked.connect(self.execute_all_statements)
|
|
86
|
+
|
|
87
|
+
self.execute_current_btn = QPushButton('F9 - Execute Current')
|
|
88
|
+
self.execute_current_btn.setToolTip('Execute current statement (F9)')
|
|
89
|
+
self.execute_current_btn.clicked.connect(self.execute_current_statement)
|
|
90
|
+
|
|
72
91
|
self.clear_btn = QPushButton('Clear')
|
|
73
92
|
self.clear_btn.clicked.connect(self.clear_query)
|
|
74
93
|
|
|
75
94
|
button_layout.addWidget(self.execute_btn)
|
|
95
|
+
button_layout.addWidget(self.execute_all_btn)
|
|
96
|
+
button_layout.addWidget(self.execute_current_btn)
|
|
76
97
|
button_layout.addWidget(self.clear_btn)
|
|
77
98
|
button_layout.addStretch()
|
|
78
99
|
|
|
@@ -91,9 +112,9 @@ class QueryTab(QWidget):
|
|
|
91
112
|
|
|
92
113
|
# Bottom part - Results section
|
|
93
114
|
results_widget = QWidget()
|
|
94
|
-
results_layout = QVBoxLayout(results_widget)
|
|
95
|
-
results_layout.setContentsMargins(16, 16, 16, 16)
|
|
96
|
-
results_layout.setSpacing(12)
|
|
115
|
+
self.results_layout = QVBoxLayout(results_widget)
|
|
116
|
+
self.results_layout.setContentsMargins(16, 16, 16, 16)
|
|
117
|
+
self.results_layout.setSpacing(12)
|
|
97
118
|
|
|
98
119
|
# Results header with row count
|
|
99
120
|
header_layout = QHBoxLayout()
|
|
@@ -107,7 +128,13 @@ class QueryTab(QWidget):
|
|
|
107
128
|
self.row_count_label.setStyleSheet(get_row_count_label_stylesheet())
|
|
108
129
|
header_layout.addWidget(self.row_count_label)
|
|
109
130
|
|
|
110
|
-
results_layout.addLayout(header_layout)
|
|
131
|
+
self.results_layout.addLayout(header_layout)
|
|
132
|
+
|
|
133
|
+
# Add descriptive text about table interactions and new F5/F9 functionality
|
|
134
|
+
help_text = QLabel("📊 <b>Table Interactions:</b> Double-click on a column header to add it to your query. Right-click for analytical capabilities. <b>🚀 Execution:</b> F5 executes all statements, F9 executes current statement (at cursor), Ctrl+Enter executes entire query.")
|
|
135
|
+
help_text.setWordWrap(True)
|
|
136
|
+
help_text.setStyleSheet("color: #7FB3D5; font-size: 11px; margin: 5px 0; padding: 8px; background-color: #F8F9FA; border-radius: 4px;")
|
|
137
|
+
self.results_layout.addWidget(help_text)
|
|
111
138
|
|
|
112
139
|
# Results table with customized header
|
|
113
140
|
self.results_table = QTableWidget()
|
|
@@ -125,7 +152,16 @@ class QueryTab(QWidget):
|
|
|
125
152
|
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
126
153
|
self.results_table.verticalHeader().setVisible(True)
|
|
127
154
|
|
|
128
|
-
|
|
155
|
+
# Connect double-click signal to handle column selection
|
|
156
|
+
self.results_table.cellDoubleClicked.connect(self.handle_cell_double_click)
|
|
157
|
+
|
|
158
|
+
# Connect header click signal to handle column header selection
|
|
159
|
+
self.results_table.horizontalHeader().sectionClicked.connect(self.handle_header_click)
|
|
160
|
+
|
|
161
|
+
# Connect header double-click signal to add column to query
|
|
162
|
+
self.results_table.horizontalHeader().sectionDoubleClicked.connect(self.handle_header_double_click)
|
|
163
|
+
|
|
164
|
+
self.results_layout.addWidget(self.results_table)
|
|
129
165
|
|
|
130
166
|
# Add widgets to splitter
|
|
131
167
|
self.splitter.addWidget(query_widget)
|
|
@@ -198,4 +234,553 @@ class QueryTab(QWidget):
|
|
|
198
234
|
return True
|
|
199
235
|
|
|
200
236
|
# Default - let the event propagate normally
|
|
201
|
-
return super().eventFilter(obj, event)
|
|
237
|
+
return super().eventFilter(obj, event)
|
|
238
|
+
|
|
239
|
+
def format_sql(self):
|
|
240
|
+
"""Format the SQL query for better readability"""
|
|
241
|
+
from sqlshell.utils.sql_formatter import format_sql
|
|
242
|
+
|
|
243
|
+
# Get current text
|
|
244
|
+
current_text = self.query_edit.toPlainText()
|
|
245
|
+
if not current_text.strip():
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# Format the SQL
|
|
250
|
+
formatted_sql = format_sql(current_text)
|
|
251
|
+
|
|
252
|
+
# Replace the text
|
|
253
|
+
self.query_edit.setPlainText(formatted_sql)
|
|
254
|
+
self.parent.statusBar().showMessage('SQL formatted successfully')
|
|
255
|
+
except Exception as e:
|
|
256
|
+
self.parent.statusBar().showMessage(f'Error formatting SQL: {str(e)}')
|
|
257
|
+
|
|
258
|
+
def show_header_context_menu(self, position):
|
|
259
|
+
"""Show context menu for header columns"""
|
|
260
|
+
# Get the column index
|
|
261
|
+
idx = self.results_table.horizontalHeader().logicalIndexAt(position)
|
|
262
|
+
if idx < 0:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Create context menu
|
|
266
|
+
menu = QMenu(self)
|
|
267
|
+
header = self.results_table.horizontalHeader()
|
|
268
|
+
|
|
269
|
+
# Get column name
|
|
270
|
+
col_name = self.results_table.horizontalHeaderItem(idx).text()
|
|
271
|
+
|
|
272
|
+
# Check if the column name needs quoting (contains spaces or special characters)
|
|
273
|
+
quoted_col_name = col_name
|
|
274
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
275
|
+
quoted_col_name = f'"{col_name}"'
|
|
276
|
+
|
|
277
|
+
# Add actions
|
|
278
|
+
copy_col_name_action = menu.addAction(f"Copy '{col_name}'")
|
|
279
|
+
menu.addSeparator()
|
|
280
|
+
|
|
281
|
+
# Check if we have a FilterHeader
|
|
282
|
+
if isinstance(header, FilterHeader):
|
|
283
|
+
# Check if this column has a bar chart
|
|
284
|
+
has_bar = idx in header.columns_with_bars
|
|
285
|
+
|
|
286
|
+
# Add toggle bar chart action
|
|
287
|
+
if not has_bar:
|
|
288
|
+
bar_action = menu.addAction("Add Bar Chart")
|
|
289
|
+
else:
|
|
290
|
+
bar_action = menu.addAction("Remove Bar Chart")
|
|
291
|
+
|
|
292
|
+
# Sort options
|
|
293
|
+
menu.addSeparator()
|
|
294
|
+
|
|
295
|
+
sort_asc_action = menu.addAction("Sort Ascending")
|
|
296
|
+
sort_desc_action = menu.addAction("Sort Descending")
|
|
297
|
+
|
|
298
|
+
# Filter options if we have data
|
|
299
|
+
if self.results_table.rowCount() > 0:
|
|
300
|
+
menu.addSeparator()
|
|
301
|
+
sel_distinct_action = menu.addAction(f"SELECT DISTINCT {quoted_col_name}")
|
|
302
|
+
count_distinct_action = menu.addAction(f"COUNT DISTINCT {quoted_col_name}")
|
|
303
|
+
group_by_action = menu.addAction(f"GROUP BY {quoted_col_name}")
|
|
304
|
+
|
|
305
|
+
# SQL generation submenu
|
|
306
|
+
menu.addSeparator()
|
|
307
|
+
sql_menu = menu.addMenu("Generate SQL")
|
|
308
|
+
select_col_action = sql_menu.addAction(f"SELECT {quoted_col_name}")
|
|
309
|
+
filter_col_action = sql_menu.addAction(f"WHERE {quoted_col_name} = ?")
|
|
310
|
+
explain_action = menu.addAction(f"Explain Column")
|
|
311
|
+
encode_action = menu.addAction(f"One-Hot Encode")
|
|
312
|
+
|
|
313
|
+
# Execute the menu
|
|
314
|
+
action = menu.exec(header.mapToGlobal(position))
|
|
315
|
+
|
|
316
|
+
# Handle actions
|
|
317
|
+
if action == copy_col_name_action:
|
|
318
|
+
QApplication.clipboard().setText(col_name)
|
|
319
|
+
self.parent.statusBar().showMessage(f"Copied '{col_name}' to clipboard")
|
|
320
|
+
|
|
321
|
+
elif action == explain_action:
|
|
322
|
+
# Call the explain column method on the parent
|
|
323
|
+
if hasattr(self.parent, 'explain_column'):
|
|
324
|
+
self.parent.explain_column(col_name)
|
|
325
|
+
|
|
326
|
+
elif action == encode_action:
|
|
327
|
+
# Call the encode text method on the parent
|
|
328
|
+
if hasattr(self.parent, 'encode_text'):
|
|
329
|
+
self.parent.encode_text(col_name)
|
|
330
|
+
|
|
331
|
+
elif action == sort_asc_action:
|
|
332
|
+
self.results_table.sortItems(idx, Qt.SortOrder.AscendingOrder)
|
|
333
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (ascending)")
|
|
334
|
+
|
|
335
|
+
elif action == sort_desc_action:
|
|
336
|
+
self.results_table.sortItems(idx, Qt.SortOrder.DescendingOrder)
|
|
337
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (descending)")
|
|
338
|
+
|
|
339
|
+
elif isinstance(header, FilterHeader) and action == bar_action:
|
|
340
|
+
# Toggle bar chart
|
|
341
|
+
header.toggle_bar_chart(idx)
|
|
342
|
+
if idx in header.columns_with_bars:
|
|
343
|
+
self.parent.statusBar().showMessage(f"Added bar chart for '{col_name}'")
|
|
344
|
+
else:
|
|
345
|
+
self.parent.statusBar().showMessage(f"Removed bar chart for '{col_name}'")
|
|
346
|
+
|
|
347
|
+
elif 'sel_distinct_action' in locals() and action == sel_distinct_action:
|
|
348
|
+
new_query = f"SELECT DISTINCT {quoted_col_name}\nFROM "
|
|
349
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
350
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
351
|
+
new_query += f"{table_name}\n"
|
|
352
|
+
else:
|
|
353
|
+
new_query += "[table_name]\n"
|
|
354
|
+
new_query += "ORDER BY 1"
|
|
355
|
+
self.set_query_text(new_query)
|
|
356
|
+
self.parent.statusBar().showMessage(f"Created SELECT DISTINCT query for '{col_name}'")
|
|
357
|
+
|
|
358
|
+
elif 'count_distinct_action' in locals() and action == count_distinct_action:
|
|
359
|
+
new_query = f"SELECT COUNT(DISTINCT {quoted_col_name}) AS distinct_{col_name.replace(' ', '_')}\nFROM "
|
|
360
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
361
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
362
|
+
new_query += f"{table_name}"
|
|
363
|
+
else:
|
|
364
|
+
new_query += "[table_name]"
|
|
365
|
+
self.set_query_text(new_query)
|
|
366
|
+
self.parent.statusBar().showMessage(f"Created COUNT DISTINCT query for '{col_name}'")
|
|
367
|
+
|
|
368
|
+
elif 'group_by_action' in locals() and action == group_by_action:
|
|
369
|
+
new_query = f"SELECT {quoted_col_name}, COUNT(*) AS count\nFROM "
|
|
370
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
371
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
372
|
+
new_query += f"{table_name}"
|
|
373
|
+
else:
|
|
374
|
+
new_query += "[table_name]"
|
|
375
|
+
new_query += f"\nGROUP BY {quoted_col_name}\nORDER BY count DESC"
|
|
376
|
+
self.set_query_text(new_query)
|
|
377
|
+
self.parent.statusBar().showMessage(f"Created GROUP BY query for '{col_name}'")
|
|
378
|
+
|
|
379
|
+
elif action == select_col_action:
|
|
380
|
+
new_query = f"SELECT {quoted_col_name}\nFROM "
|
|
381
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
382
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
383
|
+
new_query += f"{table_name}"
|
|
384
|
+
else:
|
|
385
|
+
new_query += "[table_name]"
|
|
386
|
+
self.set_query_text(new_query)
|
|
387
|
+
self.parent.statusBar().showMessage(f"Created SELECT query for '{col_name}'")
|
|
388
|
+
|
|
389
|
+
elif action == filter_col_action:
|
|
390
|
+
current_text = self.get_query_text()
|
|
391
|
+
if current_text and "WHERE" in current_text.upper():
|
|
392
|
+
# Add as AND condition
|
|
393
|
+
lines = current_text.splitlines()
|
|
394
|
+
for i, line in enumerate(lines):
|
|
395
|
+
if "WHERE" in line.upper() and "ORDER BY" not in line.upper() and "GROUP BY" not in line.upper():
|
|
396
|
+
lines[i] = f"{line} AND {quoted_col_name} = ?"
|
|
397
|
+
break
|
|
398
|
+
self.set_query_text("\n".join(lines))
|
|
399
|
+
else:
|
|
400
|
+
# Create new query with WHERE clause
|
|
401
|
+
new_query = f"SELECT *\nFROM "
|
|
402
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
403
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
404
|
+
new_query += f"{table_name}"
|
|
405
|
+
else:
|
|
406
|
+
new_query += "[table_name]"
|
|
407
|
+
new_query += f"\nWHERE {quoted_col_name} = ?"
|
|
408
|
+
self.set_query_text(new_query)
|
|
409
|
+
self.parent.statusBar().showMessage(f"Added filter condition for '{col_name}'")
|
|
410
|
+
|
|
411
|
+
def handle_cell_double_click(self, row, column):
|
|
412
|
+
"""Handle double-click on a cell to add column to query editor"""
|
|
413
|
+
# Get column name
|
|
414
|
+
col_name = self.results_table.horizontalHeaderItem(column).text()
|
|
415
|
+
|
|
416
|
+
# Check if the column name needs quoting (contains spaces or special characters)
|
|
417
|
+
quoted_col_name = col_name
|
|
418
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
419
|
+
quoted_col_name = f'"{col_name}"'
|
|
420
|
+
|
|
421
|
+
# Get current query text
|
|
422
|
+
current_text = self.get_query_text().strip()
|
|
423
|
+
|
|
424
|
+
# Get cursor position
|
|
425
|
+
cursor = self.query_edit.textCursor()
|
|
426
|
+
cursor_position = cursor.position()
|
|
427
|
+
|
|
428
|
+
# Check if we already have an existing query
|
|
429
|
+
if current_text:
|
|
430
|
+
# If there's existing text, try to insert at cursor position
|
|
431
|
+
if cursor_position > 0:
|
|
432
|
+
# Check if we need to add a comma before the column name
|
|
433
|
+
text_before_cursor = self.query_edit.toPlainText()[:cursor_position]
|
|
434
|
+
text_after_cursor = self.query_edit.toPlainText()[cursor_position:]
|
|
435
|
+
|
|
436
|
+
# Add comma if needed (we're in a list of columns)
|
|
437
|
+
needs_comma = (not text_before_cursor.strip().endswith(',') and
|
|
438
|
+
not text_before_cursor.strip().endswith('(') and
|
|
439
|
+
not text_before_cursor.strip().endswith('SELECT') and
|
|
440
|
+
not re.search(r'\bFROM\s*$', text_before_cursor) and
|
|
441
|
+
not re.search(r'\bWHERE\s*$', text_before_cursor) and
|
|
442
|
+
not re.search(r'\bGROUP\s+BY\s*$', text_before_cursor) and
|
|
443
|
+
not re.search(r'\bORDER\s+BY\s*$', text_before_cursor) and
|
|
444
|
+
not re.search(r'\bHAVING\s*$', text_before_cursor) and
|
|
445
|
+
not text_after_cursor.strip().startswith(','))
|
|
446
|
+
|
|
447
|
+
# Insert with comma if needed
|
|
448
|
+
if needs_comma:
|
|
449
|
+
cursor.insertText(f", {quoted_col_name}")
|
|
450
|
+
else:
|
|
451
|
+
cursor.insertText(quoted_col_name)
|
|
452
|
+
|
|
453
|
+
self.query_edit.setTextCursor(cursor)
|
|
454
|
+
self.query_edit.setFocus()
|
|
455
|
+
self.parent.statusBar().showMessage(f"Inserted '{col_name}' at cursor position")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# If cursor is at start, check if we have a SELECT query to modify
|
|
459
|
+
if current_text.upper().startswith("SELECT"):
|
|
460
|
+
# Try to find the SELECT clause
|
|
461
|
+
select_match = re.match(r'(?i)SELECT\s+(.*?)(?:\sFROM\s|$)', current_text)
|
|
462
|
+
if select_match:
|
|
463
|
+
select_clause = select_match.group(1).strip()
|
|
464
|
+
|
|
465
|
+
# If it's "SELECT *", replace it with the column name
|
|
466
|
+
if select_clause == "*":
|
|
467
|
+
modified_text = current_text.replace("SELECT *", f"SELECT {quoted_col_name}")
|
|
468
|
+
self.set_query_text(modified_text)
|
|
469
|
+
# Otherwise append the column if it's not already there
|
|
470
|
+
elif quoted_col_name not in select_clause:
|
|
471
|
+
modified_text = current_text.replace(select_clause, f"{select_clause}, {quoted_col_name}")
|
|
472
|
+
self.set_query_text(modified_text)
|
|
473
|
+
|
|
474
|
+
self.query_edit.setFocus()
|
|
475
|
+
self.parent.statusBar().showMessage(f"Added '{col_name}' to SELECT clause")
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
# If we can't modify an existing SELECT clause, append to the end
|
|
479
|
+
# Go to the end of the document
|
|
480
|
+
cursor.movePosition(cursor.MoveOperation.End)
|
|
481
|
+
# Insert a new line if needed
|
|
482
|
+
if not current_text.endswith('\n'):
|
|
483
|
+
cursor.insertText('\n')
|
|
484
|
+
# Insert a simple column reference
|
|
485
|
+
cursor.insertText(quoted_col_name)
|
|
486
|
+
self.query_edit.setTextCursor(cursor)
|
|
487
|
+
self.query_edit.setFocus()
|
|
488
|
+
self.parent.statusBar().showMessage(f"Appended '{col_name}' to query")
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
# If we don't have an existing query or couldn't modify it, create a new one
|
|
492
|
+
table_name = self._get_table_name(current_text)
|
|
493
|
+
new_query = f"SELECT {quoted_col_name}\nFROM {table_name}"
|
|
494
|
+
self.set_query_text(new_query)
|
|
495
|
+
self.query_edit.setFocus()
|
|
496
|
+
self.parent.statusBar().showMessage(f"Created new SELECT query for '{col_name}'")
|
|
497
|
+
|
|
498
|
+
def handle_header_click(self, idx):
|
|
499
|
+
"""Handle a click on a column header"""
|
|
500
|
+
# Store the column index and delay showing the context menu to allow for double-clicks
|
|
501
|
+
|
|
502
|
+
# Store the current index and time for processing
|
|
503
|
+
self._last_header_click_idx = idx
|
|
504
|
+
|
|
505
|
+
# Create a timer to show the context menu after a short delay
|
|
506
|
+
# This ensures we don't interfere with double-click detection
|
|
507
|
+
timer = QTimer()
|
|
508
|
+
timer.setSingleShot(True)
|
|
509
|
+
timer.timeout.connect(lambda: self._show_header_context_menu(idx))
|
|
510
|
+
timer.start(200) # 200ms delay
|
|
511
|
+
|
|
512
|
+
def _show_header_context_menu(self, idx):
|
|
513
|
+
"""Show context menu for column header after delay"""
|
|
514
|
+
# Get the header
|
|
515
|
+
header = self.results_table.horizontalHeader()
|
|
516
|
+
if not header:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
# Get the column name
|
|
520
|
+
if not hasattr(self, 'current_df') or self.current_df is None:
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
if idx >= len(self.current_df.columns):
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
# Get column name
|
|
527
|
+
col_name = self.current_df.columns[idx]
|
|
528
|
+
|
|
529
|
+
# Check if column name needs quoting (contains spaces or special chars)
|
|
530
|
+
quoted_col_name = col_name
|
|
531
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
532
|
+
quoted_col_name = f'"{col_name}"'
|
|
533
|
+
|
|
534
|
+
# Get the position for the context menu (at the header cell)
|
|
535
|
+
position = header.mapToGlobal(header.rect().bottomLeft())
|
|
536
|
+
|
|
537
|
+
# Create the context menu
|
|
538
|
+
menu = QMenu()
|
|
539
|
+
col_header_action = menu.addAction(f"Column: {col_name}")
|
|
540
|
+
col_header_action.setEnabled(False)
|
|
541
|
+
menu.addSeparator()
|
|
542
|
+
|
|
543
|
+
# Add copy action
|
|
544
|
+
copy_col_name_action = menu.addAction("Copy Column Name")
|
|
545
|
+
|
|
546
|
+
# Add sorting actions
|
|
547
|
+
sort_menu = menu.addMenu("Sort")
|
|
548
|
+
sort_asc_action = sort_menu.addAction("Sort Ascending")
|
|
549
|
+
sort_desc_action = sort_menu.addAction("Sort Descending")
|
|
550
|
+
|
|
551
|
+
# Add bar chart toggle if numeric column
|
|
552
|
+
bar_action = None
|
|
553
|
+
if isinstance(header, FilterHeader):
|
|
554
|
+
is_numeric = False
|
|
555
|
+
try:
|
|
556
|
+
# Check if first non-null value is numeric
|
|
557
|
+
for i in range(min(100, len(self.current_df))):
|
|
558
|
+
if pd.notna(self.current_df.iloc[i, idx]):
|
|
559
|
+
val = self.current_df.iloc[i, idx]
|
|
560
|
+
if isinstance(val, (int, float, np.number)):
|
|
561
|
+
is_numeric = True
|
|
562
|
+
break
|
|
563
|
+
except:
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
if is_numeric:
|
|
567
|
+
menu.addSeparator()
|
|
568
|
+
if idx in header.columns_with_bars:
|
|
569
|
+
bar_action = menu.addAction("Remove Bar Chart")
|
|
570
|
+
else:
|
|
571
|
+
bar_action = menu.addAction("Add Bar Chart")
|
|
572
|
+
|
|
573
|
+
sql_menu = menu.addMenu("Generate SQL")
|
|
574
|
+
select_col_action = sql_menu.addAction(f"SELECT {quoted_col_name}")
|
|
575
|
+
filter_col_action = sql_menu.addAction(f"WHERE {quoted_col_name} = ?")
|
|
576
|
+
explain_action = menu.addAction(f"Explain Column")
|
|
577
|
+
encode_action = menu.addAction(f"One-Hot Encode")
|
|
578
|
+
|
|
579
|
+
# Execute the menu
|
|
580
|
+
action = menu.exec(position)
|
|
581
|
+
|
|
582
|
+
# Handle actions
|
|
583
|
+
if action == copy_col_name_action:
|
|
584
|
+
QApplication.clipboard().setText(col_name)
|
|
585
|
+
self.parent.statusBar().showMessage(f"Copied '{col_name}' to clipboard")
|
|
586
|
+
|
|
587
|
+
elif action == explain_action:
|
|
588
|
+
# Call the explain column method on the parent
|
|
589
|
+
if hasattr(self.parent, 'explain_column'):
|
|
590
|
+
self.parent.explain_column(col_name)
|
|
591
|
+
|
|
592
|
+
elif action == encode_action:
|
|
593
|
+
# Call the encode text method on the parent
|
|
594
|
+
if hasattr(self.parent, 'encode_text'):
|
|
595
|
+
self.parent.encode_text(col_name)
|
|
596
|
+
|
|
597
|
+
elif action == sort_asc_action:
|
|
598
|
+
self.results_table.sortItems(idx, Qt.SortOrder.AscendingOrder)
|
|
599
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (ascending)")
|
|
600
|
+
|
|
601
|
+
elif action == sort_desc_action:
|
|
602
|
+
self.results_table.sortItems(idx, Qt.SortOrder.DescendingOrder)
|
|
603
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (descending)")
|
|
604
|
+
|
|
605
|
+
elif isinstance(header, FilterHeader) and action == bar_action:
|
|
606
|
+
# Toggle bar chart
|
|
607
|
+
header.toggle_bar_chart(idx)
|
|
608
|
+
if idx in header.columns_with_bars:
|
|
609
|
+
self.parent.statusBar().showMessage(f"Added bar chart for '{col_name}'")
|
|
610
|
+
else:
|
|
611
|
+
self.parent.statusBar().showMessage(f"Removed bar chart for '{col_name}'")
|
|
612
|
+
|
|
613
|
+
elif action == select_col_action:
|
|
614
|
+
# Insert SQL snippet at cursor position in query editor
|
|
615
|
+
if hasattr(self, 'query_edit'):
|
|
616
|
+
cursor = self.query_edit.textCursor()
|
|
617
|
+
cursor.insertText(f"SELECT {quoted_col_name}")
|
|
618
|
+
self.query_edit.setFocus()
|
|
619
|
+
|
|
620
|
+
elif action == filter_col_action:
|
|
621
|
+
# Insert SQL snippet at cursor position in query editor
|
|
622
|
+
if hasattr(self, 'query_edit'):
|
|
623
|
+
cursor = self.query_edit.textCursor()
|
|
624
|
+
cursor.insertText(f"WHERE {quoted_col_name} = ")
|
|
625
|
+
self.query_edit.setFocus()
|
|
626
|
+
|
|
627
|
+
def handle_header_double_click(self, idx):
|
|
628
|
+
"""Handle double-click on a column header to add it to the query editor"""
|
|
629
|
+
# Get column name
|
|
630
|
+
if not hasattr(self, 'current_df') or self.current_df is None:
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
if idx >= len(self.current_df.columns):
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
# Get column name
|
|
637
|
+
col_name = self.current_df.columns[idx]
|
|
638
|
+
|
|
639
|
+
# Check if column name needs quoting (contains spaces or special chars)
|
|
640
|
+
quoted_col_name = col_name
|
|
641
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
642
|
+
quoted_col_name = f'"{col_name}"'
|
|
643
|
+
|
|
644
|
+
# Get current query text
|
|
645
|
+
current_text = self.get_query_text().strip()
|
|
646
|
+
|
|
647
|
+
# Get cursor position
|
|
648
|
+
cursor = self.query_edit.textCursor()
|
|
649
|
+
cursor_position = cursor.position()
|
|
650
|
+
|
|
651
|
+
# Check if we already have an existing query
|
|
652
|
+
if current_text:
|
|
653
|
+
# If there's existing text, try to insert at cursor position
|
|
654
|
+
if cursor_position > 0:
|
|
655
|
+
# Check if we need to add a comma before the column name
|
|
656
|
+
text_before_cursor = self.query_edit.toPlainText()[:cursor_position]
|
|
657
|
+
text_after_cursor = self.query_edit.toPlainText()[cursor_position:]
|
|
658
|
+
|
|
659
|
+
# Add comma if needed (we're in a list of columns)
|
|
660
|
+
needs_comma = (not text_before_cursor.strip().endswith(',') and
|
|
661
|
+
not text_before_cursor.strip().endswith('(') and
|
|
662
|
+
not text_before_cursor.strip().endswith('SELECT') and
|
|
663
|
+
not re.search(r'\bFROM\s*$', text_before_cursor) and
|
|
664
|
+
not re.search(r'\bWHERE\s*$', text_before_cursor) and
|
|
665
|
+
not re.search(r'\bGROUP\s+BY\s*$', text_before_cursor) and
|
|
666
|
+
not re.search(r'\bORDER\s+BY\s*$', text_before_cursor) and
|
|
667
|
+
not re.search(r'\bHAVING\s*$', text_before_cursor) and
|
|
668
|
+
not text_after_cursor.strip().startswith(','))
|
|
669
|
+
|
|
670
|
+
# Insert with comma if needed
|
|
671
|
+
if needs_comma:
|
|
672
|
+
cursor.insertText(f", {quoted_col_name}")
|
|
673
|
+
else:
|
|
674
|
+
cursor.insertText(quoted_col_name)
|
|
675
|
+
|
|
676
|
+
self.query_edit.setTextCursor(cursor)
|
|
677
|
+
self.query_edit.setFocus()
|
|
678
|
+
self.parent.statusBar().showMessage(f"Inserted '{col_name}' at cursor position")
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
# If cursor is at start, check if we have a SELECT query to modify
|
|
682
|
+
if current_text.upper().startswith("SELECT"):
|
|
683
|
+
# Try to find the SELECT clause
|
|
684
|
+
select_match = re.match(r'(?i)SELECT\s+(.*?)(?:\sFROM\s|$)', current_text)
|
|
685
|
+
if select_match:
|
|
686
|
+
select_clause = select_match.group(1).strip()
|
|
687
|
+
|
|
688
|
+
# If it's "SELECT *", replace it with the column name
|
|
689
|
+
if select_clause == "*":
|
|
690
|
+
modified_text = current_text.replace("SELECT *", f"SELECT {quoted_col_name}")
|
|
691
|
+
self.set_query_text(modified_text)
|
|
692
|
+
# Otherwise append the column if it's not already there
|
|
693
|
+
elif quoted_col_name not in select_clause:
|
|
694
|
+
modified_text = current_text.replace(select_clause, f"{select_clause}, {quoted_col_name}")
|
|
695
|
+
self.set_query_text(modified_text)
|
|
696
|
+
|
|
697
|
+
self.query_edit.setFocus()
|
|
698
|
+
self.parent.statusBar().showMessage(f"Added '{col_name}' to SELECT clause")
|
|
699
|
+
return
|
|
700
|
+
|
|
701
|
+
# If we can't modify an existing SELECT clause, append to the end
|
|
702
|
+
# Go to the end of the document
|
|
703
|
+
cursor.movePosition(cursor.MoveOperation.End)
|
|
704
|
+
# Insert a new line if needed
|
|
705
|
+
if not current_text.endswith('\n'):
|
|
706
|
+
cursor.insertText('\n')
|
|
707
|
+
# Insert a simple column reference
|
|
708
|
+
cursor.insertText(quoted_col_name)
|
|
709
|
+
self.query_edit.setTextCursor(cursor)
|
|
710
|
+
self.query_edit.setFocus()
|
|
711
|
+
self.parent.statusBar().showMessage(f"Appended '{col_name}' to query")
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
# If we don't have an existing query or couldn't modify it, create a new one
|
|
715
|
+
table_name = self._get_table_name(current_text)
|
|
716
|
+
new_query = f"SELECT {quoted_col_name}\nFROM {table_name}"
|
|
717
|
+
self.set_query_text(new_query)
|
|
718
|
+
self.query_edit.setFocus()
|
|
719
|
+
self.parent.statusBar().showMessage(f"Created new SELECT query for '{col_name}'")
|
|
720
|
+
|
|
721
|
+
def _get_table_name(self, current_text):
|
|
722
|
+
"""Extract table name from current query or DataFrame, with fallbacks"""
|
|
723
|
+
# First, try to get the currently selected table in the UI
|
|
724
|
+
if self.parent and hasattr(self.parent, 'get_selected_table'):
|
|
725
|
+
selected_table = self.parent.get_selected_table()
|
|
726
|
+
if selected_table:
|
|
727
|
+
return selected_table
|
|
728
|
+
|
|
729
|
+
# Try to extract table name from the current DataFrame
|
|
730
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
731
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
732
|
+
if table_name:
|
|
733
|
+
return table_name
|
|
734
|
+
|
|
735
|
+
# Try to extract the table name from the current query
|
|
736
|
+
if current_text:
|
|
737
|
+
# Look for FROM clause
|
|
738
|
+
from_match = re.search(r'(?i)FROM\s+([a-zA-Z0-9_."]+(?:\s*,\s*[a-zA-Z0-9_."]+)*)', current_text)
|
|
739
|
+
if from_match:
|
|
740
|
+
# Get the last table in the FROM clause (could be multiple tables joined)
|
|
741
|
+
tables = from_match.group(1).split(',')
|
|
742
|
+
last_table = tables[-1].strip()
|
|
743
|
+
|
|
744
|
+
# Remove any alias
|
|
745
|
+
last_table = re.sub(r'(?i)\s+as\s+\w+$', '', last_table)
|
|
746
|
+
last_table = re.sub(r'\s+\w+$', '', last_table)
|
|
747
|
+
|
|
748
|
+
# Remove any quotes
|
|
749
|
+
last_table = last_table.strip('"\'`[]')
|
|
750
|
+
|
|
751
|
+
return last_table
|
|
752
|
+
|
|
753
|
+
# If all else fails, return placeholder
|
|
754
|
+
return "[table_name]"
|
|
755
|
+
|
|
756
|
+
def _execute_query_callback(self, query_text):
|
|
757
|
+
"""Callback function for the execution handler to execute a single query."""
|
|
758
|
+
# This is called by the execution handler when F5/F9 is pressed
|
|
759
|
+
if hasattr(self.parent, 'execute_specific_query'):
|
|
760
|
+
self.parent.execute_specific_query(query_text)
|
|
761
|
+
else:
|
|
762
|
+
# Fallback: execute using the standard method
|
|
763
|
+
original_text = self.query_edit.toPlainText()
|
|
764
|
+
cursor_pos = self.query_edit.textCursor().position() # Save current cursor position
|
|
765
|
+
self.query_edit.setPlainText(query_text)
|
|
766
|
+
if hasattr(self.parent, 'execute_query'):
|
|
767
|
+
self.parent.execute_query()
|
|
768
|
+
self.query_edit.setPlainText(original_text)
|
|
769
|
+
# Restore cursor position (as close as possible)
|
|
770
|
+
doc_length = len(self.query_edit.toPlainText())
|
|
771
|
+
restored_pos = min(cursor_pos, doc_length)
|
|
772
|
+
cursor = self.query_edit.textCursor()
|
|
773
|
+
cursor.setPosition(restored_pos)
|
|
774
|
+
self.query_edit.setTextCursor(cursor)
|
|
775
|
+
|
|
776
|
+
def execute_all_statements(self):
|
|
777
|
+
"""Execute all statements in the editor (F5 functionality)."""
|
|
778
|
+
if self.execution_integration:
|
|
779
|
+
return self.execution_integration.execute_all_statements()
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
def execute_current_statement(self):
|
|
783
|
+
"""Execute the current statement (F9 functionality)."""
|
|
784
|
+
if self.execution_integration:
|
|
785
|
+
return self.execution_integration.execute_current_statement()
|
|
786
|
+
return None
|