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 63e7652868 feat(c/driver/postgresql): implement COPY writer for time
types (#4819)
63e7652868 is described below
commit 63e765286840aa5ff375de0a3bd68ffa2582af45
Author: David Li <[email protected]>
AuthorDate: Tue Sep 29 20:00:20 2026 -0700
feat(c/driver/postgresql): implement COPY writer for time types (#4819)
time64[us] was supported; add the other three possibilities.
Closes #3841.
Assisted-by: GPT-6 Astra
---
.../postgresql/copy/postgres_copy_writer_test.cc | 87 +++++++++++++++-------
c/driver/postgresql/copy/writer.h | 49 +++++++++++-
.../type/bind/time_ms.txtcase | 8 +-
.../type/bind/time_ns.txtcase | 8 +-
.../type/bind/time_s.txtcase | 8 +-
.../validation/queries/ingest/time_ms.txtcase | 30 +++++++-
.../validation/queries/ingest/time_ns.txtcase | 30 +++++++-
.../validation/queries/ingest/time_s.txtcase | 30 +++++++-
.../validation/queries/type/bind/time_ms.txtcase | 25 ++++++-
.../validation/queries/type/bind/time_ns.txtcase | 32 +++++++-
.../validation/queries/type/bind/time_s.txtcase | 25 ++++++-
11 files changed, 281 insertions(+), 51 deletions(-)
diff --git a/c/driver/postgresql/copy/postgres_copy_writer_test.cc
b/c/driver/postgresql/copy/postgres_copy_writer_test.cc
index f2bd5e23bf..e485d2b80c 100644
--- a/c/driver/postgresql/copy/postgres_copy_writer_test.cc
+++ b/c/driver/postgresql/copy/postgres_copy_writer_test.cc
@@ -473,33 +473,70 @@ TEST_F(PostgresCopyTest, PostgresCopyWriteDate) {
}
TEST_F(PostgresCopyTest, PostgresCopyWriteTime) {
- adbc_validation::Handle<struct ArrowSchema> schema;
- adbc_validation::Handle<struct ArrowArray> array;
- struct ArrowError na_error;
-
- const enum ArrowTimeUnit unit = NANOARROW_TIME_UNIT_MICRO;
- const auto values =
- std::vector<std::optional<int64_t>>{0, 86399000000, 49376123456,
std::nullopt};
-
- ArrowSchemaInit(&schema.value);
- ArrowSchemaSetTypeStruct(&schema.value, 1);
- ArrowSchemaSetTypeDateTime(schema->children[0], NANOARROW_TYPE_TIME64, unit,
nullptr);
- ArrowSchemaSetName(schema->children[0], "col");
- ASSERT_EQ(
- adbc_validation::MakeBatch<int64_t>(&schema.value, &array.value,
&na_error, values),
- ADBC_STATUS_OK);
+ // COPY (SELECT CAST(col AS TIME) FROM (VALUES ('00:00:00'), ('23:59:59'),
+ // (NULL)) AS drvd(col)) TO STDOUT WITH (FORMAT binary);
+ static const uint8_t expected[] = {
+ 0x50, 0x47, 0x43, 0x4f, 0x50, 0x59, 0x0a, 0xff, 0x0d, 0x0a, 0x00, 0x00,
0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00,
0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00,
0x00, 0x00,
+ 0x14, 0x1d, 0xc8, 0x1d, 0xc0, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff};
+ struct TimeTestParamType {
+ enum ArrowType type;
+ enum ArrowTimeUnit unit;
+ std::vector<std::optional<int64_t>> values;
+ const uint8_t* expected;
+ size_t expected_size;
+ };
+ const TimeTestParamType params[] = {
+ {NANOARROW_TYPE_TIME32,
+ NANOARROW_TIME_UNIT_SECOND,
+ {0, 86399, std::nullopt},
+ expected,
+ sizeof(expected)},
+ {NANOARROW_TYPE_TIME32,
+ NANOARROW_TIME_UNIT_MILLI,
+ {0, 86399000, std::nullopt},
+ expected,
+ sizeof(expected)},
+ {NANOARROW_TYPE_TIME64,
+ NANOARROW_TIME_UNIT_MICRO,
+ {0, 86399000000, 49376123456, std::nullopt},
+ kTestPgCopyTime,
+ sizeof(kTestPgCopyTime)},
+ {NANOARROW_TYPE_TIME64,
+ NANOARROW_TIME_UNIT_NANO,
+ {0, 86399000000000, 49376123456000, std::nullopt},
+ kTestPgCopyTime,
+ sizeof(kTestPgCopyTime)},
+ };
- PostgresCopyStreamWriteTester tester;
- ASSERT_EQ(tester.Init(&schema.value, &array.value, *type_resolver_),
NANOARROW_OK);
- ASSERT_EQ(tester.WriteAll(nullptr), ENODATA);
+ for (const auto& param : params) {
+ SCOPED_TRACE(param.unit);
+ adbc_validation::Handle<struct ArrowSchema> schema;
+ adbc_validation::Handle<struct ArrowArray> array;
+ struct ArrowError na_error;
- const struct ArrowBuffer buf = tester.WriteBuffer();
- // The last 2 bytes of a message can be transmitted via PQputCopyData
- // so no need to test those bytes from the Writer
- constexpr size_t buf_size = sizeof(kTestPgCopyTime) - 2;
- ASSERT_EQ(buf.size_bytes, buf_size);
- for (size_t i = 0; i < buf_size; i++) {
- ASSERT_EQ(buf.data[i], kTestPgCopyTime[i]);
+ ArrowSchemaInit(&schema.value);
+ ASSERT_EQ(ArrowSchemaSetTypeStruct(&schema.value, 1), NANOARROW_OK);
+ ASSERT_EQ(
+ ArrowSchemaSetTypeDateTime(schema->children[0], param.type,
param.unit, nullptr),
+ NANOARROW_OK);
+ ASSERT_EQ(ArrowSchemaSetName(schema->children[0], "col"), NANOARROW_OK);
+ ASSERT_EQ(adbc_validation::MakeBatch<int64_t>(&schema.value, &array.value,
&na_error,
+ param.values),
+ ADBC_STATUS_OK);
+
+ PostgresCopyStreamWriteTester tester;
+ ASSERT_EQ(tester.Init(&schema.value, &array.value, *type_resolver_),
NANOARROW_OK);
+ ASSERT_EQ(tester.WriteAll(nullptr), ENODATA);
+
+ const struct ArrowBuffer buf = tester.WriteBuffer();
+ // The end marker is sent separately by the caller.
+ const size_t buf_size = param.expected_size - 2;
+ ASSERT_EQ(buf.size_bytes, buf_size);
+ for (size_t i = 0; i < buf_size; i++) {
+ ASSERT_EQ(buf.data[i], param.expected[i]);
+ }
}
}
diff --git a/c/driver/postgresql/copy/writer.h
b/c/driver/postgresql/copy/writer.h
index 729f6060d9..4b38e710f7 100644
--- a/c/driver/postgresql/copy/writer.h
+++ b/c/driver/postgresql/copy/writer.h
@@ -925,6 +925,34 @@ class PostgresCopyListFieldWriter : public
PostgresCopyFieldWriter {
std::unique_ptr<PostgresCopyFieldWriter> child_;
};
+template <enum ArrowTimeUnit TU>
+class PostgresCopyTimeFieldWriter : public PostgresCopyFieldWriter {
+ public:
+ ArrowErrorCode Write(ArrowBuffer* buffer, int64_t index, ArrowError* error)
override {
+ constexpr int32_t field_size_bytes = sizeof(int64_t);
+ NANOARROW_RETURN_NOT_OK(WriteChecked<int32_t>(buffer, field_size_bytes,
error));
+
+ // We assume data is valid, so multiply can't overflow
+ int64_t value = ArrowArrayViewGetIntUnsafe(array_view_, index);
+ switch (TU) {
+ case NANOARROW_TIME_UNIT_SECOND:
+ value *= 1000000;
+ break;
+ case NANOARROW_TIME_UNIT_MILLI:
+ value *= 1000;
+ break;
+ case NANOARROW_TIME_UNIT_MICRO:
+ break;
+ case NANOARROW_TIME_UNIT_NANO:
+ // We truncate; at some point we should add an option to control
behavior
+ value /= 1000;
+ break;
+ }
+
+ return WriteChecked<int64_t>(buffer, value, error);
+ }
+};
+
template <enum ArrowTimeUnit TU>
class PostgresCopyTimestampFieldWriter : public PostgresCopyFieldWriter {
public:
@@ -1074,12 +1102,29 @@ static inline ArrowErrorCode MakeCopyFieldWriter(
*out = T::Create<T>(array_view);
return NANOARROW_OK;
}
+ case NANOARROW_TYPE_TIME32:
case NANOARROW_TYPE_TIME64: {
switch (schema_view.time_unit) {
- case NANOARROW_TIME_UNIT_MICRO:
- using T = PostgresCopyNetworkEndianFieldWriter<int64_t>;
+ case NANOARROW_TIME_UNIT_SECOND: {
+ using T = PostgresCopyTimeFieldWriter<NANOARROW_TIME_UNIT_SECOND>;
*out = T::Create<T>(array_view);
return NANOARROW_OK;
+ }
+ case NANOARROW_TIME_UNIT_MILLI: {
+ using T = PostgresCopyTimeFieldWriter<NANOARROW_TIME_UNIT_MILLI>;
+ *out = T::Create<T>(array_view);
+ return NANOARROW_OK;
+ }
+ case NANOARROW_TIME_UNIT_MICRO: {
+ using T = PostgresCopyTimeFieldWriter<NANOARROW_TIME_UNIT_MICRO>;
+ *out = T::Create<T>(array_view);
+ return NANOARROW_OK;
+ }
+ case NANOARROW_TIME_UNIT_NANO: {
+ using T = PostgresCopyTimeFieldWriter<NANOARROW_TIME_UNIT_NANO>;
+ *out = T::Create<T>(array_view);
+ return NANOARROW_OK;
+ }
default:
return ADBC_STATUS_NOT_IMPLEMENTED;
}
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_ms.txtcase
similarity index 91%
copy from c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
copy to
c/driver/postgresql/validation/queries-materialize/type/bind/time_ms.txtcase
index 30dfb7d9b3..dcb6e22da4 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
+++
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_ms.txtcase
@@ -15,7 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+// part: setup_query
-// part: metadata
-
-skip = "COPY Writer not implemented"
+CREATE TABLE test_time (
+ idx INT,
+ res TIME
+);
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_ns.txtcase
similarity index 91%
copy from c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
copy to
c/driver/postgresql/validation/queries-materialize/type/bind/time_ns.txtcase
index 30dfb7d9b3..dcb6e22da4 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
+++
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_ns.txtcase
@@ -15,7 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+// part: setup_query
-// part: metadata
-
-skip = "COPY Writer not implemented"
+CREATE TABLE test_time (
+ idx INT,
+ res TIME
+);
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_s.txtcase
similarity index 91%
copy from c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
copy to
c/driver/postgresql/validation/queries-materialize/type/bind/time_s.txtcase
index 30dfb7d9b3..dcb6e22da4 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
+++
b/c/driver/postgresql/validation/queries-materialize/type/bind/time_s.txtcase
@@ -15,7 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+// part: setup_query
-// part: metadata
-
-skip = "COPY Writer not implemented"
+CREATE TABLE test_time (
+ idx INT,
+ res TIME
+);
diff --git a/c/driver/postgresql/validation/queries/ingest/time_ms.txtcase
b/c/driver/postgresql/validation/queries/ingest/time_ms.txtcase
index 5ee360e824..f9d691bea0 100644
--- a/c/driver/postgresql/validation/queries/ingest/time_ms.txtcase
+++ b/c/driver/postgresql/validation/queries/ingest/time_ms.txtcase
@@ -15,10 +15,34 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
-
[tags]
field-type-name = "time"
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "idx",
+ "format": "l",
+ "flags": ["nullable"]
+ },
+ {
+ "name": "value",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"idx": 0, "value": 0}
+{"idx": 1, "value": 3661000000}
+{"idx": 2, "value": 43200000000}
+{"idx": 3, "value": 49531123000}
+{"idx": 4, "value": 86399999000}
+{"idx": 5, "value": null}
diff --git a/c/driver/postgresql/validation/queries/ingest/time_ns.txtcase
b/c/driver/postgresql/validation/queries/ingest/time_ns.txtcase
index 5ee360e824..8f5eb1d340 100644
--- a/c/driver/postgresql/validation/queries/ingest/time_ns.txtcase
+++ b/c/driver/postgresql/validation/queries/ingest/time_ns.txtcase
@@ -15,10 +15,34 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
-
[tags]
field-type-name = "time"
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "idx",
+ "format": "l",
+ "flags": ["nullable"]
+ },
+ {
+ "name": "value",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"idx": 0, "value": 0}
+{"idx": 1, "value": 3661000000}
+{"idx": 2, "value": 43200000000}
+{"idx": 3, "value": 49531123456}
+{"idx": 4, "value": 86399999999}
+{"idx": 5, "value": null}
diff --git a/c/driver/postgresql/validation/queries/ingest/time_s.txtcase
b/c/driver/postgresql/validation/queries/ingest/time_s.txtcase
index 5ee360e824..db24221186 100644
--- a/c/driver/postgresql/validation/queries/ingest/time_s.txtcase
+++ b/c/driver/postgresql/validation/queries/ingest/time_s.txtcase
@@ -15,10 +15,34 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
-
[tags]
field-type-name = "time"
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "idx",
+ "format": "l",
+ "flags": ["nullable"]
+ },
+ {
+ "name": "value",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"idx": 0, "value": 0}
+{"idx": 1, "value": 3661000000}
+{"idx": 2, "value": 43200000000}
+{"idx": 3, "value": 49531000000}
+{"idx": 4, "value": 86399000000}
+{"idx": 5, "value": null}
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
b/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
index 30dfb7d9b3..83fd316a8c 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
+++ b/c/driver/postgresql/validation/queries/type/bind/time_ms.txtcase
@@ -15,7 +15,28 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
+[tags]
+sql-type-name = "time"
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "res",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"res": 0}
+{"res": null}
+{"res": 49531123000}
+{"res": 45045500000}
+{"res": 86399999000}
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
b/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
index 30dfb7d9b3..62fad42471 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
+++ b/c/driver/postgresql/validation/queries/type/bind/time_ns.txtcase
@@ -15,7 +15,35 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
+[tags]
+sql-type-name = "time"
+
+// part: setup_query
+
+CREATE TABLE test_time (
+ idx INT,
+ res TIME(6)
+);
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "res",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"res": 0}
+{"res": null}
+{"res": 49531123456}
+{"res": 45045500000}
+{"res": 86399999999}
diff --git a/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
b/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
index 30dfb7d9b3..0bc1666e1b 100644
--- a/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
+++ b/c/driver/postgresql/validation/queries/type/bind/time_s.txtcase
@@ -15,7 +15,28 @@
// specific language governing permissions and limitations
// under the License.
-
// part: metadata
-skip = "COPY Writer not implemented"
+[tags]
+sql-type-name = "time"
+
+// part: expected_schema
+
+{
+ "format": "+s",
+ "children": [
+ {
+ "name": "res",
+ "format": "ttu",
+ "flags": ["nullable"]
+ }
+ ]
+}
+
+// part: expected
+
+{"res": 0}
+{"res": null}
+{"res": 49531000000}
+{"res": 45045000000}
+{"res": 86399000000}