diff --git a/src/dbspecific.h b/src/dbspecific.h index e6af0a93..a411c17d 100644 --- a/src/dbspecific.h +++ b/src/dbspecific.h @@ -16,6 +16,9 @@ #define SQL_DB2_DECFLOAT -360 // IBM DB/2 DECFLOAT type #define SQL_DB2_XML -370 // IBM DB/2 XML type #define SQL_SS_TIME2 -154 // SQL Server 2008 time type +#define SQL_INFX_BIGINT -114 // IBM Informix BIGINT type +#define SQL_INFX_UDT_BLOB -102 // IBM Informix BLOB type +#define SQL_INFX_UDT_CLOB -103 // IBM Informix CLOB type struct SQL_SS_TIME2_STRUCT { diff --git a/src/getdata.cpp b/src/getdata.cpp index c401a35a..e5f3a278 100644 --- a/src/getdata.cpp +++ b/src/getdata.cpp @@ -725,6 +725,7 @@ PyObject* GetData(Cursor* cur, Py_ssize_t iCol) case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: + case SQL_INFX_UDT_BLOB: return GetBinary(cur, iCol); case SQL_DECIMAL: @@ -741,6 +742,7 @@ PyObject* GetData(Cursor* cur, Py_ssize_t iCol) return GetDataLong(cur, iCol); case SQL_BIGINT: + case SQL_INFX_BIGINT: return GetDataLongLong(cur, iCol); case SQL_REAL: diff --git a/tests/accesstests.py b/tests/accesstests.py new file mode 100644 index 00000000..d86b1c4e --- /dev/null +++ b/tests/accesstests.py @@ -0,0 +1,670 @@ +"""Unit tests for Access + +Access SQL data types: http://msdn2.microsoft.com/en-us/library/bb208866.aspx +""" + +import datetime +import decimal +import pathlib +import uuid + +import pyodbc +import pytest + +DRIVER = "{Microsoft Access Driver (*.mdb, *.accdb)}" +DIRECTORY = pathlib.Path(__file__).parent +LEGACY_PATH = DIRECTORY / f"xxx_pyodbc_test.mdb" +MODERN_PATH = DIRECTORY / f"xxx_pyodbc_test.accdb" +LEGACY_BYTES = (DIRECTORY / "empty.mdb").read_bytes() +MODERN_BYTES = (DIRECTORY / "empty.accdb").read_bytes() +LEGACY_INITIAL_SIZE = LEGACY_PATH.write_bytes(LEGACY_BYTES) +MODERN_INITIAL_SIZE = MODERN_PATH.write_bytes(MODERN_BYTES) +LEGACY_CONNSTR = f"DRIVER={DRIVER};DBQ={LEGACY_PATH};ExtendedAnsiSQL=1" +MODERN_CONNSTR = f"DRIVER={DRIVER};DBQ={MODERN_PATH};ExtendedAnsiSQL=1" +SMALL_FENCEPOST_SIZES = 0, 1, 254, 255 # text fields <= 255 +LARGE_FENCEPOST_SIZES = 256, 270, 304, 508, 510, 511, 512, 1023, 1024, 2047, 2048, 4000, 4095, 4096, 4097, 10 * 1024, 20 * 1024 + +#---------------------------------------------------------------------- +# Helper functions +#---------------------------------------------------------------------- + +def connect(legacy=False, autocommit=False): + """Helper function to create a new Connection object""" + + connstr = LEGACY_CONNSTR if legacy else MODERN_CONNSTR + return pyodbc.connect(connstr, autocommit=autocommit) + + +@pytest.fixture +def cursors(): + """Create a pair of Cursor objects and remove any leftover test tables""" + + connections = connect(legacy=True), connect(legacy=False) + cursors = [conn.cursor() for conn in connections] + for cursor in cursors: + for i in range(3): + try: + cursor.execute(f"drop table t{i + 1}") + except pyodbc.ProgrammingError: + pass + cursor.connection.commit() + + yield cursors + + for cursor in cursors: + if not cursor.connection.closed: + connection = cursor.connection + cursor.close() + connection.close() + + +def _generate_str(length, encoding=None): + """ + Returns either a string or bytes, depending on whether encoding is specified, of the requested length. + + To enhance performance, there are 3 ways data is read, based on the length of the value, so most data types are + tested with 3 lengths. This function helps us generate the test data. + + We use a recognizable data set instead of a single character to make it less likely that "overlap" errors will + be hidden and to help us manually identify where a break occurs. + """ + + characters = "0123456789-abcdefghijklmnopqrstuvwxyz-" + if length <= len(characters): + v = characters + else: + c = (length + len(characters)-1) // len(characters) + v = characters * c + v = v[:length] + if encoding: + v = v.encode(encoding) + return v + + +def _test_strtype(cursor, sqltype, value, colsize=None): + """Helper function for testing string and byte columns""" + + assert colsize is None or value is None or colsize >= len(value), f"colsize={colsize} vallen={len(value)}" + sqltype = f"{sqltype}({colsize})" if colsize else sqltype + cursor.execute(f"create table t1 (n1 int not null, s1 {sqltype}, s2 {sqltype})") + cursor.execute("insert into t1 values (1, ?, ?)", (value, value)) + row = cursor.execute("select s1, s2 from t1").fetchone() + for i in range(2): + v = row[i] + assert type(v) == type(value) + if value is not None: + assert len(v) == len(value) + assert v == value + cursor.execute("drop table t1") + + +#---------------------------------------------------------------------- +# Tests for value types supported by Access +#---------------------------------------------------------------------- + +def test_binary_null(cursors): + """Test handling of a NULL value for a binary column""" + + for cursor in cursors: + _test_strtype(cursor, "binary", None) + + +def test_bit(cursors): + """Test handling of BIT column values""" + + value = True + for cursor in cursors: + cursor.execute("create table t1 (b bit)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select b from t1").fetchval() + assert type(result) is bool + assert value == result + + +def test_bit_null(cursors): + """Test handling of NULL in BIT columns""" + + value = None + for cursor in cursors: + cursor.execute("create table t1 (b bit)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select b from t1").fetchval() + assert type(result) is bool + assert False == result + + +def test_datetime(cursors): + """Verify proper handling of DATETIME column values""" + + value = datetime.datetime(2007, 1, 15, 3, 4, 5) + for cursor in cursors: + cursor.execute("create table t1 (dt datetime)") + cursor.execute("insert into t1 values (?)", value) + + result = cursor.execute("select dt from t1").fetchval() + assert result == value + + +def test_decimal(cursors): + """Test handling of DECIMAL column values""" + + value = decimal.Decimal("12345.6789") + for cursor in cursors: + cursor.execute("create table t1 (n numeric(10,4))") + cursor.execute("insert into t1 values(?)", value) + v = cursor.execute("select n from t1").fetchval() + assert isinstance(v, decimal.Decimal) + assert v == value + + +def test_negative_decimal(cursors): + """Test handling of negative DECIMAL column values""" + + value = decimal.Decimal("-10.0010") + for cursor in cursors: + cursor.execute("create table t1 (d numeric(19,4))") + cursor.execute("insert into t1 values(?)", value) + v = cursor.execute("select * from t1").fetchval() + assert isinstance(v, decimal.Decimal) + assert v == value + + +def test_float(cursors): + """Test handling of FLOAT column values""" + + for cursor in cursors: + value = 1234.567 + cursor.execute("create table t1 (n float)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert result == value + + +def test_negative_float(cursors): + """Test handling of negative FLOAT column values""" + + for cursor in cursors: + value = -200.5 + cursor.execute("create table t1 (n float)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert value == result + + +def test_guid(cursors): + """Test handling of GUID column values""" + + values = "de2ac9c6-8676-4b0b-b8a6-217a8580cbee", uuid.uuid4() + for cursor in cursors: + cursor.execute("create table t1 (i int, g uniqueidentifier)") + for i, value in enumerate(values, 1): + cursor.execute("insert into t1 values (?, ?)", (i, value)) + results = cursor.execute("select g from t1 order by i") + results = [row.g for row in cursor.fetchall()] + for result, value in zip(results, map(str, values)): + print(type(result), type(value)) + assert type(result) == type(value) + assert len(result) == len(value) + + +def test_image(cursors): + """Test handling of IMAGE columns of various lengths""" + + for cursor in cursors: + for size in SMALL_FENCEPOST_SIZES + LARGE_FENCEPOST_SIZES: + value = _generate_str(size, encoding="utf-8") + _test_strtype(cursor, "image", value) + + +def test_image_null(cursors): + """Test handling of a NULL value for an IMAGE column""" + + for cursor in cursors: + _test_strtype(cursor, "image", None) + + +def test_int(cursors): + """Test handling of INT column values""" + + value = 1234 + for cursor in cursors: + cursor.execute("create table t1 (n int)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert result == value + + +def test_smallint(cursors): + """Test handling of SMALLINT column values""" + + for cursor in cursors: + value = 32767 + cursor.execute("create table t1 (n smallint)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert result == value + + +def test_tinyint(cursors): + """Test handling of TINYINT column values""" + + for cursor in cursors: + cursor.execute("create table t1 (n tinyint)") + value = 10 + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert type(result) == type(value) + assert value == result + + +def test_negative_int(cursors): + """Test handling of negative INT column values""" + + for cursor in cursors: + value = -1 + cursor.execute("create table t1 (n int)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert result == value + + +def test_memo(cursors): + """Test handling of MEMO columns of various lengths""" + + for cursor in cursors: + for size in SMALL_FENCEPOST_SIZES + LARGE_FENCEPOST_SIZES: + value = _generate_str(size) + _test_strtype(cursor, "memo", value) + + +def test_memo_null(cursors): + """Test handling of a NULL value for a memo column""" + + for cursor in cursors: + _test_strtype(cursor, "memo", None) + + +def test_money(cursors): + """Test handling of MONEY column values""" + + value = decimal.Decimal("1234.45") + for cursor in cursors: + cursor.execute("create table t1 (n money)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert type(result) == type(value) + assert value == result + + +def test_real(cursors): + """Test handling of REAL column values""" + + for cursor in cursors: + value = 1234.5 + cursor.execute("create table t1 (n real)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert result == value + + +def test_negative_real(cursors): + """Test handling of negative REAL column values""" + + for cursor in cursors: + value = -200.5 + cursor.execute("create table t1 (n real)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchval() + assert value == result + + +def test_varbinary(cursors): + """Test handling of VARBINARY columns of various lengths""" + + for cursor in cursors: + for size in SMALL_FENCEPOST_SIZES: + value = _generate_str(size, encoding="utf-8") + _test_strtype(cursor, "varbinary", value, size) + + +def test_varchar_null(cursors): + """Test handling of a NULL value for a varchar column""" + + for cursor in cursors: + _test_strtype(cursor, "varchar", None) + + +#---------------------------------------------------------------------- +# Tests for module-level properties and methods +#---------------------------------------------------------------------- + +def test_datasources(): + """Confirm that the dataSource() method is implemented""" + + p = pyodbc.dataSources() + assert isinstance(p, dict) + + +def test_drivers(): + """Verify that the drivers() method is implemented""" + + p = pyodbc.drivers() + assert isinstance(p, list) + + +def test_lower_case(): + "Verify that pyodbc.lowercase forces returned column names to lowercase." + + # Has to be set before creating the cursor, so we must create our own cursors. + pyodbc.lowercase = True + for legacy in (True, False): + cursor = connect(legacy=legacy).cursor() + cursor.execute("create table t1 (Abc int, dEf int)") + cursor.execute("select * from t1") + names = sorted([column[0] for column in cursor.description]) + assert names == ["abc", "def"] + + # Put it back so other tests don't fail. + pyodbc.lowercase = False + + +def test_version(): + """Confirm that the module version property returns a well-formed version number (e.g., 5.3.0)""" + assert len(pyodbc.version.split(".")) == 3 + + +#---------------------------------------------------------------------- +# Tests for Connection methods and properties +#---------------------------------------------------------------------- + +def test_autocommit(cursors): + """Confirm the independence of the autocommit property of different Connection objects""" + + legacy = True + for cursor in cursors: + assert cursor.connection.autocommit is False + othercnxn = connect(autocommit=True, legacy=legacy) + legacy = False + assert othercnxn.autocommit is True + assert cursor.connection.autocommit is False + cursor.connection.autocommit = True + othercnxn.autocommit = False + assert othercnxn.autocommit is False + assert cursor.connection.autocommit is True + + +def test_closed_reflects_connection_state(cursors): + """Confirm that the closed property tracks the state of the Connection object""" + + for cursor in cursors: + assert not cursor.connection.closed + cursor.connection.close() + assert cursor.connection.closed + + +def test_getinfo_bool(cursors): + """Test retrieval of a Boolean property of a Connection object""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) + assert isinstance(value, bool) + + +def test_getinfo_int(cursors): + """Test retrieval of an integer property of a Connection object""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) + assert isinstance(value, int) + + +def test_getinfo_smallint(cursors): + """Test retrieval of a SMALLINT property of a Connection object""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) + assert isinstance(value, int) + + +def test_getinfo_string(cursors): + """Test retrieval of a string property of a Connection object""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) + assert isinstance(value, str) + + +#---------------------------------------------------------------------- +# Tests for Cursor methods and properties +#---------------------------------------------------------------------- + +def test_cursor_with_closed_connection(cursors): + """Make sure using a Cursor after closing its connection fails""" + + for cursor in cursors: + cursor.execute("create table t1 (id integer, s varchar(20))") + cursor.execute("insert into t1 values (?,?)", (1, "test")) + cursor.execute("select * from t1") + cursor.connection.close() + + # Now that the connection is closed, we expect an exception. (If the code attempts to use + # the HSTMT, we'll get an access violation instead.) + with pytest.raises(pyodbc.ProgrammingError): + cursor.execute("select * from t1") + + +def test_concatenation(cursors): + """Test string concatenation expressions in SELECT statements""" + + v2 = "0123456789" * 25 + v3 = "9876543210" * 25 + expected = v2 + "x" + v3 + for cursor in cursors: + cursor.execute("create table t1 (c2 varchar(250), c3 varchar(250))") + cursor.execute("insert into t1 (c2, c3) values (?,?)", v2, v3) + observed = cursor.execute("select c2 + 'x' + c3 from t1").fetchval() + assert observed == expected + + +def test_different_bindings(cursors): + """Test use of a single cursor using more than one table""" + + for cursor in cursors: + cursor.execute("create table t1 (n int)") + cursor.execute("create table t2 (d datetime)") + cursor.execute("insert into t1 values (?)", 1) + cursor.execute("insert into t2 values (?)", datetime.datetime.now()) + + +def test_executemany(cursors): + """Test inserting multiple rows in a single call""" + + params = [ (i, str(i)) for i in range(1, 6) ] + for cursor in cursors: + cursor.execute("create table t1 (a int, b varchar(10))") + cursor.executemany("insert into t1 (a, b) values (?,?)", params) + count = cursor.execute("select count(*) from t1").fetchval() + assert count == len(params) + cursor.execute("select a, b from t1 order by a") + rows = cursor.fetchall() + assert count == len(rows) + for param, row in zip(params, rows): + assert param[0] == row[0] + assert param[1] == row[1] + + +def test_executemany_failure(cursors): + """Verify that an exception is raised if one query in an executemany fails""" + + for cursor in cursors: + cursor.execute("create table t1 (a int, b varchar(10))") + params = [ + (1, "good"), + ("error", "not an int"), + (3, "good"), + ] + with pytest.raises(pyodbc.Error): + cursor.executemany("insert into t1 (a, b) values (?, ?)", params) + + # This check would fail, because the driver misbehaves and lets one of the + # inserts through, even though autocommit is off and we haven't committed + # anything. + # rows = cursor.execute("select * from t1").fetchall() + # assert rows == [] + + +def test_multiple_bindings(cursors): + """Test multiple binds and selects on a cursor""" + + for cursor in cursors: + cursor.execute("create table t1 (n int)") + cursor.execute("insert into t1 values (?)", 1) + cursor.execute("insert into t1 values (?)", 2) + cursor.execute("insert into t1 values (?)", 3) + for i in range(3): + cursor.execute("select n from t1 where n < ?", 10) + cursor.execute("select n from t1 where n < 3") + + +def test_rowcount_delete(cursors): + """Verify that we get the correct rowcount following a DELETE""" + + count = 4 + for cursor in cursors: + assert cursor.rowcount == -1 + cursor.execute("create table t1 (i int)") + for i in range(count): + cursor.execute("insert into t1 values (?)", i) + cursor.execute("delete from t1") + assert cursor.rowcount == count + + +def test_rowcount_nodata(cursors): + """Verify that we get a zero rowcount for a DELETE which resulted in SQL_NO_DATA""" + + for cursor in cursors: + """ + This represents a different code path than a delete that deleted something. + + The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over + the code that errors out and drop down to the same SQLRowCount code. On the other hand, we could hardcode a + zero return value. + """ + + cursor.execute("create table t1 (i int)") + # This is a different code path internally. + cursor.execute("delete from t1") + assert cursor.rowcount == 0 + + +def test_rowcount_reset(cursors): + """Verify that rowcount is reset to -1""" + + count = 4 + for cursor in cursors: + cursor.execute("create table t1 (i int)") + for i in range(count): + cursor.execute("insert into t1 values (?)", i) + assert cursor.rowcount == 1 + cursor.execute("create table t2(i int)") + assert cursor.rowcount == -1 + + +def test_rowcount_select(cursors): + """Verify that Cursor.rowcount is set properly after a select statement""" + + count = 4 + for cursor in cursors: + cursor.execute("create table t1 (i int)") + count = 4 + for i in range(count): + cursor.execute("insert into t1 values (?)", i) + cursor.execute("select * from t1") + assert cursor.rowcount == -1 + rows = cursor.fetchall() + assert len(rows) == count + assert cursor.rowcount == -1 + + +def test_row_description(cursors): + """Make sure Cursor.description is accessible as Row.cursor_description""" + + for cursor in cursors: + cursor.execute("create table t1 (a int, b char(3))") + cursor.commit() + cursor.execute("insert into t1 values(1, 'abc')") + row = cursor.execute("select * from t1").fetchone() + assert cursor.description == row.cursor_description + + +def test_subquery_params(cursors): + """Verify that parameter markers work in a subquery""" + + for cursor in cursors: + cursor.execute("create table t1 (id integer, s varchar(20))") + cursor.execute("insert into t1 values (?,?)", 1, "test") + row = cursor.execute("""\ + select x.id + from ( + select id + from t1 + where s = ? + and id between ? and ? + ) x + """, ("test", 1, 10)).fetchone() + assert row is not None + assert row.id == 1 + cursor.execute("drop table t1") + + +def test_unicode_query(cursors): + """Verify that non-ASCII parameters are handled correctly""" + + name = "王伟" + for cursor in cursors: + cursor.execute("create table t1 (id int, name varchar)") + cursor.execute("insert into t1 values (?, ?)", (1, "John Smith")) + cursor.execute("insert into t1 values (?, ?)", (2, name)) + cursor.execute("select id from t1 where name = ?", name) + rows = cursor.fetchall() + assert len(rows) == 1 + assert rows[0].id == 2 + + +#---------------------------------------------------------------------- +# Tests for Row methods and properties +#---------------------------------------------------------------------- + +def test_negative_row_index(cursors): + """Verify that the Row class handles negative index access correctly""" + + for cursor in cursors: + cursor.execute("create table t1 (s varchar(20))") + cursor.execute("insert into t1 values (?)", "1") + row = cursor.execute("select * from t1").fetchone() + assert row[0] == "1" + assert row[-1] == "1" + + +def test_row_repr(cursors): + """Test serialization of Row class objects""" + + for cursor in cursors: + cursor.execute("create table t1 (a int, b int, c int, d int)"); + cursor.execute("insert into t1 values (1,2,3,4)") + row = cursor.execute("select * from t1").fetchone() + assert str(row) == "(1, 2, 3, 4)" + assert str(row[:-1]) == "(1, 2, 3)" + assert str(row[:1]) == "(1,)" + + +def test_row_slicing(cursors): + """Verify that the Row class handles slicing operations correctly""" + + for cursor in cursors: + cursor.execute("create table t1 (a int, b int, c int, d int)"); + cursor.execute("insert into t1 values (1,2,3,4)") + row = cursor.execute("select * from t1").fetchone() + assert row[:] is row + assert row[:-1] == (1,2,3) + assert row[0:4] is row diff --git a/tests/old/empty.accdb b/tests/empty.accdb similarity index 100% rename from tests/old/empty.accdb rename to tests/empty.accdb diff --git a/tests/old/empty.mdb b/tests/empty.mdb similarity index 100% rename from tests/old/empty.mdb rename to tests/empty.mdb diff --git a/tests/exceltests.py b/tests/exceltests.py new file mode 100644 index 00000000..425519ae --- /dev/null +++ b/tests/exceltests.py @@ -0,0 +1,118 @@ +"""Test pyodbc with Excel workbooks + +Ported from the old unit tests. + +These are read-only tests, using tests/pyodbc_test.xlsx. Microsoft's driver +produces a corrupted Excel file when used for creating and populating tables. +""" + +import pathlib +import pyodbc +import pytest + +DRIVER = "{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}" +DIRECTORY = pathlib.Path(__file__).parent +LEGACY_PATH = DIRECTORY / f"pyodbc_test.xls" +MODERN_PATH = DIRECTORY / f"pyodbc_test.xlsx" +LEGACY_CONNSTR = f"DRIVER={DRIVER};DBQ={LEGACY_PATH};Extended Properties='HDR=YES';" +MODERN_CONNSTR = f"DRIVER={DRIVER};DBQ={MODERN_PATH};Extended Properties='HDR=YES';" + + +#---------------------------------------------------------------------- +# Helper functions +#---------------------------------------------------------------------- + +def connect(legacy=False): + """Helper function to create a new Connection object""" + + connstr = LEGACY_CONNSTR if legacy else MODERN_CONNSTR + return pyodbc.connect(connstr, autocommit=True) + + +@pytest.fixture +def cursors(): + """Create a pair of Cursor objects""" + + connections = connect(legacy=True), connect(legacy=False) + cursors = [conn.cursor() for conn in connections] + + yield cursors + + for cursor in cursors: + if not cursor.connection.closed: + connection = cursor.connection + cursor.close() + connection.close() + + +def test_getinfo_string(cursors): + """Confirm that we can fetch string attributes from the driver""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) + assert isinstance(value, str) + + +def test_getinfo_bool(cursors): + """Confirm that we can fetch Boolean attributes from the driver""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) + assert isinstance(value, bool) + + +def test_getinfo_int(cursors): + """Confirm that we can fetch integer attributes from the driver""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) + assert isinstance(value, int) + + +def test_getinfo_smallint(cursors): + """Confirm that we can fetch SMALLINT attributes from the driver""" + + for cursor in cursors: + value = cursor.connection.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) + assert isinstance(value, int) + + +def test_read_sheet(cursors): + """Verify that we can read values from a sheet as a table""" + + for cursor in cursors: + rows = cursor.execute("select * from [Sheet1$]").fetchall() + assert len(rows) == 3 + + +def test_read_range(cursors): + """Verify that we can read values from a named range as a table""" + + for cursor in cursors: + rows = cursor.execute("select * from Sales").fetchall() + assert len(rows) == 4 + + +def test_tables(cursors): + """Verify that we can find out which tables are available""" + + for cursor in cursors: + tables = [row.table_name for row in cursor.tables()] + assert "Sheet1$" in tables + assert "Sales" in tables + + +def test_join(cursors): + """Actually the driver doesn't understand JOIN so we have to use stone age syntax""" + + for cursor in cursors: + cursor.execute("""\ + select e.name, sum(s.sales) + from [Sheet1$] e, Sales s + where s.eid = e.id + group by e.name + order by 1""" + ) + expected = pytest.approx([("Bart Schmidt", 397000.0), ("Sally Jones", 613567.89)]) + observed = [tuple(row) for row in cursor] + assert observed == expected diff --git a/tests/informix_test.py b/tests/informix_test.py new file mode 100644 index 00000000..d12a2afe --- /dev/null +++ b/tests/informix_test.py @@ -0,0 +1,1135 @@ +"""Unit tests for Informix DB.""" + +from datetime import date, datetime +from decimal import Decimal +from json import dumps, loads +from os import environ +from pathlib import Path +from typing import Iterator +from uuid import UUID + +import pyodbc +import pytest + +CNXNSTR = environ.get("PYODBC_INFORMIX", "DSN=pyodbc-informix") + + +#---------------------------------------------------------------------- +# Helper functions +#---------------------------------------------------------------------- + +def connect(autocommit=False, attrs_before=None): + """Create a connection to the Informix database""" + return pyodbc.connect(CNXNSTR, autocommit=autocommit, attrs_before=attrs_before) + + +DRIVER = connect().getinfo(pyodbc.SQL_DRIVER_NAME) + + +@pytest.fixture +def cursor() -> Iterator[pyodbc.Cursor]: + """Cursor object supplied on demand to test functions""" + + cnxn = connect() + cur = cnxn.cursor() + + cur.execute("drop table if exists t1") + cur.execute("drop table if exists t2") + cur.execute("drop table if exists t3") + cnxn.commit() + + yield cur + + if not cnxn.closed: + cur.close() + cnxn.close() + + +def _generate_str(length, encoding=None): + """ + Returns either a string or bytes, depending on whether encoding is provided, + that is `length` elements long. + + If length is None, None is returned. This simplifies the tests by letting us put None into + an array of other lengths and pass them here, moving the special case check into one place. + """ + + if length is None: + return None + + seed = "0123456789-abcdefghijklmnopqrstuvwxyz-" + + if length <= len(seed): + v = seed + else: + c = (length + len(seed) - 1 // len(seed)) + v = seed * c + + v = v[:length] + if encoding: + v = v.encode(encoding) + + return v + + +#---------------------------------------------------------------------- +# Tests for individual data types supported by Informix +#---------------------------------------------------------------------- + +def test_bigint(cursor: pyodbc.Cursor): + """Test the Informix BIGINT type (a.k.a. INT8), including the reserved sentinel value""" + + values =-1, 0, 1, 0x7FFF_FFFE, 0x1_2345_6789, -9_223_372_036_854_775_807, 9_223_372_036_854_775_807 + for typename in ("bigint", "int8"): + for value in values: + cursor.execute(f"create table t1 (col {typename})") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select col from t1").fetchval() + assert result == value + cursor.execute("drop table t1") + cursor.execute(f"create table t1 (col {typename})") + reserved = 9_223_372_036_854_775_808 + with pytest.raises(OverflowError): + cursor.execute("insert into t1 values (?)", reserved) + cursor.execute("drop table t1") + + +def test_bigserial(cursor: pyodbc.Cursor): + """Test the Informix BIGSERIAL type""" + + cursor.execute("create table t1 (id bigserial, name varchar(32))") + cursor.execute("insert into t1 (name) values (?)", "John") + cursor.execute("insert into t1 (name) values (?)", "Paul") + cursor.execute("insert into t1 (name) values (?)", "George") + cursor.execute("insert into t1 (id, name) values (?, ?)", (9_223_372_036_854_775_807, "Ringo")) + cursor.execute("select * from t1 order by id") + rows = [tuple(row) for row in cursor.fetchall()] + assert rows == [(1, "John"), (2, "Paul"), (3, "George"), (9_223_372_036_854_775_807, "Ringo")] + + +def test_blob(cursor: pyodbc.Cursor, tmp_path): + """Test the BLOB data type + + This type and the sibling CLOB type don't work the way normal column values do. + They're basically intended to come and go directly from/to files, not memory. + If anyone knows otherwise, feel free to jump in and improve this test. + The odd-looking lotofile function has nothing to do with playing the numbers. + It stands for "large object to file" (and we'll see it again when we test CLOB). + As you can see the results set gives you the *real* path for the file into which + the BLOB value was written, based on the path we gave it. + """ + + value = _generate_str(4096, encoding="ascii") + insert_path = tmp_path / "blob.bin" + insert_path.write_bytes(value) + cursor.execute("create table t1 (id int, value blob)") + cursor.execute("insert into t1 (id, value) values (42, filetoblob(?, 'client'))", str(insert_path)) + cursor.commit() + fetch_path = tmp_path / "retrieved_blob.bin" + cursor.execute("select lotofile(value, ?, 'client') as output_path from t1 where id = ?", (str(fetch_path), 42)) + output_path = Path(cursor.fetchval()) + retrieved_bytes = output_path.read_bytes() + assert retrieved_bytes == value + + +def test_boolean(cursor: pyodbc.Cursor): + """Test the Informix BOOLEAN type""" + + cursor.execute("create table t1 (id int, flag boolean)") + cursor.execute("insert into t1 values (?, ?)", (1, True)) + cursor.execute("insert into t1 values (?, ?)", (2, False)) + cursor.execute("select * from t1 order by id") + rows = [tuple(row) for row in cursor.fetchall()] + assert rows == [(1, True), (2, False)] + + +def test_bson(cursor: pyodbc.Cursor): + """Test the Informix BSON column type""" + + cursor.execute("create table t1 (id int, person bson)") + cursor.execute("""\ + create index person_surname_idx + on t1(bson_get(person, 'name.family')) using bson + """) + people = [ + {"name": {"given": "Jim", "family": "Flynn"}, "age": 29, "cars": ["Dodge", "Olds"]}, + {"name": {"given": "Penélope", "family": "Cruz"}, "age": 32, "cars": ["Chevy"]}, + {"name": {"given": "Günther", "family": "Schmidt"}, "age": 19, "cars": ["Toyota", "Ford"]}, + ] + for i, record in enumerate(people, 1): + cursor.execute(f"insert into t1 values (?, ?::json)", (i, dumps(record))) + cursor.execute(""" + select person::json::lvarchar(8192) as person + from t1 + where bson_value_lvarchar(person, 'name.family') < 'M' + order by bson_value_lvarchar(person, 'name.family') + """) + observed = [loads(row.person) for row in cursor.fetchall()] + expected = [people[1], people[0]] + assert observed == expected + + +def test_byte(cursor: pyodbc.Cursor): + """Test the Informix BYTE type + + Same problem as with the TEXT type (see test_text() below): we can't fetch the data + using ODBC. Trying to do so with isql gets "[Informix ODBC Driver]Communication link + failure. [Informix ODBC Driver]Data truncated." + """ + + lengths = None, 0, 100, 1000, 4000 + values = [_generate_str(length, encoding="utf-8") for length in lengths] + cursor.execute("create table t1 (id int, val byte)") + for i, value in enumerate(values, 1): + cursor.execute("insert into t1 values (?, ?)", (i, value)) + cursor.commit() + cursor.execute("select length(val) as vlen from t1 where val is not null order by 1") + observed_lengths = [row.vlen for row in cursor.fetchall()] + expected_lengths = sorted([length for length in lengths if length is not None]) + assert observed_lengths == expected_lengths + + +def test_char(cursor: pyodbc.Cursor): + """Test the Informix CHAR type""" + + lengths = 10, 100, 1000, 4000, 32767 + table = "t1" + for length in lengths: + value = _generate_str(length) + cursor.execute(f"create table {table} (val char({length}))") + cursor.execute(f"insert into {table} values (?)", value) + cursor.execute(f"insert into {table} values (?)", None) + cursor.execute(f"select val from {table} where val is not null") + result = cursor.fetchval() + assert result == value + cursor.execute(f"drop table {table}") + with pytest.raises(pyodbc.Error): + cursor.execute(f"create table {table} (val char(32768))") + + +def test_clob(cursor: pyodbc.Cursor, tmp_path): + """Test the CLOB data type (see notes on BLOB type above)""" + + value = _generate_str(4096, encoding="utf-8") + insert_path = tmp_path / "clob.bin" + insert_path.write_bytes(value) + cursor.execute("create table t1 (id int, value clob)") + cursor.execute("insert into t1 (id, value) values (42, filetoclob(?, 'client'))", str(insert_path)) + cursor.commit() + fetch_path = tmp_path / "retrieved_clob.bin" + cursor.execute("select lotofile(value, ?, 'client') as output_path from t1 where id = ?", (str(fetch_path), 42)) + output_path = Path(cursor.fetchval()) + retrieved_bytes = output_path.read_bytes() + assert retrieved_bytes == value + + +def test_date(cursor: pyodbc.Cursor): + """Test the Informix DATE type""" + + cursor.execute("create table t1 (d date)") + values = date(1800, 1, 1), date(3456, 12, 25), date(1776, 7, 4), date(1066, 10, 14) + for value in values: + cursor.execute("insert into t1 values (?)", value) + cursor.execute("insert into t1 values (?)", None) + n = cursor.execute("select count(*) from t1").fetchval() + assert n == len(values) * 2 + rows = cursor.execute("select * from t1 where d is not null").fetchall() + dates = sorted([row.d for row in rows]) + for d in dates: + assert type(d) is date + assert dates == sorted(values) + + +def test_datetime(cursor: pyodbc.Cursor): + """Test the Informix DATETIME type""" + + cases = ( + ("year to year", "2001", datetime(2001, 1, 1)), + ("year to month", "2001-02", datetime(2001, 2, 1)), + ("year to day", "2001-02-03", date(2001, 2, 3)), + ("year to hour", "2001-02-03 04", datetime(2001, 2, 3, 4)), + ("year to minute", "2001-02-03 04:05", datetime(2001, 2, 3, 4, 5)), + ("year to second", "2001-02-03 04:05:06", datetime(2001, 2, 3, 4, 5, 6)), + ("year to fraction(1)", "2001-02-03 04:05:06.7", datetime(2001, 2, 3, 4, 5, 6, 700000)), + ("year to fraction(2)", "2001-02-03 04:05:06.78", datetime(2001, 2, 3, 4, 5, 6, 780000)), + ("year to fraction(3)", "2001-02-03 04:05:06.789", datetime(2001, 2, 3, 4, 5, 6, 789000)), + ("year to fraction(4)", "2001-02-03 04:05:06.7891", datetime(2001, 2, 3, 4, 5, 6, 789100)), + ("year to fraction(5)", "2001-02-03 04:05:06.78912", datetime(2001, 2, 3, 4, 5, 6, 789120)), + ("month to minute", "02-03 04:05", datetime(1200, 2, 3, 4, 5)), + ) + for precision, value, expected in cases: + cursor.execute(f"create table t1 (dt datetime {precision})") + cursor.execute("insert into t1 values (?)", value) + value = cursor.execute("select dt from t1").fetchval() + assert value == expected + cursor.execute("drop table t1") + with pytest.raises(pyodbc.ProgrammingError): + cursor.execute("create table t1 (dt datetime year to fraction(6)") + + +def test_decimal(cursor: pyodbc.Cursor): + """Test the Informix DECIMAL type (for which NUMERIC is an alias)""" + + typenames = "decimal", "numeric" + params = [Decimal(n) for n in "-1000.10 -1234.56 -1 0 1 1000.10 1234.56 100010 123456789.21".split()] + params.append(None) + for alias in typenames: + + # If you specify a scale, you get the true ANSI SQL DECIMAL/NUMERIC type. + cursor.execute(f"create table t1 (val {alias}(20,6))") + for param in params: + cursor.execute("truncate table t1") + cursor.commit() + cursor.execute("insert into t1 values (?)", param) + result = cursor.execute("select val from t1").fetchval() + assert result == param + + # If you leave off the scale, you get Informix's "floating" decimal type if not in ANSI-compliant mode. + try: + dbname = cursor.execute("SELECT DBINFO('dbname') FROM systables WHERE tabid = 1").fetchval().strip() + is_ansi = cursor.execute("SELECT is_ansi FROM sysmaster:sysdatabases WHERE name = ?", dbname).fetchval() + except: + # We could skip the rest of the test altogether, but let's go with the default setting. + is_ansi = False + cursor.execute("drop table t1") + cursor.execute(f"create table t1 (id int, val {alias}(5))") + cursor.execute("insert into t1 values (?, ?)", (1, "12345")) + cursor.execute("insert into t1 values (?, ?)", (2, "1.2345")) + cursor.execute("insert into t1 values (?, ?)", (3, "12.345")) + cursor.execute("insert into t1 values (?, ?)", (4, "123.45")) + cursor.execute("insert into t1 values (?, ?)", (5, "1234.5")) + if not is_ansi: + cursor.execute("insert into t1 values (?, ?)", (6, "123456789")) + cursor.execute("insert into t1 values (?, ?)", (7, ".123456789")) + rows = cursor.execute("select val from t1 order by id").fetchall() + if is_ansi: + expected = [Decimal("12345"), Decimal("1"), Decimal("12"), Decimal("123"), Decimal("1235")] + else: + expected = [ + Decimal("12345.0"), + Decimal("1.2345"), + Decimal("12.345"), + Decimal("123.45"), + Decimal("1234.5"), + Decimal("123460000.0"), + Decimal("0.12346"), + ] + observed = [row.val for row in rows] + assert observed == expected + cursor.execute("drop table t1") + + +def test_float(cursor: pyodbc.Cursor): + """Test the Informix FLOAT data type""" + + values = [None, -200, -1, 0, 1, 1234.5, -200, .00012345] + cursor.execute("create table t1 (i int, f float)") + for i, v in enumerate(values, 1): + cursor.execute("insert into t1 values (?, ?)", (i, v)) + cursor.execute("select f from t1 order by i") + results = [row.f for row in cursor.fetchall()] + assert pytest.approx(results) == values + + +def test_integer(cursor: pyodbc.Cursor): + """Test the Informix INTEGER type, including the reserved sentinel value""" + + for alias in ("int", "integer"): + cursor.execute(f"create table t1 (col {alias})") + for param in [-1, 0, 1, -2_147_483_647, 2_147_483_647]: + cursor.execute("truncate table t1") + cursor.commit() + cursor.execute("insert into t1 values (?)", param) + result = cursor.execute("select col from t1").fetchval() + assert result == param + reserved = 2_147_483_648 + with pytest.raises(pyodbc.Error): + cursor.execute("insert into t1 values (?)", reserved) + cursor.execute("drop table t1") + + +def test_interval(cursor: pyodbc.Cursor): + """Test the Informix INTERVAL type + + Until https://github.com/mkleehammer/pyodbc/issues/60 is resolved, we won't be able + to retrieve the values properly. Best we can do is retrieve the values cast to strings. + """ + + cursor.execute("create table t1 (i interval year to month, s varchar(32))") + cursor.execute("insert into t1 (i, s) values ('3-6', 'time to market')") + cursor.execute("insert into t1 (i, s) values (?, ?)", ("1-3", "time to obsolescence")) + observed = [row.s for row in cursor.execute("select s from t1 order by 1").fetchall()] + expected = ["time to market", "time to obsolescence"] + assert observed == expected + cursor.execute("select * from t1") + with pytest.raises(pyodbc.ProgrammingError): + rows = cursor.fetchall() + cursor.execute("select cast(i as varchar(25)) as i from t1 order by s") + observed = [row.i.strip() for row in cursor.fetchall()] + expected = ["3-06", "1-03"] + assert observed == expected + + +def test_json(cursor: pyodbc.Cursor): + """Test the Informix JSON column type (unsupported type -115 so we use cast)""" + + people = [ + {"name": {"given": "Jim", "family": "Flynn"}, "age": 29, "cars": ["Dodge", "Olds"]}, + {"name": {"given": "Penélope", "family": "Cruz"}, "age": 32, "cars": ["Chevy"]}, + {"name": {"given": "Günther", "family": "Schmidt"}, "age": 19, "cars": ["Toyota", "Ford"]}, + ] + cursor.execute("create table t1 (id int, person json)") + for i, person in enumerate(people, 1): + cursor.execute("insert into t1 values(?, ?)", (i, dumps(person))) + cursor.execute("select person::json::lvarchar(8192) as person from t1 order by id") + results = [loads(row.person) for row in cursor.fetchall()] + assert results == people + + +def test_list(cursor: pyodbc.Cursor): + """Test the Informix LIST data type + + This type (SQL type -107) is not yet directly supported, so we use a cast. + """ + + cursor.execute("create table t1 (name varchar(50), sales list(money not null))") + cursor.execute("insert into t1 values (?, ?)", ("Jenny Chow", "list{587900, 600000}")); + cursor.execute("select * from t1") + with pytest.raises(pyodbc.ProgrammingError): + rows = cursor.fetchall() + cursor.execute("select name, cast(sales as lvarchar(255)) from t1") + observed = tuple(cursor.fetchone()) + expected = "Jenny Chow", "LIST{'$587900.00','$600000.00'}" + assert observed == expected + + +def test_lvarchar(cursor: pyodbc.Cursor): + """Test the Informix LVARCHAR type""" + + lengths = 10, 100, 255, 32739 + table = "t1" + for length in lengths: + value = _generate_str(length) + cursor.execute(f"create table {table} (val lvarchar({length}))") + cursor.execute(f"insert into {table} values (?)", value) + cursor.execute(f"select val from {table}") + result = cursor.fetchval() + assert result == value + cursor.execute(f"drop table {table}") + with pytest.raises(pyodbc.Error): + cursor.execute(f"create table {table} (val character varying(32740))") + + +def test_money(cursor: pyodbc.Cursor): + """Test the Informix MONEY data type""" + + cursor.execute("create table t1 (m money)") + params = [Decimal(n) for n in "-1000.10 -1234.56 -1 0 1 1000.10 1234.56 100010 123456789.21".split()] + params.append(None) + for param in params: + cursor.execute("truncate table t1") + cursor.commit() + cursor.execute("insert into t1 values (?)", param) + result = cursor.execute("select m from t1").fetchval() + assert result == param + + +def test_multiset(cursor: pyodbc.Cursor): + """Test the Informix MULTISET data type + + This type (SQL type -107) is not yet directly supported, so we use a cast. + """ + + cursor.execute("create table t1 (id int, colors multiset(varchar(32) not null))") + cursor.execute("insert into t1 values (?, ?)", (1, "multiset{'blue', 'green', 'yellow'}")); + cursor.execute("insert into t1 values (?, ?)", (2, "multiset{'blue', 'green', 'yellow', 'blue'}")); + cursor.execute("select * from t1") + with pytest.raises(pyodbc.ProgrammingError): + rows = cursor.fetchall() + cursor.execute("select cast(colors as lvarchar(255)) as colors from t1 order by id") + expected = ["MULTISET{'blue','green','yellow'}", "MULTISET{'blue','green','yellow','blue'}"] + observed = [row.colors for row in cursor.fetchall()] + assert observed == expected + + +def test_nchar(cursor: pyodbc.Cursor): + """Test the Informix NCHAR type""" + + # Careful, Informix counts bytes, not characters! + table = "t1" + value = "我的" + cursor.execute(f"create table {table} (val nchar(4))") + cursor.execute(f"insert into {table} values (?)", value) + stored = cursor.execute(f"select * from {table}").fetchval() + assert stored != value # Informix silently truncates the value + lengths = 10, 100, 1000, 4000, 32767 + cursor.execute(f"drop table {table}") + for length in lengths: + value = _generate_str(length) + cursor.execute(f"create table {table} (val char({length}))") + cursor.execute(f"insert into {table} values (?)", value) + cursor.execute(f"insert into {table} values (?)", None) + cursor.execute(f"select val from {table} where val is not null") + result = cursor.fetchval() + assert result == value + cursor.execute(f"drop table {table}") + with pytest.raises(pyodbc.Error): + cursor.execute(f"create table {table} (val nchar(32768))") + + +def test_nvarchar(cursor: pyodbc.Cursor): + """Test the Informix NVARCHAR type""" + + lengths = 10, 100, 255 + table = "t1" + for length in lengths: + value = _generate_str(length) + cursor.execute(f"create table {table} (val nvarchar({length}))") + cursor.execute(f"insert into {table} values (?)", value) + cursor.execute(f"select val from {table}") + result = cursor.fetchval() + assert result == value + cursor.execute(f"drop table {table}") + with pytest.raises(pyodbc.Error): + cursor.execute(f"create table {table} (val nvarchar(256))") + + +def test_real(cursor: pyodbc.Cursor): + """Test the Informix REAL data type (synonym for SMALLFLOAT)""" + + values = [None, -200, -1, 0, 1, 1234.5, -200, .00012345] + for typename in ("real", "smallfloat"): + cursor.execute(f"create table t1 (i int, f {typename})") + for i, v in enumerate(values, 1): + cursor.execute("insert into t1 values (?, ?)", (i, v)) + cursor.execute("select f from t1 order by i") + results = [row.f for row in cursor.fetchall()] + assert pytest.approx(results) == values + cursor.execute(f"drop table t1") + + +def test_row(cursor: pyodbc.Cursor): + """Test the Informix ROW data type (SQL type -105, not yet directly supported)""" + + # Named ROW type + values = [(1, "Joe", "123 Main", "Our Town"), (2, "Ann", "23 Skidoo", "Kalamazoo")] + cursor.execute("create row type address_t (street varchar(20), city varchar(20))") + cursor.execute("create table t1 (id int, name varchar(20), address address_t)") + for id, name, street, city in values: + cursor.execute("insert into t1 values (?, ?, ?)", (id, name, f"row('{street}', '{city}')")) + cursor.execute("select * from t1") + with pytest.raises(pyodbc.ProgrammingError): + row = cursor.fetchall() + cursor.execute("select id, name, address.street, address.city from t1 order by id") + results = [tuple(row) for row in cursor.fetchall()] + assert results == values + cursor.execute("drop table t1") + + # Unnamed ROW type + values = [(1, "Raul", "Kaminsky"), (2, "Esmé", "Squalor")] + cursor.execute("create table t1 (id int, name row(given varchar(50), family varchar(50)))") + for id, forename, surname in values: + cursor.execute("insert into t1 values (?, ?)", (id, f"row('{forename}', '{surname}')")) + cursor.execute("select id, name.given, name.family from t1 order by id") + results = [tuple(row) for row in cursor.fetchall()] + assert results == values + cursor.execute("drop table t1") + + # Table based on named ROW type + wettest = [ + (1, "Hawaii", 63.7), + (2, "Louisiana", 60.1), + (3, "Mississippi", 59), + (4, "Alabama", 58.3), + (5, "Florida", 54.5), + ] + cursor.execute("create row type annual_precipitation_t (rank int, state varchar(32), amount float)") + cursor.execute("create table rainfall of type annual_precipitation_t") + for values in wettest: + cursor.execute("insert into rainfall values (?, ?, ?)", values) + cursor.execute("select rank, state, amount from rainfall order by rank") + results = [tuple(row) for row in cursor.fetchall()] + assert pytest.approx(results) == wettest + + +def test_serial(cursor: pyodbc.Cursor): + """Test the Informix SERIAL type""" + + cursor.execute("create table t1 (id serial, name varchar(32))") + cursor.execute("insert into t1 (name) values (?)", "Manny") + cursor.execute("insert into t1 (name) values (?)", "Moe") + cursor.execute("insert into t1 (id, name) values (?, ?)", (2_147_483_647, "Jack")) + cursor.execute("select * from t1 order by id") + rows = [tuple(row) for row in cursor.fetchall()] + assert rows == [(1, "Manny"), (2, "Moe"), (2_147_483_647, "Jack")] + cursor.execute("drop table t1") + cursor.execute("create table t1 (id serial(501), name varchar(20))") + cursor.execute("insert into t1 (name) values (?)", "Matthew") + cursor.execute("insert into t1 (name) values (?)", "Mark") + cursor.execute("insert into t1 (id, name) values (?, ?)", (801, "Luke")) + cursor.execute("insert into t1 (name) values (?)", "John") + cursor.execute("select * from t1 order by id") + rows = [tuple(row) for row in cursor.fetchall()] + assert rows == [(501, "Matthew"), (502, "Mark"), (801, "Luke"), (802, "John")] + + +def test_serial8(cursor: pyodbc.Cursor): + """Test the Informix SERIAL8 type, including the reserved sentinel value""" + + values = [ + (9_000_000_000_000_000_001, "Antennaria neglecta"), + (9_000_000_000_000_000_002, "Antennaria plantaginifolia"), + (9_000_000_000_000_000_003, "Aquilegia canadensis"), + (9_000_000_000_000_000_004, "Arisaema triphyllum"), + ] + cursor.execute("create table t1 (id serial8(9000000000000000001), flower varchar(50))") + for _, flower in values: + cursor.execute("insert into t1 (flower) values (?)", flower) + cursor.execute("select * from t1 order by 1") + results = [tuple(row) for row in cursor.fetchall()] + assert results == values + + +def test_set(cursor: pyodbc.Cursor): + """Test the Informix SET data type + + This type (SQL type -108) is not yet directly supported, so we use a cast. + The semantics for this type match Python's (duplicate values are eliminated). + """ + + cursor.execute("create table t1 (id int, colors set(varchar(32) not null))") + cursor.execute("insert into t1 values (?, ?)", (1, "set{'blue', 'green', 'yellow'}")); + cursor.execute("insert into t1 values (?, ?)", (2, "set{'blue', 'green', 'yellow', 'blue'}")); + cursor.execute("select * from t1") + with pytest.raises(pyodbc.ProgrammingError): + rows = cursor.fetchall() + cursor.execute("select cast(colors as lvarchar(255)) as colors from t1 order by id") + expected = ["SET{'blue','green','yellow'}", "SET{'blue','green','yellow'}"] + observed = [row.colors for row in cursor.fetchall()] + assert observed == expected + + +def test_smallint(cursor: pyodbc.Cursor): + """Test the Informix SMALLINT type, including the reserved sentinel value""" + + cursor.execute("create table t1 (col smallint)") + for param in [-1, 0, 1, -32_767, 32_767]: + cursor.execute("truncate table t1") + cursor.commit() + cursor.execute("insert into t1 values (?)", param) + result = cursor.execute("select col from t1").fetchval() + assert result == param + reserved = -32_768 + with pytest.raises(pyodbc.Error): + cursor.execute("insert into t1 values (?)", reserved) + + +def test_text(cursor: pyodbc.Cursor): + """Test the TEXT data type + + We can get the data into the table, but we can't fetch it using ODBC (isql hangs, + so it's not a problem with pyodbc). By running this test by itself (thereby leaving + the data committed) we can confirm the data in the table is what it should be using + IBM's dbaccess client. Best we can do here in this test is fetch and confirm the + expected value lengths (in bytes). We can't even do a "select count(*) ... where + v = ?" because "Blobs are not allowed in this expression (-615)." + + This is an ancient data type, and it works more like a BLOB than what the name implies + (we have to pass the parameter value as bytes instead of a string, otherwise we get + "Illegal attempt to convert Text/Byte blob type (-608)"). + + There is a function provided by Informix for converting large objects to strings, + but when I tested it values over 2048 bytes long were truncated (even though the + documentation claims truncation doesn't kick in until 32000 bytes). You can cast + the text value to a smart character large object (CLOB) and get it out via the file + system. See what we're doing with test_blob() and test_clob() above. + + TODO: figure out if the bugs are in the driver manager or the ODBC driver. + """ + + cursor.execute("create table t1 (v text)") + lengths = None, 0, 100, 1000, 4000 + values = [_generate_str(length, encoding="utf-8") for length in lengths] + for value in values: + cursor.execute("insert into t1 values (?)", value) + cursor.commit() + cursor.execute("select length(v) as vlen from t1 where v is not null order by 1") + observed_lengths = [row.vlen for row in cursor.fetchall()] + expected_lengths = sorted([length for length in lengths if length is not None]) + assert observed_lengths == expected_lengths + + +def test_uuid(cursor: pyodbc.Cursor): + """Test creation of UUID + + Informix does not have a UUID column type, but it does provide a way to generate the values. + """ + + cursor.execute("""\ + create function make_uuid() returning char(36) + external name 'com.informix.judrs.IfxStrings.getUUID()' + language java; + grant execute on function make_uuid() to public; + """) + cursor.execute("create table t1 (uuid char(36), title varchar(255))") + cursor.execute("insert into t1 values (make_uuid(), 'Count of Monte Cristo')") + row = cursor.execute("select * from t1").fetchone() + assert row.title == "Count of Monte Cristo" + assert len(row.uuid) == 36 + uuid = UUID(row.uuid) + assert type(uuid) is UUID + assert str(uuid).lower() == row.uuid.lower() + + +def test_varchar(cursor: pyodbc.Cursor): + """Test the Informix CHARACTER VARYING (a.k.a. VARCHAR) type""" + + lengths = 10, 100, 255 + table = "t1" + for typename in ("character varying", "varchar"): + for length in lengths: + value = _generate_str(length) + cursor.execute(f"create table {table} (val {typename}({length}))") + cursor.execute(f"insert into {table} values (?)", value) + cursor.execute(f"select val from {table}") + result = cursor.fetchval() + assert result == value + cursor.execute(f"drop table {table}") + with pytest.raises(pyodbc.Error): + cursor.execute(f"create table {table} (val {typename}(256))") + + +#---------------------------------------------------------------------- +# User-defined functions and procedures +#---------------------------------------------------------------------- + +def test_create_function(cursor: pyodbc.Cursor): + """Test creating and invoking a custom function""" + + cursor.execute("drop function if exists add_numbers") + cursor.execute("""\ + create function add_numbers(a real, b real) + returning real + return a + b; + end function + """) + cursor.execute("create table t1 (id int, r1 real, r2 real)") + values = [(-2, 1.1), (42, 43.3333), (1234.56789, -1234.56789), (3.14, 1.414)] + expected = pytest.approx([(a + b) for a, b in values]) + for i, pair in enumerate(values, 1): + cursor.execute("insert into t1 values (?, ?, ?)", (i, *pair)) + cursor.execute("select add_numbers(r1, r2) as s from t1 order by id") + observed = [row.s for row in cursor] + assert observed == expected + + +def test_create_procedure(cursor: pyodbc.Cursor): + """Test creating and invoke a custom stored procedure""" + + cursor.execute("create table t1 (name varchar(100), value int)") + cursor.execute("drop procedure if exists insert_t1") + cursor.execute("""\ + create procedure insert_t1(p_name varchar(100), p_value int) + insert into t1 (name, value) values (p_name, p_value); + end procedure; + """) + values = [("Kathy", 98), ("Abdul", 86), ("Mac", 99), ("Rolf", 75)] + for n, v in values: + cursor.execute("execute procedure insert_t1(?, ?)", (n, v)) + cursor.execute("select value, name from t1 order by value") + observed = [tuple(row) for row in cursor.fetchall()] + expected = [(75, "Rolf"), (86, "Abdul"), (98, "Kathy"), (99, "Mac")] + assert observed == expected + + +#---------------------------------------------------------------------- +# Module-level properties and methods +#---------------------------------------------------------------------- + +def test_datasources(): + """Confirm that the dataSources() method is implemented and returns a list""" + + p = pyodbc.dataSources() + assert isinstance(p, dict) + + +def test_drivers(): + """Confirm that the drivers() method is implemented and returns a list""" + + p = pyodbc.drivers() + assert isinstance(p, list) + + +def test_lower_case(cursor: pyodbc.Cursor): + """Verify that pyodbc.lowercase forces returned column names to lowercase.""" + + passed = False + try: + pyodbc.lowercase = True + cursor.execute("create table t1 (Abc int, dEf int)") + cursor.execute("select * from t1") + names = {col[0] for col in cursor.description} + assert names == {"abc", "def"} + passed = True + finally: + pyodbc.lowercase = False + assert passed + + +def test_version(): + """Verify that we get a well-formed version string""" + assert len(pyodbc.version.split('.')) == 3 + + +#---------------------------------------------------------------------- +# Connection methods and properties +#---------------------------------------------------------------------- + +def test_autocommit(cursor: pyodbc.Cursor): + """Verify that autocommit properties for different connections are independent of each other""" + + assert cursor.connection.autocommit is False + othercnxn = connect(autocommit=True) + assert othercnxn.autocommit is True + othercnxn.autocommit = False + assert othercnxn.autocommit is False + + +def test_cnxn_execute_error(cursor: pyodbc.Cursor): + """Make sure that Connection.execute (not Cursor) errors are not "eaten" + + See GitHub issue #74 + """ + cursor.execute("create table t1(a int primary key)") + cursor.execute("insert into t1 values (1)") + with pytest.raises(pyodbc.Error): + cursor.connection.execute("insert into t1 values (1)") + + +def test_cnxn_set_attr(cursor: pyodbc.Cursor): + """We can't test more than that this doesn't crash + + The values are from the unixODBC sqlext.h header file. + """ + + SQL_ATTR_ACCESS_MODE = 101 + SQL_MODE_READ_ONLY = 1 + cursor.connection.set_attr(SQL_ATTR_ACCESS_MODE, SQL_MODE_READ_ONLY) + + +def test_cnxn_set_attr_before(): + """We can't test more than that this doesn't crash + + The value is from the unixODBC sqlext.h header file. + """ + + SQL_ATTR_PACKET_SIZE = 112 + _cnxn = connect(attrs_before={ SQL_ATTR_PACKET_SIZE : 1024 * 32 }) + + +def test_getinfo_bool(cursor: pyodbc.Cursor): + """Test getting an ODBC Boolean attribute""" + + value = cursor.connection.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) + assert isinstance(value, bool) + + +def test_getinfo_int(cursor: pyodbc.Cursor): + """Test getting an ODBC integer attribute""" + + value = cursor.connection.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) + assert isinstance(value, int) + + +def test_getinfo_smallint(cursor: pyodbc.Cursor): + """Test getting an ODBC small integer attribute""" + + value = cursor.connection.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) + assert isinstance(value, int) + + +def test_getinfo_string(cursor: pyodbc.Cursor): + """Test getting an ODBC string attribute""" + + value = cursor.connection.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) + assert isinstance(value, str) + + +def test_output_conversion(cursor: pyodbc.Cursor): + """Make sure output converters are invoked + + Note the use of SQL_VARCHAR, not SQL_WVARCHAR. + """ + + def convert(value): + # The value is the raw bytes (as a bytes object) read from the + # database. We'll simply add an X at the beginning at the end. + return "X" + value.decode("latin1") + "X" + + cursor.execute("create table t1(n int, v varchar(10))") + cursor.execute("insert into t1 values (1, '123.45')") + + cursor.connection.add_output_converter(pyodbc.SQL_VARCHAR, convert) + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "X123.45X" + + # Clear all conversions and try again. There should be no Xs this time. + cursor.connection.clear_output_converters() + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "123.45" + + # Same but clear using remove_output_converter. + cursor.connection.add_output_converter(pyodbc.SQL_VARCHAR, convert) + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "X123.45X" + + cursor.connection.remove_output_converter(pyodbc.SQL_VARCHAR) + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "123.45" + + # And lastly, clear by passing None for the converter. + cursor.connection.add_output_converter(pyodbc.SQL_VARCHAR, convert) + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "X123.45X" + + cursor.connection.add_output_converter(pyodbc.SQL_VARCHAR, None) + value = cursor.execute("select v from t1").fetchone()[0] + assert value == "123.45" + + +#---------------------------------------------------------------------- +# Cursor methods and properties +#---------------------------------------------------------------------- + +def test_cancel(cursor: pyodbc.Cursor): + """No reliable way to cause things to hang, so just make sure nothing blows up""" + + cursor.execute("select 1") + cursor.cancel() + + +def test_close_cnxn(cursor: pyodbc.Cursor): + """Make sure using a Cursor after closing its connection fails.""" + + cursor.execute("create table t1 (id integer, s varchar(20))") + cursor.execute("insert into t1 values (?,?)", 1, 'test') + cursor.execute("select * from t1") + + cursor.connection.close() + + # Now that the connection is closed, we expect an exception. (If the code attempts to use + # the HSTMT, we'll get an access violation instead.) + + with pytest.raises(pyodbc.ProgrammingError): + cursor.execute("select * from t1") + + +def test_columns(cursor: pyodbc.Cursor): + """Make sure the driver handles SQLColumnsW() correctly""" + + expected = { + "a": {"type_name": "INTEGER", "column_size": 10, "buffer_length": 4}, + "b": {"type_name": "VARCHAR", "column_size": 3, "buffer_length": 3}, + "ώ": {"type_name": "VARCHAR", "column_size": 4, "buffer_length": 4}, + } + cursor.execute("create table t1(a int, b varchar(3), ώ varchar(4))") + cursor.columns("t1") + observed = {row.column_name: row for row in cursor} + for name, column in observed.items(): + for key in expected[name]: + assert getattr(column, key) == expected[name][key] + cursor.columns("t1", schema=None, catalog=None) + observed = {row.column_name: row for row in cursor} + for name, column in observed.items(): + for key in expected[name]: + assert getattr(column, key) == expected[name][key] + + +def test_exc_integrity(cursor: pyodbc.Cursor): + """Make sure an IntegretyError is raised + + This is really making sure we are properly encoding and comparing the SQLSTATEs. + """ + cursor.execute("create table t1(s1 varchar(10) primary key)") + cursor.execute("insert into t1 values ('one')") + with pytest.raises(pyodbc.IntegrityError): + cursor.execute("insert into t1 values ('one')") + + +def test_executemany(cursor: pyodbc.Cursor): + """Make sure executemany() works correctly""" + + # Without fast_executemany + params = [(i, str(i)) for i in range(1, 6)] + cursor.execute("create table t1 (col1 int, col2 varchar(10))") + cursor.executemany("insert into t1 (col1, col2) values (?, ?)", params) + cursor.execute("select col1, col2 from t1 order by col1") + results = [tuple(row) for row in cursor] + assert results == params + cursor.execute("drop table t1") + + # With fast_executemany + pyodbc.fast_executemany = True + cursor.execute("create table t1 (col1 int, col2 varchar(10))") + cursor.executemany("insert into t1(col1, col2) values (?,?)", params) + cursor.execute("select col1, col2 from t1 order by col1") + results = [tuple(row) for row in cursor] + assert results == params + pyodbc.fast_executemany = False + + +def test_executemany_failure(cursor: pyodbc.Cursor): + """Confirm that an exception is raised if one query in an executemany fails""" + + cursor.execute("create table t1(a int, b varchar(10))") + params = [(1, 'good'), ('error', 'not an int'), (3, 'good')] + with pytest.raises(pyodbc.Error): + cursor.executemany("insert into t1 (a, b) value (?, ?)", params) + rows = cursor.execute("select * from t1").fetchall() + assert rows == [] + + +def test_rowcount(): + """Make sure SQLRowCount() behavior complies with the specification""" + + cursor = connect().cursor() + assert cursor.rowcount == -1 + cursor.execute("drop table if exists t1") + cursor.execute("create table t1 (col int)") + count = 4 + for i in range(count): + cursor.execute("insert into t1 values (?)", i) + cursor.execute("select * from t1") + assert cursor.rowcount == -1 + cursor.execute("update t1 set col=col+1") + assert cursor.rowcount == count + cursor.execute("delete from t1") + assert cursor.rowcount == count + cursor.execute("delete from t1") + assert cursor.rowcount == 0 + + +#---------------------------------------------------------------------- +# Row class properties and methods +#---------------------------------------------------------------------- + +def test_row_description(cursor: pyodbc.Cursor): + """Verify that Cursor.description is accessible as Row.cursor_description""" + + cursor.execute("create table t1 (col1 int, col2 char(3))") + cursor.execute("insert into t1 values (1, 'abc')") + row = cursor.execute("select col1, col2 from t1").fetchone() + assert row.cursor_description == cursor.description + + +def test_row_repr(cursor: pyodbc.Cursor): + """Validate the serialization of Row objects""" + + cursor.execute("create table t1(a int, b int, c int, d int)") + cursor.execute("insert into t1 values(1,2,3,4)") + + row = cursor.execute("select * from t1").fetchone() + + result = str(row) + assert result == "(1, 2, 3, 4)" + + result = str(row[:-1]) + assert result == "(1, 2, 3)" + + result = str(row[:1]) + assert result == "(1,)" + + +def test_row_slicing(cursor: pyodbc.Cursor): + """Confirm that the Row class implements slicing correctly""" + + cursor.execute("create table t1 (a int, b int, c int, d int)") + cursor.execute("insert into t1 values(1,2,3,4)") + row = cursor.execute("select * from t1").fetchone() + assert row[:] is row + assert row[:-1] == (1, 2, 3) + assert row[0:4] is row + + +#---------------------------------------------------------------------- +# Testing of pyodbc internals +#---------------------------------------------------------------------- + +def test_refcount_encoding(): + """Confirm we handle the reference count to `encoding` properly + + In the past we freed a borrowed reference. This would cause a segfault. + See https://github.com/mkleehammer/pyodbc/issues/1343. + """ + + import sys + encoding = "utf-16le" + count_before = sys.getrefcount(encoding) + + def _test(): + # I've moved this into a function so the exception's stack trace will be freed under + # the covers when we leave the function. Otherwise we'd have a 2nd reference to + # `encoding` in the stack trace of the exception. + try: + cnxn = pyodbc.connect(CNXNSTR, encoding=encoding) + except: + pass + + for i in range(10): + _test() + + count_after = sys.getrefcount(encoding) + + assert count_after == count_before + + +#---------------------------------------------------------------------- +# Miscellaneous tests +#---------------------------------------------------------------------- + +def test_chinese(cursor: pyodbc.Cursor): + """Can we round-trip Unicode values?""" + + cursor.execute("create table t1 (col nvarchar(10))") + value = "我的" + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select col from t1").fetchval() + assert result == value + + +#---------------------------------------------------------------------- +# Skipped tests +#---------------------------------------------------------------------- + +@pytest.mark.skip("Informix uses file-based tracing instead of messages for the caller") +def test_cursor_messages(cursor: pyodbc.Cursor): + """Test the Cursor.messages attribute""" + + +@pytest.mark.skip(reason="driver returns 'An illegal character has been found in the statement (-202)' for this test") +def test_emoticons(cursor: pyodbc.Cursor): + """Verify that the back end handles 4-byte Unicode characters correctly""" + + v = "x \U0001F31C z" + cursor.execute("create table t1 (s varchar(100))") + cursor.execute("insert into t1 values (?)", v) + result = cursor.execute("select s from t1").fetchval() + assert result == v + cursor.execute("drop table t1") + cursor.execute("create table t1 (s varchar(100))") + cursor.execute(f"insert into t1 values ('{v}')") + result = cursor.execute("select s from t1").fetchval() + assert result == v + + +@pytest.mark.skip(reason="driver returns 'Communication link failure (-11010)' for this test") +def test_maxwrite(cursor: pyodbc.Cursor): + """Ensure that setting the maxwrite property doesn't break anything""" + + cursor.connection.maxwrite = 500 + cursor.execute("create table t1 (col lvarchar(2000))") + param = _generate_str(1000) + cursor.execute("insert into t1 values (?)", param) + cursor.execute("select col from t1") + result = cursor.fetchval() + assert result == param diff --git a/tests/old/README.md b/tests/old/README.md deleted file mode 100644 index 697a2f89..00000000 --- a/tests/old/README.md +++ /dev/null @@ -1,2 +0,0 @@ -These tests have not been ported to pytest. If you want to help, please do so and move up to -the tests directory. diff --git a/tests/old/accesstests.py b/tests/old/accesstests.py deleted file mode 100644 index 225ce02f..00000000 --- a/tests/old/accesstests.py +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/python - -usage="""\ -%(prog)s [options] filename - -Unit tests for Microsoft Access - -These run using the version from the 'build' directory, not the version -installed into the Python directories. You must run python setup.py build -before running the tests. - -To run, pass the file EXTENSION of an Access database on the command line: - - accesstests accdb - -An empty Access 2000 database (empty.mdb) or an empty Access 2007 database -(empty.accdb), are automatically created for the tests. - -To run a single test, use the -t option: - - accesstests -t unicode_null accdb - -If you want to report an error, it would be helpful to include the driver information -by using the verbose flag and redirecting the output to a file: - - accesstests -v accdb >& results.txt - -You can pass the verbose flag twice for more verbose output: - - accesstests -vv accdb -""" - -# Access SQL data types: http://msdn2.microsoft.com/en-us/library/bb208866.aspx - -import sys, os, re -import unittest -from decimal import Decimal -from datetime import datetime, date, time -from os.path import abspath, dirname, join -import shutil -from testutils import * - -CNXNSTRING = None - -_TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' - -def _generate_test_string(length): - """ - Returns a string of composed of `seed` to make a string `length` characters long. - - To enhance performance, there are 3 ways data is read, based on the length of the value, so most data types are - tested with 3 lengths. This function helps us generate the test data. - - We use a recognizable data set instead of a single character to make it less likely that "overlap" errors will - be hidden and to help us manually identify where a break occurs. - """ - if length <= len(_TESTSTR): - return _TESTSTR[:length] - - c = (length + len(_TESTSTR)-1) // len(_TESTSTR) - v = _TESTSTR * c - return v[:length] - - -class AccessTestCase(unittest.TestCase): - - SMALL_FENCEPOST_SIZES = [ 0, 1, 254, 255 ] # text fields <= 255 - LARGE_FENCEPOST_SIZES = [ 256, 270, 304, 508, 510, 511, 512, 1023, 1024, 2047, 2048, 4000, 4095, 4096, 4097, 10 * 1024, 20 * 1024 ] - - CHAR_FENCEPOSTS = [ _generate_test_string(size) for size in SMALL_FENCEPOST_SIZES ] - IMAGE_FENCEPOSTS = CHAR_FENCEPOSTS + [ _generate_test_string(size) for size in LARGE_FENCEPOST_SIZES ] - - def __init__(self, method_name): - unittest.TestCase.__init__(self, method_name) - - def setUp(self): - self.cnxn = pyodbc.connect(CNXNSTRING) - self.cursor = self.cnxn.cursor() - - for i in range(3): - try: - self.cursor.execute("drop table t%d" % i) - self.cnxn.commit() - except: - pass - - self.cnxn.rollback() - - def tearDown(self): - try: - self.cursor.close() - self.cnxn.close() - except: - # If we've already closed the cursor or connection, exceptions are thrown. - pass - - def test_closed_reflects_connection_state(self): - self.assertFalse(self.cnxn.closed) - self.cnxn.close() - self.assertTrue(self.cnxn.closed) - - def test_multiple_bindings(self): - "More than one bind and select on a cursor" - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t1 values (?)", 2) - self.cursor.execute("insert into t1 values (?)", 3) - for i in range(3): - self.cursor.execute("select n from t1 where n < ?", 10) - self.cursor.execute("select n from t1 where n < 3") - - - def test_different_bindings(self): - self.cursor.execute("create table t1(n int)") - self.cursor.execute("create table t2(d datetime)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t2 values (?)", datetime.now()) - - def test_drivers(self): - p = pyodbc.drivers() - self.assertTrue(isinstance(p, list)) - - def test_datasources(self): - p = pyodbc.dataSources() - self.assertTrue(isinstance(p, dict)) - - def test_getinfo_string(self): - value = self.cnxn.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) - self.assertTrue(isinstance(value, str)) - - def test_getinfo_bool(self): - value = self.cnxn.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) - self.assertTrue(isinstance(value, bool)) - - def test_getinfo_int(self): - value = self.cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertTrue(isinstance(value, int)) - - def test_getinfo_smallint(self): - value = self.cnxn.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) - self.assertTrue(isinstance(value, int)) - - def _test_strtype(self, sqltype, value, colsize=None): - """ - The implementation for string, Unicode, and binary tests. - """ - assert colsize is None or (value is None or colsize >= len(value)), 'colsize={} value={}'.format(colsize, (value is None) and 'none' or len(value)) - - if colsize: - sql = "create table t1(n1 int not null, s1 {}({}), s2 {}({}))".format(sqltype, colsize, sqltype, colsize) - else: - sql = "create table t1(n1 int not null, s1 {}, s2 {})".format(sqltype, sqltype) - - self.cursor.execute(sql) - self.cursor.execute("insert into t1 values(1, ?, ?)", (value, value)) - row = self.cursor.execute("select s1, s2 from t1").fetchone() - - for i in range(2): - v = row[i] - - self.assertEqual(type(v), type(value)) - - if value is not None: - self.assertEqual(len(v), len(value)) - - self.assertEqual(v, value) - - - def test_varchar_null(self): - self._test_strtype('varchar', None, 255) - - # Generate a test for each fencepost size: test_varchar_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('varchar', value, len(value)) - t.__doc__ = 'varchar %s' % len(value) - return t - for value in CHAR_FENCEPOSTS: - locals()['test_varchar_%s' % len(value)] = _maketest(value) - - # - # binary - # - - def test_null_binary(self): - self._test_strtype('binary', None) - - # Generate a test for each fencepost size: test_varchar_0, etc. - def _maketest(value): - def t(self): - # Convert to UTF-8 to create a byte array - self._test_strtype('varbinary', value.encode('utf-8'), len(value)) - t.__doc__ = 'binary %s' % len(value) - return t - for value in CHAR_FENCEPOSTS: - locals()['test_binary_%s' % len(value)] = _maketest(value) - - - # # - # # image - # # - - # def test_null_image(self): - # self._test_strtype('image', None) - - # # Generate a test for each fencepost size: test_varchar_0, etc. - # def _maketest(value): - # def t(self): - # self._test_strtype('image', value.encode('utf-8')) - # t.__doc__ = 'image %s' % len(value) - # return t - # for value in IMAGE_FENCEPOSTS: - # locals()['test_image_%s' % len(value)] = _maketest(value) - - # - # memo - # - - def test_null_memo(self): - self._test_strtype('memo', None) - - # Generate a test for each fencepost size: test_varchar_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('memo', value) - t.__doc__ = 'Unicode to memo %s' % len(value) - return t - for value in IMAGE_FENCEPOSTS: - locals()['test_memo_%s' % len(value)] = _maketest(value) - - def test_subquery_params(self): - """Ensure parameter markers work in a subquery""" - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - row = self.cursor.execute(""" - select x.id - from ( - select id - from t1 - where s = ? - and id between ? and ? - ) x - """, 'test', 1, 10).fetchone() - self.assertNotEqual(row, None) - self.assertEqual(row[0], 1) - - def _exec(self): - self.cursor.execute(self.sql) - - def test_close_cnxn(self): - """Make sure using a Cursor after closing its connection doesn't crash.""" - - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - self.cursor.execute("select * from t1") - - self.cnxn.close() - - # Now that the connection is closed, we expect an exception. (If the code attempts to use - # the HSTMT, we'll get an access violation instead.) - self.sql = "select * from t1" - self.assertRaises(pyodbc.ProgrammingError, self._exec) - - - def test_unicode_query(self): - self.cursor.execute("select 1") - - def test_negative_row_index(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "1") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row[0], "1") - self.assertEqual(row[-1], "1") - - def test_version(self): - self.assertEqual(3, len(pyodbc.version.split('.'))) # 1.3.1 etc. - - # - # date, time, datetime - # - - def test_datetime(self): - value = datetime(2007, 1, 15, 3, 4, 5) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(value, result) - - # - # ints and floats - # - - def test_int(self): - value = 1234 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_int(self): - value = -1 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_smallint(self): - value = 32767 - self.cursor.execute("create table t1(n smallint)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_real(self): - value = 1234.5 - self.cursor.execute("create table t1(n real)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_real(self): - value = -200.5 - self.cursor.execute("create table t1(n real)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(value, result) - - def test_float(self): - value = 1234.567 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_float(self): - value = -200.5 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(value, result) - - def test_tinyint(self): - self.cursor.execute("create table t1(n tinyint)") - value = 10 - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(type(result), type(value)) - self.assertEqual(value, result) - - # - # decimal & money - # - - def test_decimal(self): - value = Decimal('12345.6789') - self.cursor.execute("create table t1(n numeric(10,4))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(type(v), Decimal) - self.assertEqual(v, value) - - def test_money(self): - self.cursor.execute("create table t1(n money)") - value = Decimal('1234.45') - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(type(result), type(value)) - self.assertEqual(value, result) - - def test_negative_decimal_scale(self): - value = Decimal('-10.0010') - self.cursor.execute("create table t1(d numeric(19,4))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), Decimal) - self.assertEqual(v, value) - - # - # bit - # - - def test_bit(self): - self.cursor.execute("create table t1(b bit)") - - value = True - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select b from t1").fetchone()[0] - self.assertEqual(type(result), bool) - self.assertEqual(value, result) - - def test_bit_null(self): - self.cursor.execute("create table t1(b bit)") - - value = None - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select b from t1").fetchone()[0] - self.assertEqual(type(result), bool) - self.assertEqual(False, result) - - def test_guid(self): - value = "de2ac9c6-8676-4b0b-b8a6-217a8580cbee" - self.cursor.execute("create table t1(g1 uniqueidentifier)") - self.cursor.execute("insert into t1 values (?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), type(value)) - self.assertEqual(len(v), len(value)) - - - # - # rowcount - # - - def test_rowcount_delete(self): - self.assertEqual(self.cursor.rowcount, -1) - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, count) - - def test_rowcount_nodata(self): - """ - This represents a different code path than a delete that deleted something. - - The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over - the code that errors out and drop down to the same SQLRowCount code. On the other hand, we could hardcode a - zero return value. - """ - self.cursor.execute("create table t1(i int)") - # This is a different code path internally. - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, 0) - - def test_rowcount_select(self): - """ - Ensure Cursor.rowcount is set properly after a select statement. - - pyodbc calls SQLRowCount after each execute and sets Cursor.rowcount, but SQL Server 2005 returns -1 after a - select statement, so we'll test for that behavior. This is valid behavior according to the DB API - specification, but people don't seem to like it. - """ - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("select * from t1") - self.assertEqual(self.cursor.rowcount, -1) - - rows = self.cursor.fetchall() - self.assertEqual(len(rows), count) - self.assertEqual(self.cursor.rowcount, -1) - - def test_rowcount_reset(self): - "Ensure rowcount is reset to -1" - - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.assertEqual(self.cursor.rowcount, 1) - - self.cursor.execute("create table t2(i int)") - self.assertEqual(self.cursor.rowcount, -1) - - # - # Misc - # - - def test_lower_case(self): - "Ensure pyodbc.lowercase forces returned column names to lowercase." - - # Has to be set before creating the cursor, so we must recreate self.cursor. - - pyodbc.lowercase = True - self.cursor = self.cnxn.cursor() - - self.cursor.execute("create table t1(Abc int, dEf int)") - self.cursor.execute("select * from t1") - - names = [ t[0] for t in self.cursor.description ] - names.sort() - - self.assertEqual(names, [ "abc", "def" ]) - - # Put it back so other tests don't fail. - pyodbc.lowercase = False - - def test_row_description(self): - """ - Ensure Cursor.description is accessible as Row.cursor_description. - """ - self.cursor = self.cnxn.cursor() - self.cursor.execute("create table t1(a int, b char(3))") - self.cnxn.commit() - self.cursor.execute("insert into t1 values(1, 'abc')") - - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(self.cursor.description, row.cursor_description) - - - def test_executemany(self): - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (i, str(i)) for i in range(1, 6) ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - - def test_executemany_failure(self): - """ - Ensure that an exception is raised if one query in an executemany fails. - """ - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, 'good'), - ('error', 'not an int'), - (3, 'good') ] - - self.assertRaises(pyodbc.Error, self.cursor.executemany, "insert into t1(a, b) value (?, ?)", params) - - - def test_row_slicing(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = row[:] - self.assertTrue(result is row) - - result = row[:-1] - self.assertEqual(result, (1,2,3)) - - result = row[0:4] - self.assertTrue(result is row) - - - def test_row_repr(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = str(row) - self.assertEqual(result, "(1, 2, 3, 4)") - - result = str(row[:-1]) - self.assertEqual(result, "(1, 2, 3)") - - result = str(row[:1]) - self.assertEqual(result, "(1,)") - - - def test_concatenation(self): - v2 = '0123456789' * 25 - v3 = '9876543210' * 25 - value = v2 + 'x' + v3 - - self.cursor.execute("create table t1(c2 varchar(250), c3 varchar(250))") - self.cursor.execute("insert into t1(c2, c3) values (?,?)", v2, v3) - - row = self.cursor.execute("select c2 + 'x' + c3 from t1").fetchone() - - self.assertEqual(row[0], value) - - - def test_autocommit(self): - self.assertEqual(self.cnxn.autocommit, False) - - othercnxn = pyodbc.connect(CNXNSTRING, autocommit=True) - self.assertEqual(othercnxn.autocommit, True) - - othercnxn.autocommit = False - self.assertEqual(othercnxn.autocommit, False) - - -def main(): - from argparse import ArgumentParser - parser = ArgumentParser(usage=usage) - parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") - parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") - parser.add_argument("-t", "--test", help="run only the named test") - parser.add_argument('type', choices=['accdb', 'mdb'], help='Which type of file to test') - - args = parser.parse_args() - - DRIVERS = { - 'accdb': 'Microsoft Access Driver (*.mdb, *.accdb)', - 'mdb': 'Microsoft Access Driver (*.mdb)' - } - - here = dirname(abspath(__file__)) - src = join(here, 'empty.' + args.type) - dest = join(here, 'test.' + args.type) - shutil.copy(src, dest) - - global CNXNSTRING - CNXNSTRING = 'DRIVER={{{}}};DBQ={};ExtendedAnsiSQL=1'.format(DRIVERS[args.type], dest) - print(CNXNSTRING) - - if args.verbose: - cnxn = pyodbc.connect(CNXNSTRING) - print_library_info(cnxn) - cnxn.close() - - suite = load_tests(AccessTestCase, args.test) - - testRunner = unittest.TextTestRunner(verbosity=args.verbose) - result = testRunner.run(suite) - - return result - - -if __name__ == '__main__': - - # Add the build directory to the path so we're testing the latest build, not the installed version. - add_to_path() - import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) diff --git a/tests/old/exceltests.py b/tests/old/exceltests.py deleted file mode 100644 index e00290ca..00000000 --- a/tests/old/exceltests.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/python - -# Tests for reading from Excel files. -# -# I have not been able to successfully create or modify Excel files. - -import sys, os, re -import unittest -from os.path import abspath -from testutils import * - -CNXNSTRING = None - -class ExcelTestCase(unittest.TestCase): - - def __init__(self, method_name): - unittest.TestCase.__init__(self, method_name) - - def setUp(self): - self.cnxn = pyodbc.connect(CNXNSTRING, autocommit=True) - self.cursor = self.cnxn.cursor() - - for i in range(3): - try: - self.cursor.execute("drop table t%d" % i) - self.cnxn.commit() - except: - pass - - self.cnxn.rollback() - - def tearDown(self): - try: - self.cursor.close() - self.cnxn.close() - except: - # If we've already closed the cursor or connection, exceptions are thrown. - pass - - def test_getinfo_string(self): - value = self.cnxn.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) - self.assertTrue(isinstance(value, str)) - - def test_getinfo_bool(self): - value = self.cnxn.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) - self.assertTrue(isinstance(value, bool)) - - def test_getinfo_int(self): - value = self.cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertTrue(isinstance(value, (int, long))) - - def test_getinfo_smallint(self): - value = self.cnxn.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) - self.assertTrue(isinstance(value, int)) - - - def test_read_sheet(self): - # The first method of reading data is to access worksheets by name in this format [name$]. - # - # Our second sheet is named Sheet2 and has two columns. The first has values 10, 20, 30, etc. - - rows = self.cursor.execute("select * from [Sheet2$]").fetchall() - self.assertEqual(len(rows), 5) - - for index, row in enumerate(rows): - self.assertEqual(row.s2num, float(index + 1) * 10) - - def test_read_range(self): - # The second method of reading data is to assign a name to a range of cells and access that as a table. - # - # Our first worksheet has a section named Table1. The first column has values 1, 2, 3, etc. - - rows = self.cursor.execute("select * from Table1").fetchall() - self.assertEqual(len(rows), 10) - - for index, row in enumerate(rows): - self.assertEqual(row.num, float(index + 1)) - self.assertEqual(row.val, chr(ord('a') + index)) - - def test_tables(self): - # This is useful for figuring out what is available - tables = [ row.table_name for row in self.cursor.tables() ] - assert 'Sheet2$' in tables, 'tables: %s' % ' '.join(tables) - - - # def test_append(self): - # rows = self.cursor.execute("select s2num, s2val from [Sheet2$]").fetchall() - # - # print rows - # - # nextnum = max([ row.s2num for row in rows ]) + 10 - # - # self.cursor.execute("insert into [Sheet2$](s2num, s2val) values (?, 'z')", nextnum) - # - # row = self.cursor.execute("select s2num, s2val from [Sheet2$] where s2num=?", nextnum).fetchone() - # self.assertTrue(row) - # - # print 'added:', nextnum, len(rows), 'rows' - # - # self.assertEqual(row.s2num, nextnum) - # self.assertEqual(row.s2val, 'z') - # - # self.cnxn.commit() - - -def main(): - from argparse import ArgumentParser - parser = ArgumentParser() #usage=usage) - parser.add_argument("-v", "--verbose", action="count", help="increment test verbosity (can be used multiple times)") - parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") - parser.add_argument("-t", "--test", help="run only the named test") - parser.add_argument("conn_str", nargs="*", help="connection string for Excel") - - args = parser.parse_args() - - if args.conn_str: - parser.error('no arguments expected') - - global CNXNSTRING - - path = dirname(abspath(__file__)) - filename = join(path, 'test.xls') - assert os.path.exists(filename) - CNXNSTRING = 'Driver={Microsoft Excel Driver (*.xls)};DBQ=%s;READONLY=FALSE' % filename - - if args.verbose: - cnxn = pyodbc.connect(CNXNSTRING, autocommit=True) - print_library_info(cnxn) - cnxn.close() - - suite = load_tests(ExcelTestCase, args.test) - - testRunner = unittest.TextTestRunner(verbosity=args.verbose) - result = testRunner.run(suite) - - return result - - -if __name__ == '__main__': - - # Add the build directory to the path so we're testing the latest build, not the installed version. - add_to_path() - import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) diff --git a/tests/old/informix_test.py b/tests/old/informix_test.py deleted file mode 100644 index 2c484780..00000000 --- a/tests/old/informix_test.py +++ /dev/null @@ -1,1265 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin-1 -*- - -usage = """\ -%(prog)s [options] connection_string - -Unit tests for Informix DB. To use, pass a connection string as the parameter. -The tests will create and drop tables t1 and t2 as necessary. - -These run using the version from the 'build' directory, not the version -installed into the Python directories. You must run python setup.py build -before running the tests. - -You can also put the connection string into a tmp/setup.cfg file like so: - - [informixtests] - connection-string=DRIVER={IBM INFORMIX ODBC DRIVER (64-bit)};SERVER=localhost;UID=uid;PWD=pwd;DATABASE=db -""" - -import sys, os, re -import unittest -from decimal import Decimal -from datetime import datetime, date, time -from os.path import join, getsize, dirname, abspath -from testutils import * - -_TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' - -def _generate_test_string(length): - """ - Returns a string of `length` characters, constructed by repeating _TESTSTR as necessary. - - To enhance performance, there are 3 ways data is read, based on the length of the value, so most data types are - tested with 3 lengths. This function helps us generate the test data. - - We use a recognizable data set instead of a single character to make it less likely that "overlap" errors will - be hidden and to help us manually identify where a break occurs. - """ - if length <= len(_TESTSTR): - return _TESTSTR[:length] - - c = (length + len(_TESTSTR)-1) / len(_TESTSTR) - v = _TESTSTR * c - return v[:length] - -class InformixTestCase(unittest.TestCase): - - SMALL_FENCEPOST_SIZES = [ 0, 1, 255, 256, 510, 511, 512, 1023, 1024, 2047, 2048, 4000 ] - LARGE_FENCEPOST_SIZES = [ 4095, 4096, 4097, 10 * 1024, 20 * 1024 ] - - ANSI_FENCEPOSTS = [ _generate_test_string(size) for size in SMALL_FENCEPOST_SIZES ] - UNICODE_FENCEPOSTS = [ unicode(s) for s in ANSI_FENCEPOSTS ] - IMAGE_FENCEPOSTS = ANSI_FENCEPOSTS + [ _generate_test_string(size) for size in LARGE_FENCEPOST_SIZES ] - - def __init__(self, method_name, connection_string): - unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string - - def setUp(self): - self.cnxn = pyodbc.connect(self.connection_string) - self.cursor = self.cnxn.cursor() - - for i in range(3): - try: - self.cursor.execute("drop table t%d" % i) - self.cnxn.commit() - except: - pass - - for i in range(3): - try: - self.cursor.execute("drop procedure proc%d" % i) - self.cnxn.commit() - except: - pass - - try: - self.cursor.execute('drop function func1') - self.cnxn.commit() - except: - pass - - self.cnxn.rollback() - - def tearDown(self): - try: - self.cursor.close() - self.cnxn.close() - except: - # If we've already closed the cursor or connection, exceptions are thrown. - pass - - def test_multiple_bindings(self): - "More than one bind and select on a cursor" - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t1 values (?)", 2) - self.cursor.execute("insert into t1 values (?)", 3) - for i in range(3): - self.cursor.execute("select n from t1 where n < ?", 10) - self.cursor.execute("select n from t1 where n < 3") - - - def test_different_bindings(self): - self.cursor.execute("create table t1(n int)") - self.cursor.execute("create table t2(d datetime)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t2 values (?)", datetime.now()) - - def test_drivers(self): - p = pyodbc.drivers() - self.assertTrue(isinstance(p, list)) - - def test_datasources(self): - p = pyodbc.dataSources() - self.assertTrue(isinstance(p, dict)) - - def test_getinfo_string(self): - value = self.cnxn.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) - self.assertTrue(isinstance(value, str)) - - def test_getinfo_bool(self): - value = self.cnxn.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) - self.assertTrue(isinstance(value, bool)) - - def test_getinfo_int(self): - value = self.cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertTrue(isinstance(value, (int, long))) - - def test_getinfo_smallint(self): - value = self.cnxn.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) - self.assertTrue(isinstance(value, int)) - - def test_noscan(self): - self.assertEqual(self.cursor.noscan, False) - self.cursor.noscan = True - self.assertEqual(self.cursor.noscan, True) - - def test_guid(self): - self.cursor.execute("create table t1(g1 uniqueidentifier)") - self.cursor.execute("insert into t1 values (newid())") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(len(v), 36) - - def test_nextset(self): - self.cursor.execute("create table t1(i int)") - for i in range(4): - self.cursor.execute("insert into t1(i) values(?)", i) - - self.cursor.execute("select i from t1 where i < 2 order by i; select i from t1 where i >= 2 order by i") - - for i, row in enumerate(self.cursor): - self.assertEqual(i, row.i) - - self.assertEqual(self.cursor.nextset(), True) - - for i, row in enumerate(self.cursor): - self.assertEqual(i + 2, row.i) - - def test_fixed_unicode(self): - value = u"t\xebsting" - self.cursor.execute("create table t1(s nchar(7))") - self.cursor.execute("insert into t1 values(?)", u"t\xebsting") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), unicode) - self.assertEqual(len(v), len(value)) # If we alloc'd wrong, the test below might work because of an embedded NULL - self.assertEqual(v, value) - - - def _test_strtype(self, sqltype, value, colsize=None): - """ - The implementation for string, Unicode, and binary tests. - """ - assert colsize is None or (value is None or colsize >= len(value)) - - if colsize: - sql = "create table t1(s %s(%s))" % (sqltype, colsize) - else: - sql = "create table t1(s %s)" % sqltype - - self.cursor.execute(sql) - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), type(value)) - - if value is not None: - self.assertEqual(len(v), len(value)) - - self.assertEqual(v, value) - - # Reported by Andy Hochhaus in the pyodbc group: In 2.1.7 and earlier, a hardcoded length of 255 was used to - # determine whether a parameter was bound as a SQL_VARCHAR or SQL_LONGVARCHAR. Apparently SQL Server chokes if - # we bind as a SQL_LONGVARCHAR and the target column size is 8000 or less, which is considers just SQL_VARCHAR. - # This means binding a 256 character value would cause problems if compared with a VARCHAR column under - # 8001. We now use SQLGetTypeInfo to determine the time to switch. - # - # [42000] [Microsoft][SQL Server Native Client 10.0][SQL Server]The data types varchar and text are incompatible in the equal to operator. - - self.cursor.execute("select * from t1 where s=?", value) - - - def _test_strliketype(self, sqltype, value, colsize=None): - """ - The implementation for text, image, ntext, and binary. - - These types do not support comparison operators. - """ - assert colsize is None or (value is None or colsize >= len(value)) - - if colsize: - sql = "create table t1(s %s(%s))" % (sqltype, colsize) - else: - sql = "create table t1(s %s)" % sqltype - - self.cursor.execute(sql) - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), type(value)) - - if value is not None: - self.assertEqual(len(v), len(value)) - - self.assertEqual(v, value) - - - # - # varchar - # - - def test_varchar_null(self): - self._test_strtype('varchar', None, 100) - - # Generate a test for each fencepost size: test_varchar_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('varchar', value, len(value)) - return t - for value in ANSI_FENCEPOSTS: - locals()['test_varchar_%s' % len(value)] = _maketest(value) - - def test_varchar_many(self): - self.cursor.execute("create table t1(c1 varchar(300), c2 varchar(300), c3 varchar(300))") - - v1 = 'ABCDEFGHIJ' * 30 - v2 = '0123456789' * 30 - v3 = '9876543210' * 30 - - self.cursor.execute("insert into t1(c1, c2, c3) values (?,?,?)", v1, v2, v3); - row = self.cursor.execute("select c1, c2, c3, len(c1) as l1, len(c2) as l2, len(c3) as l3 from t1").fetchone() - - self.assertEqual(v1, row.c1) - self.assertEqual(v2, row.c2) - self.assertEqual(v3, row.c3) - - def test_varchar_upperlatin(self): - self._test_strtype('varchar', '�') - - # - # unicode - # - - def test_unicode_null(self): - self._test_strtype('nvarchar', None, 100) - - # Generate a test for each fencepost size: test_unicode_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('nvarchar', value, len(value)) - return t - for value in UNICODE_FENCEPOSTS: - locals()['test_unicode_%s' % len(value)] = _maketest(value) - - def test_unicode_upperlatin(self): - self._test_strtype('varchar', '�') - - # - # binary - # - - def test_null_binary(self): - self._test_strtype('varbinary', None, 100) - - def test_large_null_binary(self): - # Bug 1575064 - self._test_strtype('varbinary', None, 4000) - - # Generate a test for each fencepost size: test_unicode_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('varbinary', buffer(value), len(value)) - return t - for value in ANSI_FENCEPOSTS: - locals()['test_binary_%s' % len(value)] = _maketest(value) - - # - # image - # - - def test_image_null(self): - self._test_strliketype('image', None) - - # Generate a test for each fencepost size: test_unicode_0, etc. - def _maketest(value): - def t(self): - self._test_strliketype('image', buffer(value)) - return t - for value in IMAGE_FENCEPOSTS: - locals()['test_image_%s' % len(value)] = _maketest(value) - - def test_image_upperlatin(self): - self._test_strliketype('image', buffer('�')) - - # - # text - # - - # def test_empty_text(self): - # self._test_strliketype('text', buffer('')) - - def test_null_text(self): - self._test_strliketype('text', None) - - # Generate a test for each fencepost size: test_unicode_0, etc. - def _maketest(value): - def t(self): - self._test_strliketype('text', value) - return t - for value in ANSI_FENCEPOSTS: - locals()['test_text_%s' % len(value)] = _maketest(value) - - def test_text_upperlatin(self): - self._test_strliketype('text', '�') - - # - # bit - # - - def test_bit(self): - value = True - self.cursor.execute("create table t1(b bit)") - self.cursor.execute("insert into t1 values (?)", value) - v = self.cursor.execute("select b from t1").fetchone()[0] - self.assertEqual(type(v), bool) - self.assertEqual(v, value) - - # - # decimal - # - - def _decimal(self, precision, scale, negative): - # From test provided by planders (thanks!) in Issue 91 - - self.cursor.execute("create table t1(d decimal(%s, %s))" % (precision, scale)) - - # Construct a decimal that uses the maximum precision and scale. - decStr = '9' * (precision - scale) - if scale: - decStr = decStr + "." + '9' * scale - if negative: - decStr = "-" + decStr - value = Decimal(decStr) - - self.cursor.execute("insert into t1 values(?)", value) - - v = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(v, value) - - def _maketest(p, s, n): - def t(self): - self._decimal(p, s, n) - return t - for (p, s, n) in [ (1, 0, False), - (1, 0, True), - (6, 0, False), - (6, 2, False), - (6, 4, True), - (6, 6, True), - (38, 0, False), - (38, 10, False), - (38, 38, False), - (38, 0, True), - (38, 10, True), - (38, 38, True) ]: - locals()['test_decimal_%s_%s_%s' % (p, s, n and 'n' or 'p')] = _maketest(p, s, n) - - - def test_decimal_e(self): - """Ensure exponential notation decimals are properly handled""" - value = Decimal((0, (1, 2, 3), 5)) # prints as 1.23E+7 - self.cursor.execute("create table t1(d decimal(10, 2))") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_subquery_params(self): - """Ensure parameter markers work in a subquery""" - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - row = self.cursor.execute(""" - select x.id - from ( - select id - from t1 - where s = ? - and id between ? and ? - ) x - """, 'test', 1, 10).fetchone() - self.assertNotEqual(row, None) - self.assertEqual(row[0], 1) - - def _exec(self): - self.cursor.execute(self.sql) - - def test_close_cnxn(self): - """Make sure using a Cursor after closing its connection doesn't crash.""" - - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - self.cursor.execute("select * from t1") - - self.cnxn.close() - - # Now that the connection is closed, we expect an exception. (If the code attempts to use - # the HSTMT, we'll get an access violation instead.) - self.sql = "select * from t1" - self.assertRaises(pyodbc.ProgrammingError, self._exec) - - def test_empty_string(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "") - - def test_fixed_str(self): - value = "testing" - self.cursor.execute("create table t1(s char(7))") - self.cursor.execute("insert into t1 values(?)", "testing") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(len(v), len(value)) # If we alloc'd wrong, the test below might work because of an embedded NULL - self.assertEqual(v, value) - - def test_empty_unicode(self): - self.cursor.execute("create table t1(s nvarchar(20))") - self.cursor.execute("insert into t1 values(?)", u"") - - def test_unicode_query(self): - self.cursor.execute(u"select 1") - - def test_negative_row_index(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "1") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row[0], "1") - self.assertEqual(row[-1], "1") - - def test_version(self): - self.assertEqual(3, len(pyodbc.version.split('.'))) # 1.3.1 etc. - - # - # date, time, datetime - # - - def test_datetime(self): - value = datetime(2007, 1, 15, 3, 4, 5) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(value), datetime) - self.assertEqual(value, result) - - def test_datetime_fraction(self): - # SQL Server supports milliseconds, but Python's datetime supports nanoseconds, so the most granular datetime - # supported is xxx000. - - value = datetime(2007, 1, 15, 3, 4, 5, 123000) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(value), datetime) - self.assertEqual(result, value) - - def test_datetime_fraction_rounded(self): - # SQL Server supports milliseconds, but Python's datetime supports nanoseconds. pyodbc rounds down to what the - # database supports. - - full = datetime(2007, 1, 15, 3, 4, 5, 123456) - rounded = datetime(2007, 1, 15, 3, 4, 5, 123000) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", full) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(result), datetime) - self.assertEqual(result, rounded) - - def test_date(self): - value = date.today() - - self.cursor.execute("create table t1(d date)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(type(value), date) - self.assertEqual(value, result) - - def test_time(self): - value = datetime.now().time() - - # We aren't yet writing values using the new extended time type so the value written to the database is only - # down to the second. - value = value.replace(microsecond=0) - - self.cursor.execute("create table t1(t time)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select t from t1").fetchone()[0] - self.assertEqual(type(value), time) - self.assertEqual(value, result) - - def test_datetime2(self): - value = datetime(2007, 1, 15, 3, 4, 5) - - self.cursor.execute("create table t1(dt datetime2)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(value), datetime) - self.assertEqual(value, result) - - # - # ints and floats - # - - def test_int(self): - value = 1234 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_int(self): - value = -1 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_bigint(self): - input = 3000000000 - self.cursor.execute("create table t1(d bigint)") - self.cursor.execute("insert into t1 values (?)", input) - result = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(result, input) - - def test_float(self): - value = 1234.567 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_float(self): - value = -200 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(value, result) - - - # - # stored procedures - # - - # def test_callproc(self): - # "callproc with a simple input-only stored procedure" - # pass - - def test_sp_results(self): - self.cursor.execute( - """ - Create procedure proc1 - AS - select top 10 name, id, xtype, refdate - from sysobjects - """) - rows = self.cursor.execute("exec proc1").fetchall() - self.assertEqual(type(rows), list) - self.assertEqual(len(rows), 10) # there has to be at least 10 items in sysobjects - self.assertEqual(type(rows[0].refdate), datetime) - - - def test_sp_results_from_temp(self): - - # Note: I've used "set nocount on" so that we don't get the number of rows deleted from #tmptable. - # If you don't do this, you'd need to call nextset() once to skip it. - - self.cursor.execute( - """ - Create procedure proc1 - AS - set nocount on - select top 10 name, id, xtype, refdate - into #tmptable - from sysobjects - - select * from #tmptable - """) - self.cursor.execute("exec proc1") - self.assertTrue(self.cursor.description is not None) - self.assertTrue(len(self.cursor.description) == 4) - - rows = self.cursor.fetchall() - self.assertEqual(type(rows), list) - self.assertEqual(len(rows), 10) # there has to be at least 10 items in sysobjects - self.assertEqual(type(rows[0].refdate), datetime) - - - def test_sp_results_from_vartbl(self): - self.cursor.execute( - """ - Create procedure proc1 - AS - set nocount on - declare @tmptbl table(name varchar(100), id int, xtype varchar(4), refdate datetime) - - insert into @tmptbl - select top 10 name, id, xtype, refdate - from sysobjects - - select * from @tmptbl - """) - self.cursor.execute("exec proc1") - rows = self.cursor.fetchall() - self.assertEqual(type(rows), list) - self.assertEqual(len(rows), 10) # there has to be at least 10 items in sysobjects - self.assertEqual(type(rows[0].refdate), datetime) - - def test_sp_with_dates(self): - # Reported in the forums that passing two datetimes to a stored procedure doesn't work. - self.cursor.execute( - """ - if exists (select * from dbo.sysobjects where id = object_id(N'[test_sp]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) - drop procedure [dbo].[test_sp] - """) - self.cursor.execute( - """ - create procedure test_sp(@d1 datetime, @d2 datetime) - AS - declare @d as int - set @d = datediff(year, @d1, @d2) - select @d - """) - self.cursor.execute("exec test_sp ?, ?", datetime.now(), datetime.now()) - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(rows[0][0] == 0) # 0 years apart - - def test_sp_with_none(self): - # Reported in the forums that passing None caused an error. - self.cursor.execute( - """ - if exists (select * from dbo.sysobjects where id = object_id(N'[test_sp]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) - drop procedure [dbo].[test_sp] - """) - self.cursor.execute( - """ - create procedure test_sp(@x varchar(20)) - AS - declare @y varchar(20) - set @y = @x - select @y - """) - self.cursor.execute("exec test_sp ?", None) - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(rows[0][0] == None) # 0 years apart - - - # - # rowcount - # - - def test_rowcount_delete(self): - self.assertEqual(self.cursor.rowcount, -1) - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, count) - - def test_rowcount_nodata(self): - """ - This represents a different code path than a delete that deleted something. - - The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over - the code that errors out and drop down to the same SQLRowCount code. On the other hand, we could hardcode a - zero return value. - """ - self.cursor.execute("create table t1(i int)") - # This is a different code path internally. - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, 0) - - def test_rowcount_select(self): - """ - Ensure Cursor.rowcount is set properly after a select statement. - - pyodbc calls SQLRowCount after each execute and sets Cursor.rowcount, but SQL Server 2005 returns -1 after a - select statement, so we'll test for that behavior. This is valid behavior according to the DB API - specification, but people don't seem to like it. - """ - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("select * from t1") - self.assertEqual(self.cursor.rowcount, -1) - - rows = self.cursor.fetchall() - self.assertEqual(len(rows), count) - self.assertEqual(self.cursor.rowcount, -1) - - def test_rowcount_reset(self): - "Ensure rowcount is reset to -1" - - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.assertEqual(self.cursor.rowcount, 1) - - self.cursor.execute("create table t2(i int)") - self.assertEqual(self.cursor.rowcount, -1) - - # - # always return Cursor - # - - # In the 2.0.x branch, Cursor.execute sometimes returned the cursor and sometimes the rowcount. This proved very - # confusing when things went wrong and added very little value even when things went right since users could always - # use: cursor.execute("...").rowcount - - def test_retcursor_delete(self): - self.cursor.execute("create table t1(i int)") - self.cursor.execute("insert into t1 values (1)") - v = self.cursor.execute("delete from t1") - self.assertEqual(v, self.cursor) - - def test_retcursor_nodata(self): - """ - This represents a different code path than a delete that deleted something. - - The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over - the code that errors out and drop down to the same SQLRowCount code. - """ - self.cursor.execute("create table t1(i int)") - # This is a different code path internally. - v = self.cursor.execute("delete from t1") - self.assertEqual(v, self.cursor) - - def test_retcursor_select(self): - self.cursor.execute("create table t1(i int)") - self.cursor.execute("insert into t1 values (1)") - v = self.cursor.execute("select * from t1") - self.assertEqual(v, self.cursor) - - # - # misc - # - - def test_lower_case(self): - "Ensure pyodbc.lowercase forces returned column names to lowercase." - - # Has to be set before creating the cursor, so we must recreate self.cursor. - - pyodbc.lowercase = True - self.cursor = self.cnxn.cursor() - - self.cursor.execute("create table t1(Abc int, dEf int)") - self.cursor.execute("select * from t1") - - names = [ t[0] for t in self.cursor.description ] - names.sort() - - self.assertEqual(names, [ "abc", "def" ]) - - # Put it back so other tests don't fail. - pyodbc.lowercase = False - - def test_row_description(self): - """ - Ensure Cursor.description is accessible as Row.cursor_description. - """ - self.cursor = self.cnxn.cursor() - self.cursor.execute("create table t1(a int, b char(3))") - self.cnxn.commit() - self.cursor.execute("insert into t1 values(1, 'abc')") - - row = self.cursor.execute("select * from t1").fetchone() - - self.assertEqual(self.cursor.description, row.cursor_description) - - - def test_temp_select(self): - # A project was failing to create temporary tables via select into. - self.cursor.execute("create table t1(s char(7))") - self.cursor.execute("insert into t1 values(?)", "testing") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(v, "testing") - - self.cursor.execute("select s into t2 from t1") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(v, "testing") - - - def test_money(self): - d = Decimal('123456.78') - self.cursor.execute("create table t1(i int identity(1,1), m money)") - self.cursor.execute("insert into t1(m) values (?)", d) - v = self.cursor.execute("select m from t1").fetchone()[0] - self.assertEqual(v, d) - - - def test_executemany(self): - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (i, str(i)) for i in range(1, 6) ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - - def test_executemany_one(self): - "Pass executemany a single sequence" - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, "test") ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - - def test_executemany_failure(self): - """ - Ensure that an exception is raised if one query in an executemany fails. - """ - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, 'good'), - ('error', 'not an int'), - (3, 'good') ] - - self.assertRaises(pyodbc.Error, self.cursor.executemany, "insert into t1(a, b) value (?, ?)", params) - - - def test_row_slicing(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = row[:] - self.assertTrue(result is row) - - result = row[:-1] - self.assertEqual(result, (1,2,3)) - - result = row[0:4] - self.assertTrue(result is row) - - - def test_row_repr(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = str(row) - self.assertEqual(result, "(1, 2, 3, 4)") - - result = str(row[:-1]) - self.assertEqual(result, "(1, 2, 3)") - - result = str(row[:1]) - self.assertEqual(result, "(1,)") - - - def test_concatenation(self): - v2 = '0123456789' * 30 - v3 = '9876543210' * 30 - - self.cursor.execute("create table t1(c1 int identity(1, 1), c2 varchar(300), c3 varchar(300))") - self.cursor.execute("insert into t1(c2, c3) values (?,?)", v2, v3) - - row = self.cursor.execute("select c2, c3, c2 + c3 as both from t1").fetchone() - - self.assertEqual(row.both, v2 + v3) - - def test_view_select(self): - # Reported in forum: Can't select from a view? I think I do this a lot, but another test never hurts. - - # Create a table (t1) with 3 rows and a view (t2) into it. - self.cursor.execute("create table t1(c1 int identity(1, 1), c2 varchar(50))") - for i in range(3): - self.cursor.execute("insert into t1(c2) values (?)", "string%s" % i) - self.cursor.execute("create view t2 as select * from t1") - - # Select from the view - self.cursor.execute("select * from t2") - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(len(rows) == 3) - - def test_autocommit(self): - self.assertEqual(self.cnxn.autocommit, False) - - othercnxn = pyodbc.connect(self.connection_string, autocommit=True) - self.assertEqual(othercnxn.autocommit, True) - - othercnxn.autocommit = False - self.assertEqual(othercnxn.autocommit, False) - - def test_unicode_results(self): - "Ensure unicode_results forces Unicode" - othercnxn = pyodbc.connect(self.connection_string, unicode_results=True) - othercursor = othercnxn.cursor() - - # ANSI data in an ANSI column ... - othercursor.execute("create table t1(s varchar(20))") - othercursor.execute("insert into t1 values(?)", 'test') - - # ... should be returned as Unicode - value = othercursor.execute("select s from t1").fetchone()[0] - self.assertEqual(value, u'test') - - - def test_informix_callproc(self): - try: - self.cursor.execute("drop procedure pyodbctest") - self.cnxn.commit() - except: - pass - - self.cursor.execute("create table t1(s varchar(10))") - self.cursor.execute("insert into t1 values(?)", "testing") - - self.cursor.execute(""" - create procedure pyodbctest @var1 varchar(32) - as - begin - select s - from t1 - return - end - """) - self.cnxn.commit() - - # for row in self.cursor.procedureColumns('pyodbctest'): - # print row.procedure_name, row.column_name, row.column_type, row.type_name - - self.cursor.execute("exec pyodbctest 'hi'") - - # print self.cursor.description - # for row in self.cursor: - # print row.s - - def test_skip(self): - # Insert 1, 2, and 3. Fetch 1, skip 2, fetch 3. - - self.cursor.execute("create table t1(id int)"); - for i in range(1, 5): - self.cursor.execute("insert into t1 values(?)", i) - self.cursor.execute("select id from t1 order by id") - self.assertEqual(self.cursor.fetchone()[0], 1) - self.cursor.skip(2) - self.assertEqual(self.cursor.fetchone()[0], 4) - - def test_timeout(self): - self.assertEqual(self.cnxn.timeout, 0) # defaults to zero (off) - - self.cnxn.timeout = 30 - self.assertEqual(self.cnxn.timeout, 30) - - self.cnxn.timeout = 0 - self.assertEqual(self.cnxn.timeout, 0) - - def test_sets_execute(self): - # Only lists and tuples are allowed. - def f(): - self.cursor.execute("create table t1 (word varchar (100))") - words = set (['a']) - self.cursor.execute("insert into t1 (word) VALUES (?)", [words]) - - self.assertRaises(pyodbc.ProgrammingError, f) - - def test_sets_executemany(self): - # Only lists and tuples are allowed. - def f(): - self.cursor.execute("create table t1 (word varchar (100))") - words = set (['a']) - self.cursor.executemany("insert into t1 (word) values (?)", [words]) - - self.assertRaises(TypeError, f) - - def test_row_execute(self): - "Ensure we can use a Row object as a parameter to execute" - self.cursor.execute("create table t1(n int, s varchar(10))") - self.cursor.execute("insert into t1 values (1, 'a')") - row = self.cursor.execute("select n, s from t1").fetchone() - self.assertNotEqual(row, None) - - self.cursor.execute("create table t2(n int, s varchar(10))") - self.cursor.execute("insert into t2 values (?, ?)", row) - - def test_row_executemany(self): - "Ensure we can use a Row object as a parameter to executemany" - self.cursor.execute("create table t1(n int, s varchar(10))") - - for i in range(3): - self.cursor.execute("insert into t1 values (?, ?)", i, chr(ord('a')+i)) - - rows = self.cursor.execute("select n, s from t1").fetchall() - self.assertNotEqual(len(rows), 0) - - self.cursor.execute("create table t2(n int, s varchar(10))") - self.cursor.executemany("insert into t2 values (?, ?)", rows) - - def test_description(self): - "Ensure cursor.description is correct" - - self.cursor.execute("create table t1(n int, s varchar(8), d decimal(5,2))") - self.cursor.execute("insert into t1 values (1, 'abc', '1.23')") - self.cursor.execute("select * from t1") - - # (I'm not sure the precision of an int is constant across different versions, bits, so I'm hand checking the - # items I do know. - - # int - t = self.cursor.description[0] - self.assertEqual(t[0], 'n') - self.assertEqual(t[1], int) - self.assertEqual(t[5], 0) # scale - self.assertEqual(t[6], True) # nullable - - # varchar(8) - t = self.cursor.description[1] - self.assertEqual(t[0], 's') - self.assertEqual(t[1], str) - self.assertEqual(t[4], 8) # precision - self.assertEqual(t[5], 0) # scale - self.assertEqual(t[6], True) # nullable - - # decimal(5, 2) - t = self.cursor.description[2] - self.assertEqual(t[0], 'd') - self.assertEqual(t[1], Decimal) - self.assertEqual(t[4], 5) # precision - self.assertEqual(t[5], 2) # scale - self.assertEqual(t[6], True) # nullable - - - def test_none_param(self): - "Ensure None can be used for params other than the first" - # Some driver/db versions would fail if NULL was not the first parameter because SQLDescribeParam (only used - # with NULL) could not be used after the first call to SQLBindParameter. This means None always worked for the - # first column, but did not work for later columns. - # - # If SQLDescribeParam doesn't work, pyodbc would use VARCHAR which almost always worked. However, - # binary/varbinary won't allow an implicit conversion. - - self.cursor.execute("create table t1(n int, blob varbinary(max))") - self.cursor.execute("insert into t1 values (1, newid())") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row.n, 1) - self.assertEqual(type(row.blob), buffer) - - self.cursor.execute("update t1 set n=?, blob=?", 2, None) - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row.n, 2) - self.assertEqual(row.blob, None) - - - def test_output_conversion(self): - def convert(value): - # `value` will be a string. We'll simply add an X at the beginning at the end. - return 'X' + value + 'X' - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) - self.cursor.execute("create table t1(n int, v varchar(10))") - self.cursor.execute("insert into t1 values (1, '123.45')") - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, 'X123.45X') - - # Now clear the conversions and try again. There should be no Xs this time. - self.cnxn.clear_output_converters() - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, '123.45') - - - def test_too_large(self): - """Ensure error raised if insert fails due to truncation""" - value = 'x' * 1000 - self.cursor.execute("create table t1(s varchar(800))") - def test(): - self.cursor.execute("insert into t1 values (?)", value) - self.assertRaises(pyodbc.DataError, test) - - def test_geometry_null_insert(self): - def convert(value): - return value - - self.cnxn.add_output_converter(-151, convert) # -151 is SQL Server's geometry - self.cursor.execute("create table t1(n int, v geometry)") - self.cursor.execute("insert into t1 values (?, ?)", 1, None) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, None) - self.cnxn.clear_output_converters() - - def test_login_timeout(self): - # This can only test setting since there isn't a way to cause it to block on the server side. - cnxns = pyodbc.connect(self.connection_string, timeout=2) - - def test_row_equal(self): - self.cursor.execute("create table t1(n int, s varchar(20))") - self.cursor.execute("insert into t1 values (1, 'test')") - row1 = self.cursor.execute("select n, s from t1").fetchone() - row2 = self.cursor.execute("select n, s from t1").fetchone() - b = (row1 == row2) - self.assertEqual(b, True) - - def test_row_gtlt(self): - self.cursor.execute("create table t1(n int, s varchar(20))") - self.cursor.execute("insert into t1 values (1, 'test1')") - self.cursor.execute("insert into t1 values (1, 'test2')") - rows = self.cursor.execute("select n, s from t1 order by s").fetchall() - self.assertTrue(rows[0] < rows[1]) - self.assertTrue(rows[0] <= rows[1]) - self.assertTrue(rows[1] > rows[0]) - self.assertTrue(rows[1] >= rows[0]) - self.assertTrue(rows[0] != rows[1]) - - rows = list(rows) - rows.sort() # uses < - - def test_context_manager(self): - with pyodbc.connect(self.connection_string) as cnxn: - cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - - # The connection should be closed now. - def test(): - cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertRaises(pyodbc.ProgrammingError, test) - - def test_untyped_none(self): - # From issue 129 - value = self.cursor.execute("select ?", None).fetchone()[0] - self.assertEqual(value, None) - - def test_large_update_nodata(self): - self.cursor.execute('create table t1(a varbinary(max))') - hundredkb = buffer('x'*100*1024) - self.cursor.execute('update t1 set a=? where 1=0', (hundredkb,)) - - def test_func_param(self): - self.cursor.execute(''' - create function func1 (@testparam varchar(4)) - returns @rettest table (param varchar(4)) - as - begin - insert @rettest - select @testparam - return - end - ''') - self.cnxn.commit() - value = self.cursor.execute("select * from func1(?)", 'test').fetchone()[0] - self.assertEqual(value, 'test') - - def test_no_fetch(self): - # Issue 89 with FreeTDS: Multiple selects (or catalog functions that issue selects) without fetches seem to - # confuse the driver. - self.cursor.execute('select 1') - self.cursor.execute('select 1') - self.cursor.execute('select 1') - - def test_drivers(self): - drivers = pyodbc.drivers() - self.assertEqual(list, type(drivers)) - self.assertTrue(len(drivers) > 1) - - m = re.search('DRIVER={?([^}]+?)}?;', self.connection_string, re.IGNORECASE) - current = m.group(1) - self.assertTrue(current in drivers) - - - -def main(): - from argparse import ArgumentParser - parser = ArgumentParser(usage=usage) - parser.add_argument("-v", "--verbose", action="count", help="increment test verbosity (can be used multiple times)") - parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") - parser.add_argument("-t", "--test", help="run only the named test") - parser.add_argument("conn_str", nargs="*", help="connection string for Informix") - - args = parser.parse_args() - - if len(args.conn_str) > 1: - parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - - if not args.conn_str: - connection_string = load_setup_connection_string('informixtests') - - if not connection_string: - parser.print_help() - raise SystemExit() - else: - connection_string = args.conn_str[0] - - if args.verbose: - cnxn = pyodbc.connect(connection_string) - print_library_info(cnxn) - cnxn.close() - - suite = load_tests(InformixTestCase, args.test, connection_string) - - testRunner = unittest.TextTestRunner(verbosity=args.verbose) - result = testRunner.run(suite) - - return result - - -if __name__ == '__main__': - - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() - - import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) diff --git a/tests/old/sparktests.py b/tests/old/sparktests.py deleted file mode 100644 index 256fd95d..00000000 --- a/tests/old/sparktests.py +++ /dev/null @@ -1,544 +0,0 @@ -#!/usr/bin/env python - -usage = """\ -%(prog)s [options] connection_string - -Unit tests for Apache Spark. To use, pass a connection string as the parameter. -The tests will create and drop tables t1 and t2 as necessary. - -These run using the version from the 'build' directory, not the version -installed into the Python directories. You must run python setup.py build -before running the tests. - -You can also put the connection string into a tmp/setup.cfg file like so: - - [sparktests] - connection-string=DSN=Spark - -These tests use Simba Spark ODBC driver. -The DSN should be configured with UseNativeQuery=0 to pass the tests. -""" - -import sys -import uuid -import unittest -from decimal import Decimal -from testutils import * - -_TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' - - -def _generate_test_string(length): - """ - Returns a string of composed of `seed` to make a string `length` characters long. - - To enhance performance, there are 3 ways data is read, based on the length of the value, so - most data types are tested with 3 lengths. This function helps us generate the test data. - - We use a recognizable data set instead of a single character to make it less likely that - "overlap" errors will be hidden and to help us manually identify where a break occurs. - """ - if length <= len(_TESTSTR): - return _TESTSTR[:length] - - c = int((length + len(_TESTSTR) - 1) / len(_TESTSTR)) - v = _TESTSTR * c - return v[:length] - - -class SparkTestCase(unittest.TestCase): - - INTEGERS = [ -1, 0, 1, 0x7FFFFFFF ] - BIGINTS = INTEGERS + [ 0xFFFFFFFF, 0x123456789 ] - - SMALL_READ = 100 - LARGE_READ = 4000 - - SMALL_STRING = _generate_test_string(SMALL_READ) - LARGE_STRING = _generate_test_string(LARGE_READ) - SMALL_BYTES = bytes(SMALL_STRING, 'utf-8') - LARGE_BYTES = bytes(LARGE_STRING, 'utf-8') - - def __init__(self, connection_string, ansi, method_name): - unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string - self.ansi = ansi - - def setUp(self): - self.cnxn = pyodbc.connect(self.connection_string, ansi=self.ansi, autocommit=True) - self.cursor = self.cnxn.cursor() - - # I've set my test database to use UTF-8 which seems most popular. - self.cnxn.setdecoding(pyodbc.SQL_WCHAR, encoding='utf-8') - self.cnxn.setencoding(encoding='utf-8') - - # As of psql 9.5.04 SQLGetTypeInfo returns absurdly small sizes leading - # to slow writes. Override them: - self.cnxn.maxwrite = 1024 * 1024 * 1024 - - for i in range(3): - try: - self.cursor.execute("drop table if exists t%d" % i) - except: - pass - - - def tearDown(self): - try: - self.cursor.close() - self.cnxn.close() - except: - # If we've already closed the cursor or connection, exceptions are thrown. - pass - - def _simpletest(datatype, value): - # A simple test that can be used for any data type where the Python - # type we write is also what we expect to receive. - def _t(self): - self.cursor.execute('create table t1(value %s)' % datatype) - self.cursor.execute('insert into t1 values (?)', value) - result = self.cursor.execute("select value from t1").fetchone()[0] - self.assertEqual(result, value) - return _t - - def test_drivers(self): - p = pyodbc.drivers() - self.assertTrue(isinstance(p, list)) - - def test_datasources(self): - p = pyodbc.dataSources() - self.assertTrue(isinstance(p, dict)) - - # def test_gettypeinfo(self): - # self.cursor.getTypeInfo(pyodbc.SQL_VARCHAR) - # cols = [t[0] for t in self.cursor.description] - # print('cols:', cols) - # for row in self.cursor: - # for col,val in zip(cols, row): - # print(' ', col, val) - - def test_getinfo_string(self): - value = self.cnxn.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) - self.assertTrue(isinstance(value, str)) - - def test_getinfo_bool(self): - value = self.cnxn.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) - self.assertTrue(isinstance(value, bool)) - - def test_getinfo_int(self): - value = self.cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertTrue(isinstance(value, int)) - - def test_getinfo_smallint(self): - value = self.cnxn.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) - self.assertTrue(isinstance(value, int)) - - - def test_negative_float(self): - value = -200 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(value, result) - - - def _test_strtype(self, sqltype, value, colsize=None, resulttype=None): - """ - The implementation for string, Unicode, and binary tests. - """ - assert colsize is None or (value is None or colsize >= len(value)) - - if colsize: - sql = "create table t1(s {}({}))".format(sqltype, colsize) - else: - sql = "create table t1(s %s)" % sqltype - - self.cursor.execute(sql) - self.cursor.execute("insert into t1 values(?)", value) - - result = self.cursor.execute("select * from t1").fetchone()[0] - - if resulttype and type(value) is not resulttype: - value = resulttype(value) - - self.assertEqual(result, value) - - # - # VARCHAR - # - - def test_empty_varchar(self): - self._test_strtype('varchar', '', self.SMALL_READ) - - def test_null_varchar(self): - self._test_strtype('varchar', None, self.SMALL_READ) - - def test_large_null_varchar(self): - # There should not be a difference, but why not find out? - self._test_strtype('varchar', None, self.LARGE_READ) - - def test_small_varchar(self): - self._test_strtype('varchar', self.SMALL_STRING, self.SMALL_READ) - - def test_large_varchar(self): - self._test_strtype('varchar', self.LARGE_STRING, self.LARGE_READ) - - def test_varchar_many(self): - self.cursor.execute("create table t1(c1 varchar(300), c2 varchar(300), c3 varchar(300))") - - v1 = 'ABCDEFGHIJ' * 30 - v2 = '0123456789' * 30 - v3 = '9876543210' * 30 - - self.cursor.execute("insert into t1(c1, c2, c3) values (?,?,?)", v1, v2, v3) - row = self.cursor.execute("select c1, c2, c3 from t1").fetchone() - - self.assertEqual(v1, row.c1) - self.assertEqual(v2, row.c2) - self.assertEqual(v3, row.c3) - - def test_chinese(self): - v = '我的' - self.cursor.execute("SELECT '我的' AS name") - row = self.cursor.fetchone() - self.assertEqual(row[0], v) - - self.cursor.execute("SELECT '我的' AS name") - rows = self.cursor.fetchall() - self.assertEqual(rows[0][0], v) - - for value in INTEGERS: - name = str(value).replace('.', '_').replace('-', 'neg_') - locals()['test_int_%s' % name] = _simpletest('int', value) - - for value in BIGINTS: - name = str(value).replace('.', '_').replace('-', 'neg_') - locals()['test_bigint_%s' % name] = _simpletest('bigint', value) - - for value in "-1234.56 -1 0 1 1234.56 123456789.21".split(): - name = value.replace('.', '_').replace('-', 'neg_') - locals()['test_decimal_%s' % name] = _simpletest('decimal(20,6)', Decimal(value)) - - for value in "-1234.56 -1 0 1 1234.56 123456789.21".split(): - name = value.replace('.', '_').replace('-', 'neg_') - locals()['test_numeric_%s' % name] = _simpletest('numeric(20,6)', Decimal(value)) - - def test_small_decimal(self): - value = Decimal('100010') # (I use this because the ODBC docs tell us how the bytes should look in the C struct) - self.cursor.execute("create table t1(d numeric(19))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), Decimal) - self.assertEqual(v, value) - - - def test_small_decimal_scale(self): - # The same as small_decimal, except with a different scale. This value exactly matches the ODBC documentation - # example in the C Data Types appendix. - value = '1000.10' - value = Decimal(value) - self.cursor.execute("create table t1(d numeric(20,6))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), Decimal) - self.assertEqual(v, value) - - - def test_negative_decimal_scale(self): - value = Decimal('-10.0010') - self.cursor.execute("create table t1(d numeric(19,4))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), Decimal) - self.assertEqual(v, value) - - def _exec(self): - self.cursor.execute(self.sql) - - def test_close_cnxn(self): - """Make sure using a Cursor after closing its connection doesn't crash.""" - - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - self.cursor.execute("select * from t1") - - self.cnxn.close() - - # Now that the connection is closed, we expect an exception. (If the code attempts to use - # the HSTMT, we'll get an access violation instead.) - self.sql = "select * from t1" - self.assertRaises(pyodbc.ProgrammingError, self._exec) - - def test_empty_string(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "") - - def test_fixed_str(self): - value = "testing" - self.cursor.execute("create table t1(s char(7))") - self.cursor.execute("insert into t1 values(?)", "testing") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(len(v), len(value)) # If we alloc'd wrong, the test below might work because of an embedded NULL - self.assertEqual(v, value) - - def test_fetchval(self): - expected = "test" - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", expected) - result = self.cursor.execute("select * from t1").fetchval() - self.assertEqual(result, expected) - - def test_negative_row_index(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "1") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row[0], "1") - self.assertEqual(row[-1], "1") - - def test_version(self): - self.assertEqual(3, len(pyodbc.version.split('.'))) # 1.3.1 etc. - - def test_lower_case(self): - "Ensure pyodbc.lowercase forces returned column names to lowercase." - - # Has to be set before creating the cursor, so we must recreate self.cursor. - - pyodbc.lowercase = True - self.cursor = self.cnxn.cursor() - - self.cursor.execute("create table t1(Abc int, dEf int)") - self.cursor.execute("select * from t1") - - names = [ t[0] for t in self.cursor.description ] - names.sort() - - self.assertEqual(names, [ "abc", "def" ]) - - # Put it back so other tests don't fail. - pyodbc.lowercase = False - - def test_long_column_name(self): - "ensure super long column names are handled correctly." - c1 = 'abcdefghij' * 50 - c2 = 'klmnopqrst' * 60 - self.cursor = self.cnxn.cursor() - - self.cursor.execute("create table t1(c1 int, c2 int)") - self.cursor.execute(f"select c1 as {c1}, c2 as {c2} from t1") - - names = [ t[0] for t in self.cursor.description ] - names.sort() - - self.assertEqual(names, [ c1, c2 ]) - - def test_row_description(self): - """ - Ensure Cursor.description is accessible as Row.cursor_description. - """ - self.cursor = self.cnxn.cursor() - self.cursor.execute("create table t1(a int, b char(3))") - self.cursor.execute("insert into t1 values(1, 'abc')") - - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(self.cursor.description, row.cursor_description) - - - def test_executemany(self): - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (i, str(i)) for i in range(1, 6) ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - def test_fast_executemany(self): - - self.fast_executemany = True - - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [(i, str(i)) for i in range(1, 6)] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - def test_executemany_failure(self): - """ - Ensure that an exception is raised if one query in an executemany fails. - """ - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, 'good'), - ('error', 'not an int'), - (3, 'good') ] - - self.assertRaises(pyodbc.Error, self.cursor.executemany, "insert into t1(a, b) value (?, ?)", params) - - - def test_row_slicing(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = row[:] - self.assertTrue(result is row) - - result = row[:-1] - self.assertEqual(result, (1,2,3)) - - result = row[0:4] - self.assertTrue(result is row) - - def test_row_repr(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)") - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = str(row) - self.assertEqual(result, "(1, 2, 3, 4)") - - result = str(row[:-1]) - self.assertEqual(result, "(1, 2, 3)") - - result = str(row[:1]) - self.assertEqual(result, "(1,)") - - def test_cnxn_set_attr_before(self): - # I don't have a getattr right now since I don't have a table telling me what kind of - # value to expect. For now just make sure it doesn't crash. - # From the unixODBC sqlext.h header file. - SQL_ATTR_PACKET_SIZE = 112 - othercnxn = pyodbc.connect(self.connection_string, attrs_before={ SQL_ATTR_PACKET_SIZE : 1024 * 32 }, autocommit=True) - - def test_cnxn_set_attr(self): - # I don't have a getattr right now since I don't have a table telling me what kind of - # value to expect. For now just make sure it doesn't crash. - # From the unixODBC sqlext.h header file. - SQL_ATTR_ACCESS_MODE = 101 - SQL_MODE_READ_ONLY = 1 - self.cnxn.set_attr(SQL_ATTR_ACCESS_MODE, SQL_MODE_READ_ONLY) - - def test_columns(self): - self.cursor.execute("create table t1(a int, b varchar(3), `xΏz` decimal(8,2))") - - self.cursor.columns('t1') - results = {row.column_name: row for row in self.cursor} - row = results['a'] - assert row.type_name == 'INT', row.type_name - row = results['b'] - assert row.type_name == 'VARCHAR' - row = results['xΏz'] - assert row.type_name == 'DECIMAL' - - # Now do the same, but specifically pass in None to one of the keywords. Old versions - # were parsing arguments incorrectly and would raise an error. (This crops up when - # calling indirectly like columns(*args, **kwargs) which aiodbc does.) - - self.cursor.columns('t1', schema=None, catalog=None) - results = {row.column_name: row for row in self.cursor} - row = results['a'] - assert row.type_name == 'INT', row.type_name - row = results['b'] - assert row.type_name == 'VARCHAR' - - def test_cancel(self): - # I'm not sure how to reliably cause a hang to cancel, so for now we'll settle with - # making sure SQLCancel is called correctly. - self.cursor.execute("select 1") - self.cursor.cancel() - - def test_emoticons_as_parameter(self): - # https://github.com/mkleehammer/pyodbc/issues/423 - # - # When sending a varchar parameter, pyodbc is supposed to set ColumnSize to the number - # of characters. Ensure it works even with 4-byte characters. - # - # http://www.fileformat.info/info/unicode/char/1f31c/index.htm - - v = "x \U0001F31C z" - - self.cursor.execute("CREATE TABLE t1(s varchar(100))") - self.cursor.execute("insert into t1 values (?)", v) - - result = self.cursor.execute("select s from t1").fetchone()[0] - - self.assertEqual(result, v) - -def main(): - from argparse import ArgumentParser - parser = ArgumentParser(usage=usage) - parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") - parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") - parser.add_argument("-t", "--test", help="run only the named test") - parser.add_argument("-a", "--ansi", help="ANSI only", default=False, action="store_true") - parser.add_argument("conn_str", nargs="*", help="connection string for Spark") - - args = parser.parse_args() - - if len(args.conn_str) > 1: - parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - - if not args.conn_str: - connection_string = load_setup_connection_string('sparktests') - - if not connection_string: - parser.print_help() - raise SystemExit() - else: - connection_string = args.conn_str[0] - - if args.verbose: - cnxn = pyodbc.connect(connection_string, ansi=args.ansi) - print_library_info(cnxn) - cnxn.close() - - if args.test: - # Run a single test - if not args.test.startswith('test_'): - args.test = 'test_%s' % (args.test) - - s = unittest.TestSuite([ SparkTestCase(connection_string, args.ansi, args.test) ]) - else: - # Run all tests in the class - - methods = [ m for m in dir(SparkTestCase) if m.startswith('test_') ] - methods.sort() - s = unittest.TestSuite([ SparkTestCase(connection_string, args.ansi, m) for m in methods ]) - - testRunner = unittest.TextTestRunner(verbosity=args.verbose) - result = testRunner.run(s) - - return result - - -if __name__ == '__main__': - - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() - - import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) diff --git a/tests/old/sqldwtests.py b/tests/old/sqldwtests.py deleted file mode 100644 index 9fd92a51..00000000 --- a/tests/old/sqldwtests.py +++ /dev/null @@ -1,1441 +0,0 @@ -#!/usr/bin/python - -x = 1 # Getting an error if starting with usage for some reason. - -usage = """\ -%(prog)s [options] connection_string - -Unit tests for Azure SQL DW. To use, pass a connection string as the parameter. -The tests will create and drop tables t1 and t2 as necessary. - -These run using the version from the 'build' directory, not the version -installed into the Python directories. You must run python setup.py build -before running the tests. - -You can also put the connection string into a tmp/setup.cfg file like so: - - [sqldwtests] - connection-string=DRIVER={SQL Server};SERVER=localhost;UID=uid;PWD=pwd;DATABASE=db - -The connection string above will use the 2000/2005 driver, even if SQL Server 2008 -is installed: - - 2000: DRIVER={SQL Server} - 2005: DRIVER={SQL Server} - 2008: DRIVER={SQL Server Native Client 10.0} - -If using FreeTDS ODBC, be sure to use version 1.00.97 or newer. -""" - -import sys, os, re, uuid -import unittest -from decimal import Decimal -from datetime import datetime, date, time -from os.path import join, getsize, dirname, abspath -from warnings import warn -from testutils import * - -_TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' - -def _generate_test_string(length): - """ - Returns a string of `length` characters, constructed by repeating _TESTSTR as necessary. - - To enhance performance, there are 3 ways data is read, based on the length of the value, so most data types are - tested with 3 lengths. This function helps us generate the test data. - - We use a recognizable data set instead of a single character to make it less likely that "overlap" errors will - be hidden and to help us manually identify where a break occurs. - """ - if length <= len(_TESTSTR): - return _TESTSTR[:length] - - c = int((length + len(_TESTSTR)-1) / len(_TESTSTR)) - v = _TESTSTR * c - return v[:length] - -class SqlServerTestCase(unittest.TestCase): - - SMALL_FENCEPOST_SIZES = [ 0, 1, 255, 256, 510, 511, 512, 1023, 1024, 2047, 2048, 4000 ] - LARGE_FENCEPOST_SIZES = [ 4095, 4096, 4097, 10 * 1024, 20 * 1024 ] - - STR_FENCEPOSTS = [ _generate_test_string(size) for size in SMALL_FENCEPOST_SIZES ] - BYTE_FENCEPOSTS = [ bytes(s, 'ascii') for s in STR_FENCEPOSTS ] - IMAGE_FENCEPOSTS = BYTE_FENCEPOSTS + [ bytes(_generate_test_string(size), 'ascii') for size in LARGE_FENCEPOST_SIZES ] - - def __init__(self, method_name, connection_string): - unittest.TestCase.__init__(self, method_name) - self.connection_string = connection_string - - def driver_type_is(self, type_name): - recognized_types = { - 'msodbcsql': '(Microsoft) ODBC Driver xx for SQL Server', - 'freetds': 'FreeTDS ODBC', - } - if not type_name in recognized_types.keys(): - raise KeyError(f'"{type_name}" is not a recognized driver type: {list(recognized_types.keys())}') - driver_name = self.cnxn.getinfo(pyodbc.SQL_DRIVER_NAME).lower() - if type_name == 'msodbcsql': - return ('msodbcsql' in driver_name) or ('sqlncli' in driver_name) or ('sqlsrv32.dll' == driver_name) - elif type_name == 'freetds': - return ('tdsodbc' in driver_name) - - def get_sqlserver_version(self): - """ - Returns the major version: 8-->2000, 9-->2005, 10-->2008 - """ - self.cursor.execute("SELECT CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR(255))") - row = self.cursor.fetchone() - return int(row[0].split('.', 1)[0]) - - def setUp(self): - self.cnxn = pyodbc.connect(self.connection_string) - self.cursor = self.cnxn.cursor() - - # I (Kleehammer) have been using a latin1 collation. If you have a - # different collation, you'll need to update this. If someone knows of - # a good way for this to be dynamic, please update. (I suppose we - # could maintain a map from collation to encoding?) - self.cnxn.setdecoding(pyodbc.SQL_CHAR, 'latin1') - - for i in range(3): - try: - self.cursor.execute("drop table t%d" % i) - except: - pass - - for i in range(3): - try: - self.cursor.execute("drop procedure proc%d" % i) - except: - pass - - try: - self.cursor.execute('drop function func1') - except: - pass - - - def tearDown(self): - try: - self.cursor.close() - self.cnxn.close() - except: - # If we've already closed the cursor or connection, exceptions are thrown. - pass - - def _simpletest(datatype, value): - # A simple test that can be used for any data type where the Python - # type we write is also what we expect to receive. - def _t(self): - self.cursor.execute('create table t1(value %s)' % datatype) - self.cursor.execute('insert into t1 values (?)', value) - result = self.cursor.execute("select value from t1").fetchone()[0] - self.assertEqual(result, value) - return _t - - def test_multiple_bindings(self): - "More than one bind and select on a cursor" - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t1 values (?)", 2) - self.cursor.execute("insert into t1 values (?)", 3) - for i in range(3): - self.cursor.execute("select n from t1 where n < ?", 10) - self.cursor.execute("select n from t1 where n < 3") - - - def test_different_bindings(self): - self.cursor.execute("create table t1(n int)") - self.cursor.execute("create table t2(d datetime)") - self.cursor.execute("insert into t1 values (?)", 1) - self.cursor.execute("insert into t2 values (?)", datetime.now()) - - def test_drivers(self): - p = pyodbc.drivers() - self.assertTrue(isinstance(p, list)) - - def test_datasources(self): - p = pyodbc.dataSources() - self.assertTrue(isinstance(p, dict)) - - def test_getinfo_string(self): - value = self.cnxn.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) - self.assertTrue(isinstance(value, str)) - - def test_getinfo_bool(self): - value = self.cnxn.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) - self.assertTrue(isinstance(value, bool)) - - def test_getinfo_int(self): - value = self.cnxn.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) - self.assertTrue(isinstance(value, (int, int))) - - def test_getinfo_smallint(self): - value = self.cnxn.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) - self.assertTrue(isinstance(value, int)) - - def test_noscan(self): - self.assertEqual(self.cursor.noscan, False) - self.cursor.noscan = True - self.assertEqual(self.cursor.noscan, True) - - def test_nextset(self): - self.cursor.execute("create table t1(i int)") - for i in range(4): - self.cursor.execute("insert into t1(i) values(?)", i) - - self.cursor.execute("select i from t1 where i < 2 order by i; select i from t1 where i >= 2 order by i") - - for i, row in enumerate(self.cursor): - self.assertEqual(i, row.i) - - self.assertEqual(self.cursor.nextset(), True) - - for i, row in enumerate(self.cursor): - self.assertEqual(i + 2, row.i) - - def test_nextset_with_raiserror(self): - self.cursor.execute("select i = 1; RAISERROR('c', 16, 1);") - row = next(self.cursor) - self.assertEqual(1, row.i) - if self.driver_type_is('freetds'): - warn('FREETDS_KNOWN_ISSUE - test_nextset_with_raiserror: test cancelled.') - # AssertionError: ProgrammingError not raised by nextset - # https://github.com/FreeTDS/freetds/issues/230 - return # for now - self.assertRaises(pyodbc.ProgrammingError, self.cursor.nextset) - - def test_fixed_unicode(self): - value = "t\xebsting" - self.cursor.execute("create table t1(s nchar(7))") - self.cursor.execute("insert into t1 values(?)", "t\xebsting") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(len(v), len(value)) # If we alloc'd wrong, the test below might work because of an embedded NULL - self.assertEqual(v, value) - - - def _test_strtype(self, sqltype, value, resulttype=None, colsize=None): - """ - The implementation for string, Unicode, and binary tests. - """ - assert colsize is None or isinstance(colsize, int), colsize - assert colsize is None or (value is None or colsize >= len(value)) - - if colsize: - sql = "create table t1(s {}({}))".format(sqltype, colsize) - else: - sql = "create table t1(s %s)" % sqltype - if colsize >= 2000 and (sqltype == 'nvarchar' or sqltype == 'varchar'): - self.cursor.setinputsizes([(pyodbc.SQL_WVARCHAR, 0, 0)]) - self.cursor.execute(sql) - - if resulttype is None: - resulttype = type(value) - - sql = "insert into t1 values(?)" - try: - self.cursor.execute(sql, value) - except pyodbc.DataError: - if self.driver_type_is('freetds'): - # FREETDS_KNOWN_ISSUE - # - # cnxn.getinfo(pyodbc.SQL_DESCRIBE_PARAMETER) returns False for FreeTDS, so - # pyodbc can't call SQLDescribeParam to get the correct parameter type. - # This can lead to errors being returned from SQL Server when sp_prepexec is called, - # e.g., "Implicit conversion from data type varchar to varbinary is not allowed." - # for test_binary_null - # - # So at least verify that the user can manually specify the parameter type - if sqltype == 'varbinary': - sql_param_type = pyodbc.SQL_VARBINARY - # (add elif blocks for other cases as required) - self.cursor.setinputsizes([(sql_param_type, colsize, 0)]) - self.cursor.execute(sql, value) - else: - raise - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), resulttype) - - if value is not None: - self.assertEqual(len(v), len(value)) - - # To allow buffer --> db --> bytearray tests, always convert the input to the expected result type before - # comparing. - if type(value) is not resulttype: - value = resulttype(value) - - self.assertEqual(v, value) - - - def _test_strliketype(self, sqltype, value, resulttype=None, colsize=None): - """ - The implementation for text, image, ntext, and binary. - - These types do not support comparison operators. - """ - assert colsize is None or isinstance(colsize, int), colsize - assert colsize is None or (value is None or colsize >= len(value)) - - if colsize: - sql = "create table t1(s {}({}))".format(sqltype, colsize) - else: - sql = "create table t1(s %s)" % sqltype - - if resulttype is None: - resulttype = type(value) - - self.cursor.execute(sql) - self.cursor.execute("insert into t1 values(?)", value) - result = self.cursor.execute("select * from t1").fetchone()[0] - - self.assertEqual(type(result), resulttype) - - # To allow buffer --> db --> bytearray tests, always convert the input to the expected result type before - # comparing. - if type(value) is not resulttype: - value = resulttype(value) - - self.assertEqual(result, value) - - - # - # varchar - # - - def test_varchar_null(self): - self._test_strtype('varchar', None, colsize=100) - - # Generate a test for each fencepost size: test_varchar_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('varchar', value, colsize=len(value)) - return t - for value in STR_FENCEPOSTS: - locals()['test_varchar_%s' % len(value)] = _maketest(value) - - def test_varchar_many(self): - self.cursor.execute("create table t1(c1 varchar(300), c2 varchar(300), c3 varchar(300))") - - v1 = 'ABCDEFGHIJ' * 30 - v2 = '0123456789' * 30 - v3 = '9876543210' * 30 - - self.cursor.execute("insert into t1(c1, c2, c3) values (?,?,?)", v1, v2, v3); - row = self.cursor.execute("select c1, c2, c3, len(c1) as l1, len(c2) as l2, len(c3) as l3 from t1").fetchone() - - self.assertEqual(v1, row.c1) - self.assertEqual(v2, row.c2) - self.assertEqual(v3, row.c3) - - # - # nvarchar - # - - def test_unicode_null(self): - self._test_strtype('nvarchar', None, colsize=100) - - # Generate a test for each fencepost size: test_unicode_0, etc. - def _maketest(value): - def t(self): - self._test_strtype('nvarchar', value, colsize=len(value)) - return t - for value in STR_FENCEPOSTS: - locals()['test_unicode_%s' % len(value)] = _maketest(value) - - def test_unicode_longmax(self): - # Issue 188: Segfault when fetching NVARCHAR(MAX) data over 511 bytes - - ver = self.get_sqlserver_version() - if ver < 9: # 2005+ - return # so pass / ignore - self.cursor.execute("select cast(replicate(N'x', 512) as nvarchar(max))") - - # From issue #206 - def _maketest(value): - def t(self): - self._test_strtype('nvarchar', value, colsize=len(value)) - return t - locals()['test_chinese_param'] = _maketest('我的') - - def test_chinese(self): - v = '我的' - self.cursor.execute("SELECT N'我的' AS [Name]") - row = self.cursor.fetchone() - self.assertEqual(row[0], v) - - self.cursor.execute("SELECT N'我的' AS [Name]") - rows = self.cursor.fetchall() - self.assertEqual(rows[0][0], v) - - def test_fast_executemany_to_local_temp_table(self): - if self.driver_type_is('freetds'): - warn('FREETDS_KNOWN_ISSUE - test_fast_executemany_to_local_temp_table: test cancelled.') - return - v = 'Ώπα' - self.cursor.execute("CREATE TABLE #issue295 (id INT, txt NVARCHAR(50))") - sql = "INSERT INTO #issue295 (txt) VALUES (?)" - params = [(v,)] - self.cursor.setinputsizes([(pyodbc.SQL_WVARCHAR, 50, 0)]) - self.cursor.fast_executemany = True - self.cursor.executemany(sql, params) - self.assertEqual(self.cursor.execute("SELECT txt FROM #issue295").fetchval(), v) - - # - # binary - # - - # def test_binary_null(self): - # self._test_strtype('varbinary', None, colsize=100) - - # bytearray - - def _maketest(value): - def t(self): - self._test_strtype('varbinary', bytearray(value), colsize=len(value), resulttype=bytes) - return t - for value in BYTE_FENCEPOSTS: - locals()['test_binary_bytearray_%s' % len(value)] = _maketest(value) - - # bytes - - def _maketest(value): - def t(self): - self._test_strtype('varbinary', bytes(value), colsize=len(value)) - return t - for value in BYTE_FENCEPOSTS: - locals()['test_binary_bytes_%s' % len(value)] = _maketest(value) - - - # bytearray - - - - # bytes - - - - # - # text - # - - - - - # - # bit - # - - def test_bit(self): - value = True - self.cursor.execute("create table t1(b bit)") - self.cursor.execute("insert into t1 values (?)", value) - v = self.cursor.execute("select b from t1").fetchone()[0] - self.assertEqual(type(v), bool) - self.assertEqual(v, value) - - # - # decimal - # - - def _decimal(self, precision, scale, negative): - # From test provided by planders (thanks!) in Issue 91 - - self.cursor.execute("create table t1(d decimal({}, {}))".format(precision, scale)) - - # Construct a decimal that uses the maximum precision and scale. - decStr = '9' * (precision - scale) - if scale: - decStr = decStr + "." + '9' * scale - if negative: - decStr = "-" + decStr - - value = Decimal(decStr) - - self.cursor.execute("insert into t1 values(?)", value) - - v = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(v, value) - - def _maketest(p, s, n): - def t(self): - self._decimal(p, s, n) - return t - for (p, s, n) in [ (1, 0, False), - (1, 0, True), - (6, 0, False), - (6, 2, False), - (6, 4, True), - (6, 6, True), - (38, 0, False), - (38, 10, False), - (38, 38, False), - (38, 0, True), - (38, 10, True), - (38, 38, True) ]: - locals()['test_decimal_{}_{}_{}'.format(p, s, n and 'n' or 'p')] = _maketest(p, s, n) - - - def test_decimal_e(self): - """Ensure exponential notation decimals are properly handled""" - value = Decimal((0, (1, 2, 3), 5)) # prints as 1.23E+7 - self.cursor.execute("create table t1(d decimal(10, 2))") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_subquery_params(self): - """Ensure parameter markers work in a subquery""" - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - row = self.cursor.execute(""" - select x.id - from ( - select id - from t1 - where s = ? - and id between ? and ? - ) x - """, 'test', 1, 10).fetchone() - self.assertNotEqual(row, None) - self.assertEqual(row[0], 1) - - def _exec(self): - self.cursor.execute(self.sql) - - def test_close_cnxn(self): - """Make sure using a Cursor after closing its connection doesn't crash.""" - - self.cursor.execute("create table t1(id integer, s varchar(20))") - self.cursor.execute("insert into t1 values (?,?)", 1, 'test') - self.cursor.execute("select * from t1") - - self.cnxn.close() - - # Now that the connection is closed, we expect an exception. (If the code attempts to use - # the HSTMT, we'll get an access violation instead.) - self.sql = "select * from t1" - self.assertRaises(pyodbc.ProgrammingError, self._exec) - - def test_empty_string(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "") - - def test_empty_string_encoding(self): - self.cnxn.setdecoding(pyodbc.SQL_CHAR, encoding='shift_jis') - value = "" - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(v, value) - - def test_fixed_str(self): - value = "testing" - self.cursor.execute("create table t1(s char(7))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(len(v), len(value)) # If we alloc'd wrong, the test below might work because of an embedded NULL - self.assertEqual(v, value) - - def test_empty_unicode(self): - self.cursor.execute("create table t1(s nvarchar(20))") - self.cursor.execute("insert into t1 values(?)", "") - - def test_empty_unicode_encoding(self): - self.cnxn.setdecoding(pyodbc.SQL_CHAR, encoding='shift_jis') - value = "" - self.cursor.execute("create table t1(s nvarchar(20))") - self.cursor.execute("insert into t1 values(?)", value) - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(v, value) - - def test_negative_row_index(self): - self.cursor.execute("create table t1(s varchar(20))") - self.cursor.execute("insert into t1 values(?)", "1") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row[0], "1") - self.assertEqual(row[-1], "1") - - def test_version(self): - self.assertEqual(3, len(pyodbc.version.split('.'))) # 1.3.1 etc. - - # - # date, time, datetime - # - - def test_datetime(self): - value = datetime(2007, 1, 15, 3, 4, 5) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(result), datetime) - self.assertEqual(value, result) - - def test_datetime_fraction(self): - # SQL Server supports milliseconds, but Python's datetime supports nanoseconds, so the most granular datetime - # supported is xxx000. - - value = datetime(2007, 1, 15, 3, 4, 5, 123000) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(result), datetime) - self.assertEqual(value, result) - - def test_datetime_fraction_rounded(self): - # SQL Server supports milliseconds, but Python's datetime supports nanoseconds. pyodbc rounds down to what the - # database supports. - - full = datetime(2007, 1, 15, 3, 4, 5, 123456) - rounded = datetime(2007, 1, 15, 3, 4, 5, 123000) - - self.cursor.execute("create table t1(dt datetime)") - self.cursor.execute("insert into t1 values (?)", full) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(result), datetime) - self.assertEqual(rounded, result) - - def test_date(self): - ver = self.get_sqlserver_version() - if ver < 10: # 2008 only - return # so pass / ignore - - value = date.today() - - self.cursor.execute("create table t1(d date)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(type(result), date) - self.assertEqual(value, result) - - def test_time(self): - ver = self.get_sqlserver_version() - if ver < 10: # 2008 only - return # so pass / ignore - - value = datetime.now().time() - - # We aren't yet writing values using the new extended time type so the value written to the database is only - # down to the second. - value = value.replace(microsecond=0) - - self.cursor.execute("create table t1(t time)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select t from t1").fetchone()[0] - self.assertEqual(type(result), time) - self.assertEqual(value, result) - - def test_datetime2(self): - value = datetime(2007, 1, 15, 3, 4, 5) - - self.cursor.execute("create table t1(dt datetime2)") - self.cursor.execute("insert into t1 values (?)", value) - - result = self.cursor.execute("select dt from t1").fetchone()[0] - self.assertEqual(type(result), datetime) - self.assertEqual(value, result) - - # - # ints and floats - # - - def test_int(self): - value = 1234 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_int(self): - value = -1 - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_bigint(self): - input = 3000000000 - self.cursor.execute("create table t1(d bigint)") - self.cursor.execute("insert into t1 values (?)", input) - result = self.cursor.execute("select d from t1").fetchone()[0] - self.assertEqual(result, input) - - def test_float(self): - value = 1234.567 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(result, value) - - def test_negative_float(self): - value = -200 - self.cursor.execute("create table t1(n float)") - self.cursor.execute("insert into t1 values (?)", value) - result = self.cursor.execute("select n from t1").fetchone()[0] - self.assertEqual(value, result) - - # - # stored procedures - # - - # def test_callproc(self): - # "callproc with a simple input-only stored procedure" - # pass - - def test_sp_results(self): - self.cursor.execute( - """ - Create procedure proc1 - AS - select top 10 name, id, xtype, refdate - from sysobjects - """) - rows = self.cursor.execute("exec proc1").fetchall() - self.assertEqual(type(rows), list) - self.assertEqual(len(rows), 10) # there has to be at least 10 items in sysobjects - self.assertEqual(type(rows[0].refdate), datetime) - - - def test_sp_results_from_temp(self): - - # Note: I've used "set nocount on" so that we don't get the number of rows deleted from #tmptable. - # If you don't do this, you'd need to call nextset() once to skip it. - - self.cursor.execute( - """ - Create procedure proc1 - AS - set nocount on - select top 10 name, id, xtype, refdate - into #tmptable - from sysobjects - - select * from #tmptable - """) - self.cursor.execute("exec proc1") - self.assertTrue(self.cursor.description is not None) - self.assertTrue(len(self.cursor.description) == 4) - - rows = self.cursor.fetchall() - self.assertEqual(type(rows), list) - self.assertEqual(len(rows), 10) # there has to be at least 10 items in sysobjects - self.assertEqual(type(rows[0].refdate), datetime) - - - def test_sp_with_dates(self): - # Reported in the forums that passing two datetimes to a stored procedure doesn't work. - self.cursor.execute( - """ - if exists (select * from dbo.sysobjects where id = object_id(N'[test_sp]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) - drop procedure [dbo].[test_sp] - """) - self.cursor.execute( - """ - create procedure test_sp(@d1 datetime, @d2 datetime) - AS - declare @d as int - set @d = datediff(year, @d1, @d2) - select @d - """) - self.cursor.execute("exec test_sp ?, ?", datetime.now(), datetime.now()) - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(rows[0][0] == 0) # 0 years apart - - def test_sp_with_none(self): - # Reported in the forums that passing None caused an error. - self.cursor.execute( - """ - if exists (select * from dbo.sysobjects where id = object_id(N'[test_sp]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) - drop procedure [dbo].[test_sp] - """) - self.cursor.execute( - """ - create procedure test_sp(@x varchar(20)) - AS - declare @y varchar(20) - set @y = @x - select @y - """) - self.cursor.execute("exec test_sp ?", None) - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(rows[0][0] == None) # 0 years apart - - - # - # rowcount - # - - def test_rowcount_delete(self): - self.assertEqual(self.cursor.rowcount, -1) - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, count) - - def test_rowcount_nodata(self): - """ - This represents a different code path than a delete that deleted something. - - The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over - the code that errors out and drop down to the same SQLRowCount code. On the other hand, we could hardcode a - zero return value. - """ - self.cursor.execute("create table t1(i int)") - # This is a different code path internally. - self.cursor.execute("delete from t1") - self.assertEqual(self.cursor.rowcount, 0) - - def test_rowcount_select(self): - """ - Ensure Cursor.rowcount is set properly after a select statement. - - pyodbc calls SQLRowCount after each execute and sets Cursor.rowcount, but SQL Server 2005 returns -1 after a - select statement, so we'll test for that behavior. This is valid behavior according to the DB API - specification, but people don't seem to like it. - """ - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.cursor.execute("select * from t1") - self.assertEqual(self.cursor.rowcount, -1) - - rows = self.cursor.fetchall() - self.assertEqual(len(rows), count) - self.assertEqual(self.cursor.rowcount, -1) - - def test_rowcount_reset(self): - "Ensure rowcount is reset after DDL" - - ddl_rowcount = 0 if self.driver_type_is('freetds') else -1 - - self.cursor.execute("create table t1(i int)") - count = 4 - for i in range(count): - self.cursor.execute("insert into t1 values (?)", i) - self.assertEqual(self.cursor.rowcount, 1) - - self.cursor.execute("create table t2(i int)") - self.assertEqual(self.cursor.rowcount, ddl_rowcount) - - # - # always return Cursor - # - - # In the 2.0.x branch, Cursor.execute sometimes returned the cursor and sometimes the rowcount. This proved very - # confusing when things went wrong and added very little value even when things went right since users could always - # use: cursor.execute("...").rowcount - - def test_retcursor_delete(self): - self.cursor.execute("create table t1(i int)") - self.cursor.execute("insert into t1 values (1)") - v = self.cursor.execute("delete from t1") - self.assertEqual(v, self.cursor) - - def test_retcursor_nodata(self): - """ - This represents a different code path than a delete that deleted something. - - The return value is SQL_NO_DATA and code after it was causing an error. We could use SQL_NO_DATA to step over - the code that errors out and drop down to the same SQLRowCount code. - """ - self.cursor.execute("create table t1(i int)") - # This is a different code path internally. - v = self.cursor.execute("delete from t1") - self.assertEqual(v, self.cursor) - - def test_retcursor_select(self): - self.cursor.execute("create table t1(i int)") - self.cursor.execute("insert into t1 values (1)") - v = self.cursor.execute("select * from t1") - self.assertEqual(v, self.cursor) - - # - # misc - # - - def table_with_spaces(self): - "Ensure we can select using [x z] syntax" - - try: - self.cursor.execute("create table [test one](int n)") - self.cursor.execute("insert into [test one] values(1)") - self.cursor.execute("select * from [test one]") - v = self.cursor.fetchone()[0] - self.assertEqual(v, 1) - finally: - self.cnxn.rollback() - - def test_lower_case(self): - "Ensure pyodbc.lowercase forces returned column names to lowercase." - - # Has to be set before creating the cursor, so we must recreate self.cursor. - - pyodbc.lowercase = True - self.cursor = self.cnxn.cursor() - - self.cursor.execute("create table t1(Abc int, dEf int)") - self.cursor.execute("select * from t1") - - names = [ t[0] for t in self.cursor.description ] - names.sort() - - self.assertEqual(names, [ "abc", "def" ]) - - # Put it back so other tests don't fail. - pyodbc.lowercase = False - - def test_row_description(self): - """ - Ensure Cursor.description is accessible as Row.cursor_description. - """ - self.cursor = self.cnxn.cursor() - self.cursor.execute("create table t1(a int, b char(3))") - self.cursor.execute("insert into t1 values(1, 'abc')") - - row = self.cursor.execute("select * from t1").fetchone() - - self.assertEqual(self.cursor.description, row.cursor_description) - - - def test_temp_select(self): - # A project was failing to create temporary tables via select into. - self.cursor.execute("create table t1(s char(7))") - self.cursor.execute("insert into t1 values(?)", "testing") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(v, "testing") - - self.cursor.execute("select s into t2 from t1") - v = self.cursor.execute("select * from t1").fetchone()[0] - self.assertEqual(type(v), str) - self.assertEqual(v, "testing") - - # Money - # - # The inputs are strings so we don't have to deal with floating point rounding. - - for value in "-1234.56 -1 0 1 1234.56 123456789.21".split(): - name = str(value).replace('.', '_').replace('-', 'neg_') - locals()['test_money_%s' % name] = _simpletest('money', Decimal(str(value))) - - def test_executemany(self): - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (i, str(i)) for i in range(1, 6) ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - - def test_executemany_one(self): - "Pass executemany a single sequence" - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, "test") ] - - self.cursor.executemany("insert into t1(a, b) values (?,?)", params) - - count = self.cursor.execute("select count(*) from t1").fetchone()[0] - self.assertEqual(count, len(params)) - - self.cursor.execute("select a, b from t1 order by a") - rows = self.cursor.fetchall() - self.assertEqual(count, len(rows)) - - for param, row in zip(params, rows): - self.assertEqual(param[0], row[0]) - self.assertEqual(param[1], row[1]) - - def test_executemany_dae_0(self): - """ - DAE for 0-length value - """ - self.cursor.execute("create table t1(a nvarchar(max)) with (heap)") - - self.cursor.fast_executemany = True - self.cursor.executemany("insert into t1(a) values(?)", [['']]) - - self.assertEqual(self.cursor.execute("select a from t1").fetchone()[0], '') - - self.cursor.fast_executemany = False - - def test_executemany_failure(self): - """ - Ensure that an exception is raised if one query in an executemany fails. - """ - self.cursor.execute("create table t1(a int, b varchar(10))") - - params = [ (1, 'good'), - ('error', 'not an int'), - (3, 'good') ] - - self.assertRaises(pyodbc.Error, self.cursor.executemany, "insert into t1(a, b) value (?, ?)", params) - - - def test_row_slicing(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = row[:] - self.assertTrue(result is row) - - result = row[:-1] - self.assertEqual(result, (1,2,3)) - - result = row[0:4] - self.assertTrue(result is row) - - - def test_row_repr(self): - self.cursor.execute("create table t1(a int, b int, c int, d int)"); - self.cursor.execute("insert into t1 values(1,2,3,4)") - - row = self.cursor.execute("select * from t1").fetchone() - - result = str(row) - self.assertEqual(result, "(1, 2, 3, 4)") - - result = str(row[:-1]) - self.assertEqual(result, "(1, 2, 3)") - - result = str(row[:1]) - self.assertEqual(result, "(1,)") - - - def test_concatenation(self): - v2 = '0123456789' * 30 - v3 = '9876543210' * 30 - - self.cursor.execute("create table t1(c1 int identity(1, 1), c2 varchar(300), c3 varchar(300))") - self.cursor.execute("insert into t1(c2, c3) values (?,?)", v2, v3) - - row = self.cursor.execute("select c2, c3, c2 + c3 as both from t1").fetchone() - - self.assertEqual(row.both, v2 + v3) - - def test_view_select(self): - # Reported in forum: Can't select from a view? I think I do this a lot, but another test never hurts. - - # Create a table (t1) with 3 rows and a view (t2) into it. - self.cursor.execute("create table t1(c1 int identity(1, 1), c2 varchar(50))") - for i in range(3): - self.cursor.execute("insert into t1(c2) values (?)", "string%s" % i) - self.cursor.execute("create view t2 as select * from t1") - - # Select from the view - self.cursor.execute("select * from t2") - rows = self.cursor.fetchall() - self.assertTrue(rows is not None) - self.assertTrue(len(rows) == 3) - self.cursor.execute("drop view t2") - - def test_autocommit(self): - self.assertEqual(self.cnxn.autocommit, False) - othercnxn = pyodbc.connect(self.connection_string, autocommit=True) - self.assertEqual(othercnxn.autocommit, True) - othercnxn.autocommit = False - self.assertEqual(othercnxn.autocommit, False) - - - - def test_skip(self): - # Insert 1, 2, and 3. Fetch 1, skip 2, fetch 3. - - self.cursor.execute("create table t1(id int)"); - for i in range(1, 5): - self.cursor.execute("insert into t1 values(?)", i) - self.cursor.execute("select id from t1 order by id") - self.assertEqual(self.cursor.fetchone()[0], 1) - self.cursor.skip(2) - self.assertEqual(self.cursor.fetchone()[0], 4) - - def test_timeout(self): - self.assertEqual(self.cnxn.timeout, 0) # defaults to zero (off) - - self.cnxn.timeout = 30 - self.assertEqual(self.cnxn.timeout, 30) - - self.cnxn.timeout = 0 - self.assertEqual(self.cnxn.timeout, 0) - - def test_sets_execute(self): - # Only lists and tuples are allowed. - def f(): - self.cursor.execute("create table t1 (word varchar (100))") - words = set (['a']) - self.cursor.execute("insert into t1 (word) VALUES (?)", [words]) - - self.assertRaises(pyodbc.ProgrammingError, f) - - def test_sets_executemany(self): - # Only lists and tuples are allowed. - def f(): - self.cursor.execute("create table t1 (word varchar (100))") - words = set (['a']) - self.cursor.executemany("insert into t1 (word) values (?)", [words]) - - self.assertRaises(TypeError, f) - - def test_row_execute(self): - "Ensure we can use a Row object as a parameter to execute" - self.cursor.execute("create table t1(n int, s varchar(10))") - self.cursor.execute("insert into t1 values (1, 'a')") - row = self.cursor.execute("select n, s from t1").fetchone() - self.assertNotEqual(row, None) - - self.cursor.execute("create table t2(n int, s varchar(10))") - self.cursor.execute("insert into t2 values (?, ?)", row) - - def test_row_executemany(self): - "Ensure we can use a Row object as a parameter to executemany" - self.cursor.execute("create table t1(n int, s varchar(10))") - - for i in range(3): - self.cursor.execute("insert into t1 values (?, ?)", i, chr(ord('a')+i)) - - rows = self.cursor.execute("select n, s from t1").fetchall() - self.assertNotEqual(len(rows), 0) - - self.cursor.execute("create table t2(n int, s varchar(10))") - self.cursor.executemany("insert into t2 values (?, ?)", rows) - - def test_description(self): - "Ensure cursor.description is correct" - - self.cursor.execute("create table t1(n int, s varchar(8), d decimal(5,2))") - self.cursor.execute("insert into t1 values (1, 'abc', '1.23')") - self.cursor.execute("select * from t1") - - # (I'm not sure the precision of an int is constant across different versions, bits, so I'm hand checking the - # items I do know. - - # int - t = self.cursor.description[0] - self.assertEqual(t[0], 'n') - self.assertEqual(t[1], int) - self.assertEqual(t[5], 0) # scale - self.assertEqual(t[6], True) # nullable - - # varchar(8) - t = self.cursor.description[1] - self.assertEqual(t[0], 's') - self.assertEqual(t[1], str) - self.assertEqual(t[4], 8) # precision - self.assertEqual(t[5], 0) # scale - self.assertEqual(t[6], True) # nullable - - # decimal(5, 2) - t = self.cursor.description[2] - self.assertEqual(t[0], 'd') - self.assertEqual(t[1], Decimal) - self.assertEqual(t[4], 5) # precision - self.assertEqual(t[5], 2) # scale - self.assertEqual(t[6], True) # nullable - - - def test_none_param(self): - "Ensure None can be used for params other than the first" - # Some driver/db versions would fail if NULL was not the first parameter because SQLDescribeParam (only used - # with NULL) could not be used after the first call to SQLBindParameter. This means None always worked for the - # first column, but did not work for later columns. - # - # If SQLDescribeParam doesn't work, pyodbc would use VARCHAR which almost always worked. However, - # binary/varbinary won't allow an implicit conversion. - - self.cursor.execute("create table t1(n int, blob varbinary(max)) with(heap)") - self.cursor.execute("insert into t1 values (1, 0x1234)") - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row.n, 1) - self.assertEqual(type(row.blob), bytes) - - sql = "update t1 set n=?, blob=?" - try: - self.cursor.setinputsizes([(), (pyodbc.SQL_VARBINARY, None, None)]) - self.cursor.execute(sql, 2, None) - except pyodbc.DataError: - if self.driver_type_is('freetds'): - # FREETDS_KNOWN_ISSUE - # - # cnxn.getinfo(pyodbc.SQL_DESCRIBE_PARAMETER) returns False for FreeTDS, so - # pyodbc can't call SQLDescribeParam to get the correct parameter type. - # This can lead to errors being returned from SQL Server when sp_prepexec is called, - # e.g., "Implicit conversion from data type varchar to varbinary(max) is not allowed." - # - # So at least verify that the user can manually specify the parameter type - self.cursor.setinputsizes([(), (pyodbc.SQL_VARBINARY, None, None)]) - self.cursor.execute(sql, 2, None) - else: - raise - row = self.cursor.execute("select * from t1").fetchone() - self.assertEqual(row.n, 2) - self.assertEqual(row.blob, None) - - - def test_output_conversion(self): - def convert(value): - # The value is the raw bytes (as a bytes object) read from the - # database. We'll simply add an X at the beginning at the end. - return 'X' + value.decode('latin1') + 'X' - - self.cursor.execute("create table t1(n int, v varchar(10))") - self.cursor.execute("insert into t1 values (1, '123.45')") - - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, 'X123.45X') - - # Clear all conversions and try again. There should be no Xs this time. - self.cnxn.clear_output_converters() - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, '123.45') - - # Same but clear using remove_output_converter. - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, 'X123.45X') - - self.cnxn.remove_output_converter(pyodbc.SQL_VARCHAR) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, '123.45') - - # And lastly, clear by passing None for the converter. - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, 'X123.45X') - - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, None) - value = self.cursor.execute("select v from t1").fetchone()[0] - self.assertEqual(value, '123.45') - - def test_too_large(self): - """Ensure error raised if insert fails due to truncation""" - value = 'x' * 1000 - self.cursor.execute("create table t1(s varchar(800))") - def test(): - self.cursor.execute("insert into t1 values (?)", value) - self.assertRaises(pyodbc.DataError, test) - - def test_login_timeout(self): - # This can only test setting since there isn't a way to cause it to block on the server side. - cnxns = pyodbc.connect(self.connection_string, timeout=2) - - def test_row_equal(self): - self.cursor.execute("create table t1(n int, s varchar(20))") - self.cursor.execute("insert into t1 values (1, 'test')") - row1 = self.cursor.execute("select n, s from t1").fetchone() - row2 = self.cursor.execute("select n, s from t1").fetchone() - b = (row1 == row2) - self.assertEqual(b, True) - - def test_row_gtlt(self): - self.cursor.execute("create table t1(n int, s varchar(20))") - self.cursor.execute("insert into t1 values (1, 'test1')") - self.cursor.execute("insert into t1 values (1, 'test2')") - rows = self.cursor.execute("select n, s from t1 order by s").fetchall() - self.assertTrue(rows[0] < rows[1]) - self.assertTrue(rows[0] <= rows[1]) - self.assertTrue(rows[1] > rows[0]) - self.assertTrue(rows[1] >= rows[0]) - self.assertTrue(rows[0] != rows[1]) - - rows = list(rows) - rows.sort() # uses < - - def test_context_manager_success(self): - "Ensure `with` commits if an exception is not raised" - self.cursor.execute("create table t1(n int)") - - with pyodbc.connect(self.connection_string) as cnxn: - cursor = cnxn.cursor() - cursor.execute("insert into t1 values (1)") - - cnxn = None - cursor = None - - rows = self.cursor.execute("select n from t1").fetchall() - self.assertEqual(len(rows), 1) - self.assertEqual(rows[0][0], 1) - - def test_context_manager_failure(self): - "Ensure `with` rolls back if an exception is raised" - # We'll insert a row and commit it. Then we'll insert another row followed by an - # exception. - - self.cursor.execute("create table t1(n int)") - self.cursor.execute("insert into t1 values (1)") - - def _fail(): - with pyodbc.connect(self.connection_string) as cnxn: - cursor = cnxn.cursor() - cursor.execute("insert into t1 values (2)") - cursor.execute("delete from bogus") - - self.assertRaises(pyodbc.Error, _fail) - - self.cursor.execute("select max(n) from t1") - val = self.cursor.fetchval() - self.assertEqual(val, 1) - - - def test_untyped_none(self): - # From issue 129 - value = self.cursor.execute("select ?", None).fetchone()[0] - self.assertEqual(value, None) - - def test_large_update_nodata(self): - self.cursor.execute('create table t1(a varbinary(max)) with(heap)') - hundredkb = b'x'*100*1024 - self.cursor.setinputsizes([(pyodbc.SQL_VARBINARY,0,0)]) - self.cursor.execute('update t1 set a=? where 1=0', (hundredkb,)) - - - def test_no_fetch(self): - # Issue 89 with FreeTDS: Multiple selects (or catalog functions that issue selects) without fetches seem to - # confuse the driver. - self.cursor.execute('select 1') - self.cursor.execute('select 1') - self.cursor.execute('select 1') - - def test_drivers(self): - drivers = pyodbc.drivers() - self.assertEqual(list, type(drivers)) - self.assertTrue(len(drivers) > 0) - - m = re.search('DRIVER={?([^}]+?)}?;', self.connection_string, re.IGNORECASE) - current = m.group(1) - self.assertTrue(current in drivers) - - def test_decode_meta(self): - """ - Ensure column names with non-ASCII characters are converted using the configured encodings. - """ - # This is from GitHub issue #190 - self.cursor.execute("create table t1(a int)") - self.cursor.execute("insert into t1 values (1)") - self.cursor.execute('select a as "Tipología" from t1') - self.assertEqual(self.cursor.description[0][0], "Tipología") - - def test_columns(self): - # When using aiohttp, `await cursor.primaryKeys('t1')` was raising the error - # - # Error: TypeError: argument 2 must be str, not None - # - # I'm not sure why, but PyArg_ParseTupleAndKeywords fails if you use "|s" for an - # optional string keyword when calling indirectly. - - self.cursor.execute("create table t1(a int, b varchar(3), xΏz varchar(4))") - - self.cursor.columns('t1') - results = {row.column_name: row for row in self.cursor} - row = results['a'] - assert row.type_name == 'int', row.type_name - row = results['b'] - assert row.type_name == 'varchar' - assert row.column_size == 3 - - # Now do the same, but specifically pass in None to one of the keywords. Old versions - # were parsing arguments incorrectly and would raise an error. (This crops up when - # calling indirectly like columns(*args, **kwargs) which aiodbc does.) - - self.cursor.columns('t1', schema=None, catalog=None) - results = {row.column_name: row for row in self.cursor} - row = results['a'] - assert row.type_name == 'int', row.type_name - row = results['b'] - assert row.type_name == 'varchar' - assert row.column_size == 3 - row = results['xΏz'] - assert row.type_name == 'varchar' - assert row.column_size == 4, row.column_size - - def test_cancel(self): - # I'm not sure how to reliably cause a hang to cancel, so for now we'll settle with - # making sure SQLCancel is called correctly. - self.cursor.execute("select 1") - self.cursor.cancel() - - def test_emoticons(self): - # https://github.com/mkleehammer/pyodbc/issues/423 - # - # When sending a varchar parameter, pyodbc is supposed to set ColumnSize to the number - # of characters. Ensure it works even with 4-byte characters. - # - # http://www.fileformat.info/info/unicode/char/1f31c/index.htm - v = "x \U0001F31C z" - - self.cursor.execute("create table t1(s nvarchar(100))") - self.cursor.execute("insert into t1 values (?)", v) - - result = self.cursor.execute("select s from t1").fetchone()[0] - - self.assertEqual(result, v) - -def main(): - from argparse import ArgumentParser - parser = ArgumentParser(usage=usage) - parser.add_argument("-v", "--verbose", action="count", default=0, help="increment test verbosity (can be used multiple times)") - parser.add_argument("-d", "--debug", action="store_true", default=False, help="print debugging items") - parser.add_argument("-t", "--test", help="run only the named test") - parser.add_argument("conn_str", nargs="*", help="connection string for Azure SQL DW") - - args = parser.parse_args() - - if len(args.conn_str) > 1: - parser.error('Only one argument is allowed. Do you need quotes around the connection string?') - - if not args.conn_str: - connection_string = load_setup_connection_string('sqldwtests') - - if not connection_string: - parser.print_help() - raise SystemExit() - else: - connection_string = args.conn_str[0] - - if args.verbose: - cnxn = pyodbc.connect(connection_string) - print_library_info(cnxn) - cnxn.close() - - suite = load_tests(SqlServerTestCase, args.test, connection_string) - - testRunner = unittest.TextTestRunner(verbosity=args.verbose) - result = testRunner.run(suite) - - return result - - -if __name__ == '__main__': - - # Add the build directory to the path so we're testing the latest build, not the installed version. - - add_to_path() - - import pyodbc - sys.exit(0 if main().wasSuccessful() else 1) diff --git a/tests/pyodbc_test.xls b/tests/pyodbc_test.xls new file mode 100644 index 00000000..8d7ee23b Binary files /dev/null and b/tests/pyodbc_test.xls differ diff --git a/tests/pyodbc_test.xlsx b/tests/pyodbc_test.xlsx new file mode 100644 index 00000000..357da719 Binary files /dev/null and b/tests/pyodbc_test.xlsx differ diff --git a/tests/sparktests.py b/tests/sparktests.py new file mode 100644 index 00000000..24f933b5 --- /dev/null +++ b/tests/sparktests.py @@ -0,0 +1,518 @@ +"""Unit tests for Apache Spark + +These tests use Simba Spark ODBC driver. +The DSN should be configured with UseNativeQuery=0 to pass the tests. + +The port to pytest has not yet been tested (don't have access to the driver). +""" + +import decimal +import os +import sys +import typing +import uuid + +import pyodbc +import pytest + +CNXNSTR = os.environ.get("PYODBC_SPARK", "DSN=pyodbc-spark") +INTEGERS = [-1, 0, 1, 0x7FFFFFFF] +BIGINTS = INTEGERS + [0xFFFFFFFF, 0x123456789] +FIXED = [decimal.Decimal(v) for v in "-1234.56 -1 0 1 1234.56 123456789.21".split()] +SMALL_READ = 100 +LARGE_READ = 4000 + + +#---------------------------------------------------------------------- +# Helper functions +#---------------------------------------------------------------------- + +def connect(autocommit=False, attrs_before=None): + """Create a connection to the Spark back end""" + return pyodbc.connect(CNXNSTR, autocommit=autocommit, attrs_before=attrs_before) + + +@pytest.fixture +def cursor() -> typing.Iterator[pyodbc.Cursor]: + """Cursor object supplied on demand to test functions""" + + cnxn = connect() + cur = cnxn.cursor() + + cnxn.setdecoding(pyodbc.SQL_WCHAR, encoding="utf-8") + cnxn.setencoding(encoding="utf-8") + cnxn.maxwrite = 1024 * 1024 * 1024 + + for i in range(1, 4): + try: + cur.execute(f"drop table t{i:d}") + except pyodbc.ProgrammingError: + pass + cnxn.commit() + + yield cur + + if not cnxn.closed: + cur.close() + cnxn.close() + + +def _generate_test_string(length, encoding=None): + """ + Returns either a string or bytes, depending on whether encoding is provided, + that is `length` elements long. + + If length is None, None is returned. This simplifies the tests by letting us put None into + an array of other lengths and pass them here, moving the special case check into one place. + """ + + if length is None: + return None + + seed = "0123456789-abcdefghijklmnopqrstuvwxyz-" + + if length <= len(seed): + v = seed + else: + c = (length + len(seed) - 1 // len(seed)) + v = seed * c + + v = v[:length] + if encoding: + v = v.encode(encoding) + + return v + + +def _simpletest(cursor, datatype, value): + """ + A simple test that can be used for any data type where the Python + type we write is also what we expect to receive. + """ + cursor.execute(f"create table t1 (value {datatype})") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select value from t1").fetchval() + assert result == value + cursor.execute("drop table t1") + + +def _test_strtype(cursor, sqltype, value, colsize=None, resulttype=None): + """ + The implementation for string and binary tests. + """ + assert colsize is None or value is None or colsize >= len(value) + + sqltype = f"{sqltype}({colsize})" if colsize else sqltype + + cursor.execute(f"create table t1 (v {sqltype})") + cursor.execute("insert into t1 values(?)", value) + + result = cursor.execute("select * from t1").fetchone()[0] + + if resulttype and type(value) is not resulttype: + value = resulttype(value) + + assert result == value + + +def test_drivers(): + p = pyodbc.drivers() + assert isinstance(p, list) + + +def test_datasources(cursor: pyodbc.Cursor): + p = pyodbc.dataSources() + assert isinstance(p, dict) + + +# def test_gettypeinfo(cursor: pyodbc.Cursor): +# cursor.getTypeInfo(pyodbc.SQL_VARCHAR) +# cols = [t[0] for t in cursor.description] +# print("cols:", cols) +# for row in cursor: +# for col,val in zip(cols, row): +# print(" ", col, val) + + +def test_getinfo_string(cursor: pyodbc.Cursor): + value = cursor.connection.getinfo(pyodbc.SQL_CATALOG_NAME_SEPARATOR) + assert isinstance(value, str) + + +def test_getinfo_bool(cursor: pyodbc.Cursor): + value = cursor.connection.getinfo(pyodbc.SQL_ACCESSIBLE_TABLES) + assert isinstance(value, bool) + + +def test_getinfo_int(cursor: pyodbc.Cursor): + value = cursor.connection.getinfo(pyodbc.SQL_DEFAULT_TXN_ISOLATION) + assert isinstance(value, int) + + +def test_getinfo_smallint(cursor: pyodbc.Cursor): + value = cursor.connection.getinfo(pyodbc.SQL_CONCAT_NULL_BEHAVIOR) + assert isinstance(value, int) + + +def test_negative_float(cursor: pyodbc.Cursor): + value = -200 + cursor.execute("create table t1(n float)") + cursor.execute("insert into t1 values (?)", value) + result = cursor.execute("select n from t1").fetchone()[0] + assert value == result + +# +# VARCHAR +# + +def test_empty_varchar(cursor: pyodbc.Cursor): + _test_strtype(cursor, "varchar", "", SMALL_READ) + + +def test_null_varchar(cursor: pyodbc.Cursor): + _test_strtype(cursor, "varchar", None, SMALL_READ) + + +def test_large_null_varchar(cursor: pyodbc.Cursor): + # There should not be a difference, but why not find out? + _test_strtype(cursor, "varchar", None, LARGE_READ) + + +def test_small_varchar(cursor: pyodbc.Cursor): + _test_strtype(cursor, "varchar", _generate_test_string(SMALL_READ), SMALL_READ) + + +def test_large_varchar(cursor: pyodbc.Cursor): + _test_strtype(cursor, "varchar", _generate_test_string(LARGE_READ), LARGE_READ) + + +def test_varchar_many(cursor: pyodbc.Cursor): + cursor.execute("create table t1(c1 varchar(300), c2 varchar(300), c3 varchar(300))") + + v1 = "ABCDEFGHIJ" * 30 + v2 = "0123456789" * 30 + v3 = "9876543210" * 30 + + cursor.execute("insert into t1(c1, c2, c3) values (?,?,?)", v1, v2, v3) + row = cursor.execute("select c1, c2, c3 from t1").fetchone() + + assert v1 == row.c1 + assert v2 == row.c2 + assert v3 == row.c3 + + +def test_chinese(cursor: pyodbc.Cursor): + v = "我的" + cursor.execute("SELECT '我的' AS name") + row = cursor.fetchone() + assert row[0] == v + + cursor.execute("SELECT '我的' AS name") + rows = cursor.fetchall() + assert rows[0][0] == v + + +# +# NUMBERS +# + +def test_int(cursor: pyodbc.Cursor): + for value in INTEGERS: + _simpletest(cursor, "int", value) + + +def test_bigint(cursor: pyodbc.Cursor): + for value in BIGINTS: + _simpletest(cursor, "bigint", value) + + +def test_decimal(cursor: pyodbc.Cursor): + for value in FIXED: + _simpletest(cursor, "decimal(20,6)", value) + + +def test_numeric(cursor: pyodbc.Cursor): + for value in FIXED: + _simpletest(cursor, "numeric(20,6)", value) + + +def test_small_decimal(cursor: pyodbc.Cursor): + value = decimal.Decimal("100010") # (I use this because the ODBC docs tell us how the bytes should look in the C struct) + cursor.execute("create table t1(d numeric(19))") + cursor.execute("insert into t1 values(?)", value) + v = cursor.execute("select * from t1").fetchone()[0] + assert type(v) == decimal.Decimal + assert v == value + + +def test_small_decimal_scale(cursor: pyodbc.Cursor): + # The same as small_decimal, except with a different scale. This value exactly matches the ODBC documentation + # example in the C Data Types appendix. + value = "1000.10" + value = decimal.Decimal(value) + cursor.execute("create table t1(d numeric(20,6))") + cursor.execute("insert into t1 values(?)", value) + v = cursor.execute("select * from t1").fetchone()[0] + assert type(v) == decimal.Decimal + assert v == value + + +def test_negative_decimal_scale(cursor: pyodbc.Cursor): + value = decimal.Decimal("-10.0010") + cursor.execute("create table t1(d numeric(19,4))") + cursor.execute("insert into t1 values(?)", value) + v = cursor.execute("select * from t1").fetchone()[0] + assert type(v) == decimal.Decimal + assert v == value + + +def test_close_cnxn(cursor: pyodbc.Cursor): + """Make sure using a Cursor after closing its connection doesn't crash.""" + + cursor.execute("create table t1(id integer, s varchar(20))") + cursor.execute("insert into t1 values (?,?)", 1, "test") + cursor.execute("select * from t1") + + cursor.connection.close() + + # Now that the connection is closed, we expect an exception. (If the code attempts to use + # the HSTMT, we'll get an access violation instead.) + with pytest.raises(pyodbc.ProgrammingError): + cursor.execute("select * from t1") + + +def test_empty_string(cursor: pyodbc.Cursor): + cursor.execute("create table t1(s varchar(20))") + cursor.execute("insert into t1 values(?)", "") + + +def test_fixed_str(cursor: pyodbc.Cursor): + value = "testing" + cursor.execute("create table t1(s char(7))") + cursor.execute("insert into t1 values(?)", "testing") + v = cursor.execute("select * from t1").fetchone()[0] + assert type(v) == str + assert len(v) == len(value) # If we alloc'd wrong, the test below might work because of an embedded NULL + assert v == value + + +def test_fetchval(cursor: pyodbc.Cursor): + expected = "test" + cursor.execute("create table t1(s varchar(20))") + cursor.execute("insert into t1 values(?)", expected) + result = cursor.execute("select * from t1").fetchval() + assert result == expected + + +def test_negative_row_index(cursor: pyodbc.Cursor): + cursor.execute("create table t1(s varchar(20))") + cursor.execute("insert into t1 values(?)", "1") + row = cursor.execute("select * from t1").fetchone() + assert row[0] == "1" + assert row[-1] == "1" + + +def test_version(cursor: pyodbc.Cursor): + assert 3 == len(pyodbc.version.split(".")) # 1.3.1 etc. + + +def test_lower_case(cursor: pyodbc.Cursor): + "Ensure pyodbc.lowercase forces returned column names to lowercase." + + # Has to be set before creating the cursor, so we must recreate cursor. + + pyodbc.lowercase = True + cursor = cursor.connection.cursor() + + cursor.execute("create table t1(Abc int, dEf int)") + cursor.execute("select * from t1") + + names = [ t[0] for t in cursor.description ] + names.sort() + + assert names == [ "abc", "def" ] + + # Put it back so other tests don't fail. + pyodbc.lowercase = False + + +def test_long_column_name(cursor: pyodbc.Cursor): + "ensure super long column names are handled correctly." + c1 = "abcdefghij" * 50 + c2 = "klmnopqrst" * 60 + cursor = cursor.connection.cursor() + + cursor.execute("create table t1(c1 int, c2 int)") + cursor.execute(f"select c1 as {c1}, c2 as {c2} from t1") + + names = [ t[0] for t in cursor.description ] + names.sort() + + assert names == [ c1, c2 ] + + +def test_row_description(cursor: pyodbc.Cursor): + """ + Ensure Cursor.description is accessible as Row.cursor_description. + """ + cursor = cursor.connection.cursor() + cursor.execute("create table t1(a int, b char(3))") + cursor.execute("insert into t1 values(1, 'abc')") + + row = cursor.execute("select * from t1").fetchone() + assert cursor.description == row.cursor_description + + +def test_executemany(cursor: pyodbc.Cursor): + cursor.execute("create table t1(a int, b varchar(10))") + + params = [ (i, str(i)) for i in range(1, 6) ] + + cursor.executemany("insert into t1(a, b) values (?,?)", params) + + count = cursor.execute("select count(*) from t1").fetchone()[0] + assert count == len(params) + + cursor.execute("select a, b from t1 order by a") + rows = cursor.fetchall() + assert count == len(rows) + + for param, row in zip(params, rows): + assert param[0] == row[0] + assert param[1] == row[1] + + +def test_fast_executemany(cursor: pyodbc.Cursor): + + pyodbc.fast_executemany = True + + cursor.execute("create table t1(a int, b varchar(10))") + + params = [(i, str(i)) for i in range(1, 6)] + + cursor.executemany("insert into t1(a, b) values (?,?)", params) + + count = cursor.execute("select count(*) from t1").fetchone()[0] + assert count == len(params) + + cursor.execute("select a, b from t1 order by a") + rows = cursor.fetchall() + assert count == len(rows) + + for param, row in zip(params, rows): + assert param[0] == row[0] + assert param[1] == row[1] + + pyodbc.fast_executemany = False + + +def test_executemany_failure(cursor: pyodbc.Cursor): + """ + Ensure that an exception is raised if one query in an executemany fails. + """ + cursor.execute("create table t1(a int, b varchar(10))") + + params = [ (1, "good"), + ("error", "not an int"), + (3, "good") ] + + with pytest.raises(pyodbc.Error): + cursor.executemany("insert into t1(a, b) value (?, ?)", params) + + +def test_row_slicing(cursor: pyodbc.Cursor): + cursor.execute("create table t1(a int, b int, c int, d int)"); + cursor.execute("insert into t1 values(1,2,3,4)") + + row = cursor.execute("select * from t1").fetchone() + + result = row[:] + assert result is row + + result = row[:-1] + assert result == (1,2,3) + + result = row[0:4] + assert result is row + + +def test_row_repr(cursor: pyodbc.Cursor): + cursor.execute("create table t1(a int, b int, c int, d int)") + cursor.execute("insert into t1 values(1,2,3,4)") + + row = cursor.execute("select * from t1").fetchone() + + result = str(row) + assert result == "(1, 2, 3, 4)" + + result = str(row[:-1]) + assert result == "(1, 2, 3)" + + result = str(row[:1]) + assert result == "(1,)" + + +def test_cnxn_set_attr_before(cursor: pyodbc.Cursor): + # I don't have a getattr right now since I don't have a table telling me what kind of + # value to expect. For now just make sure it doesn't crash. + # From the unixODBC sqlext.h header file. + SQL_ATTR_PACKET_SIZE = 112 + othercnxn = connect(attrs_before={SQL_ATTR_PACKET_SIZE : 1024 * 32}, autocommit=True) + + +def test_cnxn_set_attr(cursor: pyodbc.Cursor): + # I don't have a getattr right now since I don't have a table telling me what kind of + # value to expect. For now just make sure it doesn't crash. + # From the unixODBC sqlext.h header file. + SQL_ATTR_ACCESS_MODE = 101 + SQL_MODE_READ_ONLY = 1 + cursor.connection.set_attr(SQL_ATTR_ACCESS_MODE, SQL_MODE_READ_ONLY) + + +def test_columns(cursor: pyodbc.Cursor): + cursor.execute("create table t1(a int, b varchar(3), ώ decimal(8,2))") + + cursor.columns("t1") + results = {row.column_name: row for row in cursor} + row = results["a"] + assert "INT" in row.type_name.upper(), row.type_name + row = results["b"] + assert row.type_name.upper() == "VARCHAR" + row = results["ώ"] + assert row.type_name.upper() in ("DECIMAL", "NUMERIC") + + # Now do the same, but specifically pass in None to one of the keywords. Old versions + # were parsing arguments incorrectly and would raise an error. (This crops up when + # calling indirectly like columns(*args, **kwargs) which aiodbc does.) + + cursor.columns("t1", schema=None, catalog=None) + results = {row.column_name: row for row in cursor} + row = results["a"] + assert "INT" in row.type_name.upper(), row.type_name + row = results["b"] + assert row.type_name.upper() == "VARCHAR" + + +def test_cancel(cursor: pyodbc.Cursor): + # I'm not sure how to reliably cause a hang to cancel, so for now we'll settle with + # making sure SQLCancel is called correctly. + cursor.execute("select 1") + cursor.cancel() + + +def test_emoticons_as_parameter(cursor: pyodbc.Cursor): + # https://github.com/mkleehammer/pyodbc/issues/423 + # + # When sending a varchar parameter, pyodbc is supposed to set ColumnSize to the number + # of characters. Ensure it works even with 4-byte characters. + # + # http://www.fileformat.info/info/unicode/char/1f31c/index.htm + + v = "x \U0001F31C z" + + cursor.execute("CREATE TABLE t1(s varchar(100))") + cursor.execute("insert into t1 values (?)", v) + + result = cursor.execute("select s from t1").fetchone()[0] + + assert result == v