sqlshell 0.1.6__tar.gz → 0.1.8__tar.gz

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.

Files changed (71) hide show
  1. sqlshell-0.1.8/MANIFEST.in +10 -0
  2. sqlshell-0.1.8/PKG-INFO +120 -0
  3. sqlshell-0.1.8/README.md +91 -0
  4. sqlshell-0.1.8/pool.db +0 -0
  5. {sqlshell-0.1.6 → sqlshell-0.1.8}/pyproject.toml +1 -1
  6. {sqlshell-0.1.6 → sqlshell-0.1.8}/setup.py +8 -1
  7. sqlshell-0.1.8/sqlshell/__init__.py +8 -0
  8. sqlshell-0.1.8/sqlshell/create_test_data.py +50 -0
  9. sqlshell-0.1.8/sqlshell/editor.py +355 -0
  10. sqlshell-0.1.8/sqlshell/main.py +1833 -0
  11. sqlshell-0.1.8/sqlshell/resources/__init__.py +1 -0
  12. sqlshell-0.1.8/sqlshell/resources/create_icon.py +53 -0
  13. sqlshell-0.1.8/sqlshell/resources/create_splash.py +66 -0
  14. sqlshell-0.1.8/sqlshell/resources/splash_screen.gif +0 -0
  15. sqlshell-0.1.8/sqlshell/splash_screen.py +177 -0
  16. sqlshell-0.1.8/sqlshell/sqlshell/create_test_data.py +118 -0
  17. sqlshell-0.1.8/sqlshell/sqlshell_demo.png +0 -0
  18. sqlshell-0.1.8/sqlshell/syntax_highlighter.py +123 -0
  19. sqlshell-0.1.8/sqlshell.egg-info/PKG-INFO +120 -0
  20. sqlshell-0.1.8/sqlshell.egg-info/SOURCES.txt +63 -0
  21. sqlshell-0.1.8/sqlshell_demo.png +0 -0
  22. sqlshell-0.1.8/sqlshell_logo.png +0 -0
  23. sqlshell-0.1.8/test_data/by_category.parquet +0 -0
  24. sqlshell-0.1.8/test_data/city_coordinates.csv +9 -0
  25. sqlshell-0.1.8/test_data/city_coordinates.parquet +0 -0
  26. sqlshell-0.1.8/test_data/city_coordinates.xlsx +0 -0
  27. sqlshell-0.1.8/test_data/customer_data.csv +101 -0
  28. sqlshell-0.1.8/test_data/customer_data.parquet +0 -0
  29. sqlshell-0.1.8/test_data/customer_data.xlsx +0 -0
  30. sqlshell-0.1.8/test_data/customers_by_country.parquet +0 -0
  31. sqlshell-0.1.8/test_data/delete_me.parquet +0 -0
  32. sqlshell-0.1.8/test_data/joined_test.parquet +0 -0
  33. sqlshell-0.1.8/test_data/price_by_category.parquet +0 -0
  34. sqlshell-0.1.8/test_data/product_catalog.csv +51 -0
  35. sqlshell-0.1.8/test_data/product_catalog.parquet +0 -0
  36. sqlshell-0.1.8/test_data/product_catalog.xlsx +0 -0
  37. sqlshell-0.1.8/test_data/product_categories.csv +81 -0
  38. sqlshell-0.1.8/test_data/product_categories.parquet +0 -0
  39. sqlshell-0.1.8/test_data/product_categories.xlsx +0 -0
  40. sqlshell-0.1.8/test_data/sample_sales_data.csv +1001 -0
  41. sqlshell-0.1.8/test_data/sample_sales_data.parquet +0 -0
  42. sqlshell-0.1.8/test_data/sample_sales_data.xlsx +0 -0
  43. sqlshell-0.1.8/test_data/stock_data.csv +101 -0
  44. sqlshell-0.1.8/test_data/stock_data.parquet +0 -0
  45. sqlshell-0.1.8/test_data/stock_data.xlsx +0 -0
  46. sqlshell-0.1.8/test_data/test.db +0 -0
  47. sqlshell-0.1.8/test_data/test.duckdb +0 -0
  48. sqlshell-0.1.8/test_data/test2.parquet +0 -0
  49. sqlshell-0.1.8/test_data/test3.parquet +0 -0
  50. sqlshell-0.1.8/test_data/test3.xlsx +0 -0
  51. sqlshell-0.1.8/test_data/test4.xlsx +0 -0
  52. sqlshell-0.1.8/test_data/test5.xlsx +0 -0
  53. sqlshell-0.1.8/test_data/testproject.sqls +23 -0
  54. sqlshell-0.1.8/test_data/weather_measurements.csv +366 -0
  55. sqlshell-0.1.8/test_data/weather_measurements.parquet +0 -0
  56. sqlshell-0.1.8/test_data/weather_measurements.xlsx +0 -0
  57. sqlshell-0.1.6/PKG-INFO +0 -92
  58. sqlshell-0.1.6/README.md +0 -63
  59. sqlshell-0.1.6/sqlshell/__init__.py +0 -6
  60. sqlshell-0.1.6/sqlshell/main.py +0 -1473
  61. sqlshell-0.1.6/sqlshell.egg-info/PKG-INFO +0 -92
  62. sqlshell-0.1.6/sqlshell.egg-info/SOURCES.txt +0 -15
  63. {sqlshell-0.1.6 → sqlshell-0.1.8}/setup.cfg +0 -0
  64. {sqlshell-0.1.6/sqlshell/sqlshell → sqlshell-0.1.8/sqlshell/data}/create_test_data.py +0 -0
  65. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/setup.py +0 -0
  66. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/sqlshell/__init__.py +0 -0
  67. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/sqlshell/create_test_databases.py +0 -0
  68. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/dependency_links.txt +0 -0
  69. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/entry_points.txt +0 -0
  70. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/requires.txt +0 -0
  71. {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/top_level.txt +0 -0
@@ -0,0 +1,10 @@
1
+ include README.md
2
+ include LICENSE
3
+ include sqlshell_logo.png
4
+ include sqlshell_demo.png
5
+ include pool.db
6
+ recursive-include sqlshell/resources *
7
+ recursive-include sqlshell/data *
8
+ recursive-include test_data *
9
+ global-exclude __pycache__
10
+ global-exclude *.py[cod]
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlshell
3
+ Version: 0.1.8
4
+ Summary: A powerful SQL shell with GUI interface for data analysis
5
+ Home-page: https://github.com/yourusername/sqlshell
6
+ Author: SQLShell Team
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/oyvinrog/SQLShell
9
+ Keywords: sql,data analysis,gui,duckdb
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pandas>=2.0.0
20
+ Requires-Dist: numpy>=1.24.0
21
+ Requires-Dist: PyQt6>=6.4.0
22
+ Requires-Dist: duckdb>=0.9.0
23
+ Requires-Dist: openpyxl>=3.1.0
24
+ Requires-Dist: pyarrow>=14.0.1
25
+ Requires-Dist: fastparquet>=2023.10.1
26
+ Requires-Dist: xlrd>=2.0.1
27
+ Dynamic: home-page
28
+ Dynamic: requires-python
29
+
30
+ # SQLShell
31
+
32
+ <div align="center">
33
+
34
+ ![SQLShell Interface](sqlshell_logo.png)
35
+
36
+ **A modern SQL REPL interface for seamless querying of Excel, Parquet, and SQLite databases**
37
+
38
+ ![SQLShell Interface](sqlshell_demo.png)
39
+
40
+ </div>
41
+
42
+ ## 🚀 Key Features
43
+
44
+ - **Interactive SQL Interface** - Rich syntax highlighting for enhanced query writing
45
+ - **DuckDB Integration** - Built-in support for local DuckDB database (pool.db)
46
+ - **Multi-Format Support** - Import and query Excel (.xlsx, .xls) and CSV files effortlessly
47
+ - **Modern UI** - Clean, tabular results display with intuitive controls
48
+ - **Productivity Tools** - Streamlined workflow with keyboard shortcuts (e.g., Ctrl+Enter for query execution)
49
+
50
+ ## 📦 Installation
51
+
52
+ ### Linux Setup with Virtual Environment
53
+
54
+ ```bash
55
+ # Create and activate virtual environment
56
+ python3 -m venv ~/.venv/sqlshell
57
+ source ~/.venv/sqlshell/bin/activate
58
+
59
+ # Install SQLShell
60
+ pip install sqlshell
61
+
62
+ # Configure shell alias
63
+ echo 'alias sqls="~/.venv/sqlshell/bin/sqls"' >> ~/.bashrc # or ~/.zshrc for Zsh
64
+ source ~/.bashrc # or source ~/.zshrc
65
+ ```
66
+
67
+ ### Windows Quick Start
68
+ SQLShell is immediately available via the `sqls` command after installation:
69
+ ```bash
70
+ pip install sqlshell
71
+ ```
72
+
73
+ ## 🎯 Getting Started
74
+
75
+ 1. **Launch the Application**
76
+ ```bash
77
+ sqls
78
+ ```
79
+
80
+ 2. **Database Connection**
81
+ - SQLShell automatically connects to a local DuckDB database named 'pool.db'
82
+
83
+ 3. **Working with Excel Files**
84
+ - Click "Browse Excel" to select your file
85
+ - File contents are loaded as 'imported_data' table
86
+ - Query using standard SQL syntax
87
+
88
+ 4. **Query Execution**
89
+ - Enter SQL in the editor
90
+ - Execute using Ctrl+Enter or the "Execute" button
91
+ - View results in the structured output panel
92
+
93
+ ## 📝 Query Examples
94
+
95
+ ### Basic Join Operation
96
+ ```sql
97
+ SELECT *
98
+ FROM sample_sales_data cd
99
+ INNER JOIN product_catalog pc ON pc.productid = cd.productid
100
+ LIMIT 3;
101
+ ```
102
+
103
+ ### Multi-Statement Queries
104
+ ```sql
105
+ -- Create a temporary view
106
+ CREATE OR REPLACE TEMPORARY VIEW test_v AS
107
+ SELECT *
108
+ FROM sample_sales_data cd
109
+ INNER JOIN product_catalog pc ON pc.productid = cd.productid;
110
+
111
+ -- Query the view
112
+ SELECT DISTINCT productid
113
+ FROM test_v;
114
+ ```
115
+
116
+ ## 💡 Pro Tips
117
+
118
+ - Use temporary views for complex query organization
119
+ - Leverage keyboard shortcuts for efficient workflow
120
+ - Explore the multi-format support for various data sources
@@ -0,0 +1,91 @@
1
+ # SQLShell
2
+
3
+ <div align="center">
4
+
5
+ ![SQLShell Interface](sqlshell_logo.png)
6
+
7
+ **A modern SQL REPL interface for seamless querying of Excel, Parquet, and SQLite databases**
8
+
9
+ ![SQLShell Interface](sqlshell_demo.png)
10
+
11
+ </div>
12
+
13
+ ## 🚀 Key Features
14
+
15
+ - **Interactive SQL Interface** - Rich syntax highlighting for enhanced query writing
16
+ - **DuckDB Integration** - Built-in support for local DuckDB database (pool.db)
17
+ - **Multi-Format Support** - Import and query Excel (.xlsx, .xls) and CSV files effortlessly
18
+ - **Modern UI** - Clean, tabular results display with intuitive controls
19
+ - **Productivity Tools** - Streamlined workflow with keyboard shortcuts (e.g., Ctrl+Enter for query execution)
20
+
21
+ ## 📦 Installation
22
+
23
+ ### Linux Setup with Virtual Environment
24
+
25
+ ```bash
26
+ # Create and activate virtual environment
27
+ python3 -m venv ~/.venv/sqlshell
28
+ source ~/.venv/sqlshell/bin/activate
29
+
30
+ # Install SQLShell
31
+ pip install sqlshell
32
+
33
+ # Configure shell alias
34
+ echo 'alias sqls="~/.venv/sqlshell/bin/sqls"' >> ~/.bashrc # or ~/.zshrc for Zsh
35
+ source ~/.bashrc # or source ~/.zshrc
36
+ ```
37
+
38
+ ### Windows Quick Start
39
+ SQLShell is immediately available via the `sqls` command after installation:
40
+ ```bash
41
+ pip install sqlshell
42
+ ```
43
+
44
+ ## 🎯 Getting Started
45
+
46
+ 1. **Launch the Application**
47
+ ```bash
48
+ sqls
49
+ ```
50
+
51
+ 2. **Database Connection**
52
+ - SQLShell automatically connects to a local DuckDB database named 'pool.db'
53
+
54
+ 3. **Working with Excel Files**
55
+ - Click "Browse Excel" to select your file
56
+ - File contents are loaded as 'imported_data' table
57
+ - Query using standard SQL syntax
58
+
59
+ 4. **Query Execution**
60
+ - Enter SQL in the editor
61
+ - Execute using Ctrl+Enter or the "Execute" button
62
+ - View results in the structured output panel
63
+
64
+ ## 📝 Query Examples
65
+
66
+ ### Basic Join Operation
67
+ ```sql
68
+ SELECT *
69
+ FROM sample_sales_data cd
70
+ INNER JOIN product_catalog pc ON pc.productid = cd.productid
71
+ LIMIT 3;
72
+ ```
73
+
74
+ ### Multi-Statement Queries
75
+ ```sql
76
+ -- Create a temporary view
77
+ CREATE OR REPLACE TEMPORARY VIEW test_v AS
78
+ SELECT *
79
+ FROM sample_sales_data cd
80
+ INNER JOIN product_catalog pc ON pc.productid = cd.productid;
81
+
82
+ -- Query the view
83
+ SELECT DISTINCT productid
84
+ FROM test_v;
85
+ ```
86
+
87
+ ## 💡 Pro Tips
88
+
89
+ - Use temporary views for complex query organization
90
+ - Leverage keyboard shortcuts for efficient workflow
91
+ - Explore the multi-format support for various data sources
sqlshell-0.1.8/pool.db ADDED
Binary file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sqlshell"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "A powerful SQL shell with GUI interface for data analysis"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -45,6 +45,13 @@ setup(
45
45
  python_requires=">=3.8",
46
46
  include_package_data=True,
47
47
  package_data={
48
- 'sqlshell': ['*.db'],
48
+ 'sqlshell': [
49
+ '*.db',
50
+ 'resources/*',
51
+ 'data/*',
52
+ 'test_data/*',
53
+ '*.png',
54
+ '*.ico'
55
+ ],
49
56
  },
50
57
  )
