From cc04e9698475833cbf34e9f994a006a0f922acec Mon Sep 17 00:00:00 2001 From: besquared Date: Tue, 22 Sep 2026 03:21:53 +0000 Subject: [PATCH 1/5] feat(c/driver/postgresql): expose numeric result type modifiers --- 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 | 17 +++++++ 7 files changed, 126 insertions(+), 2 deletions(-) diff --git a/c/driver/postgresql/postgres_type.h b/c/driver/postgresql/postgres_type.h index 825d9bbf90..83b96c76e0 100644 --- a/c/driver/postgresql/postgres_type.h +++ b/c/driver/postgresql/postgres_type.h @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -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); @@ -369,6 +376,7 @@ class PostgresType { private: uint32_t oid_; + std::optional typmod_; PostgresTypeId type_id_; std::string typname_; std::string field_name_; @@ -376,6 +384,7 @@ class PostgresType { 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(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 c157091b7c..f992cccba5 100644 --- a/c/driver/postgresql/postgresql_test.cc +++ b/c/driver/postgresql/postgresql_test.cc @@ -2042,6 +2042,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( + reader.array_view->children[0], {"1.250000000"})); + ASSERT_NO_FATAL_FAILURE(adbc_validation::CompareArray( + reader.array_view->children[1], {"1.25"})); + ASSERT_NO_FATAL_FAILURE(adbc_validation::CompareArray( + 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 child_reader; diff --git a/docs/source/driver/postgresql.rst b/docs/source/driver/postgresql.rst index 0fb23cea5f..96b5fceee0 100644 --- a/docs/source/driver/postgresql.rst +++ b/docs/source/driver/postgresql.rst @@ -299,6 +299,23 @@ being read or written. overflow/underflow; an error will be returned if this would be the case. +Numeric Result Metadata +~~~~~~~~~~~~~~~~~~~~~~~ + +Numeric result fields include ``POSTGRESQL:typmod`` metadata containing the +signed decimal string returned by libpq's ``PQfmod``. This preserves declared +precision and scale without changing the Arrow string representation or +performing decimal conversion. The value is ``-1`` when PostgreSQL supplies +no type modifier; it is not inferred from result values. + +For example, ``NUMERIC(29,9)`` has modifier ``1900557``. After subtracting +4, the upper 16 bits contain precision and the lower 11 bits contain signed +scale. Expressions need not retain their operands' modifiers. + +This metadata is available for numeric result columns from both COPY and +non-COPY execution and from ``AdbcStatementExecuteSchema``, including empty +results. It does not add modifiers to parameter schemas or catalog discovery. + Unknown Types ~~~~~~~~~~~~~ From 61ba8eda6312363303881622d7c2ae18f1682ca8 Mon Sep 17 00:00:00 2001 From: besquared Date: Tue, 22 Sep 2026 03:50:44 +0000 Subject: [PATCH 2/5] style(c/driver/postgresql): group numeric modifier with type name --- c/driver/postgresql/postgres_type.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c/driver/postgresql/postgres_type.h b/c/driver/postgresql/postgres_type.h index 83b96c76e0..16720f9ea0 100644 --- a/c/driver/postgresql/postgres_type.h +++ b/c/driver/postgresql/postgres_type.h @@ -376,9 +376,9 @@ class PostgresType { private: uint32_t oid_; - std::optional typmod_; PostgresTypeId type_id_; std::string typname_; + std::optional typmod_; std::string field_name_; std::vector children_; From 4701b5f26dafb856aef2f6f1bc3cb2fd84cab7e3 Mon Sep 17 00:00:00 2001 From: besquared Date: Tue, 22 Sep 2026 03:51:54 +0000 Subject: [PATCH 3/5] docs(c/driver/postgresql): link numeric type modifier references --- docs/source/driver/postgresql.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/driver/postgresql.rst b/docs/source/driver/postgresql.rst index 96b5fceee0..dec8dd5530 100644 --- a/docs/source/driver/postgresql.rst +++ b/docs/source/driver/postgresql.rst @@ -303,7 +303,9 @@ Numeric Result Metadata ~~~~~~~~~~~~~~~~~~~~~~~ Numeric result fields include ``POSTGRESQL:typmod`` metadata containing the -signed decimal string returned by libpq's ``PQfmod``. This preserves declared +signed decimal string returned by libpq's +`PQfmod `_. +This preserves declared precision and scale without changing the Arrow string representation or performing decimal conversion. The value is ``-1`` when PostgreSQL supplies no type modifier; it is not inferred from result values. @@ -311,6 +313,10 @@ no type modifier; it is not inferred from result values. For example, ``NUMERIC(29,9)`` has modifier ``1900557``. After subtracting 4, the upper 16 bits contain precision and the lower 11 bits contain signed scale. Expressions need not retain their operands' modifiers. +The encoding and signed-scale extraction are defined by PostgreSQL's +``make_numeric_typmod``, ``numeric_typmod_precision``, and +``numeric_typmod_scale`` in +`numeric.c `_. This metadata is available for numeric result columns from both COPY and non-COPY execution and from ``AdbcStatementExecuteSchema``, including empty From f0d03399fdfe6bc765ca5acdf461147d5f5d1f89 Mon Sep 17 00:00:00 2001 From: besquared Date: Tue, 22 Sep 2026 03:53:35 +0000 Subject: [PATCH 4/5] docs(c/driver/postgresql): align numeric metadata documentation style --- docs/source/driver/postgresql.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/driver/postgresql.rst b/docs/source/driver/postgresql.rst index dec8dd5530..c9c656d975 100644 --- a/docs/source/driver/postgresql.rst +++ b/docs/source/driver/postgresql.rst @@ -305,10 +305,10 @@ Numeric Result Metadata Numeric result fields include ``POSTGRESQL:typmod`` metadata containing the signed decimal string returned by libpq's `PQfmod `_. -This preserves declared -precision and scale without changing the Arrow string representation or -performing decimal conversion. The value is ``-1`` when PostgreSQL supplies -no type modifier; it is not inferred from result values. +This preserves declared precision and scale without changing the Arrow +string representation or performing decimal conversion. The value is ``-1`` +when PostgreSQL supplies no type modifier; it is not inferred from result +values. For example, ``NUMERIC(29,9)`` has modifier ``1900557``. After subtracting 4, the upper 16 bits contain precision and the lower 11 bits contain signed @@ -319,7 +319,7 @@ The encoding and signed-scale extraction are defined by PostgreSQL's `numeric.c `_. This metadata is available for numeric result columns from both COPY and -non-COPY execution and from ``AdbcStatementExecuteSchema``, including empty +non-COPY execution and from :c:func:`AdbcStatementExecuteSchema`, including empty results. It does not add modifiers to parameter schemas or catalog discovery. Unknown Types From c8157414ddc54c879f1a8f8fa27c7be7391514bd Mon Sep 17 00:00:00 2001 From: besquared Date: Sat, 3 Oct 2026 19:19:13 +0000 Subject: [PATCH 5/5] docs(c/driver/postgresql): simplify result metadata reference --- docs/source/driver/postgresql.rst | 37 +++++++++++++------------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/docs/source/driver/postgresql.rst b/docs/source/driver/postgresql.rst index c9c656d975..832373b17b 100644 --- a/docs/source/driver/postgresql.rst +++ b/docs/source/driver/postgresql.rst @@ -299,28 +299,21 @@ being read or written. overflow/underflow; an error will be returned if this would be the case. -Numeric Result Metadata -~~~~~~~~~~~~~~~~~~~~~~~ - -Numeric result fields include ``POSTGRESQL:typmod`` metadata containing the -signed decimal string returned by libpq's -`PQfmod `_. -This preserves declared precision and scale without changing the Arrow -string representation or performing decimal conversion. The value is ``-1`` -when PostgreSQL supplies no type modifier; it is not inferred from result -values. - -For example, ``NUMERIC(29,9)`` has modifier ``1900557``. After subtracting -4, the upper 16 bits contain precision and the lower 11 bits contain signed -scale. Expressions need not retain their operands' modifiers. -The encoding and signed-scale extraction are defined by PostgreSQL's -``make_numeric_typmod``, ``numeric_typmod_precision``, and -``numeric_typmod_scale`` in -`numeric.c `_. - -This metadata is available for numeric result columns from both COPY and -non-COPY execution and from :c:func:`AdbcStatementExecuteSchema`, including empty -results. It does not add modifiers to parameter schemas or catalog discovery. +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 `_, + as a signed decimal string. Encodes precision and scale using + PostgreSQL's `numeric typmod encoding + `_. + Value is ``-1`` if no modifier is available. Unknown Types ~~~~~~~~~~~~~