Merge pull request #625 from essence-tech/oracle-support

Feature: Oracle query runner
This commit is contained in:
Arik Fraimovich 2015-11-03 21:56:38 +02:00
commit b548cb1d8f
4 changed files with 196 additions and 1 deletions

View File

@ -201,3 +201,18 @@ Vertica
- **Additional requirements**:
- ``vertica-python`` python package
Oracle
------
- **Options**
- DSN Service name
- User
- Password
- Host
- Port
- **Additional requirements**
- ``cx_Oracle`` python package. This requires the installation of the Oracle `instant client <http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html>`__.

View File

@ -0,0 +1,175 @@
import json
import logging
import sys
from redash.query_runner import *
from redash.utils import JSONEncoder
try:
import cx_Oracle
TYPES_MAP = {
cx_Oracle.DATETIME: TYPE_DATETIME,
cx_Oracle.CLOB: TYPE_STRING,
cx_Oracle.LOB: TYPE_STRING,
cx_Oracle.FIXED_CHAR: TYPE_STRING,
cx_Oracle.FIXED_NCHAR: TYPE_STRING,
cx_Oracle.FIXED_UNICODE: TYPE_STRING,
cx_Oracle.INTERVAL: TYPE_DATETIME,
cx_Oracle.LONG_NCHAR: TYPE_STRING,
cx_Oracle.LONG_STRING: TYPE_STRING,
cx_Oracle.LONG_UNICODE: TYPE_STRING,
cx_Oracle.NATIVE_FLOAT: TYPE_FLOAT,
cx_Oracle.NCHAR: TYPE_STRING,
cx_Oracle.NUMBER: TYPE_FLOAT,
cx_Oracle.ROWID: TYPE_INTEGER,
cx_Oracle.STRING: TYPE_STRING,
cx_Oracle.TIMESTAMP: TYPE_DATETIME,
cx_Oracle.UNICODE: TYPE_STRING,
}
ENABLED = True
except ImportError:
ENABLED = False
logger = logging.getLogger(__name__)
class Oracle(BaseQueryRunner):
@classmethod
def get_col_type(cls, col_type, scale):
if col_type == cx_Oracle.NUMBER:
return TYPE_FLOAT if scale > 0 else TYPE_INTEGER
else:
return TYPES_MAP.get(col_type, None)
@classmethod
def enabled(cls):
return ENABLED
@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"user": {
"type": "string"
},
"password": {
"type": "string"
},
"host": {
"type": "string"
},
"port": {
"type": "number"
},
"servicename": {
"type": "string",
"title": "DSN Service Name"
}
},
"required": ["servicename"],
"secret": ["password"]
}
@classmethod
def type(cls):
return "oracle"
def __init__(self, configuration_json):
super(Oracle, self).__init__(configuration_json)
dsn = cx_Oracle.makedsn(
self.configuration["host"],
self.configuration["port"],
service_name=self.configuration["servicename"])
self.connection_string = "{}/{}@{}".format(self.configuration["user"], self.configuration["password"], dsn)
def get_schema(self):
query = """
SELECT
user_tables.TABLESPACE_NAME,
all_tab_cols.TABLE_NAME,
all_tab_cols.COLUMN_NAME
FROM all_tab_cols
JOIN user_tables ON (all_tab_cols.TABLE_NAME = user_tables.TABLE_NAME)
"""
results, error = self.run_query(query)
if error is not None:
raise Exception("Failed getting schema.")
results = json.loads(results)
schema = {}
for row in results['rows']:
if row['TABLESPACE_NAME'] != None:
table_name = '{}.{}'.format(row['TABLESPACE_NAME'], row['TABLE_NAME'])
else:
table_name = row['TABLE_NAME']
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}
schema[table_name]['columns'].append(row['COLUMN_NAME'])
return schema.values()
@classmethod
def _convert_number(cls, value):
try:
return int(value)
except:
return value
@classmethod
def output_handler(cls, cursor, name, default_type, length, precision, scale):
if default_type in (cx_Oracle.CLOB, cx_Oracle.LOB):
return cursor.var(cx_Oracle.LONG_STRING, 80000, cursor.arraysize)
if default_type in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR):
return cursor.var(unicode, length, cursor.arraysize)
if default_type == cx_Oracle.NUMBER:
if scale <= 0:
return cursor.var(cx_Oracle.STRING, 255, outconverter=Oracle._convert_number, arraysize=cursor.arraysize)
def run_query(self, query):
connection = cx_Oracle.connect(self.connection_string)
connection.outputtypehandler = Oracle.output_handler
cursor = connection.cursor()
try:
cursor.execute(query)
if cursor.description is not None:
columns = self.fetch_columns([(i[0], Oracle.get_col_type(i[1], i[5])) for i in cursor.description])
rows = [dict(zip((c['name'] for c in columns), row)) for row in cursor]
data = {'columns': columns, 'rows': rows}
error = None
json_data = json.dumps(data, cls=JSONEncoder)
else:
error = 'Query completed but it returned no data.'
json_data = None
except cx_Oracle.DatabaseError as err:
logging.exception(err.message)
error = "Query failed. {}.".format(err.message)
json_data = None
except KeyboardInterrupt:
connection.cancel()
error = "Query cancelled by user."
json_data = None
except Exception as err:
raise sys.exc_info()[1], None, sys.exc_info()[2]
finally:
connection.close()
return json_data, error
register(Oracle)

View File

@ -127,7 +127,8 @@ default_query_runners = [
'redash.query_runner.hive_ds',
'redash.query_runner.impala_ds',
'redash.query_runner.vertica',
'redash.query_runner.treasuredata'
'redash.query_runner.treasuredata',
'redash.query_runner.oracle',
]
enabled_query_runners = array_from_string(os.environ.get("REDASH_ENABLED_QUERY_RUNNERS", ",".join(default_query_runners)))

View File

@ -0,0 +1,4 @@
# Requires installation of, or similar versions of:
# oracle-instantclient12.1-basic_12.1.0.2.0-2_amd64.deb
# oracle-instantclient12.1-devel_12.1.0.2.0-2_amd64.deb
cx_Oracle==5.2