sqlshell 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlshell might be problematic. Click here for more details.

@@ -0,0 +1,275 @@
1
+ """
2
+ Integration module for context-aware SQL suggestions.
3
+
4
+ This module provides the glue code needed to connect the ContextSuggester
5
+ with the SQL editor component for seamless context-aware autocompletion.
6
+ """
7
+
8
+ from PyQt6.QtCore import QStringListModel, Qt
9
+ from PyQt6.QtWidgets import QCompleter
10
+ from typing import Dict, List, Any, Optional
11
+ import re
12
+
13
+ from sqlshell.context_suggester import ContextSuggester
14
+
15
+
16
+ class SuggestionManager:
17
+ """
18
+ Manages the integration between the ContextSuggester and the SQLEditor.
19
+
20
+ This class acts as a bridge between the database schema information,
21
+ query history tracking, and the editor's autocompletion functionality.
22
+ """
23
+
24
+ def __init__(self):
25
+ """Initialize the suggestion manager."""
26
+ self.suggester = ContextSuggester()
27
+ self._completers = {} # {editor_id: completer}
28
+ self._editors = {} # {editor_id: editor_instance}
29
+
30
+ def register_editor(self, editor, editor_id=None):
31
+ """
32
+ Register an editor to receive context-aware suggestions.
33
+
34
+ Args:
35
+ editor: The SQLEditor instance to register
36
+ editor_id: Optional identifier for the editor (defaults to object id)
37
+ """
38
+ if editor_id is None:
39
+ editor_id = id(editor)
40
+
41
+ # Create a completer for this editor if it doesn't have one
42
+ if not hasattr(editor, 'completer') or not editor.completer:
43
+ completer = QCompleter()
44
+ completer.setWidget(editor)
45
+ completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
46
+ completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
47
+ completer.activated.connect(editor.insert_completion)
48
+ editor.completer = completer
49
+
50
+ self._completers[editor_id] = editor.completer
51
+ self._editors[editor_id] = editor
52
+
53
+ # Hook into editor's context detection methods if possible
54
+ if hasattr(editor, 'get_context_at_cursor'):
55
+ # Save the original method
56
+ if not hasattr(editor, '_original_get_context_at_cursor'):
57
+ editor._original_get_context_at_cursor = editor.get_context_at_cursor
58
+
59
+ # Replace with our enhanced version
60
+ def enhanced_get_context(editor_ref=editor, suggestion_mgr=self):
61
+ # Get the original context first
62
+ original_context = editor_ref._original_get_context_at_cursor()
63
+
64
+ # Get our enhanced context
65
+ tc = editor_ref.textCursor()
66
+ position = tc.position()
67
+ doc = editor_ref.document()
68
+
69
+ # Get text before cursor - the error was in this section
70
+ # Using a simpler approach that doesn't rely on QTextDocument.find()
71
+ text_before_cursor = editor_ref.toPlainText()[:position]
72
+ current_word = editor_ref.get_word_under_cursor()
73
+
74
+ enhanced_context = suggestion_mgr.suggester.analyze_context(
75
+ text_before_cursor,
76
+ current_word
77
+ )
78
+
79
+ # Merge the contexts (our enhanced context takes precedence)
80
+ merged_context = {**original_context, **enhanced_context}
81
+ return merged_context
82
+
83
+ editor.get_context_at_cursor = enhanced_get_context
84
+
85
+ # Hook into editor's complete method if possible
86
+ if hasattr(editor, 'complete'):
87
+ # Save the original method
88
+ if not hasattr(editor, '_original_complete'):
89
+ editor._original_complete = editor.complete
90
+
91
+ # Replace with our enhanced version
92
+ def enhanced_complete(editor_ref=editor, suggestion_mgr=self):
93
+ tc = editor_ref.textCursor()
94
+ position = tc.position()
95
+ text_before_cursor = editor_ref.toPlainText()[:position]
96
+ current_word = editor_ref.get_word_under_cursor()
97
+
98
+ # Check if Ctrl key is being held down
99
+ from PyQt6.QtWidgets import QApplication
100
+ from PyQt6.QtCore import Qt
101
+
102
+ # Don't show completions if Ctrl key is pressed (could be in preparation for Ctrl+Enter)
103
+ modifiers = QApplication.keyboardModifiers()
104
+ if modifiers & Qt.KeyboardModifier.ControlModifier:
105
+ # If Ctrl is held down, don't show completions as user might be about to execute
106
+ if editor_ref.completer and editor_ref.completer.popup().isVisible():
107
+ editor_ref.completer.popup().hide()
108
+
109
+ # Also check for Ctrl+Enter specifically
110
+ if hasattr(QApplication, 'keyboardModifiers'):
111
+ if QApplication.keyboardModifiers() == (Qt.KeyboardModifier.ControlModifier):
112
+ # Find main window and trigger query execution
113
+ parent = editor_ref
114
+ while parent is not None:
115
+ if hasattr(parent, 'execute_query'):
116
+ # This is likely the main window
117
+ return True
118
+ parent = parent.parent()
119
+ return
120
+
121
+ # Special handling for function argument completions
122
+ # This helps with context like SELECT AVG(...) FROM table
123
+ in_function = False
124
+ open_parens = text_before_cursor.count('(')
125
+ close_parens = text_before_cursor.count(')')
126
+
127
+ if open_parens > close_parens:
128
+ in_function = True
129
+ # Get further context for better suggestions inside function arguments
130
+ context = suggestion_mgr.suggester.analyze_context(text_before_cursor, current_word)
131
+ if 'tables_in_from' not in context or not context['tables_in_from']:
132
+ # If tables not yet detected, try to look ahead for FROM clause
133
+ full_text = editor_ref.toPlainText()
134
+ after_cursor = full_text[position:]
135
+ # Look for FROM clause after current position
136
+ from_match = re.search(r'FROM\s+([a-zA-Z0-9_]+)', after_cursor, re.IGNORECASE)
137
+ if from_match:
138
+ table_name = from_match.group(1)
139
+ # Add this table to the context for better suggestions
140
+ context['tables_in_from'] = [table_name]
141
+ # Update the context in suggester
142
+ suggestion_mgr.suggester._context_cache[f"{text_before_cursor}:{current_word}"] = context
143
+
144
+ # Get context-aware suggestions
145
+ suggestions = suggestion_mgr.get_suggestions(text_before_cursor, current_word)
146
+
147
+ if suggestions:
148
+ # Update completer model with suggestions
149
+ model = QStringListModel(suggestions)
150
+ editor_ref.completer.setModel(model)
151
+ editor_ref.completer.setCompletionPrefix(current_word)
152
+
153
+ # Check if we have completions
154
+ if editor_ref.completer.completionCount() > 0:
155
+ # Get popup and position it
156
+ popup = editor_ref.completer.popup()
157
+ popup.setCurrentIndex(editor_ref.completer.completionModel().index(0, 0))
158
+
159
+ try:
160
+ # Calculate position for the popup
161
+ cr = editor_ref.cursorRect()
162
+
163
+ # Ensure cursorRect is valid
164
+ if not cr.isValid() or cr.x() < 0 or cr.y() < 0:
165
+ # Try to recompute using the text cursor
166
+ cr = editor_ref.cursorRect(tc)
167
+
168
+ # If still invalid, use a default position
169
+ if not cr.isValid() or cr.x() < 0 or cr.y() < 0:
170
+ pos = editor_ref.mapToGlobal(editor_ref.pos())
171
+ cr = QRect(pos.x() + 10, pos.y() + 10, 10, editor_ref.fontMetrics().height())
172
+
173
+ # Calculate width for the popup that fits the content
174
+ suggested_width = popup.sizeHintForColumn(0) + popup.verticalScrollBar().sizeHint().width()
175
+ # Ensure minimum width
176
+ popup_width = max(suggested_width, 200)
177
+ cr.setWidth(popup_width)
178
+
179
+ # Show the popup at the correct position
180
+ editor_ref.completer.complete(cr)
181
+ except Exception as e:
182
+ # In case of any error, try a more direct approach
183
+ print(f"Error positioning completion popup in suggestion manager: {e}")
184
+ try:
185
+ cursor_pos = editor_ref.mapToGlobal(editor_ref.cursorRect().bottomLeft())
186
+ popup.move(cursor_pos)
187
+ popup.show()
188
+ except:
189
+ # Last resort - if all else fails, hide the popup to avoid showing it in the wrong place
190
+ popup.hide()
191
+ else:
192
+ editor_ref.completer.popup().hide()
193
+ else:
194
+ # Fall back to original completion if no context-aware suggestions
195
+ if hasattr(editor_ref, '_original_complete'):
196
+ editor_ref._original_complete()
197
+ else:
198
+ editor_ref.completer.popup().hide()
199
+
200
+ editor.complete = enhanced_complete
201
+
202
+ def unregister_editor(self, editor_id):
203
+ """
204
+ Unregister an editor from receiving context-aware suggestions.
205
+
206
+ Args:
207
+ editor_id: The identifier of the editor to unregister
208
+ """
209
+ if editor_id in self._editors:
210
+ editor = self._editors[editor_id]
211
+
212
+ # Restore original methods if we replaced them
213
+ if hasattr(editor, '_original_get_context_at_cursor'):
214
+ editor.get_context_at_cursor = editor._original_get_context_at_cursor
215
+ delattr(editor, '_original_get_context_at_cursor')
216
+
217
+ if hasattr(editor, '_original_complete'):
218
+ editor.complete = editor._original_complete
219
+ delattr(editor, '_original_complete')
220
+
221
+ # Remove from tracked collections
222
+ del self._editors[editor_id]
223
+
224
+ if editor_id in self._completers:
225
+ del self._completers[editor_id]
226
+
227
+ def update_schema(self, tables, table_columns, column_types=None):
228
+ """
229
+ Update schema information for all registered editors.
230
+
231
+ Args:
232
+ tables: Set of table names
233
+ table_columns: Dictionary mapping table names to column lists
234
+ column_types: Optional dictionary of column data types
235
+ """
236
+ # Update the context suggester with new schema information
237
+ self.suggester.update_schema(tables, table_columns, column_types)
238
+
239
+ def record_query(self, query_text):
240
+ """
241
+ Record a query to improve suggestion relevance.
242
+
243
+ Args:
244
+ query_text: The SQL query to record
245
+ """
246
+ self.suggester.record_query(query_text)
247
+
248
+ def get_suggestions(self, text_before_cursor, current_word=""):
249
+ """
250
+ Get context-aware suggestions for the given text context.
251
+
252
+ Args:
253
+ text_before_cursor: Text from start of document to cursor position
254
+ current_word: The current word being typed (possibly empty)
255
+
256
+ Returns:
257
+ List of suggestion strings relevant to the current context
258
+ """
259
+ return self.suggester.get_suggestions(text_before_cursor, current_word)
260
+
261
+ def update_all_completers(self):
262
+ """Update all registered completers with current schema and usage data."""
263
+ for editor_id, editor in self._editors.items():
264
+ # Force a completion update next time complete() is called
265
+ if hasattr(editor, '_context_cache'):
266
+ editor._context_cache = {}
267
+
268
+
269
+ # Create a singleton instance to be used application-wide
270
+ suggestion_manager = SuggestionManager()
271
+
272
+
273
+ def get_suggestion_manager():
274
+ """Get the global suggestion manager instance."""
275
+ return suggestion_manager