sqlsaber 0.25.0__py3-none-any.whl → 0.26.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 sqlsaber might be problematic. Click here for more details.

@@ -0,0 +1,258 @@
1
+ """SQLite database connection and schema introspection."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import aiosqlite
7
+
8
+ from .base import (
9
+ DEFAULT_QUERY_TIMEOUT,
10
+ BaseDatabaseConnection,
11
+ BaseSchemaIntrospector,
12
+ QueryTimeoutError,
13
+ )
14
+
15
+
16
+ class SQLiteConnection(BaseDatabaseConnection):
17
+ """SQLite database connection using aiosqlite."""
18
+
19
+ def __init__(self, connection_string: str):
20
+ super().__init__(connection_string)
21
+ # Extract database path from sqlite:///path format
22
+ self.database_path = connection_string.replace("sqlite:///", "")
23
+
24
+ async def get_pool(self):
25
+ """SQLite doesn't use connection pooling, return database path."""
26
+ return self.database_path
27
+
28
+ async def close(self):
29
+ """SQLite connections are created per query, no persistent pool to close."""
30
+ pass
31
+
32
+ async def execute_query(
33
+ self, query: str, *args, timeout: float | None = None
34
+ ) -> list[dict[str, Any]]:
35
+ """Execute a query and return results as list of dicts.
36
+
37
+ All queries run in a transaction that is rolled back at the end,
38
+ ensuring no changes are persisted to the database.
39
+ """
40
+ effective_timeout = timeout or DEFAULT_QUERY_TIMEOUT
41
+
42
+ async with aiosqlite.connect(self.database_path) as conn:
43
+ # Enable row factory for dict-like access
44
+ conn.row_factory = aiosqlite.Row
45
+
46
+ # Start transaction
47
+ await conn.execute("BEGIN")
48
+ try:
49
+ # Execute query with client-side timeout (SQLite has no server-side timeout)
50
+ if effective_timeout:
51
+ cursor = await asyncio.wait_for(
52
+ conn.execute(query, args if args else ()),
53
+ timeout=effective_timeout,
54
+ )
55
+ rows = await asyncio.wait_for(
56
+ cursor.fetchall(), timeout=effective_timeout
57
+ )
58
+ else:
59
+ cursor = await conn.execute(query, args if args else ())
60
+ rows = await cursor.fetchall()
61
+
62
+ return [dict(row) for row in rows]
63
+ except asyncio.TimeoutError as exc:
64
+ raise QueryTimeoutError(effective_timeout or 0) from exc
65
+ finally:
66
+ # Always rollback to ensure no changes are committed
67
+ await conn.rollback()
68
+
69
+
70
+ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
71
+ """SQLite-specific schema introspection."""
72
+
73
+ async def _execute_query(self, connection, query: str, params=()) -> list:
74
+ """Helper method to execute queries on both SQLite and CSV connections."""
75
+ # Handle both SQLite and CSV connections
76
+ if hasattr(connection, "database_path"):
77
+ # Regular SQLite connection
78
+ async with aiosqlite.connect(connection.database_path) as conn:
79
+ conn.row_factory = aiosqlite.Row
80
+ cursor = await conn.execute(query, params)
81
+ return await cursor.fetchall()
82
+ else:
83
+ # CSV connection - use the existing connection
84
+ conn = await connection.get_pool()
85
+ cursor = await conn.execute(query, params)
86
+ return await cursor.fetchall()
87
+
88
+ async def get_tables_info(
89
+ self, connection, table_pattern: str | None = None
90
+ ) -> dict[str, Any]:
91
+ """Get tables information for SQLite."""
92
+ where_conditions = ["type IN ('table', 'view')", "name NOT LIKE 'sqlite_%'"]
93
+ params = ()
94
+
95
+ if table_pattern:
96
+ where_conditions.append("name LIKE ?")
97
+ params = (table_pattern,)
98
+
99
+ query = f"""
100
+ SELECT
101
+ 'main' as table_schema,
102
+ name as table_name,
103
+ type as table_type
104
+ FROM sqlite_master
105
+ WHERE {" AND ".join(where_conditions)}
106
+ ORDER BY name;
107
+ """
108
+
109
+ return await self._execute_query(connection, query, params)
110
+
111
+ async def get_columns_info(self, connection, tables: list) -> list:
112
+ """Get columns information for SQLite."""
113
+ if not tables:
114
+ return []
115
+
116
+ columns = []
117
+ for table in tables:
118
+ table_name = table["table_name"]
119
+
120
+ # Get table info using PRAGMA
121
+ pragma_query = f"PRAGMA table_info({table_name})"
122
+ table_columns = await self._execute_query(connection, pragma_query)
123
+
124
+ for col in table_columns:
125
+ columns.append(
126
+ {
127
+ "table_schema": "main",
128
+ "table_name": table_name,
129
+ "column_name": col["name"],
130
+ "data_type": col["type"],
131
+ "is_nullable": "YES" if not col["notnull"] else "NO",
132
+ "column_default": col["dflt_value"],
133
+ "character_maximum_length": None,
134
+ "numeric_precision": None,
135
+ "numeric_scale": None,
136
+ }
137
+ )
138
+
139
+ return columns
140
+
141
+ async def get_foreign_keys_info(self, connection, tables: list) -> list:
142
+ """Get foreign keys information for SQLite."""
143
+ if not tables:
144
+ return []
145
+
146
+ foreign_keys = []
147
+ for table in tables:
148
+ table_name = table["table_name"]
149
+
150
+ # Get foreign key info using PRAGMA
151
+ pragma_query = f"PRAGMA foreign_key_list({table_name})"
152
+ table_fks = await self._execute_query(connection, pragma_query)
153
+
154
+ for fk in table_fks:
155
+ foreign_keys.append(
156
+ {
157
+ "table_schema": "main",
158
+ "table_name": table_name,
159
+ "column_name": fk["from"],
160
+ "foreign_table_schema": "main",
161
+ "foreign_table_name": fk["table"],
162
+ "foreign_column_name": fk["to"],
163
+ }
164
+ )
165
+
166
+ return foreign_keys
167
+
168
+ async def get_primary_keys_info(self, connection, tables: list) -> list:
169
+ """Get primary keys information for SQLite."""
170
+ if not tables:
171
+ return []
172
+
173
+ primary_keys = []
174
+ for table in tables:
175
+ table_name = table["table_name"]
176
+
177
+ # Get table info using PRAGMA to find primary keys
178
+ pragma_query = f"PRAGMA table_info({table_name})"
179
+ table_columns = await self._execute_query(connection, pragma_query)
180
+
181
+ for col in table_columns:
182
+ if col["pk"]: # Primary key indicator
183
+ primary_keys.append(
184
+ {
185
+ "table_schema": "main",
186
+ "table_name": table_name,
187
+ "column_name": col["name"],
188
+ }
189
+ )
190
+
191
+ return primary_keys
192
+
193
+ async def get_indexes_info(self, connection, tables: list) -> list:
194
+ """Get indexes information for SQLite."""
195
+ if not tables:
196
+ return []
197
+
198
+ indexes = []
199
+ for table in tables:
200
+ table_name = table["table_name"]
201
+
202
+ # Get index list using PRAGMA
203
+ pragma_query = f"PRAGMA index_list({table_name})"
204
+ table_indexes = await self._execute_query(connection, pragma_query)
205
+
206
+ for idx in table_indexes:
207
+ idx_name = idx["name"]
208
+ unique = bool(idx["unique"])
209
+
210
+ # Skip auto-generated primary key indexes
211
+ if idx_name.startswith("sqlite_autoindex_"):
212
+ continue
213
+
214
+ # Get index columns using PRAGMA
215
+ pragma_info_query = f"PRAGMA index_info({idx_name})"
216
+ idx_cols = await self._execute_query(connection, pragma_info_query)
217
+ columns = [
218
+ c["name"] for c in sorted(idx_cols, key=lambda r: r["seqno"])
219
+ ]
220
+
221
+ indexes.append(
222
+ {
223
+ "table_schema": "main",
224
+ "table_name": table_name,
225
+ "index_name": idx_name,
226
+ "is_unique": unique,
227
+ "index_type": None, # SQLite only has B-tree currently
228
+ "column_names": columns,
229
+ }
230
+ )
231
+
232
+ return indexes
233
+
234
+ async def list_tables_info(self, connection) -> list[dict[str, Any]]:
235
+ """Get list of tables with basic information for SQLite."""
236
+ # Get table names without row counts for better performance
237
+ tables_query = """
238
+ SELECT
239
+ 'main' as table_schema,
240
+ name as table_name,
241
+ type as table_type
242
+ FROM sqlite_master
243
+ WHERE type IN ('table', 'view')
244
+ AND name NOT LIKE 'sqlite_%'
245
+ ORDER BY name;
246
+ """
247
+
248
+ tables = await self._execute_query(connection, tables_query)
249
+
250
+ # Convert to expected format
251
+ return [
252
+ {
253
+ "table_schema": table["table_schema"],
254
+ "table_name": table["table_name"],
255
+ "table_type": table["table_type"],
256
+ }
257
+ for table in tables
258
+ ]
sqlsaber/mcp/mcp.py CHANGED
@@ -6,7 +6,7 @@ from fastmcp import FastMCP
6
6
 
