diff --git a/README.rst b/README.rst index 52e8f29..2ac8730 100644 --- a/README.rst +++ b/README.rst @@ -133,7 +133,7 @@ Changelog - Free resources after ``executemany`` call. - - Improved type handling. + - Improved type handling. Initial support for BLOB columns. - 0.1.2 diff --git a/README_development.rst b/README_development.rst index a72ff8e..4b4a63a 100644 --- a/README_development.rst +++ b/README_development.rst @@ -17,7 +17,8 @@ Build a new release 3. Assert the right connect method is configured for tests. -4. Run test suite. :: +4. Run test suite. Once for cPython, once for Jython and ideally + against all accessible databases. :: $ nosetests diff --git a/src/jaydebeapi/dbapi2.py b/src/jaydebeapi/dbapi2.py index d328318..7edaba0 100644 --- a/src/jaydebeapi/dbapi2.py +++ b/src/jaydebeapi/dbapi2.py @@ -25,6 +25,8 @@ import sys _jdbc_connect = None +_java_array_byte = None + def _jdbc_connect_jython(jclassname, *args): if _converters is None: from java.sql import Types @@ -37,14 +39,17 @@ def _jdbc_connect_jython(jclassname, *args): _init_converters(types_map) # register driver for DriverManager __import__(jclassname) + global _java_array_byte + if _java_array_byte is None: + import jarray + def _java_array_byte(data): + return jarray.array(data, 'b') from java.sql import DriverManager return DriverManager.getConnection(*args) def _prepare_jython(): global _jdbc_connect _jdbc_connect = _jdbc_connect_jython - # TODO: find solution for jython - # Binary = buffer def _jdbc_connect_jpype(jclassname, *args): import jpype @@ -58,6 +63,10 @@ def _jdbc_connect_jpype(jclassname, *args): for i in types.__javaclass__.getClassFields(): types_map[i.getName()] = i.getStaticAttribute() _init_converters(types_map) + global _java_array_byte + if _java_array_byte is None: + def _java_array_byte(data): + return jpype.JArray(jpype.JByte, 1)(data) # register driver for DriverManager jpype.JClass(jclassname) return jpype.java.sql.DriverManager.getConnection(*args) @@ -65,9 +74,6 @@ def _jdbc_connect_jpype(jclassname, *args): def _prepare_jpype(): global _jdbc_connect _jdbc_connect = _jdbc_connect_jpype - # TODO: doesn't work for Jython -# global Binary -# Binary = buffer if sys.platform.lower().startswith('java'): _prepare_jython() @@ -143,11 +149,22 @@ class NotSupportedError(DatabaseError): pass # DB-API 2.0 Type Objects and Constructors -Date = datetime.date -Time = datetime.time +def _java_sql_blob(data): + return _java_array_byte(data) -Timestamp = datetime.datetime +Binary = _java_sql_blob + +def _str_func(func): + def to_str(*parms): + return str(func(*parms)) + return to_str + +Date = _str_func(datetime.date) + +Time = _str_func(datetime.time) + +Timestamp = _str_func(datetime.datetime) def DateFromTicks(ticks): return apply(Date, time.localtime(ticks)[:3]) @@ -235,10 +252,11 @@ class Cursor(object): # TODO: this is a possible way to close the open result sets # but I'm not sure when __del__ will be called - #__del__ = _close_last - + __del__ = _close_last + def _set_stmt_parms(self, prep_stmt, parameters): for i in range(len(parameters)): + # print (i, parameters[i], type(parameters[i])) prep_stmt.setObject(i + 1, parameters[i]) def execute(self, operation, parameters=None): @@ -272,6 +290,7 @@ class Cursor(object): row = [] for col in range(1, self._meta.getColumnCount() + 1): sqltype = self._meta.getColumnType(col) + # print sqltype v = self._rs.getObject(col) if v: converter = _converters.get(sqltype) @@ -319,21 +338,31 @@ class Cursor(object): def setoutputsize(self, size, column): pass -def to_datetime(java_val): - #d=datetime.datetime.strptime(str(java_val)[:-7], "%Y-%m-%d %H:%M:%S") - #return d.replace(microsecond=int(str(java_val.getNanos())[:6])) +def _to_datetime(java_val): + d = datetime.datetime.strptime(str(java_val)[:19], "%Y-%m-%d %H:%M:%S") + 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): + d = datetime.datetime.strptime(str(java_val)[:10], "%Y-%m-%d") + return d.strftime("%Y-%m-%d") + # return str(java_val) + +def _to_string(java_val): return str(java_val) -def to_date(java_val): - #d=datetime.datetime.strptime(str(java_val)[:-7], "%Y-%m-%d %H:%M:%S") - #return d.replace(microsecond=int(str(java_val.getNanos())[:6])) - return str(java_val) +def _java_to_py(java_method): + def to_py(java_val): + if isinstance(java_val, (basestring, int, long, float, bool)): + return java_val + return getattr(java_val, java_method)() + return to_py -def to_float(java_val): - return java_val.doubleValue() +_to_double = _java_to_py('doubleValue') -def to_int(java_val): - return java_val.intValue() +_to_int = _java_to_py('intValue') def _init_converters(types_map): """Prepares the converters for conversion of java types to python @@ -353,9 +382,14 @@ _DEFAULT_CONVERTERS = { # see # http://download.oracle.com/javase/1.4.2/docs/api/java/sql/Types.html # for possible keys - 'TIMESTAMP': to_datetime, - 'DATE': to_date, - 'DECIMAL': to_float, - 'NUMERIC': to_float, - 'INTEGER': to_int, + 'TIMESTAMP': _to_datetime, + 'DATE': _to_date, + 'BINARY': _to_string, + 'DECIMAL': _to_double, + 'NUMERIC': _to_double, + 'DOUBLE': _to_double, + 'FLOAT': _to_double, + 'INTEGER': _to_int, + 'SMALLINT': _to_int, + 'BOOLEAN': _java_to_py('booleanValue'), } diff --git a/src/test/data/create.sql b/src/test/data/create.sql index 63b7a17..b372a6f 100644 --- a/src/test/data/create.sql +++ b/src/test/data/create.sql @@ -3,6 +3,11 @@ create table Account ( "ACCOUNT_NO" INTEGER not null, "BALANCE" DECIMAL default 0.0 not null, "BLOCKING" DECIMAL, +"DBL_COL" DOUBLE, +"OPENED_AT" DATE, +"VALID" BOOLEAN, +"PRODUCT_NAME" VARCHAR, +"STUFF" BLOB, primary key ("ACCOUNT_ID") ); diff --git a/src/test/data/insert.sql b/src/test/data/insert.sql index 7783390..f7b15af 100644 --- a/src/test/data/insert.sql +++ b/src/test/data/insert.sql @@ -1,2 +1,2 @@ -insert into ACCOUNT values ('2009-09-10 14:15:22.123456', 18, 12.4, null); -insert into ACCOUNT values ('2009-09-11 14:15:22.123456', 19, 12.9, 1); +insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING) values ('2009-09-10 14:15:22.123456', 18, 12.4, null); +insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING) values ('2009-09-11 14:15:22.123456', 19, 12.9, 1); diff --git a/src/test/integration_test.py b/src/test/integration_test.py index b555430..d36036e 100644 --- a/src/test/integration_test.py +++ b/src/test/integration_test.py @@ -73,13 +73,14 @@ class IntegrationTest(TestCase): msg = "Warinng: Your are not running the tests against JayDeBeApi." print >> sys.stderr, msg import sqlite3 - return sqlite3.connect(':memory:') + return sqlite3, sqlite3.connect(':memory:') def connect(self): jar_names = [ 'sqlitejdbc-v056.jar', 'hsqldb.jar', 'sqlite.jar' ] jars = [ path.join(jar_dir, i) for i in jar_names ] if is_jython(): sys.path.extend(jars) + # print "CLASSPATH=%s" % path.pathsep.join(jars) else: self.setup_jpype(jars, [jar_dir]) # http://www.zentus.com/sqlitejdbc/ @@ -96,10 +97,10 @@ class IntegrationTest(TestCase): # crap as it returns decimal values as VARCHAR type # conn = jaydebeapi.connect('SQLite.JDBCDriver', # 'jdbc:sqlite:/:memory:') - return conn + return jaydebeapi, conn def setUp(self): - self.conn = self.connect() + (self.dbapi, self.conn) = self.connect() self.sql_file(create_sql) self.sql_file(insert_sql) @@ -116,20 +117,23 @@ class IntegrationTest(TestCase): def test_execute_and_fetch(self): cursor = self.conn.cursor() - cursor.execute("select * from ACCOUNT") + cursor.execute("select ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING " \ + "from ACCOUNT") result = cursor.fetchall() assert [(u'2009-09-10 14:15:22.123456', 18, 12.4, None), (u'2009-09-11 14:15:22.123456', 19, 12.9, 1)] == result def test_execute_and_fetch_parameter(self): cursor = self.conn.cursor() - cursor.execute("select * from ACCOUNT where ACCOUNT_NO = ?", (18,)) + cursor.execute("select ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING " \ + "from ACCOUNT where ACCOUNT_NO = ?", (18,)) result = cursor.fetchall() assert [(u'2009-09-10 14:15:22.123456', 18, 12.4, None)] == result def test_execute_and_fetchone(self): cursor = self.conn.cursor() - cursor.execute("select * from ACCOUNT order by ACCOUNT_NO") + cursor.execute("select ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING " \ + "from ACCOUNT order by ACCOUNT_NO") result = cursor.fetchone() assert (u'2009-09-10 14:15:22.123456', 18, 12.4, None) == result cursor.close() @@ -154,17 +158,19 @@ class IntegrationTest(TestCase): def test_execute_and_fetchmany(self): cursor = self.conn.cursor() - cursor.execute("select * from ACCOUNT order by ACCOUNT_NO") + cursor.execute("select ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING " \ + "from ACCOUNT order by ACCOUNT_NO") result = cursor.fetchmany() assert [(u'2009-09-10 14:15:22.123456', 18, 12.4, None)] == result # TODO: find out why this cursor has to be closed in order to - # let this test work with sqlite -# cursor.close() + # let this test work with sqlite if __del__ is not overridden + # in cursor + # cursor.close() def test_executemany(self): cursor = self.conn.cursor() - stmt = "insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE)" \ - " values (?, ?, ?)" + stmt = "insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE) " \ + "values (?, ?, ?)" parms = ( ( '2009-09-11 14:15:22.123450', 20, 13.1 ), ( '2009-09-11 14:15:22.123451', 21, 13.2 ), @@ -172,3 +178,46 @@ class IntegrationTest(TestCase): ) cursor.executemany(stmt, parms) assert cursor.rowcount == 3 + + def test_execute_types(self): + cursor = self.conn.cursor() + stmt = "insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE, " \ + "BLOCKING, DBL_COL, OPENED_AT, VALID, PRODUCT_NAME) " \ + "values (?, ?, ?, ?, ?, ?, ?, ?)" + d = self.dbapi + account_id = d.Timestamp(2010, 01, 26, 14, 31, 59) + account_no = 20 + balance = 1.2 + blocking = 10.0 + dbl_col = 3.5 + opened_at = d.Date(2008, 02, 27) + valid = 1 + product_name = u'Savings account' + parms = (account_id, account_no, balance, blocking, dbl_col, + opened_at, valid, product_name) + cursor.execute(stmt, parms) + stmt = "select ACCOUNT_ID, ACCOUNT_NO, BALANCE, BLOCKING, " \ + "DBL_COL, OPENED_AT, VALID, PRODUCT_NAME " \ + "from ACCOUNT where ACCOUNT_NO = ?" + parms = (20, ) + cursor.execute(stmt, parms) + result = cursor.fetchone() + cursor.close() + exp = ( '2010-01-26 14:31:59', account_no, balance, blocking, + dbl_col, '2008-02-27', valid, product_name ) + assert exp == result + + def test_execute_type_blob(self): + cursor = self.conn.cursor() + stmt = "insert into ACCOUNT (ACCOUNT_ID, ACCOUNT_NO, BALANCE, " \ + "STUFF) values (?, ?, ?, ?)" + stuff = self.dbapi.Binary('abcdef') + parms = ('2009-09-11 14:15:22.123450', 20, 13.1, stuff) + cursor.execute(stmt, parms) + stmt = "select STUFF from ACCOUNT where ACCOUNT_NO = ?" + parms = (20, ) + cursor.execute(stmt, parms) + result = cursor.fetchone() + cursor.close() + value = result[0] + assert 'abcdef' == value