sqlsaber 0.35.0__tar.gz → 0.36.0__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 sqlsaber might be problematic. Click here for more details.

Files changed (130) hide show
  1. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/PKG-INFO +1 -1
  2. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/changelog.md +15 -0
  3. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/database-setup.mdx +19 -10
  4. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/reference/commands.md +23 -0
  5. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/pyproject.toml +1 -1
  6. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/application/db_setup.py +38 -2
  7. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/commands.py +5 -1
  8. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/database.py +160 -12
  9. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/interactive.py +2 -0
  10. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/threads.py +4 -2
  11. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/database.py +14 -1
  12. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/__init__.py +13 -6
  13. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/base.py +57 -0
  14. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/duckdb.py +29 -6
  15. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/mysql.py +30 -7
  16. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/postgresql.py +7 -15
  17. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/resolver.py +17 -7
  18. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_config/test_database.py +2 -0
  19. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_duckdb_module.py +42 -0
  20. sqlsaber-0.36.0/tests/test_database/test_mysql_module.py +49 -0
  21. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_postgresql_module.py +36 -2
  22. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_schema_display.py +3 -1
  23. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database_resolver.py +15 -0
  24. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/uv.lock +1 -1
  25. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.github/workflows/claude-code-review.yml +0 -0
  26. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.github/workflows/claude.yml +0 -0
  27. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.github/workflows/deploy-docs.yml +0 -0
  28. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.github/workflows/publish.yml +0 -0
  29. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.github/workflows/test.yml +0 -0
  30. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.gitignore +0 -0
  31. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/.python-version +0 -0
  32. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/AGENTS.md +0 -0
  33. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/CLAUDE.md +0 -0
  34. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/LICENSE +0 -0
  35. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/README.md +0 -0
  36. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/.gitignore +0 -0
  37. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/.vscode/extensions.json +0 -0
  38. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/.vscode/launch.json +0 -0
  39. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/CLAUDE.md +0 -0
  40. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/astro.config.mjs +0 -0
  41. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/package-lock.json +0 -0
  42. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/package.json +0 -0
  43. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/public/CNAME +0 -0
  44. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/public/favicon.svg +0 -0
  45. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/assets/sqlsaber.gif +0 -0
  46. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/authentication.mdx +0 -0
  47. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/getting-started.mdx +0 -0
  48. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/memory.mdx +0 -0
  49. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/models.mdx +0 -0
  50. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/queries.mdx +0 -0
  51. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/guides/threads.md +0 -0
  52. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/index.mdx +0 -0
  53. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content/docs/installation.mdx +0 -0
  54. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/content.config.ts +0 -0
  55. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/src/styles/global.css +0 -0
  56. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/docs/tsconfig.json +0 -0
  57. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/legislators.db +0 -0
  58. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/pytest.ini +0 -0
  59. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/sqlsaber.gif +0 -0
  60. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/sqlsaber.svg +0 -0
  61. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/__init__.py +0 -0
  62. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/__main__.py +0 -0
  63. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/agents/__init__.py +0 -0
  64. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/agents/base.py +0 -0
  65. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/agents/pydantic_ai_agent.py +0 -0
  66. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/application/__init__.py +0 -0
  67. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/application/auth_setup.py +0 -0
  68. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/application/model_selection.py +0 -0
  69. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/application/prompts.py +0 -0
  70. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/__init__.py +0 -0
  71. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/auth.py +0 -0
  72. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/completers.py +0 -0
  73. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/display.py +0 -0
  74. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/memory.py +0 -0
  75. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/models.py +0 -0
  76. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/onboarding.py +0 -0
  77. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/streaming.py +0 -0
  78. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/cli/theme.py +0 -0
  79. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/__init__.py +0 -0
  80. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/api_keys.py +0 -0
  81. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/auth.py +0 -0
  82. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/logging.py +0 -0
  83. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/oauth_flow.py +0 -0
  84. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
  85. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/providers.py +0 -0
  86. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/config/settings.py +0 -0
  87. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/csv.py +0 -0
  88. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/schema.py +0 -0
  89. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/database/sqlite.py +0 -0
  90. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/memory/__init__.py +0 -0
  91. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/memory/manager.py +0 -0
  92. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/memory/storage.py +0 -0
  93. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/prompts/__init__.py +0 -0
  94. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/prompts/claude.py +0 -0
  95. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/prompts/memory.py +0 -0
  96. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/prompts/openai.py +0 -0
  97. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/theme/__init__.py +0 -0
  98. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/theme/manager.py +0 -0
  99. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/threads/__init__.py +0 -0
  100. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/threads/storage.py +0 -0
  101. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/tools/__init__.py +0 -0
  102. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/tools/base.py +0 -0
  103. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/tools/registry.py +0 -0
  104. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/tools/sql_guard.py +0 -0
  105. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/src/sqlsaber/tools/sql_tools.py +0 -0
  106. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/__init__.py +0 -0
  107. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/conftest.py +0 -0
  108. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_application/test_auth_setup.py +0 -0
  109. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_cli/__init__.py +0 -0
  110. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_cli/test_auth_reset.py +0 -0
  111. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_cli/test_commands.py +0 -0
  112. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_cli/test_threads.py +0 -0
  113. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_config/__init__.py +0 -0
  114. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_config/test_oauth.py +0 -0
  115. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_config/test_providers.py +0 -0
  116. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_config/test_settings.py +0 -0
  117. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/__init__.py +0 -0
  118. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_connection.py +0 -0
  119. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_csv_connection.py +0 -0
  120. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_csv_module.py +0 -0
  121. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_schema.py +0 -0
  122. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_sqlite_module.py +0 -0
  123. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_database/test_timeout.py +0 -0
  124. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_theme/test_manager.py +0 -0
  125. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_threads_storage.py +0 -0
  126. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_tools/__init__.py +0 -0
  127. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_tools/test_base.py +0 -0
  128. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_tools/test_registry.py +0 -0
  129. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_tools/test_sql_guard.py +0 -0
  130. {sqlsaber-0.35.0 → sqlsaber-0.36.0}/tests/test_tools/test_sql_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.35.0
