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 6e0d7a135 feat(c/driver/postgresql): support bind/query arrow.uuid <=> 
uuid (#4612)
6e0d7a135 is described below

commit 6e0d7a1355c56cd9eb95ea2a2f86ae8a33b872eb
Author: Mandukhai Alimaa <[email protected]>
AuthorDate: Wed Jul 29 16:36:16 2026 -0700

    feat(c/driver/postgresql): support bind/query arrow.uuid <=> uuid (#4612)
    
    Closes #4567
---
 c/driver/postgresql/copy/reader.h                  | 37 ++++++++++
 c/driver/postgresql/postgres_type.h                | 38 +++++++++-
 c/driver/postgresql/postgres_type_test.cc          | 35 +++++++++
 .../validation/queries/ingest/uuid.txtcase         | 50 +++++++++++++
 .../validation/queries/type/bind/uuid.txtcase      | 86 ++++++++++++++++++++++
 .../validation/queries/type/literal/uuid.txtcase   | 45 +++++++++++
 .../validation/queries/type/select/uuid.txtcase    | 65 ++++++++++++++++
 c/driver/postgresql/validation/tests/postgresql.py |  1 +
 8 files changed, 353 insertions(+), 4 deletions(-)

diff --git a/c/driver/postgresql/copy/reader.h 
b/c/driver/postgresql/copy/reader.h
index df9209a34..b2cddbb94 100644
--- a/c/driver/postgresql/copy/reader.h
+++ b/c/driver/postgresql/copy/reader.h
@@ -459,6 +459,39 @@ class PostgresCopyBinaryFieldReader : public 
PostgresCopyFieldReader {
   }
 };
 
+// Reader for Pg->Arrow conversions into a fixed-width Arrow binary type (e.g.
+// FixedSizeBinary(16) for a canonical arrow.uuid field).
+class PostgresCopyFixedSizeBinaryFieldReader : public PostgresCopyFieldReader {
+ public:
+  ArrowErrorCode Read(ArrowBufferView* data, int32_t field_size_bytes, 
ArrowArray* array,
+                      ArrowError* error) override {
+    // -1 for NULL (0 would be empty string)
+    if (field_size_bytes < 0) {
+      return ArrowArrayAppendNull(array, 1);
+    }
+
+    if (field_size_bytes != schema_view_.fixed_size) {
+      ArrowErrorSet(error, "Expected field with %d bytes but found field with 
%d bytes",
+                    static_cast<int>(schema_view_.fixed_size),
+                    static_cast<int>(field_size_bytes));  // 
NOLINT(runtime/int)
+      return EINVAL;
+    }
+
+    if (field_size_bytes > data->size_bytes) {
+      ArrowErrorSet(error, "Expected %d bytes of field data but got %d bytes 
of input",
+                    static_cast<int>(field_size_bytes),
+                    static_cast<int>(data->size_bytes));  // 
NOLINT(runtime/int)
+      return EINVAL;
+    }
+
+    NANOARROW_RETURN_NOT_OK(ArrowBufferAppend(data_, data->data.data, 
field_size_bytes));
+    data->data.as_uint8 += field_size_bytes;
+    data->size_bytes -= field_size_bytes;
+
+    return AppendValid(array);
+  }
+};
+
 /// Postgres JSONB emits as the JSON string prefixed with a version number
 /// 
(https://github.com/postgres/postgres/blob/3f44959f47460fb350d25d760cf2384f9aa14e9a/src/backend/utils/adt/jsonb.c#L80-L87
 /// ) Currently there is only one version, so functionally this is a just 
string prefixed
@@ -860,6 +893,10 @@ static inline ArrowErrorCode MakeCopyFieldReader(
       *out = std::make_unique<PostgresCopyBinaryFieldReader>();
       return NANOARROW_OK;
 
+    case NANOARROW_TYPE_FIXED_SIZE_BINARY:
+      *out = std::make_unique<PostgresCopyFixedSizeBinaryFieldReader>();
+      return NANOARROW_OK;
+
     case NANOARROW_TYPE_LIST:
       switch (pg_type.type_id()) {
         case PostgresTypeId::kArray: {
diff --git a/c/driver/postgresql/postgres_type.h 
b/c/driver/postgresql/postgres_type.h
index 248bf2a13..ed27f5ba1 100644
--- a/c/driver/postgresql/postgres_type.h
+++ b/c/driver/postgresql/postgres_type.h
@@ -275,6 +275,20 @@ class PostgresType {
       case PostgresTypeId::kBytea:
         NANOARROW_RETURN_NOT_OK(ArrowSchemaSetType(schema, 
NANOARROW_TYPE_BINARY));
         break;
+      case PostgresTypeId::kUuid: {
+        NANOARROW_RETURN_NOT_OK(
+            ArrowSchemaSetTypeFixedSize(schema, 
NANOARROW_TYPE_FIXED_SIZE_BINARY, 16));
+        nanoarrow::UniqueBuffer buffer;
+
+        NANOARROW_RETURN_NOT_OK(ArrowMetadataBuilderInit(buffer.get(), 
nullptr));
+        NANOARROW_RETURN_NOT_OK(
+            ArrowMetadataBuilderAppend(buffer.get(), 
ArrowCharView(kExtensionName),
+                                       ArrowCharView(kUuidExtensionName)));
+        NANOARROW_RETURN_NOT_OK(
+            ArrowSchemaSetMetadata(schema, 
reinterpret_cast<char*>(buffer->data)));
+
+        break;
+      }
 
       // ---- Temporal --------------------
       case PostgresTypeId::kDate:
@@ -359,6 +373,7 @@ class PostgresType {
   static constexpr const char* kExtensionName = "ARROW:extension:name";
   static constexpr const char* kOpaqueExtensionName = "arrow.opaque";
   static constexpr const char* kJsonExtensionName = "arrow.json";
+  static constexpr const char* kUuidExtensionName = "arrow.uuid";
   static constexpr const char* kExtensionMetadata = "ARROW:extension:metadata";
 
   ArrowErrorCode AddPostgresTypeMetadata(ArrowSchema* schema,
@@ -590,10 +605,13 @@ inline ArrowErrorCode PostgresType::FromSchema(const 
PostgresTypeResolver& resol
   ArrowSchemaView schema_view;
   NANOARROW_RETURN_NOT_OK(ArrowSchemaViewInit(&schema_view, schema, error));
 
-  if (schema_view.extension_name.data != nullptr &&
-      std::string_view(schema_view.extension_name.data,
-                       schema_view.extension_name.size_bytes)
-              .compare("arrow.json") == 0) {
+  std::string_view extension_name;
+  if (schema_view.extension_name.data != nullptr) {
+    extension_name = std::string_view(schema_view.extension_name.data,
+                                      schema_view.extension_name.size_bytes);
+  }
+
+  if (extension_name == kJsonExtensionName) {
     switch (schema_view.type) {
       case NANOARROW_TYPE_STRING:
       case NANOARROW_TYPE_LARGE_STRING:
@@ -608,6 +626,18 @@ inline ArrowErrorCode PostgresType::FromSchema(const 
PostgresTypeResolver& resol
     return EINVAL;
   }
 
+  if (extension_name == kUuidExtensionName) {
+    if (schema_view.type == NANOARROW_TYPE_FIXED_SIZE_BINARY &&
+        schema_view.fixed_size == 16) {
+      return resolver.Find(resolver.GetOID(PostgresTypeId::kUuid), out, error);
+    }
+    ArrowErrorSet(error,
+                  "Field '%s' is of type arrow.uuid but storage type is not "
+                  "FixedSizeBinary(16)",
+                  schema_view.schema->name);
+    return EINVAL;
+  }
+
   switch (schema_view.type) {
     case NANOARROW_TYPE_BOOL:
       return resolver.Find(resolver.GetOID(PostgresTypeId::kBool), out, error);
diff --git a/c/driver/postgresql/postgres_type_test.cc 
b/c/driver/postgresql/postgres_type_test.cc
index 599e63074..9ef74b969 100644
--- a/c/driver/postgresql/postgres_type_test.cc
+++ b/c/driver/postgresql/postgres_type_test.cc
@@ -198,6 +198,21 @@ TEST(PostgresTypeTest, PostgresTypeSetSchema) {
   EXPECT_STREQ(schema->format, "z");
   schema.reset();
 
+  ArrowSchemaInit(schema.get());
+  EXPECT_EQ(PostgresType(PostgresTypeId::kUuid).SetSchema(schema.get()), 
NANOARROW_OK);
+  EXPECT_STREQ(schema->format, "w:16");
+  typnameMetadataValue = ArrowCharView("<not found>");
+  ArrowMetadataGetValue(schema->metadata, 
ArrowCharView("ARROW:extension:name"),
+                        &typnameMetadataValue);
+  EXPECT_EQ(std::string(typnameMetadataValue.data, 
typnameMetadataValue.size_bytes),
+            "arrow.uuid");
+  typnameMetadataValue = ArrowCharView("<not found>");
+  ArrowMetadataGetValue(schema->metadata, 
ArrowCharView("ARROW:extension:metadata"),
+                        &typnameMetadataValue);
+  EXPECT_EQ(std::string(typnameMetadataValue.data, 
typnameMetadataValue.size_bytes),
+            "<not found>");
+  schema.reset();
+
   ArrowSchemaInit(schema.get());
   EXPECT_EQ(PostgresType(PostgresTypeId::kNumeric).SetSchema(schema.get()), 
NANOARROW_OK);
   EXPECT_STREQ(schema->format, "u");
@@ -375,6 +390,26 @@ TEST(PostgresTypeTest, PostgresTypeFromSchema) {
   EXPECT_EQ(PostgresType::FromSchema(resolver, schema.get(), &type, &error), 
ENOTSUP);
   EXPECT_STREQ(error.message, "Can't map Arrow type 'interval_months' to 
Postgres type");
   schema.reset();
+
+  // A canonical arrow.uuid field (FixedSizeBinary(16) storage) resolves to 
uuid
+  ArrowSchemaInit(schema.get());
+  ASSERT_EQ(
+      ArrowSchemaSetTypeFixedSize(schema.get(), 
NANOARROW_TYPE_FIXED_SIZE_BINARY, 16),
+      NANOARROW_OK);
+  {
+    nanoarrow::UniqueBuffer buffer;
+    ASSERT_EQ(ArrowMetadataBuilderInit(buffer.get(), nullptr), NANOARROW_OK);
+    ASSERT_EQ(
+        ArrowMetadataBuilderAppend(buffer.get(), 
ArrowCharView("ARROW:extension:name"),
+                                   ArrowCharView("arrow.uuid")),
+        NANOARROW_OK);
+    ASSERT_EQ(ArrowSchemaSetMetadata(schema.get(), 
reinterpret_cast<char*>(buffer->data)),
+              NANOARROW_OK);
+  }
+  EXPECT_EQ(PostgresType::FromSchema(resolver, schema.get(), &type, nullptr),
+            NANOARROW_OK);
+  EXPECT_EQ(type.type_id(), PostgresTypeId::kUuid);
+  schema.reset();
 }
 
 TEST(PostgresTypeTest, PostgresTypeResolver) {
diff --git a/c/driver/postgresql/validation/queries/ingest/uuid.txtcase 
b/c/driver/postgresql/validation/queries/ingest/uuid.txtcase
new file mode 100644
index 000000000..5dc1f4bff
--- /dev/null
+++ b/c/driver/postgresql/validation/queries/ingest/uuid.txtcase
@@ -0,0 +1,50 @@
+// 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.
+
+// part: metadata
+
+[tags]
+sql-type-name = "UUID"
+
+// part: input_schema
+
+{
+    "format": "+s",
+    "children": [
+        {
+            "name": "idx",
+            "format": "l",
+            "flags": ["nullable"]
+        },
+        {
+            "name": "value",
+            "format": "w:16",
+            "flags": ["nullable"],
+            "metadata": {
+                "ARROW:extension:name": "arrow.uuid"
+            }
+        }
+    ]
+}
+
+// part: input
+
+{"idx": 0, "value": "AAAAAAAAAAAAAAAAAAAAAA=="}
+{"idx": 1, "value": "/////////////////////w=="}
+{"idx": 2, "value": "Ej5FZ+ibEtOkVkJmFBdAAA=="}
+{"idx": 3, "value": "VQ6EAOKbQdSnFkRmVUQAAA=="}
+{"idx": 4, "value": null}
diff --git a/c/driver/postgresql/validation/queries/type/bind/uuid.txtcase 
b/c/driver/postgresql/validation/queries/type/bind/uuid.txtcase
new file mode 100644
index 000000000..a6f7259c8
--- /dev/null
+++ b/c/driver/postgresql/validation/queries/type/bind/uuid.txtcase
@@ -0,0 +1,86 @@
+// 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.
+
+// part: metadata
+
+[setup]
+drop = "test_uuid"
+
+[tags]
+sql-type-name = "UUID"
+
+// part: setup_query
+
+CREATE TABLE test_uuid (
+    res UUID
+);
+
+// part: bind_query
+
+INSERT INTO test_uuid VALUES ($1)
+
+// part: bind_schema
+
+{
+    "format": "+s",
+    "children": [
+        {
+            "name": "res",
+            "format": "w:16",
+            "flags": ["nullable"],
+            "metadata": {
+                "ARROW:extension:name": "arrow.uuid"
+            }
+        }
+    ]
+}
+
+// part: bind
+
+{"res": "AAAAAAAAAAAAAAAAAAAAAA=="}
+{"res": "Ej5FZ+ibEtOkVkJmFBdAAA=="}
+{"res": "VQ6EAOKbQdSnFkRmVUQAAA=="}
+{"res": "/////////////////////w=="}
+{"res": null}
+
+// part: query
+
+SELECT res FROM test_uuid ORDER BY res ASC NULLS LAST
+
+// part: expected_schema
+
+{
+    "format": "+s",
+    "children": [
+        {
+            "name": "res",
+            "format": "w:16",
+            "flags": ["nullable"],
+            "metadata": {
+                "ARROW:extension:name": "arrow.uuid"
+            }
+        }
+    ]
+}
+
+// part: expected
+
+{"res": "AAAAAAAAAAAAAAAAAAAAAA=="}
+{"res": "Ej5FZ+ibEtOkVkJmFBdAAA=="}
+{"res": "VQ6EAOKbQdSnFkRmVUQAAA=="}
+{"res": "/////////////////////w=="}
+{"res": null}
diff --git a/c/driver/postgresql/validation/queries/type/literal/uuid.txtcase 
b/c/driver/postgresql/validation/queries/type/literal/uuid.txtcase
new file mode 100644
index 000000000..b2dfe170f
--- /dev/null
+++ b/c/driver/postgresql/validation/queries/type/literal/uuid.txtcase
@@ -0,0 +1,45 @@
+// 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.
+
+// part: metadata
+
+[tags]
+sql-type-name = "UUID"
+
+// part: query
+
+SELECT '12345678-1234-5678-1234-567812345678'::uuid AS res
+
+// part: expected_schema
+
+{
+    "format": "+s",
+    "children": [
+        {
+            "name": "res",
+            "format": "w:16",
+            "flags": ["nullable"],
+            "metadata": {
+                "ARROW:extension:name": "arrow.uuid"
+            }
+        }
+    ]
+}
+
+// part: expected
+
+{"res": "EjRWeBI0VngSNFZ4EjRWeA=="}
diff --git a/c/driver/postgresql/validation/queries/type/select/uuid.txtcase 
b/c/driver/postgresql/validation/queries/type/select/uuid.txtcase
new file mode 100644
index 000000000..c268fbdfe
--- /dev/null
+++ b/c/driver/postgresql/validation/queries/type/select/uuid.txtcase
@@ -0,0 +1,65 @@
+// 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.
+
+// part: metadata
+
+[setup]
+drop = "test_uuid"
+
+[tags]
+sql-type-name = "UUID"
+
+// part: setup_query
+
+CREATE TABLE test_uuid (
+    idx INT,
+    res UUID
+);
+
+INSERT INTO test_uuid (idx, res) VALUES (1, NULL);
+INSERT INTO test_uuid (idx, res) VALUES (2, 
'00000000-0000-0000-0000-000000000000');
+INSERT INTO test_uuid (idx, res) VALUES (3, 
'ffffffff-ffff-ffff-ffff-ffffffffffff');
+INSERT INTO test_uuid (idx, res) VALUES (4, 
'123e4567-e89b-12d3-a456-426614174000');
+INSERT INTO test_uuid (idx, res) VALUES (5, 
'550e8400-e29b-41d4-a716-446655440000');
+
+// part: query
+
+SELECT res FROM test_uuid ORDER BY idx ASC
+
+// part: expected_schema
+
+{
+    "format": "+s",
+    "children": [
+        {
+            "name": "res",
+            "format": "w:16",
+            "flags": ["nullable"],
+            "metadata": {
+                "ARROW:extension:name": "arrow.uuid"
+            }
+        }
+    ]
+}
+
+// part: expected
+
+{"res": null}
+{"res": "AAAAAAAAAAAAAAAAAAAAAA=="}
+{"res": "/////////////////////w=="}
+{"res": "Ej5FZ+ibEtOkVkJmFBdAAA=="}
+{"res": "VQ6EAOKbQdSnFkRmVUQAAA=="}
diff --git a/c/driver/postgresql/validation/tests/postgresql.py 
b/c/driver/postgresql/validation/tests/postgresql.py
index 84148a0fa..1ebd21ef8 100644
--- a/c/driver/postgresql/validation/tests/postgresql.py
+++ b/c/driver/postgresql/validation/tests/postgresql.py
@@ -39,6 +39,7 @@ class PostgreSQLQuirks(model.DriverQuirks):
         statement_bulk_ingest_catalog=False,
         statement_bulk_ingest_schema=False,
         statement_bulk_ingest_temporary=False,
+        statement_bind=True,
         statement_execute_schema=True,
         statement_get_parameter_schema=True,
         statement_prepare=True,

Reply via email to