Future of PQfn(): Retiring PostgreSQL's Fast-Path Function Call Interface
Core Problem
PQfn() is PostgreSQL's "fast path" interface in libpq, which allows clients to invoke server-side functions directly via the FunctionCall protocol message (PqMsg_FunctionCall) without going through the SQL parser. This interface has been considered problematic for over two decades:
- Marked "somewhat obsolete" in 2003 (commit efc3a25bb0) — the interface was already seen as unnecessary given that SQL function calls provide the same capability with more safety.
- Marked "unsafe" in 2026 (commit bd48114937) — the interface was formally designated as unsafe, and a new internal-only
PQnfn()function was created to serve the one remaining internal consumer: the frontend Large Object (LO) interface.
The key architectural issue is that PQfn() bypasses the SQL layer entirely, communicating via a legacy binary protocol that lacks the safety guarantees of the extended query protocol. It's an exported symbol from libpq, meaning it's part of the ABI contract, but it has essentially zero external adoption — all found references are merely language binding wrappers that expose it mechanically without actually using it in application code.
Proposed Solutions
1. Retire PQfn() in v20
Nathan Bossart proposes keeping the symbol (ABI compatibility) but having it unconditionally return an error. This is the standard PostgreSQL approach for deprecating exported library functions without breaking the dynamic linker contract.
2. Frontend LO Interface Alternatives
The more interesting architectural question is what to do with the Large Object frontend code, which is the sole real consumer of the fast-path mechanism (now via internal PQnfn()):
- Option A: Keep PQnfn() as internal function — simplest approach, preserves current performance characteristics. This is the short-term winner.
- Option B: Use prepared statements with binary params/results — follows the project's own advice to users but has two serious problems:
- Name collisions: libpq-managed prepared statement names could collide with user-created statements.
- State fragility:
DISCARD ALLorDEALLOCATE ALLwould silently break libpq's internal prepared statements.
- Option C: Use PQexecParams() — simple but measured at ~41% slowdown for LO operations (tested with 10K LO create+unlink), making it unacceptable for performance-sensitive workloads.
3. Long-term Protocol Improvements
Two forward-looking ideas emerged:
- Protocol notification for deallocated statements: Teaching the server to inform clients when prepared statements are invalidated would solve the DISCARD/DEALLOCATE fragility problem not just for libpq but for all client libraries (JDBC has the same problem, as documented in pgjdbc).
- OneShotCachedPlan for unnamed statements: Jelte Fennema-Nio mentions a WIP patch (AI-generated, unvetted) that uses
CreateOneShotCachedPlanfor unnamed prepared statements to bring extended protocol performance closer to simple protocol. This could reduce or eliminate the 41% penalty of the PQexecParams() approach.
Key Technical Insights
The Prepared Statement Layering Problem
The most architecturally significant insight in this thread is Jacob Champion's observation that libpq's inability to safely use prepared statements internally is a general layering violation problem that affects all libpq clients. The server's prepared statement namespace is flat and shared between the application and any library code using the same connection. There's no mechanism for libraries to "own" private prepared statements without risk of collision or unexpected deallocation.
Nathan's proposed mitigation — using a documented prefix like libpq_internal_ — is pragmatic but imperfect. It handles accidental collisions but not the DISCARD/DEALLOCATE problem.
Performance Gap Between Protocol Paths
The 41% slowdown when switching from FunctionCall to simple query protocol for LO operations reveals a meaningful performance difference between the protocol paths. The FunctionCall message skips parsing, planning, and the full executor pipeline. For high-frequency, simple operations like LO manipulation, this overhead is substantial. Jelte's suggestion to test with CreateOneShotCachedPlan is well-targeted — it would eliminate the plan caching overhead for one-shot queries while still using the extended protocol.
Documentation Strategy
The thread reveals a minor disagreement on documentation approach: Nathan leans toward leaving a stub noting the removal, while Jacob suggests wiping all mentions. Given the Debian codesearch evidence showing only mechanical wrappers (no actual application usage), complete removal seems defensible but a brief deprecation note provides better archaeology for the inevitable future developer who encounters PQfn references in legacy binding code.
Architectural Context
The PQfn() retirement is part of a broader trend in PostgreSQL of consolidating on the extended query protocol as the single recommended client-server communication path. The FunctionCall message type is a relic from the pre-SQL-function era when direct function invocation was the only way to call server-side code. Its removal simplifies the protocol surface area and reduces attack surface (the "unsafe" designation likely relates to type safety and injection concerns in the binary parameter passing).