PostgreSQL Error 25007: Schema and Data Statement Mixing Not Supported
PostgreSQL error code 25007 is raised when you attempt to mix DDL (Data Definition Language) statements and DML (Data Manipulation Language) statements in a context where PostgreSQL cannot guarantee transactional consistency for both types simultaneously. This most commonly surfaces inside PL/pgSQL blocks, pipeline query modes, or read-only transaction contexts. Understanding the root cause quickly saves you from hours of debugging in production environments.
Top 3 Causes
1. Executing DDL Inside a Read-Only Transaction
Setting a transaction to READ ONLY and then attempting any DDL statement will immediately trigger error 25007. PostgreSQL strictly enforces that no structural changes can occur within a read-only session.
-- This will cause ERROR 25007
BEGIN;
SET TRANSACTION READ ONLY;
CREATE TABLE accounts (id SERIAL PRIMARY KEY, balance NUMERIC);
COMMIT;
-- Correct approach: separate transactions
BEGIN;
CREATE TABLE accounts (id SERIAL PRIMARY KEY, balance NUMERIC);
COMMIT;
BEGIN;
SET TRANSACTION READ ONLY;
SELECT * FROM accounts;
COMMIT;
-- Check current transaction mode before executing DDL
SHOW transaction_read_only;
SELECT pg_is_in_recovery(); -- TRUE means you're on a replica — DDL not allowed
2. Mixing DDL and DML in Pipeline / Async Query Mode
When using libpq pipeline mode or certain ORM frameworks that batch queries asynchronously, mixing DDL and DML within a single pipeline batch causes this error. PostgreSQL cannot safely determine execution boundaries when schema-changing and data-changing statements are queued together.
-- Problematic pattern (conceptual — splitting DDL and DML batches)
-- WRONG: sending both in one pipeline batch
CREATE TABLE events (event_id SERIAL, event_name TEXT);
INSERT INTO events (event_name) VALUES ('launch'); -- 25007 in pipeline context
-- CORRECT: Flush and sync after DDL before sending DML
-- Step 1 — DDL batch, then COMMIT/sync
BEGIN;
CREATE TABLE events (event_id SERIAL, event_name TEXT);
CREATE INDEX idx_events_name ON events(event_name);
COMMIT;
-- Step 2 — DML batch only after DDL is fully committed
BEGIN;
INSERT INTO events (event_name) VALUES ('launch'), ('shutdown');
COMMIT;
3. DDL Followed by Immediate Static DML Reference in PL/pgSQL
Inside a PL/pgSQL DO block or function, PostgreSQL's planner pre-compiles the execution plan. If you create a table via DDL and immediately reference it with a static DML statement in the same block, the planner may not resolve the new object, triggering the error.
-- WRONG: static DML immediately after DDL in same block
DO $$
BEGIN
CREATE TABLE temp_log (id SERIAL, msg TEXT);
INSERT INTO temp_log (msg) VALUES ('hello'); -- can cause issues
END;
$$;
-- CORRECT: use EXECUTE for dynamic DML after DDL
DO $$
BEGIN
EXECUTE 'CREATE TABLE IF NOT EXISTS temp_log (id SERIAL, msg TEXT)';
EXECUTE 'INSERT INTO temp_log (msg) VALUES ($1)' USING 'hello';
RAISE NOTICE 'Done successfully';
END;
$$;
-- Practical partition automation example
DO $$
DECLARE
v_part TEXT := 'sales_2024_q1';
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF sales
FOR VALUES FROM (%L) TO (%L)',
v_part, '2024-01-01', '2024-04-01'
);
EXECUTE format(
'INSERT INTO %I (amount) VALUES ($1)', v_part
) USING 9999.99;
END;
$$;
Quick Fix Solutions
| Situation | Fix |
|---|---|
| Read-only transaction + DDL | Split into two separate transactions |
| Pipeline mode mixing | Flush pipeline after DDL; send DML in new batch |
| PL/pgSQL DDL + DML | Wrap DML in EXECUTE for dynamic resolution |
| Replica connection | Redirect DDL connections to primary only |
-- Verify session state before running DDL
SELECT
current_setting('transaction_read_only') AS read_only_mode,
pg_is_in_recovery() AS on_replica;
-- Reset session to writable defaults if needed
RESET ALL;
SET default_transaction_read_only = OFF;
Prevention Tips
1. Enforce strict DDL/DML separation in migration scripts.
Always split schema changes and data changes into separate migration files and separate transactions. Tools like Flyway or Liquibase make this straightforward — name files explicitly (V1__schema.sql for DDL, V2__data.sql for DML) to prevent accidental mixing.
2. Validate connection mode at application startup.
Before executing any DDL, query SHOW transaction_read_only and SELECT pg_is_in_recovery() to confirm you are connected to a writable primary. Add this as a health-check step in your database connection initialization routine, especially when using connection pools like PgBouncer.
Related Errors
-
25006 (
read_only_sql_transaction): Raised when DML write operations are attempted on a read-only transaction or replica — closely related to 25007. -
25001 (
active_sql_transaction): Triggered when transaction-level settings are changed after a transaction has already started. -
0A000 (
feature_not_supported): A broader error sometimes confused with 25007 when unsupported operations are attempted in restricted execution contexts.
📖 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)