This is an automated email from the ASF dual-hosted git repository.

lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 53416d1d713 GH-50852: [C++][FlightRPC][ODBC] Decode wide 
connection/descriptor string attributes with the wide decoder (#50853)
53416d1d713 is described below

commit 53416d1d713acba688951486c9b72cc58792c3a1
Author: Vikrant Puppala <[email protected]>
AuthorDate: Fri Oct 16 06:34:35 2026 +0530

    GH-50852: [C++][FlightRPC][ODBC] Decode wide connection/descriptor string 
attributes with the wide decoder (#50853)
    
    ### Rationale for this change
    
    Two string attributes in the ODBC driver are decoded with the byte-wise
    decoder even when the value arrives through a wide (Unicode / `*W`) entry 
point,
    so the wide buffer is misread and corrupted.
    
    `ODBCConnection::SetConnectAttr(SQL_ATTR_CURRENT_CATALOG)` had its two 
decode
    branches swapped:
    
    ```cpp
    if (is_unicode) {
      SetAttributeUTF8(value, string_length, catalog);      // byte-wise
    } else {
      SetAttributeSQLWCHAR(value, string_length, catalog);  // wide
    }
    ```
    
    `is_unicode` selects the buffer **width**: a unicode (`*W`) call passes a 
wide
    `SQLWCHAR` buffer that must be decoded with `SetAttributeSQLWCHAR`; a 
non-unicode
    call passes a byte string decoded with `SetAttributeUTF8`. With the branches
    swapped, a wide catalog name (e.g. UTF-16 `"my_catalog"`) is reinterpreted
    byte-wise and stored with the wide encoding's embedded NULs
    (`"m\0y\0_\0c\0a\0t\0a\0l\0o\0g\0"`), so catalog scoping operates on a 
corrupt
    name. This is the inverse of the mapping the getter already uses:
    `GetStringAttribute` maps `is_unicode -> GetAttributeSQLWCHAR`.
    
    `ODBCDescriptor::SetField(SQL_DESC_NAME)` is the same class of bug: it
    unconditionally used the byte-wise `SetAttributeUTF8`, even though the 
matching
    getter (`GetField` / `SQL_DESC_NAME`) reads the field back with
    `GetAttributeSQLWCHAR`.
    
    **How this happened (history):**
    - The `SQL_ATTR_CURRENT_CATALOG` branches were swapped from the start, in 
the
      original driver import (GH-46522, #40939). Note the getter side was 
written
      against the shared `GetStringAttribute` helper (which decides width in one
      place and got it right), while the setter open-coded the branch inline and
      inverted the polarity — there is no matching `SetStringAttribute` helper.
    - The descriptor `SQL_DESC_NAME` case was originally *consistent* (getter 
and
      setter both byte-wise). GH-47721 (#48050) later migrated the descriptor
      string getters to the wide `GetAttributeSQLWCHAR` for correct Unicode 
column
      attributes, but left the `SQL_DESC_NAME` setter on the byte-wise decoder,
      creating the asymmetry.
    
    ### What changes are included in this PR?
    
    - `odbc_connection.cc`: swap the `SQL_ATTR_CURRENT_CATALOG` decode branches 
so a
      unicode call uses `SetAttributeSQLWCHAR` and a non-unicode call uses
      `SetAttributeUTF8`, matching `GetStringAttribute`.
    - `odbc_descriptor.cc`: decode `SQL_DESC_NAME` with `SetAttributeSQLWCHAR` 
to
      match its getter.
    - `connection_attr_test.cc`: add 
`TestSQLSetGetConnectAttrCurrentCatalogWide`, a
      `TYPED_TEST` (mock + remote fixtures) that sets a multi-character catalog
      through the wide entry point and asserts the full name round-trips back.
    - `odbc_descriptor_test.cc` (new): add 
`ODBCDescriptorTest.SetGetNameWideRoundTrips`,
      a `SetField`/`GetField` round-trip on `SQL_DESC_NAME` with a 
multi-character
      wide name. There is no `SQLSetDescField` entry point in this tree, so this
      covers the descriptor change with a direct unit test on `ODBCDescriptor`.
    
    ### Are these changes tested?
    
    Yes. Each change has a round-trip test that distinguishes the fixed and 
unfixed
    code:
    
    | check | result |
    |---|---|
    | catalog test, **with** fix (mock fixture) | PASS — `out_catalog == 
"my_catalog"` |
    | catalog test, **without** fix (mock fixture) | FAIL — `out_catalog == 
"m\0y\0_\0c\0a\0t\0a\0l\0o\0g\0"` |
    | descriptor test, **with** fix | PASS — name reads back as `"my_column"` |
    | descriptor test, **without** fix | FAIL — byte-wise decode corrupts the 
stored UTF-16 |
    | `arrow-odbc-spi-impl-test` (unit suite) | 85/85 pass |
    | `ConnectionAttributeTest/0.*` (mock suite) | 25/25 pass |
    | clang-format | clean on all changed files |
    
    The connection catalog test uses the mock fixture 
(`FlightSQLODBCMockTestBase`,
    an in-process SQLite Flight SQL server); its remote variant runs when
    `ARROW_FLIGHT_SQL_ODBC_CONN` is set. The descriptor test is a standalone 
unit
    test. Verified locally against the mock fixture and the unit suite.
    
    ### Are there any user-facing changes?
    
    Yes. A catalog name (and descriptor `SQL_DESC_NAME`) set through the wide 
entry
    point is now decoded correctly instead of being corrupted. Applications 
that set
    a multi-character catalog through `SQLSetConnectAttrW` now scope to the 
intended
    catalog.
    
    ### AI usage
    
    Per the [AI-generated code 
guidance](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code):
    the diagnosis, the fix, and the test were produced with the assistance of 
an AI
    coding agent and reviewed and verified by me. Correctness was checked by
    (1) confirming the getter path (`GetStringAttribute`) maps `is_unicode` to 
the
    wide decoder, establishing the intended mapping the setters violated, and
    (2) running the new round-trip test against both the fixed and unfixed 
driver to
    confirm it distinguishes them (clean name vs. embedded-NUL corruption).
    
    ---
    _🤖 Drafted by Claude Code (an AI agent) and reviewed & verified by 
vikrantpuppala._
    
    * GitHub Issue: #50852
    
    Lead-authored-by: Vikrant Puppala <[email protected]>
    Co-authored-by: Isaac
    Signed-off-by: David Li <[email protected]>
---
 .../arrow/flight/sql/odbc/odbc_impl/CMakeLists.txt |  1 +
 .../flight/sql/odbc/odbc_impl/odbc_connection.cc   |  4 +-
 .../flight/sql/odbc/odbc_impl/odbc_descriptor.cc   |  2 +-
 .../sql/odbc/odbc_impl/odbc_descriptor_test.cc     | 53 ++++++++++++++++++++++
 .../flight/sql/odbc/tests/connection_attr_test.cc  | 25 ++++++++++
 5 files changed, 82 insertions(+), 3 deletions(-)

diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/CMakeLists.txt 
b/cpp/src/arrow/flight/sql/odbc/odbc_impl/CMakeLists.txt
index 4d990b3e89a..17d2ee1ffee 100644
--- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/CMakeLists.txt
+++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/CMakeLists.txt
@@ -206,6 +206,7 @@ add_arrow_test(odbc_spi_impl_test
                json_converter_test.cc
                record_batch_transformer_test.cc
                util_test.cc
+               odbc_descriptor_test.cc
                EXTRA_LINK_LIBS
                ${ODBC_SPI_IMPL_TEST_EXTRA_LIBS}
                STATIC_LINK_LIBS
diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_connection.cc 
b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_connection.cc
index 142dac53ab6..e452008b27b 100644
--- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_connection.cc
+++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_connection.cc
@@ -465,9 +465,9 @@ void ODBCConnection::SetConnectAttr(SQLINTEGER attribute, 
SQLPOINTER value,
     case SQL_ATTR_CURRENT_CATALOG: {
       std::string catalog;
       if (is_unicode) {
-        SetAttributeUTF8(value, string_length, catalog);
-      } else {
         SetAttributeSQLWCHAR(value, string_length, catalog);
+      } else {
+        SetAttributeUTF8(value, string_length, catalog);
       }
       if (!spi_connection_->SetAttribute(Connection::CURRENT_CATALOG, 
catalog)) {
         throw DriverException("Option value changed.", "01S02");
diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc 
b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc
index 11e4512eb8d..d58d8b93ba0 100644
--- a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc
+++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc
@@ -215,7 +215,7 @@ void ODBCDescriptor::SetField(SQLSMALLINT record_number, 
SQLSMALLINT field_ident
       has_bindings_changed_ = true;
       break;
     case SQL_DESC_NAME:
-      SetAttributeUTF8(value, buffer_length, record.name);
+      SetAttributeSQLWCHAR(value, buffer_length, record.name);
       has_bindings_changed_ = true;
       break;
     case SQL_DESC_OCTET_LENGTH:
diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor_test.cc 
b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor_test.cc
new file mode 100644
index 00000000000..f11711497cf
--- /dev/null
+++ b/cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor_test.cc
@@ -0,0 +1,53 @@
+// 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 "arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.h"
+
+#include "arrow/flight/sql/odbc/odbc_impl/diagnostics.h"
+#include "arrow/flight/sql/odbc/odbc_impl/encoding.h"
+#include "arrow/flight/sql/odbc/odbc_impl/encoding_utils.h"
+
+#include <sql.h>
+#include <sqlext.h>
+
+#include "gtest/gtest.h"
+
+namespace arrow::flight::sql::odbc {
+
+using ODBC::ODBCDescriptor;
+
+TEST(ODBCDescriptorTest, SetGetNameWideRoundTrips) {
+  arrow::flight::sql::odbc::Diagnostics diagnostics("vendor", "component",
+                                                    OdbcVersion::V_3);
+  ODBCDescriptor desc(diagnostics, nullptr, nullptr, 
/*is_app_descriptor=*/true,
+                      /*is_writable=*/true, /*is_2x_connection=*/false);
+
+  desc.SetHeaderField(SQL_DESC_COUNT, reinterpret_cast<SQLPOINTER>(1), 0);
+
+  std::vector<uint8_t> wide;
+  Utf8ToWcs("my_column", &wide);
+  desc.SetField(1, SQL_DESC_NAME, wide.data(), 
static_cast<SQLINTEGER>(wide.size()));
+
+  SQLWCHAR out[256];
+  SQLINTEGER out_len = 0;
+  desc.GetField(1, SQL_DESC_NAME, out, sizeof(out), &out_len);
+  std::string name =
+      ODBC::SqlWcharToString(out, static_cast<SQLSMALLINT>(out_len / 
GetSqlWCharSize()));
+  EXPECT_EQ("my_column", name);
+}
+
+}  // namespace arrow::flight::sql::odbc
diff --git a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc 
b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc
index 65aadf1d8f9..07a5517ecef 100644
--- a/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc
+++ b/cpp/src/arrow/flight/sql/odbc/tests/connection_attr_test.cc
@@ -410,4 +410,29 @@ TYPED_TEST(ConnectionAttributeTest, 
TestSQLSetConnectAttrPacketSizeValid) {
 #endif
 }
 
+// A multi-character catalog set through the wide entry point must round-trip
+// intact.
+TYPED_TEST(ConnectionAttributeTest, 
TestSQLSetGetConnectAttrCurrentCatalogWide) {
+  ASSIGN_SQLWCHAR_ARR_AND_LEN(catalog, L"my_catalog");
+
+  ASSERT_EQ(SQL_SUCCESS, SQLSetConnectAttr(this->conn, 
SQL_ATTR_CURRENT_CATALOG, catalog,
+                                           catalog_len * GetSqlWCharSize()));
+
+  SQLWCHAR out_str[kOdbcBufferSize];
+  SQLINTEGER out_str_len;
+  ASSERT_EQ(SQL_SUCCESS, SQLGetConnectAttr(this->conn, 
SQL_ATTR_CURRENT_CATALOG, out_str,
+                                           kOdbcBufferSize, &out_str_len));
+  // SQLGetConnectAttr returns the length in bytes; convert to characters.
+  out_str_len /= GetSqlWCharSize();
+  std::string out_catalog =
+      ODBC::SqlWcharToString(out_str, static_cast<SQLSMALLINT>(out_str_len));
+  EXPECT_EQ("my_catalog", out_catalog);
+
+  // Connections are shared across test suites, so restore the default catalog.
+  ASSIGN_SQLWCHAR_ARR_AND_LEN(empty_catalog, L"");
+  ASSERT_EQ(SQL_SUCCESS,
+            SQLSetConnectAttr(this->conn, SQL_ATTR_CURRENT_CATALOG, 
empty_catalog,
+                              empty_catalog_len * GetSqlWCharSize()));
+}
+
 }  // namespace arrow::flight::sql::odbc

Reply via email to