sqlsaber 0.34.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/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
@@ -24,6 +26,7 @@ class ColumnInfo(TypedDict):
24
26
  max_length: int | None
25
27
  precision: int | None
26
28
  scale: int | None
29
+ comment: str | None
27
30
 
28
31
 
29
32
  class ForeignKeyInfo(TypedDict):
@@ -48,6 +51,7 @@ class SchemaInfo(TypedDict):
48
51
  schema: str
49
52
  name: str
50
53
  type: str
54
+ comment: str | None
51
55
  columns: dict[str, ColumnInfo]
52
56
  primary_keys: list[str]
53
57
  foreign_keys: list[ForeignKeyInfo]
@@ -60,6 +64,7 @@ class BaseDatabaseConnection(ABC):
60
64
  def __init__(self, connection_string: str):
61
65
  self.connection_string = connection_string
62
66
  self._pool = None
67
+ self._excluded_schemas: list[str] = []
63
68
 
64
69
  @property
65
70
  @abstractmethod
@@ -93,6 +98,27 @@ class BaseDatabaseConnection(ABC):
93
98
  """
94
99
  pass
95
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
+
96
122
 
97
123
  class BaseSchemaIntrospector(ABC):
98
124
  """Abstract base class for database-specific schema introspection."""
@@ -128,3 +154,36 @@ class BaseSchemaIntrospector(ABC):
128
154
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
129
155
  """Get list of tables with basic information."""
130
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
@@ -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,30 +138,43 @@ 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
- where_conditions = [
135
- "table_schema NOT IN ('information_schema', 'pg_catalog', 'duckdb_catalog')"
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)
142
- where_conditions.append("(table_schema LIKE ? AND table_name LIKE ?)")
153
+ where_conditions.append(
154
+ "(t.table_schema LIKE ? AND t.table_name LIKE ?)"
155
+ )
143
156
  params.extend([schema_pattern, table_name_pattern])
144
157
  else:
145
158
  where_conditions.append(
146
- "(table_name LIKE ? OR table_schema || '.' || table_name LIKE ?)"
159
+ "(t.table_name LIKE ? OR t.table_schema || '.' || t.table_name LIKE ?)"
147
160
  )
148
161
  params.extend([table_pattern, table_pattern])
149
162
 
163
+ if not where_conditions:
164
+ where_conditions.append("1=1")
165
+
150
166
  query = f"""
151
167
  SELECT
152
- table_schema,
153
- table_name,
154
- table_type
155
- FROM information_schema.tables
168
+ t.table_schema,
169
+ t.table_name,
170
+ t.table_type,
171
+ dt.comment AS table_comment
172
+ FROM information_schema.tables t
173
+ LEFT JOIN duckdb_tables() dt
174
+ ON t.table_schema = dt.schema_name
175
+ AND t.table_name = dt.table_name
156
176
  WHERE {" AND ".join(where_conditions)}
157
- ORDER BY table_schema, table_name;
177
+ ORDER BY t.table_schema, t.table_name;
158
178
  """
159
179
 
160
180
  return await self._execute_query(connection, query, tuple(params))
@@ -166,7 +186,7 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
166
186
 
167
187
  table_filters = []
168
188
  for table in tables:
169
- table_filters.append("(table_schema = ? AND table_name = ?)")
189
+ table_filters.append("(c.table_schema = ? AND c.table_name = ?)")
170
190
 
171
191
  params: list[Any] = []
172
192
  for table in tables:
@@ -174,18 +194,23 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
174
194
 
175
195
  query = f"""
176
196
  SELECT
177
- table_schema,
178
- table_name,
179
- column_name,
180
- data_type,
181
- is_nullable,
182
- column_default,
183
- character_maximum_length,
184
- numeric_precision,
185
- numeric_scale
186
- FROM information_schema.columns
197
+ c.table_schema,
198
+ c.table_name,
199
+ c.column_name,
200
+ c.data_type,
201
+ c.is_nullable,
202
+ c.column_default,
203
+ c.character_maximum_length,
204
+ c.numeric_precision,
205
+ c.numeric_scale,
206
+ dc.comment AS column_comment
207
+ FROM information_schema.columns c
208
+ LEFT JOIN duckdb_columns() dc
209
+ ON c.table_schema = dc.schema_name
210
+ AND c.table_name = dc.table_name
211
+ AND c.column_name = dc.column_name
187
212
  WHERE {" OR ".join(table_filters)}
