sqlsaber 0.35.0__py3-none-any.whl → 0.36.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.
- sqlsaber/application/db_setup.py +38 -2
- sqlsaber/cli/commands.py +5 -1
- sqlsaber/cli/database.py +160 -12
- sqlsaber/cli/interactive.py +2 -0
- sqlsaber/cli/threads.py +4 -2
- sqlsaber/config/database.py +14 -1
- sqlsaber/database/__init__.py +13 -6
- sqlsaber/database/base.py +57 -0
- sqlsaber/database/duckdb.py +29 -6
- sqlsaber/database/mysql.py +30 -7
- sqlsaber/database/postgresql.py +7 -15
- sqlsaber/database/resolver.py +17 -7
- {sqlsaber-0.35.0.dist-info → sqlsaber-0.36.0.dist-info}/METADATA +1 -1
- {sqlsaber-0.35.0.dist-info → sqlsaber-0.36.0.dist-info}/RECORD +17 -17
- {sqlsaber-0.35.0.dist-info → sqlsaber-0.36.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.35.0.dist-info → sqlsaber-0.36.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.35.0.dist-info → sqlsaber-0.36.0.dist-info}/licenses/LICENSE +0 -0
sqlsaber/application/db_setup.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Shared database setup logic for onboarding and CLI."""
|
|
2
2
|
|
|
3
3
|
import getpass
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from sqlsaber.application.prompts import Prompter
|
|
@@ -11,6 +11,21 @@ from sqlsaber.theme.manager import create_console
|
|
|
11
11
|
console = create_console()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _normalize_schemas(schemas: list[str]) -> list[str]:
|
|
15
|
+
"""Deduplicate schema list while preserving order and case."""
|
|
16
|
+
normalized: list[str] = []
|
|
17
|
+
seen: set[str] = set()
|
|
18
|
+
for schema in schemas:
|
|
19
|
+
name = schema.strip()
|
|
20
|
+
if not name:
|
|
21
|
+
continue
|
|
22
|
+
if name in seen:
|
|
23
|
+
continue
|
|
24
|
+
seen.add(name)
|
|
25
|
+
normalized.append(name)
|
|
26
|
+
return normalized
|
|
27
|
+
|
|
28
|
+
|
|
14
29
|
@dataclass
|
|
15
30
|
class DatabaseInput:
|
|
16
31
|
"""Input data for database configuration."""
|
|
@@ -26,6 +41,7 @@ class DatabaseInput:
|
|
|
26
41
|
ssl_ca: str | None = None
|
|
27
42
|
ssl_cert: str | None = None
|
|
28
43
|
ssl_key: str | None = None
|
|
44
|
+
exclude_schemas: list[str] = field(default_factory=list)
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
async def collect_db_input(
|
|
@@ -69,11 +85,20 @@ async def collect_db_input(
|
|
|
69
85
|
port = 0
|
|
70
86
|
username = db_type
|
|
71
87
|
password = ""
|
|
88
|
+
exclude_schemas: list[str] = []
|
|
72
89
|
ssl_mode = None
|
|
73
90
|
ssl_ca = None
|
|
74
91
|
ssl_cert = None
|
|
75
92
|
ssl_key = None
|
|
76
93
|
|
|
94
|
+
if db_type == "duckdb":
|
|
95
|
+
exclude_prompt = await prompter.text(
|
|
96
|
+
"Schemas to exclude (comma separated, optional):", default=""
|
|
97
|
+
)
|
|
98
|
+
if exclude_prompt is None:
|
|
99
|
+
return None
|
|
100
|
+
exclude_schemas = _normalize_schemas(exclude_prompt.split(","))
|
|
101
|
+
|
|
77
102
|
else:
|
|
78
103
|
# PostgreSQL/MySQL need connection details
|
|
79
104
|
host = await prompter.text("Host:", default="localhost")
|
|
@@ -155,6 +180,13 @@ async def collect_db_input(
|
|
|
155
180
|
"SSL client private key file:"
|
|
156
181
|
)
|
|
157
182
|
|
|
183
|
+
exclude_prompt = await prompter.text(
|
|
184
|
+
"Schemas to exclude (comma separated, optional):", default=""
|
|
185
|
+
)
|
|
186
|
+
if exclude_prompt is None:
|
|
187
|
+
return None
|
|
188
|
+
exclude_schemas = _normalize_schemas(exclude_prompt.split(","))
|
|
189
|
+
|
|
158
190
|
return DatabaseInput(
|
|
159
191
|
name=name,
|
|
160
192
|
type=db_type,
|
|
@@ -167,6 +199,7 @@ async def collect_db_input(
|
|
|
167
199
|
ssl_ca=ssl_ca,
|
|
168
200
|
ssl_cert=ssl_cert,
|
|
169
201
|
ssl_key=ssl_key,
|
|
202
|
+
exclude_schemas=exclude_schemas,
|
|
170
203
|
)
|
|
171
204
|
|
|
172
205
|
|
|
@@ -183,6 +216,7 @@ def build_config(db_input: DatabaseInput) -> DatabaseConfig:
|
|
|
183
216
|
ssl_ca=db_input.ssl_ca,
|
|
184
217
|
ssl_cert=db_input.ssl_cert,
|
|
185
218
|
ssl_key=db_input.ssl_key,
|
|
219
|
+
exclude_schemas=_normalize_schemas(db_input.exclude_schemas),
|
|
186
220
|
)
|
|
187
221
|
|
|
188
222
|
|
|
@@ -200,7 +234,9 @@ async def test_connection(config: DatabaseConfig, password: str | None) -> bool:
|
|
|
200
234
|
|
|
201
235
|
try:
|
|
202
236
|
connection_string = config.to_connection_string()
|
|
203
|
-
db_conn = DatabaseConnection(
|
|
237
|
+
db_conn = DatabaseConnection(
|
|
238
|
+
connection_string, excluded_schemas=config.exclude_schemas
|
|
239
|
+
)
|
|
204
240
|
await db_conn.execute_query("SELECT 1 as test")
|
|
205
241
|
await db_conn.close()
|
|
206
242
|
return True
|
sqlsaber/cli/commands.py
CHANGED
|
@@ -214,7 +214,9 @@ def query(
|
|
|
214
214
|
|
|
215
215
|
# Create database connection
|
|
216
216
|
try:
|
|
217
|
-
db_conn = DatabaseConnection(
|
|
217
|
+
db_conn = DatabaseConnection(
|
|
218
|
+
connection_string, excluded_schemas=resolved.excluded_schemas
|
|
219
|
+
)
|
|
218
220
|
log.info("db.connection.created", db_type=type(db_conn).__name__)
|
|
219
221
|
except Exception as e:
|
|
220
222
|
log.exception("db.connection.error", error=str(e))
|
|
@@ -229,8 +231,10 @@ def query(
|
|
|
229
231
|
# Single query mode with streaming
|
|
230
232
|
streaming_handler = StreamingQueryHandler(console)
|
|
231
233
|
db_type = sqlsaber_agent.db_type
|
|
234
|
+
model_name = sqlsaber_agent.agent.model.model_name
|
|
232
235
|
console.print(
|
|
233
236
|
f"[primary]Connected to:[/primary] {db_name} ({db_type})\n"
|
|
237
|
+
f"[primary]Model:[/primary] {model_name}\n"
|
|
234
238
|
)
|
|
235
239
|
log.info("query.execute.start", db_name=db_name, db_type=db_type)
|
|
236
240
|
run = await streaming_handler.execute_streaming_query(
|
sqlsaber/cli/database.py
CHANGED
|
@@ -26,6 +26,28 @@ db_app = cyclopts.App(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _normalize_schema_list(raw_schemas: list[str]) -> list[str]:
|
|
30
|
+
"""Deduplicate schemas while preserving order and case."""
|
|
31
|
+
schemas: list[str] = []
|
|
32
|
+
seen: set[str] = set()
|
|
33
|
+
for schema in raw_schemas:
|
|
34
|
+
item = schema.strip()
|
|
35
|
+
if not item:
|
|
36
|
+
continue
|
|
37
|
+
if item in seen:
|
|
38
|
+
continue
|
|
39
|
+
seen.add(item)
|
|
40
|
+
schemas.append(item)
|
|
41
|
+
return schemas
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_schema_list(raw: str | None) -> list[str]:
|
|
45
|
+
"""Parse comma-separated schema list into cleaned list."""
|
|
46
|
+
if not raw:
|
|
47
|
+
return []
|
|
48
|
+
return _normalize_schema_list(raw.split(","))
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
@db_app.command
|
|
30
52
|
def add(
|
|
31
53
|
name: Annotated[str, cyclopts.Parameter(help="Name for the database connection")],
|
|
@@ -71,6 +93,13 @@ def add(
|
|
|
71
93
|
str | None,
|
|
72
94
|
cyclopts.Parameter(["--ssl-key"], help="SSL client private key file path"),
|
|
73
95
|
] = None,
|
|
96
|
+
exclude_schemas: Annotated[
|
|
97
|
+
str | None,
|
|
98
|
+
cyclopts.Parameter(
|
|
99
|
+
["--exclude-schemas"],
|
|
100
|
+
help="Comma-separated list of schemas to exclude from introspection",
|
|
101
|
+
),
|
|
102
|
+
] = None,
|
|
74
103
|
interactive: Annotated[
|
|
75
104
|
bool,
|
|
76
105
|
cyclopts.Parameter(
|
|
@@ -119,6 +148,7 @@ def add(
|
|
|
119
148
|
ssl_ca = db_input.ssl_ca
|
|
120
149
|
ssl_cert = db_input.ssl_cert
|
|
121
150
|
ssl_key = db_input.ssl_key
|
|
151
|
+
exclude_schema_list = _normalize_schema_list(db_input.exclude_schemas)
|
|
122
152
|
else:
|
|
123
153
|
# Non-interactive mode - use provided values or defaults
|
|
124
154
|
if type == "sqlite":
|
|
@@ -160,6 +190,7 @@ def add(
|
|
|
160
190
|
if questionary.confirm("Enter password?").ask()
|
|
161
191
|
else ""
|
|
162
192
|
)
|
|
193
|
+
exclude_schema_list = _parse_schema_list(exclude_schemas)
|
|
163
194
|
|
|
164
195
|
# Create database config
|
|
165
196
|
# At this point, all required values should be set
|
|
@@ -180,6 +211,7 @@ def add(
|
|
|
180
211
|
ssl_ca=ssl_ca,
|
|
181
212
|
ssl_cert=ssl_cert,
|
|
182
213
|
ssl_key=ssl_key,
|
|
214
|
+
exclude_schemas=exclude_schema_list,
|
|
183
215
|
)
|
|
184
216
|
|
|
185
217
|
try:
|
|
@@ -219,6 +251,7 @@ def list():
|
|
|
219
251
|
table.add_column("Port", style="warning")
|
|
220
252
|
table.add_column("Database", style="info")
|
|
221
253
|
table.add_column("Username", style="info")
|
|
254
|
+
table.add_column("Excluded Schemas", style="muted")
|
|
222
255
|
table.add_column("SSL", style="success")
|
|
223
256
|
table.add_column("Default", style="error")
|
|
224
257
|
|
|
@@ -241,6 +274,7 @@ def list():
|
|
|
241
274
|
str(db.port) if db.port else "",
|
|
242
275
|
db.database,
|
|
243
276
|
db.username,
|
|
277
|
+
", ".join(db.exclude_schemas) if db.exclude_schemas else "",
|
|
244
278
|
ssl_status,
|
|
245
279
|
is_default,
|
|
246
280
|
)
|
|
@@ -249,6 +283,116 @@ def list():
|
|
|
249
283
|
logger.info("db.list.complete", count=len(databases))
|
|
250
284
|
|
|
251
285
|
|
|
286
|
+
@db_app.command
|
|
287
|
+
def exclude(
|
|
288
|
+
name: Annotated[
|
|
289
|
+
str,
|
|
290
|
+
cyclopts.Parameter(help="Name of the database connection to update"),
|
|
291
|
+
],
|
|
292
|
+
set_schemas: Annotated[
|
|
293
|
+
str | None,
|
|
294
|
+
cyclopts.Parameter(
|
|
295
|
+
["--set"],
|
|
296
|
+
help="Replace excluded schemas with this comma-separated list",
|
|
297
|
+
),
|
|
298
|
+
] = None,
|
|
299
|
+
add_schemas: Annotated[
|
|
300
|
+
str | None,
|
|
301
|
+
cyclopts.Parameter(
|
|
302
|
+
["--add"],
|
|
303
|
+
help="Add comma-separated schemas to the existing exclude list",
|
|
304
|
+
),
|
|
305
|
+
] = None,
|
|
306
|
+
remove_schemas: Annotated[
|
|
307
|
+
str | None,
|
|
308
|
+
cyclopts.Parameter(
|
|
309
|
+
["--remove"],
|
|
310
|
+
help="Remove comma-separated schemas from the existing exclude list",
|
|
311
|
+
),
|
|
312
|
+
] = None,
|
|
313
|
+
clear: Annotated[
|
|
314
|
+
bool,
|
|
315
|
+
cyclopts.Parameter(
|
|
316
|
+
["--clear", "--no-clear"],
|
|
317
|
+
help="Clear all excluded schemas",
|
|
318
|
+
),
|
|
319
|
+
] = False,
|
|
320
|
+
):
|
|
321
|
+
"""Update excluded schemas for a database connection."""
|
|
322
|
+
logger.info(
|
|
323
|
+
"db.exclude.start",
|
|
324
|
+
name=name,
|
|
325
|
+
set=bool(set_schemas),
|
|
326
|
+
add=bool(add_schemas),
|
|
327
|
+
remove=bool(remove_schemas),
|
|
328
|
+
clear=clear,
|
|
329
|
+
)
|
|
330
|
+
db_config = config_manager.get_database(name)
|
|
331
|
+
if not db_config:
|
|
332
|
+
console.print(
|
|
333
|
+
f"[bold error]Error: Database connection '{name}' not found[/bold error]"
|
|
334
|
+
)
|
|
335
|
+
logger.error("db.exclude.not_found", name=name)
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
|
|
338
|
+
actions_selected = sum(
|
|
339
|
+
bool(flag)
|
|
340
|
+
for flag in [
|
|
341
|
+
set_schemas is not None,
|
|
342
|
+
add_schemas is not None,
|
|
343
|
+
remove_schemas is not None,
|
|
344
|
+
clear,
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
if actions_selected > 1:
|
|
348
|
+
console.print(
|
|
349
|
+
"[bold error]Error: Specify only one of --set, --add, --remove, or --clear[/bold error]"
|
|
350
|
+
)
|
|
351
|
+
logger.error("db.exclude.multiple_actions", name=name)
|
|
352
|
+
sys.exit(1)
|
|
353
|
+
|
|
354
|
+
current = [*(db_config.exclude_schemas or [])]
|
|
355
|
+
|
|
356
|
+
if clear:
|
|
357
|
+
updated = []
|
|
358
|
+
elif set_schemas is not None:
|
|
359
|
+
updated = _parse_schema_list(set_schemas)
|
|
360
|
+
elif add_schemas is not None:
|
|
361
|
+
additions = _parse_schema_list(add_schemas)
|
|
362
|
+
updated = [*current]
|
|
363
|
+
current_set = set(current)
|
|
364
|
+
for schema in additions:
|
|
365
|
+
if schema not in current_set:
|
|
366
|
+
updated.append(schema)
|
|
367
|
+
current_set.add(schema)
|
|
368
|
+
elif remove_schemas is not None:
|
|
369
|
+
removals = set(_parse_schema_list(remove_schemas))
|
|
370
|
+
updated = [schema for schema in current if schema not in removals]
|
|
371
|
+
else:
|
|
372
|
+
console.print(
|
|
373
|
+
"[info]Update excluded schemas for "
|
|
374
|
+
f"[primary]{name}[/primary] (leave blank to clear)[/info]"
|
|
375
|
+
)
|
|
376
|
+
default_value = ", ".join(current)
|
|
377
|
+
response = questionary.text(
|
|
378
|
+
"Schemas to exclude (comma separated):", default=default_value
|
|
379
|
+
).ask()
|
|
380
|
+
if response is None:
|
|
381
|
+
console.print("[warning]Operation cancelled[/warning]")
|
|
382
|
+
logger.info("db.exclude.cancelled", name=name)
|
|
383
|
+
return
|
|
384
|
+
updated = _parse_schema_list(response)
|
|
385
|
+
|
|
386
|
+
db_config.exclude_schemas = _normalize_schema_list(updated)
|
|
387
|
+
config_manager.update_database(db_config)
|
|
388
|
+
|
|
389
|
+
console.print(
|
|
390
|
+
f"[success]Updated excluded schemas for '{name}':[/success] "
|
|
391
|
+
f"{', '.join(db_config.exclude_schemas) if db_config.exclude_schemas else '(none)'}"
|
|
392
|
+
)
|
|
393
|
+
logger.info("db.exclude.success", name=name, count=len(db_config.exclude_schemas))
|
|
394
|
+
|
|
395
|
+
|
|
252
396
|
@db_app.command
|
|
253
397
|
def remove(
|
|
254
398
|
name: Annotated[
|
|
@@ -259,7 +403,7 @@ def remove(
|
|
|
259
403
|
logger.info("db.remove.start", name=name)
|
|
260
404
|
if not config_manager.get_database(name):
|
|
261
405
|
console.print(
|
|
262
|
-
f"[bold error]Error:
|
|
406
|
+
f"[bold error]Error: Database connection '{name}' not found[/bold error]"
|
|
263
407
|
)
|
|
264
408
|
logger.error("db.remove.not_found", name=name)
|
|
265
409
|
sys.exit(1)
|
|
@@ -269,17 +413,17 @@ def remove(
|
|
|
269
413
|
).ask():
|
|
270
414
|
if config_manager.remove_database(name):
|
|
271
415
|
console.print(
|
|
272
|
-
f"[
|
|
416
|
+
f"[success]Successfully removed database connection '{name}'[/success]"
|
|
273
417
|
)
|
|
274
418
|
logger.info("db.remove.success", name=name)
|
|
275
419
|
else:
|
|
276
420
|
console.print(
|
|
277
|
-
f"[bold error]Error:
|
|
421
|
+
f"[bold error]Error: Failed to remove database connection '{name}'[/bold error]"
|
|
278
422
|
)
|
|
279
423
|
logger.error("db.remove.failed", name=name)
|
|
280
424
|
sys.exit(1)
|
|
281
425
|
else:
|
|
282
|
-
console.print("Operation cancelled")
|
|
426
|
+
console.print("[warning]Operation cancelled[/warning]")
|
|
283
427
|
logger.info("db.remove.cancelled", name=name)
|
|
284
428
|
|
|
285
429
|
|
|
@@ -294,17 +438,19 @@ def set_default(
|
|
|
294
438
|
logger.info("db.default.start", name=name)
|
|
295
439
|
if not config_manager.get_database(name):
|
|
296
440
|
console.print(
|
|
297
|
-
f"[bold error]Error:
|
|
441
|
+
f"[bold error]Error: Database connection '{name}' not found[/bold error]"
|
|
298
442
|
)
|
|
299
443
|
logger.error("db.default.not_found", name=name)
|
|
300
444
|
sys.exit(1)
|
|
301
445
|
|
|
302
446
|
if config_manager.set_default_database(name):
|
|
303
|
-
console.print(
|
|
447
|
+
console.print(
|
|
448
|
+
f"[success]Successfully set '{name}' as default database[/success]"
|
|
449
|
+
)
|
|
304
450
|
logger.info("db.default.success", name=name)
|
|
305
451
|
else:
|
|
306
452
|
console.print(
|
|
307
|
-
f"[bold error]Error:
|
|
453
|
+
f"[bold error]Error: Failed to set '{name}' as default[/bold error]"
|
|
308
454
|
)
|
|
309
455
|
logger.error("db.default.failed", name=name)
|
|
310
456
|
sys.exit(1)
|
|
@@ -330,7 +476,7 @@ def test(
|
|
|
330
476
|
db_config = config_manager.get_database(name)
|
|
331
477
|
if not db_config:
|
|
332
478
|
console.print(
|
|
333
|
-
f"[bold error]Error:
|
|
479
|
+
f"[bold error]Error: Database connection '{name}' not found[/bold error]"
|
|
334
480
|
)
|
|
335
481
|
logger.error("db.test.not_found", name=name)
|
|
336
482
|
sys.exit(1)
|
|
@@ -338,7 +484,7 @@ def test(
|
|
|
338
484
|
db_config = config_manager.get_default_database()
|
|
339
485
|
if not db_config:
|
|
340
486
|
console.print(
|
|
341
|
-
"[bold error]Error:
|
|
487
|
+
"[bold error]Error: No default database configured[/bold error]"
|
|
342
488
|
)
|
|
343
489
|
console.print(
|
|
344
490
|
"Use 'sqlsaber db add <name>' to add a database connection"
|
|
@@ -350,14 +496,16 @@ def test(
|
|
|
350
496
|
|
|
351
497
|
try:
|
|
352
498
|
connection_string = db_config.to_connection_string()
|
|
353
|
-
db_conn = DatabaseConnection(
|
|
499
|
+
db_conn = DatabaseConnection(
|
|
500
|
+
connection_string, excluded_schemas=db_config.exclude_schemas
|
|
501
|
+
)
|
|
354
502
|
|
|
355
503
|
# Try to connect and run a simple query
|
|
356
504
|
await db_conn.execute_query("SELECT 1 as test")
|
|
357
505
|
await db_conn.close()
|
|
358
506
|
|
|
359
507
|
console.print(
|
|
360
|
-
f"[
|
|
508
|
+
f"[success]✓ Connection to '{db_config.name}' successful[/success]"
|
|
361
509
|
)
|
|
362
510
|
logger.info("db.test.success", name=db_config.name)
|
|
363
511
|
|
|
@@ -369,7 +517,7 @@ def test(
|
|
|
369
517
|
),
|
|
370
518
|
error=str(e),
|
|
371
519
|
)
|
|
372
|
-
console.print(f"[bold error]✗ Connection failed:[/bold error]
|
|
520
|
+
console.print(f"[bold error]✗ Connection failed: {e}[/bold error]")
|
|
373
521
|
sys.exit(1)
|
|
374
522
|
|
|
375
523
|
asyncio.run(test_connection())
|
sqlsaber/cli/interactive.py
CHANGED
|
@@ -135,8 +135,10 @@ class InteractiveSession:
|
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
db_name = self.database_name or "Unknown"
|
|
138
|
+
model_name = self.sqlsaber_agent.agent.model.model_name
|
|
138
139
|
self.console.print(
|
|
139
140
|
f"[heading]\nConnected to {db_name} ({self._db_type_name()})[/heading]\n"
|
|
141
|
+
f"[heading]Model: {model_name}[/heading]\n"
|
|
140
142
|
)
|
|
141
143
|
|
|
142
144
|
if self._thread_id:
|
sqlsaber/cli/threads.py
CHANGED
|
@@ -229,7 +229,7 @@ def list_threads(
|
|
|
229
229
|
logger.info("threads.cli.list.empty")
|
|
230
230
|
return
|
|
231
231
|
table = Table(title="Threads")
|
|
232
|
-
table.add_column("ID", style=tm.style("info"))
|
|
232
|
+
table.add_column("ID", style=tm.style("info"), no_wrap=True, min_width=36)
|
|
233
233
|
table.add_column("Database", style=tm.style("accent"))
|
|
234
234
|
table.add_column("Title", style=tm.style("success"))
|
|
235
235
|
table.add_column("Last Activity", style=tm.style("muted"))
|
|
@@ -318,7 +318,9 @@ def resume(
|
|
|
318
318
|
)
|
|
319
319
|
return
|
|
320
320
|
|
|
321
|
-
db_conn = DatabaseConnection(
|
|
321
|
+
db_conn = DatabaseConnection(
|
|
322
|
+
connection_string, excluded_schemas=resolved.excluded_schemas
|
|
323
|
+
)
|
|
322
324
|
try:
|
|
323
325
|
sqlsaber_agent = SQLSaberAgent(db_conn, db_name)
|
|
324
326
|
history = await store.get_thread_messages(thread_id)
|
sqlsaber/config/database.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import platform
|
|
6
6
|
import stat
|
|
7
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
from urllib.parse import quote_plus
|
|
@@ -29,6 +29,7 @@ class DatabaseConfig:
|
|
|
29
29
|
ssl_cert: str | None = None
|
|
30
30
|
ssl_key: str | None = None
|
|
31
31
|
schema: str | None = None
|
|
32
|
+
exclude_schemas: list[str] = field(default_factory=list)
|
|
32
33
|
|
|
33
34
|
def to_connection_string(self) -> str:
|
|
34
35
|
"""Convert config to database connection string."""
|
|
@@ -149,6 +150,7 @@ class DatabaseConfig:
|
|
|
149
150
|
"ssl_cert": self.ssl_cert,
|
|
150
151
|
"ssl_key": self.ssl_key,
|
|
151
152
|
"schema": self.schema,
|
|
153
|
+
"exclude_schemas": self.exclude_schemas,
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
@classmethod
|
|
@@ -166,6 +168,7 @@ class DatabaseConfig:
|
|
|
166
168
|
ssl_cert=data.get("ssl_cert"),
|
|
167
169
|
ssl_key=data.get("ssl_key"),
|
|
168
170
|
schema=data.get("schema"),
|
|
171
|
+
exclude_schemas=list(data.get("exclude_schemas", [])),
|
|
169
172
|
)
|
|
170
173
|
|
|
171
174
|
|
|
@@ -246,6 +249,16 @@ class DatabaseConfigManager:
|
|
|
246
249
|
|
|
247
250
|
self._save_config(config)
|
|
248
251
|
|
|
252
|
+
def update_database(self, db_config: DatabaseConfig) -> None:
|
|
253
|
+
"""Update an existing database configuration."""
|
|
254
|
+
config = self._load_config()
|
|
255
|
+
|
|
256
|
+
if db_config.name not in config["connections"]:
|
|
257
|
+
raise ValueError(f"Database '{db_config.name}' does not exist")
|
|
258
|
+
|
|
259
|
+
config["connections"][db_config.name] = db_config.to_dict()
|
|
260
|
+
self._save_config(config)
|
|
261
|
+
|
|
249
262
|
def get_database(self, name: str) -> DatabaseConfig | None:
|
|
250
263
|
"""Get a database configuration by name."""
|
|
251
264
|
config = self._load_config()
|
sqlsaber/database/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Database module for SQLSaber."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
3
5
|
from .base import (
|
|
4
6
|
DEFAULT_QUERY_TIMEOUT,
|
|
5
7
|
BaseDatabaseConnection,
|
|
@@ -18,23 +20,28 @@ from .schema import SchemaManager
|
|
|
18
20
|
from .sqlite import SQLiteConnection, SQLiteSchemaIntrospector
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
def DatabaseConnection(
|
|
23
|
+
def DatabaseConnection(
|
|
24
|
+
connection_string: str, *, excluded_schemas: Iterable[str] | None = None
|
|
25
|
+
) -> BaseDatabaseConnection:
|
|
22
26
|
"""Factory function to create appropriate database connection based on connection string."""
|
|
23
27
|
if connection_string.startswith("postgresql://"):
|
|
24
|
-
|
|
28
|
+
conn = PostgreSQLConnection(connection_string)
|
|
25
29
|
elif connection_string.startswith("mysql://"):
|
|
26
|
-
|
|
30
|
+
conn = MySQLConnection(connection_string)
|
|
27
31
|
elif connection_string.startswith("sqlite:///"):
|
|
28
|
-
|
|
32
|
+
conn = SQLiteConnection(connection_string)
|
|
29
33
|
elif connection_string.startswith("duckdb://"):
|
|
30
|
-
|
|
34
|
+
conn = DuckDBConnection(connection_string)
|
|
31
35
|
elif connection_string.startswith("csv:///"):
|
|
32
|
-
|
|
36
|
+
conn = CSVConnection(connection_string)
|
|
33
37
|
else:
|
|
34
38
|
raise ValueError(
|
|
35
39
|
f"Unsupported database type in connection string: {connection_string}"
|
|
36
40
|
)
|
|
37
41
|
|
|
42
|
+
conn.set_excluded_schemas(excluded_schemas)
|
|
43
|
+
return conn
|
|
44
|
+
|
|
38
45
|
|
|
39
46
|
__all__ = [
|
|
40
47
|
# Base classes and types
|
sqlsaber/database/base.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Base classes and type definitions for database connections and schema introspection."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Iterable
|
|
4
6
|
from typing import Any, TypedDict
|
|
5
7
|
|
|
6
8
|
# Default query timeout to prevent runaway queries
|
|
@@ -62,6 +64,7 @@ class BaseDatabaseConnection(ABC):
|
|
|
62
64
|
def __init__(self, connection_string: str):
|
|
63
65
|
self.connection_string = connection_string
|
|
64
66
|
self._pool = None
|
|
67
|
+
self._excluded_schemas: list[str] = []
|
|
65
68
|
|
|
66
69
|
@property
|
|
67
70
|
@abstractmethod
|
|
@@ -95,6 +98,27 @@ class BaseDatabaseConnection(ABC):
|
|
|
95
98
|
"""
|
|
96
99
|
pass
|
|
97
100
|
|
|
101
|
+
def set_excluded_schemas(self, schemas: Iterable[str] | None) -> None:
|
|
102
|
+
"""Set schemas to exclude from introspection for this connection."""
|
|
103
|
+
self._excluded_schemas = []
|
|
104
|
+
if not schemas:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
seen: set[str] = set()
|
|
108
|
+
for schema in schemas:
|
|
109
|
+
clean = schema.strip()
|
|
110
|
+
if not clean:
|
|
111
|
+
continue
|
|
112
|
+
if clean in seen:
|
|
113
|
+
continue
|
|
114
|
+
seen.add(clean)
|
|
115
|
+
self._excluded_schemas.append(clean)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def excluded_schemas(self) -> list[str]:
|
|
119
|
+
"""Return list of excluded schemas for this connection."""
|
|
120
|
+
return list(self._excluded_schemas)
|
|
121
|
+
|
|
98
122
|
|
|
99
123
|
class BaseSchemaIntrospector(ABC):
|
|
100
124
|
"""Abstract base class for database-specific schema introspection."""
|
|
@@ -130,3 +154,36 @@ class BaseSchemaIntrospector(ABC):
|
|
|
130
154
|
async def list_tables_info(self, connection) -> list[dict[str, Any]]:
|
|
131
155
|
"""Get list of tables with basic information."""
|
|
132
156
|
pass
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _merge_excluded_schemas(
|
|
160
|
+
connection: BaseDatabaseConnection,
|
|
161
|
+
defaults: Iterable[str],
|
|
162
|
+
env_var: str | None = None,
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Combine default, connection, and environment schema exclusions."""
|
|
165
|
+
|
|
166
|
+
combined: list[str] = []
|
|
167
|
+
seen: set[str] = set()
|
|
168
|
+
|
|
169
|
+
def _add(items: Iterable[str]) -> None:
|
|
170
|
+
for item in items:
|
|
171
|
+
name = item.strip()
|
|
172
|
+
if not name:
|
|
173
|
+
continue
|
|
174
|
+
if name in seen:
|
|
175
|
+
continue
|
|
176
|
+
seen.add(name)
|
|
177
|
+
combined.append(name)
|
|
178
|
+
|
|
179
|
+
_add(defaults)
|
|
180
|
+
_add(getattr(connection, "excluded_schemas", []) or [])
|
|
181
|
+
|
|
182
|
+
if env_var:
|
|
183
|
+
raw = os.getenv(env_var, "")
|
|
184
|
+
if raw:
|
|
185
|
+
# Support comma-separated values
|
|
186
|
+
values = [part.strip() for part in raw.split(",")]
|
|
187
|
+
_add(values)
|
|
188
|
+
|
|
189
|
+
return combined
|
sqlsaber/database/duckdb.py
CHANGED
|
@@ -95,6 +95,13 @@ class DuckDBConnection(BaseDatabaseConnection):
|
|
|
95
95
|
class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
|
|
96
96
|
"""DuckDB-specific schema introspection."""
|
|
97
97
|
|
|
98
|
+
def _get_excluded_schemas(self, connection) -> list[str]:
|
|
99
|
+
"""Return schemas to exclude during introspection."""
|
|
100
|
+
defaults = ["information_schema", "pg_catalog", "duckdb_catalog"]
|
|
101
|
+
return self._merge_excluded_schemas(
|
|
102
|
+
connection, defaults, env_var="SQLSABER_DUCKDB_EXCLUDE_SCHEMAS"
|
|
103
|
+
)
|
|
104
|
+
|
|
98
105
|
async def _execute_query(
|
|
99
106
|
self,
|
|
100
107
|
connection,
|
|
@@ -131,11 +138,15 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
131
138
|
self, connection, table_pattern: str | None = None
|
|
132
139
|
) -> list[dict[str, Any]]:
|
|
133
140
|
"""Get tables information for DuckDB."""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
]
|
|
141
|
+
excluded = self._get_excluded_schemas(connection)
|
|
142
|
+
where_conditions: list[str] = []
|
|
137
143
|
params: list[Any] = []
|
|
138
144
|
|
|
145
|
+
if excluded:
|
|
146
|
+
placeholders = ", ".join(["?"] * len(excluded))
|
|
147
|
+
where_conditions.append(f"t.table_schema NOT IN ({placeholders})")
|
|
148
|
+
params.extend(excluded)
|
|
149
|
+
|
|
139
150
|
if table_pattern:
|
|
140
151
|
if "." in table_pattern:
|
|
141
152
|
schema_pattern, table_name_pattern = table_pattern.split(".", 1)
|
|
@@ -149,6 +160,9 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
149
160
|
)
|
|
150
161
|
params.extend([table_pattern, table_pattern])
|
|
151
162
|
|
|
163
|
+
if not where_conditions:
|
|
164
|
+
where_conditions.append("1=1")
|
|
165
|
+
|
|
152
166
|
query = f"""
|
|
153
167
|
SELECT
|
|
154
168
|
t.table_schema,
|
|
@@ -316,7 +330,16 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
316
330
|
|
|
317
331
|
async def list_tables_info(self, connection) -> list[dict[str, Any]]:
|
|
318
332
|
"""Get list of tables with basic information for DuckDB."""
|
|
319
|
-
|
|
333
|
+
excluded = self._get_excluded_schemas(connection)
|
|
334
|
+
params: list[Any] = []
|
|
335
|
+
if excluded:
|
|
336
|
+
placeholders = ", ".join(["?"] * len(excluded))
|
|
337
|
+
where_clause = f"WHERE t.table_schema NOT IN ({placeholders})"
|
|
338
|
+
params.extend(excluded)
|
|
339
|
+
else:
|
|
340
|
+
where_clause = ""
|
|
341
|
+
|
|
342
|
+
query = f"""
|
|
320
343
|
SELECT
|
|
321
344
|
t.table_schema,
|
|
322
345
|
t.table_name,
|
|
@@ -326,8 +349,8 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
326
349
|
LEFT JOIN duckdb_tables() dt
|
|
327
350
|
ON t.table_schema = dt.schema_name
|
|
328
351
|
AND t.table_name = dt.table_name
|
|
329
|
-
|
|
352
|
+
{where_clause}
|
|
330
353
|
ORDER BY t.table_schema, t.table_name;
|
|
331
354
|
"""
|
|
332
355
|
|
|
333
|
-
return await self._execute_query(connection, query)
|
|
356
|
+
return await self._execute_query(connection, query, tuple(params))
|
sqlsaber/database/mysql.py
CHANGED
|
@@ -153,6 +153,13 @@ class MySQLConnection(BaseDatabaseConnection):
|
|
|
153
153
|
class MySQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
154
154
|
"""MySQL-specific schema introspection."""
|
|
155
155
|
|
|
156
|
+
def _get_excluded_schemas(self, connection) -> list[str]:
|
|
157
|
+
"""Return schemas to exclude during introspection."""
|
|
158
|
+
defaults = ["information_schema", "performance_schema", "mysql", "sys"]
|
|
159
|
+
return self._merge_excluded_schemas(
|
|
160
|
+
connection, defaults, env_var="SQLSABER_MYSQL_EXCLUDE_SCHEMAS"
|
|
161
|
+
)
|
|
162
|
+
|
|
156
163
|
def _build_table_filter_clause(self, tables: list) -> tuple[str, list]:
|
|
157
164
|
"""Build row constructor with bind parameters for table filtering.
|
|
158
165
|
|
|
@@ -178,10 +185,14 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
178
185
|
async with pool.acquire() as conn:
|
|
179
186
|
async with conn.cursor() as cursor:
|
|
180
187
|
# Build WHERE clause for filtering
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
]
|
|
184
|
-
|
|
188
|
+
excluded = self._get_excluded_schemas(connection)
|
|
189
|
+
where_conditions = []
|
|
190
|
+
params: list[Any] = []
|
|
191
|
+
|
|
192
|
+
if excluded:
|
|
193
|
+
placeholders = ", ".join(["%s"] * len(excluded))
|
|
194
|
+
where_conditions.append(f"table_schema NOT IN ({placeholders})")
|
|
195
|
+
params.extend(excluded)
|
|
185
196
|
|
|
186
197
|
if table_pattern:
|
|
187
198
|
# Support patterns like 'schema.table' or just 'table'
|
|
@@ -197,6 +208,9 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
197
208
|
)
|
|
198
209
|
params.extend([table_pattern, table_pattern])
|
|
199
210
|
|
|
211
|
+
if not where_conditions:
|
|
212
|
+
where_conditions.append("1=1")
|
|
213
|
+
|
|
200
214
|
# Get tables
|
|
201
215
|
tables_query = f"""
|
|
202
216
|
SELECT
|
|
@@ -329,17 +343,26 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
329
343
|
async with pool.acquire() as conn:
|
|
330
344
|
async with conn.cursor() as cursor:
|
|
331
345
|
# Get tables without row counts for better performance
|
|
332
|
-
|
|
346
|
+
excluded = self._get_excluded_schemas(connection)
|
|
347
|
+
params: list[Any] = []
|
|
348
|
+
if excluded:
|
|
349
|
+
placeholders = ", ".join(["%s"] * len(excluded))
|
|
350
|
+
where_clause = f"WHERE t.table_schema NOT IN ({placeholders})"
|
|
351
|
+
params.extend(excluded)
|
|
352
|
+
else:
|
|
353
|
+
where_clause = ""
|
|
354
|
+
|
|
355
|
+
tables_query = f"""
|
|
333
356
|
SELECT
|
|
334
357
|
t.table_schema,
|
|
335
358
|
t.table_name,
|
|
336
359
|
t.table_type,
|
|
337
360
|
t.table_comment
|
|
338
361
|
FROM information_schema.tables t
|
|
339
|
-
|
|
362
|
+
{where_clause}
|
|
340
363
|
ORDER BY t.table_schema, t.table_name;
|
|
341
364
|
"""
|
|
342
|
-
await cursor.execute(tables_query)
|
|
365
|
+
await cursor.execute(tables_query, params if params else None)
|
|
343
366
|
rows = await cursor.fetchall()
|
|
344
367
|
|
|
345
368
|
# Convert rows to dictionaries
|
sqlsaber/database/postgresql.py
CHANGED
|
@@ -135,7 +135,7 @@ class PostgreSQLConnection(BaseDatabaseConnection):
|
|
|
135
135
|
class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
136
136
|
"""PostgreSQL-specific schema introspection."""
|
|
137
137
|
|
|
138
|
-
def _get_excluded_schemas(self) -> list[str]:
|
|
138
|
+
def _get_excluded_schemas(self, connection) -> list[str]:
|
|
139
139
|
"""Return schemas to exclude during introspection.
|
|
140
140
|
|
|
141
141
|
Defaults include PostgreSQL system schemas and TimescaleDB internal
|
|
@@ -143,10 +143,7 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
143
143
|
environment variable `SQLSABER_PG_EXCLUDE_SCHEMAS` to a comma-separated
|
|
144
144
|
list of schema names.
|
|
145
145
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# Base exclusions: system schemas and TimescaleDB internal partitions
|
|
149
|
-
excluded = [
|
|
146
|
+
defaults = [
|
|
150
147
|
"pg_catalog",
|
|
151
148
|
"information_schema",
|
|
152
149
|
"_timescaledb_internal",
|
|
@@ -155,14 +152,9 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
155
152
|
"_timescaledb_catalog",
|
|
156
153
|
]
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
name = item.strip()
|
|
162
|
-
if name and name not in excluded:
|
|
163
|
-
excluded.append(name)
|
|
164
|
-
|
|
165
|
-
return excluded
|
|
155
|
+
return self._merge_excluded_schemas(
|
|
156
|
+
connection, defaults, env_var="SQLSABER_PG_EXCLUDE_SCHEMAS"
|
|
157
|
+
)
|
|
166
158
|
|
|
167
159
|
def _build_table_filter_clause(self, tables: list) -> tuple[str, list]:
|
|
168
160
|
"""Build VALUES clause with bind parameters for table filtering.
|
|
@@ -193,7 +185,7 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
193
185
|
where_conditions: list[str] = []
|
|
194
186
|
params: list[Any] = []
|
|
195
187
|
|
|
196
|
-
excluded = self._get_excluded_schemas()
|
|
188
|
+
excluded = self._get_excluded_schemas(connection)
|
|
197
189
|
if excluded:
|
|
198
190
|
placeholders = ", ".join(f"${i + 1}" for i in range(len(excluded)))
|
|
199
191
|
where_conditions.append(f"table_schema NOT IN ({placeholders})")
|
|
@@ -354,7 +346,7 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
354
346
|
pool = await connection.get_pool()
|
|
355
347
|
async with pool.acquire() as conn:
|
|
356
348
|
# Exclude system schemas (and TimescaleDB internals) for performance
|
|
357
|
-
excluded = self._get_excluded_schemas()
|
|
349
|
+
excluded = self._get_excluded_schemas(connection)
|
|
358
350
|
params: list[Any] = []
|
|
359
351
|
if excluded:
|
|
360
352
|
placeholders = ", ".join(f"${i + 1}" for i in range(len(excluded)))
|
sqlsaber/database/resolver.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Database connection resolution from CLI input."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
from dataclasses import dataclass
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from urllib.parse import urlparse
|
|
@@ -21,6 +19,7 @@ class ResolvedDatabase:
|
|
|
21
19
|
|
|
22
20
|
name: str # Human-readable name for display/logging
|
|
23
21
|
connection_string: str # Canonical connection string for DatabaseConnection factory
|
|
22
|
+
excluded_schemas: list[str]
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
SUPPORTED_SCHEMES = {"postgresql", "mysql", "sqlite", "duckdb", "csv"}
|
|
@@ -60,6 +59,7 @@ def resolve_database(
|
|
|
60
59
|
return ResolvedDatabase(
|
|
61
60
|
name=db_cfg.name,
|
|
62
61
|
connection_string=db_cfg.to_connection_string(),
|
|
62
|
+
excluded_schemas=list(db_cfg.exclude_schemas),
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# 1. Connection string?
|
|
@@ -71,22 +71,30 @@ def resolve_database(
|
|
|
71
71
|
db_name = Path(urlparse(spec).path).stem or "database"
|
|
72
72
|
else: # should not happen because of SUPPORTED_SCHEMES
|
|
73
73
|
db_name = "database"
|
|
74
|
-
return ResolvedDatabase(
|
|
74
|
+
return ResolvedDatabase(
|
|
75
|
+
name=db_name, connection_string=spec, excluded_schemas=[]
|
|
76
|
+
)
|
|
75
77
|
|
|
76
78
|
# 2. Raw file path?
|
|
77
79
|
path = Path(spec).expanduser().resolve()
|
|
78
80
|
if path.suffix.lower() == ".csv":
|
|
79
81
|
if not path.exists():
|
|
80
82
|
raise DatabaseResolutionError(f"CSV file '{spec}' not found.")
|
|
81
|
-
return ResolvedDatabase(
|
|
83
|
+
return ResolvedDatabase(
|
|
84
|
+
name=path.stem, connection_string=f"csv:///{path}", excluded_schemas=[]
|
|
85
|
+
)
|
|
82
86
|
if path.suffix.lower() in {".db", ".sqlite", ".sqlite3"}:
|
|
83
87
|
if not path.exists():
|
|
84
88
|
raise DatabaseResolutionError(f"SQLite file '{spec}' not found.")
|
|
85
|
-
return ResolvedDatabase(
|
|
89
|
+
return ResolvedDatabase(
|
|
90
|
+
name=path.stem, connection_string=f"sqlite:///{path}", excluded_schemas=[]
|
|
91
|
+
)
|
|
86
92
|
if path.suffix.lower() in {".duckdb", ".ddb"}:
|
|
87
93
|
if not path.exists():
|
|
88
94
|
raise DatabaseResolutionError(f"DuckDB file '{spec}' not found.")
|
|
89
|
-
return ResolvedDatabase(
|
|
95
|
+
return ResolvedDatabase(
|
|
96
|
+
name=path.stem, connection_string=f"duckdb:///{path}", excluded_schemas=[]
|
|
97
|
+
)
|
|
90
98
|
|
|
91
99
|
# 3. Must be a configured name
|
|
92
100
|
db_cfg: DatabaseConfig | None = config_mgr.get_database(spec)
|
|
@@ -96,5 +104,7 @@ def resolve_database(
|
|
|
96
104
|
"Use 'sqlsaber db list' to see available connections."
|
|
97
105
|
)
|
|
98
106
|
return ResolvedDatabase(
|
|
99
|
-
name=db_cfg.name,
|
|
107
|
+
name=db_cfg.name,
|
|
108
|
+
connection_string=db_cfg.to_connection_string(),
|
|
109
|
+
excluded_schemas=list(db_cfg.exclude_schemas),
|
|
100
110
|
)
|
|
@@ -5,38 +5,38 @@ sqlsaber/agents/base.py,sha256=T05UsMZPwAlMhsJFpuuVI1RNDhdiwiEsgCWr9MbPoAU,2654
|
|
|
5
5
|
sqlsaber/agents/pydantic_ai_agent.py,sha256=6_KppII8YcMw74KOGsYI5Dt6AP8WSduK3yAXCawVex4,10643
|
|
6
6
|
sqlsaber/application/__init__.py,sha256=KY_-d5nEdQyAwNOsK5r-f7Tb69c63XbuEkHPeLpJal8,84
|
|
7
7
|
sqlsaber/application/auth_setup.py,sha256=wbi9MaYl6q27LjcSBZmqFC12JtE5hrUHEX1NmD-7UVc,7778
|
|
8
|
-
sqlsaber/application/db_setup.py,sha256=
|
|
8
|
+
sqlsaber/application/db_setup.py,sha256=QX4VX8X9BiCBS3kFDd1-cPcEsCX0NwqNAOhX44pPtC4,8127
|
|
9
9
|
sqlsaber/application/model_selection.py,sha256=fSC06MZNKinHDR-csMFVYYJFyK8MydKf6pStof74Jp0,3191
|
|
10
10
|
sqlsaber/application/prompts.py,sha256=4rMGcWpYJbNWPMzqVWseUMx0nwvXOkWS6GaTAJ5mhfc,3473
|
|
11
11
|
sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
|
|
12
12
|
sqlsaber/cli/auth.py,sha256=TmAD4BJr2gIjPeW2LA1QkKK5gUZ2OOS6vOmBvB6PC0M,7051
|
|
13
|
-
sqlsaber/cli/commands.py,sha256=
|
|
13
|
+
sqlsaber/cli/commands.py,sha256=P49Qylnk_kEmGCP5fHY5YrNyDhhL_GU20qcXBAv_QLI,11167
|
|
14
14
|
sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
|
|
15
|
-
sqlsaber/cli/database.py,sha256=
|
|
15
|
+
sqlsaber/cli/database.py,sha256=RIBPFoLqXT5_RQ2rp-vYDh6wXqR6RAgbYTqZRrA9GkU,17212
|
|
16
16
|
sqlsaber/cli/display.py,sha256=wxkEFceyyUloDUB49Gzqhl6JGRGq2cQdmJ3XQeYd6ok,18599
|
|
17
|
-
sqlsaber/cli/interactive.py,sha256=
|
|
17
|
+
sqlsaber/cli/interactive.py,sha256=K2SwD1uY19ZK_X9MMNLJk1K7BVhDT95if8WJDvsLF0g,14301
|
|
18
18
|
sqlsaber/cli/memory.py,sha256=kAY5LLFueIF30gJ8ibfrFw42rOyy5wajeJGS4h5XQw4,9475
|
|
19
19
|
sqlsaber/cli/models.py,sha256=aVHazP_fiT-Mj9AtCdjliDtq3E3fJrhgP4oF5p4CuwI,9593
|
|
20
20
|
sqlsaber/cli/onboarding.py,sha256=iBGT-W-OJFRvQoEpuHYyO1c9Mym5c97eIefRvxGHtTg,11265
|
|
21
21
|
sqlsaber/cli/streaming.py,sha256=jicSDLWQ3efitpdc2y4QsasHcEW8ogZ4lHcWmftq9Ao,6763
|
|
22
22
|
sqlsaber/cli/theme.py,sha256=D6HIt7rmF00B5ZOCV5lXKzPICE4uppHdraOdVs7k5Nw,4672
|
|
23
|
-
sqlsaber/cli/threads.py,sha256=
|
|
23
|
+
sqlsaber/cli/threads.py,sha256=4pCpnEwkAW3TIaLaDfn0oixhW18LUjHvf14GuUX9ZWk,14737
|
|
24
24
|
sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
|
|
25
25
|
sqlsaber/config/api_keys.py,sha256=H7xBU1mflngXIlnaDjbTvuNqZsW_92AIve31eDLij34,4513
|
|
26
26
|
sqlsaber/config/auth.py,sha256=G1uulySUclWSId8EIt1hPNmsUNhbfRzfp8VVQftYyG8,2964
|
|
27
|
-
sqlsaber/config/database.py,sha256=
|
|
27
|
+
sqlsaber/config/database.py,sha256=Mu92aYjc2ONNA4xbnDcEuq3A4yDFpsp8RY5aOjX7Fs0,12034
|
|
28
28
|
sqlsaber/config/logging.py,sha256=vv4oCuQePeYQ7bMs0OLKj8ZSiNcFWbHmWtdC0lTsUyc,6173
|
|
29
29
|
sqlsaber/config/oauth_flow.py,sha256=cDfaJjqr4spNuzxbAlzuJfk6SEe1ojSRAkoOWlvQYy0,11037
|
|
30
30
|
sqlsaber/config/oauth_tokens.py,sha256=KCC2u3lOjdh0M-rd0K1rW0PWk58w7mqpodAhlPVp9NE,6424
|
|
31
31
|
sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
|
|
32
32
|
sqlsaber/config/settings.py,sha256=-nIBNt9E0tCRGd14bk4x-bNAwO12sbsjRsN8fFannK4,6449
|
|
33
|
-
sqlsaber/database/__init__.py,sha256=
|
|
34
|
-
sqlsaber/database/base.py,sha256=
|
|
33
|
+
sqlsaber/database/__init__.py,sha256=s1NgBAuLfWVDDXut4Mcm4wqbiC6jPQzNP4tVfm6pI-A,2184
|
|
34
|
+
sqlsaber/database/base.py,sha256=VFPlOx0_pfAj4-V8oe_r4cbnwh_B-amL48E6W_H8U-c,5502
|
|
35
35
|
sqlsaber/database/csv.py,sha256=41wuP40FaGPfj28HMiD0I69uG0JbUxArpoTLC3MG2uc,4464
|
|
36
|
-
sqlsaber/database/duckdb.py,sha256=
|
|
37
|
-
sqlsaber/database/mysql.py,sha256=
|
|
38
|
-
sqlsaber/database/postgresql.py,sha256=
|
|
39
|
-
sqlsaber/database/resolver.py,sha256=
|
|
36
|
+
sqlsaber/database/duckdb.py,sha256=s3dpGKXac-WsPSHzh_Y6TDcz7Lne_0FnOf38l52snWQ,12808
|
|
37
|
+
sqlsaber/database/mysql.py,sha256=sqeuJZTXrKIk06hqI6qe3uaSPbrkfz-DbV2OQjn1sZc,15616
|
|
38
|
+
sqlsaber/database/postgresql.py,sha256=mfk3_JOzJBR9wc1opjhZ5bvTk2Cq3YhFJGooZU9S0to,15473
|
|
39
|
+
sqlsaber/database/resolver.py,sha256=ENuXUPwfeDweHq7rrdL9eJAHS5UkSO_btAsW3onp_DY,3919
|
|
40
40
|
sqlsaber/database/schema.py,sha256=T4dEYOk-Cy5XkhvvQ3GqAU2w584eRivbiu9UZ0nHTBo,7138
|
|
41
41
|
sqlsaber/database/sqlite.py,sha256=juEVIhSwbeT0gMkEhkjwNUJn-Mif_IIDmXVqostNT-o,9918
|
|
42
42
|
sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
|
|
@@ -55,8 +55,8 @@ sqlsaber/tools/base.py,sha256=NKEEooliPKTJj_Pomwte_wW0Xd9Z5kXNfVdCRfTppuw,883
|
|
|
55
55
|
sqlsaber/tools/registry.py,sha256=XmBzERq0LJXtg3BZ-r8cEyt8J54NUekgUlTJ_EdSYMk,2204
|
|
56
56
|
sqlsaber/tools/sql_guard.py,sha256=dTDwcZP-N4xPGzcr7MQtKUxKrlDzlc1irr9aH5a4wvk,6182
|
|
57
57
|
sqlsaber/tools/sql_tools.py,sha256=q479PNneuAlpaDCp2cyw1MFhLUY4vcUHV_ZyIuSMHK0,7796
|
|
58
|
-
sqlsaber-0.
|
|
59
|
-
sqlsaber-0.
|
|
60
|
-
sqlsaber-0.
|
|
61
|
-
sqlsaber-0.
|
|
62
|
-
sqlsaber-0.
|
|
58
|
+
sqlsaber-0.36.0.dist-info/METADATA,sha256=55XqjlW56tN7F_jTMcrRpyPEBmmNLWiHjtQnNjbXzf8,5915
|
|
59
|
+
sqlsaber-0.36.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
60
|
+
sqlsaber-0.36.0.dist-info/entry_points.txt,sha256=tw1mB0fjlkXQiOsC0434X6nE-o1cFCuQwt2ZYHv_WAE,91
|
|
61
|
+
sqlsaber-0.36.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
62
|
+
sqlsaber-0.36.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|