Here's the explanation about PostgreSQL functions and rollback behavior with primary key conflicts:
## Default Behavior Demonstration
```sql
-- Create test table
CREATE TABLE test_table (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- Create function: intentionally cause a primary key conflict on the second insert
CREATE OR REPLACE FUNCTION test_insert_with_conflict()
RETURNS VOID AS $$
BEGIN
-- First insert (should succeed)
INSERT INTO test_table (id, name) VALUES (1, 'First');
-- Second insert (attempts to insert existing id=1, will cause conflict)
INSERT INTO test_table (id, name) VALUES (1, 'Second');
-- Third insert (will not be reached)
INSERT INTO test_table (id, name) VALUES (2, 'Third');
END;
$$ LANGUAGE plpgsql;
-- Execute function (will error)
SELECT test_insert_with_conflict();
-- Query result: no data was inserted
SELECT * FROM test_table;
-- Result is empty, all inserts were rolled back
```
## Using Subtransactions (SAVEPOINT) for Partial Rollback
If you want to catch the conflict and allow other inserts to succeed, you can use subtransactions:
```sql
-- Create function: isolate conflicting insert using subtransaction
CREATE OR REPLACE FUNCTION test_insert_with_savepoint()
RETURNS VOID AS $$
BEGIN
-- First insert
INSERT INTO test_table (id, name) VALUES (1, 'First');
-- Use subtransaction to handle potential conflict
BEGIN
INSERT INTO test_table (id, name) VALUES (1, 'Second');
EXCEPTION
WHEN unique_violation THEN
-- Only roll back the subtransaction on conflict, main transaction unaffected
RAISE NOTICE 'Primary key conflict, skipping duplicate insert';
END;
-- Third insert (will succeed)
INSERT INTO test_table (id, name) VALUES (2, 'Third');
END;
$$ LANGUAGE plpgsql;
-- Truncate table and re-execute
TRUNCATE test_table;
SELECT test_insert_with_savepoint();
-- Query result
SELECT * FROM test_table;
```
Result:
```
id | name
---|------
1 | First
2 | Third
```
## Using ON CONFLICT for Graceful Handling
The recommended approach is to use `ON CONFLICT`:
```sql
CREATE OR REPLACE FUNCTION test_insert_on_conflict()
RETURNS VOID AS $$
BEGIN
INSERT INTO test_table (id, name) VALUES (1, 'First')
ON CONFLICT (id) DO NOTHING;
INSERT INTO test_table (id, name) VALUES (1, 'Second')
ON CONFLICT (id) DO NOTHING;
INSERT INTO test_table (id, name) VALUES (2, 'Third')
ON CONFLICT (id) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
TRUNCATE test_table;
SELECT test_insert_on_conflict();
SELECT * FROM test_table; -- Result: one row each for id=1 and id=2
```
## Summary
- **Default behavior**: Any error (including primary key conflicts) will roll back all operations in the entire function
- **Using subtransactions (BEGIN...EXCEPTION)**: Only the conflicting statement is rolled back, other statements commit normally
- **Using ON CONFLICT**: The most elegant approach, avoids throwing exceptions
- PostgreSQL functions execute within a **single transaction** by default, unless you explicitly use an `EXCEPTION` block to create savepoints
No comments yet