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 f6721148c feat(c/driver/postgresql): use preinitialized type resolver
(#4775)
f6721148c is described below
commit f6721148c370ed002e937b89850938d829f5ef34
Author: David Li <[email protected]>
AuthorDate: Thu Sep 17 09:13:08 2026 +0900
feat(c/driver/postgresql): use preinitialized type resolver (#4775)
When a certain option is set, then we don't query the database for type
information at all and instead rely on a hardcoded table generated from
Postgres. Currently, we don't try to implement any sort of smart
fallback.
Closes #1755.
Assisted-by: GPT-6 Astra <[email protected]>
---
.github/workflows/integration.yml | 29 ++-
c/driver/postgresql/CMakeLists.txt | 1 +
c/driver/postgresql/codegen/pgtype.h | 185 +++++++++++++++++++
c/driver/postgresql/codegen/pgtype.py | 192 ++++++++++++++++++++
c/driver/postgresql/database.cc | 196 +++++----------------
c/driver/postgresql/database.h | 3 +-
c/driver/postgresql/meson.build | 3 +-
c/driver/postgresql/postgres_type.h | 4 +
c/driver/postgresql/postgres_type_test.cc | 40 +++++
c/driver/postgresql/postgresql_test.cc | 26 +++
c/driver/postgresql/type_resolver_init.cc | 190 ++++++++++++++++++++
.../literal/uuid.txtcase => type_resolver_init.h} | 46 +++--
.../ingest/decimal.txtcase} | 25 +--
.../ingest/decimal_scale_equals_precision.txtcase} | 23 +--
.../ingest/decimal_scale_negative.txtcase} | 23 +--
.../ingest/decimal_scale_zero.txtcase} | 33 ++--
.../type/select/jsonb.txtcase} | 18 +-
.../type/literal/uuid.txtcase | 0
.../type/select/uuid.txtcase | 0
c/driver/postgresql/validation/tests/postgresql.py | 37 ++--
docs/source/driver/postgresql.rst | 58 +++++-
r/adbcpostgresql/src/Makevars.in | 3 +-
r/adbcpostgresql/src/Makevars.ucrt | 3 +-
r/adbcpostgresql/src/Makevars.win | 3 +-
24 files changed, 869 insertions(+), 272 deletions(-)
diff --git a/.github/workflows/integration.yml
b/.github/workflows/integration.yml
index eac45b90b..d1c9db29f 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -295,6 +295,9 @@ jobs:
if [ "$postgres_version" = "18" ]; then
uv --project=./c/driver/postgresql/validation/ run \
ci/scripts/validate.sh "$(pwd)" --vendor postgresql
+ env POSTGRES_TYPE_RESOLVER_MODE=builtin \
+ uv --project=./c/driver/postgresql/validation/ run \
+ ci/scripts/validate.sh "$(pwd)" --vendor postgresql
fi
docker compose stop postgres-test
docker compose rm --force --stop --volumes postgres-test
@@ -349,13 +352,11 @@ jobs:
vendor: yugabytedb
uri:
postgresql://localhost:5439/yugabyte?user=yugabyte&password=yugabyte
experimental: false
- # Currently fails, but could be supported in the future.
- # https://github.com/apache/arrow-adbc/issues/1755
- # - name: CedarDB
- # service: cedardb-test
- # vendor: cedardb
- # uri:
postgresql://localhost:5433/postgres?user=postgres&password=CedarDB2026%21
- # experimental: true
+ - name: CedarDB
+ service: cedardb-test
+ vendor: cedardb
+ uri:
postgresql://localhost:5433/postgres?user=postgres&password=CedarDB2026%21
+ experimental: true
steps:
- name: Free up disk space
run: |
@@ -401,12 +402,26 @@ jobs:
run: |
docker compose up --wait --detach "${{ matrix.service }}"
- name: Run validation suite
+ # Currently fails, but could be supported in the future.
+ # https://github.com/apache/arrow-adbc/issues/1755
+ if: matrix.vendor != 'cedardb'
+ env:
+ ADBC_POSTGRESQL_TEST_URI: ${{ matrix.uri }}
+ ADBC_USE_ASAN: "ON"
+ ADBC_USE_UBSAN: "ON"
+ BUILD_ALL: "0"
+ BUILD_DRIVER_POSTGRESQL: "1"
+ run: |
+ uv --project=./c/driver/postgresql/validation/ run \
+ ci/scripts/validate.sh "$(pwd)" --vendor "${{ matrix.vendor }}"
+ - name: Run validation suite (with alternate type resolver)
env:
ADBC_POSTGRESQL_TEST_URI: ${{ matrix.uri }}
ADBC_USE_ASAN: "ON"
ADBC_USE_UBSAN: "ON"
BUILD_ALL: "0"
BUILD_DRIVER_POSTGRESQL: "1"
+ POSTGRES_TYPE_RESOLVER_MODE: "builtin"
run: |
uv --project=./c/driver/postgresql/validation/ run \
ci/scripts/validate.sh "$(pwd)" --vendor "${{ matrix.vendor }}"
diff --git a/c/driver/postgresql/CMakeLists.txt
b/c/driver/postgresql/CMakeLists.txt
index 980c3e80d..479fc906e 100644
--- a/c/driver/postgresql/CMakeLists.txt
+++ b/c/driver/postgresql/CMakeLists.txt
@@ -35,6 +35,7 @@ add_arrow_lib(adbc_driver_postgresql
result_helper.cc
result_reader.cc
statement.cc
+ type_resolver_init.cc
OUTPUTS
ADBC_LIBRARIES
CMAKE_PACKAGE_NAME
diff --git a/c/driver/postgresql/codegen/pgtype.h
b/c/driver/postgresql/codegen/pgtype.h
new file mode 100644
index 000000000..27c5fb9c4
--- /dev/null
+++ b/c/driver/postgresql/codegen/pgtype.h
@@ -0,0 +1,185 @@
+// 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.
+
+// !! DO NOT EDIT!!
+// Auto-generated by pgtype.py.
+
+#pragma once
+
+#include <array>
+
+#include "postgresql/postgres_type.h"
+
+namespace adbcpq {
+
+constexpr std::array<PostgresTypeResolver::Item, 153> kBuiltinTypeItems = {{
+ {16, "bool", "boolrecv", 0, 0, 0},
+ {1000, "_bool", "array_recv", 16, 0, 0},
+ {17, "bytea", "bytearecv", 0, 0, 0},
+ {1001, "_bytea", "array_recv", 17, 0, 0},
+ {18, "char", "charrecv", 0, 0, 0},
+ {1002, "_char", "array_recv", 18, 0, 0},
+ {19, "name", "namerecv", 0, 0, 0},
+ {1003, "_name", "array_recv", 19, 0, 0},
+ {20, "int8", "int8recv", 0, 0, 0},
+ {1016, "_int8", "array_recv", 20, 0, 0},
+ {21, "int2", "int2recv", 0, 0, 0},
+ {1005, "_int2", "array_recv", 21, 0, 0},
+ {22, "int2vector", "int2vectorrecv", 0, 0, 0},
+ {1006, "_int2vector", "array_recv", 22, 0, 0},
+ {23, "int4", "int4recv", 0, 0, 0},
+ {1007, "_int4", "array_recv", 23, 0, 0},
+ {24, "regproc", "regprocrecv", 0, 0, 0},
+ {1008, "_regproc", "array_recv", 24, 0, 0},
+ {25, "text", "textrecv", 0, 0, 0},
+ {1009, "_text", "array_recv", 25, 0, 0},
+ {26, "oid", "oidrecv", 0, 0, 0},
+ {1028, "_oid", "array_recv", 26, 0, 0},
+ {27, "tid", "tidrecv", 0, 0, 0},
+ {1010, "_tid", "array_recv", 27, 0, 0},
+ {28, "xid", "xidrecv", 0, 0, 0},
+ {1011, "_xid", "array_recv", 28, 0, 0},
+ {29, "cid", "cidrecv", 0, 0, 0},
+ {1012, "_cid", "array_recv", 29, 0, 0},
+ {30, "oidvector", "oidvectorrecv", 0, 0, 0},
+ {1013, "_oidvector", "array_recv", 30, 0, 0},
+ {114, "json", "json_recv", 0, 0, 0},
+ {199, "_json", "array_recv", 114, 0, 0},
+ {142, "xml", "xml_recv", 0, 0, 0},
+ {143, "_xml", "array_recv", 142, 0, 0},
+ {194, "pg_node_tree", "pg_node_tree_recv", 0, 0, 0},
+ {3361, "pg_ndistinct", "pg_ndistinct_recv", 0, 0, 0},
+ {3402, "pg_dependencies", "pg_dependencies_recv", 0, 0, 0},
+ {5017, "pg_mcv_list", "pg_mcv_list_recv", 0, 0, 0},
+ {32, "pg_ddl_command", "pg_ddl_command_recv", 0, 0, 0},
+ {5069, "xid8", "xid8recv", 0, 0, 0},
+ {271, "_xid8", "array_recv", 5069, 0, 0},
+ {600, "point", "point_recv", 0, 0, 0},
+ {1017, "_point", "array_recv", 600, 0, 0},
+ {601, "lseg", "lseg_recv", 0, 0, 0},
+ {1018, "_lseg", "array_recv", 601, 0, 0},
+ {602, "path", "path_recv", 0, 0, 0},
+ {1019, "_path", "array_recv", 602, 0, 0},
+ {603, "box", "box_recv", 0, 0, 0},
+ {1020, "_box", "array_recv", 603, 0, 0},
+ {604, "polygon", "poly_recv", 0, 0, 0},
+ {1027, "_polygon", "array_recv", 604, 0, 0},
+ {628, "line", "line_recv", 0, 0, 0},
+ {629, "_line", "array_recv", 628, 0, 0},
+ {700, "float4", "float4recv", 0, 0, 0},
+ {1021, "_float4", "array_recv", 700, 0, 0},
+ {701, "float8", "float8recv", 0, 0, 0},
+ {1022, "_float8", "array_recv", 701, 0, 0},
+ {705, "unknown", "unknownrecv", 0, 0, 0},
+ {718, "circle", "circle_recv", 0, 0, 0},
+ {719, "_circle", "array_recv", 718, 0, 0},
+ {790, "money", "cash_recv", 0, 0, 0},
+ {791, "_money", "array_recv", 790, 0, 0},
+ {829, "macaddr", "macaddr_recv", 0, 0, 0},
+ {1040, "_macaddr", "array_recv", 829, 0, 0},
+ {869, "inet", "inet_recv", 0, 0, 0},
+ {1041, "_inet", "array_recv", 869, 0, 0},
+ {650, "cidr", "cidr_recv", 0, 0, 0},
+ {651, "_cidr", "array_recv", 650, 0, 0},
+ {774, "macaddr8", "macaddr8_recv", 0, 0, 0},
+ {775, "_macaddr8", "array_recv", 774, 0, 0},
+ {1042, "bpchar", "bpcharrecv", 0, 0, 0},
+ {1014, "_bpchar", "array_recv", 1042, 0, 0},
+ {1043, "varchar", "varcharrecv", 0, 0, 0},
+ {1015, "_varchar", "array_recv", 1043, 0, 0},
+ {1082, "date", "date_recv", 0, 0, 0},
+ {1182, "_date", "array_recv", 1082, 0, 0},
+ {1083, "time", "time_recv", 0, 0, 0},
+ {1183, "_time", "array_recv", 1083, 0, 0},
+ {1114, "timestamp", "timestamp_recv", 0, 0, 0},
+ {1115, "_timestamp", "array_recv", 1114, 0, 0},
+ {1184, "timestamptz", "timestamptz_recv", 0, 0, 0},
+ {1185, "_timestamptz", "array_recv", 1184, 0, 0},
+ {1186, "interval", "interval_recv", 0, 0, 0},
+ {1187, "_interval", "array_recv", 1186, 0, 0},
+ {1266, "timetz", "timetz_recv", 0, 0, 0},
+ {1270, "_timetz", "array_recv", 1266, 0, 0},
+ {1560, "bit", "bit_recv", 0, 0, 0},
+ {1561, "_bit", "array_recv", 1560, 0, 0},
+ {1562, "varbit", "varbit_recv", 0, 0, 0},
+ {1563, "_varbit", "array_recv", 1562, 0, 0},
+ {1700, "numeric", "numeric_recv", 0, 0, 0},
+ {1231, "_numeric", "array_recv", 1700, 0, 0},
+ {1790, "refcursor", "textrecv", 0, 0, 0},
+ {2201, "_refcursor", "array_recv", 1790, 0, 0},
+ {2202, "regprocedure", "regprocedurerecv", 0, 0, 0},
+ {2207, "_regprocedure", "array_recv", 2202, 0, 0},
+ {2203, "regoper", "regoperrecv", 0, 0, 0},
+ {2208, "_regoper", "array_recv", 2203, 0, 0},
+ {2204, "regoperator", "regoperatorrecv", 0, 0, 0},
+ {2209, "_regoperator", "array_recv", 2204, 0, 0},
+ {2205, "regclass", "regclassrecv", 0, 0, 0},
+ {2210, "_regclass", "array_recv", 2205, 0, 0},
+ {4191, "regcollation", "regcollationrecv", 0, 0, 0},
+ {4192, "_regcollation", "array_recv", 4191, 0, 0},
+ {2206, "regtype", "regtyperecv", 0, 0, 0},
+ {2211, "_regtype", "array_recv", 2206, 0, 0},
+ {4096, "regrole", "regrolerecv", 0, 0, 0},
+ {4097, "_regrole", "array_recv", 4096, 0, 0},
+ {4089, "regnamespace", "regnamespacerecv", 0, 0, 0},
+ {4090, "_regnamespace", "array_recv", 4089, 0, 0},
+ {6490, "regdatabase", "regdatabaserecv", 0, 0, 0},
+ {6491, "_regdatabase", "array_recv", 6490, 0, 0},
+ {2950, "uuid", "uuid_recv", 0, 0, 0},
+ {2951, "_uuid", "array_recv", 2950, 0, 0},
+ {3220, "pg_lsn", "pg_lsn_recv", 0, 0, 0},
+ {3221, "_pg_lsn", "array_recv", 3220, 0, 0},
+ {3614, "tsvector", "tsvectorrecv", 0, 0, 0},
+ {3643, "_tsvector", "array_recv", 3614, 0, 0},
+ {3615, "tsquery", "tsqueryrecv", 0, 0, 0},
+ {3645, "_tsquery", "array_recv", 3615, 0, 0},
+ {3734, "regconfig", "regconfigrecv", 0, 0, 0},
+ {3735, "_regconfig", "array_recv", 3734, 0, 0},
+ {3769, "regdictionary", "regdictionaryrecv", 0, 0, 0},
+ {3770, "_regdictionary", "array_recv", 3769, 0, 0},
+ {3802, "jsonb", "jsonb_recv", 0, 0, 0},
+ {3807, "_jsonb", "array_recv", 3802, 0, 0},
+ {4072, "jsonpath", "jsonpath_recv", 0, 0, 0},
+ {4073, "_jsonpath", "array_recv", 4072, 0, 0},
+ {2970, "txid_snapshot", "txid_snapshot_recv", 0, 0, 0},
+ {2949, "_txid_snapshot", "array_recv", 2970, 0, 0},
+ {5038, "pg_snapshot", "pg_snapshot_recv", 0, 0, 0},
+ {5039, "_pg_snapshot", "array_recv", 5038, 0, 0},
+ {4451, "int4multirange", "multirange_recv", 0, 0, 0},
+ {6150, "_int4multirange", "array_recv", 4451, 0, 0},
+ {4532, "nummultirange", "multirange_recv", 0, 0, 0},
+ {6151, "_nummultirange", "array_recv", 4532, 0, 0},
+ {4533, "tsmultirange", "multirange_recv", 0, 0, 0},
+ {6152, "_tsmultirange", "array_recv", 4533, 0, 0},
+ {4534, "tstzmultirange", "multirange_recv", 0, 0, 0},
+ {6153, "_tstzmultirange", "array_recv", 4534, 0, 0},
+ {4535, "datemultirange", "multirange_recv", 0, 0, 0},
+ {6155, "_datemultirange", "array_recv", 4535, 0, 0},
+ {4536, "int8multirange", "multirange_recv", 0, 0, 0},
+ {6157, "_int8multirange", "array_recv", 4536, 0, 0},
+ {2275, "cstring", "cstring_recv", 0, 0, 0},
+ {1263, "_cstring", "array_recv", 2275, 0, 0},
+ {2277, "anyarray", "anyarray_recv", 0, 0, 0},
+ {2278, "void", "void_recv", 0, 0, 0},
+ {5078, "anycompatiblearray", "anycompatiblearray_recv", 0, 0, 0},
+ {4600, "pg_brin_bloom_summary", "brin_bloom_summary_recv", 0, 0, 0},
+ {4601, "pg_brin_minmax_multi_summary", "brin_minmax_multi_summary_recv",
0, 0, 0},
+ {6437, "oid8", "oid8recv", 0, 0, 0},
+ {6442, "_oid8", "array_recv", 6437, 0, 0},
+}};
+
+} // namespace adbcpq
diff --git a/c/driver/postgresql/codegen/pgtype.py
b/c/driver/postgresql/codegen/pgtype.py
new file mode 100644
index 000000000..b56e3347c
--- /dev/null
+++ b/c/driver/postgresql/codegen/pgtype.py
@@ -0,0 +1,192 @@
+# 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.
+
+"""
+Parse pg_type.dat (in the PostgreSQL source tree) and generate C++ code
+that pre-initializes a TypeResolver based on the given information.
+
+pg_type.dat is effectively a list of (OID, typrecv) rows (as far as we're
+concerned) in a custom format.
+"""
+
+import argparse
+import ast
+import dataclasses
+import types
+from pathlib import Path
+
+
[email protected](frozen=True)
+class PgTypeDef:
+ oid: int
+ typalign: str
+ typbyval: str
+ typcategory: str
+ typinput: str
+ typlen: str
+ typname: str
+ typoutput: str
+ typreceive: str
+ typsend: str
+ descr: str = ""
+ array_type_oid: int | None = None
+ typanalyze: str | None = None
+ typarray: str | None = None
+ typcollation: str | None = None
+ typdelim: str | None = None
+ typelem: str | None = None
+ typispreferred: str | None = None
+ typmodin: str | None = None
+ typmodout: str | None = None
+ typrelid: str | None = None
+ typstorage: str | None = None
+ typsubscript: str | None = None
+ typtype: str | None = None
+
+
+template = """// 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.
+
+// !! DO NOT EDIT!!
+// Auto-generated by pgtype.py.
+
+#pragma once
+
+#include <array>
+
+#include "postgresql/postgres_type.h"
+
+namespace adbcpq {{
+
+constexpr std::array<PostgresTypeResolver::Item, {item_count}>
kBuiltinTypeItems = {{{{
+{items}
+}}}};
+
+}} // namespace adbcpq
+""" # noqa:E501
+
+
+def parse(pgtypedat):
+ # Don't attempt to build a full parser; just remove comments, then extract
+ # dictionary-shaped blocks and munge the result so we can hand it to
+ # ast.literal_eval. May need adjusting if the format of pg_type.dat
+ # changes in future PostgreSQL versions.
+ lines = [(i + 1, line.strip()) for i, line in
enumerate(pgtypedat.splitlines())]
+ lines = [
+ (lineno, line) for (lineno, line) in lines if line and not
line.startswith("#")
+ ]
+ lines = [(lineno, line) for (lineno, line) in lines if line not in ("[",
"]")]
+
+ fields = {f.name: f for f in dataclasses.fields(PgTypeDef)}
+
+ while lines:
+ lineno, line = lines.pop(0)
+ if not line.startswith("{"):
+ raise ValueError(f"line {line:03}: expected '{{', got {line!r}")
+ typedeflines = [line]
+ while lines:
+ if not lines:
+ raise ValueError(f"line {lineno:03}: unexpected EOF")
+ lineno, line = lines.pop(0)
+ typedeflines.append(line)
+ if line.endswith("},"):
+ break
+
+ typedeftext = " ".join(typedeflines)
+ while True:
+ pos = typedeftext.find("=>")
+ if pos == -1:
+ break
+ while pos > 0 and typedeftext[pos - 1].isspace():
+ pos -= 1
+ end = pos
+ while pos > 0 and not typedeftext[pos - 1].isspace():
+ pos -= 1
+ start = pos
+ typedeftext = (
+ typedeftext[:start]
+ + "'"
+ + typedeftext[start:end]
+ + "'"
+ + typedeftext[end:]
+ )
+ typedeftext = typedeftext.replace("=>", ":", 1)
+ typedeftext = typedeftext.rstrip(",")
+ parts = ast.literal_eval(typedeftext)
+
+ # align the parts with the dataclass fields
+ args = {}
+ for k, v in parts.items():
+ if k not in fields:
+ raise ValueError(f"line {lineno:03}: unknown field {k!r}")
+ f = fields[k]
+ if f.type == int: # noqa:E721
+ v = int(v)
+ elif isinstance(f.type, types.UnionType) and int in
f.type.__args__:
+ v = int(v)
+ args[k] = v
+ yield PgTypeDef(**args)
+
+
+def generate(defs) -> str:
+ lines = []
+ for defn in defs:
+ if defn.typrelid or defn.typarray:
+ continue
+ if defn.typreceive in ("", "-", "array_recv", "range_recv"):
+ continue
+
+ lines.append(
+ f' {{{defn.oid}, "{defn.typname}", "{defn.typreceive}", 0, 0,
0}},'
+ )
+
+ if defn.array_type_oid:
+ lines.append(
+ f' {{{defn.array_type_oid}, "_{defn.typname}",
"array_recv", '
+ f"{defn.oid}, 0, 0}},"
+ )
+
+ return template.format(item_count=len(lines),
items="\n".join(lines)).strip()
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("pgtypedat", type=Path, help="Path to pg_type.dat")
+ args = parser.parse_args()
+
+ with args.pgtypedat.open() as f:
+ defs = list(parse(f.read()))
+
+ print(generate(defs))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/c/driver/postgresql/database.cc b/c/driver/postgresql/database.cc
index a329b36bc..a16447355 100644
--- a/c/driver/postgresql/database.cc
+++ b/c/driver/postgresql/database.cc
@@ -51,6 +51,18 @@ AdbcStatusCode PostgresDatabase::GetOption(const char*
option, char* value,
std::string output;
if (std::strcmp(option, ADBC_POSTGRESQL_OPTION_USE_COPY) == 0) {
output = use_copy_ ? ADBC_OPTION_VALUE_ENABLED :
ADBC_OPTION_VALUE_DISABLED;
+ } else if (std::strcmp(option, "adbc.postgresql.type_resolver_mode") == 0) {
+ switch (type_resolver_mode_) {
+ case TypeResolverMode::kBuiltin:
+ output = "builtin";
+ break;
+ case TypeResolverMode::kServer:
+ output = "server";
+ break;
+ default:
+ InternalAdbcSetError(error, "[libpq] unknown type resolver mode");
+ return ADBC_STATUS_INTERNAL;
+ }
} else {
InternalAdbcSetError(error, "[libpq] unknown database option '%s'",
option);
return ADBC_STATUS_NOT_FOUND;
@@ -85,7 +97,24 @@ AdbcStatusCode PostgresDatabase::Init(struct AdbcError*
error) {
return status.ToAdbc(error);
}
- status = RebuildTypeResolver(conn);
+ auto resolver = std::make_shared<PostgresTypeResolver>();
+ switch (type_resolver_mode_) {
+ case TypeResolverMode::kBuiltin: {
+ status = InitializeTypeResolver(*resolver);
+ break;
+ }
+ case TypeResolverMode::kServer: {
+ status = RebuildTypeResolver(conn, *resolver);
+ break;
+ }
+ }
+ if (!status.ok()) {
+ RAISE_ADBC(Disconnect(&conn, nullptr));
+ return status.ToAdbc(error);
+ }
+
+ type_resolver_ = std::move(resolver);
+
RAISE_ADBC(Disconnect(&conn, nullptr));
return status.ToAdbc(error);
}
@@ -103,15 +132,28 @@ AdbcStatusCode PostgresDatabase::SetOption(const char*
key, const char* value,
struct AdbcError* error) {
if (std::strcmp(key, "uri") == 0) {
uri_ = value;
- } else if (strcmp(key, ADBC_POSTGRESQL_OPTION_USE_COPY) == 0) {
- if (strcmp(value, ADBC_OPTION_VALUE_ENABLED) == 0) {
+ } else if (std::strcmp(key, ADBC_POSTGRESQL_OPTION_USE_COPY) == 0) {
+ if (std::strcmp(value, ADBC_OPTION_VALUE_ENABLED) == 0) {
use_copy_ = true;
- } else if (strcmp(value, ADBC_OPTION_VALUE_DISABLED) == 0) {
+ } else if (std::strcmp(value, ADBC_OPTION_VALUE_DISABLED) == 0) {
use_copy_ = false;
} else {
InternalAdbcSetError(error, "[libpq] Invalid value for option %s=%s",
key, value);
return ADBC_STATUS_INVALID_ARGUMENT;
}
+ } else if (std::strcmp(key, "adbc.postgresql.type_resolver_mode") == 0) {
+ if (std::strcmp(value, "auto") == 0) {
+ // TODO(https://github.com/apache/arrow-adbc/issues/4782): implement
fallback mode
+ InternalAdbcSetError(error, "[libpq] %s=%s not yet supported", key,
value);
+ return ADBC_STATUS_NOT_IMPLEMENTED;
+ } else if (std::strcmp(value, "builtin") == 0) {
+ type_resolver_mode_ = TypeResolverMode::kBuiltin;
+ } else if (std::strcmp(value, "server") == 0) {
+ type_resolver_mode_ = TypeResolverMode::kServer;
+ } else {
+ InternalAdbcSetError(error, "[libpq] Invalid value for option %s=%s",
key, value);
+ return ADBC_STATUS_INVALID_ARGUMENT;
+ }
} else {
InternalAdbcSetError(error, "%s%s", "[libpq] Unknown database option ",
key);
return ADBC_STATUS_NOT_IMPLEMENTED;
@@ -236,150 +278,4 @@ Status PostgresDatabase::InitVersions(PGconn* conn) {
return Status::Ok();
}
-static Status InsertPgAttributeResult(
- const PqResultHelper& result, const std::shared_ptr<PostgresTypeResolver>&
resolver);
-
-static Status InsertPgTypeResult(const PqResultHelper& result,
- const std::shared_ptr<PostgresTypeResolver>&
resolver);
-
-Status PostgresDatabase::RebuildTypeResolver(PGconn* conn) {
- // We need a few queries to build the resolver. The current strategy might
- // fail for some recursive definitions (e.g., arrays of records of arrays).
- // First, one on the pg_attribute table to resolve column names/oids for
- // record types.
- const std::string kColumnsQuery = R"(
-SELECT
- attrelid,
- attname,
- atttypid
-FROM
- pg_catalog.pg_attribute
-ORDER BY
- attrelid, attnum
-)";
-
- // Second, a query of the pg_type table. This query may need a few attempts
to handle
- // recursive definitions (e.g., record types with array column). This
currently won't
- // handle range types because those rows don't have child OID information.
Arrays types
- // are inserted after a successful insert of the element type.
- std::string type_query =
- "SELECT oid, typname, typreceive, typbasetype, typrelid, typarray FROM "
- "pg_catalog.pg_type WHERE (typreceive != 0 OR typsend != 0) AND typtype
!= 'r' AND "
- "typreceive::TEXT != 'array_recv'";
-
- // Create a new type resolver (this instance's type_resolver_ member
- // will be updated at the end if this succeeds).
- auto resolver = std::make_shared<PostgresTypeResolver>();
-
- // Insert record type definitions (this includes table schemas)
- PqResultHelper columns(conn, kColumnsQuery.c_str());
- UNWRAP_STATUS(columns.Execute());
- UNWRAP_STATUS(InsertPgAttributeResult(columns, resolver));
-
- // Attempt filling the resolver a few times to handle recursive definitions.
- int32_t max_attempts = 3;
- PqResultHelper types(conn, type_query);
- for (int32_t i = 0; i < max_attempts; i++) {
- UNWRAP_STATUS(types.Execute());
- UNWRAP_STATUS(InsertPgTypeResult(types, resolver));
- }
-
- type_resolver_ = std::move(resolver);
- return Status::Ok();
-}
-
-static Status InsertPgAttributeResult(
- const PqResultHelper& result, const std::shared_ptr<PostgresTypeResolver>&
resolver) {
- int num_rows = result.NumRows();
- std::vector<std::pair<std::string, uint32_t>> columns;
- int64_t current_type_oid = 0;
-
- if (result.NumColumns() != 3) {
- return Status::Internal(
- "Expected 3 columns from type resolver pg_attribute query but got ",
- result.NumColumns());
- }
-
- for (int row = 0; row < num_rows; row++) {
- PqResultRow item = result.Row(row);
- int64_t type_oid;
- UNWRAP_RESULT(type_oid, item[0].ParseInteger());
- std::string_view col_name = item[1].value();
- int64_t col_oid;
- UNWRAP_RESULT(col_oid, item[2].ParseInteger());
-
- if (type_oid != current_type_oid && !columns.empty()) {
- resolver->InsertClass(static_cast<uint32_t>(current_type_oid), columns);
- columns.clear();
- current_type_oid = type_oid;
- }
-
- columns.push_back({std::string(col_name), static_cast<uint32_t>(col_oid)});
- }
-
- if (!columns.empty()) {
- resolver->InsertClass(static_cast<uint32_t>(current_type_oid), columns);
- }
-
- return Status::Ok();
-}
-
-static Status InsertPgTypeResult(const PqResultHelper& result,
- const std::shared_ptr<PostgresTypeResolver>&
resolver) {
- if (result.NumColumns() != 5 && result.NumColumns() != 6) {
- return Status::Internal(
- "Expected 5 or 6 columns from type resolver pg_type query but got ",
- result.NumColumns());
- }
-
- int num_rows = result.NumRows();
- int num_cols = result.NumColumns();
- PostgresTypeResolver::Item type_item;
-
- for (int row = 0; row < num_rows; row++) {
- PqResultRow item = result.Row(row);
- int64_t oid;
- UNWRAP_RESULT(oid, item[0].ParseInteger());
- const char* typname = item[1].data;
- const char* typreceive = item[2].data;
- int64_t typbasetype;
- UNWRAP_RESULT(typbasetype, item[3].ParseInteger());
- int64_t typrelid;
- UNWRAP_RESULT(typrelid, item[4].ParseInteger());
-
- int64_t typarray;
- if (num_cols == 6) {
- UNWRAP_RESULT(typarray, item[5].ParseInteger());
- } else {
- typarray = 0;
- }
-
- // Special case the aclitem because it shows up in a bunch of internal
tables
- if (strcmp(typname, "aclitem") == 0) {
- typreceive = "aclitem_recv";
- }
-
- type_item.oid = static_cast<uint32_t>(oid);
- type_item.typname = typname;
- type_item.typreceive = typreceive;
- type_item.class_oid = static_cast<uint32_t>(typrelid);
- type_item.base_oid = static_cast<uint32_t>(typbasetype);
-
- int insert_result = resolver->Insert(type_item, nullptr);
-
- // If there's an array type and the insert succeeded, add that now too
- if (insert_result == NANOARROW_OK && typarray != 0) {
- std::string array_typname = "_" + std::string(typname);
- type_item.oid = static_cast<uint32_t>(typarray);
- type_item.typname = array_typname.c_str();
- type_item.typreceive = "array_recv";
- type_item.child_oid = static_cast<uint32_t>(oid);
-
- resolver->Insert(type_item, nullptr);
- }
- }
-
- return Status::Ok();
-}
-
} // namespace adbcpq
diff --git a/c/driver/postgresql/database.h b/c/driver/postgresql/database.h
index 1bb421cdb..a608ef2d3 100644
--- a/c/driver/postgresql/database.h
+++ b/c/driver/postgresql/database.h
@@ -27,6 +27,7 @@
#include "driver/framework/status.h"
#include "postgres_type.h"
+#include "type_resolver_init.h"
namespace adbcpq {
using adbc::driver::Status;
@@ -63,7 +64,6 @@ class PostgresDatabase {
}
Status InitVersions(PGconn* conn);
- Status RebuildTypeResolver(PGconn* conn);
std::string_view VendorName() { return "PostgreSQL"; }
const std::array<int, 3>& VendorVersion() { return postgres_server_version_;
}
bool use_copy() const { return use_copy_; }
@@ -74,5 +74,6 @@ class PostgresDatabase {
std::shared_ptr<PostgresTypeResolver> type_resolver_;
std::array<int, 3> postgres_server_version_{};
bool use_copy_ = true;
+ TypeResolverMode type_resolver_mode_ = TypeResolverMode::kServer;
};
} // namespace adbcpq
diff --git a/c/driver/postgresql/meson.build b/c/driver/postgresql/meson.build
index fcb87c01a..e0a56130b 100644
--- a/c/driver/postgresql/meson.build
+++ b/c/driver/postgresql/meson.build
@@ -27,8 +27,9 @@ adbc_postgres_driver_lib = library(
'result_helper.cc',
'result_reader.cc',
'statement.cc',
+ 'type_resolver_init.cc',
],
- include_directories: [include_dir, c_dir, safe_math_dir],
+ include_directories: [include_dir, c_dir, driver_dir, safe_math_dir],
link_with: [adbc_common_lib, adbc_framework_lib],
dependencies: [nanoarrow_dep, fmt_dep, libpq_dep],
)
diff --git a/c/driver/postgresql/postgres_type.h
b/c/driver/postgresql/postgres_type.h
index 178980508..68c4aef1d 100644
--- a/c/driver/postgresql/postgres_type.h
+++ b/c/driver/postgresql/postgres_type.h
@@ -580,6 +580,10 @@ class PostgresTypeResolver {
classes_.insert({oid, cls});
}
+ std::unordered_map<uint32_t, PostgresType> const& oid_mapping() const {
+ return mapping_;
+ }
+
private:
std::unordered_map<uint32_t, PostgresType> mapping_;
// We can't use PostgresTypeId as an unordered map key because there is no
diff --git a/c/driver/postgresql/postgres_type_test.cc
b/c/driver/postgresql/postgres_type_test.cc
index ac80f11df..146a75a8e 100644
--- a/c/driver/postgresql/postgres_type_test.cc
+++ b/c/driver/postgresql/postgres_type_test.cc
@@ -20,9 +20,12 @@
#include <vector>
#include <gtest/gtest.h>
+#include <libpq-fe.h>
#include <nanoarrow/nanoarrow.hpp>
+#include "database.h"
#include "postgres_type.h"
+#include "type_resolver_init.h"
namespace adbcpq {
@@ -553,4 +556,41 @@ TEST(PostgresTypeTest, PostgresTypeResolveInt2vector) {
EXPECT_EQ(0, type.n_children());
}
+TEST(PostgresTypeTest, BuiltinResolver) {
+ const char* uri = std::getenv("ADBC_POSTGRESQL_TEST_URI");
+ if (!uri) {
+ FAIL() << "Must provide env var ADBC_POSTGRESQL_TEST_URI";
+ }
+
+ auto* conn = PQconnectdb(uri);
+ if (auto status = PQstatus(conn); status != CONNECTION_OK) {
+ std::string message = PQerrorMessage(conn);
+ PQfinish(conn);
+ ASSERT_EQ(CONNECTION_OK, status) << message;
+ }
+
+ PostgresTypeResolver builtin, dynamic;
+ ASSERT_TRUE(InternalAdbcInitializeTypeResolver(builtin).ok());
+ ASSERT_TRUE(InternalAdbcRebuildTypeResolver(conn, dynamic).ok());
+
+ PQfinish(conn);
+
+ for (const auto& [oid, type] : builtin.oid_mapping()) {
+ // types from PostgreSQL 19 - ignore them
+ if (oid == 6437) continue; // oid8
+ if (oid == 6442) continue; // oid8 array
+ if (oid == 6490) continue; // regdatabase
+ if (oid == 6491) continue; // regdatabase array
+
+ SCOPED_TRACE("oid = " + std::to_string(oid));
+ auto dynamic_type = dynamic.oid_mapping().find(oid);
+ EXPECT_NE(dynamic_type, dynamic.oid_mapping().end());
+ if (dynamic_type == dynamic.oid_mapping().end()) {
+ continue;
+ }
+ EXPECT_EQ(type.type_id(), dynamic_type->second.type_id());
+ EXPECT_EQ(type.typname(), dynamic_type->second.typname());
+ }
+}
+
} // namespace adbcpq
diff --git a/c/driver/postgresql/postgresql_test.cc
b/c/driver/postgresql/postgresql_test.cc
index d202a430f..c157091b7 100644
--- a/c/driver/postgresql/postgresql_test.cc
+++ b/c/driver/postgresql/postgresql_test.cc
@@ -296,6 +296,32 @@ TEST_F(PostgresDatabaseTest, UseCopyOptionDefault) {
ASSERT_THAT(AdbcStatementRelease(&statement.value, &error),
IsOkStatus(&error));
}
+TEST_F(PostgresDatabaseTest, TypeResolverModeOption) {
+ const char* key = "adbc.postgresql.type_resolver_mode";
+ std::optional<std::string> type_resolver_mode;
+
+ adbc_validation::Handle<struct AdbcDatabase> database;
+ ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error));
+ ASSERT_THAT(quirks_.SetupDatabase(&database.value, &error),
IsOkStatus(&error));
+ ASSERT_THAT(AdbcDatabaseSetOption(&database.value, key, "builtin", &error),
+ IsOkStatus(&error));
+ ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error));
+ type_resolver_mode = adbc_validation::DatabaseGetOption(&database.value,
key, &error);
+ EXPECT_THAT(type_resolver_mode, ::testing::Optional("builtin"s));
+}
+
+TEST_F(PostgresDatabaseTest, TypeResolverModeOptionDefault) {
+ const char* key = "adbc.postgresql.type_resolver_mode";
+ std::optional<std::string> type_resolver_mode;
+
+ adbc_validation::Handle<struct AdbcDatabase> database;
+ ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error));
+ ASSERT_THAT(quirks_.SetupDatabase(&database.value, &error),
IsOkStatus(&error));
+ ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error));
+ type_resolver_mode = adbc_validation::DatabaseGetOption(&database.value,
key, &error);
+ EXPECT_THAT(type_resolver_mode, ::testing::Optional("server"s));
+}
+
int Canary(const struct AdbcError*) { return 0; }
TEST_F(PostgresDatabaseTest, AdbcDriverBackwardsCompatibility) {
diff --git a/c/driver/postgresql/type_resolver_init.cc
b/c/driver/postgresql/type_resolver_init.cc
new file mode 100644
index 000000000..1ea4b6260
--- /dev/null
+++ b/c/driver/postgresql/type_resolver_init.cc
@@ -0,0 +1,190 @@
+// 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.
+
+// For #warning Please include winsock2.h before windows.h on RTools/msys2
+#ifdef _WIN32
+#include <winsock2.h>
+#endif
+
+#include "type_resolver_init.h"
+
+#include <cstdint>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "codegen/pgtype.h"
+#include "postgresql/result_helper.h"
+
+using adbc::driver::Status;
+
+namespace adbcpq {
+
+Status InitializeTypeResolver(PostgresTypeResolver& resolver) {
+ ArrowError na_error = {};
+ for (const auto& item : kBuiltinTypeItems) {
+ UNWRAP_NANOARROW(na_error, Internal, resolver.Insert(item, &na_error));
+ }
+ return Status::Ok();
+}
+
+static Status InsertPgAttributeResult(const PqResultHelper& result,
+ PostgresTypeResolver& resolver) {
+ int num_rows = result.NumRows();
+ std::vector<std::pair<std::string, uint32_t>> columns;
+ int64_t current_type_oid = 0;
+
+ if (result.NumColumns() != 3) {
+ return Status::Internal(
+ "Expected 3 columns from type resolver pg_attribute query but got ",
+ result.NumColumns());
+ }
+
+ for (int row = 0; row < num_rows; row++) {
+ PqResultRow item = result.Row(row);
+ int64_t type_oid;
+ UNWRAP_RESULT(type_oid, item[0].ParseInteger());
+ std::string_view col_name = item[1].value();
+ int64_t col_oid;
+ UNWRAP_RESULT(col_oid, item[2].ParseInteger());
+
+ if (type_oid != current_type_oid && !columns.empty()) {
+ resolver.InsertClass(static_cast<uint32_t>(current_type_oid), columns);
+ columns.clear();
+ current_type_oid = type_oid;
+ }
+
+ columns.push_back({std::string(col_name), static_cast<uint32_t>(col_oid)});
+ }
+
+ if (!columns.empty()) {
+ resolver.InsertClass(static_cast<uint32_t>(current_type_oid), columns);
+ }
+
+ return Status::Ok();
+}
+
+static Status InsertPgTypeResult(const PqResultHelper& result,
+ PostgresTypeResolver& resolver) {
+ if (result.NumColumns() != 5 && result.NumColumns() != 6) {
+ return Status::Internal(
+ "Expected 5 or 6 columns from type resolver pg_type query but got ",
+ result.NumColumns());
+ }
+
+ int num_rows = result.NumRows();
+ int num_cols = result.NumColumns();
+ PostgresTypeResolver::Item type_item;
+
+ for (int row = 0; row < num_rows; row++) {
+ PqResultRow item = result.Row(row);
+ int64_t oid;
+ UNWRAP_RESULT(oid, item[0].ParseInteger());
+ const char* typname = item[1].data;
+ const char* typreceive = item[2].data;
+ int64_t typbasetype;
+ UNWRAP_RESULT(typbasetype, item[3].ParseInteger());
+ int64_t typrelid;
+ UNWRAP_RESULT(typrelid, item[4].ParseInteger());
+
+ int64_t typarray;
+ if (num_cols == 6) {
+ UNWRAP_RESULT(typarray, item[5].ParseInteger());
+ } else {
+ typarray = 0;
+ }
+
+ // Special case the aclitem because it shows up in a bunch of internal
tables
+ if (strcmp(typname, "aclitem") == 0) {
+ typreceive = "aclitem_recv";
+ }
+
+ type_item.oid = static_cast<uint32_t>(oid);
+ type_item.typname = typname;
+ type_item.typreceive = typreceive;
+ type_item.class_oid = static_cast<uint32_t>(typrelid);
+ type_item.base_oid = static_cast<uint32_t>(typbasetype);
+
+ // XXX: it seems to be intentional that we ignore errors here?
+ int insert_result = resolver.Insert(type_item, nullptr);
+
+ // If there's an array type and the insert succeeded, add that now too
+ if (insert_result == NANOARROW_OK && typarray != 0) {
+ std::string array_typname = "_" + std::string(typname);
+ type_item.oid = static_cast<uint32_t>(typarray);
+ type_item.typname = array_typname.c_str();
+ type_item.typreceive = "array_recv";
+ type_item.child_oid = static_cast<uint32_t>(oid);
+
+ resolver.Insert(type_item, nullptr);
+ }
+ }
+
+ return Status::Ok();
+}
+
+Status RebuildTypeResolver(PGconn* conn, PostgresTypeResolver& resolver) {
+ // We need a few queries to build the resolver. The current strategy might
+ // fail for some recursive definitions (e.g., arrays of records of arrays).
+ // First, one on the pg_attribute table to resolve column names/oids for
+ // record types.
+ const std::string kColumnsQuery = R"(
+SELECT
+ attrelid,
+ attname,
+ atttypid
+FROM
+ pg_catalog.pg_attribute
+ORDER BY
+ attrelid, attnum
+)";
+
+ // Second, a query of the pg_type table. This query may need a few attempts
to handle
+ // recursive definitions (e.g., record types with array column). This
currently won't
+ // handle range types because those rows don't have child OID information.
Arrays types
+ // are inserted after a successful insert of the element type.
+ std::string type_query =
+ "SELECT oid, typname, typreceive, typbasetype, typrelid, typarray FROM "
+ "pg_catalog.pg_type WHERE (typreceive != 0 OR typsend != 0) AND typtype
!= 'r' AND "
+ "typreceive::TEXT != 'array_recv'";
+
+ // Insert record type definitions (this includes table schemas)
+ PqResultHelper columns(conn, kColumnsQuery.c_str());
+ UNWRAP_STATUS(columns.Execute());
+ UNWRAP_STATUS(InsertPgAttributeResult(columns, resolver));
+
+ // Attempt filling the resolver a few times to handle recursive definitions.
+ int32_t max_attempts = 3;
+ PqResultHelper types(conn, type_query);
+ for (int32_t i = 0; i < max_attempts; i++) {
+ UNWRAP_STATUS(types.Execute());
+ UNWRAP_STATUS(InsertPgTypeResult(types, resolver));
+ }
+
+ return Status::Ok();
+}
+
+} // namespace adbcpq
+
+Status InternalAdbcRebuildTypeResolver(PGconn* conn,
+ adbcpq::PostgresTypeResolver& resolver)
{
+ return adbcpq::RebuildTypeResolver(conn, resolver);
+}
+
+Status InternalAdbcInitializeTypeResolver(adbcpq::PostgresTypeResolver&
resolver) {
+ return adbcpq::InitializeTypeResolver(resolver);
+}
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/type_resolver_init.h
similarity index 52%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to c/driver/postgresql/type_resolver_init.h
index ab6e94bc4..12cf916c7 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++ b/c/driver/postgresql/type_resolver_init.h
@@ -15,19 +15,33 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
-
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+#pragma once
+
+#include <libpq-fe.h>
+
+#include "driver/framework/status.h"
+#include "postgres_type.h"
+
+namespace adbcpq {
+
+enum class TypeResolverMode {
+ // Only use the built-in type OID definitions
+ kBuiltin,
+ // Query the database up front
+ kServer,
+};
+
+adbc::driver::Status RebuildTypeResolver(PGconn* conn, PostgresTypeResolver&
resolver);
+adbc::driver::Status InitializeTypeResolver(PostgresTypeResolver& resolver);
+
+} // namespace adbcpq
+
+// exposed for testing
+
+ADBC_EXPORT
+adbc::driver::Status InternalAdbcRebuildTypeResolver(
+ PGconn* conn, adbcpq::PostgresTypeResolver& resolver);
+
+ADBC_EXPORT
+adbc::driver::Status InternalAdbcInitializeTypeResolver(
+ adbcpq::PostgresTypeResolver& resolver);
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal.txtcase
similarity index 67%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to c/driver/postgresql/validation/queries-cedardb/ingest/decimal.txtcase
index ab6e94bc4..e5255e093 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++ b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal.txtcase
@@ -15,19 +15,12 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
-
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+// part: expected
+{"idx": 0, "value": null}
+{"idx": 1, "value": "0.000000"}
+{"idx": 2, "value": "123.450000"}
+{"idx": 3, "value": "-123.450000"}
+{"idx": 4, "value": "9999999.990000"}
+{"idx": 5, "value": "-9999999.990000"}
+{"idx": 6, "value": "99999999.990000"}
+{"idx": 7, "value": "-99999999.990000"}
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_equals_precision.txtcase
similarity index 67%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to
c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_equals_precision.txtcase
index ab6e94bc4..ec101a53d 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_equals_precision.txtcase
@@ -15,19 +15,10 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
-
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+// part: expected
+{"idx": 0, "value": null}
+{"idx": 1, "value": "0.000000"}
+{"idx": 2, "value": "0.990000"}
+{"idx": 3, "value": "-0.990000"}
+{"idx": 4, "value": "0.100000"}
+{"idx": 5, "value": "-0.100000"}
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_negative.txtcase
similarity index 67%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to
c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_negative.txtcase
index ab6e94bc4..1fa9b4a52 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_negative.txtcase
@@ -15,19 +15,10 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
-
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+// part: expected
+{"idx": 0, "value": null}
+{"idx": 1, "value": "0.000000"}
+{"idx": 2, "value": "9900.000000"}
+{"idx": 3, "value": "-9900.000000"}
+{"idx": 4, "value": "1000.000000"}
+{"idx": 5, "value": "-1000.000000"}
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_zero.txtcase
similarity index 57%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to
c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_zero.txtcase
index ab6e94bc4..2a7aef421 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++
b/c/driver/postgresql/validation/queries-cedardb/ingest/decimal_scale_zero.txtcase
@@ -15,19 +15,20 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
-
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+// part: expected
+{"idx": 0, "value": null}
+{"idx": 1, "value": "0.000000"}
+{"idx": 2, "value": "12345.000000"}
+{"idx": 3, "value": "-12345.000000"}
+{"idx": 4, "value": "999999999.000000"}
+{"idx": 5, "value": "-999999999.000000"}
+{"idx": 6, "value": "1.000000"}
+{"idx": 7, "value": "10.000000"}
+{"idx": 8, "value": "100.000000"}
+{"idx": 9, "value": "1000.000000"}
+{"idx": 10, "value": "10000.000000"}
+{"idx": 11, "value": "100000.000000"}
+{"idx": 12, "value": "1000000.000000"}
+{"idx": 13, "value": "10000000.000000"}
+{"idx": 14, "value": "100000000.000000"}
+{"idx": 15, "value": "1000000000.000000"}
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cedardb/type/select/jsonb.txtcase
similarity index 67%
copy from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
copy to c/driver/postgresql/validation/queries-cedardb/type/select/jsonb.txtcase
index ab6e94bc4..dfa6e5a3f 100644
--- a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
+++ b/c/driver/postgresql/validation/queries-cedardb/type/select/jsonb.txtcase
@@ -15,19 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-// part: expected_schema
+// part: metadata
-{
- "format": "+s",
- "children": [
- {
- "name": "res",
- "format": "z",
- "flags": ["nullable"],
- "metadata": {
- "ARROW:extension:name": "arrow.opaque",
- "ARROW:extension:metadata": "{\"type_name\": \"uuid\",
\"vendor_name\": \"PostgreSQL\"}"
- }
- }
- ]
-}
+# "binary format not implemented for type 'jsonb'"
+hide = true
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
b/c/driver/postgresql/validation/queries-cratedb-typeresolver/type/literal/uuid.txtcase
similarity index 100%
rename from
c/driver/postgresql/validation/queries-cratedb/type/literal/uuid.txtcase
rename to
c/driver/postgresql/validation/queries-cratedb-typeresolver/type/literal/uuid.txtcase
diff --git
a/c/driver/postgresql/validation/queries-cratedb/type/select/uuid.txtcase
b/c/driver/postgresql/validation/queries-cratedb-typeresolver/type/select/uuid.txtcase
similarity index 100%
rename from
c/driver/postgresql/validation/queries-cratedb/type/select/uuid.txtcase
rename to
c/driver/postgresql/validation/queries-cratedb-typeresolver/type/select/uuid.txtcase
diff --git a/c/driver/postgresql/validation/tests/postgresql.py
b/c/driver/postgresql/validation/tests/postgresql.py
index ef16c15eb..0dee33afc 100644
--- a/c/driver/postgresql/validation/tests/postgresql.py
+++ b/c/driver/postgresql/validation/tests/postgresql.py
@@ -16,6 +16,7 @@
# under the License.
import contextlib
+import os
import re
import typing
from pathlib import Path
@@ -24,6 +25,13 @@ from adbc_drivers_validation import model, quirks
import adbc_driver_manager.dbapi
+_database_args = {
+ "uri": model.FromEnv("ADBC_POSTGRESQL_TEST_URI"),
+ "adbc.postgresql.type_resolver_mode": os.environ.get(
+ "POSTGRES_TYPE_RESOLVER_MODE", "server"
+ ),
+}
+
class PostgreSQLQuirks(model.DriverQuirks):
name = "postgresql"
@@ -55,9 +63,7 @@ class PostgreSQLQuirks(model.DriverQuirks):
supported_xdbc_fields=["xdbc_type_name"],
)
setup = model.DriverSetup(
- database={
- "uri": model.FromEnv("ADBC_POSTGRESQL_TEST_URI"),
- },
+ database=_database_args,
connection={},
statement={},
)
@@ -101,13 +107,18 @@ class CedarDBQuirks(PostgreSQLQuirks):
vendor_version = re.compile(r"16[0-9]{4}")
short_version = "16"
setup = model.DriverSetup(
- database={
- "uri": model.FromEnv("ADBC_POSTGRESQL_TEST_URI"),
- },
+ database=_database_args,
connection={},
statement={"adbc.postgresql.use_copy": "false"},
)
+ @property
+ def queries_paths(self) -> tuple[Path]:
+ return (
+ *super().queries_paths,
+ Path(__file__).parent.parent / "queries-cedardb",
+ )
+
class CitusQuirks(PostgreSQLQuirks):
vendor_name = "PostgreSQL"
@@ -122,9 +133,7 @@ class CockroachDBQuirks(PostgreSQLQuirks):
update={"connection_get_table_schema": False}
)
setup = model.DriverSetup(
- database={
- "uri": model.FromEnv("ADBC_POSTGRESQL_TEST_URI"),
- },
+ database=_database_args,
connection={},
statement={"adbc.postgresql.use_copy": "false"},
)
@@ -149,18 +158,22 @@ class CrateDBQuirks(PostgreSQLQuirks):
statement_bulk_ingest=False,
)
setup = model.DriverSetup(
- database={
- "uri": model.FromEnv("ADBC_POSTGRESQL_TEST_URI"),
- },
+ database=_database_args,
connection={},
statement={"adbc.postgresql.use_copy": "false"},
)
@property
def queries_paths(self) -> tuple[Path]:
+ extra_paths: tuple[str] = ()
+ if os.environ.get("POSTGRES_TYPE_RESOLVER_MODE") != "builtin":
+ extra_paths = (
+ Path(__file__).parent.parent / "queries-cratedb-typeresolver",
+ )
return (
*super().queries_paths,
Path(__file__).parent.parent / "queries-cratedb",
+ *extra_paths,
)
@contextlib.contextmanager
diff --git a/docs/source/driver/postgresql.rst
b/docs/source/driver/postgresql.rst
index 257b81f23..002bfa223 100644
--- a/docs/source/driver/postgresql.rst
+++ b/docs/source/driver/postgresql.rst
@@ -316,11 +316,63 @@ returned a binary column from when it returned a binary
column as a fallback.
column, but this has been deprecated in favor of the Opaque type
and you should not rely on this key continuing to exist.
-Software Versions
-=================
+Resolving Composite and User-Defined Types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+PostgreSQL allows users to define their own types. On top of that, databases
+may use the PostgreSQL wire protocol and binary format but not necessarily the
+same data types. The driver has to somehow determine which Arrow type to use
+for a given PostgreSQL type.
+
+By default, the driver will query system tables upon initial connection to
+determine the type mapping, and to be able to understand user-defined and
+composite types. This can result in a significant startup cost, however,
+especially if the database contains many tables, columns, and/or user-defined
+types; some users have reported that initial connection takes several minutes!
+(Note that in PostgreSQL, every table automatically has a user-defined type
+defined for it.)
+
+Starting in driver version 1.13 (ADBC release 25), the driver now has an
+alternative: by setting the database option
+``adbc.postgresql.type_resolver_mode`` to ``builtin`` on initial connect, the
+driver will skip these queries and instead use a hardcoded table of *type
+OIDs*. In this mode, the driver will be unable to read composite and
+user-defined types, because it does not have the necessary information. We
+cannot guarantee compatibility with non-PostgreSQL database vendors that use
+the PostgreSQL wire protocol in this mode.
+
+We plan to improve support here: eventually, the driver will start with the
+hardcoded type table and will fetch type information on-the-fly as necessary.
+
+Software Versions & Vendor Compatibility
+========================================
For Python wheels, the shipped version of the PostgreSQL client libraries is
18.4. For conda-forge packages, the version of libpq is the same as the
version of libpq in your Conda environment.
-The PostgreSQL driver is tested against PostgreSQL versions 14 through 18.
+The PostgreSQL driver is tested against the following DBMSes, which all use
+the PostgreSQL wire protocol:
+
+- PostgreSQL (version 14 through 18)
+- Citus
+- CockroachDB
+- CrateDB
+- Google AlloyDB Omni
+- ParadeDB
+- TimescaleDB
+- YugabyteDB
+
+Note that for vendors besides PostgreSQL, certain features and/or data types
+may not be supported. In particular, COPY query execution (see above) is often
+not supported.
+
+We are aware that the driver is not currently compatible with the following
+vendors:
+
+- CedarDB v2026-8-13 (unless the "builtin" type resolver is used, see above)
+
+The driver is not and will not support the following vendors:
+
+- Amazon Redshift (note that a dedicated driver is available for Redshift from
+ a third party, see :ref:`driver-table`)
diff --git a/r/adbcpostgresql/src/Makevars.in b/r/adbcpostgresql/src/Makevars.in
index b35a4dc85..8d8b9882c 100644
--- a/r/adbcpostgresql/src/Makevars.in
+++ b/r/adbcpostgresql/src/Makevars.in
@@ -16,7 +16,7 @@
# under the License.
CXX_STD = CXX20
-PKG_CPPFLAGS=-I../src/c -I../src/c/include -I../src/c/vendor/
-I../src/c/vendor/portable-snippets/include/ -I../src/c/vendor/fmt/include
@cppflags@ -DADBC_EXPORT="" -DFMT_HEADER_ONLY=1
+PKG_CPPFLAGS=-I../src/c -I../src/c/include -I../src/c/driver/
-I../src/c/vendor/ -I../src/c/vendor/portable-snippets/include/
-I../src/c/vendor/fmt/include @cppflags@ -DADBC_EXPORT="" -DFMT_HEADER_ONLY=1
PKG_LIBS=@libs@
OBJECTS = init.o \
@@ -30,4 +30,5 @@ OBJECTS = init.o \
c/driver/postgresql/result_helper.o \
c/driver/postgresql/result_reader.o \
c/driver/postgresql/statement.o \
+ c/driver/postgresql/type_resolver_init.o \
c/vendor/nanoarrow/nanoarrow.o
diff --git a/r/adbcpostgresql/src/Makevars.ucrt
b/r/adbcpostgresql/src/Makevars.ucrt
index 77ec9c666..72c1f893c 100644
--- a/r/adbcpostgresql/src/Makevars.ucrt
+++ b/r/adbcpostgresql/src/Makevars.ucrt
@@ -16,7 +16,7 @@
# under the License.
CXX_STD = CXX20
-PKG_CPPFLAGS = -I../src/c -I../src/c/include -I../src/c/vendor/
-I../src/c/vendor/portable-snippets/include/ -I../src/c/vendor/fmt/include
-DADBC_EXPORT="" -D__USE_MINGW_ANSI_STDIO -DFMT_HEADER_ONLY=1
+PKG_CPPFLAGS = -I../src/c -I../src/c/include -I../src/c/driver/
-I../src/c/vendor/ -I../src/c/vendor/portable-snippets/include/
-I../src/c/vendor/fmt/include -DADBC_EXPORT="" -D__USE_MINGW_ANSI_STDIO
-DFMT_HEADER_ONLY=1
PKG_LIBS = -lpq -lpgcommon -lpgport -lssl -lcrypto -lz -lsecur32 -lws2_32
-lwldap32 -lcrypt32
@@ -31,4 +31,5 @@ OBJECTS = init.o \
c/driver/postgresql/result_helper.o \
c/driver/postgresql/result_reader.o \
c/driver/postgresql/statement.o \
+ c/driver/postgresql/type_resolver_init.o \
c/vendor/nanoarrow/nanoarrow.o
diff --git a/r/adbcpostgresql/src/Makevars.win
b/r/adbcpostgresql/src/Makevars.win
index aaba18ecf..275a4c888 100644
--- a/r/adbcpostgresql/src/Makevars.win
+++ b/r/adbcpostgresql/src/Makevars.win
@@ -18,7 +18,7 @@
VERSION = 13.2.0
RWINLIB = ../windows/libpq-$(VERSION)
CXX_STD = CXX20
-PKG_CPPFLAGS = -I$(RWINLIB)/include -I../src/c -I../src/c/include
-I../src/c/vendor/ -I../src/c/vendor/portable-snippets/include/
-I../src/c/vendor/fmt/include -DADBC_EXPORT="" -D__USE_MINGW_ANSI_STDIO
-DFMT_HEADER_ONLY=1
+PKG_CPPFLAGS = -I$(RWINLIB)/include -I../src/c -I../src/c/include
-I../src/c/driver/ -I../src/c/vendor/
-I../src/c/vendor/portable-snippets/include/ -I../src/c/vendor/fmt/include
-DADBC_EXPORT="" -D__USE_MINGW_ANSI_STDIO -DFMT_HEADER_ONLY=1
PKG_LIBS = -L$(RWINLIB)/lib${R_ARCH}${CRT} \
-lpq -lpgport -lpgcommon -lssl -lcrypto -lwsock32 -lsecur32 -lws2_32
-lgdi32 -lcrypt32 -lwldap32
@@ -33,6 +33,7 @@ OBJECTS = init.o \
c/driver/postgresql/result_helper.o \
c/driver/postgresql/result_reader.o \
c/driver/postgresql/statement.o \
+ c/driver/postgresql/type_resolver_init.o \
c/vendor/nanoarrow/nanoarrow.o
$(SHLIB):