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 2dd3d10d8b feat(c/driver/postgresql): expose numeric result type 
modifiers (#4798)
2dd3d10d8b is described below

commit 2dd3d10d8b45d7ac212826e52d8e24e2a14770f7
Author: Josh Ferguson <[email protected]>
AuthorDate: Sun Oct 4 19:16:28 2026 -0700

    feat(c/driver/postgresql): expose numeric result type modifiers (#4798)
    
    ## Summary
    
    Carry libpq PQfmod into numeric result-field metadata as
    POSTGRESQL:typmod.
    
    Closes #4797.
    
    Assisted-by: OpenAI Codex
---
 c/driver/postgresql/postgres_type.h       | 14 ++++++++
 c/driver/postgresql/postgres_type_test.cc | 33 +++++++++++++++++++
 c/driver/postgresql/postgresql_test.cc    | 55 +++++++++++++++++++++++++++++++
 c/driver/postgresql/result_helper.cc      |  3 +-
 c/driver/postgresql/result_helper.h       |  3 ++
 c/driver/postgresql/result_reader.cc      |  3 +-
 docs/source/driver/postgresql.rst         | 16 +++++++++
 7 files changed, 125 insertions(+), 2 deletions(-)

diff --git a/c/driver/postgresql/postgres_type.h 
b/c/driver/postgresql/postgres_type.h
index 825d9bbf90..16720f9ea0 100644
--- a/c/driver/postgresql/postgres_type.h
+++ b/c/driver/postgresql/postgres_type.h
@@ -19,6 +19,7 @@
 
 #include <cerrno>
 #include <cstdint>
+#include <optional>
 #include <string>
 #include <unordered_map>
 #include <utility>
@@ -166,6 +167,12 @@ class PostgresType {
     return out;
   }
 
+  PostgresType WithTypeModifier(int32_t typmod) const {
+    PostgresType out(*this);
+    out.typmod_ = typmod;
+    return out;
+  }
+
   PostgresType Array(uint32_t oid = 0, const std::string& typname = "") const {
     PostgresType out(PostgresTypeId::kArray);
     out.AppendChild("item", *this);
@@ -371,11 +378,13 @@ class PostgresType {
   uint32_t oid_;
   PostgresTypeId type_id_;
   std::string typname_;
+  std::optional<int32_t> typmod_;
   std::string field_name_;
   std::vector<PostgresType> children_;
 
   static constexpr const char* kPostgresTypeKey = "ADBC:postgresql:typname";
   static constexpr const char* kTypeKey = "POSTGRESQL:type";
+  static constexpr const char* kTypeModifierKey = "POSTGRESQL:typmod";
   static constexpr const char* kExtensionName = "ARROW:extension:name";
   static constexpr const char* kOpaqueExtensionName = "arrow.opaque";
   static constexpr const char* kJsonExtensionName = "arrow.json";
@@ -389,6 +398,11 @@ class PostgresType {
     NANOARROW_RETURN_NOT_OK(ArrowMetadataBuilderInit(buffer.get(), 
schema->metadata));
     NANOARROW_RETURN_NOT_OK(ArrowMetadataBuilderAppend(
         buffer.get(), ArrowCharView(kTypeKey), ArrowCharView(typname)));
+    if (type_id_ == PostgresTypeId::kNumeric && typmod_.has_value()) {
+      const std::string typmod = std::to_string(*typmod_);
+      NANOARROW_RETURN_NOT_OK(ArrowMetadataBuilderAppend(
+          buffer.get(), ArrowCharView(kTypeModifierKey), 
ArrowCharView(typmod.c_str())));
+    }
     NANOARROW_RETURN_NOT_OK(
         ArrowSchemaSetMetadata(schema, reinterpret_cast<char*>(buffer->data)));
     return NANOARROW_OK;
diff --git a/c/driver/postgresql/postgres_type_test.cc 
b/c/driver/postgresql/postgres_type_test.cc
index 146a75a8ed..f428568966 100644
--- a/c/driver/postgresql/postgres_type_test.cc
+++ b/c/driver/postgresql/postgres_type_test.cc
@@ -29,6 +29,39 @@
 
 namespace adbcpq {
 
+TEST(PostgresTypeTest, NumericTypeModifier) {
+  for (const int32_t typmod : {-1, 1900557, 329729, 131080}) {
+    SCOPED_TRACE(typmod);
+    nanoarrow::UniqueSchema schema;
+    ArrowSchemaInit(schema.get());
+    ASSERT_EQ(PostgresType(PostgresTypeId::kNumeric)
+                  .WithTypeModifier(typmod)
+                  .WithFieldName("amount")
+                  .SetSchema(schema.get()),
+              NANOARROW_OK);
+    EXPECT_STREQ(schema->format, "u");
+    EXPECT_STREQ(schema->name, "amount");
+    ArrowStringView value = ArrowCharView("");
+    ASSERT_EQ(ArrowMetadataGetValue(schema->metadata, 
ArrowCharView("POSTGRESQL:typmod"),
+                                    &value),
+              NANOARROW_OK);
+    EXPECT_EQ(std::string(value.data, value.size_bytes), 
std::to_string(typmod));
+    ASSERT_EQ(ArrowMetadataGetValue(schema->metadata,
+                                    ArrowCharView("ARROW:extension:name"), 
&value),
+              NANOARROW_OK);
+    EXPECT_EQ(std::string(value.data, value.size_bytes), "arrow.opaque");
+  }
+
+  nanoarrow::UniqueSchema schema;
+  ArrowSchemaInit(schema.get());
+  ASSERT_EQ(PostgresType(PostgresTypeId::kNumeric).SetSchema(schema.get()), 
NANOARROW_OK);
+  ArrowStringView value = {nullptr, 0};
+  ASSERT_EQ(
+      ArrowMetadataGetValue(schema->metadata, 
ArrowCharView("POSTGRESQL:typmod"), &value),
+      NANOARROW_OK);
+  EXPECT_EQ(value.data, nullptr);
+}
+
 class MockTypeResolver : public PostgresTypeResolver {
  public:
   ArrowErrorCode Init() {
diff --git a/c/driver/postgresql/postgresql_test.cc 
b/c/driver/postgresql/postgresql_test.cc
index deef9103d6..42e5df9056 100644
--- a/c/driver/postgresql/postgresql_test.cc
+++ b/c/driver/postgresql/postgresql_test.cc
@@ -2044,6 +2044,61 @@ TEST_F(PostgresStatementTest, 
ExecuteSchemaParameterizedQuery) {
   ASSERT_THAT(AdbcStatementRelease(&statement, &error), IsOkStatus(&error));
 }
 
+TEST_F(PostgresStatementTest, NumericTypeModifier) {
+  ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error), 
IsOkStatus(&error));
+  for (const char* use_copy : {"true", "false"}) {
+    SCOPED_TRACE(use_copy);
+    ASSERT_THAT(
+        AdbcStatementSetOption(&statement, "adbc.postgresql.use_copy", 
use_copy, &error),
+        IsOkStatus(&error));
+    for (const char* predicate : {"true", "false"}) {
+      SCOPED_TRACE(predicate);
+      const std::string query =
+          "SELECT 1.25::numeric(29,9), 1.25::numeric, NULL::numeric(29,9), "
+          "1.25::numeric(29,9) + 1.25::numeric(29,9), 1::integer WHERE "s +
+          predicate;
+      ASSERT_THAT(AdbcStatementSetSqlQuery(&statement, query.c_str(), &error),
+                  IsOkStatus(&error));
+      nanoarrow::UniqueSchema described;
+      ASSERT_THAT(AdbcStatementExecuteSchema(&statement, described.get(), 
&error),
+                  IsOkStatus(&error));
+      adbc_validation::StreamReader reader;
+      ASSERT_THAT(AdbcStatementExecuteQuery(&statement, &reader.stream.value,
+                                            &reader.rows_affected, &error),
+                  IsOkStatus(&error));
+      ASSERT_NO_FATAL_FAILURE(reader.GetSchema());
+      for (const ArrowSchema* schema : {described.get(), 
&reader.schema.value}) {
+        ASSERT_EQ(schema->n_children, 5);
+        for (int i = 0; i < 5; i++) {
+          ArrowStringView value = {nullptr, 0};
+          ASSERT_EQ(ArrowMetadataGetValue(schema->children[i]->metadata,
+                                          ArrowCharView("POSTGRESQL:typmod"), 
&value),
+                    NANOARROW_OK);
+          if (i == 4) {
+            EXPECT_EQ(value.data, nullptr);
+          } else {
+            EXPECT_STREQ(schema->children[i]->format, "u");
+            EXPECT_EQ(std::string(value.data, value.size_bytes),
+                      (i == 0 || i == 2) ? "1900557" : "-1");
+          }
+        }
+      }
+      ASSERT_NO_FATAL_FAILURE(reader.Next());
+      if (std::string(predicate) == "true") {
+        ASSERT_NE(reader.array->release, nullptr);
+        ASSERT_NO_FATAL_FAILURE(adbc_validation::CompareArray<std::string>(
+            reader.array_view->children[0], {"1.250000000"}));
+        ASSERT_NO_FATAL_FAILURE(adbc_validation::CompareArray<std::string>(
+            reader.array_view->children[1], {"1.25"}));
+        ASSERT_NO_FATAL_FAILURE(adbc_validation::CompareArray<std::string>(
+            reader.array_view->children[2], {std::nullopt}));
+        ASSERT_NO_FATAL_FAILURE(reader.Next());
+      }
+      ASSERT_EQ(reader.array->release, nullptr);
+    }
+  }
+}
+
 TEST_F(PostgresStatementTest, ExecuteParameterizedQueryWithResult) {
   nanoarrow::UniqueSchema schema_bind;
   ArrowSchemaInit(schema_bind.get());
diff --git a/c/driver/postgresql/result_helper.cc 
b/c/driver/postgresql/result_helper.cc
index 7e276aff75..95a686730f 100644
--- a/c/driver/postgresql/result_helper.cc
+++ b/c/driver/postgresql/result_helper.cc
@@ -183,7 +183,8 @@ Status 
PqResultHelper::ResolveOutputTypes(PostgresTypeResolver& type_resolver,
       pg_type = PostgresType::Unnamed(pg_oid);
     }
 
-    root_type.AppendChild(PQfname(result_, i), pg_type);
+    root_type.AppendChild(PQfname(result_, i),
+                          pg_type.WithTypeModifier(PQfmod(result_, i)));
   }
 
   *result_types = root_type;
diff --git a/c/driver/postgresql/result_helper.h 
b/c/driver/postgresql/result_helper.h
index 4b0e9a6b3c..661851a1ec 100644
--- a/c/driver/postgresql/result_helper.h
+++ b/c/driver/postgresql/result_helper.h
@@ -169,6 +169,9 @@ class PqResultHelper {
     return PQfname(result_, column_number);
   }
   Oid FieldType(int column_number) const { return PQftype(result_, 
column_number); }
+  int FieldTypeModifier(int column_number) const {
+    return PQfmod(result_, column_number);
+  }
   PqResultRow Row(int i) const { return PqResultRow(result_, i); }
 
   class iterator {
diff --git a/c/driver/postgresql/result_reader.cc 
b/c/driver/postgresql/result_reader.cc
index b44dba3d87..df478fce3a 100644
--- a/c/driver/postgresql/result_reader.cc
+++ b/c/driver/postgresql/result_reader.cc
@@ -203,7 +203,8 @@ Status PqResultArrayReader::Initialize(int64_t* 
rows_affected) {
     UNWRAP_ERRNO(Internal,
                  type_resolver_->FindWithDefault(helper_.FieldType(i), 
&child_type));
 
-    child_type = child_type.WithFieldName(helper_.FieldName(i));
+    child_type = child_type.WithFieldName(helper_.FieldName(i))
+                     .WithTypeModifier(helper_.FieldTypeModifier(i));
     UNWRAP_ERRNO(Internal, child_type.SetSchema(schema_->children[i], 
vendor_name_));
 
     std::unique_ptr<PostgresCopyFieldReader> child_reader;
diff --git a/docs/source/driver/postgresql.rst 
b/docs/source/driver/postgresql.rst
index 0fb23cea5f..832373b17b 100644
--- a/docs/source/driver/postgresql.rst
+++ b/docs/source/driver/postgresql.rst
@@ -299,6 +299,22 @@ being read or written.
                 overflow/underflow; an error will be returned if this would be
                 the case.
 
+Result Field Metadata
+~~~~~~~~~~~~~~~~~~~~~
+
+The driver attaches the following PostgreSQL metadata to Arrow result fields:
+
+``POSTGRESQL:type``
+    The PostgreSQL type name, such as ``int4`` or ``numeric``.
+
+``POSTGRESQL:typmod``
+    The type modifier for a top-level numeric column, returned by
+    `PQfmod 
<https://www.postgresql.org/docs/18/libpq-exec.html#LIBPQ-PQFMOD>`_,
+    as a signed decimal string.  Encodes precision and scale using
+    PostgreSQL's `numeric typmod encoding
+    
<https://github.com/postgres/postgres/blob/REL_18_0/src/backend/utils/adt/numeric.c#L891-L947>`_.
+    Value is ``-1`` if no modifier is available.
+
 Unknown Types
 ~~~~~~~~~~~~~
 

Reply via email to