diff --git a/mssql_python/connection.py b/mssql_python/connection.py index 12760df4..2a7af146 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -185,6 +185,30 @@ def cursor(self) -> Cursor: cursor = Cursor(self) self._cursors.add(cursor) # Track the cursor return cursor + + def getinfo(self, info_type): + """ + Return general information about the driver and data source. + + Args: + info_type (int): The type of information to return. See the ODBC + SQLGetInfo documentation for the supported values. + + Returns: + The requested information. The type of the returned value depends + on the information requested. It will be a string, integer, or boolean. + + Raises: + DatabaseError: If there is an error retrieving the information. + InterfaceError: If the connection is closed. + """ + if self._closed: + raise InterfaceError( + driver_error="Cannot get info on closed connection", + ddbc_error="Cannot get info on closed connection", + ) + + return self._conn.get_info(info_type) def commit(self) -> None: """ diff --git a/mssql_python/constants.py b/mssql_python/constants.py index 81e60d37..e75b7444 100644 --- a/mssql_python/constants.py +++ b/mssql_python/constants.py @@ -117,6 +117,144 @@ class ConstantsDDBC(Enum): SQL_NULLABLE = 1 SQL_MAX_NUMERIC_LEN = 16 +class GetInfoConstants(Enum): + """ + These constants are used with various methods like getinfo(). + """ + + # Driver and database information + SQL_DRIVER_NAME = 6 + SQL_DRIVER_VER = 7 + SQL_DRIVER_ODBC_VER = 77 + SQL_DRIVER_HLIB = 76 + SQL_DRIVER_HENV = 75 + SQL_DRIVER_HDBC = 74 + SQL_DATA_SOURCE_NAME = 2 + SQL_DATABASE_NAME = 16 + SQL_SERVER_NAME = 13 + SQL_USER_NAME = 47 + + # SQL conformance and support + SQL_SQL_CONFORMANCE = 118 + SQL_KEYWORDS = 89 + SQL_IDENTIFIER_CASE = 28 + SQL_IDENTIFIER_QUOTE_CHAR = 29 + SQL_SPECIAL_CHARACTERS = 94 + SQL_SQL92_ENTRY_SQL = 127 + SQL_SQL92_INTERMEDIATE_SQL = 128 + SQL_SQL92_FULL_SQL = 129 + SQL_SUBQUERIES = 95 + SQL_EXPRESSIONS_IN_ORDERBY = 27 + SQL_CORRELATION_NAME = 74 + SQL_SEARCH_PATTERN_ESCAPE = 14 + + # Catalog and schema support + SQL_CATALOG_TERM = 42 + SQL_CATALOG_NAME_SEPARATOR = 41 + SQL_SCHEMA_TERM = 39 + SQL_TABLE_TERM = 45 + SQL_PROCEDURES = 21 + SQL_ACCESSIBLE_TABLES = 19 + SQL_ACCESSIBLE_PROCEDURES = 20 + SQL_CATALOG_NAME = 10002 + SQL_CATALOG_USAGE = 92 + SQL_SCHEMA_USAGE = 91 + SQL_COLUMN_ALIAS = 87 + SQL_DESCRIBE_PARAMETER = 10002 + + # Transaction support + SQL_TXN_CAPABLE = 46 + SQL_TXN_ISOLATION_OPTION = 72 + SQL_DEFAULT_TXN_ISOLATION = 26 + SQL_MULTIPLE_ACTIVE_TXN = 37 + SQL_TXN_ISOLATION_LEVEL = 108 + + # Data type support + SQL_NUMERIC_FUNCTIONS = 49 + SQL_STRING_FUNCTIONS = 50 + SQL_DATETIME_FUNCTIONS = 51 + SQL_SYSTEM_FUNCTIONS = 58 + SQL_CONVERT_FUNCTIONS = 48 + SQL_LIKE_ESCAPE_CLAUSE = 113 + + # Numeric limits + SQL_MAX_COLUMN_NAME_LEN = 30 + SQL_MAX_TABLE_NAME_LEN = 35 + SQL_MAX_SCHEMA_NAME_LEN = 32 + SQL_MAX_CATALOG_NAME_LEN = 34 + SQL_MAX_IDENTIFIER_LEN = 10005 + SQL_MAX_STATEMENT_LEN = 105 + SQL_MAX_CHAR_LITERAL_LEN = 108 + SQL_MAX_BINARY_LITERAL_LEN = 112 + SQL_MAX_COLUMNS_IN_TABLE = 101 + SQL_MAX_COLUMNS_IN_SELECT = 100 + SQL_MAX_COLUMNS_IN_GROUP_BY = 97 + SQL_MAX_COLUMNS_IN_ORDER_BY = 99 + SQL_MAX_COLUMNS_IN_INDEX = 98 + SQL_MAX_TABLES_IN_SELECT = 106 + SQL_MAX_CONCURRENT_ACTIVITIES = 1 + SQL_MAX_DRIVER_CONNECTIONS = 0 + SQL_MAX_ROW_SIZE = 104 + SQL_MAX_USER_NAME_LEN = 107 + + # Connection attributes + SQL_ACTIVE_CONNECTIONS = 0 + SQL_ACTIVE_STATEMENTS = 1 + SQL_DATA_SOURCE_READ_ONLY = 25 + SQL_NEED_LONG_DATA_LEN = 111 + SQL_GETDATA_EXTENSIONS = 81 + + # Result set and cursor attributes + SQL_CURSOR_COMMIT_BEHAVIOR = 23 + SQL_CURSOR_ROLLBACK_BEHAVIOR = 24 + SQL_CURSOR_SENSITIVITY = 10001 + SQL_BOOKMARK_PERSISTENCE = 82 + SQL_DYNAMIC_CURSOR_ATTRIBUTES1 = 144 + SQL_DYNAMIC_CURSOR_ATTRIBUTES2 = 145 + SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1 = 146 + SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES2 = 147 + SQL_STATIC_CURSOR_ATTRIBUTES1 = 150 + SQL_STATIC_CURSOR_ATTRIBUTES2 = 151 + SQL_KEYSET_CURSOR_ATTRIBUTES1 = 148 + SQL_KEYSET_CURSOR_ATTRIBUTES2 = 149 + SQL_SCROLL_OPTIONS = 44 + SQL_SCROLL_CONCURRENCY = 43 + SQL_FETCH_DIRECTION = 8 + SQL_ROWSET_SIZE = 9 + SQL_CONCURRENCY = 7 + SQL_ROW_NUMBER = 14 + SQL_STATIC_SENSITIVITY = 83 + SQL_BATCH_SUPPORT = 121 + SQL_BATCH_ROW_COUNT = 120 + SQL_PARAM_ARRAY_ROW_COUNTS = 153 + SQL_PARAM_ARRAY_SELECTS = 154 + + # Positioned statement support + SQL_POSITIONED_STATEMENTS = 80 + + # Other constants + SQL_GROUP_BY = 88 + SQL_OJ_CAPABILITIES = 65 + SQL_ORDER_BY_COLUMNS_IN_SELECT = 90 + SQL_OUTER_JOINS = 38 + SQL_QUOTED_IDENTIFIER_CASE = 93 + SQL_CONCAT_NULL_BEHAVIOR = 22 + SQL_NULL_COLLATION = 85 + SQL_ALTER_TABLE = 86 + SQL_UNION = 96 + SQL_DDL_INDEX = 170 + SQL_MULT_RESULT_SETS = 36 + SQL_OWNER_USAGE = 91 + SQL_QUALIFIER_USAGE = 92 + SQL_TIMEDATE_ADD_INTERVALS = 109 + SQL_TIMEDATE_DIFF_INTERVALS = 110 + + # Return values for some getinfo functions + SQL_IC_UPPER = 1 + SQL_IC_LOWER = 2 + SQL_IC_SENSITIVE = 3 + SQL_IC_MIXED = 4 + class AuthType(Enum): """Constants for authentication types""" INTERACTIVE = "activedirectoryinteractive" diff --git a/mssql_python/pybind/connection/connection.cpp b/mssql_python/pybind/connection/connection.cpp index 9782efd2..9c1263f1 100644 --- a/mssql_python/pybind/connection/connection.cpp +++ b/mssql_python/pybind/connection/connection.cpp @@ -314,4 +314,111 @@ SqlHandlePtr ConnectionHandle::allocStatementHandle() { ThrowStdException("Connection object is not initialized"); } return _conn->allocStatementHandle(); +} + +py::object Connection::getInfo(SQLUSMALLINT infoType) const { + if (!_dbcHandle) { + ThrowStdException("Connection handle not allocated"); + } + + LOG("Getting connection info for type {}", infoType); + + // For string results - allocate a buffer + char charBuffer[1024] = {0}; + SQLSMALLINT stringLength = 0; + SQLRETURN ret; + + // First try to get the info as a string or binary data + ret = SQLGetInfo_ptr(_dbcHandle->get(), infoType, charBuffer, sizeof(charBuffer), &stringLength); + if (!SQL_SUCCEEDED(ret)) { + checkError(ret); + } + + // Determine return type based on the InfoType + // String types usually have InfoType > 10000 + if (infoType > 10000 || + infoType == SQL_DATA_SOURCE_NAME || + infoType == SQL_DBMS_NAME || + infoType == SQL_DBMS_VER || + infoType == SQL_DRIVER_NAME || + infoType == SQL_DRIVER_VER) { + // Return as string + return py::str(charBuffer); + } + else if (infoType == SQL_DRIVER_ODBC_VER || + infoType == SQL_SERVER_NAME) { + // Return as string + return py::str(charBuffer); + } + else { + // For numeric types, we need to interpret the buffer based on the expected return type + // Handle common numeric types + switch (infoType) { + // 16-bit unsigned integers + case SQL_MAX_CONCURRENT_ACTIVITIES: + case SQL_MAX_DRIVER_CONNECTIONS: + case SQL_ODBC_API_CONFORMANCE: + case SQL_ODBC_SQL_CONFORMANCE: + { + SQLUSMALLINT value = *reinterpret_cast(charBuffer); + return py::int_(value); + } + + // 32-bit unsigned integers + case SQL_ASYNC_MODE: + case SQL_GETDATA_EXTENSIONS: + case SQL_MAX_ASYNC_CONCURRENT_STATEMENTS: + case SQL_MAX_COLUMNS_IN_GROUP_BY: + case SQL_MAX_COLUMNS_IN_ORDER_BY: + case SQL_MAX_COLUMNS_IN_SELECT: + case SQL_MAX_COLUMNS_IN_TABLE: + case SQL_MAX_ROW_SIZE: + case SQL_MAX_TABLES_IN_SELECT: + case SQL_MAX_USER_NAME_LEN: + case SQL_NUMERIC_FUNCTIONS: + case SQL_STRING_FUNCTIONS: + case SQL_SYSTEM_FUNCTIONS: + case SQL_TIMEDATE_FUNCTIONS: + { + SQLUINTEGER value = *reinterpret_cast(charBuffer); + return py::int_(value); + } + + // Boolean flags (32-bit mask) + case SQL_AGGREGATE_FUNCTIONS: + case SQL_ALTER_TABLE: + case SQL_CATALOG_USAGE: + case SQL_DATETIME_LITERALS: + case SQL_INDEX_KEYWORDS: + case SQL_INSERT_STATEMENT: + case SQL_SCHEMA_USAGE: + case SQL_SQL_CONFORMANCE: + case SQL_SQL92_DATETIME_FUNCTIONS: + case SQL_SQL92_NUMERIC_VALUE_FUNCTIONS: + case SQL_SQL92_PREDICATES: + case SQL_SQL92_RELATIONAL_JOIN_OPERATORS: + case SQL_SQL92_STRING_FUNCTIONS: + case SQL_STATIC_CURSOR_ATTRIBUTES1: + case SQL_STATIC_CURSOR_ATTRIBUTES2: + { + SQLUINTEGER value = *reinterpret_cast(charBuffer); + return py::int_(value); + } + + // Handle any other types as integers + default: + SQLUINTEGER value = *reinterpret_cast(charBuffer); + return py::int_(value); + } + } + + // Default return in case nothing matched + return py::none(); +} + +py::object ConnectionHandle::getInfo(SQLUSMALLINT infoType) const { + if (!_conn) { + ThrowStdException("Connection object is not initialized"); + } + return _conn->getInfo(infoType); } \ No newline at end of file diff --git a/mssql_python/pybind/connection/connection.h b/mssql_python/pybind/connection/connection.h index 6129125e..66dd5895 100644 --- a/mssql_python/pybind/connection/connection.h +++ b/mssql_python/pybind/connection/connection.h @@ -42,6 +42,9 @@ class Connection { // Allocate a new statement handle on this connection. SqlHandlePtr allocStatementHandle(); + // Get information about the driver and data source + py::object getInfo(SQLUSMALLINT infoType) const; + private: void allocateDbcHandle(); void checkError(SQLRETURN ret) const; @@ -67,6 +70,9 @@ class ConnectionHandle { bool getAutocommit() const; SqlHandlePtr allocStatementHandle(); + // Get information about the driver and data source + py::object getInfo(SQLUSMALLINT infoType) const; + private: std::shared_ptr _conn; bool _usePool; diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index b5588a25..142b3c2f 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -123,6 +123,7 @@ SQLBindColFunc SQLBindCol_ptr = nullptr; SQLDescribeColFunc SQLDescribeCol_ptr = nullptr; SQLMoreResultsFunc SQLMoreResults_ptr = nullptr; SQLColAttributeFunc SQLColAttribute_ptr = nullptr; +SQLGetInfoFunc SQLGetInfo_ptr = nullptr; // Transaction APIs SQLEndTranFunc SQLEndTran_ptr = nullptr; @@ -779,6 +780,7 @@ DriverHandle LoadDriverOrThrowException() { SQLDescribeCol_ptr = GetFunctionPointer(handle, "SQLDescribeColW"); SQLMoreResults_ptr = GetFunctionPointer(handle, "SQLMoreResults"); SQLColAttribute_ptr = GetFunctionPointer(handle, "SQLColAttributeW"); + SQLGetInfo_ptr = GetFunctionPointer(handle, "SQLGetInfoW"); SQLEndTran_ptr = GetFunctionPointer(handle, "SQLEndTran"); SQLDisconnect_ptr = GetFunctionPointer(handle, "SQLDisconnect"); @@ -796,7 +798,7 @@ DriverHandle LoadDriverOrThrowException() { SQLGetData_ptr && SQLNumResultCols_ptr && SQLBindCol_ptr && SQLDescribeCol_ptr && SQLMoreResults_ptr && SQLColAttribute_ptr && SQLEndTran_ptr && SQLDisconnect_ptr && SQLFreeHandle_ptr && - SQLFreeStmt_ptr && SQLGetDiagRec_ptr; + SQLFreeStmt_ptr && SQLGetDiagRec_ptr && SQLGetInfo_ptr; if (!success) { ThrowStdException("Failed to load required function pointers from driver."); @@ -2554,7 +2556,8 @@ PYBIND11_MODULE(ddbc_bindings, m) { .def("rollback", &ConnectionHandle::rollback, "Rollback the current transaction") .def("set_autocommit", &ConnectionHandle::setAutocommit) .def("get_autocommit", &ConnectionHandle::getAutocommit) - .def("alloc_statement_handle", &ConnectionHandle::allocStatementHandle); + .def("alloc_statement_handle", &ConnectionHandle::allocStatementHandle) + .def("get_info", &ConnectionHandle::getInfo, py::arg("info_type")); m.def("enable_pooling", &enable_pooling, "Enable global connection pooling"); m.def("close_pooling", []() {ConnectionPoolManager::getInstance().closePools();}); m.def("DDBCSQLExecDirect", &SQLExecDirect_wrap, "Execute a SQL query directly"); diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index d142276c..21ba959c 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -105,6 +105,7 @@ typedef SQLRETURN (SQL_API* SQLDescribeColFunc)(SQLHSTMT, SQLUSMALLINT, SQLWCHAR typedef SQLRETURN (SQL_API* SQLMoreResultsFunc)(SQLHSTMT); typedef SQLRETURN (SQL_API* SQLColAttributeFunc)(SQLHSTMT, SQLUSMALLINT, SQLUSMALLINT, SQLPOINTER, SQLSMALLINT, SQLSMALLINT*, SQLPOINTER); +typedef SQLRETURN (SQL_API* SQLGetInfoFunc)(SQLHDBC, SQLUSMALLINT, SQLPOINTER, SQLSMALLINT, SQLSMALLINT*); // Transaction APIs typedef SQLRETURN (SQL_API* SQLEndTranFunc)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT); @@ -148,6 +149,7 @@ extern SQLBindColFunc SQLBindCol_ptr; extern SQLDescribeColFunc SQLDescribeCol_ptr; extern SQLMoreResultsFunc SQLMoreResults_ptr; extern SQLColAttributeFunc SQLColAttribute_ptr; +extern SQLGetInfoFunc SQLGetInfo_ptr; // Transaction APIs extern SQLEndTranFunc SQLEndTran_ptr; diff --git a/tests/test_003_connection.py b/tests/test_003_connection.py index 51fce818..8ddb33ef 100644 --- a/tests/test_003_connection.py +++ b/tests/test_003_connection.py @@ -23,6 +23,7 @@ import time from mssql_python import Connection, connect, pooling import threading +from mssql_python.constants import GetInfoConstants as sql_const def drop_table_if_exists(cursor, table_name): """Drop the table if it exists""" @@ -485,3 +486,216 @@ def test_connection_pooling_basic(conn_str): conn1.close() conn2.close() + +def test_getinfo_basic_driver_info(db_connection): + """Test basic driver information info types.""" + + try: + # Driver name should be available + driver_name = db_connection.getinfo(sql_const.SQL_DRIVER_NAME.value) + print("Driver Name = ",driver_name) + assert driver_name is not None, "Driver name should not be None" + + # Driver version should be available + driver_ver = db_connection.getinfo(sql_const.SQL_DRIVER_VER.value) + print("Driver Version = ",driver_ver) + assert driver_ver is not None, "Driver version should not be None" + + # Data source name should be available + dsn = db_connection.getinfo(sql_const.SQL_DATA_SOURCE_NAME.value) + print("Data source name = ",dsn) + assert dsn is not None, "Data source name should not be None" + + # Server name should be available (might be empty in some configurations) + server_name = db_connection.getinfo(sql_const.SQL_SERVER_NAME.value) + print("Server Name = ",server_name) + assert server_name is not None, "Server name should not be None" + + # User name should be available (might be empty if using integrated auth) + user_name = db_connection.getinfo(sql_const.SQL_USER_NAME.value) + print("User Name = ",user_name) + assert user_name is not None, "User name should not be None" + + except Exception as e: + pytest.fail(f"getinfo failed for basic driver info: {e}") + +def test_getinfo_sql_support(db_connection): + """Test SQL support and conformance info types.""" + + try: + # SQL conformance level + sql_conformance = db_connection.getinfo(sql_const.SQL_SQL_CONFORMANCE.value) + print("SQL Conformance = ",sql_conformance) + assert sql_conformance is not None, "SQL conformance should not be None" + + # Keywords - may return a very long string + keywords = db_connection.getinfo(sql_const.SQL_KEYWORDS.value) + print("Keywords = ",keywords) + assert keywords is not None, "SQL keywords should not be None" + + # Identifier quote character + quote_char = db_connection.getinfo(sql_const.SQL_IDENTIFIER_QUOTE_CHAR.value) + print(f"Identifier quote char: '{quote_char}'") + assert quote_char is not None, "Identifier quote char should not be None" + + except Exception as e: + pytest.fail(f"getinfo failed for SQL support info: {e}") + +def test_getinfo_numeric_limits(db_connection): + """Test numeric limitation info types.""" + + try: + # Max column name length - should be a positive integer + max_col_name_len = db_connection.getinfo(sql_const.SQL_MAX_COLUMN_NAME_LEN.value) + assert isinstance(max_col_name_len, int), "Max column name length should be an integer" + assert max_col_name_len >= 0, "Max column name length should be non-negative" + + # Max table name length + max_table_name_len = db_connection.getinfo(sql_const.SQL_MAX_TABLE_NAME_LEN.value) + assert isinstance(max_table_name_len, int), "Max table name length should be an integer" + assert max_table_name_len >= 0, "Max table name length should be non-negative" + + # Max statement length - may return 0 for "unlimited" + max_statement_len = db_connection.getinfo(sql_const.SQL_MAX_STATEMENT_LEN.value) + assert isinstance(max_statement_len, int), "Max statement length should be an integer" + assert max_statement_len >= 0, "Max statement length should be non-negative" + + # Max connections - may return 0 for "unlimited" + max_connections = db_connection.getinfo(sql_const.SQL_MAX_DRIVER_CONNECTIONS.value) + assert isinstance(max_connections, int), "Max connections should be an integer" + assert max_connections >= 0, "Max connections should be non-negative" + + except Exception as e: + pytest.fail(f"getinfo failed for numeric limits info: {e}") + +def test_getinfo_catalog_support(db_connection): + """Test catalog support info types.""" + + try: + # Catalog support for tables + catalog_term = db_connection.getinfo(sql_const.SQL_CATALOG_TERM.value) + print("Catalof term = ",catalog_term) + assert catalog_term is not None, "Catalog term should not be None" + + # Catalog name separator + catalog_separator = db_connection.getinfo(sql_const.SQL_CATALOG_NAME_SEPARATOR.value) + print(f"Catalog name separator: '{catalog_separator}'") + assert catalog_separator is not None, "Catalog separator should not be None" + + # Schema term + schema_term = db_connection.getinfo(sql_const.SQL_SCHEMA_TERM.value) + print("Schema term = ",schema_term) + assert schema_term is not None, "Schema term should not be None" + + # Stored procedures support + procedures = db_connection.getinfo(sql_const.SQL_PROCEDURES.value) + print("Procedures = ",procedures) + assert procedures is not None, "Procedures support should not be None" + + except Exception as e: + pytest.fail(f"getinfo failed for catalog support info: {e}") + +def test_getinfo_transaction_support(db_connection): + """Test transaction support info types.""" + + try: + # Transaction support + txn_capable = db_connection.getinfo(sql_const.SQL_TXN_CAPABLE.value) + print("Transaction capable = ",txn_capable) + assert txn_capable is not None, "Transaction capability should not be None" + + # Default transaction isolation + default_txn_isolation = db_connection.getinfo(sql_const.SQL_DEFAULT_TXN_ISOLATION.value) + print("Default Transaction isolation = ",default_txn_isolation) + assert default_txn_isolation is not None, "Default transaction isolation should not be None" + + # Multiple active transactions support + multiple_txn = db_connection.getinfo(sql_const.SQL_MULTIPLE_ACTIVE_TXN.value) + print("Multiple transaction = ",multiple_txn) + assert multiple_txn is not None, "Multiple active transactions support should not be None" + + except Exception as e: + pytest.fail(f"getinfo failed for transaction support info: {e}") + +def test_getinfo_data_types(db_connection): + """Test data type support info types.""" + + try: + # Numeric functions + numeric_functions = db_connection.getinfo(sql_const.SQL_NUMERIC_FUNCTIONS.value) + assert isinstance(numeric_functions, int), "Numeric functions should be an integer" + + # String functions + string_functions = db_connection.getinfo(sql_const.SQL_STRING_FUNCTIONS.value) + assert isinstance(string_functions, int), "String functions should be an integer" + + # Date/time functions + datetime_functions = db_connection.getinfo(sql_const.SQL_DATETIME_FUNCTIONS.value) + assert isinstance(datetime_functions, int), "Datetime functions should be an integer" + + except Exception as e: + pytest.fail(f"getinfo failed for data type support info: {e}") + +def test_getinfo_invalid_constant(db_connection): + """Test getinfo behavior with invalid constants.""" + # Use a constant that doesn't exist in ODBC + non_existent_constant = 9999 + try: + result = db_connection.getinfo(non_existent_constant) + # If it doesn't raise an exception, it should return None or an empty value + assert result is None or result == 0 or result == "", "Invalid constant should return None/empty" + except Exception: + # It's also acceptable to raise an exception for invalid constants + pass + +def test_getinfo_type_consistency(db_connection): + """Test that getinfo returns consistent types for repeated calls.""" + + # Choose a few representative info types that don't depend on DBMS + info_types = [ + sql_const.SQL_DRIVER_NAME.value, + sql_const.SQL_MAX_COLUMN_NAME_LEN.value, + sql_const.SQL_TXN_CAPABLE.value, + sql_const.SQL_IDENTIFIER_QUOTE_CHAR.value + ] + + for info_type in info_types: + # Call getinfo twice with the same info type + result1 = db_connection.getinfo(info_type) + result2 = db_connection.getinfo(info_type) + + # Results should be consistent in type and value + assert type(result1) == type(result2), f"Type inconsistency for info type {info_type}" + assert result1 == result2, f"Value inconsistency for info type {info_type}" + +def test_getinfo_standard_types(db_connection): + """Test a representative set of standard ODBC info types.""" + + # Dictionary of common info types and their expected value types + # Avoid DBMS-specific info types + info_types = { + sql_const.SQL_ACCESSIBLE_TABLES.value: str, # "Y" or "N" + sql_const.SQL_DATA_SOURCE_NAME.value: str, # DSN + sql_const.SQL_TABLE_TERM.value: str, # Usually "table" + sql_const.SQL_PROCEDURES.value: str, # "Y" or "N" + sql_const.SQL_MAX_IDENTIFIER_LEN.value: int, # Max identifier length + sql_const.SQL_OUTER_JOINS.value: str, # "Y" or "N" + } + + for info_type, expected_type in info_types.items(): + try: + info_value = db_connection.getinfo(info_type) + + # Skip None values (unsupported by driver) + if info_value is None: + continue + + # Check type, allowing empty strings for string types + if expected_type == str: + assert isinstance(info_value, str), f"Info type {info_type} should return a string" + elif expected_type == int: + assert isinstance(info_value, int), f"Info type {info_type} should return an integer" + + except Exception as e: + # Log but don't fail - some drivers might not support all info types + print(f"Info type {info_type} failed: {e}") \ No newline at end of file