188
- ORDER BY table_schema, table_name, ordinal_position;
213
+ ORDER BY c.table_schema, c.table_name, c.ordinal_position;
189
214
  """
190
215
 
191
216
  return await self._execute_query(connection, query, tuple(params))
@@ -305,14 +330,27 @@ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
305
330
 
306
331
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
307
332
  """Get list of tables with basic information for DuckDB."""
308
- query = """
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"""
309
343
  SELECT
310
- table_schema,
311
- table_name,
312
- table_type
313
- FROM information_schema.tables
314
- WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'duckdb_catalog')
315
- ORDER BY table_schema, table_name;
344
+ t.table_schema,
345
+ t.table_name,
346
+ t.table_type,
347
+ dt.comment AS table_comment
348
+ FROM information_schema.tables t
349
+ LEFT JOIN duckdb_tables() dt
350
+ ON t.table_schema = dt.schema_name
351
+ AND t.table_name = dt.table_name
352
+ {where_clause}
353
+ ORDER BY t.table_schema, t.table_name;
316
354
  """
317
355
 
318
- return await self._execute_query(connection, query)
356
+ return await self._execute_query(connection, query, tuple(params))
@@ -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
- where_conditions = [
182
- "table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')"
183
- ]
184
- params = []
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,12 +208,16 @@ 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
203
217
  table_schema,
204
218
  table_name,
205
- table_type
219
+ table_type,
220
+ table_comment
206
221
  FROM information_schema.tables
207
222
  WHERE {" AND ".join(where_conditions)}
208
223
  ORDER BY table_schema, table_name;
@@ -230,7 +245,8 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
230
245
  c.column_default,
231
246
  c.character_maximum_length,
232
247
  c.numeric_precision,
233
- c.numeric_scale
248
+ c.numeric_scale,
249
+ c.column_comment
234
250
  FROM information_schema.columns c
235
251
  WHERE (c.table_schema, c.table_name) IN ({placeholders})
236
252
  ORDER BY c.table_schema, c.table_name, c.ordinal_position;
@@ -327,16 +343,26 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
327
343
  async with pool.acquire() as conn:
328
344
  async with conn.cursor() as cursor:
329
345
  # Get tables without row counts for better performance
330
- tables_query = """
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"""
331
356
  SELECT
332
357
  t.table_schema,
333
358
  t.table_name,
334
- t.table_type
359
+ t.table_type,
360
+ t.table_comment
335
361
  FROM information_schema.tables t
336
- WHERE t.table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
362
+ {where_clause}
337
363
  ORDER BY t.table_schema, t.table_name;
