sqlshell 0.2.2__py3-none-any.whl → 0.3.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/README.md +5 -1
- sqlshell/__init__.py +35 -5
- sqlshell/create_test_data.py +29 -0
- sqlshell/db/__init__.py +2 -1
- sqlshell/db/database_manager.py +336 -23
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/main.py +784 -143
- sqlshell/query_tab.py +592 -7
- sqlshell/table_list.py +90 -1
- sqlshell/ui/filter_header.py +36 -1
- sqlshell/utils/profile_column.py +2515 -0
- sqlshell/utils/profile_distributions.py +613 -0
- sqlshell/utils/profile_foreign_keys.py +547 -0
- sqlshell/utils/profile_ohe.py +631 -0
- sqlshell-0.3.0.dist-info/METADATA +400 -0
- {sqlshell-0.2.2.dist-info → sqlshell-0.3.0.dist-info}/RECORD +21 -14
- {sqlshell-0.2.2.dist-info → sqlshell-0.3.0.dist-info}/WHEEL +1 -1
- sqlshell-0.2.2.dist-info/METADATA +0 -198
- {sqlshell-0.2.2.dist-info → sqlshell-0.3.0.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.2.2.dist-info → sqlshell-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modular SQL execution handler for SQLShell.
|
|
3
|
+
Provides F5 (execute all statements) and F9 (execute current statement) functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import List, Tuple, Optional, Callable
|
|
8
|
+
from PyQt6.QtWidgets import QPlainTextEdit
|
|
9
|
+
from PyQt6.QtGui import QTextCursor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLExecutionHandler:
|
|
13
|
+
"""
|
|
14
|
+
Handles SQL statement parsing and execution for different execution modes.
|
|
15
|
+
|
|
16
|
+
Supports:
|
|
17
|
+
- F5: Execute all statements in the editor
|
|
18
|
+
- F9: Execute the current statement (statement containing cursor)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, execute_callback: Callable[[str], None] = None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the execution handler.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
execute_callback: Function to call to execute a SQL query string
|
|
27
|
+
"""
|
|
28
|
+
self.execute_callback = execute_callback
|
|
29
|
+
|
|
30
|
+
def set_execute_callback(self, callback: Callable[[str], None]):
|
|
31
|
+
"""Set the callback function for executing queries."""
|
|
32
|
+
self.execute_callback = callback
|
|
33
|
+
|
|
34
|
+
def parse_sql_statements(self, text: str) -> List[Tuple[str, int, int]]:
|
|
35
|
+
"""
|
|
36
|
+
Parse SQL text into individual statements.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: SQL text to parse
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of tuples: (statement_text, start_position, end_position)
|
|
43
|
+
"""
|
|
44
|
+
statements = []
|
|
45
|
+
if not text.strip():
|
|
46
|
+
return statements
|
|
47
|
+
|
|
48
|
+
# Create position mapping between original text and comment-removed text
|
|
49
|
+
original_to_clean, clean_to_original = self._create_position_mapping(text)
|
|
50
|
+
|
|
51
|
+
# Remove comments while preserving position information
|
|
52
|
+
text_without_comments = self._remove_comments(text)
|
|
53
|
+
|
|
54
|
+
# Split by semicolons, but be smart about it
|
|
55
|
+
# Handle string literals and quoted identifiers properly
|
|
56
|
+
current_statement = ""
|
|
57
|
+
statement_start_pos = 0
|
|
58
|
+
i = 0
|
|
59
|
+
in_string = False
|
|
60
|
+
string_char = None
|
|
61
|
+
escaped = False
|
|
62
|
+
|
|
63
|
+
# Find the first non-whitespace character to start
|
|
64
|
+
while statement_start_pos < len(text_without_comments) and text_without_comments[statement_start_pos].isspace():
|
|
65
|
+
statement_start_pos += 1
|
|
66
|
+
|
|
67
|
+
while i < len(text_without_comments):
|
|
68
|
+
char = text_without_comments[i]
|
|
69
|
+
|
|
70
|
+
if escaped:
|
|
71
|
+
escaped = False
|
|
72
|
+
current_statement += char
|
|
73
|
+
i += 1
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if char == '\\':
|
|
77
|
+
escaped = True
|
|
78
|
+
current_statement += char
|
|
79
|
+
i += 1
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if not in_string and char in ('"', "'"):
|
|
83
|
+
in_string = True
|
|
84
|
+
string_char = char
|
|
85
|
+
current_statement += char
|
|
86
|
+
elif in_string and char == string_char:
|
|
87
|
+
# Check for escaped quotes (double quotes)
|
|
88
|
+
if i + 1 < len(text_without_comments) and text_without_comments[i + 1] == string_char:
|
|
89
|
+
current_statement += char + string_char
|
|
90
|
+
i += 2
|
|
91
|
+
continue
|
|
92
|
+
else:
|
|
93
|
+
in_string = False
|
|
94
|
+
string_char = None
|
|
95
|
+
current_statement += char
|
|
96
|
+
elif not in_string and char == ';':
|
|
97
|
+
# End of statement
|
|
98
|
+
statement_text = current_statement.strip()
|
|
99
|
+
if statement_text:
|
|
100
|
+
# Convert clean text positions back to original text positions
|
|
101
|
+
original_start = clean_to_original.get(statement_start_pos, statement_start_pos)
|
|
102
|
+
original_end = clean_to_original.get(i + 1, len(text))
|
|
103
|
+
statements.append((statement_text, original_start, original_end))
|
|
104
|
+
|
|
105
|
+
# Find next non-whitespace character for next statement start
|
|
106
|
+
j = i + 1
|
|
107
|
+
while j < len(text_without_comments) and text_without_comments[j].isspace():
|
|
108
|
+
j += 1
|
|
109
|
+
statement_start_pos = j
|
|
110
|
+
current_statement = ""
|
|
111
|
+
else:
|
|
112
|
+
current_statement += char
|
|
113
|
+
|
|
114
|
+
i += 1
|
|
115
|
+
|
|
116
|
+
# Handle the last statement if it doesn't end with semicolon
|
|
117
|
+
statement_text = current_statement.strip()
|
|
118
|
+
if statement_text:
|
|
119
|
+
original_start = clean_to_original.get(statement_start_pos, statement_start_pos)
|
|
120
|
+
original_end = len(text)
|
|
121
|
+
statements.append((statement_text, original_start, original_end))
|
|
122
|
+
|
|
123
|
+
return statements
|
|
124
|
+
|
|
125
|
+
def _create_position_mapping(self, text: str) -> tuple[dict, dict]:
|
|
126
|
+
"""
|
|
127
|
+
Create bidirectional position mapping between original text and comment-removed text.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
text: Original SQL text
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (original_to_clean, clean_to_original) position mappings
|
|
134
|
+
"""
|
|
135
|
+
original_to_clean = {}
|
|
136
|
+
clean_to_original = {}
|
|
137
|
+
clean_pos = 0
|
|
138
|
+
i = 0
|
|
139
|
+
in_string = False
|
|
140
|
+
string_char = None
|
|
141
|
+
|
|
142
|
+
while i < len(text):
|
|
143
|
+
char = text[i]
|
|
144
|
+
|
|
145
|
+
if not in_string:
|
|
146
|
+
# Check for string start
|
|
147
|
+
if char in ('"', "'"):
|
|
148
|
+
in_string = True
|
|
149
|
+
string_char = char
|
|
150
|
+
original_to_clean[i] = clean_pos
|
|
151
|
+
clean_to_original[clean_pos] = i
|
|
152
|
+
clean_pos += 1
|
|
153
|
+
# Check for line comment
|
|
154
|
+
elif char == '-' and i + 1 < len(text) and text[i + 1] == '-':
|
|
155
|
+
# Skip to end of line, but preserve newline
|
|
156
|
+
while i < len(text) and text[i] != '\n':
|
|
157
|
+
original_to_clean[i] = clean_pos
|
|
158
|
+
i += 1
|
|
159
|
+
if i < len(text): # Add the newline
|
|
160
|
+
original_to_clean[i] = clean_pos
|
|
161
|
+
clean_to_original[clean_pos] = i
|
|
162
|
+
clean_pos += 1
|
|
163
|
+
continue
|
|
164
|
+
# Check for block comment
|
|
165
|
+
elif char == '/' and i + 1 < len(text) and text[i + 1] == '*':
|
|
166
|
+
# Skip to end of block comment
|
|
167
|
+
start_i = i
|
|
168
|
+
i += 2
|
|
169
|
+
while i + 1 < len(text):
|
|
170
|
+
original_to_clean[i] = clean_pos
|
|
171
|
+
if text[i] == '*' and text[i + 1] == '/':
|
|
172
|
+
original_to_clean[i + 1] = clean_pos
|
|
173
|
+
i += 2
|
|
174
|
+
break
|
|
175
|
+
i += 1
|
|
176
|
+
continue
|
|
177
|
+
else:
|
|
178
|
+
original_to_clean[i] = clean_pos
|
|
179
|
+
clean_to_original[clean_pos] = i
|
|
180
|
+
clean_pos += 1
|
|
181
|
+
else:
|
|
182
|
+
# In string
|
|
183
|
+
if char == string_char:
|
|
184
|
+
# Check for escaped quote
|
|
185
|
+
if i + 1 < len(text) and text[i + 1] == string_char:
|
|
186
|
+
original_to_clean[i] = clean_pos
|
|
187
|
+
original_to_clean[i + 1] = clean_pos + 1
|
|
188
|
+
clean_to_original[clean_pos] = i
|
|
189
|
+
clean_to_original[clean_pos + 1] = i + 1
|
|
190
|
+
clean_pos += 2
|
|
191
|
+
i += 2
|
|
192
|
+
continue
|
|
193
|
+
else:
|
|
194
|
+
in_string = False
|
|
195
|
+
string_char = None
|
|
196
|
+
original_to_clean[i] = clean_pos
|
|
197
|
+
clean_to_original[clean_pos] = i
|
|
198
|
+
clean_pos += 1
|
|
199
|
+
else:
|
|
200
|
+
original_to_clean[i] = clean_pos
|
|
201
|
+
clean_to_original[clean_pos] = i
|
|
202
|
+
clean_pos += 1
|
|
203
|
+
|
|
204
|
+
i += 1
|
|
205
|
+
|
|
206
|
+
return original_to_clean, clean_to_original
|
|
207
|
+
|
|
208
|
+
def _remove_comments(self, text: str) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Remove SQL comments while preserving string literals.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
text: SQL text
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Text with comments removed
|
|
217
|
+
"""
|
|
218
|
+
result = []
|
|
219
|
+
i = 0
|
|
220
|
+
in_string = False
|
|
221
|
+
string_char = None
|
|
222
|
+
|
|
223
|
+
while i < len(text):
|
|
224
|
+
char = text[i]
|
|
225
|
+
|
|
226
|
+
if not in_string:
|
|
227
|
+
# Check for string start
|
|
228
|
+
if char in ('"', "'"):
|
|
229
|
+
in_string = True
|
|
230
|
+
string_char = char
|
|
231
|
+
result.append(char)
|
|
232
|
+
# Check for line comment
|
|
233
|
+
elif char == '-' and i + 1 < len(text) and text[i + 1] == '-':
|
|
234
|
+
# Skip to end of line
|
|
235
|
+
while i < len(text) and text[i] != '\n':
|
|
236
|
+
i += 1
|
|
237
|
+
if i < len(text):
|
|
238
|
+
result.append('\n') # Preserve newline
|
|
239
|
+
continue
|
|
240
|
+
# Check for block comment
|
|
241
|
+
elif char == '/' and i + 1 < len(text) and text[i + 1] == '*':
|
|
242
|
+
# Skip to end of block comment
|
|
243
|
+
i += 2
|
|
244
|
+
while i + 1 < len(text):
|
|
245
|
+
if text[i] == '*' and text[i + 1] == '/':
|
|
246
|
+
i += 2
|
|
247
|
+
break
|
|
248
|
+
i += 1
|
|
249
|
+
continue
|
|
250
|
+
else:
|
|
251
|
+
result.append(char)
|
|
252
|
+
else:
|
|
253
|
+
# In string
|
|
254
|
+
if char == string_char:
|
|
255
|
+
# Check for escaped quote
|
|
256
|
+
if i + 1 < len(text) and text[i + 1] == string_char:
|
|
257
|
+
result.append(char + string_char)
|
|
258
|
+
i += 2
|
|
259
|
+
continue
|
|
260
|
+
else:
|
|
261
|
+
in_string = False
|
|
262
|
+
string_char = None
|
|
263
|
+
result.append(char)
|
|
264
|
+
else:
|
|
265
|
+
result.append(char)
|
|
266
|
+
|
|
267
|
+
i += 1
|
|
268
|
+
|
|
269
|
+
return ''.join(result)
|
|
270
|
+
|
|
271
|
+
def get_current_statement(self, text: str, cursor_position: int) -> Optional[Tuple[str, int, int]]:
|
|
272
|
+
"""
|
|
273
|
+
Get the statement that contains the cursor position.
|
|
274
|
+
If cursor is not inside any statement, returns the closest statement before the cursor.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
text: SQL text
|
|
278
|
+
cursor_position: Current cursor position
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Tuple of (statement_text, start_position, end_position) or None
|
|
282
|
+
"""
|
|
283
|
+
statements = self.parse_sql_statements(text)
|
|
284
|
+
|
|
285
|
+
# First try to find a statement containing the cursor
|
|
286
|
+
for statement_text, start_pos, end_pos in statements:
|
|
287
|
+
if start_pos <= cursor_position <= end_pos:
|
|
288
|
+
return (statement_text, start_pos, end_pos)
|
|
289
|
+
|
|
290
|
+
# If no statement contains the cursor, find the closest statement before the cursor
|
|
291
|
+
closest_statement = None
|
|
292
|
+
closest_distance = float('inf')
|
|
293
|
+
|
|
294
|
+
for statement_text, start_pos, end_pos in statements:
|
|
295
|
+
if end_pos <= cursor_position: # Statement is before cursor
|
|
296
|
+
distance = cursor_position - end_pos
|
|
297
|
+
if distance < closest_distance:
|
|
298
|
+
closest_distance = distance
|
|
299
|
+
closest_statement = (statement_text, start_pos, end_pos)
|
|
300
|
+
|
|
301
|
+
return closest_statement
|
|
302
|
+
|
|
303
|
+
def execute_all_statements(self, text: str) -> List[str]:
|
|
304
|
+
"""
|
|
305
|
+
Execute all statements in the text (F5 functionality).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
text: SQL text containing one or more statements
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of executed statement texts
|
|
312
|
+
"""
|
|
313
|
+
if not self.execute_callback:
|
|
314
|
+
raise ValueError("No execute callback set")
|
|
315
|
+
|
|
316
|
+
statements = self.parse_sql_statements(text)
|
|
317
|
+
executed_statements = []
|
|
318
|
+
|
|
319
|
+
for statement_text, _, _ in statements:
|
|
320
|
+
if statement_text.strip():
|
|
321
|
+
self.execute_callback(statement_text)
|
|
322
|
+
executed_statements.append(statement_text)
|
|
323
|
+
|
|
324
|
+
return executed_statements
|
|
325
|
+
|
|
326
|
+
def execute_current_statement(self, text: str, cursor_position: int) -> Optional[str]:
|
|
327
|
+
"""
|
|
328
|
+
Execute the statement containing the cursor (F9 functionality).
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
text: SQL text
|
|
332
|
+
cursor_position: Current cursor position
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Executed statement text or None if no statement found
|
|
336
|
+
"""
|
|
337
|
+
if not self.execute_callback:
|
|
338
|
+
raise ValueError("No execute callback set")
|
|
339
|
+
|
|
340
|
+
current_statement = self.get_current_statement(text, cursor_position)
|
|
341
|
+
|
|
342
|
+
if current_statement:
|
|
343
|
+
statement_text, _, _ = current_statement
|
|
344
|
+
if statement_text.strip():
|
|
345
|
+
self.execute_callback(statement_text)
|
|
346
|
+
return statement_text
|
|
347
|
+
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
def execute_from_editor(self, editor: QPlainTextEdit, mode: str = "current") -> Optional[str]:
|
|
351
|
+
"""
|
|
352
|
+
Execute statements from a QPlainTextEdit widget.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
editor: The text editor widget
|
|
356
|
+
mode: "current" for F9 or "all" for F5
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Executed statement(s) or None
|
|
360
|
+
"""
|
|
361
|
+
text = editor.toPlainText()
|
|
362
|
+
cursor = editor.textCursor()
|
|
363
|
+
cursor_position = cursor.position()
|
|
364
|
+
|
|
365
|
+
if mode == "all":
|
|
366
|
+
executed = self.execute_all_statements(text)
|
|
367
|
+
return "; ".join(executed) if executed else None
|
|
368
|
+
elif mode == "current":
|
|
369
|
+
return self.execute_current_statement(text, cursor_position)
|
|
370
|
+
else:
|
|
371
|
+
raise ValueError(f"Invalid mode: {mode}. Use 'current' or 'all'")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class ExecutionKeyHandler:
|
|
375
|
+
"""
|
|
376
|
+
Key handler for F5 and F9 execution functionality.
|
|
377
|
+
Integrates with QPlainTextEdit widgets.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def __init__(self, execution_handler: SQLExecutionHandler):
|
|
381
|
+
"""
|
|
382
|
+
Initialize the key handler.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
execution_handler: The execution handler to use
|
|
386
|
+
"""
|
|
387
|
+
self.execution_handler = execution_handler
|
|
388
|
+
|
|
389
|
+
def handle_key_press(self, editor: QPlainTextEdit, key: int, modifiers: int) -> bool:
|
|
390
|
+
"""
|
|
391
|
+
Handle key press events for execution shortcuts.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
editor: The text editor widget
|
|
395
|
+
key: Key code
|
|
396
|
+
modifiers: Keyboard modifiers
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
True if the key was handled, False otherwise
|
|
400
|
+
"""
|
|
401
|
+
from PyQt6.QtCore import Qt
|
|
402
|
+
|
|
403
|
+
# F5 - Execute all statements
|
|
404
|
+
if key == Qt.Key.Key_F5:
|
|
405
|
+
try:
|
|
406
|
+
executed = self.execution_handler.execute_from_editor(editor, "all")
|
|
407
|
+
return True
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print(f"Error executing all statements: {e}")
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
# F9 - Execute current statement
|
|
413
|
+
elif key == Qt.Key.Key_F9:
|
|
414
|
+
try:
|
|
415
|
+
executed = self.execution_handler.execute_from_editor(editor, "current")
|
|
416
|
+
return True
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print(f"Error executing current statement: {e}")
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
return False
|