@@ -0,0 +1,8 @@
1
+ """
2
+ SQLShell - A powerful SQL shell with GUI interface for data analysis
3
+ """
4
+
5
+ __version__ = "0.1.8"
6
+ __author__ = "SQLShell Team"
7
+
8
+ # SQLShell package initialization
@@ -0,0 +1,50 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from datetime import datetime, timedelta
4
+
5
+ def create_sales_data(num_records=1000):
6
+ """Create sample sales data"""
7
+ # Generate random dates within the last year
8
+ end_date = datetime.now()
9
+ start_date = end_date - timedelta(days=365)
10
+ dates = pd.date_range(start=start_date, end=end_date, periods=num_records)
11
+
12
+ # Generate random data
13
+ data = {
14
+ 'orderid': range(1, num_records + 1),
15
+ 'orderdate': dates,
16
+ 'customerid': np.random.randint(1, 101, num_records),
17
+ 'productid': np.random.randint(1, 51, num_records),
18
+ 'quantity': np.random.randint(1, 11, num_records),
19
+ 'unitprice': np.random.uniform(10.0, 1000.0, num_records).round(2)
20
+ }
21
+
22
+ return pd.DataFrame(data)
23
+
24
+ def create_customer_data(num_customers=100):
25
+ """Create sample customer data"""
26
+ # Generate random customer data
27
+ data = {
28
+ 'customerid': range(1, num_customers + 1),
29
+ 'customername': [f"Customer {i}" for i in range(1, num_customers + 1)],
30
+ 'email': [f"customer{i}@example.com" for i in range(1, num_customers + 1)],
31
+ 'country': np.random.choice(['USA', 'UK', 'Canada', 'Australia', 'Germany'], num_customers),
32
+ 'joindate': pd.date_range(start='2020-01-01', periods=num_customers).tolist()
33
+ }
34
+
35
+ return pd.DataFrame(data)
36
+
37
+ def create_product_data(num_products=50):
38
+ """Create sample product data"""
39
+ categories = ['Electronics', 'Books', 'Clothing', 'Home & Garden', 'Sports']
40
+
41
+ # Generate random product data
42
+ data = {
43
+ 'productid': range(1, num_products + 1),
44
+ 'productname': [f"Product {i}" for i in range(1, num_products + 1)],
45
+ 'category': np.random.choice(categories, num_products),
46
+ 'baseprice': np.random.uniform(5.0, 500.0, num_products).round(2),
47
+ 'instock': np.random.choice([True, False], num_products, p=[0.8, 0.2])
48
+ }
49
+
50
+ return pd.DataFrame(data)
@@ -0,0 +1,355 @@
1
+ from PyQt6.QtWidgets import QPlainTextEdit, QWidget, QCompleter
2
+ from PyQt6.QtCore import Qt, QSize, QRect, QStringListModel
3
+ from PyQt6.QtGui import QFont, QColor, QTextCursor, QPainter, QBrush
4
+
5
+ class LineNumberArea(QWidget):
6
+ def __init__(self, editor):
7
+ super().__init__(editor)
8
+ self.editor = editor
9
+
10
+ def sizeHint(self):
11
+ return QSize(self.editor.line_number_area_width(), 0)
12
+
13
+ def paintEvent(self, event):
14
+ self.editor.line_number_area_paint_event(event)
15
+
16
+ class SQLEditor(QPlainTextEdit):
17
+ def __init__(self, parent=None):
18
+ super().__init__(parent)
19
+ self.line_number_area = LineNumberArea(self)
20
+
21
+ # Set monospaced font
22
+ font = QFont("Consolas", 12) # Increased font size for better readability
23
+ font.setFixedPitch(True)
24
+ self.setFont(font)
25
+
26
+ # Connect signals
27
+ self.blockCountChanged.connect(self.update_line_number_area_width)
28
+ self.updateRequest.connect(self.update_line_number_area)
29
+
30
+ # Initialize
31
+ self.update_line_number_area_width(0)
32
+
33
+ # Set tab width to 4 spaces
34
+ self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
35
+
36
+ # Set placeholder text
37
+ self.setPlaceholderText("Enter your SQL query here...")
38
+
39
+ # Initialize completer
40
+ self.completer = None
41
+
42
+ # SQL Keywords for autocomplete
43
+ self.sql_keywords = [
44
+ "SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
45
+ "ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
46
+ "CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
47
+ "TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
48
+ "UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
49
+ "AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
50
+ "AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
51
+ ]
52
+
53
+ # Initialize with SQL keywords
54
+ self.set_completer(QCompleter(self.sql_keywords))
55
+
56
+ # Set modern selection color
57
+ self.selection_color = QColor("#3498DB")
58
+ self.selection_color.setAlpha(50) # Make it semi-transparent
59
+
60
+ def set_completer(self, completer):
61
+ """Set the completer for the editor"""
62
+ if self.completer:
63
+ self.completer.disconnect(self)
64
+
65
+ self.completer = completer
66
+
67
+ if not self.completer:
68
+ return
69
+
70
+ self.completer.setWidget(self)
71
+ self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
72
+ self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
73
+ self.completer.activated.connect(self.insert_completion)
74
+
75
+ def update_completer_model(self, words):
76
+ """Update the completer model with new words"""
77
+ if not self.completer:
78
+ return
79
+
80
+ # Combine SQL keywords with table/column names
81
+ all_words = self.sql_keywords + words
82
+
83
+ # Create a model with all words
84
+ model = QStringListModel()
85
+ model.setStringList(all_words)
86
+
87
+ # Set the model to the completer
88
+ self.completer.setModel(model)
89
+
90
+ def text_under_cursor(self):
91
+ """Get the text under the cursor for completion"""
92
+ tc = self.textCursor()
93
+ tc.select(QTextCursor.SelectionType.WordUnderCursor)
94
+ return tc.selectedText()
95
+
96
+ def insert_completion(self, completion):
97
+ """Insert the completion text"""
98
+ if self.completer.widget() != self:
99
+ return
100
+
101
+ tc = self.textCursor()
102
+ extra = len(completion) - len(self.completer.completionPrefix())
103
+ tc.movePosition(QTextCursor.MoveOperation.Left)
104
+ tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
105
+ tc.insertText(completion[-extra:] + " ")
106
+ self.setTextCursor(tc)
107
+
108
+ def complete(self):
109
+ """Show completion popup"""
110
+ prefix = self.text_under_cursor()
111
+
112
+ if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
113
+ if self.completer.popup().isVisible():
114
+ self.completer.popup().hide()
115
+ return
116
+
117
+ self.completer.setCompletionPrefix(prefix)
118
+
119
+ # If no completions, hide popup
120
+ if self.completer.completionCount() == 0:
121
+ self.completer.popup().hide()
122
+ return
123
+
124
+ # Get popup and position it under the current text
125
+ popup = self.completer.popup()
126
+ popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
127
+
128
+ # Calculate position for the popup
129
+ cr = self.cursorRect()
130
+ cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
131
+ self.completer.popup().verticalScrollBar().sizeHint().width())
132
+
133
+ # Show the popup
134
+ self.completer.complete(cr)
135
+
136
+ def keyPressEvent(self, event):
137
+ # Handle completer popup navigation
138
+ if self.completer and self.completer.popup().isVisible():
139
+ # Handle navigation keys for the popup
140
+ if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
141
+ Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
142
+ event.ignore()
143
+ return
144
+
145
+ # Handle special key combinations
146
+ if event.key() == Qt.Key.Key_Tab:
147
+ # Insert 4 spaces instead of a tab character
148
+ self.insertPlainText(" ")
149
+ return
150
+
151
+ # Auto-indentation for new lines
152
+ if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
153
+ cursor = self.textCursor()
154
+ block = cursor.block()
155
+ text = block.text()
156
+
157
+ # Get the indentation of the current line
158
+ indentation = ""
159
+ for char in text:
160
+ if char.isspace():
161
+ indentation += char
162
+ else:
163
+ break
164
+
165
+ # Check if line ends with an opening bracket or keywords that should increase indentation
166
+ increase_indent = ""
167
+ if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
168
+ ["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
169
+ increase_indent = " "
170
+
171
+ # Insert new line with proper indentation
172
+ super().keyPressEvent(event)
173
+ self.insertPlainText(indentation + increase_indent)
174
+ return
175
+
176
+ # Handle keyboard shortcuts
177
+ if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
178
+ if event.key() == Qt.Key.Key_Space:
179
+ # Show completion popup
180
+ self.complete()
181
+ return
182
+ elif event.key() == Qt.Key.Key_K:
183
+ # Comment/uncomment the selected lines
184
+ self.toggle_comment()
185
+ return
186
+
187
+ # For normal key presses
188
+ super().keyPressEvent(event)
189
+
190
+ # Check for autocomplete after typing
191
+ if event.text() and not event.text().isspace():
192
+ self.complete()
193
+
194
+ def paintEvent(self, event):
195
+ # Call the parent's paintEvent first
196
+ super().paintEvent(event)
197
+
198
+ # Get the current cursor
199
+ cursor = self.textCursor()
200
+
201
+ # If there's a selection, paint custom highlight
202
+ if cursor.hasSelection():
203
+ # Create a painter for this widget
204
+ painter = QPainter(self.viewport())
205
+
206
+ # Get the selection start and end positions
207
+ start = cursor.selectionStart()
208
+ end = cursor.selectionEnd()
209
+
210
+ # Create temporary cursor to get the rectangles
211
+ temp_cursor = QTextCursor(cursor)
212
+
213
+ # Move to start and get the starting position
214
+ temp_cursor.setPosition(start)
215
+ start_pos = self.cursorRect(temp_cursor)
216
+
217
+ # Move to end and get the ending position
218
+ temp_cursor.setPosition(end)
219
+ end_pos = self.cursorRect(temp_cursor)
220
+
221
+ # Set the highlight color with transparency
222
+ painter.setBrush(QBrush(self.selection_color))
223
+ painter.setPen(Qt.PenStyle.NoPen)
224
+
225
+ # Draw the highlight rectangle
226
+ if start_pos.top() == end_pos.top():
227
+ # Single line selection
228
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
229
+ end_pos.right() - start_pos.left(), start_pos.height()))
230
+ else:
231
+ # Multi-line selection
232
+ # First line
233
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
234
+ self.viewport().width() - start_pos.left(), start_pos.height()))
235
+
236
+ # Middle lines (if any)
237
+ if end_pos.top() > start_pos.top() + start_pos.height():
238
+ painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
239
+ self.viewport().width(),
240
+ end_pos.top() - (start_pos.top() + start_pos.height())))
241
+
242
+ # Last line
243
+ painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
244
+
245
+ painter.end()
246
+
247
+ def focusInEvent(self, event):
248
+ super().focusInEvent(event)
249
+ # Show temporary hint in status bar when editor gets focus
250
+ if hasattr(self.parent(), 'statusBar'):
251
+ self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
252
+
253
+ def toggle_comment(self):
254
+ cursor = self.textCursor()
255
+ if cursor.hasSelection():
256
+ # Get the selected text
257
+ start = cursor.selectionStart()
258
+ end = cursor.selectionEnd()
259
+
260
+ # Remember the selection
261
+ cursor.setPosition(start)
262
+ start_block = cursor.blockNumber()
263
+ cursor.setPosition(end)
264
+ end_block = cursor.blockNumber()
265
+
266
+ # Process each line in the selection
267
+ cursor.setPosition(start)
268
+ cursor.beginEditBlock()
269
+
270
+ for _ in range(start_block, end_block + 1):
271
+ # Move to start of line
272
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
273
+
274
+ # Check if the line is already commented
275
+ line_text = cursor.block().text().lstrip()
276
+ if line_text.startswith('--'):
277
+ # Remove comment
278
+ pos = cursor.block().text().find('--')
279
+ cursor.setPosition(cursor.block().position() + pos)
280
+ cursor.deleteChar()
281
+ cursor.deleteChar()
282
+ else:
283
+ # Add comment
284
+ cursor.insertText('--')
285
+
286
+ # Move to next line if not at the end
287
+ if not cursor.atEnd():
288
+ cursor.movePosition(cursor.MoveOperation.NextBlock)
289
+
290
+ cursor.endEditBlock()
291
+ else:
292
+ # Comment/uncomment current line
293
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
294
+ cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
295
+ line_text = cursor.selectedText().lstrip()
296
+
297
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
298
+ if line_text.startswith('--'):
299
+ # Remove comment
300
+ pos = cursor.block().text().find('--')
301
+ cursor.setPosition(cursor.block().position() + pos)
302
+ cursor.deleteChar()
303
+ cursor.deleteChar()
304
+ else:
305
+ # Add comment
306
+ cursor.insertText('--')
307
+
308
+ def line_number_area_width(self):
309
+ digits = 1
310
+ max_num = max(1, self.blockCount())
311
+ while max_num >= 10:
312
+ max_num //= 10
313
+ digits += 1
314
+
315
+ space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
316
+ return space
317
+
318
+ def update_line_number_area_width(self, _):
319
+ self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
320
+
321
+ def update_line_number_area(self, rect, dy):
322
+ if dy:
323
+ self.line_number_area.scroll(0, dy)
324
+ else:
325
+ self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
326
+
327
+ if rect.contains(self.viewport().rect()):
328
+ self.update_line_number_area_width(0)
329
+
330
+ def resizeEvent(self, event):
331
+ super().resizeEvent(event)
332
+ cr = self.contentsRect()
333
+ self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
334
+
335
+ def line_number_area_paint_event(self, event):
336
+ painter = QPainter(self.line_number_area)
337
+ painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
338
+
339
+ block = self.firstVisibleBlock()
340
+ block_number = block.blockNumber()
341
+ top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
342
+ bottom = top + round(self.blockBoundingRect(block).height())
343
+
344
+ while block.isValid() and top <= event.rect().bottom():
345
+ if block.isVisible() and bottom >= event.rect().top():
346
+ number = str(block_number + 1)
347
+ painter.setPen(QColor("#808080")) # Gray text
348
+ painter.drawText(0, top, self.line_number_area.width() - 5,
349
+ self.fontMetrics().height(),
350
+ Qt.AlignmentFlag.AlignRight, number)
351
+
352
+ block = block.next()
353
+ top = bottom
354
+ bottom = top + round(self.blockBoundingRect(block).height())
355
+ block_number += 1