338
364
  """
339
- await cursor.execute(tables_query)
365
+ await cursor.execute(tables_query, params if params else None)
340
366
  rows = await cursor.fetchall()
341
367
 
342
368
  # Convert rows to dictionaries
@@ -345,6 +371,7 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
345
371
  "table_schema": row["table_schema"],
346
372
  "table_name": row["table_name"],
347
373
  "table_type": row["table_type"],
374
+ "table_comment": row["table_comment"],
348
375
  }
349
376
  for row in rows
350
377
  ]
@@ -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
- import os
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
- extra = os.getenv("SQLSABER_PG_EXCLUDE_SCHEMAS", "")
159
- if extra:
160
- for item in extra.split(","):
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})")
@@ -226,7 +218,8 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
226
218
  SELECT
227
219
  table_schema,
228
220
  table_name,
229
- table_type
221
+ table_type,
222
+ obj_description(('"' || table_schema || '"."' || table_name || '"')::regclass, 'pg_class') AS table_comment
230
223
  FROM information_schema.tables
231
224
  WHERE {" AND ".join(where_conditions)}
232
225
  ORDER BY table_schema, table_name;
@@ -252,7 +245,8 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
252
245
  c.column_default,
253
246
  c.character_maximum_length,
254
247
  c.numeric_precision,
255
- c.numeric_scale
248
+ c.numeric_scale,
249
+ col_description(('"' || c.table_schema || '"."' || c.table_name || '"')::regclass::oid, c.ordinal_position::INT) AS column_comment
256
250
  FROM information_schema.columns c
257
251
  WHERE (c.table_schema, c.table_name) IN (VALUES {values_clause})
258
252
  ORDER BY c.table_schema, c.table_name, c.ordinal_position;
@@ -352,7 +346,7 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
352
346
  pool = await connection.get_pool()
353
347
  async with pool.acquire() as conn:
354
348
  # Exclude system schemas (and TimescaleDB internals) for performance
355
- excluded = self._get_excluded_schemas()
349
+ excluded = self._get_excluded_schemas(connection)
356
350
  params: list[Any] = []
357
351
  if excluded:
358
352
  placeholders = ", ".join(f"${i + 1}" for i in range(len(excluded)))
@@ -367,7 +361,8 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
367
361
  SELECT
368
362
  table_schema,
369
363
  table_name,
370
- table_type
364
+ table_type,
365
+ obj_description(('"' || table_schema || '"."' || table_name || '"')::regclass, 'pg_class') AS table_comment
371
366
  FROM information_schema.tables
372
367
  WHERE {where_clause}
373
368
  ORDER BY table_schema, table_name;
@@ -380,6 +375,7 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
380
375
  "table_schema": table["table_schema"],
381
376
  "table_name": table["table_name"],
382
377
  "table_type": table["table_type"],
378
+ "table_comment": table["table_comment"],
383
379
  }
384
380
  for table in tables
385
381
  ]
@@ -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(name=db_name, connection_string=spec)
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(name=path.stem, connection_string=f"csv:///{path}")
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(name=path.stem, connection_string=f"sqlite:///{path}")
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(name=path.stem, connection_string=f"duckdb:///{path}")
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, connection_string=db_cfg.to_connection_string()
107
+ name=db_cfg.name,
108
+ connection_string=db_cfg.to_connection_string(),
109
+ excluded_schemas=list(db_cfg.exclude_schemas),
100
110
  )
@@ -72,6 +72,7 @@ class SchemaManager:
72
72
  "schema": schema_name,
73
73
  "name": table_name,
74
74
  "type": table["table_type"],
75
+ "comment": table["table_comment"],
75
76
  "columns": {},
76
77
  "primary_keys": [],
77
78
  "foreign_keys": [],
@@ -85,6 +86,7 @@ class SchemaManager:
85
86
  for col in columns:
86
87
  full_name = f"{col['table_schema']}.{col['table_name']}"
87
88
  if full_name in schema_info:
89
+ # Handle different row types (dict vs Row objects)
88
90
  column_info: ColumnInfo = {
89
91
  "data_type": col["data_type"],
90
92
  "nullable": col.get("is_nullable", "YES") == "YES",
@@ -92,6 +94,7 @@ class SchemaManager:
92
94
  "max_length": col.get("character_maximum_length"),
93
95
  "precision": col.get("numeric_precision"),
94
96
  "scale": col.get("numeric_scale"),
97
+ "comment": col.get("column_comment"),
95
98
  }
96
99
  # Add type field for display compatibility
97
100
  column_info["type"] = col["data_type"]
@@ -93,7 +93,10 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
93
93
  async def get_tables_info(
94
94
  self, connection, table_pattern: str | None = None
95
95
  ) -> dict[str, Any]:
96
- """Get tables information for SQLite."""
96
+ """Get tables information for SQLite.
97
+
98
+ Note: SQLite does not support native table comments, so table_comment is always None.
99
+ """
97
100
  where_conditions = ["type IN ('table', 'view')", "name NOT LIKE 'sqlite_%'"]
98
101
  params = ()
99
102
 
@@ -105,7 +108,8 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
105
108
  SELECT
106
109
  'main' as table_schema,
107
110
  name as table_name,
108
- type as table_type
111
+ type as table_type,
112
+ NULL as table_comment
109
113
  FROM sqlite_master
110
114
  WHERE {" AND ".join(where_conditions)}
111
115
  ORDER BY name;
@@ -114,7 +118,10 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
114
118
  return await self._execute_query(connection, query, params)
115
119
 
116
120
  async def get_columns_info(self, connection, tables: list) -> list:
117
- """Get columns information for SQLite."""
121
+ """Get columns information for SQLite.
122
+
123
+ Note: SQLite does not support native column comments, so column_comment is always None.
124
+ """
118
125
  if not tables:
119
126
  return []
120
127
 
@@ -138,6 +145,7 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
138
145
  "character_maximum_length": None,
139
146
  "numeric_precision": None,
140
147
  "numeric_scale": None,
148
+ "column_comment": None,
141
149
  }
142
150
  )
143
151
 
@@ -237,13 +245,17 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
237
245
  return indexes
238
246
 
239
247
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
240
- """Get list of tables with basic information for SQLite."""
248
+ """Get list of tables with basic information for SQLite.
249
+
250
+ Note: SQLite does not support native table comments, so table_comment is always None.
251
+ """
241
252
  # Get table names without row counts for better performance
242
253
  tables_query = """
