DEV Community

umzzil nng
umzzil nng

Posted on Originally published at oraerror.com

PostgreSQL 2F002 Error: Causes and Solutions Complete Guide

PostgreSQL Error 2F002: modifying sql data not permitted

PostgreSQL error 2F002 (modifying sql data not permitted) occurs when a function or procedure attempts to execute data-modifying statements (INSERT, UPDATE, DELETE) in a context that prohibits such operations. This typically happens when a function is declared with incorrect volatility settings, or when write operations are attempted inside a read-only transaction context. Understanding the root cause is essential because this error can surface in subtle ways during production deployments.


Top 3 Causes and Fixes

1. Incorrect Function Volatility Declaration

The most common cause. Declaring a function as IMMUTABLE or STABLE while including DML statements inside it will immediately trigger 2F002. PostgreSQL treats these declarations as a promise that the function will not modify data.

-- ❌ Problematic: STABLE function with DML inside
CREATE OR REPLACE FUNCTION record_event(event_name TEXT)
RETURNS VOID
LANGUAGE plpgsql
STABLE  -- Wrong! This function modifies data
AS $$
BEGIN
    INSERT INTO event_log (event_name, created_at)
    VALUES (event_name, NOW());
END;
$$;

-- ✅ Fix: Change volatility to VOLATILE
CREATE OR REPLACE FUNCTION record_event(event_name TEXT)
RETURNS VOID
LANGUAGE plpgsql
VOLATILE  -- Correct: functions that modify data must be VOLATILE
AS $$
BEGIN
    INSERT INTO event_log (event_name, created_at)
    VALUES (event_name, NOW());
END;
$$;

-- Alternatively, alter the existing function
ALTER FUNCTION record_event(TEXT) VOLATILE;
Enter fullscreen mode Exit fullscreen mode

2. Calling a Write Function Inside a Read-Only Transaction

Any function that performs DML will fail if invoked within a READ ONLY transaction. This also applies to connections made to PostgreSQL hot standby (read replica) servers.

-- ❌ Problematic: calling a write function in a read-only transaction
BEGIN READ ONLY;
    SELECT record_event('USER_LOGIN');  -- Triggers 2F002
COMMIT;

-- ✅ Fix: Use a READ WRITE transaction (default)
BEGIN;  -- READ WRITE is the default
    SELECT record_event('USER_LOGIN');  -- Works correctly
COMMIT;

-- Check current transaction mode
SHOW transaction_read_only;

-- Check session-level default
SHOW default_transaction_read_only;

-- Reset to read-write if needed
SET default_transaction_read_only = OFF;
Enter fullscreen mode Exit fullscreen mode

3. DML Inside Restricted Trigger Contexts

Certain trigger contexts, particularly CONSTRAINT TRIGGER, can restrict data modifications. Placing complex DML logic inside a BEFORE trigger that conflicts with PostgreSQL's execution context rules may produce this error.

-- ❌ Problematic pattern in a constraint trigger
CREATE CONSTRAINT TRIGGER trg_check_stock
AFTER INSERT ON order_items
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_stock();

-- ✅ Fix: Use a standard AFTER trigger with VOLATILE function
CREATE OR REPLACE FUNCTION sync_inventory()
RETURNS TRIGGER
LANGUAGE plpgsql
VOLATILE
AS $$
BEGIN
    UPDATE inventory
    SET stock = stock - NEW.quantity
    WHERE product_id = NEW.product_id;

    IF NOT FOUND THEN
        RAISE WARNING 'Product % not found in inventory', NEW.product_id;
    END IF;

    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_sync_inventory
AFTER INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION sync_inventory();
Enter fullscreen mode Exit fullscreen mode

Quick Prevention Tips

Audit your function volatility regularly using the system catalog to catch mismatches before they reach production:

-- Audit all non-system functions for volatility settings
SELECT
    n.nspname AS schema_name,
    p.proname AS function_name,
    CASE p.provolatile
        WHEN 'i' THEN 'IMMUTABLE'
        WHEN 's' THEN 'STABLE'
        WHEN 'v' THEN 'VOLATILE'
    END AS volatility
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
  AND p.prokind = 'f'
ORDER BY volatility, function_name;
Enter fullscreen mode Exit fullscreen mode

Always explicitly declare volatility when creating functions that contain DML, and include read-only transaction context tests in your CI/CD pipeline to catch 2F002 issues before deployment.


Related Errors

  • 2F000 – General SQL routine exception (parent class of 2F002)
  • 2F003 – Prohibited SQL statement attempted in a restricted routine
  • 2F004 – Reading SQL data not permitted (read-side equivalent)
  • 25006 – Read-only SQL transaction; often appears alongside 2F002 when connecting to a replica

📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.

Top comments (0)