Improve robustness of java to python type conversion.

master
baztian 2015-02-08 15:43:27 +01:00
parent 3fef45e608
commit e03b657497
1 changed files with 83 additions and 48 deletions

View File

@ -1,6 +1,6 @@
#-*- coding: utf-8 -*- #-*- coding: utf-8 -*-
# Copyright 2010, 2011, 2012, 2013 Bastian Bowe # Copyright 2010-2015 Bastian Bowe
# #
# This file is part of JayDeBeApi. # This file is part of JayDeBeApi.
# JayDeBeApi is free software: you can redistribute it and/or modify # JayDeBeApi is free software: you can redistribute it and/or modify
@ -29,6 +29,12 @@ import re
import sys import sys
import warnings import warnings
# Mapping from java.sql.Types attribute name to attribute value
_jdbc_name_to_const = None
# Mapping from java.sql.Types attribute constant value to it's attribute name
_jdbc_const_to_name = None
_jdbc_connect = None _jdbc_connect = None
_java_array_byte = None _java_array_byte = None
@ -43,7 +49,7 @@ def _handle_sql_exception_jython(ex):
raise ex raise ex
def _jdbc_connect_jython(jclassname, jars, libs, *args): def _jdbc_connect_jython(jclassname, jars, libs, *args):
if _converters is None: if _jdbc_name_to_const is None:
from java.sql import Types from java.sql import Types
types = Types types = Types
types_map = {} types_map = {}
@ -51,7 +57,7 @@ def _jdbc_connect_jython(jclassname, jars, libs, *args):
for i in dir(types): for i in dir(types):
if const_re.match(i): if const_re.match(i):
types_map[i] = getattr(types, i) types_map[i] = getattr(types, i)
_init_converters(types_map) _init_types(types_map)
global _java_array_byte global _java_array_byte
if _java_array_byte is None: if _java_array_byte is None:
import jarray import jarray
@ -125,12 +131,12 @@ def _jdbc_connect_jpype(jclassname, jars, libs, *driver_args):
jpype.startJVM(jvm_path, *args) jpype.startJVM(jvm_path, *args)
if not jpype.isThreadAttachedToJVM(): if not jpype.isThreadAttachedToJVM():
jpype.attachThreadToJVM() jpype.attachThreadToJVM()
if _converters is None: if _jdbc_name_to_const is None:
types = jpype.java.sql.Types types = jpype.java.sql.Types
types_map = {} types_map = {}
for i in types.__javaclass__.getClassFields(): for i in types.__javaclass__.getClassFields():
types_map[i.getName()] = i.getStaticAttribute() types_map[i.getName()] = i.getStaticAttribute()
_init_converters(types_map) _init_types(types_map)
global _java_array_byte global _java_array_byte
if _java_array_byte is None: if _java_array_byte is None:
def _java_array_byte(data): def _java_array_byte(data):
@ -175,50 +181,58 @@ paramstyle = 'qmark'
class DBAPITypeObject(object): class DBAPITypeObject(object):
_mappings = {} _mappings = {}
def __init__(self,*values): def __init__(self, *values):
"""Construct new DB-API 2.0 type object.
values: Attribute names of java.sql.Types constants"""
self.values = values self.values = values
for i in values: for type_name in values:
if i in DBAPITypeObject._mappings: if type_name in DBAPITypeObject._mappings:
raise ValueError, "Non unique mapping for type '%s'" % i raise ValueError, "Non unique mapping for type '%s'" % type_name
DBAPITypeObject._mappings[i] = self DBAPITypeObject._mappings[type_name] = self
def __cmp__(self,other): def __cmp__(self, other):
if other in self.values: if other in self.values:
return 0 return 0
if other < self.values: if other < self.values:
return 1 return 1
else: else:
return -1 return -1
def __repr__(self):
return 'DBAPITypeObject(%s)' % ", ".join([repr(i) for i in self.values])
@classmethod @classmethod
def _map_jdbc_type_to_dbapi(cls, jdbc_type): def _map_jdbc_type_to_dbapi(cls, jdbc_type_const):
try: try:
return cls._mappings[jdbc_type.upper()] type_name = _jdbc_const_to_name[jdbc_type_const]
except KeyError: except KeyError:
warnings.warn("No type mapping for JDBC type '%s'. " warnings.warn("Unknown JDBC type with constant value %d. "
"Using None as a default." % jdbc_type) "Using None as a default type_code." % jdbc_type_const)
return None
try:
return cls._mappings[type_name]
except KeyError:
warnings.warn("No type mapping for JDBC type '%s' (constant value %d). "
"Using None as a default type_code." % (type_name, jdbc_type_const))
return None return None
STRING = DBAPITypeObject("CHARACTER", "CHAR", "VARCHAR", STRING = DBAPITypeObject('CHAR', 'NCHAR', 'NVARCHAR', 'VARCHAR', 'OTHER')
"CHARACTER VARYING", "CHAR VARYING", "STRING",)
TEXT = DBAPITypeObject("CLOB", "CHARACTER LARGE OBJECT", TEXT = DBAPITypeObject('CLOB', 'LONGNVARCHAR', 'LONGNARCHAR', 'NCLOB', 'SQLXML')
"CHAR LARGE OBJECT", "XML",)
BINARY = DBAPITypeObject("BLOB", "BINARY LARGE OBJECT",) BINARY = DBAPITypeObject('BINARY', 'BLOB', 'LONGVARBINARY', 'VARBINARY')
NUMBER = DBAPITypeObject("INTEGER", "INT", "SMALLINT", "BIGINT",) NUMBER = DBAPITypeObject('BOOLEAN', 'BIGINT', 'INTEGER', 'SMALLINT')
FLOAT = DBAPITypeObject("FLOAT", "REAL", "DOUBLE", "DECFLOAT") FLOAT = DBAPITypeObject('FLOAT', 'REAL', 'DOUBLE')
DECIMAL = DBAPITypeObject("DECIMAL", "DEC", "NUMERIC", "NUM",) DECIMAL = DBAPITypeObject('DECIMAL', 'NUMERIC')
DATE = DBAPITypeObject("DATE",) DATE = DBAPITypeObject('DATE')
TIME = DBAPITypeObject("TIME",) TIME = DBAPITypeObject('TIME')
DATETIME = DBAPITypeObject("TIMESTAMP",) DATETIME = DBAPITypeObject('TIMESTAMP')
ROWID = DBAPITypeObject(()) ROWID = DBAPITypeObject('ROWID')
# DB-API 2.0 Module Interface Exceptions # DB-API 2.0 Module Interface Exceptions
class Error(exceptions.StandardError): class Error(exceptions.StandardError):
@ -371,8 +385,13 @@ class Cursor(object):
self._description = [] self._description = []
for col in range(1, count + 1): for col in range(1, count + 1):
size = m.getColumnDisplaySize(col) size = m.getColumnDisplaySize(col)
jdbc_type = m.getColumnTypeName(col) jdbc_type = m.getColumnType(col)
dbapi_type = DBAPITypeObject._map_jdbc_type_to_dbapi(jdbc_type) if jdbc_type == 0:
# PEP-0249: SQL NULL values are represented by the
# Python None singleton
dbapi_type = None
else:
dbapi_type = DBAPITypeObject._map_jdbc_type_to_dbapi(jdbc_type)
col_desc = ( m.getColumnName(col), col_desc = ( m.getColumnName(col),
dbapi_type, dbapi_type,
size, size,
@ -450,14 +469,8 @@ class Cursor(object):
row = [] row = []
for col in range(1, self._meta.getColumnCount() + 1): for col in range(1, self._meta.getColumnCount() + 1):
sqltype = self._meta.getColumnType(col) sqltype = self._meta.getColumnType(col)
# print sqltype converter = self._converters.get(sqltype, _unknownSqlTypeConverter)
# TODO: Oracle 11 will read a oracle.sql.TIMESTAMP v = converter(self._rs, col)
# which can't be converted to string easily
v = self._rs.getObject(col)
if v:
converter = self._converters.get(sqltype)
if converter:
v = converter(v)
row.append(v) row.append(v)
return tuple(row) return tuple(row)
@ -502,20 +515,35 @@ class Cursor(object):
def setoutputsize(self, size, column=None): def setoutputsize(self, size, column=None):
pass pass
def _to_datetime(java_val): def _unknownSqlTypeConverter(rs, col):
d = datetime.datetime.strptime(str(java_val)[:19], "%Y-%m-%d %H:%M:%S") return rs.getObject(col)
if not isinstance(java_val, basestring):
d = d.replace(microsecond=int(str(java_val.getNanos())[:6]))
return str(d)
# return str(java_val)
def _to_date(java_val): def _to_datetime(rs, col):
java_val = rs.getTimestamp(col)
if not java_val:
return
d = datetime.datetime.strptime(str(java_val)[:19], "%Y-%m-%d %H:%M:%S")
d = d.replace(microsecond=int(str(java_val.getNanos())[:6]))
return str(d)
def _to_date(rs, col):
java_val = rs.getDate(col)
if not java_val:
return
d = datetime.datetime.strptime(str(java_val)[:10], "%Y-%m-%d") d = datetime.datetime.strptime(str(java_val)[:10], "%Y-%m-%d")
return d.strftime("%Y-%m-%d") return d.strftime("%Y-%m-%d")
# return str(java_val)
def _to_binary(rs, col):
java_val = rs.getObject(col)
if java_val is None:
return
return str(java_val)
def _java_to_py(java_method): def _java_to_py(java_method):
def to_py(java_val): def to_py(rs, col):
java_val = rs.getObject(col)
if java_val is None:
return
if isinstance(java_val, (basestring, int, long, float, bool)): if isinstance(java_val, (basestring, int, long, float, bool)):
return java_val return java_val
return getattr(java_val, java_method)() return getattr(java_val, java_method)()
@ -525,6 +553,13 @@ _to_double = _java_to_py('doubleValue')
_to_int = _java_to_py('intValue') _to_int = _java_to_py('intValue')
def _init_types(types_map):
global _jdbc_name_to_const
_jdbc_name_to_const = types_map
global _jdbc_const_to_name
_jdbc_const_to_name = dict((y,x) for x,y in types_map.iteritems())
_init_converters(types_map)
def _init_converters(types_map): def _init_converters(types_map):
"""Prepares the converters for conversion of java types to python """Prepares the converters for conversion of java types to python
objects. objects.
@ -541,11 +576,11 @@ _converters = None
_DEFAULT_CONVERTERS = { _DEFAULT_CONVERTERS = {
# see # see
# http://download.oracle.com/javase/1.4.2/docs/api/java/sql/Types.html # http://download.oracle.com/javase/6/docs/api/java/sql/Types.html
# for possible keys # for possible keys
'TIMESTAMP': _to_datetime, 'TIMESTAMP': _to_datetime,
'DATE': _to_date, 'DATE': _to_date,
'BINARY': str, 'BINARY': _to_binary,
'DECIMAL': _to_double, 'DECIMAL': _to_double,
'NUMERIC': _to_double, 'NUMERIC': _to_double,
'DOUBLE': _to_double, 'DOUBLE': _to_double,