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
~~~~~~~~~~~~~