diff --git a/src/api_info.cpp b/src/api_info.cpp index 5f5399b5..73e9f2bd 100644 --- a/src/api_info.cpp +++ b/src/api_info.cpp @@ -265,6 +265,8 @@ SQLSMALLINT ApiInfo::FindRelatedSQLType(duckdb::LogicalTypeId type_id) { return SQL_DOUBLE; case LogicalTypeId::LIST: return SQL_VARCHAR; + case LogicalTypeId::BIT: + return SQL_BIT; default: return SQL_UNKNOWN_TYPE; } diff --git a/src/common/odbc_utils.cpp b/src/common/odbc_utils.cpp index 4f418b1a..2015bf88 100644 --- a/src/common/odbc_utils.cpp +++ b/src/common/odbc_utils.cpp @@ -77,64 +77,103 @@ string OdbcUtils::ParseStringFilter(const string &filter_name, const string &fil string OdbcUtils::GetQueryDuckdbColumns(const string &catalog_filter, const string &schema_filter, const string &table_filter, const string &column_filter) { string sql_duckdb_columns = R"( - SELECT - NULL "TABLE_CAT", - schema_name "TABLE_SCHEM", - table_name "TABLE_NAME", - column_name "COLUMN_NAME", - data_type_id "DATA_TYPE", - data_type "TYPE_NAME", - CASE - WHEN data_type='DATE' THEN 12 - WHEN data_type='TIME' THEN 15 - WHEN data_type LIKE 'TIMESTAMP%' THEN 26 - WHEN data_type='CHAR' OR data_type='BOOLEAN' THEN 1 - WHEN data_type='VARCHAR' OR data_type='BLOB' THEN character_maximum_length - WHEN data_type LIKE '%INT%' THEN numeric_precision - WHEN data_type like 'DECIMAL%' THEN numeric_precision - WHEN data_type='FLOAT' OR data_type='DOUBLE' THEN numeric_precision - ELSE NULL - END as "COLUMN_SIZE", - CASE - WHEN data_type='DATE' THEN 4 - WHEN data_type='TIME' THEN 8 - WHEN data_type LIKE 'TIMESTAMP%' THEN 8 - WHEN data_type='CHAR' OR data_type='BOOLEAN' THEN 1 - WHEN data_type='VARCHAR' OR data_type='BLOB' THEN 16 - WHEN data_type LIKE '%TINYINT' THEN 1 - WHEN data_type LIKE '%SMALLINT' THEN 2 - WHEN data_type LIKE '%INTEGER' THEN 4 - WHEN data_type LIKE '%BIGINT' THEN 8 - WHEN data_type='HUGEINT' THEN 16 - WHEN data_type='FLOAT' THEN 4 - WHEN data_type='DOUBLE' THEN 8 - ELSE NULL - END as "BUFFER_LENGTH", - numeric_scale "DECIMAL_DIGITS", - numeric_precision_radix "NUM_PREC_RADIX", - CASE is_nullable - WHEN false THEN 0 - WHEN true THEN 1 - ELSE 2 - END as "NULLABLE", - NULL "REMARKS", - column_default "COLUMN_DEF", - data_type_id "SQL_DATA_TYPE", - CASE - WHEN data_type='DATE' OR data_type='TIME' OR data_type LIKE 'TIMESTAMP%' THEN data_type_id - ELSE NULL - END as "SQL_DATETIME_SUB", - CASE - WHEN data_type='%CHAR' OR data_type='BLOB' THEN character_maximum_length - ELSE NULL - END as "CHAR_OCTET_LENGTH", - column_index as "ORDINAL_POSITION", - CASE is_nullable - WHEN false THEN 'NO' - WHEN true THEN 'YES' - ELSE '' - END as "IS_NULLABLE" - FROM duckdb_columns + SELECT * EXCLUDE mapping + FROM ( + SELECT NULL "TABLE_CAT", + SCHEMA_NAME "TABLE_SCHEM", + TABLE_NAME "TABLE_NAME", + COLUMN_NAME "COLUMN_NAME", + MAP { + 'BOOLEAN': 1, -- SQL_CHAR + 'TINYINT': -6, -- SQL_TINYINT + 'UTINYINT': -6, -- SQL_TINYINT + 'SMALLINT': 5, -- SQL_SMALLINT + 'USMALLINT': 5, -- SQL_SMALLINT + 'INTEGER': 4, -- SQL_INTEGER + 'UINTEGER': 4, -- SQL_INTEGER + 'BIGINT': -5, -- SQL_BIGINT + 'UBIGINT': -5, -- SQL_BIGINT + 'FLOAT': 6, -- SQL_FLOAT + 'HUGEINT': 8, -- SQL_DOUBLE + 'DOUBLE': 8, -- SQL_DOUBLE + 'DATE': 91, -- SQL_TYPE_DATE + 'TIMESTAMP': 93, -- SQL_TYPE_TIMESTAMP + 'TIME': 92, -- SQL_TYPE_TIME + 'VARCHAR': 12, -- SQL_VARCHAR + 'BLOB': -3, -- SQL_VARBINARY + 'INTERVAL': 10, -- SQL_INTERVAL + 'DECIMAL': 8, -- SQL_DOUBLE + 'BIT': -7, -- SQL_BIT + 'LIST': 12 -- SQL_VARCHAR + } AS mapping, + CASE + WHEN len(mapping[data_type]) != 0 THEN mapping[data_type][1]::BIGINT + ELSE data_type_id + END AS "DATA_TYPE", + data_type "TYPE_NAME", + CASE + WHEN data_type='DATE' THEN 12 + WHEN data_type='TIME' THEN 15 + WHEN data_type LIKE 'TIMESTAMP%' THEN 26 + WHEN data_type='CHAR' + OR data_type='BOOLEAN' THEN 1 + WHEN data_type='VARCHAR' + OR data_type='BLOB' THEN character_maximum_length + WHEN data_type LIKE '%INT%' THEN numeric_precision + WHEN data_type like 'DECIMAL%' THEN numeric_precision + WHEN data_type='FLOAT' + OR data_type='DOUBLE' THEN numeric_precision + ELSE NULL + END AS "COLUMN_SIZE", + CASE + WHEN data_type='DATE' THEN 4 + WHEN data_type='TIME' THEN 8 + WHEN data_type LIKE 'TIMESTAMP%' THEN 8 + WHEN data_type='CHAR' + OR data_type='BOOLEAN' THEN 1 + WHEN data_type='VARCHAR' + OR data_type='BLOB' THEN 16 + WHEN data_type LIKE '%TINYINT' THEN 1 + WHEN data_type LIKE '%SMALLINT' THEN 2 + WHEN data_type LIKE '%INTEGER' THEN 4 + WHEN data_type LIKE '%BIGINT' THEN 8 + WHEN data_type='HUGEINT' THEN 16 + WHEN data_type='FLOAT' THEN 4 + WHEN data_type='DOUBLE' THEN 8 + ELSE NULL + END AS "BUFFER_LENGTH", + numeric_scale "DECIMAL_DIGITS", + numeric_precision_radix "NUM_PREC_RADIX", + CASE is_nullable + WHEN FALSE THEN 0 + WHEN TRUE THEN 1 + ELSE 2 + END AS "NULLABLE", + NULL "REMARKS", + column_default "COLUMN_DEF", + CASE + WHEN len(mapping[data_type]) != 0 THEN mapping[data_type][1]::BIGINT + ELSE data_type_id + END AS "SQL_DATA_TYPE", + CASE + WHEN data_type='DATE' + OR data_type='TIME' + OR data_type LIKE 'TIMESTAMP%' THEN data_type_id + ELSE NULL + END AS "SQL_DATETIME_SUB", + CASE + WHEN data_type='%CHAR' + OR data_type='BLOB' THEN character_maximum_length + ELSE NULL + END AS "CHAR_OCTET_LENGTH", + column_index AS "ORDINAL_POSITION", + CASE is_nullable + WHEN FALSE THEN 'NO' + WHEN TRUE THEN 'YES' + ELSE '' + END AS "IS_NULLABLE" + FROM duckdb_columns + ) )"; sql_duckdb_columns += " WHERE "; diff --git a/test/tests/catalog_functions.cpp b/test/tests/catalog_functions.cpp index cf6857c6..e9348d30 100644 --- a/test/tests/catalog_functions.cpp +++ b/test/tests/catalog_functions.cpp @@ -1,4 +1,5 @@ #include "odbc_test_common.h" +#include #include @@ -204,13 +205,13 @@ static void TestSQLColumns(HSTMT &hstmt, std::map &types_m } std::vector> expected_data = { - {"bool_table", "id", "13", "INTEGER"}, {"bool_table", "t", "25", "VARCHAR"}, - {"bool_table", "b", "10", "BOOLEAN"}, {"bytea_table", "id", "13", "INTEGER"}, - {"bytea_table", "t", "26", "BLOB"}, {"interval_table", "id", "13", "INTEGER"}, - {"interval_table", "iv", "27", "INTERVAL"}, {"interval_table", "d", "25", "VARCHAR"}, - {"lo_test_table", "id", "13", "INTEGER"}, {"lo_test_table", "large_data", "26", "BLOB"}, - {"test_table_1", "id", "13", "INTEGER"}, {"test_table_1", "t", "25", "VARCHAR"}, - {"test_view", "id", "13", "INTEGER"}, {"test_view", "t", "25", "VARCHAR"}}; + {"bool_table", "id", "4", "INTEGER"}, {"bool_table", "t", "12", "VARCHAR"}, + {"bool_table", "b", "1", "BOOLEAN"}, {"bytea_table", "id", "4", "INTEGER"}, + {"bytea_table", "t", "-3", "BLOB"}, {"interval_table", "id", "4", "INTEGER"}, + {"interval_table", "iv", "10", "INTERVAL"}, {"interval_table", "d", "12", "VARCHAR"}, + {"lo_test_table", "id", "4", "INTEGER"}, {"lo_test_table", "large_data", "-3", "BLOB"}, + {"test_table_1", "id", "4", "INTEGER"}, {"test_table_1", "t", "12", "VARCHAR"}, + {"test_view", "id", "4", "INTEGER"}, {"test_view", "t", "12", "VARCHAR"}}; for (int i = 0; i < expected_data.size(); i++) { SQLRETURN ret = SQLFetch(hstmt); @@ -278,3 +279,128 @@ TEST_CASE("Test Catalog Functions (SQLGetTypeInfo, SQLTables, SQLColumns, SQLGet // Disconnect from the database DISCONNECT_FROM_DATABASE(env, dbc); } + +// SQLColumns DATA_TYPE and SQL_DATA_TYPE should return SQL types and shoud be the same as the column type in SQLDescribeCol +TEST_CASE("Test SQLColumns DATA_TYPE and SQL_DATA_TYPE and compare to SQLDescribeCol", "[odbc]") { + SQLHANDLE env; + SQLHANDLE dbc; + + HSTMT hstmt = SQL_NULL_HSTMT; + + // Connect to the database using SQLConnect + CONNECT_TO_DATABASE(env, dbc); + + // Allocate a statement handle + EXECUTE_AND_CHECK("SQLAllocHandle (HSTMT)", SQLAllocHandle, SQL_HANDLE_STMT, dbc, &hstmt); + + std::vector SQL_types = { + SQL_VARCHAR, + SQL_SMALLINT, + SQL_INTEGER, + SQL_FLOAT, + SQL_DOUBLE, + SQL_BIT, + SQL_TINYINT, + SQL_BIGINT, + SQL_VARBINARY, + SQL_TYPE_DATE, + SQL_TYPE_TIME, + }; + + std::string query = "CREATE TABLE test_table ("; + for(int i = 0; i < SQL_types.size(); i++) { + query += "c" + std::to_string(i) + " "; + switch(SQL_types[i]) { + case SQL_VARCHAR: + query += "VARCHAR"; + break; + case SQL_SMALLINT: + query += "SMALLINT"; + break; + case SQL_INTEGER: + query += "INTEGER"; + break; + case SQL_FLOAT: + query += "FLOAT"; + break; + case SQL_DOUBLE: + query += "DOUBLE"; + break; + case SQL_BIT: + query += "BIT"; + break; + case SQL_TINYINT: + query += "TINYINT"; + break; + case SQL_BIGINT: + query += "BIGINT"; + break; + case SQL_VARBINARY: + query += "BLOB"; + break; + case SQL_TYPE_DATE: + query += "DATE"; + break; + case SQL_TYPE_TIME: + query += "TIME"; + break; + } + query += (i == SQL_types.size() - 1) ? ")" : ", "; + } + + // Create a table with different SQL types + EXECUTE_AND_CHECK("SQLExecDirect (CREATE TABLE)", SQLExecDirect, hstmt, + ConvertToSQLCHAR(query), SQL_NTS); + + // Insert data into the table + EXECUTE_AND_CHECK("SQLExecDirect (INSERT INTO)", SQLExecDirect, hstmt, + ConvertToSQLCHAR("INSERT INTO test_table VALUES ('a', 1, 2, 3.14, 3.14159, true, 1, 10000000000, 'blob', '2021-01-01', '12:00:00')"), SQL_NTS); + + // Call SQLColumns to get the columns of the test_table + EXECUTE_AND_CHECK("SQLColumns", SQLColumns, hstmt, nullptr, 0, ConvertToSQLCHAR("main"), SQL_NTS, + ConvertToSQLCHAR("test_table"), SQL_NTS, nullptr, 0); + + // Retrieve SQL_DATA_TYPE and DATA_TYPE for each column + SQLINTEGER sql_data_type; + SQLINTEGER data_type; + SQLLEN len_or_ind_ptr; + EXECUTE_AND_CHECK("SQLBindCol", SQLBindCol, hstmt, 5, SQL_INTEGER, &data_type, sizeof(data_type), &len_or_ind_ptr); + EXECUTE_AND_CHECK("SQLBindCol", SQLBindCol, hstmt, 14, SQL_INTEGER, &sql_data_type, sizeof(sql_data_type), &len_or_ind_ptr); + + // Fetch the results + for(int i = 0; i < SQL_types.size(); i++) { + SQLRETURN ret = SQLFetch(hstmt); + if (ret == SQL_SUCCESS_WITH_INFO) { + std::string state, message; + ACCESS_DIAGNOSTIC(state, message, hstmt, SQL_HANDLE_STMT); + REQUIRE(state == "07006"); + REQUIRE(duckdb::StringUtil::Contains(message, "Invalid Input Error")); + ret = SQL_SUCCESS; + } else { + ODBC_CHECK(ret, "SQLFetch"); + } + + REQUIRE(data_type == SQL_types[i]); + REQUIRE(sql_data_type == SQL_types[i]); + } + + // Use SQLDescribeCol to assert that the data type is the same for each column + // Select * from the table + EXECUTE_AND_CHECK("SQLExecDirect (SELECT *)", SQLExecDirect, hstmt, ConvertToSQLCHAR("SELECT * FROM test_table"), SQL_NTS); + + // Fetch the results + EXECUTE_AND_CHECK("SQLFetch", SQLFetch, hstmt); + + // Check the data types of the columns using METADATA_CHECK which calls SQLDescribeCol + for(int i = 0; i < SQL_types.size(); i++) { + std::string col_name = "c" + std::to_string(i); + METADATA_CHECK(hstmt, i + 1, col_name, col_name.size(), SQL_types[i], 0, 0, SQL_NULLABLE_UNKNOWN); + } + + // Free the statement handle + EXECUTE_AND_CHECK("SQLFreeStmt (HSTMT)", SQLFreeStmt, hstmt, SQL_CLOSE); + EXECUTE_AND_CHECK("SQLFreeHandle (HSTMT)", SQLFreeHandle, SQL_HANDLE_STMT, hstmt); + + // Disconnect from the database + DISCONNECT_FROM_DATABASE(env, dbc); +} \ No newline at end of file