├── setup.cfg ├── .gitignore ├── MANIFEST.in ├── LICENSE ├── scripts ├── test.py ├── runtests.py ├── driver.py └── dbapi20.py ├── setup.py ├── README.rst └── sqlanydb.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for python build 2 | build 3 | dist 4 | sqlanydb.egg-info 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst 2 | recursive-include scripts *.py 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/test.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved. 3 | # *************************************************************************** 4 | # This sample code is provided AS IS, without warranty or liability 5 | # of any kind. 6 | # 7 | # You may use, reproduce, modify and distribute this sample code 8 | # without limitation, on the condition that you retain the foregoing 9 | # copyright notice and disclaimer as to the original code. 10 | # 11 | # *************************************************************************** 12 | import sqlanydb 13 | try: input = raw_input 14 | except NameError: pass 15 | myuid = input("Enter your user ID: ") 16 | mypwd = input("Enter your password: ") 17 | con = sqlanydb.connect(userid=myuid, pwd=mypwd) 18 | cur = con.cursor() 19 | cur.execute('select count(*) from Employees') 20 | assert cur.fetchone()[0] > 0 21 | con.close() 22 | print('sqlanydb successfully installed.') 23 | -------------------------------------------------------------------------------- /scripts/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # *************************************************************************** 3 | # Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved. 4 | # *************************************************************************** 5 | # This sample code is provided AS IS, without warranty or liability 6 | # of any kind. 7 | # 8 | # You may use, reproduce, modify and distribute this sample code 9 | # without limitation, on the condition that you retain the foregoing 10 | # copyright notice and disclaimer as to the original code. 11 | # *************************************************************************** 12 | # This sample program contains a hard-coded userid and password 13 | # to connect to the demo database. This is done to simplify the 14 | # sample program. The use of hard-coded passwords is strongly 15 | # discouraged in production code. A best practice for production 16 | # code would be to prompt the user for the userid and password. 17 | # *************************************************************************** 18 | import sys 19 | import sqlanydb 20 | import dbapi20 21 | import unittest 22 | 23 | class test_sqlanydb(dbapi20.DatabaseAPI20Test): 24 | driver = sqlanydb 25 | connect_args = () 26 | connect_kw_args = dict(userid='DBA', password='sqlanydb_pw') 27 | 28 | def test_setoutputsize(self): pass 29 | def test_setoutputsize_basic(self): pass 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | print('''Done''') 34 | -------------------------------------------------------------------------------- /scripts/driver.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved. 3 | # *************************************************************************** 4 | # This sample program contains a hard-coded userid and password 5 | # to connect to the demo database. This is done to simplify the 6 | # sample program. The use of hard-coded passwords is strongly 7 | # discouraged in production code. A best practice for production 8 | # code would be to prompt the user for the userid and password. 9 | # *************************************************************************** 10 | import sqlanydb 11 | import os, unittest 12 | from runtests import test_sqlanydb 13 | 14 | dbname = 'test' 15 | 16 | def removedb(name): 17 | fname = name if name[-3:] == '.db' else name + '.db' 18 | if os.path.exists(fname): 19 | ret = os.system('dberase -y %s' % fname) 20 | if ret != 0: 21 | raise Exception('dberase failed (%d)' % ret) 22 | 23 | def cleandb(name): 24 | removedb(name) 25 | ret = os.system('dbinit %s -dba dba,sqlanydb_pw' % name) 26 | if ret != 0: 27 | raise Exception('dbinit failed (%d)' % ret) 28 | 29 | def stopdb(name): 30 | ret = os.system('dbstop -y -c "uid=dba;pwd=sqlanydb_pw"') 31 | if ret != 0: 32 | raise Exception('dbstop failed (%d)' % ret) 33 | 34 | if __name__ == '__main__': 35 | cleandb(dbname) 36 | # Auto-start engine 37 | c = sqlanydb.connect(uid='dba', pwd='sqlanydb_pw', dbf=dbname) 38 | results = open('summary.out', 'w+') 39 | unittest.main(testRunner=unittest.TextTestRunner(results)) 40 | results.close() 41 | # to cover bug272737, don't call c.close() to explicitly close the connection with 42 | # the driver that does not have the fix, the test will crash with the error like 43 | # Exception WindowsError: 'exception: access violation writing 0x0000000000000024' in 44 | # > ignored 45 | # with the new one, the driver will close all outstanding connections before exit. 46 | # c.close() 47 | 48 | stopdb(dbname) 49 | removedb(dbname) 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # *************************************************************************** 3 | # Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. 4 | # *************************************************************************** 5 | 6 | r"""sqlanydb - pure Python SQL Anywhere database interface. 7 | 8 | sqlanydb lets one access and manipulate SQL Anywhere databases 9 | in pure Python. 10 | 11 | https://github.com/sqlanywhere/sqlanydb 12 | 13 | ----------------------------------------------------------------""" 14 | 15 | from setuptools import setup, find_packages 16 | import os,re 17 | 18 | with open( os.path.join( os.path.dirname(__file__), 'sqlanydb.py' ) ) as v: 19 | VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1) 20 | 21 | setup(name='sqlanydb', 22 | version=VERSION, 23 | description='pure Python SQL Anywhere database interface', 24 | long_description=open('README.rst').read(), 25 | author='Dan Cummins', 26 | author_email='sqlany_interfaces@sap.com', 27 | url='https://github.com/sqlanywhere/sqlanydb', 28 | packages=find_packages(), 29 | py_modules=['sqlanydb'], 30 | license='Apache 2.0', 31 | classifiers=[ 32 | 'Development Status :: 5 - Production/Stable', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: Apache Software License', 35 | 'Natural Language :: English', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.4', 39 | 'Programming Language :: Python :: 2.5', 40 | 'Programming Language :: Python :: 2.6', 41 | 'Programming Language :: Python :: 2.7', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.0', 44 | 'Programming Language :: Python :: 3.1', 45 | 'Programming Language :: Python :: 3.2', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: 3.6', 50 | 'Programming Language :: Python :: 3.7', 51 | 'Programming Language :: Python :: 3.8', 52 | 'Programming Language :: Python :: 3.9', 53 | 'Topic :: Database', 54 | 'Topic :: Software Development :: Libraries :: Python Modules' 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. *************************************************************************** 2 | .. Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. 3 | .. *************************************************************************** 4 | 5 | sqlanydb 6 | ======== 7 | 8 | This package provides a python interface to the SQL Anywhere database 9 | server. This interface conforms to PEP 249. 10 | 11 | Requirements 12 | ------------ 13 | Before installing the sqlanydb interface please make sure the 14 | following components are installed on your system. 15 | 16 | * Python 2.4 or greater (including Python 3.x) 17 | * Python ctypes module if missing 18 | * SQL Anywhere 10 or higher 19 | 20 | Installing the sqlanydb module 21 | ------------------------------ 22 | Run the following command as an administrative user to install 23 | sqlanydb:: 24 | 25 | python setup.py install 26 | 27 | Alternatively, you can use pip:: 28 | 29 | pip install sqlanydb 30 | 31 | Converter Functions 32 | ------------------- 33 | This library wraps around the sqlanydb ``dbcapi`` C library. When retrieving 34 | values from the database, the C API returns one of these types: 35 | 36 | * A_INVALID_TYPE 37 | * A_BINARY 38 | * A_STRING 39 | * A_DOUBLE 40 | * A_VAL64 41 | * A_UVAL64 42 | * A_VAL32 43 | * A_UVAL32 44 | * A_VAL16 45 | * A_UVAL16 46 | * A_VAL8 47 | * A_UVAL8 48 | 49 | Other types are returned as the above types. For example, the NUMERIC type is 50 | returned as a string. 51 | 52 | To have ``sqlanydb`` return a different or custom python object, you can register 53 | callbacks with the ``sqlanydb`` module, using 54 | ``register_converter(datatype, callback)``. Callback is a function that takes 55 | one argument, the type to be converted, and should return the converted value. 56 | Datatype is one of the ``DT_`` variables present in the module. 57 | 58 | The types available to register a converter for: 59 | 60 | * DT_NOTYPE 61 | * DT_DATE 62 | * DT_TIME 63 | * DT_TIMESTAMP 64 | * DT_DATETIMEX 65 | * DT_VARCHAR 66 | * DT_FIXCHAR 67 | * DT_LONGVARCHAR 68 | * DT_STRING 69 | * DT_DOUBLE 70 | * DT_FLOAT 71 | * DT_DECIMAL 72 | * DT_INT 73 | * DT_SMALLINT 74 | * DT_BINARY 75 | * DT_LONGBINARY 76 | * DT_TINYINT 77 | * DT_BIGINT 78 | * DT_UNSINT 79 | * DT_UNSSMALLINT 80 | * DT_UNSBIGINT 81 | * DT_BIT 82 | * DT_LONGNVARCHAR 83 | 84 | For example, to have NUMERIC types be returned as a python Decimal object:: 85 | 86 | 87 | import decimal 88 | 89 | def decimal_callback(valueToConvert): 90 | return decimal.Decimal(valueToConvert) 91 | 92 | sqlanydb.register_converter(sqlanydb.DT_DECIMAL, decimal_callback) 93 | 94 | 95 | Testing the sqlanydb module 96 | --------------------------- 97 | To test that the Python interface to SQL Anywhere is working correctly 98 | first start the demo database included with your SQL Anywhere 99 | installation and then create a file named test_sqlany.py with the 100 | following contents:: 101 | 102 | import sqlanydb 103 | conn = sqlanydb.connect(uid='dba', pwd='sql', eng='demo', dbn='demo' ) 104 | curs = conn.cursor() 105 | curs.execute("select 'Hello, world!'") 106 | print( "SQL Anywhere says: %s" % curs.fetchone() ) 107 | curs.close() 108 | conn.close() 109 | 110 | Run the test script and ensure that you get the expected output:: 111 | 112 | > python test_sqlany.py 113 | SQL Anywhere says: Hello, world! 114 | 115 | License 116 | ------- 117 | This package is licensed under the terms of the Apache License, Version 2.0. See 118 | the LICENSE file for details. 119 | 120 | Feedback and Questions 121 | ---------------------- 122 | For feedback on this project, or for general questions about using SQL Anywhere 123 | please use the SQL Anywhere Forum at http://sqlanywhere-forum.sap.com/ 124 | -------------------------------------------------------------------------------- /sqlanydb.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE or an SAP affiliate company. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # While not a requirement of the license, if you do modify this file, we 17 | # would appreciate hearing about it. Please email sqlany_interfaces@sap.com 18 | 19 | 20 | """SQLAnydb - A DB API v2.0 compatible interface to SQL Anywhere. 21 | 22 | This package provides a DB API v2.0 interface 23 | http://www.python.org/dev/peps/pep-0249 24 | to the sqlanywhere dbcapi library. 25 | 26 | """ 27 | 28 | __version__ = '1.0.14' 29 | 30 | import os 31 | import sys 32 | import atexit 33 | import time 34 | import logging 35 | import struct 36 | 37 | 38 | 39 | try: 40 | import exceptions 41 | # pre 3.0 42 | Exception = exceptions.StandardError 43 | bytes = str 44 | str = unicode 45 | v3list = lambda x: x 46 | except: 47 | # 3.0 or later 48 | xrange = range 49 | v3list = list 50 | import codecs 51 | from ctypes import * 52 | from struct import pack, unpack, calcsize 53 | 54 | lg = logging.getLogger(__name__) 55 | 56 | API_VERSION = 1 57 | API_VERSION_EX = 2 58 | 59 | # NB: The following must match those in sacapi.h for the specified API_VERSION! 60 | 61 | A_INVALID_TYPE = 0 62 | A_BINARY = 1 63 | A_STRING = 2 64 | A_DOUBLE = 3 65 | A_VAL64 = 4 66 | A_UVAL64 = 5 67 | A_VAL32 = 6 68 | A_UVAL32 = 7 69 | A_VAL16 = 8 70 | A_UVAL16 = 9 71 | A_VAL8 = 10 72 | A_UVAL8 = 11 73 | 74 | DT_NOTYPE = 0 75 | DT_DATE = 384 76 | DT_TIME = 388 77 | DT_TIMESTAMP = 392 78 | DT_DATETIMEX = 396 79 | DT_VARCHAR = 448 80 | DT_FIXCHAR = 452 81 | DT_LONGVARCHAR = 456 82 | DT_STRING = 460 83 | DT_DOUBLE = 480 84 | DT_FLOAT = 482 85 | DT_DECIMAL = 484 86 | DT_INT = 496 87 | DT_SMALLINT = 500 88 | DT_BINARY = 524 89 | DT_LONGBINARY = 528 90 | DT_TINYINT = 604 91 | DT_BIGINT = 608 92 | DT_UNSINT = 612 93 | DT_UNSSMALLINT = 616 94 | DT_UNSBIGINT = 620 95 | DT_BIT = 624 96 | DT_LONGNVARCHAR = 640 97 | 98 | DD_INVALID = 0x0 99 | DD_INPUT = 0x1 100 | DD_OUTPUT = 0x2 101 | DD_INPUT_OUTPUT = 0x3 102 | 103 | class DataValue(Structure): 104 | """Must match a_sqlany_data_value.""" 105 | 106 | _fields_ = [("buffer", POINTER(c_char)), 107 | ("buffer_size", c_size_t), 108 | ("length", POINTER(c_size_t)), 109 | ("type", c_int), 110 | ("is_null", POINTER(c_int))] 111 | 112 | 113 | class BindParam(Structure): 114 | """Must match a_sqlany_bind_param.""" 115 | 116 | _fields_ = [("direction", c_int), 117 | ("value", DataValue), 118 | ("name", c_char_p)] 119 | 120 | 121 | class ColumnInfo(Structure): 122 | """Must match a_sqlany_column_info.""" 123 | 124 | _fields_ = [("name", c_char_p), 125 | ("type", c_int), 126 | ("native_type", c_int), 127 | ("precision", c_short), 128 | ("scale", c_short), 129 | ("max_size", c_size_t), 130 | ("nullable", c_int32)] 131 | 132 | 133 | class DataInfo(Structure): 134 | """Must match a_sqlany_data_info.""" 135 | 136 | _fields_ = [("index", c_int), 137 | ("type", c_int), 138 | ("is_null", c_int), 139 | ("data_size", c_size_t)] 140 | 141 | def init_sacapi(api): 142 | sacapi_i32 = c_int32 143 | sacapi_bool = sacapi_i32 144 | sacapi_u32 = c_uint32 145 | p_sacapi_u32 = POINTER(sacapi_u32) 146 | p_sqlany_interface_context = c_void_p 147 | p_sqlany_connection = c_void_p 148 | p_sqlany_stmt = c_void_p 149 | p_sqlany_bind_param = c_void_p 150 | p_sqlany_bind_param_info = c_void_p 151 | p_sqlany_data_value = c_void_p 152 | p_sqlany_data_info = c_void_p 153 | p_sqlany_column_info = c_void_p 154 | 155 | def defun(name, *types): 156 | try: 157 | setattr(api, name, CFUNCTYPE(*types)((name, api),)) 158 | except: 159 | pass 160 | 161 | defun("sqlany_init", 162 | sacapi_bool, c_char_p, sacapi_u32, p_sacapi_u32) 163 | defun("sqlany_init_ex", 164 | p_sqlany_interface_context, c_char_p, sacapi_u32, p_sacapi_u32) 165 | defun("sqlany_fini", 166 | None) 167 | defun("sqlany_fini_ex", 168 | None, p_sqlany_interface_context) 169 | defun("sqlany_new_connection", 170 | p_sqlany_connection) 171 | defun("sqlany_new_connection_ex", 172 | p_sqlany_connection, p_sqlany_interface_context) 173 | defun("sqlany_free_connection", 174 | None, p_sqlany_connection) 175 | defun("sqlany_make_connection", 176 | p_sqlany_connection, c_void_p) 177 | defun("sqlany_make_connection_ex", 178 | p_sqlany_connection, p_sqlany_interface_context, c_void_p) 179 | defun("sqlany_connect", 180 | sacapi_bool, p_sqlany_connection, c_char_p) 181 | defun("sqlany_disconnect", 182 | sacapi_bool, p_sqlany_connection) 183 | defun("sqlany_cancel", 184 | None, p_sqlany_connection) 185 | defun("sqlany_execute_immediate", 186 | sacapi_bool, p_sqlany_connection, c_char_p) 187 | defun("sqlany_prepare", 188 | p_sqlany_stmt, p_sqlany_connection, c_char_p) 189 | defun("sqlany_free_stmt", 190 | None, p_sqlany_stmt) 191 | defun("sqlany_num_params", 192 | sacapi_i32, p_sqlany_stmt) 193 | defun("sqlany_describe_bind_param", 194 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_bind_param) 195 | defun("sqlany_bind_param", 196 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_bind_param) 197 | defun("sqlany_send_param_data", 198 | sacapi_bool, p_sqlany_stmt, sacapi_u32, c_void_p, c_size_t) 199 | defun("sqlany_reset", 200 | sacapi_bool, p_sqlany_stmt) 201 | defun("sqlany_get_bind_param_info", 202 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_bind_param_info) 203 | defun("sqlany_execute", 204 | sacapi_bool, p_sqlany_stmt) 205 | defun("sqlany_execute_direct", 206 | p_sqlany_stmt, p_sqlany_connection, c_char_p) 207 | defun("sqlany_fetch_absolute", 208 | sacapi_bool, p_sqlany_stmt, sacapi_i32) 209 | defun("sqlany_fetch_next", 210 | sacapi_bool, p_sqlany_stmt) 211 | defun("sqlany_get_next_result", 212 | sacapi_bool, p_sqlany_stmt) 213 | defun("sqlany_affected_rows", 214 | sacapi_i32, p_sqlany_stmt) 215 | defun("sqlany_num_cols", 216 | sacapi_i32, p_sqlany_stmt) 217 | defun("sqlany_num_rows", 218 | sacapi_i32, p_sqlany_stmt) 219 | defun("sqlany_get_column", 220 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_data_value) 221 | defun("sqlany_get_data", 222 | sacapi_i32, p_sqlany_stmt, sacapi_u32, c_size_t, c_void_p, c_size_t) 223 | defun("sqlany_get_data_info", 224 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_data_info) 225 | defun("sqlany_get_column_info", 226 | sacapi_bool, p_sqlany_stmt, sacapi_u32, p_sqlany_column_info) 227 | defun("sqlany_commit", 228 | sacapi_bool, p_sqlany_connection) 229 | defun("sqlany_rollback", 230 | sacapi_bool, p_sqlany_connection) 231 | defun("sqlany_client_version", 232 | sacapi_bool, c_void_p, c_size_t) 233 | defun("sqlany_client_version_ex", 234 | sacapi_bool, p_sqlany_interface_context, c_void_p, c_size_t) 235 | defun("sqlany_error", 236 | sacapi_i32, p_sqlany_connection, c_void_p, c_size_t) 237 | defun("sqlany_sqlstate", 238 | c_size_t, p_sqlany_connection, c_void_p, c_size_t) 239 | defun("sqlany_clear_error", 240 | None, p_sqlany_connection) 241 | return api 242 | 243 | 244 | # NB: The preceding must match those in sacapi.h for the specified API_VERSION! 245 | 246 | 247 | class DBAPISet(frozenset): 248 | 249 | """A special type of set for which A == x is true if A is a 250 | DBAPISet and x is a member of that set.""" 251 | 252 | def __eq__(self, other): 253 | if isinstance(other, DBAPISet): 254 | return frozenset.__eq__(self, other) 255 | else: 256 | return other in self 257 | 258 | def __ne__(self, other): 259 | return not self == other 260 | 261 | def __hash__(self): 262 | return frozenset.__hash__(self) 263 | 264 | STRING = DBAPISet([DT_VARCHAR, 265 | DT_FIXCHAR, 266 | DT_LONGVARCHAR, 267 | DT_STRING, 268 | DT_LONGNVARCHAR]) 269 | BINARY = DBAPISet([DT_BINARY, 270 | DT_LONGBINARY]) 271 | NUMBER = DBAPISet([DT_DOUBLE, 272 | DT_FLOAT, 273 | DT_DECIMAL, 274 | DT_INT, 275 | DT_SMALLINT, 276 | DT_TINYINT]) 277 | DATE = DBAPISet([DT_DATE]) 278 | TIME = DBAPISet([DT_TIME]) 279 | TIMESTAMP = DBAPISet([DT_TIMESTAMP, 280 | DT_DATETIMEX]) 281 | DATETIME = TIMESTAMP 282 | ROWID = DBAPISet() 283 | 284 | ToPyType = {DT_DATE : DATE, 285 | DT_TIME : TIME, 286 | DT_TIMESTAMP : TIMESTAMP, 287 | DT_DATETIMEX : TIMESTAMP, 288 | DT_VARCHAR : STRING, 289 | DT_FIXCHAR : STRING, 290 | DT_LONGVARCHAR : STRING, 291 | DT_STRING : STRING, 292 | DT_DOUBLE : NUMBER, 293 | DT_FLOAT : NUMBER, 294 | DT_DECIMAL : NUMBER, 295 | DT_INT : NUMBER, 296 | DT_SMALLINT : NUMBER, 297 | DT_BINARY : BINARY, 298 | DT_LONGBINARY : BINARY, 299 | DT_TINYINT : NUMBER, 300 | DT_BIGINT : NUMBER, 301 | DT_UNSINT : NUMBER, 302 | DT_UNSSMALLINT : NUMBER, 303 | DT_UNSBIGINT : NUMBER, 304 | DT_BIT : NUMBER, 305 | DT_LONGNVARCHAR : STRING} 306 | 307 | 308 | class Error(Exception): 309 | def __init__(self,err,sqlcode=0): 310 | self._errortext = err 311 | self._errorcode = sqlcode 312 | @property 313 | def errortext(self): return self._errortext 314 | @property 315 | def errorcode(self): return self._errorcode 316 | def __repr__(self): 317 | return "%s(%s, %s)" % (self.__class__.__name__, repr(self.errortext), 318 | repr(self.errorcode)) 319 | def __str__(self): 320 | return repr((self.errortext, self.errorcode)) 321 | 322 | class Warning(Exception): 323 | """Raise for important warnings like data truncation while inserting.""" 324 | def __init__(self,err,sqlcode=0): 325 | self._errortext = err 326 | self._errorcode = sqlcode 327 | @property 328 | def errortext(self): return self._errortext 329 | @property 330 | def errorcode(self): return self._errorcode 331 | def __repr__(self): 332 | return "%s(%s, %s)" % (self.__class__.__name__, repr(self.errortext), 333 | repr(self.errorcode)) 334 | def __str__(self): 335 | return repr((self.errortext, self.errorcode)) 336 | 337 | class InterfaceError(Error): 338 | """Raise for interface, not database, related errors.""" 339 | def __init__(self, *args): 340 | super(InterfaceError,self).__init__(*args) 341 | 342 | class DatabaseError(Error): 343 | def __init__(self, *args): 344 | super(DatabaseError,self).__init__(*args) 345 | 346 | class InternalError(DatabaseError): 347 | """Raise for internal errors: cursor not valid, etc.""" 348 | def __init__(self, *args): 349 | super(InternalError,self).__init__(*args) 350 | 351 | class OperationalError(DatabaseError): 352 | """Raise for database related errors, not under programmer's control: 353 | unexpected disconnect, memory allocation error, etc.""" 354 | def __init__(self, *args): 355 | super(OperationalError,self).__init__(*args) 356 | 357 | class ProgrammingError(DatabaseError): 358 | """Raise for programming errors: table not found, incorrect syntax, etc.""" 359 | def __init__(self, *args): 360 | super(ProgrammingError,self).__init__(*args) 361 | 362 | class IntegrityError(DatabaseError): 363 | """Raise for database constraint failures: missing primary key, etc.""" 364 | def __init__(self, *args): 365 | super(IntegrityError,self).__init__(*args) 366 | 367 | class DataError(DatabaseError): 368 | def __init__(self, *args): 369 | super(DataError,self).__init__(*args) 370 | 371 | class NotSupportedError(DatabaseError): 372 | """Raise for methods or APIs not supported by database.""" 373 | def __init__(self, *args): 374 | super(NotSupportedError,self).__init__(*args) 375 | 376 | def standardErrorHandler(connection, cursor, errorclass, errorvalue, sqlcode=0): 377 | error=(errorclass, errorvalue) 378 | if connection: 379 | connection.messages.append(error) 380 | if cursor: 381 | cursor.messages.append(error) 382 | if errorclass != Warning: 383 | raise errorclass(errorvalue,sqlcode) 384 | 385 | 386 | # format types indexed by A_* values 387 | format = 'xxxdqQiIhHbB' 388 | 389 | def mk_valueof(raw, char_set): 390 | def valueof(data): 391 | if data.is_null.contents: 392 | return None 393 | elif data.type in raw: 394 | return data.buffer[:data.length.contents.value] 395 | elif data.type in (A_STRING,): 396 | return data.buffer[:data.length.contents.value].decode(char_set) 397 | else: 398 | fmt = format[data.type] 399 | return unpack(fmt, data.buffer[:calcsize(fmt)])[0] 400 | return valueof 401 | 402 | 403 | def mk_assign(char_set): 404 | def assign(param, value): 405 | is_null = value is None 406 | param.value.is_null = pointer(c_int(is_null)) 407 | if is_null and param.direction == DD_INPUT: 408 | value = 0 409 | if param.value.type == A_INVALID_TYPE: 410 | if is_null and param.direction == DD_INPUT: 411 | param.value.type = A_STRING 412 | elif isinstance(value, int): 413 | if value >= 0: 414 | param.value.type = A_UVAL64 415 | else: 416 | param.value.type = A_VAL64 417 | elif isinstance(value, float): 418 | param.value.type = A_DOUBLE 419 | elif isinstance(value, Binary): 420 | param.value.type = A_BINARY 421 | else: 422 | param.value.type = A_STRING 423 | fmt = format[param.value.type] 424 | if fmt == 'x': 425 | if isinstance(value, bytes): 426 | pass 427 | elif isinstance(value, str): 428 | value = value.encode(char_set) 429 | else: 430 | value = str(value).encode(char_set) 431 | size = length = len(value) 432 | if param.direction != DD_INPUT: 433 | if size < param.value.buffer_size: 434 | size = param.value.buffer_size 435 | buffer = create_string_buffer(value) 436 | else: 437 | buffer = create_string_buffer(pack(fmt, value)) 438 | size = length = calcsize(fmt) 439 | param.value.buffer = cast(buffer, POINTER(c_char)) 440 | param.value.buffer_size = c_size_t(size) 441 | param.value.length = pointer(c_size_t(length)) 442 | return assign 443 | 444 | 445 | threadsafety = 1 446 | apilevel = '2.0' 447 | paramstyle = 'qmark' 448 | 449 | __all__ = [ 'threadsafety', 'apilevel', 'paramstyle', 'connect'] 450 | 451 | def find_dbcapi_path(): 452 | for p in sys.path: 453 | if os.path.isdir(p): 454 | for f in os.listdir(p): 455 | if f.lower() == 'dbcapi.dll': 456 | return p 457 | return None 458 | 459 | def load_library(*names): 460 | if sys.version_info >= (3, 8) and sys.platform == 'win32' and os.getenv('SQLANY_API_DLL') is None: 461 | dbcapi_dir = None 462 | if os.getenv('IQDIR17') is not None or os.getenv('SQLANY17') is not None: 463 | if os.getenv('IQDIR17') is not None: 464 | iqpath = os.getenv('IQDIR17') 465 | else: 466 | iqpath = os.getenv('SQLANY17') 467 | pointersize = struct.calcsize("P") * 8 468 | if pointersize == 64: 469 | dbcapi_dir = os.path.join(iqpath, 'bin64') 470 | else: 471 | dbcapi_dir = os.path.join(iqpath, 'bin32') 472 | else: 473 | dbcapi_dir = find_dbcapi_path() 474 | if dbcapi_dir is None: 475 | raise InterfaceError("Could not find dbcapi.dll. Please append the directory that contains dbcapi.dll to PYTHONPATH") 476 | else: 477 | os.add_dll_directory( dbcapi_dir ) 478 | 479 | for name in names: 480 | if name is None or name == '': 481 | continue 482 | try: 483 | dll = cdll.LoadLibrary(name) 484 | lg.debug("Successfully loaded dbcapi library '%s' with name '%s'", dll, name) 485 | return init_sacapi(dll) 486 | except OSError as ose: 487 | continue 488 | raise InterfaceError("Could not load dbcapi. Tried: " + ','.join(map(str, names))) 489 | 490 | 491 | class Root(object): 492 | connections = [] 493 | def __init__(self, name): 494 | 495 | lg.debug("Attempting to load dbcapi library") 496 | self.api = load_library(os.getenv( 'SQLANY_API_DLL', None ), 'dbcapi.dll', 'libdbcapi_r.so', 497 | 'libdbcapi_r.dylib') 498 | ver = c_uint(0) 499 | try: 500 | self.api.sqlany_init_ex.restype = POINTER(c_int) 501 | lg.debug("Attempting to initalize dbcapi context (self.api.sqlany_init_ex) with arguments:" \ 502 | " app name: '%s', api version: '%s'", 503 | name, API_VERSION_EX) 504 | context = self.api.sqlany_init_ex(name.encode('utf-8'), API_VERSION_EX, byref(ver)) 505 | if not context: 506 | lg.error("Failed to initalize dbcapi context (self.api.sqlany_init_ex returned NULL)," \ 507 | "perhaps you are missing some required sqlanywhere libaries?") 508 | 509 | raise InterfaceError("Failed to initalize dbcapi context, dbcapi version %d required." \ 510 | " Perhaps you are missing some sqlanywhere libaries?" % 511 | API_VERSION_EX) 512 | else: 513 | lg.debug("Initalization of dbcapi context successful, max api version supported: %s", ver) 514 | 515 | def new_connection(): 516 | return self.api.sqlany_new_connection_ex(context) 517 | self.api.sqlany_new_connection = new_connection 518 | def fini(): 519 | self.api.sqlany_fini_ex(context) 520 | self.api.sqlany_fini = fini 521 | def client_version(): 522 | length = 1000 523 | buffer = create_string_buffer(length) 524 | ret = self.api.sqlany_client_version_ex(context, buffer, length) 525 | if ret: 526 | vers = buffer.value 527 | else: 528 | vers = None 529 | return vers 530 | self.api.sqlany_client_version = client_version 531 | except InterfaceError: 532 | raise 533 | except: 534 | if (not self.api.sqlany_init(name.encode('utf-8'), API_VERSION, byref(ver))): 535 | raise InterfaceError("dbcapi version %d required." % 536 | API_VERSION) 537 | self.api.sqlany_new_connection.restype = POINTER(c_int) 538 | # Need to set return type to some pointer type other than void 539 | # to avoid automatic conversion to a (32 bit) int. 540 | self.api.sqlany_prepare.restype = POINTER(c_int) 541 | atexit.register(self.__del__) 542 | 543 | def __del__(self): 544 | 545 | # close all outstanding connections 546 | myconn = self.connections[:] 547 | for conn in myconn: 548 | conn.close() 549 | 550 | # if we fail to load the library, then we won't get a chance 551 | # to even set the 'api' instance variable 552 | if hasattr(self, "api") and self.api: 553 | lg.debug("__del__ called on sqlany.Root object, finalizng dbcapi context") 554 | self.api.sqlany_fini() 555 | self.api = None 556 | 557 | def add_conn(self, conn): 558 | self.connections.append(conn) 559 | 560 | def remove_conn(self, conn): 561 | self.connections.remove(conn); 562 | 563 | 564 | def connect(*args, **kwargs): 565 | """Constructor for creating a connection to a database.""" 566 | return Connection(args, kwargs) 567 | 568 | 569 | class Connection(object): 570 | 571 | # cache the api object so we don't have to load and unload every single time 572 | cls_parent = None 573 | 574 | def __init__(self, args, kwargs, parent = None): 575 | 576 | # make it so we don't load Root() and therefore the 577 | # dbcapi C library just on import 578 | if parent == None: 579 | 580 | # cache the Root() object so we don't load it every time 581 | if Connection.cls_parent == None: 582 | parent = Connection.cls_parent = Root("PYTHON") 583 | else: 584 | parent = Connection.cls_parent 585 | 586 | self.Error = Error 587 | self.Warning = Warning 588 | self.InterfaceError = InterfaceError 589 | self.DatabaseError = DatabaseError 590 | self.InternalError = InternalError 591 | self.OperationalError = OperationalError 592 | self.ProgrammingError = ProgrammingError 593 | self.IntegrityError = IntegrityError 594 | self.DataError = DataError 595 | self.NotSupportedError = NotSupportedError 596 | 597 | self.errorhandler = None 598 | self.messages = [] 599 | 600 | self.cursors = set() 601 | 602 | self.parent, self.api = parent, parent.api 603 | self.c = self.api.sqlany_new_connection(); 604 | params = ';'.join(kw+'='+arg for kw, arg in v3list(list(kwargs.items()))) 605 | char_set = 'utf-8' 606 | if isinstance(params, str): 607 | params = params.encode(char_set) 608 | if self.api.sqlany_connect(self.c, params): 609 | self.valueof = mk_valueof((A_BINARY, A_STRING), char_set) 610 | self.assign = mk_assign(char_set) 611 | self.char_set = char_set 612 | cur = self.cursor() 613 | try: 614 | cur.execute("select connection_property('CharSet')") 615 | char_set = cur.fetchone()[0] 616 | if isinstance(char_set, bytes): 617 | char_set = char_set.decode() 618 | # iso_1 means iso-8859-1 in sql anywhere 619 | # details see: http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.help.sqlanywhere.12.0.1/dbadmin/determining-locale-natlang.html 620 | if(char_set=='iso_1'): 621 | char_set = 'iso-8859-1' 622 | if codecs.lookup(char_set): 623 | self.valueof = mk_valueof((A_BINARY,), char_set) 624 | self.assign = mk_assign(char_set) 625 | self.char_set = char_set 626 | finally: 627 | cur.close() 628 | parent.add_conn(self) 629 | else: 630 | error = self.error() 631 | self.api.sqlany_free_connection(self.c) 632 | self.c = None 633 | self.handleerror(*error) 634 | 635 | def __del__(self): 636 | # if we fail to load the library, then we won't get a chance 637 | # to even set the 'c' instance variable 638 | if hasattr(self, "c") and self.c: 639 | self.close() 640 | 641 | def handleerror(self, errorclass, errorvalue, sqlcode): 642 | if errorclass: 643 | eh = self.errorhandler or standardErrorHandler 644 | eh(self, None, errorclass, errorvalue, sqlcode) 645 | 646 | def con(self): 647 | if not self.c: 648 | self.handleerror(InterfaceError, "not connected", -101) 649 | return self.c 650 | 651 | def commit(self): 652 | self.messages = [] 653 | return self.api.sqlany_commit(self.con()) 654 | 655 | def rollback(self): 656 | self.messages = [] 657 | return self.api.sqlany_rollback(self.con()) 658 | 659 | def cancel(self): 660 | self.messages = [] 661 | try: 662 | return self.api.sqlany_cancel(self.con()) 663 | except AttributeError: 664 | self.handleerror(InterfaceError, "cancel not supported", -1965) 665 | 666 | def mk_error(): 667 | buf = create_string_buffer(256) 668 | buf_size = sizeof(buf) 669 | def error(self): 670 | rc = self.api.sqlany_error(self.con(), buf, buf_size) 671 | if rc == 0: 672 | return (None, None, 0) 673 | elif rc > 0: 674 | return (Warning, buf.value, rc) 675 | elif rc in (-193,-194,-195,-196): 676 | return (IntegrityError, buf.value, rc) 677 | else: 678 | return (OperationalError, buf.value, rc) 679 | return error 680 | 681 | error = mk_error() 682 | 683 | def clear_error(self): 684 | return self.api.sqlany_clear_error(self.con()) 685 | 686 | def close(self): 687 | self.messages = [] 688 | c = self.con() 689 | if self.c != None: 690 | for x in self.cursors: 691 | x.close(remove=False) 692 | self.cursors = None 693 | self.api.sqlany_disconnect(c) 694 | self.api.sqlany_free_connection(c) 695 | self.parent.remove_conn(self) 696 | self.c = None 697 | 698 | def cursor(self): 699 | self.messages = [] 700 | x = Cursor(self) 701 | self.cursors.add(x) 702 | return x 703 | 704 | def __enter__(self): return self.cursor() 705 | 706 | def __exit__(self, exc, value, tb): 707 | if exc: 708 | self.rollback() 709 | else: 710 | self.commit() 711 | 712 | class Cursor(object): 713 | class TypeConverter(object): 714 | def __init__(self,types): 715 | def find_converter(t): 716 | return CONVERSION_CALLBACKS.get(t, lambda x: x) 717 | self.converters = v3list(list(map(find_converter, types))) 718 | 719 | def gen(self,values): 720 | for converter, value in zip(self.converters, values): 721 | yield converter(value) 722 | 723 | def __init__(self, parent): 724 | self.messages = [] 725 | self.parent, self.api = parent, parent.api 726 | self.valueof = self.parent.valueof 727 | self.assign = self.parent.assign 728 | self.char_set = self.parent.char_set 729 | self.errorhandler = self.parent.errorhandler 730 | self.arraysize = 1 731 | self.converter = None 732 | self.rowcount = -1 733 | self.__stmt = None 734 | self.description = None 735 | 736 | def handleerror(self, errorclass, errorvalue, sqlcode): 737 | if errorclass: 738 | eh = self.errorhandler or standardErrorHandler 739 | eh(self.parent, self, errorclass, errorvalue, sqlcode) 740 | 741 | def __stmt_get(self): 742 | if self.__stmt is None: 743 | self.handleerror(InterfaceError, "no statement") 744 | elif not self.__stmt: 745 | self.handleerror(*self.parent.error()) 746 | return self.__stmt 747 | 748 | def __stmt_set(self, value): 749 | self.__stmt = value 750 | 751 | stmt = property(__stmt_get, __stmt_set) 752 | 753 | def __del__(self): 754 | self.close() 755 | 756 | def con(self): 757 | if not self.parent: 758 | self.handleerror(InterfaceError, "not connected", -101) 759 | return self.parent.con() 760 | 761 | def get_stmt(self): 762 | return self.stmt 763 | 764 | def new_statement(self, operation): 765 | self.free_statement() 766 | self.stmt = self.api.sqlany_prepare(self.con(), operation) 767 | 768 | def free_statement(self): 769 | if self.__stmt: 770 | self.api.sqlany_free_stmt(self.stmt) 771 | self.stmt = None 772 | self.description = None 773 | self.converter = None 774 | self.rowcount = -1 775 | 776 | def close(self, remove=True): 777 | p = self.parent 778 | if p: 779 | self.parent = None 780 | if remove: 781 | p.cursors.remove(self) 782 | self.free_statement() 783 | 784 | def columns(self): 785 | info = ColumnInfo() 786 | for i in range(self.api.sqlany_num_cols(self.get_stmt())): 787 | self.api.sqlany_get_column_info(self.get_stmt(), i, byref(info)) 788 | yield ((info.name.decode('utf-8'), 789 | ToPyType[info.native_type], 790 | None, 791 | info.max_size, 792 | info.precision, 793 | info.scale, 794 | info.nullable), 795 | info.native_type) 796 | 797 | def executemany(self, operation, seq_of_parameters): 798 | self.messages = [] 799 | 800 | def bind(k, col): 801 | param = BindParam() 802 | self.api.sqlany_describe_bind_param(self.stmt, k, byref(param)) 803 | (self.assign)(param, col) 804 | self.api.sqlany_bind_param(self.stmt, k, byref(param)) 805 | return param 806 | 807 | try: 808 | if isinstance(operation, str): 809 | operation = operation.encode(self.char_set) 810 | self.new_statement(operation) 811 | bind_count = self.api.sqlany_num_params(self.stmt) 812 | self.rowcount = 0 813 | for parameters in seq_of_parameters: 814 | parms = [bind(k, col) 815 | for k, col in enumerate(parameters[:bind_count])] 816 | if not self.api.sqlany_execute(self.stmt): 817 | self.handleerror(*self.parent.error()) 818 | 819 | try: 820 | self.description, types = v3list(list(zip(*self.columns()))) 821 | rowcount = self.api.sqlany_num_rows(self.stmt) 822 | self.converter = self.TypeConverter(types) 823 | except ValueError: 824 | rowcount = self.api.sqlany_affected_rows(self.stmt) 825 | self.description = None 826 | self.converter = None 827 | 828 | if rowcount < 0: 829 | # Can happen if number of rows is only an estimate 830 | self.rowcount = -1 831 | elif self.rowcount >= 0: 832 | self.rowcount += rowcount 833 | except: 834 | self.rowcount = -1 835 | raise 836 | 837 | return [(self.valueof)(param.value) for param in parms] 838 | 839 | def execute(self, operation, parameters = ()): 840 | self.executemany(operation, [parameters]) 841 | 842 | def callproc(self, procname, parameters = ()): 843 | stmt = 'call '+procname+'('+','.join(len(parameters)*('?',))+')' 844 | return self.executemany(stmt, [parameters]) 845 | 846 | def values(self): 847 | value = DataValue() 848 | for i in range(self.api.sqlany_num_cols(self.get_stmt())): 849 | rc = self.api.sqlany_get_column(self.get_stmt(), i, byref(value)) 850 | if rc < 0: 851 | # print "truncation of column %d"%i 852 | self.handleerror(*self.parent.error()) 853 | yield (self.valueof)(value) 854 | 855 | def rows(self): 856 | if not self.description: 857 | self.handleerror(InterfaceError, "no result set", -872) 858 | 859 | while self.api.sqlany_fetch_next(self.get_stmt()): 860 | self.handleerror(*self.parent.error()) 861 | yield tuple(self.converter.gen(v3list(list(self.values())))) 862 | self.handleerror(*self.parent.error()) 863 | 864 | def fetchmany(self, size=None): 865 | if size is None: 866 | size = self.arraysize 867 | return [row for i,row in zip(range(size), self.rows())] 868 | 869 | def fetchone(self): 870 | rows = self.fetchmany(size=1) 871 | if rows: 872 | return rows[0] 873 | return None 874 | 875 | def fetchall(self): 876 | return list(self.rows()) 877 | 878 | def nextset(self): 879 | self.messages = [] 880 | return self.api.sqlany_get_next_result(self.get_stmt()) or None 881 | 882 | def setinputsizes(self, sizes): 883 | self.messages = [] 884 | pass 885 | 886 | def setoutputsize(self, sizes, column): 887 | self.messages = [] 888 | pass 889 | 890 | 891 | def Date(*ymd): 892 | return "%04d/%02d/%02d"%ymd 893 | 894 | def Time(*hms): 895 | return "%02d:%02d:%02d"%hms 896 | 897 | def Timestamp(*ymdhms): 898 | return "%04d/%02d/%02d %02d:%02d:%02d"%ymdhms 899 | 900 | def DateFromTicks(ticks): 901 | return Date(*time.localtime(ticks)[:3]) 902 | 903 | def TimeFromTicks(ticks): 904 | return Time(*time.localtime(ticks)[3:6]) 905 | 906 | def TimestampFromTicks(ticks): 907 | return Timestamp(*time.localtime(ticks)[:6]) 908 | 909 | class Binary( bytes ): 910 | pass 911 | 912 | CONVERSION_CALLBACKS = {} 913 | def register_converter(datatype, callback): 914 | CONVERSION_CALLBACKS[datatype] = callback 915 | -------------------------------------------------------------------------------- /scripts/dbapi20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # *************************************************************************** 3 | # Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved. 4 | # *************************************************************************** 5 | ''' Python DB API 2.0 driver compliance unit test suite. 6 | 7 | This software is Public Domain and may be used without restrictions. 8 | 9 | "Now we have booze and barflies entering the discussion, plus rumours of 10 | DBAs on drugs... and I won't tell you what flashes through my mind each 11 | time I read the subject line with 'Anal Compliance' in it. All around 12 | this is turning out to be a thoroughly unwholesome unit test." 13 | 14 | -- Ian Bicking 15 | ''' 16 | 17 | __rcs_id__ = '$Id: dbapi20.py 396 2006-02-25 03:44:32Z adustman $' 18 | __version__ = '$Revision: 396 $'[11:-2] 19 | __author__ = 'Stuart Bishop ' 20 | 21 | import unittest 22 | import time 23 | 24 | # $Log$ 25 | # Revision 1.1.2.1 2006/02/25 03:44:32 adustman 26 | # Generic DB-API unit test module 27 | # 28 | # Revision 1.10 2003/10/09 03:14:14 zenzen 29 | # Add test for DB API 2.0 optional extension, where database exceptions 30 | # are exposed as attributes on the Connection object. 31 | # 32 | # Revision 1.9 2003/08/13 01:16:36 zenzen 33 | # Minor tweak from Stefan Fleiter 34 | # 35 | # Revision 1.8 2003/04/10 00:13:25 zenzen 36 | # Changes, as per suggestions by M.-A. Lemburg 37 | # - Add a table prefix, to ensure namespace collisions can always be avoided 38 | # 39 | # Revision 1.7 2003/02/26 23:33:37 zenzen 40 | # Break out DDL into helper functions, as per request by David Rushby 41 | # 42 | # Revision 1.6 2003/02/21 03:04:33 zenzen 43 | # Stuff from Henrik Ekelund: 44 | # added test_None 45 | # added test_nextset & hooks 46 | # 47 | # Revision 1.5 2003/02/17 22:08:43 zenzen 48 | # Implement suggestions and code from Henrik Eklund - test that cursor.arraysize 49 | # defaults to 1 & generic cursor.callproc test added 50 | # 51 | # Revision 1.4 2003/02/15 00:16:33 zenzen 52 | # Changes, as per suggestions and bug reports by M.-A. Lemburg, 53 | # Matthew T. Kromer, Federico Di Gregorio and Daniel Dittmar 54 | # - Class renamed 55 | # - Now a subclass of TestCase, to avoid requiring the driver stub 56 | # to use multiple inheritance 57 | # - Reversed the polarity of buggy test in test_description 58 | # - Test exception heirarchy correctly 59 | # - self.populate is now self._populate(), so if a driver stub 60 | # overrides self.ddl1 this change propogates 61 | # - VARCHAR columns now have a width, which will hopefully make the 62 | # DDL even more portible (this will be reversed if it causes more problems) 63 | # - cursor.rowcount being checked after various execute and fetchXXX methods 64 | # - Check for fetchall and fetchmany returning empty lists after results 65 | # are exhausted (already checking for empty lists if select retrieved 66 | # nothing 67 | # - Fix bugs in test_setoutputsize_basic and test_setinputsizes 68 | # 69 | 70 | class DatabaseAPI20Test(unittest.TestCase): 71 | ''' Test a database self.driver for DB API 2.0 compatibility. 72 | This implementation tests Gadfly, but the TestCase 73 | is structured so that other self.drivers can subclass this 74 | test case to ensure compiliance with the DB-API. It is 75 | expected that this TestCase may be expanded in the future 76 | if ambiguities or edge conditions are discovered. 77 | 78 | The 'Optional Extensions' are not yet being tested. 79 | 80 | self.drivers should subclass this test, overriding setUp, tearDown, 81 | self.driver, connect_args and connect_kw_args. Class specification 82 | should be as follows: 83 | 84 | import dbapi20 85 | class mytest(dbapi20.DatabaseAPI20Test): 86 | [...] 87 | 88 | Don't 'import DatabaseAPI20Test from dbapi20', or you will 89 | confuse the unit tester - just 'import dbapi20'. 90 | ''' 91 | 92 | # The self.driver module. This should be the module where the 'connect' 93 | # method is to be found 94 | driver = None 95 | connect_args = () # List of arguments to pass to connect 96 | connect_kw_args = {} # Keyword arguments for connect 97 | table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables 98 | 99 | ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix 100 | ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix 101 | xddl1 = 'drop table %sbooze' % table_prefix 102 | xddl2 = 'drop table %sbarflys' % table_prefix 103 | 104 | # lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase 105 | lowerfunc = None # not supported out of the box 106 | 107 | # Some drivers may need to override these helpers, for example adding 108 | # a 'commit' after the execute. 109 | def executeDDL1(self,cursor): 110 | cursor.execute(self.ddl1) 111 | 112 | def executeDDL2(self,cursor): 113 | cursor.execute(self.ddl2) 114 | 115 | def setUp(self): 116 | ''' self.drivers should override this method to perform required setup 117 | if any is necessary, such as creating the database. 118 | ''' 119 | pass 120 | 121 | def tearDown(self): 122 | ''' self.drivers should override this method to perform required cleanup 123 | if any is necessary, such as deleting the test database. 124 | The default drops the tables that may be created. 125 | ''' 126 | con = self._connect() 127 | try: 128 | cur = con.cursor() 129 | for ddl in (self.xddl1,self.xddl2): 130 | try: 131 | cur.execute(ddl) 132 | con.commit() 133 | except self.driver.Error: 134 | # Assume table didn't exist. Other tests will check if 135 | # execute is busted. 136 | pass 137 | finally: 138 | con.close() 139 | 140 | def _connect(self): 141 | try: 142 | return self.driver.connect( 143 | *self.connect_args,**self.connect_kw_args 144 | ) 145 | except AttributeError: 146 | self.fail("No connect method found in self.driver module") 147 | 148 | def dropTable(self,cursor,tableName): 149 | try: 150 | cursor.execute("drop table %s" % tableName) 151 | except self.driver.Error as err: 152 | pass 153 | 154 | def test_connect(self): 155 | con = self._connect() 156 | con.close() 157 | 158 | def test_apilevel(self): 159 | try: 160 | # Must exist 161 | apilevel = self.driver.apilevel 162 | # Must equal 2.0 163 | self.assertEqual(apilevel,'2.0') 164 | except AttributeError: 165 | self.fail("Driver doesn't define apilevel") 166 | 167 | def test_threadsafety(self): 168 | try: 169 | # Must exist 170 | threadsafety = self.driver.threadsafety 171 | # Must be a valid value 172 | self.failUnless(threadsafety in (0,1,2,3)) 173 | except AttributeError: 174 | self.fail("Driver doesn't define threadsafety") 175 | 176 | def test_paramstyle(self): 177 | try: 178 | # Must exist 179 | paramstyle = self.driver.paramstyle 180 | # Must be a valid value 181 | self.failUnless(paramstyle in ( 182 | 'qmark','numeric','named','format','pyformat' 183 | )) 184 | except AttributeError: 185 | self.fail("Driver doesn't define paramstyle") 186 | 187 | def test_Exceptions(self): 188 | # Make sure required exceptions exist, and are in the 189 | # defined heirarchy. 190 | self.failUnless(issubclass(self.driver.Warning,Exception)) 191 | self.failUnless(issubclass(self.driver.Error,Exception)) 192 | self.failUnless( 193 | issubclass(self.driver.InterfaceError,self.driver.Error) 194 | ) 195 | self.failUnless( 196 | issubclass(self.driver.DatabaseError,self.driver.Error) 197 | ) 198 | self.failUnless( 199 | issubclass(self.driver.OperationalError,self.driver.Error) 200 | ) 201 | self.failUnless( 202 | issubclass(self.driver.IntegrityError,self.driver.Error) 203 | ) 204 | self.failUnless( 205 | issubclass(self.driver.InternalError,self.driver.Error) 206 | ) 207 | self.failUnless( 208 | issubclass(self.driver.ProgrammingError,self.driver.Error) 209 | ) 210 | self.failUnless( 211 | issubclass(self.driver.NotSupportedError,self.driver.Error) 212 | ) 213 | 214 | def test_ExceptionsAsConnectionAttributes(self): 215 | # OPTIONAL EXTENSION 216 | # Test for the optional DB API 2.0 extension, where the exceptions 217 | # are exposed as attributes on the Connection object 218 | # I figure this optional extension will be implemented by any 219 | # driver author who is using this test suite, so it is enabled 220 | # by default. 221 | con = self._connect() 222 | drv = self.driver 223 | self.failUnless(con.Warning is drv.Warning) 224 | self.failUnless(con.Error is drv.Error) 225 | self.failUnless(con.InterfaceError is drv.InterfaceError) 226 | self.failUnless(con.DatabaseError is drv.DatabaseError) 227 | self.failUnless(con.OperationalError is drv.OperationalError) 228 | self.failUnless(con.IntegrityError is drv.IntegrityError) 229 | self.failUnless(con.InternalError is drv.InternalError) 230 | self.failUnless(con.ProgrammingError is drv.ProgrammingError) 231 | self.failUnless(con.NotSupportedError is drv.NotSupportedError) 232 | 233 | 234 | def test_commit(self): 235 | con = self._connect() 236 | try: 237 | # Commit must work, even if it doesn't do anything 238 | con.commit() 239 | finally: 240 | con.close() 241 | 242 | def test_rollback(self): 243 | con = self._connect() 244 | # If rollback is defined, it should either work or throw 245 | # the documented exception 246 | if hasattr(con,'rollback'): 247 | try: 248 | con.rollback() 249 | except self.driver.NotSupportedError: 250 | pass 251 | 252 | def test_cursor(self): 253 | con = self._connect() 254 | try: 255 | cur = con.cursor() 256 | finally: 257 | con.close() 258 | 259 | def test_cursor_isolation(self): 260 | con = self._connect() 261 | try: 262 | # Make sure cursors created from the same connection have 263 | # the documented transaction isolation level 264 | cur1 = con.cursor() 265 | cur2 = con.cursor() 266 | self.executeDDL1(cur1) 267 | cur1.execute("insert into %sbooze values ('Victoria Bitter')" % ( 268 | self.table_prefix 269 | )) 270 | cur2.execute("select name from %sbooze" % self.table_prefix) 271 | booze = cur2.fetchall() 272 | self.assertEqual(len(booze),1) 273 | self.assertEqual(len(booze[0]),1) 274 | self.assertEqual(booze[0][0],'Victoria Bitter') 275 | finally: 276 | con.close() 277 | 278 | def test_description(self): 279 | con = self._connect() 280 | try: 281 | cur = con.cursor() 282 | self.executeDDL1(cur) 283 | self.assertEqual(cur.description,None, 284 | 'cursor.description should be none after executing a ' 285 | 'statement that can return no rows (such as DDL)' 286 | ) 287 | cur.execute('select name from %sbooze' % self.table_prefix) 288 | self.assertEqual(len(cur.description),1, 289 | 'cursor.description describes too many columns' 290 | ) 291 | self.assertEqual(len(cur.description[0]),7, 292 | 'cursor.description[x] tuples must have 7 elements' 293 | ) 294 | self.assertEqual(cur.description[0][0].lower(),'name', 295 | 'cursor.description[x][0] must return column name' 296 | ) 297 | self.assertEqual(cur.description[0][1],self.driver.STRING, 298 | 'cursor.description[x][1] must return column type. Got %r' 299 | % cur.description[0][1] 300 | ) 301 | 302 | # Make sure self.description gets reset 303 | self.executeDDL2(cur) 304 | self.assertEqual(cur.description,None, 305 | 'cursor.description not being set to None when executing ' 306 | 'no-result statements (eg. DDL)' 307 | ) 308 | finally: 309 | con.close() 310 | 311 | def test_rowcount(self): 312 | con = self._connect() 313 | try: 314 | cur = con.cursor() 315 | self.executeDDL1(cur) 316 | self.assertEqual(cur.rowcount,-1, 317 | 'cursor.rowcount should be -1 after executing no-result ' 318 | 'statements' 319 | ) 320 | cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( 321 | self.table_prefix 322 | )) 323 | self.failUnless(cur.rowcount in (-1,1), 324 | 'cursor.rowcount should == number or rows inserted, or ' 325 | 'set to -1 after executing an insert statement' 326 | ) 327 | cur.execute("select name from %sbooze" % self.table_prefix) 328 | self.failUnless(cur.rowcount in (-1,1), 329 | 'cursor.rowcount should == number of rows returned, or ' 330 | 'set to -1 after executing a select statement' 331 | ) 332 | self.executeDDL2(cur) 333 | self.assertEqual(cur.rowcount,-1, 334 | 'cursor.rowcount not being reset to -1 after executing ' 335 | 'no-result statements' 336 | ) 337 | finally: 338 | con.close() 339 | 340 | lower_func = lowerfunc 341 | def test_callproc(self): 342 | con = self._connect() 343 | try: 344 | cur = con.cursor() 345 | if self.lower_func and hasattr(cur,'callproc'): 346 | r = cur.callproc(self.lower_func,('FOO',)) 347 | self.assertEqual(len(r),1) 348 | self.assertEqual(r[0],'FOO') 349 | r = cur.fetchall() 350 | self.assertEqual(len(r),1,'callproc produced no result set') 351 | self.assertEqual(len(r[0]),1, 352 | 'callproc produced invalid result set' 353 | ) 354 | self.assertEqual(r[0][0],'foo', 355 | 'callproc produced invalid results' 356 | ) 357 | finally: 358 | con.close() 359 | 360 | def test_close(self): 361 | con = self._connect() 362 | try: 363 | cur = con.cursor() 364 | finally: 365 | con.close() 366 | 367 | # cursor.execute should raise an Error if called after connection 368 | # closed 369 | self.assertRaises(self.driver.Error,self.executeDDL1,cur) 370 | 371 | # connection.commit should raise an Error if called after connection' 372 | # closed.' 373 | self.assertRaises(self.driver.Error,con.commit) 374 | 375 | # connection.close should raise an Error if called more than once 376 | self.assertRaises(self.driver.Error,con.close) 377 | 378 | def test_execute(self): 379 | con = self._connect() 380 | try: 381 | cur = con.cursor() 382 | self._paraminsert(cur) 383 | finally: 384 | con.close() 385 | 386 | def _paraminsert(self,cur): 387 | self.executeDDL1(cur) 388 | cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( 389 | self.table_prefix 390 | )) 391 | self.failUnless(cur.rowcount in (-1,1)) 392 | 393 | if self.driver.paramstyle == 'qmark': 394 | cur.execute( 395 | 'insert into %sbooze values (?)' % self.table_prefix, 396 | ("Cooper's",) 397 | ) 398 | elif self.driver.paramstyle == 'numeric': 399 | cur.execute( 400 | 'insert into %sbooze values (:1)' % self.table_prefix, 401 | ("Cooper's",) 402 | ) 403 | elif self.driver.paramstyle == 'named': 404 | cur.execute( 405 | 'insert into %sbooze values (:beer)' % self.table_prefix, 406 | {'beer':"Cooper's"} 407 | ) 408 | elif self.driver.paramstyle == 'format': 409 | cur.execute( 410 | 'insert into %sbooze values (%%s)' % self.table_prefix, 411 | ("Cooper's",) 412 | ) 413 | elif self.driver.paramstyle == 'pyformat': 414 | cur.execute( 415 | 'insert into %sbooze values (%%(beer)s)' % self.table_prefix, 416 | {'beer':"Cooper's"} 417 | ) 418 | else: 419 | self.fail('Invalid paramstyle') 420 | self.failUnless(cur.rowcount in (-1,1)) 421 | 422 | cur.execute('select name from %sbooze' % self.table_prefix) 423 | res = cur.fetchall() 424 | self.assertEqual(len(res),2,'cursor.fetchall returned too few rows') 425 | beers = [res[0][0],res[1][0]] 426 | beers.sort() 427 | self.assertEqual(beers[0],"Cooper's", 428 | 'cursor.fetchall retrieved incorrect data, or data inserted ' 429 | 'incorrectly' 430 | ) 431 | self.assertEqual(beers[1],"Victoria Bitter", 432 | 'cursor.fetchall retrieved incorrect data, or data inserted ' 433 | 'incorrectly' 434 | ) 435 | 436 | def test_executemany(self): 437 | con = self._connect() 438 | try: 439 | cur = con.cursor() 440 | self.executeDDL1(cur) 441 | largs = [ ("Cooper's",) , ("Boag's",) ] 442 | margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ] 443 | if self.driver.paramstyle == 'qmark': 444 | cur.executemany( 445 | 'insert into %sbooze values (?)' % self.table_prefix, 446 | largs 447 | ) 448 | elif self.driver.paramstyle == 'numeric': 449 | cur.executemany( 450 | 'insert into %sbooze values (:1)' % self.table_prefix, 451 | largs 452 | ) 453 | elif self.driver.paramstyle == 'named': 454 | cur.executemany( 455 | 'insert into %sbooze values (:beer)' % self.table_prefix, 456 | margs 457 | ) 458 | elif self.driver.paramstyle == 'format': 459 | cur.executemany( 460 | 'insert into %sbooze values (%%s)' % self.table_prefix, 461 | largs 462 | ) 463 | elif self.driver.paramstyle == 'pyformat': 464 | cur.executemany( 465 | 'insert into %sbooze values (%%(beer)s)' % ( 466 | self.table_prefix 467 | ), 468 | margs 469 | ) 470 | else: 471 | self.fail('Unknown paramstyle') 472 | self.failUnless(cur.rowcount in (-1,2), 473 | 'insert using cursor.executemany set cursor.rowcount to ' 474 | 'incorrect value %r' % cur.rowcount 475 | ) 476 | cur.execute('select name from %sbooze' % self.table_prefix) 477 | res = cur.fetchall() 478 | self.assertEqual(len(res),2, 479 | 'cursor.fetchall retrieved incorrect number of rows' 480 | ) 481 | beers = [res[0][0],res[1][0]] 482 | beers.sort() 483 | self.assertEqual(beers[0],"Boag's",'incorrect data retrieved') 484 | self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved') 485 | finally: 486 | con.close() 487 | 488 | def test_fetchone(self): 489 | con = self._connect() 490 | try: 491 | cur = con.cursor() 492 | 493 | # cursor.fetchone should raise an Error if called before 494 | # executing a select-type query 495 | self.assertRaises(self.driver.Error,cur.fetchone) 496 | 497 | # cursor.fetchone should raise an Error if called after 498 | # executing a query that cannnot return rows 499 | self.executeDDL1(cur) 500 | self.assertRaises(self.driver.Error,cur.fetchone) 501 | 502 | cur.execute('select name from %sbooze' % self.table_prefix) 503 | self.assertEqual(cur.fetchone(),None, 504 | 'cursor.fetchone should return None if a query retrieves ' 505 | 'no rows' 506 | ) 507 | self.failUnless(cur.rowcount in (-1,0)) 508 | 509 | # cursor.fetchone should raise an Error if called after 510 | # executing a query that cannnot return rows 511 | cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( 512 | self.table_prefix 513 | )) 514 | self.assertRaises(self.driver.Error,cur.fetchone) 515 | 516 | cur.execute('select name from %sbooze' % self.table_prefix) 517 | r = cur.fetchone() 518 | self.assertEqual(len(r),1, 519 | 'cursor.fetchone should have retrieved a single row' 520 | ) 521 | self.assertEqual(r[0],'Victoria Bitter', 522 | 'cursor.fetchone retrieved incorrect data' 523 | ) 524 | self.assertEqual(cur.fetchone(),None, 525 | 'cursor.fetchone should return None if no more rows available' 526 | ) 527 | self.failUnless(cur.rowcount in (-1,1)) 528 | finally: 529 | con.close() 530 | 531 | samples = [ 532 | 'Carlton Cold', 533 | 'Carlton Draft', 534 | 'Mountain Goat', 535 | 'Redback', 536 | 'Victoria Bitter', 537 | 'XXXX' 538 | ] 539 | 540 | def _populate(self): 541 | ''' Return a list of sql commands to setup the DB for the fetch 542 | tests. 543 | ''' 544 | populate = [ 545 | "insert into %sbooze values ('%s')" % (self.table_prefix,s) 546 | for s in self.samples 547 | ] 548 | return populate 549 | 550 | def test_fetchmany(self): 551 | con = self._connect() 552 | try: 553 | cur = con.cursor() 554 | 555 | # cursor.fetchmany should raise an Error if called without 556 | #issuing a query 557 | self.assertRaises(self.driver.Error,cur.fetchmany,4) 558 | 559 | self.executeDDL1(cur) 560 | for sql in self._populate(): 561 | cur.execute(sql) 562 | 563 | cur.execute('select name from %sbooze' % self.table_prefix) 564 | r = cur.fetchmany() 565 | self.assertEqual(len(r),1, 566 | 'cursor.fetchmany retrieved incorrect number of rows, ' 567 | 'default of arraysize is one.' 568 | ) 569 | cur.arraysize=10 570 | r = cur.fetchmany(3) # Should get 3 rows 571 | self.assertEqual(len(r),3, 572 | 'cursor.fetchmany retrieved incorrect number of rows' 573 | ) 574 | r = cur.fetchmany(4) # Should get 2 more 575 | self.assertEqual(len(r),2, 576 | 'cursor.fetchmany retrieved incorrect number of rows' 577 | ) 578 | r = cur.fetchmany(4) # Should be an empty sequence 579 | self.assertEqual(len(r),0, 580 | 'cursor.fetchmany should return an empty sequence after ' 581 | 'results are exhausted' 582 | ) 583 | self.failUnless(cur.rowcount in (-1,6)) 584 | 585 | # Same as above, using cursor.arraysize 586 | cur.arraysize=4 587 | cur.execute('select name from %sbooze' % self.table_prefix) 588 | r = cur.fetchmany() # Should get 4 rows 589 | self.assertEqual(len(r),4, 590 | 'cursor.arraysize not being honoured by fetchmany' 591 | ) 592 | r = cur.fetchmany() # Should get 2 more 593 | self.assertEqual(len(r),2) 594 | r = cur.fetchmany() # Should be an empty sequence 595 | self.assertEqual(len(r),0) 596 | self.failUnless(cur.rowcount in (-1,6)) 597 | 598 | cur.arraysize=6 599 | cur.execute('select name from %sbooze' % self.table_prefix) 600 | rows = cur.fetchmany() # Should get all rows 601 | self.failUnless(cur.rowcount in (-1,6)) 602 | self.assertEqual(len(rows),6) 603 | self.assertEqual(len(rows),6) 604 | rows = [r[0] for r in rows] 605 | rows.sort() 606 | 607 | # Make sure we get the right data back out 608 | for i in range(0,6): 609 | self.assertEqual(rows[i],self.samples[i], 610 | 'incorrect data retrieved by cursor.fetchmany' 611 | ) 612 | 613 | rows = cur.fetchmany() # Should return an empty list 614 | self.assertEqual(len(rows),0, 615 | 'cursor.fetchmany should return an empty sequence if ' 616 | 'called after the whole result set has been fetched' 617 | ) 618 | self.failUnless(cur.rowcount in (-1,6)) 619 | 620 | cur.execute('select name from %sbooze where name = \'foo\'' % 621 | self.table_prefix) 622 | self.assertEqual(cur.rowcount, -1, 623 | 'estimated row count should be reported as -1 ' 624 | 'but was %d' % cur.rowcount) 625 | 626 | self.executeDDL2(cur) 627 | cur.execute('select name from %sbarflys' % self.table_prefix) 628 | r = cur.fetchmany() # Should get empty sequence 629 | self.assertEqual(len(r),0, 630 | 'cursor.fetchmany should return an empty sequence if ' 631 | 'query retrieved no rows' 632 | ) 633 | self.failUnless(cur.rowcount in (-1,0)) 634 | 635 | finally: 636 | con.close() 637 | 638 | def test_fetchall(self): 639 | con = self._connect() 640 | try: 641 | cur = con.cursor() 642 | # cursor.fetchall should raise an Error if called 643 | # without executing a query that may return rows (such 644 | # as a select) 645 | self.assertRaises(self.driver.Error, cur.fetchall) 646 | 647 | self.executeDDL1(cur) 648 | for sql in self._populate(): 649 | cur.execute(sql) 650 | 651 | # cursor.fetchall should raise an Error if called 652 | # after executing a a statement that cannot return rows 653 | self.assertRaises(self.driver.Error,cur.fetchall) 654 | 655 | cur.execute('select name from %sbooze' % self.table_prefix) 656 | rows = cur.fetchall() 657 | self.failUnless(cur.rowcount in (-1,len(self.samples))) 658 | self.assertEqual(len(rows),len(self.samples), 659 | 'cursor.fetchall did not retrieve all rows' 660 | ) 661 | rows = [r[0] for r in rows] 662 | rows.sort() 663 | for i in range(0,len(self.samples)): 664 | self.assertEqual(rows[i],self.samples[i], 665 | 'cursor.fetchall retrieved incorrect rows' 666 | ) 667 | rows = cur.fetchall() 668 | self.assertEqual( 669 | len(rows),0, 670 | 'cursor.fetchall should return an empty list if called ' 671 | 'after the whole result set has been fetched' 672 | ) 673 | self.failUnless(cur.rowcount in (-1,len(self.samples))) 674 | 675 | self.executeDDL2(cur) 676 | cur.execute('select name from %sbarflys' % self.table_prefix) 677 | rows = cur.fetchall() 678 | self.failUnless(cur.rowcount in (-1,0)) 679 | self.assertEqual(len(rows),0, 680 | 'cursor.fetchall should return an empty list if ' 681 | 'a select query returns no rows' 682 | ) 683 | 684 | finally: 685 | con.close() 686 | 687 | def test_mixedfetch(self): 688 | con = self._connect() 689 | try: 690 | cur = con.cursor() 691 | self.executeDDL1(cur) 692 | for sql in self._populate(): 693 | cur.execute(sql) 694 | 695 | cur.execute('select name from %sbooze' % self.table_prefix) 696 | rows1 = cur.fetchone() 697 | rows23 = cur.fetchmany(2) 698 | rows4 = cur.fetchone() 699 | rows56 = cur.fetchall() 700 | self.failUnless(cur.rowcount in (-1,6)) 701 | self.assertEqual(len(rows23),2, 702 | 'fetchmany returned incorrect number of rows' 703 | ) 704 | self.assertEqual(len(rows56),2, 705 | 'fetchall returned incorrect number of rows' 706 | ) 707 | 708 | rows = [rows1[0]] 709 | rows.extend([rows23[0][0],rows23[1][0]]) 710 | rows.append(rows4[0]) 711 | rows.extend([rows56[0][0],rows56[1][0]]) 712 | rows.sort() 713 | for i in range(0,len(self.samples)): 714 | self.assertEqual(rows[i],self.samples[i], 715 | 'incorrect data retrieved or inserted' 716 | ) 717 | finally: 718 | con.close() 719 | 720 | def help_nextset_setUp(self,cur): 721 | ''' Should create a procedure called deleteme 722 | that returns two result sets, first the 723 | number of rows in booze then "name from booze" 724 | ''' 725 | sql=""" 726 | create procedure deleteme as 727 | begin 728 | select count(*) from %sbooze 729 | select name from %sbooze 730 | end 731 | """ % (self.table_prefix, self.table_prefix) 732 | cur.execute(sql) 733 | 734 | def help_nextset_tearDown(self,cur): 735 | 'If cleaning up is needed after nextSetTest' 736 | cur.execute("drop procedure deleteme") 737 | 738 | def test_nextset(self): 739 | con = self._connect() 740 | try: 741 | cur = con.cursor() 742 | if not hasattr(cur,'nextset'): 743 | return 744 | 745 | try: 746 | self.executeDDL1(cur) 747 | sql=self._populate() 748 | for sql in self._populate(): 749 | cur.execute(sql) 750 | 751 | self.help_nextset_setUp(cur) 752 | 753 | cur.callproc('deleteme') 754 | numberofrows=cur.fetchone() 755 | assert numberofrows[0]== len(self.samples) 756 | assert cur.nextset() 757 | names=cur.fetchall() 758 | assert len(names) == len(self.samples) 759 | s=cur.nextset() 760 | assert s == None,'No more return sets, should return None' 761 | finally: 762 | self.help_nextset_tearDown(cur) 763 | 764 | finally: 765 | con.close() 766 | 767 | def test_arraysize(self): 768 | # Not much here - rest of the tests for this are in test_fetchmany 769 | con = self._connect() 770 | try: 771 | cur = con.cursor() 772 | self.failUnless(hasattr(cur,'arraysize'), 773 | 'cursor.arraysize must be defined' 774 | ) 775 | finally: 776 | con.close() 777 | 778 | def test_setinputsizes(self): 779 | con = self._connect() 780 | try: 781 | cur = con.cursor() 782 | cur.setinputsizes( (25,) ) 783 | self._paraminsert(cur) # Make sure cursor still works 784 | finally: 785 | con.close() 786 | 787 | def test_setoutputsize_basic(self): 788 | # Basic test is to make sure setoutputsize doesn't blow up 789 | con = self._connect() 790 | try: 791 | cur = con.cursor() 792 | cur.setoutputsize(1000) 793 | cur.setoutputsize(2000,0) 794 | self._paraminsert(cur) # Make sure the cursor still works 795 | finally: 796 | con.close() 797 | 798 | def test_setoutputsize(self): 799 | # Real test for setoutputsize is driver dependant 800 | raise NotImplementedError('Driver need to override this test') 801 | 802 | def test_None(self): 803 | con = self._connect() 804 | try: 805 | cur = con.cursor() 806 | self.executeDDL1(cur) 807 | cur.execute('insert into %sbooze values (NULL)' % self.table_prefix) 808 | cur.execute('select name from %sbooze' % self.table_prefix) 809 | r = cur.fetchall() 810 | self.assertEqual(len(r),1) 811 | self.assertEqual(len(r[0]),1) 812 | self.assertEqual(r[0][0],None,'NULL value not returned as None') 813 | finally: 814 | con.close() 815 | 816 | def test_Date(self): 817 | d1 = self.driver.Date(2002,12,25) 818 | d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0))) 819 | # Can we assume this? API doesn't specify, but it seems implied 820 | # self.assertEqual(str(d1),str(d2)) 821 | 822 | def test_Time(self): 823 | t1 = self.driver.Time(13,45,30) 824 | t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0))) 825 | # Can we assume this? API doesn't specify, but it seems implied 826 | # self.assertEqual(str(t1),str(t2)) 827 | 828 | def test_Timestamp(self): 829 | t1 = self.driver.Timestamp(2002,12,25,13,45,30) 830 | t2 = self.driver.TimestampFromTicks( 831 | time.mktime((2002,12,25,13,45,30,0,0,0)) 832 | ) 833 | # Can we assume this? API doesn't specify, but it seems implied 834 | # self.assertEqual(str(t1),str(t2)) 835 | 836 | def test_Binary(self): 837 | b = self.driver.Binary('Something'.encode('utf-8')) 838 | b = self.driver.Binary(''.encode('utf-8')) 839 | 840 | def test_STRING(self): 841 | self.failUnless(hasattr(self.driver,'STRING'), 842 | 'module.STRING must be defined' 843 | ) 844 | 845 | def test_BINARY(self): 846 | self.failUnless(hasattr(self.driver,'BINARY'), 847 | 'module.BINARY must be defined.' 848 | ) 849 | 850 | def test_NUMBER(self): 851 | self.failUnless(hasattr(self.driver,'NUMBER'), 852 | 'module.NUMBER must be defined.' 853 | ) 854 | 855 | def test_DATETIME(self): 856 | self.failUnless(hasattr(self.driver,'DATETIME'), 857 | 'module.DATETIME must be defined.' 858 | ) 859 | 860 | def test_ROWID(self): 861 | self.failUnless(hasattr(self.driver,'ROWID'), 862 | 'module.ROWID must be defined.' 863 | ) 864 | 865 | def test_nullTimestampWithTimeZone(self): 866 | con = self._connect() 867 | cur = con.cursor() 868 | self.dropTable(cur, 'table_timestamp_with_timezone') 869 | try: 870 | cur.execute("create table table_timestamp_with_timezone (c timestamp with time zone null)") 871 | cur.execute("insert into table_timestamp_with_timezone values(?)",(None,)) 872 | except self.driver.Error as err: 873 | self.fail("error: %s" % str(err)) 874 | finally: 875 | con.close() 876 | 877 | def test_nullGeometry(self): 878 | con = self._connect() 879 | cur = con.cursor() 880 | self.dropTable(cur, 'table_geometry') 881 | try: 882 | cur.execute("create table table_geometry (c st_geometry null)") 883 | cur.execute("insert into table_geometry values(?)",(None,)) 884 | except self.driver.Error as err: 885 | self.fail("error: %s" % str(err)) 886 | finally: 887 | con.close() 888 | 889 | def test_nullBinary(self): 890 | con = self._connect() 891 | cur = con.cursor() 892 | self.dropTable(cur, 'table_binary') 893 | try: 894 | cur.execute("create table table_binary (c varbinary(128))") 895 | cur.execute("insert into table_binary values(?)",(None,)) 896 | except self.driver.Error as err: 897 | self.fail("error: %s" % str(err)) 898 | finally: 899 | con.close() 900 | 901 | def test_nullNumeric(self): 902 | con = self._connect() 903 | cur = con.cursor() 904 | self.dropTable(cur, 'table_numeric') 905 | try: 906 | cur.execute("create table table_numeric (c numeric null)") 907 | cur.execute("insert into table_numeric values(?)",(None,)) 908 | except self.driver.Error as err: 909 | self.fail("error: %s" % str(err)) 910 | finally: 911 | con.close() 912 | 913 | 914 | def test_nullTimestamp(self): 915 | con = self._connect() 916 | cur = con.cursor() 917 | self.dropTable(cur, 'table_timestamp') 918 | try: 919 | cur.execute("create table table_timestamp (c timestamp null)") 920 | cur.execute("insert into table_timestamp values(?)",(None,)) 921 | except self.driver.Error as err: 922 | self.fail("error: %s" % str(err)) 923 | finally: 924 | con.close() 925 | 926 | --------------------------------------------------------------------------------