sqlshell 0.4.4__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.
Files changed (54) hide show
  1. sqlshell/__init__.py +84 -0
  2. sqlshell/__main__.py +4926 -0
  3. sqlshell/ai_autocomplete.py +392 -0
  4. sqlshell/ai_settings_dialog.py +337 -0
  5. sqlshell/context_suggester.py +768 -0
  6. sqlshell/create_test_data.py +152 -0
  7. sqlshell/data/create_test_data.py +137 -0
  8. sqlshell/db/__init__.py +6 -0
  9. sqlshell/db/database_manager.py +1318 -0
  10. sqlshell/db/export_manager.py +188 -0
  11. sqlshell/editor.py +1166 -0
  12. sqlshell/editor_integration.py +127 -0
  13. sqlshell/execution_handler.py +421 -0
  14. sqlshell/menus.py +262 -0
  15. sqlshell/notification_manager.py +370 -0
  16. sqlshell/query_tab.py +904 -0
  17. sqlshell/resources/__init__.py +1 -0
  18. sqlshell/resources/icon.png +0 -0
  19. sqlshell/resources/logo_large.png +0 -0
  20. sqlshell/resources/logo_medium.png +0 -0
  21. sqlshell/resources/logo_small.png +0 -0
  22. sqlshell/resources/splash_screen.gif +0 -0
  23. sqlshell/space_invaders.py +501 -0
  24. sqlshell/splash_screen.py +405 -0
  25. sqlshell/sqlshell/__init__.py +5 -0
  26. sqlshell/sqlshell/create_test_data.py +118 -0
  27. sqlshell/sqlshell/create_test_databases.py +96 -0
  28. sqlshell/sqlshell_demo.png +0 -0
  29. sqlshell/styles.py +257 -0
  30. sqlshell/suggester_integration.py +330 -0
  31. sqlshell/syntax_highlighter.py +124 -0
  32. sqlshell/table_list.py +996 -0
  33. sqlshell/ui/__init__.py +6 -0
  34. sqlshell/ui/bar_chart_delegate.py +49 -0
  35. sqlshell/ui/filter_header.py +469 -0
  36. sqlshell/utils/__init__.py +16 -0
  37. sqlshell/utils/profile_cn2.py +1661 -0
  38. sqlshell/utils/profile_column.py +2635 -0
  39. sqlshell/utils/profile_distributions.py +616 -0
  40. sqlshell/utils/profile_entropy.py +347 -0
  41. sqlshell/utils/profile_foreign_keys.py +779 -0
  42. sqlshell/utils/profile_keys.py +2834 -0
  43. sqlshell/utils/profile_ohe.py +934 -0
  44. sqlshell/utils/profile_ohe_advanced.py +754 -0
  45. sqlshell/utils/profile_ohe_comparison.py +237 -0
  46. sqlshell/utils/profile_prediction.py +926 -0
  47. sqlshell/utils/profile_similarity.py +876 -0
  48. sqlshell/utils/search_in_df.py +90 -0
  49. sqlshell/widgets.py +400 -0
  50. sqlshell-0.4.4.dist-info/METADATA +441 -0
  51. sqlshell-0.4.4.dist-info/RECORD +54 -0
  52. sqlshell-0.4.4.dist-info/WHEEL +5 -0
  53. sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
  54. sqlshell-0.4.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,127 @@