243
254
  SELECT
244
255
  'main' as table_schema,
245
256
  name as table_name,
246
- type as table_type
257
+ type as table_type,
258
+ NULL as table_comment
247
259
  FROM sqlite_master
248
260
  WHERE type IN ('table', 'view')
249
261
  AND name NOT LIKE 'sqlite_%'
@@ -258,6 +270,7 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
258
270
  "table_schema": table["table_schema"],
259
271
  "table_name": table["table_name"],
260
272
  "table_type": table["table_type"],
273
+ "table_comment": table["table_comment"],
261
274
  }
262
275
  for table in tables
263
276
  ]
@@ -95,27 +95,38 @@ class IntrospectSchemaTool(SQLTool):
95
95
  # Format the schema information
96
96
  formatted_info = {}
97
97
  for table_name, table_info in schema_info.items():
98
- formatted_info[table_name] = {
99
- "columns": {
100
- col_name: {
101
- "type": col_info["data_type"],
102
- "nullable": col_info["nullable"],
103
- "default": col_info["default"],
104
- }
105
- for col_name, col_info in table_info["columns"].items()
106
- },
107
- "primary_keys": table_info["primary_keys"],
108
- "foreign_keys": [
109
- f"{fk['column']} -> {fk['references']['table']}.{fk['references']['column']}"
110
- for fk in table_info["foreign_keys"]
111
- ],
112
- "indexes": [
113
- f"{idx['name']} ({', '.join(idx['columns'])})"
114
- + (" UNIQUE" if idx["unique"] else "")
115
- + (f" [{idx['type']}]" if idx["type"] else "")
116
- for idx in table_info["indexes"]
117
- ],
118
- }
98
+ table_data = {}
99
+
100
+ # Add table comment if present
101
+ if table_info.get("comment"):
102
+ table_data["comment"] = table_info["comment"]
103
+
104
+ # Add columns with comments if present
105
+ table_data["columns"] = {}
106
+ for col_name, col_info in table_info["columns"].items():
107
+ column_data = {
108
+ "type": col_info["data_type"],
109
+ "nullable": col_info["nullable"],
110
+ "default": col_info["default"],
111
+ }
112
+ if col_info.get("comment"):
113
+ column_data["comment"] = col_info["comment"]
114
+ table_data["columns"][col_name] = column_data
115
+
116
+ # Add other schema information
117
+ table_data["primary_keys"] = table_info["primary_keys"]
118
+ table_data["foreign_keys"] = [
119
+ f"{fk['column']} -> {fk['references']['table']}.{fk['references']['column']}"
120
+ for fk in table_info["foreign_keys"]
121
+ ]
122
+ table_data["indexes"] = [
123
+ f"{idx['name']} ({', '.join(idx['columns'])})"
124
+ + (" UNIQUE" if idx["unique"] else "")
125
+ + (f" [{idx['type']}]" if idx["type"] else "")
126
+ for idx in table_info["indexes"]
127
+ ]
128
+
129
+ formatted_info[table_name] = table_data
119
130
 
120
131
  return json.dumps(formatted_info)
121
132
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.34.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