The 39P02 error occurs when a set-returning function (SRF) or table function violates PostgreSQL's internal protocol requirements, typically when the function doesn't properly implement the expected return mode or fails to follow the correct sequence for returning multiple rows.
The PostgreSQL error code 39P02 (SRF_PROTOCOL_VIOLATED) is part of the "External Routine Exception" class (39xxx) and indicates that a set-returning function (SRF) has violated PostgreSQL's internal protocol for returning multiple rows. SRFs are functions that can return more than one row, such as RETURN NEXT, RETURN QUERY, or C-language table functions. This error most commonly occurs in two scenarios: First, when a C-language table function doesn't properly check whether the calling context supports the required return mode (like materialize mode) before attempting to return data. Second, when upgrading PostgreSQL versions (especially from v13 to v16+) where internal protocol changes may expose previously working but technically incorrect function implementations. The error can also manifest as "table-function protocol for materialize mode was not followed" when a table function attempts to use materialized returns without verifying that the caller supports this mode. This is a lower-level error than typical SQL mistakes—it usually indicates issues with function implementation or extension code rather than query syntax.
Review the error message to determine which function is causing the protocol violation:
-- Check the full error context
-- Example error:
-- ERROR: table-function protocol for materialize mode was not followed
-- CONTEXT: SQL function "list_postgres_log_files"
-- Identify if it's a system function or custom function
SELECT proname, prosrc, prorettype, proretset
FROM pg_proc
WHERE proname = 'your_function_name';If the function is part of an extension, note the extension name for the next steps.
If the error started after a PostgreSQL version upgrade, recreate the affected extensions:
-- For admin functions like list_postgres_log_files()
DROP EXTENSION IF EXISTS adminpack CASCADE;
CREATE EXTENSION adminpack;
-- For custom extensions
DROP EXTENSION IF EXISTS your_extension CASCADE;
CREATE EXTENSION your_extension;
-- If using foreign data wrappers
DROP EXTENSION IF EXISTS postgres_fdw CASCADE;
CREATE EXTENSION postgres_fdw;Important: Use CASCADE carefully—it will drop dependent objects. Review dependencies first:
SELECT dependent.relname
FROM pg_depend
JOIN pg_extension ON pg_depend.refobjid = pg_extension.oid
JOIN pg_class dependent ON pg_depend.objid = dependent.oid
WHERE pg_extension.extname = 'your_extension';For PL/pgSQL functions returning SETOF, ensure proper use of RETURN NEXT/QUERY and final RETURN:
Before (missing final RETURN):
CREATE OR REPLACE FUNCTION get_users()
RETURNS SETOF users AS $$
BEGIN
RETURN QUERY SELECT * FROM users WHERE active = true;
-- Missing final RETURN to signal completion
END;
$$ LANGUAGE plpgsql;After (correct protocol):
CREATE OR REPLACE FUNCTION get_users()
RETURNS SETOF users AS $$
BEGIN
RETURN QUERY SELECT * FROM users WHERE active = true;
RETURN; -- Signal function completion
END;
$$ LANGUAGE plpgsql;For functions with conditional returns:
CREATE OR REPLACE FUNCTION conditional_rows(limit_rows BOOLEAN)
RETURNS SETOF integer AS $$
BEGIN
IF limit_rows THEN
RETURN QUERY SELECT generate_series(1, 10);
ELSE
RETURN QUERY SELECT generate_series(1, 100);
END IF;
RETURN; -- Always end with RETURN
END;
$$ LANGUAGE plpgsql;If you maintain C-language extensions, ensure proper protocol checking:
Correct implementation pattern:
#include "postgres.h"
#include "funcapi.h"
Datum
my_table_function(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
// Check if caller supports materialize mode
if (!rsinfo || !(rsinfo->allowedModes & SFRM_Materialize))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("materialize mode required, but not supported in this context")));
if (SRF_IS_FIRSTCALL())
{
funcctx = SRF_FIRSTCALL_INIT();
// Initialize result tuple descriptor
// Build tuplestore for materialized results
}
funcctx = SRF_PERCALL_SETUP();
// Return rows...
SRF_RETURN_DONE(funcctx);
}This ensures the function only operates in supported modes.
Ensure functions are called in appropriate contexts:
Problematic contexts for SRFs (pre-PostgreSQL 10):
-- SRF in SELECT clause (deprecated in PostgreSQL 10+)
SELECT id, generate_series(1, 5) FROM users;
-- Better: Use LATERAL join
SELECT u.id, g.n
FROM users u
CROSS JOIN LATERAL generate_series(1, 5) AS g(n);Check function return type matches usage:
-- Verify function signature
SELECT
proname,
pg_get_function_result(oid) AS return_type,
proretset AS returns_set
FROM pg_proc
WHERE proname = 'your_function';
-- Ensure calling query expects set-returning resultsCreate a minimal test version to isolate the issue:
-- Simple test function
CREATE OR REPLACE FUNCTION test_srf()
RETURNS SETOF integer AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3);
RETURN;
END;
$$ LANGUAGE plpgsql;
-- Test in different contexts
SELECT * FROM test_srf(); -- Should work
SELECT test_srf(); -- Also should work
-- If test function works but original doesn't, compare implementationsFor table functions returning composite types:
CREATE TYPE test_result AS (id INT, name TEXT);
CREATE OR REPLACE FUNCTION test_table_func()
RETURNS SETOF test_result AS $$
BEGIN
RETURN QUERY SELECT 1, 'first'::TEXT;
RETURN QUERY SELECT 2, 'second'::TEXT;
RETURN;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM test_table_func();Understanding PostgreSQL SRF Protocol Changes:
PostgreSQL 10 introduced significant changes to how set-returning functions are evaluated. Prior to v10, SRFs in SELECT clauses had unpredictable behavior. From v10 onward, SRFs are evaluated before scalar expressions, essentially treating them as if they were in a LATERAL FROM clause.
Set-Returning Function Modes:
PostgreSQL supports three modes for returning sets:
1. ValuePerCall (SFRM_ValuePerCall): Function returns one row per call
2. Materialize (SFRM_Materialize): Function builds complete result set in memory/disk
3. Materialize_Random (SFRM_Materialize_Random): Materialized with random access
C-language functions must explicitly check which modes are supported by the calling context using:
if (rsinfo->allowedModes & SFRM_Materialize) {
// Materialize mode is allowed
}Debugging Protocol Violations:
Enable detailed logging to capture function execution:
SET client_min_messages = DEBUG1;
SET log_error_verbosity = VERBOSE;
-- Execute problematic function to see detailed error contextCheck if function volatility is correctly declared:
SELECT
proname,
CASE provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility,
proretset
FROM pg_proc
WHERE proname = 'your_function';Extension Version Compatibility:
After major PostgreSQL upgrades, always check extension versions:
SELECT * FROM pg_available_extensions WHERE name = 'your_extension';
-- Compare installed vs available version
SELECT
extname,
extversion AS installed,
(SELECT version FROM pg_available_extension_versions
WHERE name = extname AND installed = false
ORDER BY version DESC LIMIT 1) AS available
FROM pg_extension;
-- Upgrade if needed
ALTER EXTENSION your_extension UPDATE TO 'new_version';Creating Custom SRF Wrapper Functions:
If you can't modify the underlying C function, create a SQL wrapper:
CREATE OR REPLACE FUNCTION safe_log_files()
RETURNS TABLE(filetime TIMESTAMP, filename TEXT) AS $$
BEGIN
BEGIN
RETURN QUERY SELECT * FROM pg_ls_logdir();
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'Log directory listing failed: %', SQLERRM;
RETURN;
END;
END;
$$ LANGUAGE plpgsql;Alternative Approaches:
If the function continues to fail:
1. Replace with equivalent SQL-only implementation using CTEs and window functions
2. Use table-valued functions with explicit OUT parameters instead of SETOF
3. Implement as a view or materialized view if data source is queryable
4. For external data, consider foreign data wrapper alternatives
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL
ERROR 42501: must be owner of table
How to fix "must be owner of table" in PostgreSQL
trigger cannot change partition destination
How to fix "Trigger cannot change partition destination" in PostgreSQL
SSL error: certificate does not match host name
SSL error: certificate does not match host name in PostgreSQL
No SSL connection
No SSL connection to PostgreSQL