thoth-dbmanager 0.4.13__py3-none-any.whl → 0.5.1__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.
- thoth_dbmanager/ThothDbManager.py +19 -227
- thoth_dbmanager/__init__.py +8 -63
- thoth_dbmanager/adapters/__init__.py +0 -6
- thoth_dbmanager/core/factory.py +0 -16
- thoth_dbmanager/dynamic_imports.py +0 -9
- thoth_dbmanager/helpers/multi_db_generator.py +1 -1
- thoth_dbmanager/plugins/__init__.py +0 -6
- thoth_dbmanager/plugins/sqlite.py +7 -0
- {thoth_dbmanager-0.4.13.dist-info → thoth_dbmanager-0.5.1.dist-info}/METADATA +228 -27
- {thoth_dbmanager-0.4.13.dist-info → thoth_dbmanager-0.5.1.dist-info}/RECORD +13 -19
- thoth_dbmanager/adapters/mysql.py +0 -165
- thoth_dbmanager/adapters/oracle.py +0 -554
- thoth_dbmanager/adapters/supabase.py +0 -249
- thoth_dbmanager/plugins/mysql.py +0 -408
- thoth_dbmanager/plugins/oracle.py +0 -150
- thoth_dbmanager/plugins/supabase.py +0 -224
- {thoth_dbmanager-0.4.13.dist-info → thoth_dbmanager-0.5.1.dist-info}/WHEEL +0 -0
- {thoth_dbmanager-0.4.13.dist-info → thoth_dbmanager-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {thoth_dbmanager-0.4.13.dist-info → thoth_dbmanager-0.5.1.dist-info}/top_level.txt +0 -0
@@ -1,554 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Oracle adapter implementation.
|
3
|
-
"""
|
4
|
-
import logging
|
5
|
-
from typing import Any, Dict, List, Optional, Union
|
6
|
-
from sqlalchemy import create_engine, inspect, text
|
7
|
-
from sqlalchemy.exc import SQLAlchemyError
|
8
|
-
|
9
|
-
from ..core.interfaces import DbAdapter
|
10
|
-
from ..documents import TableDocument, ColumnDocument, ForeignKeyDocument, SchemaDocument, IndexDocument
|
11
|
-
|
12
|
-
logger = logging.getLogger(__name__)
|
13
|
-
|
14
|
-
|
15
|
-
class OracleAdapter(DbAdapter):
|
16
|
-
"""Oracle database adapter implementation."""
|
17
|
-
|
18
|
-
def __init__(self, connection_params: Dict[str, Any]):
|
19
|
-
super().__init__(connection_params)
|
20
|
-
self.engine = None
|
21
|
-
self.host = connection_params.get('host', 'localhost')
|
22
|
-
self.port = connection_params.get('port', 1521)
|
23
|
-
self.service_name = connection_params.get('service_name')
|
24
|
-
self.user = connection_params.get('user')
|
25
|
-
self.password = connection_params.get('password')
|
26
|
-
|
27
|
-
def connect(self) -> None:
|
28
|
-
"""Establish database connection."""
|
29
|
-
try:
|
30
|
-
# Build connection string for Oracle
|
31
|
-
connection_string = self._build_connection_string()
|
32
|
-
self.engine = create_engine(connection_string, pool_pre_ping=True)
|
33
|
-
|
34
|
-
# Test connection
|
35
|
-
with self.engine.connect() as conn:
|
36
|
-
conn.execute(text("SELECT 1 FROM DUAL"))
|
37
|
-
|
38
|
-
self._initialized = True
|
39
|
-
logger.info("Oracle connection established successfully")
|
40
|
-
|
41
|
-
except Exception as e:
|
42
|
-
logger.error(f"Failed to connect to Oracle: {e}")
|
43
|
-
raise ConnectionError(f"Failed to connect to Oracle: {e}")
|
44
|
-
|
45
|
-
def _build_connection_string(self) -> str:
|
46
|
-
"""Build SQLAlchemy connection string for Oracle"""
|
47
|
-
if not all([self.service_name, self.user, self.password]):
|
48
|
-
raise ValueError("Missing required connection parameters: service_name, user, password")
|
49
|
-
|
50
|
-
# Try different Oracle connection methods in order of preference
|
51
|
-
connection_methods = [
|
52
|
-
# Try python-oracledb (thin mode - no client required)
|
53
|
-
lambda: f"oracle+oracledb://{self.user}:{self.password}@{self.host}:{self.port}/?service_name={self.service_name}&mode=thin",
|
54
|
-
# Try python-oracledb (thick mode)
|
55
|
-
lambda: f"oracle+oracledb://{self.user}:{self.password}@{self.host}:{self.port}/?service_name={self.service_name}",
|
56
|
-
# Try cx_Oracle with service name format
|
57
|
-
lambda: f"oracle+cx_oracle://{self.user}:{self.password}@{self.host}:{self.port}/?service_name={self.service_name}",
|
58
|
-
# Try oracledb with SID format (fallback)
|
59
|
-
lambda: f"oracle+oracledb://{self.user}:{self.password}@{self.host}:{self.port}/{self.service_name}",
|
60
|
-
# Try cx_Oracle with SID format (fallback)
|
61
|
-
lambda: f"oracle+cx_oracle://{self.user}:{self.password}@{self.host}:{self.port}/{self.service_name}",
|
62
|
-
]
|
63
|
-
|
64
|
-
# Try each connection method until one works
|
65
|
-
for i, method in enumerate(connection_methods):
|
66
|
-
try:
|
67
|
-
connection_string = method()
|
68
|
-
logger.info(f"Attempting connection method {i+1}: {connection_string.split('@')[0]}@...")
|
69
|
-
|
70
|
-
# Test the connection string by creating a temporary engine
|
71
|
-
test_engine = create_engine(connection_string, pool_pre_ping=True)
|
72
|
-
with test_engine.connect() as conn:
|
73
|
-
conn.execute(text("SELECT 1 FROM DUAL"))
|
74
|
-
test_engine.dispose()
|
75
|
-
|
76
|
-
driver_name = "python-oracledb" if "oracledb" in connection_string else "cx_oracle"
|
77
|
-
mode = "thin" if "mode=thin" in connection_string else "thick"
|
78
|
-
logger.info(f"Successfully connected using {driver_name} ({mode} mode)")
|
79
|
-
return connection_string
|
80
|
-
|
81
|
-
except Exception as e:
|
82
|
-
logger.debug(f"Connection method {i+1} failed: {e}")
|
83
|
-
continue
|
84
|
-
|
85
|
-
# If all methods fail, provide helpful error message
|
86
|
-
raise ConnectionError(
|
87
|
-
f"Failed to connect to Oracle using any available method. "
|
88
|
-
f"Tried: python-oracledb (thin/thick), cx_Oracle. "
|
89
|
-
f"For cx_Oracle, ensure Oracle Instant Client is installed. "
|
90
|
-
f"For python-oracledb, ensure the package is installed: pip install oracledb"
|
91
|
-
)
|
92
|
-
|
93
|
-
def disconnect(self) -> None:
|
94
|
-
"""Close database connection."""
|
95
|
-
if self.engine:
|
96
|
-
self.engine.dispose()
|
97
|
-
self.engine = None
|
98
|
-
self._initialized = False
|
99
|
-
|
100
|
-
def execute_query(self, query: str, params: Optional[Dict] = None, fetch: Union[str, int] = "all", timeout: int = 60) -> Any:
|
101
|
-
"""Execute SQL query"""
|
102
|
-
if not self.engine:
|
103
|
-
raise RuntimeError("Not connected to database")
|
104
|
-
|
105
|
-
try:
|
106
|
-
with self.engine.connect() as conn:
|
107
|
-
# Execute query
|
108
|
-
if params:
|
109
|
-
result = conn.execute(text(query), params)
|
110
|
-
else:
|
111
|
-
result = conn.execute(text(query))
|
112
|
-
|
113
|
-
# Handle different fetch modes
|
114
|
-
if query.strip().upper().startswith(('SELECT', 'WITH')):
|
115
|
-
if fetch == "all":
|
116
|
-
return result.fetchall()
|
117
|
-
elif fetch == "one":
|
118
|
-
return result.fetchone()
|
119
|
-
elif isinstance(fetch, int):
|
120
|
-
return result.fetchmany(fetch)
|
121
|
-
else:
|
122
|
-
return result.fetchall()
|
123
|
-
else:
|
124
|
-
# For non-SELECT queries, return rowcount
|
125
|
-
conn.commit()
|
126
|
-
return result.rowcount
|
127
|
-
|
128
|
-
except SQLAlchemyError as e:
|
129
|
-
logger.error(f"Oracle query error: {e}")
|
130
|
-
raise
|
131
|
-
|
132
|
-
def execute_update(self, query: str, params: Optional[Dict[str, Any]] = None) -> int:
|
133
|
-
"""Execute an update query and return affected row count."""
|
134
|
-
if not self.engine:
|
135
|
-
self.connect()
|
136
|
-
|
137
|
-
try:
|
138
|
-
with self.engine.connect() as conn:
|
139
|
-
result = conn.execute(text(query), params or {})
|
140
|
-
conn.commit()
|
141
|
-
return result.rowcount
|
142
|
-
except SQLAlchemyError as e:
|
143
|
-
raise RuntimeError(f"Oracle update failed: {e}")
|
144
|
-
|
145
|
-
def get_tables(self) -> List[str]:
|
146
|
-
"""Get list of tables in the database."""
|
147
|
-
query = """
|
148
|
-
SELECT table_name as name
|
149
|
-
FROM user_tables
|
150
|
-
ORDER BY table_name
|
151
|
-
"""
|
152
|
-
result = self.execute_query(query)
|
153
|
-
return [row['name'] for row in result]
|
154
|
-
|
155
|
-
def get_table_schema(self, table_name: str) -> Dict[str, Any]:
|
156
|
-
"""Get schema information for a specific table."""
|
157
|
-
query = f"""
|
158
|
-
SELECT
|
159
|
-
column_name as name,
|
160
|
-
data_type as type,
|
161
|
-
nullable,
|
162
|
-
data_default as default_value,
|
163
|
-
CASE WHEN constraint_type = 'P' THEN 1 ELSE 0 END as is_primary_key
|
164
|
-
FROM user_tab_columns c
|
165
|
-
LEFT JOIN (
|
166
|
-
SELECT cc.column_name, uc.constraint_type
|
167
|
-
FROM user_constraints uc
|
168
|
-
JOIN user_cons_columns cc ON uc.constraint_name = cc.constraint_name
|
169
|
-
WHERE uc.table_name = '{table_name.upper()}'
|
170
|
-
AND uc.constraint_type = 'P'
|
171
|
-
) pk ON c.column_name = pk.column_name
|
172
|
-
WHERE c.table_name = '{table_name.upper()}'
|
173
|
-
ORDER BY c.column_id
|
174
|
-
"""
|
175
|
-
|
176
|
-
columns = self.execute_query(query)
|
177
|
-
|
178
|
-
schema = {
|
179
|
-
'table_name': table_name,
|
180
|
-
'columns': []
|
181
|
-
}
|
182
|
-
|
183
|
-
for col in columns:
|
184
|
-
schema['columns'].append({
|
185
|
-
'name': col['name'],
|
186
|
-
'type': col['type'],
|
187
|
-
'nullable': col['nullable'] == 'Y',
|
188
|
-
'default': col['default_value'],
|
189
|
-
'primary_key': bool(col['is_primary_key'])
|
190
|
-
})
|
191
|
-
|
192
|
-
return schema
|
193
|
-
|
194
|
-
def get_indexes(self, table_name: str) -> List[Dict[str, Any]]:
|
195
|
-
"""Get index information for a table."""
|
196
|
-
query = f"""
|
197
|
-
SELECT
|
198
|
-
index_name as name,
|
199
|
-
column_name,
|
200
|
-
uniqueness as unique_index,
|
201
|
-
index_type
|
202
|
-
FROM user_ind_columns ic
|
203
|
-
JOIN user_indexes i ON ic.index_name = i.index_name
|
204
|
-
WHERE ic.table_name = '{table_name.upper()}'
|
205
|
-
ORDER BY ic.index_name, ic.column_position
|
206
|
-
"""
|
207
|
-
|
208
|
-
return self.execute_query(query)
|
209
|
-
|
210
|
-
def get_foreign_keys(self, table_name: str) -> List[Dict[str, Any]]:
|
211
|
-
"""Get foreign key information for a table."""
|
212
|
-
query = f"""
|
213
|
-
SELECT
|
214
|
-
constraint_name as name,
|
215
|
-
column_name,
|
216
|
-
r_table_name as referenced_table,
|
217
|
-
r_column_name as referenced_column
|
218
|
-
FROM user_cons_columns cc
|
219
|
-
JOIN user_constraints c ON cc.constraint_name = c.constraint_name
|
220
|
-
JOIN user_cons_columns rcc ON c.r_constraint_name = rcc.constraint_name
|
221
|
-
WHERE c.table_name = '{table_name.upper()}'
|
222
|
-
AND c.constraint_type = 'R'
|
223
|
-
ORDER BY cc.constraint_name, cc.position
|
224
|
-
"""
|
225
|
-
|
226
|
-
return self.execute_query(query)
|
227
|
-
|
228
|
-
def create_table(self, table_name: str, schema: Dict[str, Any]) -> None:
|
229
|
-
"""Create a new table with the given schema."""
|
230
|
-
columns = []
|
231
|
-
for col in schema.get('columns', []):
|
232
|
-
col_def = f'"{col["name"]}" {col["type"]}'
|
233
|
-
if not col.get('nullable', True):
|
234
|
-
col_def += " NOT NULL"
|
235
|
-
if col.get('default') is not None:
|
236
|
-
col_def += f" DEFAULT {col['default']}"
|
237
|
-
if col.get('primary_key'):
|
238
|
-
col_def += " PRIMARY KEY"
|
239
|
-
columns.append(col_def)
|
240
|
-
|
241
|
-
query = f'CREATE TABLE "{table_name}" ({", ".join(columns)})'
|
242
|
-
self.execute_update(query)
|
243
|
-
|
244
|
-
def drop_table(self, table_name: str) -> None:
|
245
|
-
"""Drop a table."""
|
246
|
-
query = f'DROP TABLE "{table_name}"'
|
247
|
-
self.execute_update(query)
|
248
|
-
|
249
|
-
def table_exists(self, table_name: str) -> bool:
|
250
|
-
"""Check if a table exists."""
|
251
|
-
query = f"""
|
252
|
-
SELECT COUNT(*) as count
|
253
|
-
FROM user_tables
|
254
|
-
WHERE table_name = '{table_name.upper()}'
|
255
|
-
"""
|
256
|
-
result = self.execute_query(query)
|
257
|
-
return result[0]['count'] > 0
|
258
|
-
|
259
|
-
def get_connection_info(self) -> Dict[str, Any]:
|
260
|
-
"""Get connection information."""
|
261
|
-
return {
|
262
|
-
'type': 'oracle',
|
263
|
-
'host': self.host,
|
264
|
-
'port': self.port,
|
265
|
-
'service_name': self.service_name,
|
266
|
-
'user': self.user,
|
267
|
-
'connected': self.engine is not None and self._initialized
|
268
|
-
}
|
269
|
-
|
270
|
-
def health_check(self) -> bool:
|
271
|
-
"""Check if Oracle database connection is healthy"""
|
272
|
-
try:
|
273
|
-
# Use Oracle-specific syntax for health check
|
274
|
-
self.execute_query("SELECT 1 FROM DUAL", fetch="one")
|
275
|
-
return True
|
276
|
-
except Exception:
|
277
|
-
return False
|
278
|
-
|
279
|
-
def get_tables_as_documents(self) -> List[TableDocument]:
|
280
|
-
"""Get tables as TableDocument objects"""
|
281
|
-
if not self.engine:
|
282
|
-
raise RuntimeError("Not connected to database")
|
283
|
-
|
284
|
-
query = "SELECT TABLE_NAME as name FROM USER_TABLES ORDER BY TABLE_NAME"
|
285
|
-
|
286
|
-
try:
|
287
|
-
result = self.execute_query(query)
|
288
|
-
tables = []
|
289
|
-
|
290
|
-
for row in result:
|
291
|
-
# Oracle consistently returns tuples, so access by index
|
292
|
-
table_name = row[0] # Keep Oracle UPPERCASE naming standard
|
293
|
-
|
294
|
-
table_doc = TableDocument(
|
295
|
-
table_name=table_name,
|
296
|
-
schema_name=self.user.upper(),
|
297
|
-
comment='',
|
298
|
-
columns=[], # Will be populated separately if needed
|
299
|
-
foreign_keys=[],
|
300
|
-
indexes=[]
|
301
|
-
)
|
302
|
-
tables.append(table_doc)
|
303
|
-
|
304
|
-
return tables
|
305
|
-
|
306
|
-
except Exception as e:
|
307
|
-
logger.error(f"Error getting tables: {e}")
|
308
|
-
raise
|
309
|
-
|
310
|
-
def get_columns_as_documents(self, table_name: str = None) -> List[ColumnDocument]:
|
311
|
-
"""Get columns as ColumnDocument objects"""
|
312
|
-
if not self.engine:
|
313
|
-
raise RuntimeError("Not connected to database")
|
314
|
-
|
315
|
-
if table_name:
|
316
|
-
# Get columns for specific table
|
317
|
-
query = f"""
|
318
|
-
SELECT
|
319
|
-
c.TABLE_NAME as table_name,
|
320
|
-
c.COLUMN_NAME as column_name,
|
321
|
-
c.DATA_TYPE as data_type,
|
322
|
-
c.NULLABLE as is_nullable,
|
323
|
-
c.DATA_DEFAULT as default_value,
|
324
|
-
CASE WHEN EXISTS (
|
325
|
-
SELECT 1 FROM ALL_CONSTRAINTS ac, ALL_CONS_COLUMNS acc
|
326
|
-
WHERE ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME
|
327
|
-
AND ac.CONSTRAINT_TYPE = 'P'
|
328
|
-
AND acc.TABLE_NAME = c.TABLE_NAME
|
329
|
-
AND acc.COLUMN_NAME = c.COLUMN_NAME
|
330
|
-
AND ac.OWNER = USER
|
331
|
-
) THEN 1 ELSE 0 END as is_primary_key
|
332
|
-
FROM ALL_TAB_COLUMNS c
|
333
|
-
WHERE c.TABLE_NAME = '{table_name.upper()}'
|
334
|
-
AND c.OWNER = USER
|
335
|
-
ORDER BY c.COLUMN_ID
|
336
|
-
"""
|
337
|
-
else:
|
338
|
-
# Get all columns
|
339
|
-
query = """
|
340
|
-
SELECT
|
341
|
-
c.TABLE_NAME as table_name,
|
342
|
-
c.COLUMN_NAME as column_name,
|
343
|
-
c.DATA_TYPE as data_type,
|
344
|
-
c.NULLABLE as is_nullable,
|
345
|
-
c.DATA_DEFAULT as default_value,
|
346
|
-
CASE WHEN EXISTS (
|
347
|
-
SELECT 1 FROM ALL_CONSTRAINTS ac, ALL_CONS_COLUMNS acc
|
348
|
-
WHERE ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME
|
349
|
-
AND ac.CONSTRAINT_TYPE = 'P'
|
350
|
-
AND acc.TABLE_NAME = c.TABLE_NAME
|
351
|
-
AND acc.COLUMN_NAME = c.COLUMN_NAME
|
352
|
-
AND ac.OWNER = SYS_CONTEXT('USERENV', 'SESSION_USER')
|
353
|
-
) THEN 1 ELSE 0 END as is_primary_key
|
354
|
-
FROM ALL_TAB_COLUMNS c
|
355
|
-
WHERE c.OWNER = SYS_CONTEXT('USERENV', 'SESSION_USER')
|
356
|
-
ORDER BY c.TABLE_NAME, c.COLUMN_ID
|
357
|
-
"""
|
358
|
-
|
359
|
-
try:
|
360
|
-
result = self.execute_query(query)
|
361
|
-
columns = []
|
362
|
-
|
363
|
-
for row in result:
|
364
|
-
# Oracle consistently returns tuples, so access by index based on SELECT order
|
365
|
-
column_doc = ColumnDocument(
|
366
|
-
table_name=row[0], # Keep Oracle UPPERCASE naming standard
|
367
|
-
column_name=row[1], # Keep Oracle UPPERCASE naming standard
|
368
|
-
data_type=row[2], # data_type (keep as-is for Oracle type names)
|
369
|
-
is_nullable=row[3] == 'Y', # is_nullable
|
370
|
-
default_value=row[4] if len(row) > 4 else None, # default_value
|
371
|
-
is_pk=bool(row[5]) if len(row) > 5 else False, # is_pk (use is_pk, not is_primary_key)
|
372
|
-
comment=''
|
373
|
-
)
|
374
|
-
columns.append(column_doc)
|
375
|
-
|
376
|
-
return columns
|
377
|
-
|
378
|
-
except Exception as e:
|
379
|
-
logger.error(f"Error getting columns: {e}")
|
380
|
-
raise
|
381
|
-
|
382
|
-
def get_foreign_keys_as_documents(self, table_name: str = None) -> List[ForeignKeyDocument]:
|
383
|
-
"""Get foreign keys as ForeignKeyDocument objects"""
|
384
|
-
if not self.engine:
|
385
|
-
raise RuntimeError("Not connected to database")
|
386
|
-
|
387
|
-
if table_name:
|
388
|
-
where_clause = f"AND ac.TABLE_NAME = '{table_name.upper()}'"
|
389
|
-
else:
|
390
|
-
where_clause = ""
|
391
|
-
|
392
|
-
query = f"""
|
393
|
-
SELECT
|
394
|
-
ac.CONSTRAINT_NAME as constraint_name,
|
395
|
-
ac.TABLE_NAME as table_name,
|
396
|
-
acc.COLUMN_NAME as column_name,
|
397
|
-
r_ac.TABLE_NAME as referenced_table,
|
398
|
-
r_acc.COLUMN_NAME as referenced_column
|
399
|
-
FROM ALL_CONSTRAINTS ac
|
400
|
-
JOIN ALL_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME
|
401
|
-
JOIN ALL_CONSTRAINTS r_ac ON ac.R_CONSTRAINT_NAME = r_ac.CONSTRAINT_NAME
|
402
|
-
JOIN ALL_CONS_COLUMNS r_acc ON r_ac.CONSTRAINT_NAME = r_acc.CONSTRAINT_NAME
|
403
|
-
WHERE ac.CONSTRAINT_TYPE = 'R'
|
404
|
-
AND ac.OWNER = USER
|
405
|
-
{where_clause}
|
406
|
-
ORDER BY ac.CONSTRAINT_NAME
|
407
|
-
"""
|
408
|
-
|
409
|
-
try:
|
410
|
-
result = self.execute_query(query)
|
411
|
-
foreign_keys = []
|
412
|
-
|
413
|
-
for row in result:
|
414
|
-
fk_doc = ForeignKeyDocument(
|
415
|
-
constraint_name=row['constraint_name'],
|
416
|
-
table_name=row['table_name'],
|
417
|
-
column_name=row['column_name'],
|
418
|
-
referenced_table=row['referenced_table'],
|
419
|
-
referenced_column=row['referenced_column']
|
420
|
-
)
|
421
|
-
foreign_keys.append(fk_doc)
|
422
|
-
|
423
|
-
return foreign_keys
|
424
|
-
|
425
|
-
except Exception as e:
|
426
|
-
logger.error(f"Error getting foreign keys: {e}")
|
427
|
-
raise
|
428
|
-
|
429
|
-
def get_indexes_as_documents(self, table_name: str = None) -> List[IndexDocument]:
|
430
|
-
"""Get indexes as IndexDocument objects"""
|
431
|
-
if not self.engine:
|
432
|
-
raise RuntimeError("Not connected to database")
|
433
|
-
|
434
|
-
if table_name:
|
435
|
-
where_clause = f"AND ai.TABLE_NAME = '{table_name.upper()}'"
|
436
|
-
else:
|
437
|
-
where_clause = ""
|
438
|
-
|
439
|
-
query = f"""
|
440
|
-
SELECT
|
441
|
-
ai.INDEX_NAME as index_name,
|
442
|
-
ai.TABLE_NAME as table_name,
|
443
|
-
aic.COLUMN_NAME as column_name,
|
444
|
-
ai.UNIQUENESS as uniqueness,
|
445
|
-
CASE WHEN EXISTS (
|
446
|
-
SELECT 1 FROM ALL_CONSTRAINTS ac
|
447
|
-
WHERE ac.CONSTRAINT_TYPE = 'P'
|
448
|
-
AND ac.INDEX_NAME = ai.INDEX_NAME
|
449
|
-
AND ac.OWNER = SYS_CONTEXT('USERENV', 'SESSION_USER')
|
450
|
-
) THEN 1 ELSE 0 END as is_primary
|
451
|
-
FROM ALL_INDEXES ai
|
452
|
-
JOIN ALL_IND_COLUMNS aic ON ai.INDEX_NAME = aic.INDEX_NAME
|
453
|
-
WHERE ai.OWNER = SYS_CONTEXT('USERENV', 'SESSION_USER')
|
454
|
-
{where_clause}
|
455
|
-
ORDER BY ai.INDEX_NAME, aic.COLUMN_POSITION
|
456
|
-
"""
|
457
|
-
|
458
|
-
try:
|
459
|
-
result = self.execute_query(query)
|
460
|
-
indexes = []
|
461
|
-
|
462
|
-
for row in result:
|
463
|
-
index_doc = IndexDocument(
|
464
|
-
index_name=row['index_name'],
|
465
|
-
table_name=row['table_name'],
|
466
|
-
column_name=row['column_name'],
|
467
|
-
is_unique=row['uniqueness'] == 'UNIQUE',
|
468
|
-
is_primary=bool(row['is_primary'])
|
469
|
-
)
|
470
|
-
indexes.append(index_doc)
|
471
|
-
|
472
|
-
return indexes
|
473
|
-
|
474
|
-
except Exception as e:
|
475
|
-
logger.error(f"Error getting indexes: {e}")
|
476
|
-
raise
|
477
|
-
|
478
|
-
def get_schemas_as_documents(self) -> List[SchemaDocument]:
|
479
|
-
"""Get schemas as SchemaDocument objects"""
|
480
|
-
if not self.engine:
|
481
|
-
raise RuntimeError("Not connected to database")
|
482
|
-
|
483
|
-
query = """
|
484
|
-
SELECT
|
485
|
-
USERNAME as schema_name,
|
486
|
-
'' as comment
|
487
|
-
FROM ALL_USERS
|
488
|
-
WHERE USERNAME = SYS_CONTEXT('USERENV', 'SESSION_USER')
|
489
|
-
ORDER BY USERNAME
|
490
|
-
"""
|
491
|
-
|
492
|
-
try:
|
493
|
-
result = self.execute_query(query)
|
494
|
-
schemas = []
|
495
|
-
|
496
|
-
for row in result:
|
497
|
-
schema_doc = SchemaDocument(
|
498
|
-
schema_name=row['schema_name'],
|
499
|
-
comment=row.get('comment', ''),
|
500
|
-
tables=[], # Will be populated separately if needed
|
501
|
-
views=[]
|
502
|
-
)
|
503
|
-
schemas.append(schema_doc)
|
504
|
-
|
505
|
-
return schemas
|
506
|
-
|
507
|
-
except Exception as e:
|
508
|
-
logger.error(f"Error getting schemas: {e}")
|
509
|
-
raise
|
510
|
-
|
511
|
-
def get_unique_values(self) -> Dict[str, Dict[str, List[str]]]:
|
512
|
-
"""Get unique values from the database."""
|
513
|
-
# This is a placeholder implementation.
|
514
|
-
# A more sophisticated version should be implemented based on requirements.
|
515
|
-
return {}
|
516
|
-
|
517
|
-
def get_example_data(self, table_name: str, number_of_rows: int = 30) -> Dict[str, List[Any]]:
|
518
|
-
"""Get example data (most frequent values) for each column in a table."""
|
519
|
-
inspector = inspect(self.engine)
|
520
|
-
try:
|
521
|
-
columns = inspector.get_columns(table_name.upper())
|
522
|
-
except SQLAlchemyError as e:
|
523
|
-
logger.error(f"Error inspecting columns for table {table_name}: {e}")
|
524
|
-
raise e
|
525
|
-
|
526
|
-
if not columns:
|
527
|
-
logger.warning(f"No columns found for table {table_name}")
|
528
|
-
return {}
|
529
|
-
|
530
|
-
most_frequent_values: Dict[str, List[Any]] = {}
|
531
|
-
|
532
|
-
for column in columns:
|
533
|
-
column_name = column['name']
|
534
|
-
try:
|
535
|
-
# Get most frequent values for this column
|
536
|
-
query = f"""
|
537
|
-
SELECT * FROM (
|
538
|
-
SELECT "{column_name}", COUNT(*) as frequency
|
539
|
-
FROM "{table_name.upper()}"
|
540
|
-
WHERE "{column_name}" IS NOT NULL
|
541
|
-
GROUP BY "{column_name}"
|
542
|
-
ORDER BY COUNT(*) DESC
|
543
|
-
) WHERE ROWNUM <= {number_of_rows}
|
544
|
-
"""
|
545
|
-
|
546
|
-
result = self.execute_query(query)
|
547
|
-
values = [row[column_name] for row in result]
|
548
|
-
most_frequent_values[column_name] = values
|
549
|
-
|
550
|
-
except Exception as e:
|
551
|
-
logger.warning(f"Error getting example data for column {column_name}: {e}")
|
552
|
-
most_frequent_values[column_name] = []
|
553
|
-
|
554
|
-
return most_frequent_values
|