3
+ Version: 0.36.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -9,6 +9,21 @@ All notable changes to SQLsaber will be documented here.
9
9
 
10
10
  ---
11
11
 
12
+ ### v0.36.0 - 2025-10-23
13
+
14
+ #### Added
15
+
16
+ - `saber db exclude` command to manage schema exclusions without re-adding a connection, plus a `--exclude-schemas` flag when creating connections.
17
+ - Model name display in both interactive and non-interactive modes alongside database connection information.
18
+
19
+ #### Changed
20
+
21
+ - Schema exclusion configuration now applies to PostgreSQL, MySQL, DuckDB, and CSV connections, combining per-connection settings with environment variables such as `SQLSABER_MYSQL_EXCLUDE_SCHEMAS` and `SQLSABER_DUCKDB_EXCLUDE_SCHEMAS`.
22
+
23
+ #### Fixed
24
+
25
+ - Fixed ID column truncation in `saber threads list` on smaller terminal screens by ensuring full UUID visibility.
26
+
12
27
  ### v0.35.0 - 2025-10-22
13
28
 
14
29
  #### Added
@@ -133,23 +133,32 @@ SQLsaber stores database passwords securely using your operating system's creden
133
133
 
134
134
  Store SSL certificates in a secure location and use absolute paths when configuring connections.
135
135
 
136
- ### Schema Introspection Filters (PostgreSQL)
136
+ ### Schema Introspection Filters
137
137
 
138
- By default, SQLsaber excludes system and TimescaleDB internal schemas during schema discovery to keep results focused and fast.
138
+ SQLsaber skips noisy system schemas for every supported database when it introspects metadata, and you can layer on your own exclusions during setup or later via `saber db exclude`.
139
139
 
140
- Excluded by default:
140
+ Default exclusions:
141
141
 
142
- - `pg_catalog`
143
- - `information_schema`
144
- - `_timescaledb_internal`
145
- - `_timescaledb_cache`
146
- - `_timescaledb_config`
147
- - `_timescaledb_catalog`
142
+ - **PostgreSQL:** `pg_catalog`, `information_schema`, `_timescaledb_internal`, `_timescaledb_cache`, `_timescaledb_config`, `_timescaledb_catalog`
143
+ - **MySQL:** `information_schema`, `performance_schema`, `mysql`, `sys`
144
+ - **DuckDB / CSV:** `information_schema`, `pg_catalog`, `duckdb_catalog`
148
145
 
149
- You can exclude additional schemas via environment variable before running `saber`:
146
+ When you run `saber db add`, the interactive flow prompts for extra schemas to ignore (or pass `--exclude-schemas schema1,schema2` in non-interactive mode). You can revisit an existing connection at any time:
147
+
148
+ ```bash
149
+ # Replace the exclusion list interactively
150
+ saber db exclude my-database
151
+
152
+ # Append specific schemas
153
+ saber db exclude my-database --add reporting_temp,rollups
154
+ ```
155
+
156
+ For ad-hoc connections created from raw URLs or file paths, use environment variables to extend the default filters:
150
157
 
151
158
  ```bash
152
159
  export SQLSABER_PG_EXCLUDE_SCHEMAS="schema1,schema2"
160
+ export SQLSABER_MYSQL_EXCLUDE_SCHEMAS="temp_db, staging"
161
+ export SQLSABER_DUCKDB_EXCLUDE_SCHEMAS="internal_schema"
153
162
  ```
