This is an automated email from the ASF dual-hosted git repository. isapego pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 82d46f420f IGNITE-20346 ODBC: Implement column metadata fetching (#2650) 82d46f420f is described below commit 82d46f420f98665191110aefd0add421bddec161 Author: Igor Sapego <isap...@apache.org> AuthorDate: Tue Oct 3 14:26:08 2023 +0400 IGNITE-20346 ODBC: Implement column metadata fetching (#2650) --- modules/platforms/cpp/CMakeLists.txt | 1 - modules/platforms/cpp/DEVNOTES.md | 143 +++++---- modules/platforms/cpp/ignite/odbc/CMakeLists.txt | 1 + .../cpp/ignite/odbc/app/application_data_buffer.h | 18 ++ .../ignite/odbc/query/column_metadata_query.cpp | 324 +++++++++++++++++++++ .../cpp/ignite/odbc/query/column_metadata_query.h | 185 ++++++++++++ modules/platforms/cpp/ignite/odbc/query/query.h | 3 + .../cpp/ignite/odbc/query/type_info_query.cpp | 2 +- .../platforms/cpp/ignite/odbc/sql_statement.cpp | 11 +- modules/platforms/cpp/ignite/odbc/type_traits.cpp | 41 ++- modules/platforms/cpp/ignite/odbc/type_traits.h | 36 +-- .../cpp/ignite/protocol/client_operation.h | 3 + .../cpp/tests/odbc-test/api_robustness_test.cpp | 18 +- .../cpp/tests/odbc-test/meta_queries_test.cpp | 34 +-- 14 files changed, 705 insertions(+), 115 deletions(-) diff --git a/modules/platforms/cpp/CMakeLists.txt b/modules/platforms/cpp/CMakeLists.txt index 2d75fcb554..fac1523dcf 100644 --- a/modules/platforms/cpp/CMakeLists.txt +++ b/modules/platforms/cpp/CMakeLists.txt @@ -26,7 +26,6 @@ set(CMAKE_PROJECT_VERSION ${PROJECT_VERSION}) option(ENABLE_CONAN "Use Conan package manager to get dependencies" ON) option(ENABLE_CLIENT "Build Ignite.C++ Client module" ON) option(ENABLE_ODBC "Build Ignite ODBC driver module" OFF) -option(ENABLE_ODBC_MSI "Build Ignite ODBC driver installer for Windows" OFF) option(ENABLE_TESTS "Build Ignite.C++ tests" OFF) option(ENABLE_ADDRESS_SANITIZER "If address sanitizer is enabled" OFF) option(ENABLE_UB_SANITIZER "If undefined behavior sanitizer is enabled" OFF) diff --git a/modules/platforms/cpp/DEVNOTES.md b/modules/platforms/cpp/DEVNOTES.md index a81bef9d4f..39a267ed72 100644 --- a/modules/platforms/cpp/DEVNOTES.md +++ b/modules/platforms/cpp/DEVNOTES.md @@ -1,16 +1,18 @@ ## Build C++ ### Prerequisites -* C++ compiler supporting C++17 -* One of build systems: make, ninja, MS Visual Studio, etc -* Conan C/C++ package manager 1.X (optional) -* CMake 3.10+ +* C++ compiler supporting C++17; +* One of build systems: make, ninja, MS Visual Studio, etc; +* Conan C/C++ package manager 1.X (optional); +* CMake 3.10+; +* To build the ODBC driver, it is required to have an ODBC driver manager with headers on your system. On Windows, it + comes with your OS, but on Unix-like systems you may need to install one, for example, unixODBC; ### Installing Conan Package Manager The Conan package manager can be obtained from [its website](https://conan.io). -Currently we support Conan versions 1.X. The version 2.0+ is **not** supported yet. +Currently, we support Conan versions 1.X. The version 2.0+ is **not** supported yet. One way to install Conan is as follows (need python in your system): @@ -18,7 +20,7 @@ One way to install Conan is as follows (need python in your system): pip install conan==1.59.0 ``` -Also before use it might be required to configure the default conan profile: +Also, before use, it might be required to configure the default conan profile: ``` conan profile new --detect default @@ -29,10 +31,8 @@ conan profile update settings.compiler.libcxx=libstdc++11 default It is possible to build the project without Conan if all the dependencies are installed on the system manually. The project dependencies include the following libraries: - - - msgpack-c 4.0.0 - - gtest 1.12.1 - - unixodbc +- msgpack-c 4.0.0 +- gtest 1.12.1 When the project is configured with the `-DENABLE_CONAN=OFF` CMake option, the Conan machinery is turned off and the dependencies are resolved by using the standard means of the build platform. For example, the project can be @@ -40,100 +40,143 @@ configured like this: ```shell ... -cmake .. -DENABLE_CONAN=0 -DCMAKE_BUILD_TYPE=Release +cmake .. -DENABLE_CONAN=OFF -DCMAKE_BUILD_TYPE=Release ... ``` -However Conan is enabled by default and so all the build examples below use it. +However, Conan is enabled by default, and so all the build examples below use it. -### Linux Build +### CMake options and examples of typical build configurations -#### Building in debug mode with tests +There are multiple configuration options supported by the CMake project that can be used to choose what exactly and how +should be built. You can check the whole list of available cmake options with the command `cmake -LAH`. Below, you can +see project-specific options only: +- ENABLE_CONAN={ON|OFF}. ON by default. The effect of this exact option is described in a section above; +- ENABLE_CLIENT={ON|OFF}. ON by default. Indicates whether the C++ client should be built; +- ENABLE_ODBC={ON|OFF}. OFF by default. Indicates whether the ODBC driver should be built; +- ENABLE_TESTS={ON|OFF}. OFF by default. Indicates whether the tests for the selected components should be built; +- WARNINGS_AS_ERRORS={ON|OFF}. OFF by default. If enabled, compiler will treat warnings as errors. It may be a good idea + to enable this option if you are planning on submitting a PR, but if you just want to build a project just keep it + disabled; -In this dir: +There are also general CMake options that you should specify. They are build type options. There are two types of build +available - `Release` and `Debug`. The choice here depends on how are you going to use resulting artifacts. If you are +going to use them in production, it is probably a good idea to use `Release` build type. If you are just an Ignite +developer or planning to submit a patch for the project, use Debug. + +You should ALWAYS specify a build type. + +There are two options for this: +- CMAKE_BUILD_TYPE={Debug|Release}. Used on single-configuration generators, like Unix Makefile generator or Ninja. This + is a parameter that you should most likely use if you are using Unix-like OS; +- CMAKE_CONFIGURATION_TYPES={Debug|Release}. Used on multi-configuration generators like Visual Studio. If you are using + Visual Studio generator, you should specify this option, as it ignores CMAKE_BUILD_TYPE. + +So, basically, if you are an Apache Ignite developer, you would want to configure your project like this: ```shell -mkdir cmake-build-debug -cd cmake-build-debug -cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Debug -cmake --build . -j8 +cmake .. -DENABLE_ODBC=ON -DENABLE_TESTS=ON -DWARNINGS_AS_ERRORS=ON -DCMAKE_BUILD_TYPE=Debug ``` -#### Building in release mode without tests +But if you just want to compile the client to use in your project, the following configuration is more fitting for you, +considering default options: -In this dir: +```shell +cmake .. -DCMAKE_BUILD_TYPE=Release +``` + +If you don't need the client and just want to compile the ODBC driver, just add the following options: ```shell -mkdir cmake-build-release -cd cmake-build-release -cmake .. -DENABLE_TESTS=OFF -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Release -cmake --build . -j8 +cmake .. -DENABLE_CLIENT=OFF -DENABLE_ODBC=ON -DCMAKE_BUILD_TYPE=Release ``` -### MacOS Build +And do not forget to replace `CMAKE_BUILD_TYPE` with `CMAKE_CONFIGURATION_TYPES` if you are using Visual Studio +generator: -On macOS it is typically required to use the C++ standard library from the LLVM project: +```shell +cmake .. -DCMAKE_CONFIGURATION_TYPES=Release +``` + +Below, you can find more detailed line-by-line instructions and configurations for different platforms and use-cases. + + +### Linux and macOS Builds + +On macOS, it is typically required to specify the C++ standard library from the LLVM project if you are using Conan: ``` conan profile update settings.compiler.libcxx=libc++11 default ``` -#### Building in debug mode with tests. +#### Building in debug mode with tests and ODBC In this dir: ```shell mkdir cmake-build-debug cd cmake-build-debug -cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Debug -cmake --build . -j8 +cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build . +``` + +#### Building only the client in release mode + +In this dir: + +```shell +mkdir cmake-build-release +cd cmake-build-release +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . ``` -#### Building in release mode without tests. +#### Building only the ODBC driver in release mode In this dir: ```shell mkdir cmake-build-release cd cmake-build-release -cmake .. -DENABLE_TESTS=OFF -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Release -cmake --build . -j8 +cmake .. -DENABLE_CLIENT=OFF -DENABLE_ODBC=ON -DCMAKE_BUILD_TYPE=Release +cmake --build . ``` ### Windows Build -#### Building in debug mode with tests +#### Building in debug mode with tests and ODBC using single-config generator -In this dir (using the ninja build system, other single-config systems can be used too): +In this dir (using Ninja, but any other single-config generator can be used): ```shell mkdir cmake-build-debug cd cmake-build-debug -cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Debug -GNinja -cmake --build . -j8 +cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=ON -DCMAKE_BUILD_TYPE=Debug -GNinja +cmake --build . ``` -#### Building in release mode without tests +#### Building only the client in release mode using single-config generator -In this dir (using the ninja build system, other single-config systems can be used too): +In this dir (using Ninja, but any other single-config generator can be used): ```shell mkdir cmake-build-release cd cmake-build-release -cmake .. -DENABLE_TESTS=OFF -DENABLE_ODBC=OFF -DCMAKE_BUILD_TYPE=Release -GNinja -cmake --build . -j8 +cmake .. -DCMAKE_BUILD_TYPE=Release -GNinja +cmake --build . ``` #### Building with Visual Studio in multi-config mode Run in this dir from, for example, [VS developer PowerShell](https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022): +We are using Visual Studio 17 2022 in this example, but any other multi-config generator can be used. ```shell mkdir cmake-build cd cmake-build -cmake .. -DENABLE_TESTS=ON -cmake --build . --config Debug -j8 -cmake --build . --config Release -j8 +cmake .. -DENABLE_TESTS=ON -DENABLE_ODBC=ON -DCMAKE_CONFIGURATION_TYPES="Debug;Release" -G "Visual Studio 17 2022" +cmake --build . --config Debug +cmake --build . --config Release ``` ## Run Tests @@ -144,29 +187,29 @@ cmake --build . --config Release -j8 ### Starting Java Test Node -Tests require a running Java node. You don't need to start it separately, if there is no running test nodes, tests will -start one internally. So prior to running tests you will obviously need to build a Java part of the product. To do that -the following command can be used from the root of the repo: +Tests require a running Java node. You don't need to start it separately, if there are no running test nodes, tests will +start one internally. So prior to running tests, you will obviously need to build a Java part of the product. To do +that, the following command can be used from the root of the repo: `./gradlew assemble compileIntegrationTestJava` Or a faster variant: `./gradlew assemble compileIntegrationTestJava -x check -x assembleDist -x distTar -x distZip --parallel` You can start a Test Node separately in the root repo. Tests will detect that there is a running node and will not start -another one. This can be useful for debugging. To start node from the console you can use the following command prompt: +another one. This can be useful for debugging. To start node from the console, you can use the following command prompt: `./gradlew :ignite-runner:runnerPlatformTest --no-daemon` You can also run `org.apache.ignite.internal.runner.app.PlatformTestNodeRunner` class in IDEA with a debugger or profiler, then run Client tests as usual. -### Starting tests in Windows +### Starting tests on Windows In modules/platforms/cpp dir: `./cmake-build-debug/bin/ignite-client-test.exe` To run a specific test: `./cmake-build-debug/bin/ignite-client-test.exe --gtest_filter=Test_Cases1*` -### Starting tests in Linux +### Starting tests on Linux In modules/platforms/cpp dir: `./cmake-build-debug/bin/ignite-client-test` diff --git a/modules/platforms/cpp/ignite/odbc/CMakeLists.txt b/modules/platforms/cpp/ignite/odbc/CMakeLists.txt index ea20e78785..309ee4ce75 100644 --- a/modules/platforms/cpp/ignite/odbc/CMakeLists.txt +++ b/modules/platforms/cpp/ignite/odbc/CMakeLists.txt @@ -35,6 +35,7 @@ set(SOURCES diagnostic/diagnostic_record_storage.cpp meta/column_meta.cpp meta/table_meta.cpp + query/column_metadata_query.cpp query/data_query.cpp query/table_metadata_query.cpp query/type_info_query.cpp diff --git a/modules/platforms/cpp/ignite/odbc/app/application_data_buffer.h b/modules/platforms/cpp/ignite/odbc/app/application_data_buffer.h index fd7ea6ee8d..96b4b57b12 100644 --- a/modules/platforms/cpp/ignite/odbc/app/application_data_buffer.h +++ b/modules/platforms/cpp/ignite/odbc/app/application_data_buffer.h @@ -150,6 +150,24 @@ public: */ conversion_result put_bool(bool value); + /** + * Put in buffer value of type string. + * + * @param value Value. + * @return Conversion result. + */ + conversion_result put_string(const std::optional<std::string> &value) { + return value ? put_string(*value) : put_null(); + } + + /** + * Put in buffer value of type string. + * + * @param value Value. + * @return Conversion result. + */ + conversion_result put_string(const char *value) { return put_string(std::string(value)); } + /** * Put in buffer value of type string. * diff --git a/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.cpp b/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.cpp new file mode 100644 index 0000000000..fb31c62911 --- /dev/null +++ b/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.cpp @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <utility> + +#include "ignite/odbc/log.h" +#include "ignite/odbc/odbc_error.h" +#include "ignite/odbc/query/column_metadata_query.h" +#include "ignite/odbc/sql_connection.h" +#include "ignite/odbc/type_traits.h" + +namespace { + +enum class result_column { + /** Catalog name. NULL if not applicable to the data source. */ + TABLE_CAT = 1, + + /** Schema name. NULL if not applicable to the data source. */ + TABLE_SCHEM, + + /** Table name. */ + TABLE_NAME, + + /** Column name. */ + COLUMN_NAME, + + /** SQL data type. */ + DATA_TYPE, + + /** Data source-dependent data type name. */ + TYPE_NAME, + + /** Column size. */ + COLUMN_SIZE, + + /** The length in bytes of data transferred on fetch. */ + BUFFER_LENGTH, + + /** The total number of significant digits to the right of the decimal point. */ + DECIMAL_DIGITS, + + /** Precision. */ + NUM_PREC_RADIX, + + /** Nullability of the data in column. */ + NULLABLE, + + /** A description of the column. */ + REMARKS +}; + +using namespace ignite; + +/** + * Reads result set metadata. + * + * @param reader Reader. + * @return Result set meta columns. + */ +std::vector<odbc_column_meta> read_meta(protocol::reader &reader) { + auto size = reader.read_int32(); + + std::vector<odbc_column_meta> columns; + columns.reserve(size); + + for (std::int32_t column_idx = 0; column_idx < size; ++column_idx) { + auto has_data = reader.read_bool(); + assert(has_data); + + auto status = reader.read_int32(); + assert(status == 0); + + auto err_msg = reader.read_string_nullable(); + assert(!err_msg); + + odbc_column_meta column{}; + column.label = reader.read_string(); + column.schema = reader.read_string_nullable(); + column.table = reader.read_string_nullable(); + column.column = reader.read_string_nullable(); + + column.data_type = ignite_type(reader.read_int32()); + column.data_type_name = reader.read_string(); + reader.skip(); // data_type_class + column.nullable = reader.read_bool(); + column.precision = reader.read_int32(); + column.scale = reader.read_int32(); + + columns.emplace_back(std::move(column)); + } + + return columns; +} + +} // anonymous namespace + +namespace ignite { + +column_metadata_query::column_metadata_query( + diagnosable_adapter &diag, sql_connection &connection, std::string schema, std::string table, std::string column) + : query(diag, query_type::COLUMN_METADATA) + , m_connection(connection) + , m_schema(std::move(schema)) + , m_table(std::move(table)) + , m_column(std::move(column)) { + m_columns_meta.reserve(12); + + const std::string sch; + const std::string tbl; + + m_columns_meta.emplace_back(sch, tbl, "TABLE_CAT", ignite_type::STRING); + m_columns_meta.emplace_back(sch, tbl, "TABLE_SCHEM", ignite_type::STRING); + m_columns_meta.emplace_back(sch, tbl, "TABLE_NAME", ignite_type::STRING); + m_columns_meta.emplace_back(sch, tbl, "COLUMN_NAME", ignite_type::STRING); + m_columns_meta.emplace_back(sch, tbl, "DATA_TYPE", ignite_type::INT16); + m_columns_meta.emplace_back(sch, tbl, "TYPE_NAME", ignite_type::STRING); + m_columns_meta.emplace_back(sch, tbl, "COLUMN_SIZE", ignite_type::INT32); + m_columns_meta.emplace_back(sch, tbl, "BUFFER_LENGTH", ignite_type::INT32); + m_columns_meta.emplace_back(sch, tbl, "DECIMAL_DIGITS", ignite_type::INT16); + m_columns_meta.emplace_back(sch, tbl, "NUM_PREC_RADIX", ignite_type::INT16); + m_columns_meta.emplace_back(sch, tbl, "NULLABLE", ignite_type::INT16); + m_columns_meta.emplace_back(sch, tbl, "REMARKS", ignite_type::STRING); +} + +sql_result column_metadata_query::execute() { + if (m_executed) + close(); + + sql_result result = make_request_get_columns_meta(); + + if (result == sql_result::AI_SUCCESS) { + m_executed = true; + m_fetched = false; + + m_cursor = m_meta.begin(); + } + + return result; +} + +sql_result column_metadata_query::fetch_next_row(column_binding_map &column_bindings) { + if (!m_executed) { + m_diag.add_status_record(sql_state::SHY010_SEQUENCE_ERROR, "Query was not executed."); + + return sql_result::AI_ERROR; + } + + if (!m_fetched) + m_fetched = true; + else + ++m_cursor; + + if (m_cursor == m_meta.end()) + return sql_result::AI_NO_DATA; + + column_binding_map::iterator it; + + for (it = column_bindings.begin(); it != column_bindings.end(); ++it) + get_column(it->first, it->second); + + return sql_result::AI_SUCCESS; +} + +sql_result column_metadata_query::get_column(std::uint16_t column_idx, application_data_buffer &buffer) { + if (!m_executed) { + m_diag.add_status_record(sql_state::SHY010_SEQUENCE_ERROR, "Query was not executed."); + + return sql_result::AI_ERROR; + } + + if (m_cursor == m_meta.end()) { + m_diag.add_status_record(sql_state::S24000_INVALID_CURSOR_STATE, "Cursor has reached end of the result set."); + + return sql_result::AI_ERROR; + } + + const auto ¤t_column = *m_cursor; + switch (result_column(column_idx)) { + case result_column::TABLE_CAT: { + buffer.put_null(); + break; + } + + case result_column::TABLE_SCHEM: { + buffer.put_string(current_column.schema); + break; + } + + case result_column::TABLE_NAME: { + buffer.put_string(current_column.table); + break; + } + + case result_column::COLUMN_NAME: { + buffer.put_string(current_column.column); + break; + } + + case result_column::DATA_TYPE: { + buffer.put_int16(ignite_type_to_sql_type(current_column.data_type)); + break; + } + + case result_column::TYPE_NAME: { + buffer.put_string(current_column.data_type_name); + break; + } + + case result_column::COLUMN_SIZE: { + if (current_column.data_type == ignite_type::DECIMAL || current_column.data_type == ignite_type::NUMBER) { + buffer.put_int16(std::int16_t(current_column.precision)); + break; + } + + std::int32_t column_size = ignite_type_max_column_size(current_column.data_type); + if (column_size < 0) + buffer.put_null(); + else + buffer.put_int32(column_size); + break; + } + + case result_column::BUFFER_LENGTH: { + buffer.put_null(); + break; + } + + case result_column::DECIMAL_DIGITS: { + std::int32_t dec_digits = ignite_type_decimal_digits(current_column.data_type, current_column.scale); + if (dec_digits < 0) + buffer.put_null(); + else + buffer.put_int16(std::int16_t(dec_digits)); + break; + } + + case result_column::NUM_PREC_RADIX: { + auto radix = std::int16_t(ignite_type_num_precision_radix(current_column.data_type)); + if (radix) + buffer.put_int16(radix); + else + buffer.put_null(); + break; + } + + case result_column::NULLABLE: { + buffer.put_int16(std::int16_t(current_column.nullable ? SQL_NULLABLE : SQL_NO_NULLS)); + break; + } + + case result_column::REMARKS: { + buffer.put_string(current_column.label); + break; + } + + default: + break; + } + + return sql_result::AI_SUCCESS; +} + +sql_result column_metadata_query::close() { + m_meta.clear(); + + m_executed = false; + + return sql_result::AI_SUCCESS; +} + +sql_result column_metadata_query::make_request_get_columns_meta() { + auto success = m_diag.catch_errors([&] { + auto response = + m_connection.sync_request(protocol::client_operation::JDBC_COLUMN_META, [&](protocol::writer &writer) { + writer.write(m_schema); + writer.write(m_table); + writer.write(m_column); + }); + + protocol::reader reader{response.get_bytes_view()}; + m_has_result_set = reader.read_bool(); + + auto status = reader.read_int32(); + auto err_msg = reader.read_string_nullable(); + if (err_msg) + throw odbc_error(response_status_to_sql_state(status), *err_msg); + + if (m_has_result_set) { + m_meta = read_meta(reader); + } + + m_executed = true; + }); + + if (!success) + return sql_result::AI_ERROR; + + size_t i = 0; + for (const auto &meta : m_meta) { + LOG_MSG("\n[" << i << "] SchemaName: " << (meta.schema ? *meta.schema : "NULL") << "\n[" << i + << "] TableName: " << (meta.table ? *meta.table : "NULL") << "\n[" << i + << "] ColumnName: " << (meta.column ? *meta.column : "NULL") << "\n[" << i + << "] ColumnType: " << static_cast<std::int32_t>(meta.data_type)); + ++i; + } + + return sql_result::AI_SUCCESS; +} + +} // namespace ignite diff --git a/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.h b/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.h new file mode 100644 index 0000000000..50dab7ac62 --- /dev/null +++ b/modules/platforms/cpp/ignite/odbc/query/column_metadata_query.h @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "ignite/odbc/meta/column_meta.h" +#include "ignite/odbc/query/query.h" + +namespace ignite { + +/** Connection forward-declaration. */ +class sql_connection; + +/** + * Column metadata. + */ +struct odbc_column_meta { + /** + * Default constructor. + */ + odbc_column_meta() = default; + + /** Label. */ + std::string label; + + /** Schema name. */ + std::optional<std::string> schema; + + /** Table name. */ + std::optional<std::string> table; + + /** Column name. */ + std::optional<std::string> column; + + /** Data type. */ + ignite_type data_type{ignite_type::UNDEFINED}; + + /** Data type name. */ + std::string data_type_name; + + /** Nullability. */ + bool nullable{false}; + + /** Precision. */ + std::int32_t precision{0}; + + /** Scale. */ + std::int32_t scale{0}; +}; + +/** + * Query. + */ +class column_metadata_query : public query { +public: + /** + * Constructor. + * + * @param diag Diagnostics collector. + * @param connection Associated connection. + * @param schema Schema search pattern. + * @param table Table search pattern. + * @param column Column search pattern. + */ + column_metadata_query(diagnosable_adapter &diag, sql_connection &connection, std::string schema, std::string table, + std::string column); + + /** + * Destructor. + */ + ~column_metadata_query() override = default; + + /** + * Execute query. + * + * @return True on success. + */ + sql_result execute() override; + + /** + * Get column metadata. + * + * @return Column metadata. + */ + const column_meta_vector *get_meta() override { return &m_columns_meta; } + + /** + * Fetch next result row to application buffers. + * + * @param column_bindings Column bindings. + * @return Operation result. + */ + sql_result fetch_next_row(column_binding_map &column_bindings) override; + + /** + * Get data of the specified column in the result set. + * + * @param column_idx Column index. + * @param buffer Buffer to put column data to. + * @return Operation result. + */ + sql_result get_column(std::uint16_t column_idx, application_data_buffer &buffer) override; + + /** + * Close query. + * + * @return True on success. + */ + sql_result close() override; + + /** + * Check if data is available. + * + * @return True if data is available. + */ + bool is_data_available() const override { return m_cursor != m_meta.end(); } + + /** + * Get number of rows affected by the statement. + * + * @return Number of rows affected by the statement. + */ + std::int64_t affected_rows() const override { return 0; } + + /** + * Move to the next result set. + * + * @return Operation result. + */ + sql_result next_result_set() override { return sql_result::AI_NO_DATA; } + +private: + /** + * Make get columns metadata requests and use response to set internal state. + * + * @return Operation result. + */ + sql_result make_request_get_columns_meta(); + + /** Connection associated with the statement. */ + sql_connection &m_connection; + + /** Schema search pattern. */ + std::string m_schema; + + /** Table search pattern. */ + std::string m_table; + + /** Column search pattern. */ + std::string m_column; + + /** Query executed. */ + bool m_executed{false}; + + /** Fetched flag. */ + bool m_fetched{false}; + + /** Result set flag. */ + bool m_has_result_set{false}; + + /** Fetched metadata. */ + std::vector<odbc_column_meta> m_meta; + + /** Metadata cursor. */ + std::vector<odbc_column_meta>::iterator m_cursor; + + /** Columns metadata. */ + column_meta_vector m_columns_meta; +}; + +} // namespace ignite diff --git a/modules/platforms/cpp/ignite/odbc/query/query.h b/modules/platforms/cpp/ignite/odbc/query/query.h index ce596eff70..49fe78d270 100644 --- a/modules/platforms/cpp/ignite/odbc/query/query.h +++ b/modules/platforms/cpp/ignite/odbc/query/query.h @@ -34,6 +34,9 @@ enum class query_type { /** Table metadata. */ TABLE_METADATA, + /** Column metadata. */ + COLUMN_METADATA, + /** Type info. */ TYPE_INFO, }; diff --git a/modules/platforms/cpp/ignite/odbc/query/type_info_query.cpp b/modules/platforms/cpp/ignite/odbc/query/type_info_query.cpp index 0fb7fe3677..b428055385 100644 --- a/modules/platforms/cpp/ignite/odbc/query/type_info_query.cpp +++ b/modules/platforms/cpp/ignite/odbc/query/type_info_query.cpp @@ -293,7 +293,7 @@ sql_result type_info_query::get_column(std::uint16_t column_idx, application_dat case result_column::MINIMUM_SCALE: case result_column::MAXIMUM_SCALE: { - buffer.put_int16(std::int16_t(ignite_type_decimal_digits(current_type))); + buffer.put_int16(std::int16_t(ignite_type_decimal_digits(current_type, -1))); break; } diff --git a/modules/platforms/cpp/ignite/odbc/sql_statement.cpp b/modules/platforms/cpp/ignite/odbc/sql_statement.cpp index e29512ae98..4a3284de49 100644 --- a/modules/platforms/cpp/ignite/odbc/sql_statement.cpp +++ b/modules/platforms/cpp/ignite/odbc/sql_statement.cpp @@ -19,6 +19,7 @@ #include "ignite/odbc/log.h" #include "ignite/odbc/odbc_error.h" +#include "ignite/odbc/query/column_metadata_query.h" #include "ignite/odbc/query/data_query.h" #include "ignite/odbc/query/table_metadata_query.h" #include "ignite/odbc/query/type_info_query.h" @@ -554,16 +555,12 @@ void sql_statement::execute_get_columns_meta_query( sql_result sql_statement::internal_execute_get_columns_meta_query( const std::string &schema, const std::string &table, const std::string &column) { - UNUSED_VALUE schema; - UNUSED_VALUE table; - UNUSED_VALUE column; if (m_current_query) m_current_query->close(); - // TODO: IGNITE-20346 Implement table column metadata fetching - add_status_record(sql_state::SHYC00_OPTIONAL_FEATURE_NOT_IMPLEMENTED, "Column metadata is not supported."); - return sql_result::AI_ERROR; + m_current_query = std::make_unique<column_metadata_query>(*this, m_connection, schema, table, column); + return m_current_query->execute(); } void sql_statement::execute_get_tables_meta_query( @@ -1013,7 +1010,7 @@ sql_result sql_statement::internal_describe_param( // TODO: IGNITE-19854 Implement meta fetching for a parameter if (decimal_digits) - *decimal_digits = int16_t(ignite_type_decimal_digits(type)); + *decimal_digits = int16_t(ignite_type_decimal_digits(type, -1)); // TODO: IGNITE-19854 Implement meta fetching for a parameter if (nullable) diff --git a/modules/platforms/cpp/ignite/odbc/type_traits.cpp b/modules/platforms/cpp/ignite/odbc/type_traits.cpp index bdafc1c767..7e41d42387 100644 --- a/modules/platforms/cpp/ignite/odbc/type_traits.cpp +++ b/modules/platforms/cpp/ignite/odbc/type_traits.cpp @@ -596,16 +596,47 @@ std::int32_t ignite_type_num_precision_radix(ignite_type typ) { return sql_type_num_precision_radix(sql_type); } -std::int32_t sql_type_decimal_digits(std::int16_t) { - // TODO: IGNITE-19854 Remove once parameters meta fetching is implemented +std::int32_t sql_type_decimal_digits(std::int16_t type, std::int32_t scale) { + switch (type) { + case SQL_BIT: + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_REAL: + case SQL_FLOAT: + case SQL_DOUBLE: + return sql_type_column_size(type); + + case SQL_TYPE_TIME: + case SQL_TYPE_TIMESTAMP: + return 9; + + case SQL_DECIMAL: + case SQL_NUMERIC: + if (scale >= 0) + return scale; + break; + + case SQL_GUID: + case SQL_TYPE_DATE: + case SQL_VARCHAR: + case SQL_CHAR: + case SQL_WCHAR: + case SQL_LONGVARBINARY: + case SQL_BINARY: + case SQL_VARBINARY: + case SQL_LONGVARCHAR: + default: + break; + } return -1; } -std::int32_t ignite_type_decimal_digits(ignite_type typ) { - // TODO: IGNITE-19854 Remove once parameters meta fetching is implemented +std::int32_t ignite_type_decimal_digits(ignite_type typ, std::int32_t scale) { std::int16_t sql_type = ignite_type_to_sql_type(typ); - return sql_type_decimal_digits(sql_type); + return sql_type_decimal_digits(sql_type, scale); } bool is_sql_type_unsigned(std::int16_t type) { diff --git a/modules/platforms/cpp/ignite/odbc/type_traits.h b/modules/platforms/cpp/ignite/odbc/type_traits.h index aa9b65cc7b..04e83e8564 100644 --- a/modules/platforms/cpp/ignite/odbc/type_traits.h +++ b/modules/platforms/cpp/ignite/odbc/type_traits.h @@ -123,7 +123,7 @@ const std::string &ignite_type_to_sql_type_name(ignite_type typ); * @param type Application type. * @return True if the type is supported. */ -bool is_sql_type_supported(int16_t type); +bool is_sql_type_supported(std::int16_t type); /** * Get corresponding binary type for ODBC SQL type. @@ -131,7 +131,7 @@ bool is_sql_type_supported(int16_t type); * @param sql_type SQL type. * @return Binary type. */ -ignite_type sql_type_to_ignite_type(int16_t sql_type); +ignite_type sql_type_to_ignite_type(std::int16_t sql_type); /** * Convert ODBC type to driver type alias. @@ -139,7 +139,7 @@ ignite_type sql_type_to_ignite_type(int16_t sql_type); * @param type ODBC type; * @return Internal driver type. */ -odbc_native_type to_driver_type(int16_t type); +odbc_native_type to_driver_type(std::int16_t type); /** * Convert Ignite data type to SQL data type. @@ -147,7 +147,7 @@ odbc_native_type to_driver_type(int16_t type); * @param typ Data type. * @return SQL data type. */ -int16_t ignite_type_to_sql_type(ignite_type typ); +std::int16_t ignite_type_to_sql_type(ignite_type typ); /** * Get Ignite type SQL nullability. @@ -158,7 +158,7 @@ int16_t ignite_type_to_sql_type(ignite_type typ); * SQL_NULLABLE_UNKNOWN if it is not known whether the * column accepts NULL values. */ -int16_t ignite_type_nullability(ignite_type typ); +std::int16_t ignite_type_nullability(ignite_type typ); /** * Get SQL type display size. @@ -166,7 +166,7 @@ int16_t ignite_type_nullability(ignite_type typ); * @param type SQL type. * @return Display size. */ -int32_t sql_type_display_size(int16_t type); +std::int32_t sql_type_display_size(std::int16_t type); /** * Get Ignite type display size. @@ -174,7 +174,7 @@ int32_t sql_type_display_size(int16_t type); * @param typ Ignite type. * @return Display size. */ -int32_t ignite_type_display_size(ignite_type typ); +std::int32_t ignite_type_display_size(ignite_type typ); /** * Get SQL type column size. @@ -182,7 +182,7 @@ int32_t ignite_type_display_size(ignite_type typ); * @param type SQL type. * @return Column size. */ -int32_t sql_type_column_size(int16_t type); +std::int32_t sql_type_column_size(std::int16_t type); /** * Get Ignite type column size. @@ -190,7 +190,7 @@ int32_t sql_type_column_size(int16_t type); * @param typ Ignite type. * @return Column size. */ -int32_t ignite_type_max_column_size(ignite_type typ); +std::int32_t ignite_type_max_column_size(ignite_type typ); /** * Get SQL type transfer octet length. @@ -198,7 +198,7 @@ int32_t ignite_type_max_column_size(ignite_type typ); * @param type SQL type. * @return Transfer octet length. */ -int32_t sql_type_transfer_length(int16_t type); +std::int32_t sql_type_transfer_length(std::int16_t type); /** * Get Ignite type transfer octet length. @@ -206,7 +206,7 @@ int32_t sql_type_transfer_length(int16_t type); * @param typ Ignite type. * @return Transfer octet length. */ -int32_t ignite_type_transfer_length(ignite_type typ); +std::int32_t ignite_type_transfer_length(ignite_type typ); /** * Get SQL type numeric precision radix. @@ -214,7 +214,7 @@ int32_t ignite_type_transfer_length(ignite_type typ); * @param type SQL type. * @return Numeric precision radix. */ -int32_t sql_type_num_precision_radix(int8_t type); +std::int32_t sql_type_num_precision_radix(int8_t type); /** * Get Ignite type numeric precision radix. @@ -222,23 +222,25 @@ int32_t sql_type_num_precision_radix(int8_t type); * @param typ Ignite type. * @return Numeric precision radix. */ -int32_t ignite_type_num_precision_radix(ignite_type typ); +std::int32_t ignite_type_num_precision_radix(ignite_type typ); /** * Get SQL type decimal digits. * * @param type SQL type. + * @param scale Scale if applies. Negative value means scale is not available. * @return big_decimal digits. */ -int32_t sql_type_decimal_digits(int16_t type); +std::int32_t sql_type_decimal_digits(std::int16_t type, std::int32_t scale); /** * Get Ignite type decimal digits. * - * @param typ Ignite type. +* @param typ Ignite type. +* @param scale Scale if applies. Negative value means scale is not available. * @return big_decimal digits. */ -int32_t ignite_type_decimal_digits(ignite_type typ); +std::int32_t ignite_type_decimal_digits(ignite_type typ, std::int32_t scale); /** * Checks if the SQL type is unsigned. @@ -246,7 +248,7 @@ int32_t ignite_type_decimal_digits(ignite_type typ); * @param type SQL type. * @return True if unsigned or non-numeric. */ -bool is_sql_type_unsigned(int16_t type); +bool is_sql_type_unsigned(std::int16_t type); /** * Checks if the Ignite type is unsigned. diff --git a/modules/platforms/cpp/ignite/protocol/client_operation.h b/modules/platforms/cpp/ignite/protocol/client_operation.h index 99a1b70be7..1652977482 100644 --- a/modules/platforms/cpp/ignite/protocol/client_operation.h +++ b/modules/platforms/cpp/ignite/protocol/client_operation.h @@ -83,6 +83,9 @@ enum class client_operation { /** Get table metadata. */ JDBC_TABLE_META = 38, + /** Get column metadata. */ + JDBC_COLUMN_META = 39, + /** Begin transaction. */ TX_BEGIN = 43, diff --git a/modules/platforms/cpp/tests/odbc-test/api_robustness_test.cpp b/modules/platforms/cpp/tests/odbc-test/api_robustness_test.cpp index 7fd7d73cd9..29114df565 100644 --- a/modules/platforms/cpp/tests/odbc-test/api_robustness_test.cpp +++ b/modules/platforms/cpp/tests/odbc-test/api_robustness_test.cpp @@ -245,9 +245,7 @@ TEST_F(api_robustness_test, sql_columns) { SQLRETURN ret = SQLColumns(m_statement, catalogName, sizeof(catalogName), schemaName, sizeof(schemaName), tableName, sizeof(tableName), columnName, sizeof(columnName)); - UNUSED_VALUE ret; - // TODO IGNITE-20346: Uncomment once column metadata is implemented. - // ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); + ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); // Sizes are nulls. SQLColumns(m_conn, catalogName, 0, schemaName, 0, tableName, 0, columnName, 0); @@ -397,17 +395,11 @@ TEST_F(api_robustness_test, sql_col_attribute) { // Everything is ok. Character attribute. ret = SQLColAttribute(m_statement, 1, SQL_COLUMN_TABLE_NAME, buffer, sizeof(buffer), &resLen, &numericAttr); - - UNUSED_VALUE ret; - // TODO IGNITE-20346: Uncomment once column metadata is implemented. - // ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); + ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); // Everything is ok. Numeric attribute. ret = SQLColAttribute(m_statement, 1, SQL_DESC_COUNT, buffer, sizeof(buffer), &resLen, &numericAttr); - - UNUSED_VALUE ret; - // TODO IGNITE-20346: Uncomment once column metadata is implemented. - // ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); + ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); SQLColAttribute(m_statement, 1, SQL_COLUMN_TABLE_NAME, buffer, sizeof(buffer), &resLen, 0); SQLColAttribute(m_statement, 1, SQL_COLUMN_TABLE_NAME, buffer, sizeof(buffer), 0, &numericAttr); @@ -444,9 +436,7 @@ TEST_F(api_robustness_test, sql_describe_col) { ret = SQLDescribeCol(m_statement, 1, columnName, sizeof(columnName), &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable); - UNUSED_VALUE ret; - // TODO IGNITE-20346: Uncomment once column metadata is implemented. - // ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); + ODBC_FAIL_ON_ERROR(ret, SQL_HANDLE_STMT, m_statement); SQLDescribeCol( m_statement, 1, 0, sizeof(columnName), &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable); diff --git a/modules/platforms/cpp/tests/odbc-test/meta_queries_test.cpp b/modules/platforms/cpp/tests/odbc-test/meta_queries_test.cpp index ed07bd9e5a..11af9248b4 100644 --- a/modules/platforms/cpp/tests/odbc-test/meta_queries_test.cpp +++ b/modules/platforms/cpp/tests/odbc-test/meta_queries_test.cpp @@ -546,23 +546,20 @@ TEST_F(meta_queries_test, get_data_with_tables) { check_single_row_result_set_with_get_data(m_statement); } -// TODO: IGNITE-20346 Implement column metadata fetching -#ifdef MUTED TEST_F(meta_queries_test, get_data_with_columns) { odbc_connect(get_basic_connection_string()); - SQLCHAR empty[] = ""; - SQLCHAR table[] = "TestType"; - SQLCHAR column[] = "str"; + SQLCHAR any[] = "%"; + SQLCHAR table[] = "META_QUERIES_TEST"; + SQLCHAR column[] = "STR"; - SQLRETURN ret = SQLColumns(m_statement, empty, SQL_NTS, empty, SQL_NTS, table, SQL_NTS, column, SQL_NTS); + SQLRETURN ret = SQLColumns(m_statement, any, SQL_NTS, any, SQL_NTS, table, SQL_NTS, column, SQL_NTS); if (!SQL_SUCCEEDED(ret)) FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); check_single_row_result_set_with_get_data(m_statement); } -#endif // MUTED TEST_F(meta_queries_test, get_data_with_select_query) { odbc_connect(get_basic_connection_string()); @@ -723,19 +720,17 @@ TEST_F(meta_queries_test, tables_meta) { EXPECT_TRUE(ret == SQL_NO_DATA); } -// TODO: IGNITE-20346 Implement table column metadata fetching -#ifdef MUTED TEST_F(meta_queries_test, ddl_columns_meta) { odbc_connect(get_basic_connection_string()); - SQLCHAR create_table[] = "create table TestTable(id int primary key, testColumn varchar)"; + SQLCHAR create_table[] = "create table if not exists DDL_COLUMNS_META(ID int primary key, TEST_COLUMN varchar)"; SQLRETURN ret = SQLExecDirect(m_statement, create_table, SQL_NTS); if (!SQL_SUCCEEDED(ret)) FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); SQLCHAR any[] = "%"; - SQLCHAR table[] = "TestTable"; + SQLCHAR table[] = "DDL_COLUMNS_META"; ret = SQLColumns(m_statement, any, SQL_NTS, any, SQL_NTS, table, SQL_NTS, any, SQL_NTS); @@ -748,8 +743,8 @@ TEST_F(meta_queries_test, ddl_columns_meta) { FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); check_string_column(m_statement, 1, ""); - check_string_column(m_statement, 2, "\"PUBLIC\""); - check_string_column(m_statement, 3, "TESTTABLE"); + check_string_column(m_statement, 2, "PUBLIC"); + check_string_column(m_statement, 3, "DDL_COLUMNS_META"); check_string_column(m_statement, 4, "ID"); ret = SQLFetch(m_statement); @@ -758,9 +753,9 @@ TEST_F(meta_queries_test, ddl_columns_meta) { FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); check_string_column(m_statement, 1, ""); - check_string_column(m_statement, 2, "\"PUBLIC\""); - check_string_column(m_statement, 3, "TESTTABLE"); - check_string_column(m_statement, 4, "TESTCOLUMN"); + check_string_column(m_statement, 2, "PUBLIC"); + check_string_column(m_statement, 3, "DDL_COLUMNS_META"); + check_string_column(m_statement, 4, "TEST_COLUMN"); ret = SQLFetch(m_statement); @@ -770,7 +765,7 @@ TEST_F(meta_queries_test, ddl_columns_meta) { TEST_F(meta_queries_test, ddl_columns_meta_escaped) { odbc_connect(get_basic_connection_string()); - SQLCHAR create_table[] = "create table ESG_FOCUS(id int primary key, TEST_COLUMN varchar)"; + SQLCHAR create_table[] = "create table if not exists ESG_FOCUS(id int primary key, TEST_COLUMN varchar)"; SQLRETURN ret = SQLExecDirect(m_statement, create_table, SQL_NTS); if (!SQL_SUCCEEDED(ret)) @@ -790,7 +785,7 @@ TEST_F(meta_queries_test, ddl_columns_meta_escaped) { FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); check_string_column(m_statement, 1, ""); - check_string_column(m_statement, 2, "\"PUBLIC\""); + check_string_column(m_statement, 2, "PUBLIC"); check_string_column(m_statement, 3, "ESG_FOCUS"); check_string_column(m_statement, 4, "ID"); @@ -800,7 +795,7 @@ TEST_F(meta_queries_test, ddl_columns_meta_escaped) { FAIL() << (get_odbc_error_message(SQL_HANDLE_STMT, m_statement)); check_string_column(m_statement, 1, ""); - check_string_column(m_statement, 2, "\"PUBLIC\""); + check_string_column(m_statement, 2, "PUBLIC"); check_string_column(m_statement, 3, "ESG_FOCUS"); check_string_column(m_statement, 4, "TEST_COLUMN"); @@ -808,7 +803,6 @@ TEST_F(meta_queries_test, ddl_columns_meta_escaped) { ASSERT_EQ(ret, SQL_NO_DATA); } -#endif // MUTED // TODO: IGNITE-19854 Implement metadata fetching for the non-executed query. #ifdef MUTED