Source code for dbtk.dialects.oracle
# dbtk/dialects/oracle.py
import re
from typing import Any, Optional
from .base import DatabaseDialect
[docs]
class OracleDialect(DatabaseDialect):
"""Oracle dialect. Uses MERGE with FROM dual and SQL*Loader-compatible temp tables."""
use_upsert = False
temp_table_all_cols = True
temp_table_cleanup_commit = True # TRUNCATE on GTT needs an explicit commit
def _merge_source_clause(self, source_cols: str) -> str:
return f"SELECT {source_cols} FROM dual"
# ------------------------------------------------------------------
# SQL type mapping
# ------------------------------------------------------------------
[docs]
def sql_type(self, type_obj: Any, internal_size: Optional[int],
precision: Optional[int], scale: Optional[int]) -> str:
if hasattr(type_obj, 'name'):
name = type_obj.name
if 'VARCHAR' in name:
return f"VARCHAR2({internal_size})" if internal_size else "VARCHAR2(4000)"
if 'CHAR' in name and 'VARCHAR' not in name:
return f"CHAR({internal_size})" if internal_size else "CHAR(1)"
if 'NUMBER' in name:
if precision and scale:
return f"NUMBER({precision},{scale})"
if precision:
return f"NUMBER({precision})"
return "NUMBER"
if 'DATE' in name:
return "DATE"
if 'TIMESTAMP' in name:
return "TIMESTAMP"
if 'CLOB' in name:
return "CLOB"
if 'BLOB' in name:
return "BLOB"
return "VARCHAR2(4000)"
# ------------------------------------------------------------------
# Schema introspection
# ------------------------------------------------------------------
[docs]
def table_metadata(self, cursor, table_name: str, add_comments: bool) -> dict:
table_name = table_name.upper()
tab_info = table_name.split('.')
schema_name = None
if len(tab_info) == 2:
schema_name = tab_info[0]
table_name = tab_info[1]
table_comment = None
if add_comments:
cmt_query = '''SELECT cmt.comments FROM all_tab_comments cmt
WHERE cmt.table_name = :table_name AND cmt.owner = COALESCE(:schema_name, cmt.owner)'''
cursor.execute(cmt_query, {'table_name': table_name, 'schema_name': schema_name})
row = cursor.fetchone()
if row and row[0]:
table_comment = row[0]
col_query = '''
SELECT LOWER(atc.column_name) column_name, atc.data_type, atc.data_scale,
atc.nullable,
CASE WHEN pkc.position IS NOT NULL THEN 'Y' ELSE 'N' END key_column,
cc.comments
FROM all_tab_cols atc
LEFT JOIN all_constraints pk ON atc.owner = pk.owner
AND atc.table_name = pk.table_name
AND pk.constraint_type = 'P'
LEFT JOIN all_col_comments cc ON atc.owner = cc.owner
AND atc.table_name = cc.table_name
AND atc.column_name = cc.column_name
LEFT JOIN all_cons_columns pkc ON atc.owner = pkc.owner
AND atc.table_name = pkc.table_name
AND atc.column_name = pkc.column_name
AND pk.constraint_name = pkc.constraint_name
WHERE atc.table_name = :table_name
AND atc.owner = COALESCE(:schema_name, atc.owner)
AND atc.virtual_column = 'NO'
ORDER BY atc.column_id
'''
cursor.execute(col_query, {'table_name': table_name, 'schema_name': schema_name})
columns = {}
column_comments = {}
for row in cursor:
col_name, data_type, data_scale, is_nullable, is_key, comment = row
if add_comments and comment:
column_comments[col_name] = comment
col_config = {'field': col_name}
if data_type == 'DATE':
col_config['fn'] = 'datetime' # Oracle DATE includes time
elif data_type in ('TIMESTAMP', 'TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE'):
col_config['fn'] = 'timestamp'
elif data_type == 'NUMBER':
col_config['fn'] = 'int' if data_scale == 0 else 'float'
elif data_type in ('FLOAT', 'BINARY_FLOAT', 'BINARY_DOUBLE'):
col_config['fn'] = 'float'
if is_key == 'Y':
col_config['primary_key'] = True
elif is_nullable == 'N':
col_config['nullable'] = False
columns[col_name] = col_config
return {
'name': table_name,
'columns': columns,
'table_comment': table_comment,
'column_comments': column_comments,
}
# ------------------------------------------------------------------
# Temp table (GLOBAL TEMPORARY TABLE … ON COMMIT PRESERVE ROWS)
# ------------------------------------------------------------------
[docs]
def create_temp_table_ddl(self, table_name: str, col_info: list):
temp_name = re.sub(r'[^A-Z0-9]+', '_', f"GTT_{table_name.upper()}")
col_defs = ', '.join(
f"{col_name} {sql_type}" for col_name, _, _, _, _, sql_type in col_info
)
create_sql = (
f"CREATE GLOBAL TEMPORARY TABLE {temp_name} ({col_defs}) ON COMMIT PRESERVE ROWS"
)
return temp_name, create_sql
[docs]
def cleanup_temp_table_sql(self, temp_name: str) -> str:
return f"TRUNCATE TABLE {temp_name}"