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.
- thoth_dbmanager-0.5.8/LICENSE.md +21 -0
- {thoth_dbmanager-0.5.2/thoth_dbmanager.egg-info → thoth_dbmanager-0.5.8}/PKG-INFO +2 -7
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/pyproject.toml +1 -6
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/__init__.py +1 -1
- thoth_dbmanager-0.5.8/thoth_dbmanager/adapters/__init__.py +39 -0
- thoth_dbmanager-0.5.8/thoth_dbmanager/adapters/mariadb.py +388 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/sqlserver.py +17 -6
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/factory.py +3 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/interfaces.py +1 -82
- thoth_dbmanager-0.5.8/thoth_dbmanager/plugins/__init__.py +42 -0
- thoth_dbmanager-0.5.8/thoth_dbmanager/plugins/mariadb.py +216 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8/thoth_dbmanager.egg-info}/PKG-INFO +2 -7
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/SOURCES.txt +1 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/requires.txt +0 -7
- thoth_dbmanager-0.5.2/thoth_dbmanager/adapters/__init__.py +0 -15
- thoth_dbmanager-0.5.2/thoth_dbmanager/adapters/mariadb.py +0 -165
- thoth_dbmanager-0.5.2/thoth_dbmanager/plugins/__init__.py +0 -17
- thoth_dbmanager-0.5.2/thoth_dbmanager/plugins/mariadb.py +0 -436
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/LICENSE +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/MANIFEST.in +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/README.md +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/setup.cfg +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/tests/test_lsh_interactive.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/tests/test_thoth_db_manager_base.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/ThothDbManager.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/postgresql.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/adapters/sqlite.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/__init__.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/core/registry.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/documents.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/dynamic_imports.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/__init__.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/multi_db_generator.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/preprocess_values.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/schema.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/helpers/search.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/__init__.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/core.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/factory.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/manager.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/lsh/storage.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/postgresql.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/sqlite.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager/plugins/sqlserver.py +0 -0
- {thoth_dbmanager-0.5.2 → thoth_dbmanager-0.5.8}/thoth_dbmanager.egg-info/dependency_links.txt +0 -0
- {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.
|
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.
|
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
|
@@ -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
|
-
|
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
|
|