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,