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.

@@ -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