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-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 37f54717f fix(c/driver/postgresql): improve SQLSTATE error mapping 
(#4587)
37f54717f is described below

commit 37f54717f3541956e7a26863eb0fc275094653da
Author: Jeremy Tan <[email protected]>
AuthorDate: Mon Jul 27 14:33:47 2026 +1000

    fix(c/driver/postgresql): improve SQLSTATE error mapping (#4587)
    
    Fixes #4579
---
 c/driver/postgresql/CMakeLists.txt     |  2 +
 c/driver/postgresql/error.cc           | 57 ++++++++++++++++++++++++++++
 c/driver/postgresql/error.h            | 15 ++------
 c/driver/postgresql/error_test.cc      | 69 ++++++++++++++++++++++++++++++++++
 c/driver/postgresql/postgresql_test.cc |  6 +--
 5 files changed, 135 insertions(+), 14 deletions(-)

diff --git a/c/driver/postgresql/CMakeLists.txt 
b/c/driver/postgresql/CMakeLists.txt
index 71dcbdc4a..b18dab6d4 100644
--- a/c/driver/postgresql/CMakeLists.txt
+++ b/c/driver/postgresql/CMakeLists.txt
@@ -81,6 +81,8 @@ if(ADBC_BUILD_TESTS)
                 EXTRA_LABELS
                 driver-postgresql
                 SOURCES
+                error.cc
+                error_test.cc
                 postgres_type_test.cc
                 postgresql_test.cc
                 EXTRA_LINK_LIBS
diff --git a/c/driver/postgresql/error.cc b/c/driver/postgresql/error.cc
index d21edb68f..2bd78d89e 100644
--- a/c/driver/postgresql/error.cc
+++ b/c/driver/postgresql/error.cc
@@ -21,6 +21,7 @@
 #include <cstdio>
 #include <cstring>
 #include <string>
+#include <string_view>
 #include <vector>
 
 #include <libpq-fe.h>
@@ -29,6 +30,62 @@
 
 namespace adbcpq {
 
+AdbcStatusCode ClassifySqlState(const char* sqlstate) {
+  if (!sqlstate) {
+    return ADBC_STATUS_IO;
+  }
+
+  const std::string_view state(sqlstate);
+
+  // https://www.postgresql.org/docs/current/errcodes-appendix.html
+  // Exact matches must precede class matches since PostgreSQL's classes 
contain
+  // errors with distinct ADBC meanings.
+  if (state == "57014") {
+    return ADBC_STATUS_CANCELLED;
+  }
+  if (state == "0A000") {
+    return ADBC_STATUS_NOT_IMPLEMENTED;
+  }
+  if (state == "42501") {
+    return ADBC_STATUS_UNAUTHORIZED;
+  }
+
+  if (state == "42P01" || state == "42P02" || state == "42703" || state == 
"42883" ||
+      state == "42704" || state == "42602" || state == "34000" || state == 
"3D000" ||
+      state == "3F000" || state == "26000" || state == "58P01") {
+    return ADBC_STATUS_NOT_FOUND;
+  }
+
+  if (state == "42701" || state == "42P03" || state == "42P04" || state == 
"42P05" ||
+      state == "42P06" || state == "42P07" || state == "42712" || state == 
"42723" ||
+      state == "42710" || state == "58P02") {
+    return ADBC_STATUS_ALREADY_EXISTS;
+  }
+
+  const auto class_code = state.substr(0, 2);
+  if (class_code == "22") {
+    return ADBC_STATUS_INVALID_DATA;
+  }
+  if (class_code == "23") {
+    return ADBC_STATUS_INTEGRITY;
+  }
+  if (class_code == "28") {
+    return ADBC_STATUS_UNAUTHENTICATED;
+  }
+  if (class_code == "25" || class_code == "2D" || class_code == "3B" ||
+      class_code == "55") {
+    return ADBC_STATUS_INVALID_STATE;
+  }
+  if (class_code == "XX") {
+    return ADBC_STATUS_INTERNAL;
+  }
+  if (class_code == "42") {
+    return ADBC_STATUS_INVALID_ARGUMENT;
+  }
+
+  return ADBC_STATUS_IO;
+}
+
 AdbcStatusCode SetError(struct AdbcError* error, PGresult* result, const char* 
format,
                         ...) {
   if (error && error->release) {
diff --git a/c/driver/postgresql/error.h b/c/driver/postgresql/error.h
index bf8a4b2e4..78bad124b 100644
--- a/c/driver/postgresql/error.h
+++ b/c/driver/postgresql/error.h
@@ -19,6 +19,7 @@
 
 #pragma once
 
+#include <cstring>
 #include <string>
 #include <vector>
 
@@ -71,6 +72,8 @@ AdbcStatusCode SetError(struct AdbcError* error, PGresult* 
result, const char* f
 
 #undef ADBC_CHECK_PRINTF_ATTRIBUTE
 
+AdbcStatusCode ClassifySqlState(const char* sqlstate);
+
 template <typename... Args>
 Status MakeStatus(PGresult* result, const char* format_string, Args&&... args) 
{
   auto message = ::fmt::vformat(format_string, 
::fmt::make_format_args(args...));
@@ -85,17 +88,7 @@ Status MakeStatus(PGresult* result, const char* 
format_string, Args&&... args) {
 
   const char* sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE);
   if (sqlstate) {
-    // https://www.postgresql.org/docs/current/errcodes-appendix.html
-    // This can be extended in the future
-    if (std::strcmp(sqlstate, "57014") == 0) {
-      code = ADBC_STATUS_CANCELLED;
-    } else if (std::strcmp(sqlstate, "42P01") == 0 ||
-               std::strcmp(sqlstate, "42602") == 0) {
-      code = ADBC_STATUS_NOT_FOUND;
-    } else if (std::strncmp(sqlstate, "42", 0) == 0) {
-      // Class 42 — Syntax Error or Access Rule Violation
-      code = ADBC_STATUS_INVALID_ARGUMENT;
-    }
+    code = ClassifySqlState(sqlstate);
   }
 
   Status status(code, message);
diff --git a/c/driver/postgresql/error_test.cc 
b/c/driver/postgresql/error_test.cc
new file mode 100644
index 000000000..75f3bbb2a
--- /dev/null
+++ b/c/driver/postgresql/error_test.cc
@@ -0,0 +1,69 @@
+// 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 <gtest/gtest.h>
+
+#include "error.h"
+
+namespace adbcpq {
+
+TEST(PostgresErrorTest, ClassifySqlState) {
+  struct Case {
+    const char* sqlstate;
+    AdbcStatusCode status;
+  };
+
+  const Case kCases[] = {
+      {nullptr, ADBC_STATUS_IO},
+      {"57014", ADBC_STATUS_CANCELLED},
+      {"0A000", ADBC_STATUS_NOT_IMPLEMENTED},
+      {"22012", ADBC_STATUS_INVALID_DATA},
+      {"22P02", ADBC_STATUS_INVALID_DATA},
+      {"23505", ADBC_STATUS_INTEGRITY},
+      {"23503", ADBC_STATUS_INTEGRITY},
+      {"28P01", ADBC_STATUS_UNAUTHENTICATED},
+      {"28000", ADBC_STATUS_UNAUTHENTICATED},
+      {"42501", ADBC_STATUS_UNAUTHORIZED},
+      {"42P01", ADBC_STATUS_NOT_FOUND},
+      {"42P02", ADBC_STATUS_NOT_FOUND},
+      {"42703", ADBC_STATUS_NOT_FOUND},
+      {"42602", ADBC_STATUS_NOT_FOUND},
+      {"3D000", ADBC_STATUS_NOT_FOUND},
+      {"58P01", ADBC_STATUS_NOT_FOUND},
+      {"42701", ADBC_STATUS_ALREADY_EXISTS},
+      {"42P03", ADBC_STATUS_ALREADY_EXISTS},
+      {"42P07", ADBC_STATUS_ALREADY_EXISTS},
+      {"42P05", ADBC_STATUS_ALREADY_EXISTS},
+      {"42712", ADBC_STATUS_ALREADY_EXISTS},
+      {"42710", ADBC_STATUS_ALREADY_EXISTS},
+      {"58P02", ADBC_STATUS_ALREADY_EXISTS},
+      {"25P02", ADBC_STATUS_INVALID_STATE},
+      {"55P03", ADBC_STATUS_INVALID_STATE},
+      {"XX000", ADBC_STATUS_INTERNAL},
+      {"42601", ADBC_STATUS_INVALID_ARGUMENT},
+      {"08006", ADBC_STATUS_IO},
+      {"53000", ADBC_STATUS_IO},
+      {"ZZ999", ADBC_STATUS_IO},
+  };
+
+  for (const auto& test_case : kCases) {
+    EXPECT_EQ(test_case.status, ClassifySqlState(test_case.sqlstate))
+        << (test_case.sqlstate ? test_case.sqlstate : "null");
+  }
+}
+
+}  // namespace adbcpq
diff --git a/c/driver/postgresql/postgresql_test.cc 
b/c/driver/postgresql/postgresql_test.cc
index d66ffd4ad..5bb9d3061 100644
--- a/c/driver/postgresql/postgresql_test.cc
+++ b/c/driver/postgresql/postgresql_test.cc
@@ -1750,7 +1750,7 @@ TEST_F(PostgresStatementTest, SqlIngestJsonInvalid) {
   ASSERT_THAT(AdbcStatementBind(&statement, &batch.value, &schema.value, 
&error),
               IsOkStatus(&error));
   ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, nullptr, &error),
-              IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error));
+              IsStatus(ADBC_STATUS_INVALID_DATA, &error));
   ASSERT_THAT(error.message, ::testing::HasSubstr("invalid input syntax for 
type json"));
 }
 
@@ -2327,13 +2327,13 @@ TEST_F(PostgresStatementTest, 
SqlExecuteCopyZeroRowOutputError) {
                                           &reader.rows_affected, &error),
                 IsOkStatus());
     ASSERT_NO_FATAL_FAILURE(reader.GetSchema());
-    ASSERT_EQ(reader.MaybeNext(), EINVAL);
+    ASSERT_EQ(reader.MaybeNext(), EIO);
 
     AdbcStatusCode status = ADBC_STATUS_OK;
     const struct AdbcError* detail =
         AdbcErrorFromArrayStream(&reader.stream.value, &status);
     ASSERT_NE(nullptr, detail);
-    ASSERT_EQ(ADBC_STATUS_INVALID_ARGUMENT, status);
+    ASSERT_EQ(ADBC_STATUS_INVALID_DATA, status);
     ASSERT_EQ("22023", std::string_view(detail->sqlstate, 5));
   }
 }

Reply via email to