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.
@@ -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