7
7
  from sqlsaber.agents.mcp import MCPSQLAgent
8
8
  from sqlsaber.config.database import DatabaseConfigManager
9
- from sqlsaber.database.connection import DatabaseConnection
9
+ from sqlsaber.database import DatabaseConnection
10
10
  from sqlsaber.tools import SQLTool, tool_registry
11
11
  from sqlsaber.tools.instructions import InstructionBuilder
12
12
 
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  from typing import Any
5
5
 
6
- from sqlsaber.database.connection import BaseDatabaseConnection
6
+ from sqlsaber.database import BaseDatabaseConnection
7
7
  from sqlsaber.database.schema import SchemaManager
8
8
 
9
9
  from .base import Tool
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -53,14 +53,15 @@ Ask your questions in natural language and `sqlsaber` will gather the right cont
53
53
 
54
54
  ## Features
55
55
 
56
- - 🔍 Automatic database schema introspection
57
- - 🛡️ Safe query execution (read-only by default)
58
- - 🧠 Memory management
59
- - 💬 Interactive REPL mode
60
- - 🧵 Conversation threads (store, display, and resume conversations)
61
- - 🗄️ Support for PostgreSQL, MySQL, SQLite, and DuckDB
62
- - 🔌 MCP (Model Context Protocol) server support
63
- - 🎨 Beautiful formatted output
56
+ - Automatic database schema introspection
57
+ - Safe query execution (read-only by default)
58
+ - Memory management
59
+ - Interactive REPL mode
60
+ - Conversation threads (store, display, and resume conversations)
61
+ - Support for PostgreSQL, MySQL, SQLite, DuckDB, and CSVs
62
+ - MCP (Model Context Protocol) server support
63
+ - Extended thinking mode for select models (Anthropic, OpenAI, Google, Groq)
64
+ - Beautiful formatted output
64
65
 