1
+ """
2
+ Integration module to add F5/F9 execution functionality to SQLEditor.
3
+ This module provides a clean way to enhance the editor without modifying the original code.
4
+ """
5
+
6
+ from PyQt6.QtCore import Qt
7
+ from PyQt6.QtWidgets import QPlainTextEdit
8
+ from .execution_handler import SQLExecutionHandler, ExecutionKeyHandler
9
+
10
+
11
+ class EditorExecutionIntegration:
12
+ """
13
+ Integration class to add F5/F9 execution functionality to SQLEditor.
14
+ """
15
+
16
+ def __init__(self, editor: QPlainTextEdit, execute_callback=None):
17
+ """
18
+ Initialize the integration.
19
+
20
+ Args:
21
+ editor: The SQLEditor instance
22
+ execute_callback: Function to call to execute queries
23
+ """
24
+ self.editor = editor
25
+ self.execution_handler = SQLExecutionHandler(execute_callback)
26
+ self.key_handler = ExecutionKeyHandler(self.execution_handler)
27
+
28
+ # Store original keyPressEvent to preserve existing functionality
29
+ self.original_key_press_event = editor.keyPressEvent
30
+
31
+ # Replace keyPressEvent with our enhanced version
32
+ editor.keyPressEvent = self.enhanced_key_press_event
33
+
34
+ def set_execute_callback(self, callback):
35
+ """Set the execution callback function."""
36
+ self.execution_handler.set_execute_callback(callback)
37
+
38
+ def enhanced_key_press_event(self, event):
39
+ """Enhanced keyPressEvent that handles F5/F9 while preserving original functionality."""
40
+
41
+ # First, try to handle F5/F9 keys
42
+ if self.key_handler.handle_key_press(self.editor, event.key(), event.modifiers()):
43
+ # Key was handled by our execution handler
44
+ return
45
+
46
+ # If not F5/F9, use the original keyPressEvent
47
+ self.original_key_press_event(event)
48
+
49
+ def execute_all_statements(self):
50
+ """Execute all statements in the editor (F5 functionality)."""
51
+ try:
52
+ return self.execution_handler.execute_from_editor(self.editor, "all")
53
+ except Exception as e:
54
+ print(f"Error executing all statements: {e}")
55
+ return None
56
+
57
+ def execute_current_statement(self):
58
+ """Execute the current statement (F9 functionality)."""
59
+ try:
60
+ return self.execution_handler.execute_from_editor(self.editor, "current")
61
+ except Exception as e:
62
+ print(f"Error executing current statement: {e}")
63
+ return None
64
+
65
+ def get_current_statement_info(self):
66
+ """Get information about the current statement at cursor position."""
67
+ text = self.editor.toPlainText()
68
+ cursor = self.editor.textCursor()
69
+ cursor_position = cursor.position()
70
+
71
+ current_stmt = self.execution_handler.get_current_statement(text, cursor_position)
72
+ if current_stmt:
73
+ stmt_text, start_pos, end_pos = current_stmt
74
+ return {
75
+ 'text': stmt_text,
76
+ 'start': start_pos,
77
+ 'end': end_pos,
78
+ 'cursor_position': cursor_position
79
+ }
80
+ return None
81
+
82
+ def get_all_statements_info(self):
83
+ """Get information about all statements in the editor."""
84
+ text = self.editor.toPlainText()
85
+ statements = self.execution_handler.parse_sql_statements(text)
86
+
87
+ return [
88
+ {
89
+ 'text': stmt_text,
90
+ 'start': start_pos,
91
+ 'end': end_pos,
92
+ 'index': i
93
+ }
94
+ for i, (stmt_text, start_pos, end_pos) in enumerate(statements)
95
+ ]
96
+
97
+
98
+ def integrate_execution_functionality(editor: QPlainTextEdit, execute_callback=None):
99
+ """
100
+ Convenience function to integrate F5/F9 execution functionality into an editor.
101
+
102
+ Args:
103
+ editor: The SQLEditor instance
104
+ execute_callback: Function to call to execute queries
105
+
106
+ Returns:
107
+ EditorExecutionIntegration instance for further customization
108
+ """
109
+ integration = EditorExecutionIntegration(editor, execute_callback)
110
+
111
+ # Store the integration instance on the editor for later access
112
+ editor._execution_integration = integration
113
+
114
+ return integration
115
+
116
+
117
+ def get_execution_integration(editor: QPlainTextEdit):
118
+ """
119
+ Get the execution integration instance from an editor.
120
+
121
+ Args:
122
+ editor: The SQLEditor instance
123
+
124
+ Returns:
125
+ EditorExecutionIntegration instance or None if not integrated
126
+ """
127
+ return getattr(editor, '_execution_integration', None)
@@ -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