sqlshell 0.1.8__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.
- sqlshell/LICENSE +21 -0
- sqlshell/MANIFEST.in +6 -0
- sqlshell/README.md +59 -0
- sqlshell/__init__.py +1 -1
- sqlshell/context_suggester.py +765 -0
- sqlshell/create_test_data.py +106 -30
- sqlshell/db/__init__.py +5 -0
- sqlshell/db/database_manager.py +837 -0
- sqlshell/editor.py +610 -52
- sqlshell/main.py +2657 -1164
- sqlshell/menus.py +171 -0
- sqlshell/query_tab.py +201 -0
- sqlshell/resources/create_icon.py +106 -28
- sqlshell/resources/create_splash.py +41 -11
- 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/splash_screen.py +276 -48
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +275 -0
- sqlshell/table_list.py +907 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +403 -0
- sqlshell/utils/__init__.py +8 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_keys.py +356 -0
- sqlshell-0.2.0.dist-info/METADATA +198 -0
- sqlshell-0.2.0.dist-info/RECORD +41 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/WHEEL +1 -1
- sqlshell/setup.py +0 -42
- sqlshell-0.1.8.dist-info/METADATA +0 -120
- sqlshell-0.1.8.dist-info/RECORD +0 -21
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|