65
66
  ## Installation
66
67
 
@@ -122,6 +123,39 @@ saber memory list
122
123
 
123
124
  > You can also add memories in an interactive query session by starting with the `#` sign
124
125
 
126
+ ### Extended Thinking Mode
127
+
128
+ For complex queries that require deeper reasoning, `sqlsaber` supports extended thinking mode. When enabled, you will see the model's reasoning process as it generates SQL queries and arrives at conclusions.
129
+
130
+ **Enable/disable via CLI flags:**
131
+
132
+ ```bash
133
+ # Enable thinking for a single query
134
+ saber --thinking "analyze sales trends across regions"
135
+
136
+ # Disable thinking for a single query
137
+ saber --no-thinking "show me all users"
138
+ ```
139
+
140
+ **Toggle in interactive mode:**
141
+
142
+ ```bash
143
+ # In interactive mode, use slash commands
144
+ /thinking on # Enable thinking
145
+ /thinking off # Disable thinking
146
+ ```
147
+
148
+ **Configure default setting:**
149
+
150
+ Thinking is disabled by default. To change the default, edit your config file at `~/.config/sqlsaber/model_config.json`:
151
+
152
+ ```json
153
+ {
154
+ "model": "anthropic:claude-sonnet-4-20250514",
155
+ "thinking_enabled": true
156
+ }
157
+ ```
158
+
125
159
  ## Usage
126
160
 
127
161
  ### Interactive Mode