154
163
 
155
164
  Both the `list_tables` and `introspect_schema` tools respect these filters.
@@ -104,6 +104,7 @@ saber db add my-database [OPTIONS]
104
104
  - `-p, --port` - Database port
105
105
  - `--database, --db` - Database name
106
106
  - `-u, --username` - Username
107
+ - `--exclude-schemas` - Comma-separated list of schemas to skip during introspection
107
108
  - `--ssl-mode` - SSL mode (see SSL options below)
108
109
  - `--ssl-ca` - SSL CA certificate file path
109
110
  - `--ssl-cert` - SSL client certificate file path
@@ -143,8 +144,28 @@ saber db list
143
144
 
144
145
  - Database names
145
146
  - Connection details (host, port, database)
147
+ - Any excluded schemas configured for the connection
146
148
  - Default database indicator
147
149
 
150
+ #### `saber db exclude NAME`
151
+
152
+ Update or inspect schema exclusions for an existing database connection.
153
+
154
+ **Usage:**
155
+
156
+ ```bash
157
+ saber db exclude my-database [--set SCHEMAS | --add SCHEMAS | --remove SCHEMAS | --clear]
158
+ ```
159
+
160
+ **Options:**
161
+
162
+ - `--set` — Replace the exclusion list entirely with the provided comma-separated schemas
163
+ - `--add` — Append schemas to the current exclusion list (duplicates are ignored)
164
+ - `--remove` — Remove the provided schemas from the exclusion list
165
+ - `--clear` — Remove all exclusions
166
+
167
+ Run without flags to interactively edit the exclusion list.
168
+
148
169
  #### `saber db set-default NAME`
149
170
 
150
171
  Set a database as the default connection.
@@ -445,3 +466,5 @@ These environment variables adjust runtime behavior:
445
466
 
446
467
  - `SQLSABER_THEME` — Override the configured theme for the session.
447
468
  - `SQLSABER_PG_EXCLUDE_SCHEMAS` — Comma-separated list of PostgreSQL schemas to exclude from schema discovery and introspection. Defaults already exclude `pg_catalog`, `information_schema`, `_timescaledb_internal`, `_timescaledb_cache`, `_timescaledb_config`, `_timescaledb_catalog`.
469
+ - `SQLSABER_MYSQL_EXCLUDE_SCHEMAS` — Comma-separated list of MySQL databases to omit from discovery. Defaults exclude `information_schema`, `performance_schema`, `mysql`, and `sys`.
470
+ - `SQLSABER_DUCKDB_EXCLUDE_SCHEMAS` — Comma-separated list of DuckDB schemas to skip during introspection. Defaults exclude `information_schema`, `pg_catalog`, and `duckdb_catalog`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.35.0"
3
+ version = "0.36.0"
4
4
  description = "SQLsaber - Open-source agentic SQL assistant"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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(connection_string)
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
@@ -214,7 +214,9 @@ def query(
214
214
 
215
215
  # Create database connection
216
216
  try:
217
- db_conn = DatabaseConnection(connection_string)
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(
@@ -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:[/bold error] Database connection '{name}' not found"
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"[green]Successfully removed database connection '{name}'[/green]"
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:[/bold error] Failed to remove database connection '{name}'"
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:[/bold error] Database connection '{name}' not found"
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(f"[green]Successfully set '{name}' as default database[/green]")
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:[/bold error] Failed to set '{name}' as default"
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:[/bold error] Database connection '{name}' not found"
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:[/bold error] No default database configured"
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(connection_string)
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"[green]✓ Connection to '{db_config.name}' successful[/green]"
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] {e}")
520
+ console.print(f"[bold error]✗ Connection failed: {e}[/bold error]")
373
521
  sys.exit(1)
374
522
 
375
523
  asyncio.run(test_connection())
@@ -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:
@@ -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(connection_string)
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)
@@ -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()
@@ -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(connection_string: str) -> BaseDatabaseConnection:
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
- return PostgreSQLConnection(connection_string)
28
+ conn = PostgreSQLConnection(connection_string)
25
29
  elif connection_string.startswith("mysql://"):
26
- return MySQLConnection(connection_string)
30
+ conn = MySQLConnection(connection_string)
27
31
  elif connection_string.startswith("sqlite:///"):
28
- return SQLiteConnection(connection_string)
32
+ conn = SQLiteConnection(connection_string)
29
33
  elif connection_string.startswith("duckdb://"):
30
- return DuckDBConnection(connection_string)
34
+ conn = DuckDBConnection(connection_string)
31
35
  elif connection_string.startswith("csv:///"):
32
- return CSVConnection(connection_string)
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