sqlshell 0.4.4__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.
- sqlshell/__init__.py +84 -0
- sqlshell/__main__.py +4926 -0
- sqlshell/ai_autocomplete.py +392 -0
- sqlshell/ai_settings_dialog.py +337 -0
- sqlshell/context_suggester.py +768 -0
- sqlshell/create_test_data.py +152 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/db/__init__.py +6 -0
- sqlshell/db/database_manager.py +1318 -0
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor.py +1166 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/menus.py +262 -0
- sqlshell/notification_manager.py +370 -0
- sqlshell/query_tab.py +904 -0
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/space_invaders.py +501 -0
- sqlshell/splash_screen.py +405 -0
- sqlshell/sqlshell/__init__.py +5 -0
- sqlshell/sqlshell/create_test_data.py +118 -0
- sqlshell/sqlshell/create_test_databases.py +96 -0
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +330 -0
- sqlshell/syntax_highlighter.py +124 -0
- sqlshell/table_list.py +996 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +469 -0
- sqlshell/utils/__init__.py +16 -0
- sqlshell/utils/profile_cn2.py +1661 -0
- sqlshell/utils/profile_column.py +2635 -0
- sqlshell/utils/profile_distributions.py +616 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_foreign_keys.py +779 -0
- sqlshell/utils/profile_keys.py +2834 -0
- sqlshell/utils/profile_ohe.py +934 -0
- sqlshell/utils/profile_ohe_advanced.py +754 -0
- sqlshell/utils/profile_ohe_comparison.py +237 -0
- sqlshell/utils/profile_prediction.py +926 -0
- sqlshell/utils/profile_similarity.py +876 -0
- sqlshell/utils/search_in_df.py +90 -0
- sqlshell/widgets.py +400 -0
- sqlshell-0.4.4.dist-info/METADATA +441 -0
- sqlshell-0.4.4.dist-info/RECORD +54 -0
- sqlshell-0.4.4.dist-info/WHEEL +5 -0
- sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
- sqlshell-0.4.4.dist-info/top_level.txt +1 -0
sqlshell/query_tab.py
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
3
|
+
QPushButton, QFrame, QHeaderView, QTableWidget, QSplitter, QApplication,
|
|
4
|
+
QToolButton, QMenu)
|
|
5
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
6
|
+
from PyQt6.QtGui import QIcon
|
|
7
|
+
import re
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from sqlshell.editor import SQLEditor
|
|
12
|
+
from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
|
|
13
|
+
from sqlshell.ui import FilterHeader
|
|
14
|
+
from sqlshell.styles import get_row_count_label_stylesheet
|
|
15
|
+
from sqlshell.editor_integration import integrate_execution_functionality
|
|
16
|
+
from sqlshell.widgets import CopyableTableWidget
|
|
17
|
+
|
|
18
|
+
class QueryTab(QWidget):
|
|
19
|
+
def __init__(self, parent, results_title="RESULTS"):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.parent = parent
|
|
22
|
+
self.current_df = None
|
|
23
|
+
self.filter_widgets = []
|
|
24
|
+
self.results_title_text = results_title
|
|
25
|
+
# Track preview mode - when True, tools should use full table data
|
|
26
|
+
self.is_preview_mode = False
|
|
27
|
+
self.preview_table_name = None # Name of table being previewed
|
|
28
|
+
self.init_ui()
|
|
29
|
+
|
|
30
|
+
def init_ui(self):
|
|
31
|
+
"""Initialize the tab's UI components"""
|
|
32
|
+
# Set main layout
|
|
33
|
+
main_layout = QVBoxLayout(self)
|
|
34
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
35
|
+
main_layout.setSpacing(0)
|
|
36
|
+
|
|
37
|
+
# Track compact mode state
|
|
38
|
+
self._compact_mode = False
|
|
39
|
+
|
|
40
|
+
# Create splitter for query and results
|
|
41
|
+
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
|
42
|
+
self.splitter.setHandleWidth(6)
|
|
43
|
+
self.splitter.setChildrenCollapsible(False)
|
|
44
|
+
|
|
45
|
+
# Top part - Query section
|
|
46
|
+
query_widget = QFrame()
|
|
47
|
+
query_widget.setObjectName("content_panel")
|
|
48
|
+
self.query_layout = QVBoxLayout(query_widget)
|
|
49
|
+
self.query_layout.setContentsMargins(8, 6, 8, 6)
|
|
50
|
+
self.query_layout.setSpacing(6)
|
|
51
|
+
|
|
52
|
+
# Query input
|
|
53
|
+
self.query_edit = SQLEditor()
|
|
54
|
+
# Apply syntax highlighting to the query editor
|
|
55
|
+
self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
|
|
56
|
+
|
|
57
|
+
# Integrate F5/F9 execution functionality
|
|
58
|
+
self.execution_integration = integrate_execution_functionality(
|
|
59
|
+
self.query_edit,
|
|
60
|
+
self._execute_query_callback
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Ensure a default completer is available
|
|
64
|
+
if not self.query_edit.completer:
|
|
65
|
+
from PyQt6.QtCore import QStringListModel
|
|
66
|
+
from PyQt6.QtWidgets import QCompleter
|
|
67
|
+
|
|
68
|
+
# Create a basic completer with SQL keywords if one doesn't exist
|
|
69
|
+
if hasattr(self.query_edit, 'all_sql_keywords'):
|
|
70
|
+
model = QStringListModel(self.query_edit.all_sql_keywords)
|
|
71
|
+
completer = QCompleter()
|
|
72
|
+
completer.setModel(model)
|
|
73
|
+
self.query_edit.set_completer(completer)
|
|
74
|
+
|
|
75
|
+
# Connect keyboard events for direct handling of Ctrl+Enter
|
|
76
|
+
self.query_edit.installEventFilter(self)
|
|
77
|
+
|
|
78
|
+
self.query_layout.addWidget(self.query_edit)
|
|
79
|
+
|
|
80
|
+
# Ultra-compact button row (22px height)
|
|
81
|
+
self.button_layout = QHBoxLayout()
|
|
82
|
+
self.button_layout.setSpacing(2)
|
|
83
|
+
self.button_layout.setContentsMargins(0, 0, 0, 0)
|
|
84
|
+
|
|
85
|
+
btn_style = "padding: 2px 8px; font-size: 11px;"
|
|
86
|
+
btn_height = 22
|
|
87
|
+
|
|
88
|
+
self.execute_btn = QPushButton('▶ Run')
|
|
89
|
+
self.execute_btn.setObjectName("primary_button")
|
|
90
|
+
self.execute_btn.setToolTip('Execute entire query (Ctrl+Enter)')
|
|
91
|
+
self.execute_btn.clicked.connect(self.execute_query)
|
|
92
|
+
self.execute_btn.setFixedHeight(btn_height)
|
|
93
|
+
self.execute_btn.setStyleSheet(btn_style)
|
|
94
|
+
|
|
95
|
+
# Compact F5/F9 buttons
|
|
96
|
+
self.execute_all_btn = QPushButton('F5')
|
|
97
|
+
self.execute_all_btn.setToolTip('Execute all statements (F5)')
|
|
98
|
+
self.execute_all_btn.clicked.connect(self.execute_all_statements)
|
|
99
|
+
self.execute_all_btn.setFixedHeight(btn_height)
|
|
100
|
+
self.execute_all_btn.setFixedWidth(32)
|
|
101
|
+
self.execute_all_btn.setStyleSheet(btn_style)
|
|
102
|
+
|
|
103
|
+
self.execute_current_btn = QPushButton('F9')
|
|
104
|
+
self.execute_current_btn.setToolTip('Execute current statement at cursor (F9)')
|
|
105
|
+
self.execute_current_btn.clicked.connect(self.execute_current_statement)
|
|
106
|
+
self.execute_current_btn.setFixedHeight(btn_height)
|
|
107
|
+
self.execute_current_btn.setFixedWidth(32)
|
|
108
|
+
self.execute_current_btn.setStyleSheet(btn_style)
|
|
109
|
+
|
|
110
|
+
self.clear_btn = QPushButton('Clear')
|
|
111
|
+
self.clear_btn.setToolTip('Clear query editor')
|
|
112
|
+
self.clear_btn.clicked.connect(self.clear_query)
|
|
113
|
+
self.clear_btn.setFixedHeight(btn_height)
|
|
114
|
+
self.clear_btn.setStyleSheet(btn_style)
|
|
115
|
+
|
|
116
|
+
self.button_layout.addWidget(self.execute_btn)
|
|
117
|
+
self.button_layout.addWidget(self.execute_all_btn)
|
|
118
|
+
self.button_layout.addWidget(self.execute_current_btn)
|
|
119
|
+
self.button_layout.addWidget(self.clear_btn)
|
|
120
|
+
self.button_layout.addStretch()
|
|
121
|
+
|
|
122
|
+
self.export_excel_btn = QPushButton('Excel')
|
|
123
|
+
self.export_excel_btn.setToolTip('Export results to Excel')
|
|
124
|
+
self.export_excel_btn.clicked.connect(self.export_to_excel)
|
|
125
|
+
self.export_excel_btn.setFixedHeight(btn_height)
|
|
126
|
+
self.export_excel_btn.setStyleSheet(btn_style)
|
|
127
|
+
|
|
128
|
+
self.export_parquet_btn = QPushButton('Parquet')
|
|
129
|
+
self.export_parquet_btn.setToolTip('Export results to Parquet')
|
|
130
|
+
self.export_parquet_btn.clicked.connect(self.export_to_parquet)
|
|
131
|
+
self.export_parquet_btn.setFixedHeight(btn_height)
|
|
132
|
+
self.export_parquet_btn.setStyleSheet(btn_style)
|
|
133
|
+
|
|
134
|
+
self.button_layout.addWidget(self.export_excel_btn)
|
|
135
|
+
self.button_layout.addWidget(self.export_parquet_btn)
|
|
136
|
+
|
|
137
|
+
self.query_layout.addLayout(self.button_layout)
|
|
138
|
+
|
|
139
|
+
# Bottom part - Results section with reduced padding
|
|
140
|
+
results_widget = QWidget()
|
|
141
|
+
self.results_layout = QVBoxLayout(results_widget)
|
|
142
|
+
self.results_layout.setContentsMargins(8, 4, 8, 4)
|
|
143
|
+
self.results_layout.setSpacing(4)
|
|
144
|
+
|
|
145
|
+
# Compact results header with row count and info button
|
|
146
|
+
header_layout = QHBoxLayout()
|
|
147
|
+
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
148
|
+
header_layout.setSpacing(6)
|
|
149
|
+
|
|
150
|
+
self.results_title = QLabel(self.results_title_text)
|
|
151
|
+
self.results_title.setObjectName("header_label")
|
|
152
|
+
self.results_title.setStyleSheet("font-size: 11px; font-weight: bold; color: #34495e;")
|
|
153
|
+
header_layout.addWidget(self.results_title)
|
|
154
|
+
|
|
155
|
+
# Compact info button with tooltip (replaces verbose help text)
|
|
156
|
+
self.help_info_btn = QToolButton()
|
|
157
|
+
self.help_info_btn.setText("ℹ")
|
|
158
|
+
self.help_info_btn.setToolTip(
|
|
159
|
+
"<b>Keyboard Shortcuts:</b><br>"
|
|
160
|
+
"• <b>Ctrl+Enter</b> - Execute entire query<br>"
|
|
161
|
+
"• <b>F5</b> - Execute all statements<br>"
|
|
162
|
+
"• <b>F9</b> - Execute current statement<br>"
|
|
163
|
+
"• <b>Ctrl+F</b> - Search in results<br>"
|
|
164
|
+
"• <b>Ctrl+C</b> - Copy selected data<br>"
|
|
165
|
+
"• <b>Ctrl+B</b> - Toggle sidebar<br>"
|
|
166
|
+
"• <b>Ctrl+Shift+C</b> - Compact mode<br><br>"
|
|
167
|
+
"<b>Table Interactions:</b><br>"
|
|
168
|
+
"• Double-click header → Add column to query<br>"
|
|
169
|
+
"• Right-click header → Analytical options"
|
|
170
|
+
)
|
|
171
|
+
self.help_info_btn.setStyleSheet("""
|
|
172
|
+
QToolButton {
|
|
173
|
+
border: none;
|
|
174
|
+
color: #3498db;
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
padding: 0 4px;
|
|
177
|
+
}
|
|
178
|
+
QToolButton:hover {
|
|
179
|
+
color: #2980b9;
|
|
180
|
+
background-color: #ecf0f1;
|
|
181
|
+
border-radius: 2px;
|
|
182
|
+
}
|
|
183
|
+
""")
|
|
184
|
+
header_layout.addWidget(self.help_info_btn)
|
|
185
|
+
|
|
186
|
+
header_layout.addStretch()
|
|
187
|
+
|
|
188
|
+
self.row_count_label = QLabel("")
|
|
189
|
+
self.row_count_label.setStyleSheet(get_row_count_label_stylesheet())
|
|
190
|
+
header_layout.addWidget(self.row_count_label)
|
|
191
|
+
|
|
192
|
+
self.results_layout.addLayout(header_layout)
|
|
193
|
+
|
|
194
|
+
# Results table with customized header
|
|
195
|
+
self.results_table = CopyableTableWidget()
|
|
196
|
+
self.results_table.setAlternatingRowColors(True)
|
|
197
|
+
|
|
198
|
+
# Set a reference to this tab so the copy functionality can access current_df
|
|
199
|
+
self.results_table._parent_tab = self
|
|
200
|
+
|
|
201
|
+
# Use custom FilterHeader for filtering
|
|
202
|
+
header = FilterHeader(self.results_table)
|
|
203
|
+
header.set_main_window(self.parent) # Set reference to main window
|
|
204
|
+
self.results_table.setHorizontalHeader(header)
|
|
205
|
+
|
|
206
|
+
# Set table properties for better performance with large datasets
|
|
207
|
+
self.results_table.setShowGrid(True)
|
|
208
|
+
self.results_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
209
|
+
self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
210
|
+
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
211
|
+
self.results_table.verticalHeader().setVisible(True)
|
|
212
|
+
|
|
213
|
+
# Connect double-click signal to handle column selection
|
|
214
|
+
self.results_table.cellDoubleClicked.connect(self.handle_cell_double_click)
|
|
215
|
+
|
|
216
|
+
# Connect header click signal to handle column header selection
|
|
217
|
+
self.results_table.horizontalHeader().sectionClicked.connect(self.handle_header_click)
|
|
218
|
+
|
|
219
|
+
# Connect header double-click signal to add column to query
|
|
220
|
+
self.results_table.horizontalHeader().sectionDoubleClicked.connect(self.handle_header_double_click)
|
|
221
|
+
|
|
222
|
+
self.results_layout.addWidget(self.results_table)
|
|
223
|
+
|
|
224
|
+
# Add widgets to splitter
|
|
225
|
+
self.splitter.addWidget(query_widget)
|
|
226
|
+
self.splitter.addWidget(results_widget)
|
|
227
|
+
|
|
228
|
+
# Set initial sizes - balanced split (45% query, 55% results)
|
|
229
|
+
# Both areas are important for SQL work
|
|
230
|
+
screen = QApplication.primaryScreen()
|
|
231
|
+
if screen:
|
|
232
|
+
available_height = screen.availableGeometry().height()
|
|
233
|
+
if available_height >= 1080: # Large screens
|
|
234
|
+
query_height = int(available_height * 0.40) # 40% for query area
|
|
235
|
+
self.splitter.setSizes([query_height, available_height - query_height])
|
|
236
|
+
else: # Smaller screens
|
|
237
|
+
self.splitter.setSizes([350, 400])
|
|
238
|
+
else:
|
|
239
|
+
self.splitter.setSizes([350, 400])
|
|
240
|
+
|
|
241
|
+
main_layout.addWidget(self.splitter)
|
|
242
|
+
|
|
243
|
+
def set_compact_mode(self, enabled):
|
|
244
|
+
"""Toggle compact mode for this tab to maximize query/results space"""
|
|
245
|
+
self._compact_mode = enabled
|
|
246
|
+
|
|
247
|
+
if enabled:
|
|
248
|
+
# Compact mode: minimize UI chrome for maximum editor/results space
|
|
249
|
+
self.query_layout.setContentsMargins(2, 2, 2, 2)
|
|
250
|
+
self.query_layout.setSpacing(2)
|
|
251
|
+
self.results_layout.setContentsMargins(2, 2, 2, 2)
|
|
252
|
+
self.results_layout.setSpacing(2)
|
|
253
|
+
self.results_title.setVisible(False)
|
|
254
|
+
self.help_info_btn.setVisible(False)
|
|
255
|
+
|
|
256
|
+
# Ultra-compact buttons (icons only)
|
|
257
|
+
self.execute_btn.setText("▶")
|
|
258
|
+
self.execute_btn.setFixedWidth(28)
|
|
259
|
+
self.clear_btn.setText("✕")
|
|
260
|
+
self.clear_btn.setFixedWidth(28)
|
|
261
|
+
self.export_excel_btn.setVisible(False)
|
|
262
|
+
self.export_parquet_btn.setVisible(False)
|
|
263
|
+
else:
|
|
264
|
+
# Normal mode
|
|
265
|
+
self.query_layout.setContentsMargins(8, 6, 8, 6)
|
|
266
|
+
self.query_layout.setSpacing(6)
|
|
267
|
+
self.results_layout.setContentsMargins(8, 4, 8, 4)
|
|
268
|
+
self.results_layout.setSpacing(4)
|
|
269
|
+
self.results_title.setVisible(True)
|
|
270
|
+
self.help_info_btn.setVisible(True)
|
|
271
|
+
|
|
272
|
+
# Restore button labels
|
|
273
|
+
self.execute_btn.setText("▶ Run")
|
|
274
|
+
self.execute_btn.setMinimumWidth(0)
|
|
275
|
+
self.execute_btn.setMaximumWidth(16777215)
|
|
276
|
+
self.clear_btn.setText("Clear")
|
|
277
|
+
self.clear_btn.setMinimumWidth(0)
|
|
278
|
+
self.clear_btn.setMaximumWidth(16777215)
|
|
279
|
+
self.export_excel_btn.setVisible(True)
|
|
280
|
+
self.export_parquet_btn.setVisible(True)
|
|
281
|
+
|
|
282
|
+
def get_query_text(self):
|
|
283
|
+
"""Get the current query text"""
|
|
284
|
+
return self.query_edit.toPlainText()
|
|
285
|
+
|
|
286
|
+
def set_query_text(self, text):
|
|
287
|
+
"""Set the query text"""
|
|
288
|
+
self.query_edit.setPlainText(text)
|
|
289
|
+
|
|
290
|
+
def execute_query(self):
|
|
291
|
+
"""Execute the current query"""
|
|
292
|
+
if hasattr(self.parent, 'execute_query'):
|
|
293
|
+
self.parent.execute_query()
|
|
294
|
+
|
|
295
|
+
def clear_query(self):
|
|
296
|
+
"""Clear the query editor"""
|
|
297
|
+
if hasattr(self.parent, 'clear_query'):
|
|
298
|
+
self.parent.clear_query()
|
|
299
|
+
|
|
300
|
+
def export_to_excel(self):
|
|
301
|
+
"""Export results to Excel"""
|
|
302
|
+
if hasattr(self.parent, 'export_to_excel'):
|
|
303
|
+
self.parent.export_to_excel()
|
|
304
|
+
|
|
305
|
+
def export_to_parquet(self):
|
|
306
|
+
"""Export results to Parquet"""
|
|
307
|
+
if hasattr(self.parent, 'export_to_parquet'):
|
|
308
|
+
self.parent.export_to_parquet()
|
|
309
|
+
|
|
310
|
+
def eventFilter(self, obj, event):
|
|
311
|
+
"""Event filter to intercept Ctrl+Enter and send it to the main window"""
|
|
312
|
+
from PyQt6.QtCore import QEvent, Qt
|
|
313
|
+
|
|
314
|
+
# Check if it's a key press event
|
|
315
|
+
if event.type() == QEvent.Type.KeyPress:
|
|
316
|
+
# Check for Ctrl+Enter specifically
|
|
317
|
+
if (event.key() == Qt.Key.Key_Return and
|
|
318
|
+
event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
319
|
+
|
|
320
|
+
# Hide any autocomplete popup if it's visible
|
|
321
|
+
if hasattr(obj, 'completer') and obj.completer and obj.completer.popup().isVisible():
|
|
322
|
+
obj.completer.popup().hide()
|
|
323
|
+
|
|
324
|
+
# Execute the query via the parent (main window)
|
|
325
|
+
if hasattr(self.parent, 'execute_query'):
|
|
326
|
+
self.parent.execute_query()
|
|
327
|
+
# Mark event as handled
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
# Default - let the event propagate normally
|
|
331
|
+
return super().eventFilter(obj, event)
|
|
332
|
+
|
|
333
|
+
def format_sql(self):
|
|
334
|
+
"""Format the SQL query for better readability"""
|
|
335
|
+
from sqlshell.utils.sql_formatter import format_sql
|
|
336
|
+
|
|
337
|
+
# Get current text
|
|
338
|
+
current_text = self.query_edit.toPlainText()
|
|
339
|
+
if not current_text.strip():
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
# Format the SQL
|
|
344
|
+
formatted_sql = format_sql(current_text)
|
|
345
|
+
|
|
346
|
+
# Replace the text
|
|
347
|
+
self.query_edit.setPlainText(formatted_sql)
|
|
348
|
+
self.parent.statusBar().showMessage('SQL formatted successfully')
|
|
349
|
+
except Exception as e:
|
|
350
|
+
self.parent.statusBar().showMessage(f'Error formatting SQL: {str(e)}')
|
|
351
|
+
|
|
352
|
+
def show_header_context_menu(self, position):
|
|
353
|
+
"""Show context menu for header columns"""
|
|
354
|
+
# Get the column index
|
|
355
|
+
idx = self.results_table.horizontalHeader().logicalIndexAt(position)
|
|
356
|
+
if idx < 0:
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
# Create context menu
|
|
360
|
+
menu = QMenu(self)
|
|
361
|
+
header = self.results_table.horizontalHeader()
|
|
362
|
+
|
|
363
|
+
# Get column name
|
|
364
|
+
col_name = self.results_table.horizontalHeaderItem(idx).text()
|
|
365
|
+
|
|
366
|
+
# Check if the column name needs quoting (contains spaces or special characters)
|
|
367
|
+
quoted_col_name = col_name
|
|
368
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
369
|
+
quoted_col_name = f'"{col_name}"'
|
|
370
|
+
|
|
371
|
+
# Add actions
|
|
372
|
+
copy_col_name_action = menu.addAction(f"Copy '{col_name}'")
|
|
373
|
+
menu.addSeparator()
|
|
374
|
+
|
|
375
|
+
# Check if we have a FilterHeader
|
|
376
|
+
if isinstance(header, FilterHeader):
|
|
377
|
+
# Check if this column has a bar chart
|
|
378
|
+
has_bar = idx in header.columns_with_bars
|
|
379
|
+
|
|
380
|
+
# Add toggle bar chart action
|
|
381
|
+
if not has_bar:
|
|
382
|
+
bar_action = menu.addAction("Add Bar Chart")
|
|
383
|
+
else:
|
|
384
|
+
bar_action = menu.addAction("Remove Bar Chart")
|
|
385
|
+
|
|
386
|
+
# Sort options
|
|
387
|
+
menu.addSeparator()
|
|
388
|
+
|
|
389
|
+
sort_asc_action = menu.addAction("Sort Ascending")
|
|
390
|
+
sort_desc_action = menu.addAction("Sort Descending")
|
|
391
|
+
|
|
392
|
+
# Filter options if we have data
|
|
393
|
+
if self.results_table.rowCount() > 0:
|
|
394
|
+
menu.addSeparator()
|
|
395
|
+
sel_distinct_action = menu.addAction(f"SELECT DISTINCT {quoted_col_name}")
|
|
396
|
+
count_distinct_action = menu.addAction(f"COUNT DISTINCT {quoted_col_name}")
|
|
397
|
+
group_by_action = menu.addAction(f"GROUP BY {quoted_col_name}")
|
|
398
|
+
|
|
399
|
+
# SQL generation submenu
|
|
400
|
+
menu.addSeparator()
|
|
401
|
+
sql_menu = menu.addMenu("Generate SQL")
|
|
402
|
+
select_col_action = sql_menu.addAction(f"SELECT {quoted_col_name}")
|
|
403
|
+
filter_col_action = sql_menu.addAction(f"WHERE {quoted_col_name} = ?")
|
|
404
|
+
explain_action = menu.addAction(f"Explain Column")
|
|
405
|
+
encode_action = menu.addAction(f"One-Hot Encode")
|
|
406
|
+
predict_action = menu.addAction(f"Predict Column")
|
|
407
|
+
discover_rules_action = menu.addAction(f"Discover Classification Rules (CN2)")
|
|
408
|
+
|
|
409
|
+
# Execute the menu
|
|
410
|
+
action = menu.exec(header.mapToGlobal(position))
|
|
411
|
+
|
|
412
|
+
# Handle actions
|
|
413
|
+
if action == copy_col_name_action:
|
|
414
|
+
QApplication.clipboard().setText(col_name)
|
|
415
|
+
self.parent.statusBar().showMessage(f"Copied '{col_name}' to clipboard")
|
|
416
|
+
|
|
417
|
+
elif action == explain_action:
|
|
418
|
+
# Call the explain column method on the parent
|
|
419
|
+
if hasattr(self.parent, 'explain_column'):
|
|
420
|
+
self.parent.explain_column(col_name)
|
|
421
|
+
|
|
422
|
+
elif action == encode_action:
|
|
423
|
+
# Call the encode text method on the parent
|
|
424
|
+
if hasattr(self.parent, 'encode_text'):
|
|
425
|
+
self.parent.encode_text(col_name)
|
|
426
|
+
|
|
427
|
+
elif action == predict_action:
|
|
428
|
+
# Call the predict column method on the parent
|
|
429
|
+
if hasattr(self.parent, 'predict_column'):
|
|
430
|
+
self.parent.predict_column(col_name)
|
|
431
|
+
|
|
432
|
+
elif action == discover_rules_action:
|
|
433
|
+
# Call the discover classification rules method on the parent
|
|
434
|
+
if hasattr(self.parent, 'discover_classification_rules'):
|
|
435
|
+
self.parent.discover_classification_rules(col_name)
|
|
436
|
+
|
|
437
|
+
elif action == sort_asc_action:
|
|
438
|
+
self.results_table.sortItems(idx, Qt.SortOrder.AscendingOrder)
|
|
439
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (ascending)")
|
|
440
|
+
|
|
441
|
+
elif action == sort_desc_action:
|
|
442
|
+
self.results_table.sortItems(idx, Qt.SortOrder.DescendingOrder)
|
|
443
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (descending)")
|
|
444
|
+
|
|
445
|
+
elif isinstance(header, FilterHeader) and action == bar_action:
|
|
446
|
+
# Toggle bar chart
|
|
447
|
+
header.toggle_bar_chart(idx)
|
|
448
|
+
if idx in header.columns_with_bars:
|
|
449
|
+
self.parent.statusBar().showMessage(f"Added bar chart for '{col_name}'")
|
|
450
|
+
else:
|
|
451
|
+
self.parent.statusBar().showMessage(f"Removed bar chart for '{col_name}'")
|
|
452
|
+
|
|
453
|
+
elif 'sel_distinct_action' in locals() and action == sel_distinct_action:
|
|
454
|
+
new_query = f"SELECT DISTINCT {quoted_col_name}\nFROM "
|
|
455
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
456
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
457
|
+
new_query += f"{table_name}\n"
|
|
458
|
+
else:
|
|
459
|
+
new_query += "[table_name]\n"
|
|
460
|
+
new_query += "ORDER BY 1"
|
|
461
|
+
self.set_query_text(new_query)
|
|
462
|
+
self.parent.statusBar().showMessage(f"Created SELECT DISTINCT query for '{col_name}'")
|
|
463
|
+
|
|
464
|
+
elif 'count_distinct_action' in locals() and action == count_distinct_action:
|
|
465
|
+
new_query = f"SELECT COUNT(DISTINCT {quoted_col_name}) AS distinct_{col_name.replace(' ', '_')}\nFROM "
|
|
466
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
467
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
468
|
+
new_query += f"{table_name}"
|
|
469
|
+
else:
|
|
470
|
+
new_query += "[table_name]"
|
|
471
|
+
self.set_query_text(new_query)
|
|
472
|
+
self.parent.statusBar().showMessage(f"Created COUNT DISTINCT query for '{col_name}'")
|
|
473
|
+
|
|
474
|
+
elif 'group_by_action' in locals() and action == group_by_action:
|
|
475
|
+
new_query = f"SELECT {quoted_col_name}, COUNT(*) AS count\nFROM "
|
|
476
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
477
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
478
|
+
new_query += f"{table_name}"
|
|
479
|
+
else:
|
|
480
|
+
new_query += "[table_name]"
|
|
481
|
+
new_query += f"\nGROUP BY {quoted_col_name}\nORDER BY count DESC"
|
|
482
|
+
self.set_query_text(new_query)
|
|
483
|
+
self.parent.statusBar().showMessage(f"Created GROUP BY query for '{col_name}'")
|
|
484
|
+
|
|
485
|
+
elif action == select_col_action:
|
|
486
|
+
new_query = f"SELECT {quoted_col_name}\nFROM "
|
|
487
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
488
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
489
|
+
new_query += f"{table_name}"
|
|
490
|
+
else:
|
|
491
|
+
new_query += "[table_name]"
|
|
492
|
+
self.set_query_text(new_query)
|
|
493
|
+
self.parent.statusBar().showMessage(f"Created SELECT query for '{col_name}'")
|
|
494
|
+
|
|
495
|
+
elif action == filter_col_action:
|
|
496
|
+
current_text = self.get_query_text()
|
|
497
|
+
if current_text and "WHERE" in current_text.upper():
|
|
498
|
+
# Add as AND condition
|
|
499
|
+
lines = current_text.splitlines()
|
|
500
|
+
for i, line in enumerate(lines):
|
|
501
|
+
if "WHERE" in line.upper() and "ORDER BY" not in line.upper() and "GROUP BY" not in line.upper():
|
|
502
|
+
lines[i] = f"{line} AND {quoted_col_name} = ?"
|
|
503
|
+
break
|
|
504
|
+
self.set_query_text("\n".join(lines))
|
|
505
|
+
else:
|
|
506
|
+
# Create new query with WHERE clause
|
|
507
|
+
new_query = f"SELECT *\nFROM "
|
|
508
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
509
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
510
|
+
new_query += f"{table_name}"
|
|
511
|
+
else:
|
|
512
|
+
new_query += "[table_name]"
|
|
513
|
+
new_query += f"\nWHERE {quoted_col_name} = ?"
|
|
514
|
+
self.set_query_text(new_query)
|
|
515
|
+
self.parent.statusBar().showMessage(f"Added filter condition for '{col_name}'")
|
|
516
|
+
|
|
517
|
+
def handle_cell_double_click(self, row, column):
|
|
518
|
+
"""Handle double-click on a cell to add column to query editor"""
|
|
519
|
+
# Get column name
|
|
520
|
+
col_name = self.results_table.horizontalHeaderItem(column).text()
|
|
521
|
+
|
|
522
|
+
# Check if the column name needs quoting (contains spaces or special characters)
|
|
523
|
+
quoted_col_name = col_name
|
|
524
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
525
|
+
quoted_col_name = f'"{col_name}"'
|
|
526
|
+
|
|
527
|
+
# Get current query text
|
|
528
|
+
current_text = self.get_query_text().strip()
|
|
529
|
+
|
|
530
|
+
# Get cursor position
|
|
531
|
+
cursor = self.query_edit.textCursor()
|
|
532
|
+
cursor_position = cursor.position()
|
|
533
|
+
|
|
534
|
+
# Check if we already have an existing query
|
|
535
|
+
if current_text:
|
|
536
|
+
# If there's existing text, try to insert at cursor position
|
|
537
|
+
if cursor_position > 0:
|
|
538
|
+
# Check if we need to add a comma before the column name
|
|
539
|
+
text_before_cursor = self.query_edit.toPlainText()[:cursor_position]
|
|
540
|
+
text_after_cursor = self.query_edit.toPlainText()[cursor_position:]
|
|
541
|
+
|
|
542
|
+
# Add comma if needed (we're in a list of columns)
|
|
543
|
+
needs_comma = (not text_before_cursor.strip().endswith(',') and
|
|
544
|
+
not text_before_cursor.strip().endswith('(') and
|
|
545
|
+
not text_before_cursor.strip().endswith('SELECT') and
|
|
546
|
+
not re.search(r'\bFROM\s*$', text_before_cursor) and
|
|
547
|
+
not re.search(r'\bWHERE\s*$', text_before_cursor) and
|
|
548
|
+
not re.search(r'\bGROUP\s+BY\s*$', text_before_cursor) and
|
|
549
|
+
not re.search(r'\bORDER\s+BY\s*$', text_before_cursor) and
|
|
550
|
+
not re.search(r'\bHAVING\s*$', text_before_cursor) and
|
|
551
|
+
not text_after_cursor.strip().startswith(','))
|
|
552
|
+
|
|
553
|
+
# Insert with comma if needed
|
|
554
|
+
if needs_comma:
|
|
555
|
+
cursor.insertText(f", {quoted_col_name}")
|
|
556
|
+
else:
|
|
557
|
+
cursor.insertText(quoted_col_name)
|
|
558
|
+
|
|
559
|
+
self.query_edit.setTextCursor(cursor)
|
|
560
|
+
self.query_edit.setFocus()
|
|
561
|
+
self.parent.statusBar().showMessage(f"Inserted '{col_name}' at cursor position")
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
# If cursor is at start, check if we have a SELECT query to modify
|
|
565
|
+
if current_text.upper().startswith("SELECT"):
|
|
566
|
+
# Try to find the SELECT clause
|
|
567
|
+
select_match = re.match(r'(?i)SELECT\s+(.*?)(?:\sFROM\s|$)', current_text)
|
|
568
|
+
if select_match:
|
|
569
|
+
select_clause = select_match.group(1).strip()
|
|
570
|
+
|
|
571
|
+
# If it's "SELECT *", replace it with the column name
|
|
572
|
+
if select_clause == "*":
|
|
573
|
+
modified_text = current_text.replace("SELECT *", f"SELECT {quoted_col_name}")
|
|
574
|
+
self.set_query_text(modified_text)
|
|
575
|
+
# Otherwise append the column if it's not already there
|
|
576
|
+
elif quoted_col_name not in select_clause:
|
|
577
|
+
modified_text = current_text.replace(select_clause, f"{select_clause}, {quoted_col_name}")
|
|
578
|
+
self.set_query_text(modified_text)
|
|
579
|
+
|
|
580
|
+
self.query_edit.setFocus()
|
|
581
|
+
self.parent.statusBar().showMessage(f"Added '{col_name}' to SELECT clause")
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
# If we can't modify an existing SELECT clause, append to the end
|
|
585
|
+
# Go to the end of the document
|
|
586
|
+
cursor.movePosition(cursor.MoveOperation.End)
|
|
587
|
+
# Insert a new line if needed
|
|
588
|
+
if not current_text.endswith('\n'):
|
|
589
|
+
cursor.insertText('\n')
|
|
590
|
+
# Insert a simple column reference
|
|
591
|
+
cursor.insertText(quoted_col_name)
|
|
592
|
+
self.query_edit.setTextCursor(cursor)
|
|
593
|
+
self.query_edit.setFocus()
|
|
594
|
+
self.parent.statusBar().showMessage(f"Appended '{col_name}' to query")
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
# If we don't have an existing query or couldn't modify it, create a new one
|
|
598
|
+
table_name = self._get_table_name(current_text)
|
|
599
|
+
new_query = f"SELECT {quoted_col_name}\nFROM {table_name}"
|
|
600
|
+
self.set_query_text(new_query)
|
|
601
|
+
self.query_edit.setFocus()
|
|
602
|
+
self.parent.statusBar().showMessage(f"Created new SELECT query for '{col_name}'")
|
|
603
|
+
|
|
604
|
+
def handle_header_click(self, idx):
|
|
605
|
+
"""Handle a click on a column header"""
|
|
606
|
+
# Store the column index and delay showing the context menu to allow for double-clicks
|
|
607
|
+
|
|
608
|
+
# Store the current index and time for processing
|
|
609
|
+
self._last_header_click_idx = idx
|
|
610
|
+
|
|
611
|
+
# Create a timer to show the context menu after a short delay
|
|
612
|
+
# This ensures we don't interfere with double-click detection
|
|
613
|
+
timer = QTimer()
|
|
614
|
+
timer.setSingleShot(True)
|
|
615
|
+
timer.timeout.connect(lambda: self._show_header_context_menu(idx))
|
|
616
|
+
timer.start(200) # 200ms delay
|
|
617
|
+
|
|
618
|
+
def _show_header_context_menu(self, idx):
|
|
619
|
+
"""Show context menu for column header after delay"""
|
|
620
|
+
# Get the header
|
|
621
|
+
header = self.results_table.horizontalHeader()
|
|
622
|
+
if not header:
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
# Get the column name
|
|
626
|
+
if not hasattr(self, 'current_df') or self.current_df is None:
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
if idx >= len(self.current_df.columns):
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
# Get column name
|
|
633
|
+
col_name = self.current_df.columns[idx]
|
|
634
|
+
|
|
635
|
+
# Check if column name needs quoting (contains spaces or special chars)
|
|
636
|
+
quoted_col_name = col_name
|
|
637
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
638
|
+
quoted_col_name = f'"{col_name}"'
|
|
639
|
+
|
|
640
|
+
# Get the position for the context menu (at the header cell)
|
|
641
|
+
position = header.mapToGlobal(header.rect().bottomLeft())
|
|
642
|
+
|
|
643
|
+
# Create the context menu
|
|
644
|
+
menu = QMenu()
|
|
645
|
+
col_header_action = menu.addAction(f"Column: {col_name}")
|
|
646
|
+
col_header_action.setEnabled(False)
|
|
647
|
+
menu.addSeparator()
|
|
648
|
+
|
|
649
|
+
# Add copy action
|
|
650
|
+
copy_col_name_action = menu.addAction("Copy Column Name")
|
|
651
|
+
|
|
652
|
+
# Add sorting actions
|
|
653
|
+
sort_menu = menu.addMenu("Sort")
|
|
654
|
+
sort_asc_action = sort_menu.addAction("Sort Ascending")
|
|
655
|
+
sort_desc_action = sort_menu.addAction("Sort Descending")
|
|
656
|
+
|
|
657
|
+
# Add bar chart toggle if numeric column
|
|
658
|
+
bar_action = None
|
|
659
|
+
if isinstance(header, FilterHeader):
|
|
660
|
+
is_numeric = False
|
|
661
|
+
try:
|
|
662
|
+
# Check if first non-null value is numeric
|
|
663
|
+
for i in range(min(100, len(self.current_df))):
|
|
664
|
+
if pd.notna(self.current_df.iloc[i, idx]):
|
|
665
|
+
val = self.current_df.iloc[i, idx]
|
|
666
|
+
if isinstance(val, (int, float, np.number)):
|
|
667
|
+
is_numeric = True
|
|
668
|
+
break
|
|
669
|
+
except:
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
if is_numeric:
|
|
673
|
+
menu.addSeparator()
|
|
674
|
+
if idx in header.columns_with_bars:
|
|
675
|
+
bar_action = menu.addAction("Remove Bar Chart")
|
|
676
|
+
else:
|
|
677
|
+
bar_action = menu.addAction("Add Bar Chart")
|
|
678
|
+
|
|
679
|
+
sql_menu = menu.addMenu("Generate SQL")
|
|
680
|
+
select_col_action = sql_menu.addAction(f"SELECT {quoted_col_name}")
|
|
681
|
+
filter_col_action = sql_menu.addAction(f"WHERE {quoted_col_name} = ?")
|
|
682
|
+
explain_action = menu.addAction(f"Explain Column")
|
|
683
|
+
encode_action = menu.addAction(f"One-Hot Encode")
|
|
684
|
+
predict_action = menu.addAction(f"Predict Column")
|
|
685
|
+
discover_rules_action = menu.addAction(f"Discover Classification Rules (CN2)")
|
|
686
|
+
|
|
687
|
+
# Execute the menu
|
|
688
|
+
action = menu.exec(position)
|
|
689
|
+
|
|
690
|
+
# Handle actions
|
|
691
|
+
if action == copy_col_name_action:
|
|
692
|
+
QApplication.clipboard().setText(col_name)
|
|
693
|
+
self.parent.statusBar().showMessage(f"Copied '{col_name}' to clipboard")
|
|
694
|
+
|
|
695
|
+
elif action == explain_action:
|
|
696
|
+
# Call the explain column method on the parent
|
|
697
|
+
if hasattr(self.parent, 'explain_column'):
|
|
698
|
+
self.parent.explain_column(col_name)
|
|
699
|
+
|
|
700
|
+
elif action == encode_action:
|
|
701
|
+
# Call the encode text method on the parent
|
|
702
|
+
if hasattr(self.parent, 'encode_text'):
|
|
703
|
+
self.parent.encode_text(col_name)
|
|
704
|
+
|
|
705
|
+
elif action == predict_action:
|
|
706
|
+
# Call the predict column method on the parent
|
|
707
|
+
if hasattr(self.parent, 'predict_column'):
|
|
708
|
+
self.parent.predict_column(col_name)
|
|
709
|
+
|
|
710
|
+
elif action == discover_rules_action:
|
|
711
|
+
# Call the discover classification rules method on the parent
|
|
712
|
+
if hasattr(self.parent, 'discover_classification_rules'):
|
|
713
|
+
self.parent.discover_classification_rules(col_name)
|
|
714
|
+
|
|
715
|
+
elif action == sort_asc_action:
|
|
716
|
+
self.results_table.sortItems(idx, Qt.SortOrder.AscendingOrder)
|
|
717
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (ascending)")
|
|
718
|
+
|
|
719
|
+
elif action == sort_desc_action:
|
|
720
|
+
self.results_table.sortItems(idx, Qt.SortOrder.DescendingOrder)
|
|
721
|
+
self.parent.statusBar().showMessage(f"Sorted by '{col_name}' (descending)")
|
|
722
|
+
|
|
723
|
+
elif isinstance(header, FilterHeader) and action == bar_action:
|
|
724
|
+
# Toggle bar chart
|
|
725
|
+
header.toggle_bar_chart(idx)
|
|
726
|
+
if idx in header.columns_with_bars:
|
|
727
|
+
self.parent.statusBar().showMessage(f"Added bar chart for '{col_name}'")
|
|
728
|
+
else:
|
|
729
|
+
self.parent.statusBar().showMessage(f"Removed bar chart for '{col_name}'")
|
|
730
|
+
|
|
731
|
+
elif action == select_col_action:
|
|
732
|
+
# Insert SQL snippet at cursor position in query editor
|
|
733
|
+
if hasattr(self, 'query_edit'):
|
|
734
|
+
cursor = self.query_edit.textCursor()
|
|
735
|
+
cursor.insertText(f"SELECT {quoted_col_name}")
|
|
736
|
+
self.query_edit.setFocus()
|
|
737
|
+
|
|
738
|
+
elif action == filter_col_action:
|
|
739
|
+
# Insert SQL snippet at cursor position in query editor
|
|
740
|
+
if hasattr(self, 'query_edit'):
|
|
741
|
+
cursor = self.query_edit.textCursor()
|
|
742
|
+
cursor.insertText(f"WHERE {quoted_col_name} = ")
|
|
743
|
+
self.query_edit.setFocus()
|
|
744
|
+
|
|
745
|
+
def handle_header_double_click(self, idx):
|
|
746
|
+
"""Handle double-click on a column header to add it to the query editor"""
|
|
747
|
+
# Get column name
|
|
748
|
+
if not hasattr(self, 'current_df') or self.current_df is None:
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
if idx >= len(self.current_df.columns):
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
# Get column name
|
|
755
|
+
col_name = self.current_df.columns[idx]
|
|
756
|
+
|
|
757
|
+
# Check if column name needs quoting (contains spaces or special chars)
|
|
758
|
+
quoted_col_name = col_name
|
|
759
|
+
if re.search(r'[\s\W]', col_name) and not col_name.startswith('"') and not col_name.endswith('"'):
|
|
760
|
+
quoted_col_name = f'"{col_name}"'
|
|
761
|
+
|
|
762
|
+
# Get current query text
|
|
763
|
+
current_text = self.get_query_text().strip()
|
|
764
|
+
|
|
765
|
+
# Get cursor position
|
|
766
|
+
cursor = self.query_edit.textCursor()
|
|
767
|
+
cursor_position = cursor.position()
|
|
768
|
+
|
|
769
|
+
# Check if we already have an existing query
|
|
770
|
+
if current_text:
|
|
771
|
+
# If there's existing text, try to insert at cursor position
|
|
772
|
+
if cursor_position > 0:
|
|
773
|
+
# Check if we need to add a comma before the column name
|
|
774
|
+
text_before_cursor = self.query_edit.toPlainText()[:cursor_position]
|
|
775
|
+
text_after_cursor = self.query_edit.toPlainText()[cursor_position:]
|
|
776
|
+
|
|
777
|
+
# Add comma if needed (we're in a list of columns)
|
|
778
|
+
needs_comma = (not text_before_cursor.strip().endswith(',') and
|
|
779
|
+
not text_before_cursor.strip().endswith('(') and
|
|
780
|
+
not text_before_cursor.strip().endswith('SELECT') and
|
|
781
|
+
not re.search(r'\bFROM\s*$', text_before_cursor) and
|
|
782
|
+
not re.search(r'\bWHERE\s*$', text_before_cursor) and
|
|
783
|
+
not re.search(r'\bGROUP\s+BY\s*$', text_before_cursor) and
|
|
784
|
+
not re.search(r'\bORDER\s+BY\s*$', text_before_cursor) and
|
|
785
|
+
not re.search(r'\bHAVING\s*$', text_before_cursor) and
|
|
786
|
+
not text_after_cursor.strip().startswith(','))
|
|
787
|
+
|
|
788
|
+
# Insert with comma if needed
|
|
789
|
+
if needs_comma:
|
|
790
|
+
cursor.insertText(f", {quoted_col_name}")
|
|
791
|
+
else:
|
|
792
|
+
cursor.insertText(quoted_col_name)
|
|
793
|
+
|
|
794
|
+
self.query_edit.setTextCursor(cursor)
|
|
795
|
+
self.query_edit.setFocus()
|
|
796
|
+
self.parent.statusBar().showMessage(f"Inserted '{col_name}' at cursor position")
|
|
797
|
+
return
|
|
798
|
+
|
|
799
|
+
# If cursor is at start, check if we have a SELECT query to modify
|
|
800
|
+
if current_text.upper().startswith("SELECT"):
|
|
801
|
+
# Try to find the SELECT clause
|
|
802
|
+
select_match = re.match(r'(?i)SELECT\s+(.*?)(?:\sFROM\s|$)', current_text)
|
|
803
|
+
if select_match:
|
|
804
|
+
select_clause = select_match.group(1).strip()
|
|
805
|
+
|
|
806
|
+
# If it's "SELECT *", replace it with the column name
|
|
807
|
+
if select_clause == "*":
|
|
808
|
+
modified_text = current_text.replace("SELECT *", f"SELECT {quoted_col_name}")
|
|
809
|
+
self.set_query_text(modified_text)
|
|
810
|
+
# Otherwise append the column if it's not already there
|
|
811
|
+
elif quoted_col_name not in select_clause:
|
|
812
|
+
modified_text = current_text.replace(select_clause, f"{select_clause}, {quoted_col_name}")
|
|
813
|
+
self.set_query_text(modified_text)
|
|
814
|
+
|
|
815
|
+
self.query_edit.setFocus()
|
|
816
|
+
self.parent.statusBar().showMessage(f"Added '{col_name}' to SELECT clause")
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
# If we can't modify an existing SELECT clause, append to the end
|
|
820
|
+
# Go to the end of the document
|
|
821
|
+
cursor.movePosition(cursor.MoveOperation.End)
|
|
822
|
+
# Insert a new line if needed
|
|
823
|
+
if not current_text.endswith('\n'):
|
|
824
|
+
cursor.insertText('\n')
|
|
825
|
+
# Insert a simple column reference
|
|
826
|
+
cursor.insertText(quoted_col_name)
|
|
827
|
+
self.query_edit.setTextCursor(cursor)
|
|
828
|
+
self.query_edit.setFocus()
|
|
829
|
+
self.parent.statusBar().showMessage(f"Appended '{col_name}' to query")
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
# If we don't have an existing query or couldn't modify it, create a new one
|
|
833
|
+
table_name = self._get_table_name(current_text)
|
|
834
|
+
new_query = f"SELECT {quoted_col_name}\nFROM {table_name}"
|
|
835
|
+
self.set_query_text(new_query)
|
|
836
|
+
self.query_edit.setFocus()
|
|
837
|
+
self.parent.statusBar().showMessage(f"Created new SELECT query for '{col_name}'")
|
|
838
|
+
|
|
839
|
+
def _get_table_name(self, current_text):
|
|
840
|
+
"""Extract table name from current query or DataFrame, with fallbacks"""
|
|
841
|
+
# First, try to get the currently selected table in the UI
|
|
842
|
+
if self.parent and hasattr(self.parent, 'get_selected_table'):
|
|
843
|
+
selected_table = self.parent.get_selected_table()
|
|
844
|
+
if selected_table:
|
|
845
|
+
return selected_table
|
|
846
|
+
|
|
847
|
+
# Try to extract table name from the current DataFrame
|
|
848
|
+
if self.current_df is not None and hasattr(self.current_df, '_query_source'):
|
|
849
|
+
table_name = getattr(self.current_df, '_query_source')
|
|
850
|
+
if table_name:
|
|
851
|
+
return table_name
|
|
852
|
+
|
|
853
|
+
# Try to extract the table name from the current query
|
|
854
|
+
if current_text:
|
|
855
|
+
# Look for FROM clause
|
|
856
|
+
from_match = re.search(r'(?i)FROM\s+([a-zA-Z0-9_."]+(?:\s*,\s*[a-zA-Z0-9_."]+)*)', current_text)
|
|
857
|
+
if from_match:
|
|
858
|
+
# Get the last table in the FROM clause (could be multiple tables joined)
|
|
859
|
+
tables = from_match.group(1).split(',')
|
|
860
|
+
last_table = tables[-1].strip()
|
|
861
|
+
|
|
862
|
+
# Remove any alias
|
|
863
|
+
last_table = re.sub(r'(?i)\s+as\s+\w+$', '', last_table)
|
|
864
|
+
last_table = re.sub(r'\s+\w+$', '', last_table)
|
|
865
|
+
|
|
866
|
+
# Remove any quotes
|
|
867
|
+
last_table = last_table.strip('"\'`[]')
|
|
868
|
+
|
|
869
|
+
return last_table
|
|
870
|
+
|
|
871
|
+
# If all else fails, return placeholder
|
|
872
|
+
return "[table_name]"
|
|
873
|
+
|
|
874
|
+
def _execute_query_callback(self, query_text):
|
|
875
|
+
"""Callback function for the execution handler to execute a single query."""
|
|
876
|
+
# This is called by the execution handler when F5/F9 is pressed
|
|
877
|
+
if hasattr(self.parent, 'execute_specific_query'):
|
|
878
|
+
self.parent.execute_specific_query(query_text)
|
|
879
|
+
else:
|
|
880
|
+
# Fallback: execute using the standard method
|
|
881
|
+
original_text = self.query_edit.toPlainText()
|
|
882
|
+
cursor_pos = self.query_edit.textCursor().position() # Save current cursor position
|
|
883
|
+
self.query_edit.setPlainText(query_text)
|
|
884
|
+
if hasattr(self.parent, 'execute_query'):
|
|
885
|
+
self.parent.execute_query()
|
|
886
|
+
self.query_edit.setPlainText(original_text)
|
|
887
|
+
# Restore cursor position (as close as possible)
|
|
888
|
+
doc_length = len(self.query_edit.toPlainText())
|
|
889
|
+
restored_pos = min(cursor_pos, doc_length)
|
|
890
|
+
cursor = self.query_edit.textCursor()
|
|
891
|
+
cursor.setPosition(restored_pos)
|
|
892
|
+
self.query_edit.setTextCursor(cursor)
|
|
893
|
+
|
|
894
|
+
def execute_all_statements(self):
|
|
895
|
+
"""Execute all statements in the editor (F5 functionality)."""
|
|
896
|
+
if self.execution_integration:
|
|
897
|
+
return self.execution_integration.execute_all_statements()
|
|
898
|
+
return None
|
|
899
|
+
|
|
900
|
+
def execute_current_statement(self):
|
|
901
|
+
"""Execute the current statement (F9 functionality)."""
|
|
902
|
+
if self.execution_integration:
|
|
903
|
+
return self.execution_integration.execute_current_statement()
|
|
904
|
+
return None
|