@@ -0,0 +1,52 @@
1
+ sqlsaber/__init__.py,sha256=HjS8ULtP4MGpnTL7njVY45NKV9Fi4e_yeYuY-hyXWQc,73
2
+ sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
3
+ sqlsaber/agents/__init__.py,sha256=qYI6rLY4q5AbF47vXH5RVoM08-yQjymBSaePh4lFIW4,116
4
+ sqlsaber/agents/base.py,sha256=40-MKEoz5rGrqVIylV1U2DaAUSPFcC75ohRin4E3-kk,2668
5
+ sqlsaber/agents/mcp.py,sha256=Pn8tdDRUEVLYQyEi5nHRp9MKNePwHVVoeNI-uqWcr0Y,757
6
+ sqlsaber/agents/pydantic_ai_agent.py,sha256=wBxKz0pjOkL-HI-TXV6B67bczZNgu7k26Rr3w5usR3o,10064
7
+ sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
+ sqlsaber/cli/auth.py,sha256=jTsRgbmlGPlASSuIKmdjjwfqtKvjfKd_cTYxX0-QqaQ,7400
9
+ sqlsaber/cli/commands.py,sha256=n25CErTLgLeRSkoJI0Ickwtns5EH6O7RLVPgPs6UBxA,7986
10
+ sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
11
+ sqlsaber/cli/database.py,sha256=qil7nZGWKm3tULL0cUsAQ_KvhU1oikK0XVh9MibrvP0,13413
12
+ sqlsaber/cli/display.py,sha256=32QaNS0RDgRz93AVy6nPo9blahvMPEoVMFC5spzh0-Y,17041
13
+ sqlsaber/cli/interactive.py,sha256=jGbWNNcEgZuQRZamc5tX5eIf1Rv1T6Sj5NI_WvonTrA,13624
14
+ sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
15
+ sqlsaber/cli/models.py,sha256=ZewtwGQwhd9b-yxBAPKePolvI1qQG-EkmeWAGMqtWNQ,8986
16
+ sqlsaber/cli/streaming.py,sha256=YViLCxUv-7WN5TCphLYtAR02HXvuHYuPttGGDZKDUKU,6921
17
+ sqlsaber/cli/threads.py,sha256=zVlbOuD3GjjEVNebXwANKeKt4I_Lunf6itiBUL0TaKA,12877
18
+ sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
19
+ sqlsaber/config/api_keys.py,sha256=RqWQCko1tY7sES7YOlexgBH5Hd5ne_kGXHdBDNqcV2U,3649
20
+ sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
21
+ sqlsaber/config/database.py,sha256=Yec6_0wdzq-ADblMNnbgvouYCimYOY_DWHT9oweaISc,11449
22
+ sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
23
+ sqlsaber/config/oauth_tokens.py,sha256=C9z35hyx-PvSAYdC1LNf3rg9_wsEIY56hkEczelbad0,6015
24
+ sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
25
+ sqlsaber/config/settings.py,sha256=iB4CnGQ4hw8gxkaa9CVLB_JEy6Y9h9FQTAams5OCVyI,6421
26
+ sqlsaber/database/__init__.py,sha256=Gi9N_NOkD459WRWXDg3hSuGoBs3xWbMDRBvsTVmnGAg,2025
27
+ sqlsaber/database/base.py,sha256=yxYcfeNhRPbO5jFRVZH7eRUGj_up-y3p1ZX_obZXi0w,3552
28
+ sqlsaber/database/csv.py,sha256=45eH9mAkBtwSu1Rc_vvG1Z40L4xvfHWSb8OMG15TbCA,4340
29
+ sqlsaber/database/duckdb.py,sha256=v6gFUhih5NMbHHpUv7By2nXyl9aqdPtLt0zhqS4-OKE,11120
30
+ sqlsaber/database/mysql.py,sha256=5qd9gnSCP3umtBJcQDTzzJfMzwqYCJhWlbOeJZ9_-6c,14349
31
+ sqlsaber/database/postgresql.py,sha256=R8I3Y-w0P9qPe47-lmae0X17syIwI8saxEG3etx6Rqc,13097
32
+ sqlsaber/database/resolver.py,sha256=wSCcn__aCqwIfpt_LCjtW2Zgb8RpG5PlmwwZHli1q_U,3628
33
+ sqlsaber/database/schema.py,sha256=68PrNcA-5eR9PZB3i-TUQw5_E7QatwiDU2wv9GgXgM4,6928
34
+ sqlsaber/database/sqlite.py,sha256=zdNj5i4mLJK21sWgftAHDHVihRUWevn__tVF9_nnLfQ,9297
35
+ sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
36
+ sqlsaber/mcp/mcp.py,sha256=tpNPHpkaCre1Xjp7c4DHXbTKeuYpDQ8qhmJZvAyr7Vk,3939
37
+ sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
38
+ sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
39
+ sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
40
+ sqlsaber/threads/__init__.py,sha256=Hh3dIG1tuC8fXprREUpslCIgPYz8_6o7aRLx4yNeO48,139
41
+ sqlsaber/threads/storage.py,sha256=rsUdxT4CR52D7xtGir9UlsFnBMk11jZeflzDrk2q4ME,11183
42
+ sqlsaber/tools/__init__.py,sha256=x3YdmX_7P0Qq_HtZHAgfIVKTLxYqKk6oc4tGsujQWsc,586
43
+ sqlsaber/tools/base.py,sha256=mHhvAj27BHmckyvuDLCPlAQdzABJyYxd9SJnaYAwwuA,1777
44
+ sqlsaber/tools/enums.py,sha256=CH32mL-0k9ZA18911xLpNtsgpV6tB85TktMj6uqGz54,411
45
+ sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U8,9024
46
+ sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
47
+ sqlsaber/tools/sql_tools.py,sha256=2xLD_pkd0t8wKndQAKIr4c9UpWzVWeHbAFpkwo5j4kY,9954
48
+ sqlsaber-0.26.0.dist-info/METADATA,sha256=o4vaJVAG_1U5Tybcx2MY3lX0FvYjBEdKxFDWhFC9xYs,7138
49
+ sqlsaber-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
+ sqlsaber-0.26.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
51
+ sqlsaber-0.26.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
52
+ sqlsaber-0.26.0.dist-info/RECORD,,