sqlshell 0.2.3__py3-none-any.whl → 0.3.1__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/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
- results_layout.addWidget(self.results_table)
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