sqlsaber 0.33.0__tar.gz → 0.35.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.33.0 → sqlsaber-0.35.0}/PKG-INFO +1 -1
  2. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/changelog.md +29 -1
  3. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/database-setup.mdx +29 -4
  4. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/getting-started.mdx +23 -45
  5. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/models.mdx +7 -6
  6. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/reference/commands.md +9 -0
  7. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/pyproject.toml +1 -1
  8. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/application/auth_setup.py +83 -11
  9. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/auth.py +3 -1
  10. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/display.py +19 -3
  11. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/interactive.py +4 -2
  12. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/api_keys.py +23 -0
  13. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/auth.py +6 -0
  14. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/logging.py +0 -1
  15. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/base.py +2 -0
  16. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/duckdb.py +41 -26
  17. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/mysql.py +7 -3
  18. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/postgresql.py +70 -14
  19. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/schema.py +3 -0
  20. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/sqlite.py +18 -5
  21. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/tools/sql_tools.py +32 -21
  22. sqlsaber-0.35.0/tests/test_application/test_auth_setup.py +122 -0
  23. sqlsaber-0.35.0/tests/test_database/test_postgresql_module.py +95 -0
  24. sqlsaber-0.35.0/tests/test_database/test_schema.py +110 -0
  25. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_schema_display.py +33 -0
  26. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/uv.lock +1 -1
  27. sqlsaber-0.33.0/tests/test_database/test_schema.py +0 -47
  28. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.github/workflows/claude-code-review.yml +0 -0
  29. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.github/workflows/claude.yml +0 -0
  30. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.github/workflows/deploy-docs.yml +0 -0
  31. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.github/workflows/publish.yml +0 -0
  32. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.github/workflows/test.yml +0 -0
  33. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.gitignore +0 -0
  34. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/.python-version +0 -0
  35. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/AGENTS.md +0 -0
  36. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/CLAUDE.md +0 -0
  37. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/LICENSE +0 -0
  38. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/README.md +0 -0
  39. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/.gitignore +0 -0
  40. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/.vscode/extensions.json +0 -0
  41. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/.vscode/launch.json +0 -0
  42. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/CLAUDE.md +0 -0
  43. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/astro.config.mjs +0 -0
  44. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/package-lock.json +0 -0
  45. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/package.json +0 -0
  46. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/public/CNAME +0 -0
  47. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/public/favicon.svg +0 -0
  48. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/assets/sqlsaber.gif +0 -0
  49. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/authentication.mdx +0 -0
  50. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/memory.mdx +0 -0
  51. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/queries.mdx +0 -0
  52. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/guides/threads.md +0 -0
  53. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/index.mdx +0 -0
  54. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content/docs/installation.mdx +0 -0
  55. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/content.config.ts +0 -0
  56. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/src/styles/global.css +0 -0
  57. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/docs/tsconfig.json +0 -0
  58. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/legislators.db +0 -0
  59. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/pytest.ini +0 -0
  60. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/sqlsaber.gif +0 -0
  61. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/sqlsaber.svg +0 -0
  62. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/__init__.py +0 -0
  63. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/__main__.py +0 -0
  64. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/agents/__init__.py +0 -0
  65. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/agents/base.py +0 -0
  66. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/agents/pydantic_ai_agent.py +0 -0
  67. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/application/__init__.py +0 -0
  68. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/application/db_setup.py +0 -0
  69. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/application/model_selection.py +0 -0
  70. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/application/prompts.py +0 -0
  71. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/__init__.py +0 -0
  72. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/commands.py +0 -0
  73. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/completers.py +0 -0
  74. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/database.py +0 -0
  75. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/memory.py +0 -0
  76. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/models.py +0 -0
  77. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/onboarding.py +0 -0
  78. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/streaming.py +0 -0
  79. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/theme.py +0 -0
  80. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/cli/threads.py +0 -0
  81. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/__init__.py +0 -0
  82. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/database.py +0 -0
  83. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/oauth_flow.py +0 -0
  84. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
  85. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/providers.py +0 -0
  86. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/config/settings.py +0 -0
  87. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/__init__.py +0 -0
  88. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/csv.py +0 -0
  89. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/database/resolver.py +0 -0
  90. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/memory/__init__.py +0 -0
  91. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/memory/manager.py +0 -0
  92. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/memory/storage.py +0 -0
  93. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/prompts/__init__.py +0 -0
  94. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/prompts/claude.py +0 -0
  95. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/prompts/memory.py +0 -0
  96. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/prompts/openai.py +0 -0
  97. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/theme/__init__.py +0 -0
  98. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/theme/manager.py +0 -0
  99. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/threads/__init__.py +0 -0
  100. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/threads/storage.py +0 -0
  101. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/tools/__init__.py +0 -0
  102. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/tools/base.py +0 -0
  103. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/tools/registry.py +0 -0
  104. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/src/sqlsaber/tools/sql_guard.py +0 -0
  105. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/__init__.py +0 -0
  106. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/conftest.py +0 -0
  107. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_cli/__init__.py +0 -0
  108. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_cli/test_auth_reset.py +0 -0
  109. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_cli/test_commands.py +0 -0
  110. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_cli/test_threads.py +0 -0
  111. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_config/__init__.py +0 -0
  112. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_config/test_database.py +0 -0
  113. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_config/test_oauth.py +0 -0
  114. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_config/test_providers.py +0 -0
  115. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_config/test_settings.py +0 -0
  116. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/__init__.py +0 -0
  117. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_connection.py +0 -0
  118. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_csv_connection.py +0 -0
  119. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_csv_module.py +0 -0
  120. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_duckdb_module.py +0 -0
  121. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_sqlite_module.py +0 -0
  122. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database/test_timeout.py +0 -0
  123. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_database_resolver.py +0 -0
  124. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_theme/test_manager.py +0 -0
  125. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_threads_storage.py +0 -0
  126. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_tools/__init__.py +0 -0
  127. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_tools/test_base.py +0 -0
  128. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_tools/test_registry.py +0 -0
  129. {sqlsaber-0.33.0 → sqlsaber-0.35.0}/tests/test_tools/test_sql_guard.py +0 -0
  130. {sqlsaber-0.33.0 → sqlsaber-0.35.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.33.0
3
+ Version: 0.35.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -9,6 +9,33 @@ All notable changes to SQLsaber will be documented here.
9
9
 
10
10
  ---
11
11
 
12
+ ### v0.35.0 - 2025-10-22
13
+
14
+ #### Added
15
+
16
+ - Table and column comment support across all databases
17
+ - Comments are now included in schema introspection to provide richer context to LLM
18
+ - PostgreSQL: Uses `obj_description()` and `col_description()` functions
19
+ - MySQL: Retrieves `table_comment` and `column_comment` from `information_schema`
20
+ - DuckDB: Joins with `duckdb_tables()` and `duckdb_columns()` for comment data
21
+ - SQLite: Returns `None` for comments (SQLite doesn't support native comments)
22
+ - Comments are conditionally included in tool output only when present, avoiding clutter
23
+ - Updated `ColumnInfo` and `SchemaInfo` TypedDicts with optional comment fields
24
+
25
+ #### Fixed
26
+
27
+ - Strip leading/trailing whitespace and new lines from submitted user inputs
28
+
29
+ ### v0.34.0 - 2025-10-17
30
+
31
+ #### Changed
32
+
33
+ - Improved `auth setup` command: Reworked to select the provider first, offer to reset existing credentials per provider, and allow configuring multiple providers in one session without manual cleanup.
34
+ - PostgreSQL schema introspection: Exclude TimescaleDB internal schemas by default and add env-based filtering
35
+ - Default exclusions now include `pg_catalog`, `information_schema`, `_timescaledb_internal`, `_timescaledb_cache`, `_timescaledb_config`, `_timescaledb_catalog`
36
+ - New environment variable `SQLSABER_PG_EXCLUDE_SCHEMAS` allows excluding additional schemas (comma-separated)
37
+ - Applies to both `list_tables` and `introspect_schema` tools
38
+
12
39
  ### v0.33.0 - 2025-10-16
13
40
 
14
41
  #### Changed
@@ -61,7 +88,7 @@ All notable changes to SQLsaber will be documented here.
61
88
 
62
89
  > `sqlsaber` can still be used as a cli in coding agents like Claude Code, Codex, or Amp.
63
90
  >
64
- > Just *ask* the coding agent to invoke `sqlsaber` cli with your question.
91
+ > Just _ask_ the coding agent to invoke `sqlsaber` cli with your question.
65
92
 
66
93
  ### v0.29.1 - 2025-10-05
67
94
 
@@ -356,6 +383,7 @@ All notable changes to SQLsaber will be documented here.
356
383
  #### Added
357
384
 
358
385
  - Table name autocomplete with "@" prefix in interactive mode
386
+
359
387
  - Type "@" followed by table name to get fuzzy matching completions
360
388
  - Supports schema-aware completions (e.g., "@sample" matches "public.sample")
361
389
 
@@ -3,7 +3,7 @@ title: Database Setup
3
3
  description: Configure database connections in SQLsaber
4
4
  ---
5
5
 
6
- import { Aside } from '@astrojs/starlight/components';
6
+ import { Aside } from "@astrojs/starlight/components";
7
7
 
8
8
  SQLsaber supports PostgreSQL, MySQL, SQLite, DuckDB, and CSV data sources. This guide covers all the ways to configure database connections.
9
9
 
@@ -66,7 +66,8 @@ saber -d "./customers.csv" "How many customers are from each state?"
66
66
  ```
67
67
 
68
68
  <Aside type="note">
69
- For CSV files, first row should contain column headers. SQLsaber will infer data types automatically.
69
+ For CSV files, first row should contain column headers. SQLsaber will infer
70
+ data types automatically.
70
71
  </Aside>
71
72
 
72
73
  ### Managing Connections
@@ -86,7 +87,7 @@ saber db set-default my-database
86
87
  ```
87
88
 
88
89
  <Aside type="note">
89
- By default, SQLsaber will set the first added connection as default.
90
+ By default, SQLsaber will set the first added connection as default.
90
91
  </Aside>
91
92
 
92
93
  #### Test Connections
@@ -115,10 +116,13 @@ While SQLsaber ensures, via checks before executing queries, that only read quer
115
116
  If you have a read-replica of your production database, consider using those credentials to setup connection.
116
117
 
117
118
  In addition to the read-only role, this will provide additional layer of security and ensure no data modifying queries are executed.
119
+
118
120
  </Aside>
119
121
 
120
122
  <Aside type="note">
121
- For development setups, this isn't required but is a good practice if you wish to ensure with absolute certainty that no data modifying queries are ever executed.
123
+ For development setups, this isn't required but is a good practice if you wish
124
+ to ensure with absolute certainty that no data modifying queries are ever
125
+ executed.
122
126
  </Aside>
123
127
 
124
128
  #### Password Storage
@@ -129,6 +133,27 @@ SQLsaber stores database passwords securely using your operating system's creden
129
133
 
130
134
  Store SSL certificates in a secure location and use absolute paths when configuring connections.
131
135
 
136
+ ### Schema Introspection Filters (PostgreSQL)
137
+
138
+ By default, SQLsaber excludes system and TimescaleDB internal schemas during schema discovery to keep results focused and fast.
139
+
140
+ Excluded by default:
141
+
142
+ - `pg_catalog`
143
+ - `information_schema`
144
+ - `_timescaledb_internal`
145
+ - `_timescaledb_cache`
146
+ - `_timescaledb_config`
147
+ - `_timescaledb_catalog`
148
+
149
+ You can exclude additional schemas via environment variable before running `saber`:
150
+
151
+ ```bash
152
+ export SQLSABER_PG_EXCLUDE_SCHEMAS="schema1,schema2"
153
+ ```
154
+
155
+ Both the `list_tables` and `introspect_schema` tools respect these filters.
156
+
132
157
  ### Troubleshooting
133
158
 
134
159
  Always test new connections after adding them.
@@ -15,72 +15,50 @@ Before you begin, make sure you have:
15
15
 
16
16
  ### Quick Setup
17
17
 
18
- #### 1. Set Up Authentication
18
+ #### 1. Launch the Onboarding Flow
19
19
 
20
- First, configure your AI provider.
21
-
22
- SQLsaber uses Claude Sonnet 4 as the model by default, but supports models from [multiple providers](/guides/models).
20
+ Run SQLsaber with no arguments to start the guided setup:
23
21
 
24
22
  ```bash
25
- saber auth setup
23
+ saber
26
24
  ```
27
25
 
28
- This will prompt you to:
26
+ On your first run, SQLsaber walks you through two guided steps:
29
27
 
30
- - Choose a provider (We recommend Anthropic)
31
- - Enter your API key
28
+ 1. **Database connection**:
29
+ Choose the database type and provide connection details.
32
30
 
33
- <Aside type="tip">
34
- If you have a Claude Pro or Max subscription, you can use it with SQLsaber. It
35
- is one of the most cost-effective options.
36
- </Aside>
31
+ 2. **Authentication**:
32
+ Sign in to your preferred AI provider and pick a model. We recommend Sonnet 4.5, 4 or GPT-5.
37
33
 
38
- <Aside type="note">
39
- If you choose a provider other than Anthropic, please select the appropriate
40
- model for the provider by running `saber models set`.
41
- </Aside>
34
+ `sqlsaber` will store your API key securely using your OS credential store.
42
35
 
43
- #### 2. Add Your Database
36
+ When both steps succeed you'll see a confirmation screen, and SQLsaber immediately opens an interactive session.
44
37
 
45
- Add a database connection:
46
-
47
- ```bash
48
- saber db add my-database
49
- ```
50
-
51
- This interactive command will ask you for:
38
+ <Aside type="tip">
39
+ If you have a Claude Pro or Max subscription, you can use it with SQLsaber for
40
+ a cost-effective setup.
41
+ </Aside>
52
42
 
53
- - Database type
54
- - PostgreSQL
55
- - MySQL
56
- - SQLite
57
- - DuckDB
58
- - Connection details (host, port, database name, username)
59
- - SSL configuration (if needed)
43
+ <Aside type="tip">
44
+ If you already have a connection string handy, pass it via `saber -d <connection-string>` to skip the onboarding flow.
45
+ </Aside>
60
46
 
61
47
  <Aside type="note">
62
- If you don't have a database handy, you can use a sample `legislators` SQLite database from SQLsaber repository:
48
+ Need a sample database?
49
+
50
+ Download the `legislators` SQLite file, then choose `sqlite` when the onboarding flow prompts for a database location (or add it with `saber db add`):
63
51
 
64
52
  ```bash
65
53
  curl -L -o legislators.db https://github.com/SarthakJariwala/sqlsaber/raw/refs/heads/main/legislators.db
66
-
67
- # Add it to SQLsaber
68
- saber db add legislators --type sqlite --database ./legislators.db
69
54
  ```
70
55
 
71
56
  </Aside>
72
57
 
73
- #### 3. Your First Query
74
-
75
- Now you're ready to ask your first question!
76
-
77
- Start SQLsaber in interactive mode:
78
-
79
- ```bash
80
- saber
81
- ```
58
+ #### 2. Ask Your First Question
82
59
 
83
- Try asking a question:
60
+ Once onboarding completes, you're already in interactive mode. Try asking a
61
+ question:
84
62
 
85
63
  ```
86
64
  > How many VPs have become presidents via election in 20th century?
@@ -3,7 +3,7 @@ title: Models
3
3
  description: Configure and select AI models for SQLsaber
4
4
  ---
5
5
 
6
- import { Aside } from '@astrojs/starlight/components';
6
+ import { Aside } from "@astrojs/starlight/components";
7
7
 
8
8
  SQLsaber supports many LLM models across different providers.
9
9
 
@@ -11,19 +11,19 @@ This guide covers model selection, configuration, and recommendations for the be
11
11
 
12
12
  ### Default Model
13
13
 
14
- SQLsaber uses **Claude Sonnet 4** by default, which provides excellent SQL generation capabilities with good speed and accuracy.
14
+ SQLsaber uses **Claude Sonnet 4.5** by default, which provides excellent SQL generation capabilities with good speed and accuracy.
15
15
 
16
16
  ### Recommended Models
17
17
 
18
18
  While SQLsaber supports a lot of models, we recommend the following for best results:
19
19
 
20
- - Claude Sonnet 4, 3.7, 3.5
21
- - Claude Opus 4, 4.1
22
20
  - GPT-5
21
+ - Claude Sonnet 4.5, 4.0, 3.7
22
+ - Claude Opus 4, 4.1
23
23
  - Gemini 2.5 Pro
24
24
 
25
25
  <Aside type="tip">
26
- Consider using one of the recommended models for best results.
26
+ Consider using one of the recommended models for best results.
27
27
  </Aside>
28
28
 
29
29
  ### Model Management
@@ -43,12 +43,13 @@ saber models set
43
43
  ```
44
44
 
45
45
  This interactive command lets you:
46
+
46
47
  1. Choose from configured providers
47
48
  2. Select a specific model
48
49
  3. Set it as your default
49
50
 
50
51
  <Aside type="tip">
51
- This list is long - type to search and filter the list.
52
+ This list is long - type to search and filter the list.
52
53
  </Aside>
53
54
 
54
55
  #### Reset to Default
@@ -436,3 +436,12 @@ When in interactive mode (`saber` with no arguments), you have access to a few a
436
436
 
437
437
  - **Table names** - Type `@table_name[TAB]` for completions
438
438
  - **Slash commands** - Type `/[TAB]` for command completions
439
+
440
+ ---
441
+
442
+ ### Environment Variables
443
+
444
+ These environment variables adjust runtime behavior:
445
+
446
+ - `SQLSABER_THEME` — Override the configured theme for the session.
447
+ - `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`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.33.0"
3
+ version = "0.35.0"
4
4
  description = "SQLsaber - Open-source agentic SQL assistant"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,7 @@
1
1
  """Shared auth setup logic for onboarding and CLI."""
2
2
 
3
3
  import asyncio
4
+ import os
4
5
 
5
6
  from questionary import Choice
6
7
 
@@ -9,6 +10,7 @@ from sqlsaber.config import providers
9
10
  from sqlsaber.config.api_keys import APIKeyManager
10
11
  from sqlsaber.config.auth import AuthConfigManager, AuthMethod
11
12
  from sqlsaber.config.oauth_flow import AnthropicOAuthFlow
13
+ from sqlsaber.config.oauth_tokens import OAuthTokenManager
12
14
  from sqlsaber.theme.manager import create_console
13
15
 
14
16
  console = create_console()
@@ -102,24 +104,49 @@ async def setup_auth(
102
104
  Returns:
103
105
  Tuple of (success: bool, provider: str | None)
104
106
  """
105
- # Check if auth is already configured
106
- if auth_manager.has_auth_configured():
107
- console.print("[success]✓ Authentication already configured![/success]")
108
- return True, None
107
+ oauth_manager = OAuthTokenManager()
109
108
 
110
- # Select provider
111
109
  provider = await select_provider(prompter, default=default_provider)
112
110
 
113
111
  if provider is None:
114
112
  return False, None
115
113
 
114
+ env_var = api_key_manager.get_env_var_name(provider)
115
+ api_key_in_env = bool(os.getenv(env_var))
116
+ api_key_in_keyring = api_key_manager.has_stored_api_key(provider)
117
+ has_oauth = (
118
+ oauth_manager.has_oauth_token("anthropic")
119
+ if provider == "anthropic" and allow_oauth
120
+ else False
121
+ )
122
+
123
+ if api_key_in_env or api_key_in_keyring or has_oauth:
124
+ parts: list[str] = []
125
+ if api_key_in_keyring:
126
+ parts.append("stored API key")
127
+ if api_key_in_env:
128
+ parts.append(f"{env_var} environment variable")
129
+ if has_oauth:
130
+ parts.append("OAuth token")
131
+ summary = ", ".join(parts)
132
+ console.print(
133
+ f"[info]Existing authentication found for {provider}: {summary}[/info]"
134
+ )
135
+
116
136
  # For Anthropic, offer OAuth or API key
117
137
  if provider == "anthropic" and allow_oauth:
138
+ api_key_label = "API Key"
139
+ if api_key_in_keyring or api_key_in_env:
140
+ api_key_label += " [configured]"
141
+ oauth_label = "Claude Pro/Max (OAuth)"
142
+ if has_oauth:
143
+ oauth_label += " [configured]"
144
+
118
145
  method_choice = await prompter.select(
119
146
  "Authentication method:",
120
147
  choices=[
121
- Choice("API Key", value=AuthMethod.API_KEY),
122
- Choice("Claude Pro/Max (OAuth)", value=AuthMethod.CLAUDE_PRO),
148
+ Choice(api_key_label, value=AuthMethod.API_KEY),
149
+ Choice(oauth_label, value=AuthMethod.CLAUDE_PRO),
123
150
  ],
124
151
  )
125
152
 
@@ -127,6 +154,28 @@ async def setup_auth(
127
154
  return False, None
128
155
 
129
156
  if method_choice == AuthMethod.CLAUDE_PRO:
157
+ if has_oauth:
158
+ reset = await prompter.confirm(
159
+ "Anthropic OAuth is already configured. Reset before continuing?",
160
+ default=False,
161
+ )
162
+ if not reset:
163
+ console.print(
164
+ "[warning]No changes made to Anthropic OAuth credentials.[/warning]"
165
+ )
166
+ return True, None
167
+
168
+ removal_success = oauth_manager.remove_oauth_token("anthropic")
169
+ if not removal_success:
170
+ console.print(
171
+ "[error]Failed to remove existing Anthropic OAuth credentials.[/error]"
172
+ )
173
+ return False, None
174
+
175
+ current_method = auth_manager.get_auth_method()
176
+ if current_method == AuthMethod.CLAUDE_PRO:
177
+ auth_manager.clear_auth_method()
178
+
130
179
  console.print()
131
180
  oauth_success = await configure_oauth_anthropic(
132
181
  auth_manager, run_in_thread=run_oauth_in_thread
@@ -136,12 +185,35 @@ async def setup_auth(
136
185
  "[green]✓ Anthropic OAuth configured successfully![/green]"
137
186
  )
138
187
  return True, provider
139
- else:
140
- console.print("[error]✗ Anthropic OAuth setup failed.[/error]")
141
- return False, None
188
+
189
+ console.print("[error]✗ Anthropic OAuth setup failed.[/error]")
190
+ return False, None
142
191
 
143
192
  # API key flow
144
- env_var = api_key_manager.get_env_var_name(provider)
193
+ if api_key_in_keyring:
194
+ reset_api_key = await prompter.confirm(
195
+ f"{provider.title()} API key is stored in your keyring. Reset before continuing?",
196
+ default=False,
197
+ )
198
+ if not reset_api_key:
199
+ console.print(
200
+ "[warning]No changes made to stored API key credentials.[/warning]"
201
+ )
202
+ return True, None
203
+ if not api_key_manager.delete_api_key(provider):
204
+ console.print(
205
+ "[error]Failed to remove existing API key credentials.[/error]"
206
+ )
207
+ return False, None
208
+ console.print(
209
+ f"[muted]{provider.title()} API key removed from keyring.[/muted]"
210
+ )
211
+ api_key_in_keyring = False
212
+
213
+ if api_key_in_env:
214
+ console.print(
215
+ f"[muted]{env_var} is set in your environment. Update it there if you need a new value.[/muted]"
216
+ )
145
217
 
146
218
  console.print()
147
219
  console.print(f"[dim]To use {provider.title()}, you need an API key.[/dim]")
@@ -167,7 +167,9 @@ def reset():
167
167
  pass
168
168
  except Exception as e:
169
169
  console.print(f"Warning: Could not remove API key: {e}", style="warning")
170
- logger.warning("auth.reset.api_key_remove_failed", provider=provider, error=str(e))
170
+ logger.warning(
171
+ "auth.reset.api_key_remove_failed", provider=provider, error=str(e)
172
+ )
171
173
 
172
174
  # Optionally clear global auth method if removing Anthropic OAuth configuration
173
175
  if provider == "anthropic" and oauth_present:
@@ -406,9 +406,17 @@ class DisplayManager:
406
406
  for table_name, table_info in data.items():
407
407
  self.console.print(f"\n[heading]Table: {table_name}[/heading]")
408
408
 
409
+ table_comment = table_info.get("comment")
410
+ if table_comment:
411
+ self.console.print(f"[muted]Comment: {table_comment}[/muted]")
412
+
409
413
  # Show columns
410
414
  table_columns = table_info.get("columns", {})
411
415
  if table_columns:
416
+ include_column_comments = any(
417
+ col_info.get("comment") for col_info in table_columns.values()
418
+ )
419
+
412
420
  # Create a table for columns
413
421
  columns = [
414
422
  {"name": "Column Name", "style": "column.name"},
@@ -416,6 +424,8 @@ class DisplayManager:
416
424
  {"name": "Nullable", "style": "info"},
417
425
  {"name": "Default", "style": "muted"},
418
426
  ]
427
+ if include_column_comments:
428
+ columns.append({"name": "Comment", "style": "muted"})
419
429
  col_table = self._create_table(columns, title="Columns")
420
430
 
421
431
  for col_name, col_info in table_columns.items():
@@ -425,9 +435,15 @@ class DisplayManager:
425
435
  if col_info.get("default")
426
436
  else ""
427
437
  )
428
- col_table.add_row(
429
- col_name, col_info.get("type", ""), nullable, default
430
- )
438
+ row = [
439
+ col_name,
440
+ col_info.get("type", ""),
441
+ nullable,
442
+ default,
443
+ ]
444
+ if include_column_comments:
445
+ row.append(col_info.get("comment") or "")
446
+ col_table.add_row(*row)
431
447
 
432
448
  self.console.print(col_table)
433
449
 
@@ -20,6 +20,7 @@ from sqlsaber.cli.completers import (
20
20
  )
21
21
  from sqlsaber.cli.display import DisplayManager
22
22
  from sqlsaber.cli.streaming import StreamingQueryHandler
23
+ from sqlsaber.config.logging import get_logger
23
24
  from sqlsaber.database import (
24
25
  CSVConnection,
25
26
  DuckDBConnection,
@@ -30,7 +31,6 @@ from sqlsaber.database import (
30
31
  from sqlsaber.database.schema import SchemaManager
31
32
  from sqlsaber.theme.manager import get_theme_manager
32
33
  from sqlsaber.threads import ThreadStorage
33
- from sqlsaber.config.logging import get_logger
34
34
 
35
35
  if TYPE_CHECKING:
36
36
  from sqlsaber.agents.pydantic_ai_agent import SQLSaberAgent
@@ -309,6 +309,8 @@ class InteractiveSession:
309
309
  style=self.tm.pt_style(),
310
310
  )
311
311
 
312
+ user_query = user_query.strip()
313
+
312
314
  if not user_query:
313
315
  continue
314
316
 
@@ -325,7 +327,7 @@ class InteractiveSession:
325
327
 
326
328
  # Handle memory addition
327
329
  if user_query.strip().startswith("#"):
328
- await self._handle_memory(user_query.strip()[1:].strip())
330
+ await self._handle_memory(user_query[1:].strip())
329
331
  continue
330
332
 
331
333
  # Execute query with cancellation support
@@ -41,6 +41,29 @@ class APIKeyManager:
41
41
  # 3. Prompt user for API key
42
42
  return self._prompt_and_store_key(provider, env_var_name, service_name)
43
43
 
44
+ def has_stored_api_key(self, provider: str) -> bool:
45
+ """Check if an API key is stored for the provider."""
46
+ service_name = self._get_service_name(provider)
47
+ try:
48
+ return keyring.get_password(service_name, provider) is not None
49
+ except Exception:
50
+ return False
51
+
52
+ def delete_api_key(self, provider: str) -> bool:
53
+ """Remove stored API key for the provider."""
54
+ service_name = self._get_service_name(provider)
55
+ try:
56
+ keyring.delete_password(service_name, provider)
57
+ return True
58
+ except keyring.errors.PasswordDeleteError:
59
+ return True
60
+ except Exception as e:
61
+ console.print(
62
+ f"Warning: Could not remove API key: {e}",
63
+ style="warning",
64
+ )
65
+ return False
66
+
44
67
  def get_env_var_name(self, provider: str) -> str:
45
68
  """Get the expected environment variable name for a provider."""
46
69
  # Normalize aliases to canonical provider keys
@@ -81,6 +81,12 @@ class AuthConfigManager:
81
81
  config["auth_method"] = auth_method.value
82
82
  self._save_config(config)
83
83
 
84
+ def clear_auth_method(self) -> None:
85
+ """Clear any configured authentication method."""
86
+ config = self._load_config()
87
+ config["auth_method"] = None
88
+ self._save_config(config)
89
+
84
90
  def has_auth_configured(self) -> bool:
85
91
  """Check if authentication method is configured."""
86
92
  return self.get_auth_method() is not None
@@ -193,4 +193,3 @@ __all__ = [
193
193
  "default_log_dir",
194
194
  "default_log_file",
195
195
  ]
196
-
@@ -24,6 +24,7 @@ class ColumnInfo(TypedDict):
24
24
  max_length: int | None
25
25
  precision: int | None
26
26
  scale: int | None
27
+ comment: str | None
27
28
 
28
29
 
29
30
  class ForeignKeyInfo(TypedDict):
@@ -48,6 +49,7 @@ class SchemaInfo(TypedDict):
48
49
  schema: str
49
50
  name: str
50
51
  type: str
52
+ comment: str | None
51
53
  columns: dict[str, ColumnInfo]
52
54
  primary_keys: list[str]
53
55
  foreign_keys: list[ForeignKeyInfo]