thoth-dbmanager 0.5.2__tar.gz → 0.5.8__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.
Files changed (46) hide show
  1. thoth_dbmanager-0.5.8/LICENSE.md +21 -0
  2. {thoth_dbmanager-0.5.2/thoth_dbmanager.egg-info → thoth_dbmanager-0.5.8}/PKG-INFO +2 -7
  3. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/pyproject.toml +1 -6
  4. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/__init__.py +1 -1
  5. thoth_dbmanager-0.5.8/thoth_dbmanager/adapters/__init__.py +39 -0
  6. thoth_dbmanager-0.5.8/thoth_dbmanager/adapters/mariadb.py +388 -0
  7. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/sqlserver.py +17 -6
  8. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/factory.py +3 -0
  9. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/interfaces.py +1 -82
  10. thoth_dbmanager-0.5.8/thoth_dbmanager/plugins/__init__.py +42 -0
  11. thoth_dbmanager-0.5.8/thoth_dbmanager/plugins/mariadb.py +216 -0
  12. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8/thoth_dbmanager.egg-info}/PKG-INFO +2 -7
  13. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/SOURCES.txt +1 -0
  14. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/requires.txt +0 -7
  15. thoth_dbmanager-0.5.2/thoth_dbmanager/adapters/__init__.py +0 -15
  16. thoth_dbmanager-0.5.2/thoth_dbmanager/adapters/mariadb.py +0 -165
  17. thoth_dbmanager-0.5.2/thoth_dbmanager/plugins/__init__.py +0 -17
  18. thoth_dbmanager-0.5.2/thoth_dbmanager/plugins/mariadb.py +0 -436
  19. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/LICENSE +0 -0
  20. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/MANIFEST.in +0 -0
  21. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/README.md +0 -0
  22. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/setup.cfg +0 -0
  23. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/tests/test_lsh_interactive.py +0 -0
  24. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/tests/test_thoth_db_manager_base.py +0 -0
  25. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/ThothDbManager.py +0 -0
  26. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/postgresql.py +0 -0
  27. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/sqlite.py +0 -0
  28. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/__init__.py +0 -0
  29. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/registry.py +0 -0
  30. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/documents.py +0 -0
  31. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/dynamic_imports.py +0 -0
  32. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/__init__.py +0 -0
  33. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/multi_db_generator.py +0 -0
  34. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/preprocess_values.py +0 -0
  35. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/schema.py +0 -0
  36. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/search.py +0 -0
  37. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/__init__.py +0 -0
  38. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/core.py +0 -0
  39. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/factory.py +0 -0
  40. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/manager.py +0 -0
  41. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/storage.py +0 -0
  42. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/postgresql.py +0 -0
  43. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/sqlite.py +0 -0
  44. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/sqlserver.py +0 -0
  45. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/dependency_links.txt +0 -0
  46. {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marco Pancotti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoth_dbmanager
3
- Version: 0.5.2
3
+ Version: 0.5.8
4
4
  Summary: A Python library for managing SQL databases with support for multiple database types, LSH-based similarity search, and a modern plugin architecture.
5
5
  Author-email: Marco Pancotti <mp@tylconsulting.it>
6
6
  Project-URL: Homepage, https://github.com/mptyl/thoth_dbmanager
@@ -22,11 +22,11 @@ Classifier: Development Status :: 4 - Beta
22
22
  Requires-Python: >=3.9
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
+ License-File: LICENSE.md
25
26
  Requires-Dist: datasketch>=1.5.0
26
27
  Requires-Dist: tqdm>=4.60.0
27
28
  Requires-Dist: SQLAlchemy>=1.4.0
28
29
  Requires-Dist: pydantic>=2.0.0
29
- Requires-Dist: pandas>=1.3.0
30
30
  Requires-Dist: requests>=2.25.0
31
31
  Provides-Extra: postgresql
32
32
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgresql"
@@ -35,15 +35,10 @@ Requires-Dist: mariadb>=1.1.0; extra == "mariadb"
35
35
  Provides-Extra: sqlserver
36
36
  Requires-Dist: pyodbc>=4.0.0; extra == "sqlserver"
37
37
  Provides-Extra: sqlite
38
- Provides-Extra: embeddings
39
- Requires-Dist: sentence-transformers>=2.0.0; extra == "embeddings"
40
- Requires-Dist: numpy>=1.21.0; extra == "embeddings"
41
38
  Provides-Extra: all
42
39
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
43
40
  Requires-Dist: mariadb>=1.1.0; extra == "all"
44
41
  Requires-Dist: pyodbc>=4.0.0; extra == "all"
45
- Requires-Dist: sentence-transformers>=2.0.0; extra == "all"
46
- Requires-Dist: numpy>=1.21.0; extra == "all"
47
42
  Provides-Extra: dev
48
43
  Requires-Dist: pytest>=7.0.0; extra == "dev"
49
44
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "thoth_dbmanager"
7
- version = "0.5.2"
7
+ version = "0.5.8"
8
8
  authors = [
9
9
  { name="Marco Pancotti", email="mp@tylconsulting.it" },
10
10
  ]
@@ -31,7 +31,6 @@ dependencies = [
31
31
  "tqdm>=4.60.0",
32
32
  "SQLAlchemy>=1.4.0",
33
33
  "pydantic>=2.0.0",
34
- "pandas>=1.3.0",
35
34
  "requests>=2.25.0",
36
35
  ]
37
36
  [project.optional-dependencies]
@@ -39,15 +38,11 @@ postgresql = ["psycopg2-binary>=2.9.0"]
39
38
  mariadb = ["mariadb>=1.1.0"]
40
39
  sqlserver = ["pyodbc>=4.0.0"]
41
40
  sqlite = []
42
- embeddings = ["sentence-transformers>=2.0.0", "numpy>=1.21.0"]
43
-
44
41
  # Convenience groups
45
42
  all = [
46
43
  "psycopg2-binary>=2.9.0",
47
44
  "mariadb>=1.1.0",
48
45
  "pyodbc>=4.0.0",
49
- "sentence-transformers>=2.0.0",
50
- "numpy>=1.21.0",
51
46
  ]
52
47
 
53
48
  # Development dependencies
@@ -72,4 +72,4 @@ __all__ = [
72
72
  "DatabaseImportError",
73
73
  ]
74
74
 
75
- __version__ = "0.5.0"
75
+ __version__ = "0.5.7"
@@ -0,0 +1,39 @@
1
+ """
2
+ Database adapters for Thoth SQL Database Manager.
3
+ """
4
+
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # Always available adapter (SQLite is built into Python)
10
+ from .sqlite import SQLiteAdapter
11
+
12
+ __all__ = [
13
+ "SQLiteAdapter",
14
+ ]
15
+
16
+ # Optional adapters - only import if dependencies are available
17
+ try:
18
+ import psycopg2
19
+ from .postgresql import PostgreSQLAdapter
20
+ __all__.append("PostgreSQLAdapter")
21
+ except ImportError:
22
+ logger.debug("psycopg2 not installed, PostgreSQLAdapter not available")
23
+ PostgreSQLAdapter = None
24
+
25
+ try:
26
+ import mariadb
27
+ from .mariadb import MariaDBAdapter
28
+ __all__.append("MariaDBAdapter")
29
+ except ImportError:
30
+ logger.debug("MariaDB connector not installed, MariaDBAdapter not available")
31
+ MariaDBAdapter = None
32
+
33
+ try:
34
+ import pyodbc
35
+ from .sqlserver import SQLServerAdapter
36
+ __all__.append("SQLServerAdapter")
37
+ except ImportError:
38
+ logger.debug("pyodbc not installed, SQLServerAdapter not available")
39
+ SQLServerAdapter = None
@@ -0,0 +1,388 @@
1
+ # Copyright (c) 2025 Marco Pancotti
2
+ # This file is part of Thoth and is released under the MIT License.
3
+ # See the LICENSE.md file in the project root for full license information.
4
+
5
+ """
6
+ MariaDB adapter implementation.
7
+ """
8
+ import logging
9
+ from typing import Any, Dict, List, Optional, Union
10
+ import mariadb
11
+ from sqlalchemy import create_engine, text, inspect
12
+ from sqlalchemy.exc import SQLAlchemyError
13
+
14
+ from ..core.interfaces import DbAdapter
15
+ from ..documents import (
16
+ TableDocument,
17
+ ColumnDocument,
18
+ SchemaDocument,
19
+ ForeignKeyDocument,
20
+ IndexDocument
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class MariaDBAdapter(DbAdapter):
27
+ """
28
+ MariaDB database adapter implementation.
29
+ """
30
+
31
+ def __init__(self, connection_params: Dict[str, Any]):
32
+ super().__init__(connection_params)
33
+ self.engine = None
34
+ self.raw_connection = None
35
+ self.host = connection_params.get('host', 'localhost')
36
+ self.port = connection_params.get('port', 3307)
37
+ self.database = connection_params.get('database')
38
+ self.user = connection_params.get('user')
39
+ self.password = connection_params.get('password')
40
+
41
+ def connect(self) -> None:
42
+ """Establish MariaDB connection"""
43
+ try:
44
+ # Create SQLAlchemy engine
45
+ connection_string = self._build_connection_string()
46
+ self.engine = create_engine(connection_string, echo=False)
47
+
48
+ # Test connection
49
+ with self.engine.connect() as conn:
50
+ conn.execute(text("SELECT 1"))
51
+
52
+ # Also create raw mariadb connection for specific operations
53
+ self.raw_connection = mariadb.connect(
54
+ host=self.host,
55
+ port=self.port,
56
+ database=self.database,
57
+ user=self.user,
58
+ password=self.password
59
+ )
60
+
61
+ self._initialized = True
62
+ logger.info("MariaDB connection established successfully")
63
+
64
+ except Exception as e:
65
+ logger.error(f"Failed to connect to MariaDB: {e}")
66
+ raise
67
+
68
+ def disconnect(self) -> None:
69
+ """Close MariaDB connection"""
70
+ try:
71
+ if self.engine:
72
+ self.engine.dispose()
73
+ self.engine = None
74
+
75
+ if self.raw_connection:
76
+ self.raw_connection.close()
77
+ self.raw_connection = None
78
+
79
+ self._initialized = False
80
+ logger.info("MariaDB connection closed")
81
+
82
+ except Exception as e:
83
+ logger.error(f"Error closing MariaDB connection: {e}")
84
+
85
+ def _build_connection_string(self) -> str:
86
+ """Build SQLAlchemy connection string for MariaDB"""
87
+ if not all([self.database, self.user, self.password]):
88
+ raise ValueError("Missing required connection parameters: database, user, password")
89
+
90
+ # MariaDB uses mysql+pymysql or mariadb+mariadbconnector dialect
91
+ return f"mariadb+mariadbconnector://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
92
+
93
+ def execute_query(self, query: str, params: Optional[Dict] = None, fetch: Union[str, int] = "all", timeout: int = 60) -> Any:
94
+ """Execute SQL query"""
95
+ if not self.engine:
96
+ raise RuntimeError("Not connected to database")
97
+
98
+ try:
99
+ with self.engine.connect() as conn:
100
+ # MariaDB doesn't have direct query timeout in the same way
101
+ # but we can set connection timeout
102
+ conn.execute(text(f"SET SESSION max_statement_time = {timeout}"))
103
+
104
+ # Execute query
105
+ if params:
106
+ result = conn.execute(text(query), params)
107
+ else:
108
+ result = conn.execute(text(query))
109
+
110
+ # Handle different fetch modes
111
+ if query.strip().upper().startswith(('SELECT', 'WITH', 'SHOW', 'DESCRIBE')):
112
+ if fetch == "all":
113
+ return [dict(row._mapping) for row in result]
114
+ elif fetch == "one":
115
+ row = result.first()
116
+ return dict(row._mapping) if row else None
117
+ elif isinstance(fetch, int):
118
+ rows = result.fetchmany(fetch)
119
+ return [dict(row._mapping) for row in rows]
120
+ else:
121
+ # For INSERT, UPDATE, DELETE
122
+ conn.commit()
123
+ return result.rowcount
124
+
125
+ except SQLAlchemyError as e:
126
+ logger.error(f"MariaDB query execution failed: {e}")
127
+ raise
128
+
129
+ def get_tables_as_documents(self) -> List[TableDocument]:
130
+ """Return tables as document objects"""
131
+ if not self.engine:
132
+ raise RuntimeError("Not connected to database")
133
+
134
+ try:
135
+ inspector = inspect(self.engine)
136
+ tables = []
137
+
138
+ for table_name in inspector.get_table_names():
139
+ # Get row count
140
+ count_result = self.execute_query(f"SELECT COUNT(*) as count FROM {table_name}", fetch="one")
141
+ row_count = count_result.get('count', 0) if count_result else 0
142
+
143
+ # Get column count
144
+ columns = inspector.get_columns(table_name)
145
+
146
+ # Get table comment (if available)
147
+ table_comment = ""
148
+ try:
149
+ comment_result = self.execute_query(
150
+ f"SELECT table_comment FROM information_schema.tables WHERE table_name = '{table_name}'",
151
+ fetch="one"
152
+ )
153
+ table_comment = comment_result.get('table_comment', '') if comment_result else ''
154
+ except:
155
+ pass
156
+
157
+ tables.append(TableDocument(
158
+ table_name=table_name,
159
+ table_type="TABLE",
160
+ row_count=row_count,
161
+ column_count=len(columns),
162
+ description=table_comment
163
+ ))
164
+
165
+ return tables
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error getting tables as documents: {e}")
169
+ raise
170
+
171
+ def get_columns_as_documents(self, table_name: str) -> List[ColumnDocument]:
172
+ """Return columns as document objects"""
173
+ if not self.engine:
174
+ raise RuntimeError("Not connected to database")
175
+
176
+ try:
177
+ inspector = inspect(self.engine)
178
+ columns = []
179
+
180
+ for col in inspector.get_columns(table_name):
181
+ columns.append(ColumnDocument(
182
+ table_name=table_name,
183
+ column_name=col['name'],
184
+ data_type=str(col['type']),
185
+ is_nullable=col.get('nullable', True),
186
+ column_default=col.get('default'),
187
+ is_pk=col.get('primary_key', False),
188
+ column_comment=col.get('comment', '')
189
+ ))
190
+
191
+ # Mark primary keys
192
+ pk_constraint = inspector.get_pk_constraint(table_name)
193
+ if pk_constraint and pk_constraint.get('constrained_columns'):
194
+ pk_columns = pk_constraint['constrained_columns']
195
+ for col in columns:
196
+ if col.column_name in pk_columns:
197
+ col.is_pk = True
198
+
199
+ return columns
200
+
201
+ except Exception as e:
202
+ logger.error(f"Error getting columns as documents: {e}")
203
+ raise
204
+
205
+ def get_foreign_keys_as_documents(self) -> List[ForeignKeyDocument]:
206
+ """Return foreign keys as document objects"""
207
+ if not self.engine:
208
+ raise RuntimeError("Not connected to database")
209
+
210
+ try:
211
+ inspector = inspect(self.engine)
212
+ foreign_keys = []
213
+
214
+ for table_name in inspector.get_table_names():
215
+ for fk in inspector.get_foreign_keys(table_name):
216
+ # Each foreign key can have multiple column pairs
217
+ for i, const_col in enumerate(fk['constrained_columns']):
218
+ foreign_keys.append(ForeignKeyDocument(
219
+ constraint_name=fk['name'],
220
+ table_name=table_name,
221
+ column_name=const_col,
222
+ foreign_table_name=fk['referred_table'],
223
+ foreign_column_name=fk['referred_columns'][i] if i < len(fk['referred_columns']) else None
224
+ ))
225
+
226
+ return foreign_keys
227
+
228
+ except Exception as e:
229
+ logger.error(f"Error getting foreign keys as documents: {e}")
230
+ raise
231
+
232
+ def get_schemas_as_documents(self) -> List[SchemaDocument]:
233
+ """Return schemas as document objects"""
234
+ # MariaDB uses database as schema concept
235
+ if not self.engine:
236
+ raise RuntimeError("Not connected to database")
237
+
238
+ try:
239
+ # Get current database as schema
240
+ result = self.execute_query("SELECT DATABASE() as db_name", fetch="one")
241
+ current_db = result.get('db_name') if result else self.database
242
+
243
+ # Get table count for current database
244
+ tables = self.get_tables_as_documents()
245
+
246
+ return [SchemaDocument(
247
+ catalog_name=current_db,
248
+ schema_name=current_db,
249
+ schema_owner=self.user,
250
+ table_count=len(tables)
251
+ )]
252
+
253
+ except Exception as e:
254
+ logger.error(f"Error getting schemas as documents: {e}")
255
+ raise
256
+
257
+ def get_indexes_as_documents(self, table_name: Optional[str] = None) -> List[IndexDocument]:
258
+ """Return indexes as document objects"""
259
+ if not self.engine:
260
+ raise RuntimeError("Not connected to database")
261
+
262
+ try:
263
+ inspector = inspect(self.engine)
264
+ indexes = []
265
+
266
+ # Get tables to process
267
+ tables = [table_name] if table_name else inspector.get_table_names()
268
+
269
+ for tbl in tables:
270
+ for idx in inspector.get_indexes(tbl):
271
+ indexes.append(IndexDocument(
272
+ table_name=tbl,
273
+ index_name=idx['name'],
274
+ column_names=idx['column_names'],
275
+ is_unique=idx.get('unique', False),
276
+ index_type='BTREE' # MariaDB default
277
+ ))
278
+
279
+ return indexes
280
+
281
+ except Exception as e:
282
+ logger.error(f"Error getting indexes as documents: {e}")
283
+ raise
284
+
285
+ def get_unique_values(self) -> Dict[str, Dict[str, List[str]]]:
286
+ """
287
+ Get unique values from the database.
288
+
289
+ Returns:
290
+ Dict[str, Dict[str, List[str]]]: Dictionary where:
291
+ - outer key is table name
292
+ - inner key is column name
293
+ - value is list of unique values
294
+ """
295
+ if not self.engine:
296
+ raise RuntimeError("Not connected to database")
297
+
298
+ try:
299
+ inspector = inspect(self.engine)
300
+ unique_values = {}
301
+
302
+ for table_name in inspector.get_table_names():
303
+ unique_values[table_name] = {}
304
+
305
+ for col in inspector.get_columns(table_name):
306
+ col_name = col['name']
307
+ # Only get unique values for reasonable data types
308
+ col_type = str(col['type']).upper()
309
+
310
+ if any(t in col_type for t in ['VARCHAR', 'CHAR', 'TEXT', 'INT', 'ENUM']):
311
+ try:
312
+ # Limit to first 100 unique values
313
+ query = f"SELECT DISTINCT `{col_name}` FROM `{table_name}` LIMIT 100"
314
+ result = self.execute_query(query)
315
+
316
+ values = []
317
+ for row in result:
318
+ val = row.get(col_name)
319
+ if val is not None:
320
+ values.append(str(val))
321
+
322
+ if values:
323
+ unique_values[table_name][col_name] = values
324
+
325
+ except Exception as e:
326
+ logger.debug(f"Could not get unique values for {table_name}.{col_name}: {e}")
327
+ continue
328
+
329
+ return unique_values
330
+
331
+ except Exception as e:
332
+ logger.error(f"Error getting unique values: {e}")
333
+ raise
334
+
335
+ def get_example_data(self, table_name: str, number_of_rows: int = 30) -> Dict[str, List[Any]]:
336
+ """
337
+ Get example data (most frequent values) for each column in a table.
338
+
339
+ Args:
340
+ table_name (str): The name of the table.
341
+ number_of_rows (int, optional): Maximum number of example values to return per column. Defaults to 30.
342
+
343
+ Returns:
344
+ Dict[str, List[Any]]: A dictionary mapping column names to lists of example values.
345
+ """
346
+ if not self.engine:
347
+ raise RuntimeError("Not connected to database")
348
+
349
+ try:
350
+ inspector = inspect(self.engine)
351
+ columns = inspector.get_columns(table_name)
352
+
353
+ example_data = {}
354
+
355
+ for col in columns:
356
+ col_name = col['name']
357
+ col_type = str(col['type']).upper()
358
+
359
+ # Skip blob/binary columns
360
+ if any(t in col_type for t in ['BLOB', 'BINARY', 'IMAGE']):
361
+ example_data[col_name] = []
362
+ continue
363
+
364
+ try:
365
+ # Get most frequent values
366
+ query = f"""
367
+ SELECT `{col_name}`, COUNT(*) as freq
368
+ FROM `{table_name}`
369
+ WHERE `{col_name}` IS NOT NULL
370
+ GROUP BY `{col_name}`
371
+ ORDER BY freq DESC
372
+ LIMIT {number_of_rows}
373
+ """
374
+
375
+ result = self.execute_query(query)
376
+ values = [row[col_name] for row in result]
377
+
378
+ example_data[col_name] = values
379
+
380
+ except Exception as e:
381
+ logger.debug(f"Could not get example data for {table_name}.{col_name}: {e}")
382
+ example_data[col_name] = []
383
+
384
+ return example_data
385
+
386
+ except Exception as e:
387
+ logger.error(f"Error getting example data: {e}")
388
+ raise
@@ -23,6 +23,7 @@ class SQLServerAdapter(DbAdapter):
23
23
  self.database = connection_params.get('database')
24
24
  self.user = connection_params.get('user')
25
25
  self.password = connection_params.get('password')
26
+ self.schema = connection_params.get('schema', 'dbo') # Default to 'dbo' for SQL Server
26
27
  self.driver = connection_params.get('driver', 'ODBC Driver 17 for SQL Server')
27
28
 
28
29
  def connect(self) -> None:
@@ -142,10 +143,11 @@ class SQLServerAdapter(DbAdapter):
142
143
 
143
144
  def get_tables(self) -> List[str]:
144
145
  """Get list of tables in the database."""
145
- query = """
146
+ query = f"""
146
147
  SELECT TABLE_NAME as name
147
148
  FROM INFORMATION_SCHEMA.TABLES
148
149
  WHERE TABLE_TYPE = 'BASE TABLE'
150
+ AND TABLE_SCHEMA = '{self.schema}'
149
151
  ORDER BY TABLE_NAME
150
152
  """
151
153
  result = self.execute_query(query)
@@ -159,15 +161,17 @@ class SQLServerAdapter(DbAdapter):
159
161
  DATA_TYPE as type,
160
162
  IS_NULLABLE as nullable,
161
163
  COLUMN_DEFAULT as default_value,
162
- CASE WHEN COLUMNPROPERTY(OBJECT_ID(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 1 ELSE 0 END as is_identity,
164
+ CASE WHEN COLUMNPROPERTY(OBJECT_ID('{self.schema}.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 THEN 1 ELSE 0 END as is_identity,
163
165
  CASE WHEN EXISTS (
164
166
  SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
165
167
  WHERE TABLE_NAME = '{table_name}'
168
+ AND TABLE_SCHEMA = '{self.schema}'
166
169
  AND COLUMN_NAME = c.COLUMN_NAME
167
170
  AND CONSTRAINT_NAME LIKE 'PK_%'
168
171
  ) THEN 1 ELSE 0 END as is_primary_key
169
172
  FROM INFORMATION_SCHEMA.COLUMNS c
170
173
  WHERE TABLE_NAME = '{table_name}'
174
+ AND TABLE_SCHEMA = '{self.schema}'
171
175
  ORDER BY ORDINAL_POSITION
172
176
  """
173
177
 
@@ -255,6 +259,7 @@ class SQLServerAdapter(DbAdapter):
255
259
  SELECT COUNT(*) as count
256
260
  FROM INFORMATION_SCHEMA.TABLES
257
261
  WHERE TABLE_NAME = '{table_name}'
262
+ AND TABLE_SCHEMA = '{self.schema}'
258
263
  AND TABLE_TYPE = 'BASE TABLE'
259
264
  """
260
265
  result = self.execute_query(query)
@@ -276,13 +281,14 @@ class SQLServerAdapter(DbAdapter):
276
281
  if not self.engine:
277
282
  raise RuntimeError("Not connected to database")
278
283
 
279
- query = """
284
+ query = f"""
280
285
  SELECT
281
286
  TABLE_NAME as name,
282
287
  TABLE_SCHEMA as schema_name,
283
288
  '' as comment
284
289
  FROM INFORMATION_SCHEMA.TABLES
285
290
  WHERE TABLE_TYPE = 'BASE TABLE'
291
+ AND TABLE_SCHEMA = '{self.schema}'
286
292
  ORDER BY TABLE_NAME
287
293
  """
288
294
 
@@ -323,9 +329,10 @@ class SQLServerAdapter(DbAdapter):
323
329
  """Get example data (most frequent values) for each column in a table."""
324
330
  inspector = inspect(self.engine)
325
331
  try:
326
- columns = inspector.get_columns(table_name)
332
+ # For SQL Server, we need to specify the schema when inspecting columns
333
+ columns = inspector.get_columns(table_name, schema=self.schema)
327
334
  except SQLAlchemyError as e:
328
- logger.error(f"Error inspecting columns for table {table_name}: {e}")
335
+ logger.error(f"Error inspecting columns for table {table_name} in schema {self.schema}: {e}")
329
336
  raise e
330
337
 
331
338
  if not columns:
@@ -374,16 +381,18 @@ class SQLServerAdapter(DbAdapter):
374
381
  CASE WHEN EXISTS (
375
382
  SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
376
383
  WHERE TABLE_NAME = c.TABLE_NAME
384
+ AND TABLE_SCHEMA = c.TABLE_SCHEMA
377
385
  AND COLUMN_NAME = c.COLUMN_NAME
378
386
  AND CONSTRAINT_NAME LIKE 'PK_%'
379
387
  ) THEN 1 ELSE 0 END as is_primary_key
380
388
  FROM INFORMATION_SCHEMA.COLUMNS c
381
389
  WHERE c.TABLE_NAME = '{table_name}'
390
+ AND c.TABLE_SCHEMA = '{self.schema}'
382
391
  ORDER BY c.ORDINAL_POSITION
383
392
  """
384
393
  else:
385
394
  # Get all columns
386
- query = """
395
+ query = f"""
387
396
  SELECT
388
397
  c.TABLE_NAME as table_name,
389
398
  c.COLUMN_NAME as column_name,
@@ -394,10 +403,12 @@ class SQLServerAdapter(DbAdapter):
394
403
  CASE WHEN EXISTS (
395
404
  SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
396
405
  WHERE TABLE_NAME = c.TABLE_NAME
406
+ AND TABLE_SCHEMA = c.TABLE_SCHEMA
397
407
  AND COLUMN_NAME = c.COLUMN_NAME
398
408
  AND CONSTRAINT_NAME LIKE 'PK_%'
399
409
  ) THEN 1 ELSE 0 END as is_primary_key
400
410
  FROM INFORMATION_SCHEMA.COLUMNS c
411
+ WHERE c.TABLE_SCHEMA = '{self.schema}'
401
412
  ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION
402
413
  """
403
414
 
@@ -6,6 +6,9 @@ from typing import Any, Dict, List, Optional
6
6
  from .registry import DbPluginRegistry
7
7
  from .interfaces import DbPlugin
8
8
 
9
+ # Import plugins to ensure they are registered
10
+ from .. import plugins # This imports all plugins and registers them
11
+
9
12
  logger = logging.getLogger(__name__)
10
13
 
11
14