The "trigger cannot change partition destination" error occurs when a BEFORE INSERT trigger modifies the partition key column on a partitioned table. PostgreSQL determines partition assignment before trigger execution, so changing the partition key value creates a conflict. Redesign triggers to avoid modifying partition key columns, or use alternative approaches like staging tables.
This error occurs when you have a BEFORE INSERT trigger on a partitioned table that attempts to modify values in the partition key column(s). PostgreSQL determines which partition a row belongs to based on the partition key value before executing BEFORE INSERT triggers. If the trigger changes the partition key, the row could belong to a different partition, creating an irresolvable conflict. PostgreSQL prevents this situation by raising this error to maintain data integrity and consistency. The partition destination must remain fixed once initially determined.
First, determine which columns form the partition key:
SELECT
schemaname, tablename,
pg_get_partition_partstr(oid) as partition_by
FROM pg_tables t
JOIN pg_class c ON c.relname = t.tablename
JOIN pg_namespace n ON n.oid = c.relnamespace
AND n.nspname = t.schemaname
WHERE t.tablename = 'your_partitioned_table';The partition key columns are the ones PostgreSQL uses to route rows. Do NOT modify these in BEFORE INSERT triggers.
Review your BEFORE INSERT trigger function:
SELECT pg_get_triggerdef(oid)
FROM pg_trigger
WHERE tgname = 'your_trigger_name';Look for any statements that modify partition key columns. For example, if your table is partitioned by created_at, the trigger should NOT contain:
NEW.created_at := NOW(); -- WRONG: modifies partition keyRedesign your trigger to avoid changing partition key values. If you need to set default values, do so in the application layer BEFORE inserting:
-- BEFORE: Wrong (modifies partition key)
CREATE OR REPLACE FUNCTION trigger_set_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.created_at := NOW(); -- ERROR: partition key
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- AFTER: Correct (doesn't touch partition key)
CREATE OR REPLACE FUNCTION trigger_audit_log()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (action, record_id, changed_at)
VALUES ('INSERT', NEW.id, NOW());
RETURN NEW; -- Partition key unchanged
END;
$$ LANGUAGE plpgsql;If you need to dynamically set partition key values, calculate them in your application before inserting:
# Python example
import psycopg2
from datetime import datetime
# Calculate partition key BEFORE insert
created_at = datetime.now()
record_id = generate_id()
# Insert with pre-set partition key
cursor.execute(
"INSERT INTO partitioned_table (id, created_at, data) VALUES (%s, %s, %s)",
(record_id, created_at, "some data")
)If your logic requires modifying what would be partition keys, use a non-partitioned staging table:
-- Staging table (non-partitioned)
CREATE TABLE staging_data (
id BIGSERIAL PRIMARY KEY,
original_date DATE,
adjusted_date DATE,
data TEXT
);
-- Partitioned target table
CREATE TABLE partitioned_data (
id BIGSERIAL PRIMARY KEY,
created_at DATE,
data TEXT
) PARTITION BY RANGE (created_at);
-- Trigger on staging table can modify freely
CREATE OR REPLACE FUNCTION stage_to_partition()
RETURNS TRIGGER AS $$
BEGIN
-- Transform data (including date adjustments) in staging
NEW.adjusted_date := COALESCE(NEW.original_date, NOW()::DATE);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Separate process moves finalized data to partitioned table
INSERT INTO partitioned_data (created_at, data)
SELECT adjusted_date, data FROM staging_data
WHERE processed = false;Verify the trigger now works without modifying partition keys:
INSERT INTO your_partitioned_table (id, created_at, other_col)
VALUES (1, '2024-01-15', 'test data');If the error persists, double-check that no NEW.partition_key assignments remain in the trigger function. Use pg_get_triggerdef() to verify the exact trigger code being executed.
PostgreSQL 13+ introduced support for BEFORE ROW triggers on partitioned tables, but with strict limitations: they cannot change partition key columns. This is by design to preserve partition routing semantics. If you absolutely must transform partition key values, consider these alternatives: (1) Legacy trigger-based partitioning (pre-PostgreSQL 10) using child tables and BEFORE INSERT triggers—manually routing to child tables instead of setting parent table keys; (2) Using INSTEAD OF triggers on views over partitioned tables; (3) Implementing the transformation in application code before insert; (4) Using a message queue or ETL process to handle complex transformations outside the database. For UPDATE operations on partitioned tables, PostgreSQL supports row movement (DELETE from source partition + INSERT to destination partition), which is handled after BEFORE UPDATE triggers execute—this is a different code path than INSERT and allows more flexibility.
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
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
pg_dump: could not obtain lock on table
pg_dump could not obtain lock on table