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.
Files changed (54) hide show
  1. sqlshell/__init__.py +84 -0
  2. sqlshell/__main__.py +4926 -0
  3. sqlshell/ai_autocomplete.py +392 -0
  4. sqlshell/ai_settings_dialog.py +337 -0
  5. sqlshell/context_suggester.py +768 -0
  6. sqlshell/create_test_data.py +152 -0
  7. sqlshell/data/create_test_data.py +137 -0
  8. sqlshell/db/__init__.py +6 -0
  9. sqlshell/db/database_manager.py +1318 -0
  10. sqlshell/db/export_manager.py +188 -0
  11. sqlshell/editor.py +1166 -0
  12. sqlshell/editor_integration.py +127 -0
  13. sqlshell/execution_handler.py +421 -0
  14. sqlshell/menus.py +262 -0
  15. sqlshell/notification_manager.py +370 -0
  16. sqlshell/query_tab.py +904 -0
  17. sqlshell/resources/__init__.py +1 -0
  18. sqlshell/resources/icon.png +0 -0
  19. sqlshell/resources/logo_large.png +0 -0
  20. sqlshell/resources/logo_medium.png +0 -0
  21. sqlshell/resources/logo_small.png +0 -0
  22. sqlshell/resources/splash_screen.gif +0 -0
  23. sqlshell/space_invaders.py +501 -0
  24. sqlshell/splash_screen.py +405 -0
  25. sqlshell/sqlshell/__init__.py +5 -0
  26. sqlshell/sqlshell/create_test_data.py +118 -0
  27. sqlshell/sqlshell/create_test_databases.py +96 -0
  28. sqlshell/sqlshell_demo.png +0 -0
  29. sqlshell/styles.py +257 -0
  30. sqlshell/suggester_integration.py +330 -0
  31. sqlshell/syntax_highlighter.py +124 -0
  32. sqlshell/table_list.py +996 -0
  33. sqlshell/ui/__init__.py +6 -0
  34. sqlshell/ui/bar_chart_delegate.py +49 -0
  35. sqlshell/ui/filter_header.py +469 -0
  36. sqlshell/utils/__init__.py +16 -0
  37. sqlshell/utils/profile_cn2.py +1661 -0
  38. sqlshell/utils/profile_column.py +2635 -0
  39. sqlshell/utils/profile_distributions.py +616 -0
  40. sqlshell/utils/profile_entropy.py +347 -0
  41. sqlshell/utils/profile_foreign_keys.py +779 -0
  42. sqlshell/utils/profile_keys.py +2834 -0
  43. sqlshell/utils/profile_ohe.py +934 -0
  44. sqlshell/utils/profile_ohe_advanced.py +754 -0
  45. sqlshell/utils/profile_ohe_comparison.py +237 -0
  46. sqlshell/utils/profile_prediction.py +926 -0
  47. sqlshell/utils/profile_similarity.py +876 -0
  48. sqlshell/utils/search_in_df.py +90 -0
  49. sqlshell/widgets.py +400 -0
  50. sqlshell-0.4.4.dist-info/METADATA +441 -0
  51. sqlshell-0.4.4.dist-info/RECORD +54 -0
  52. sqlshell-0.4.4.dist-info/WHEEL +5 -0
  53. sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
  54. 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