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):

Reply via email to