Savvi Studio

Transaction Examples

These examples demonstrate transaction patterns with proper setup and teardown.

Referenced by: best-practices.md, patterns.md

Example 1: Simple Transaction

import { withClient } from '@/lib/db';
import { createResource } from '@db/graph';

const result = await withClient(
    {
        setup: async (client) => {
            await client.query('BEGIN');
        },
        teardown: async (client, error) => {
            if (error) {
                await client.query('ROLLBACK');
            } else {
                await client.query('COMMIT');
            }
        }
    },
    async (client) => {
        const id1 = await createResource(client, {
            p_type_namespace: 'test.type',
            p_external_id: 'item-1'
        });
        const id2 = await createResource(client, {
            p_type_namespace: 'test.type',
            p_external_id: 'item-2'
        });
        return { id1, id2 };
    }
);

Example 2: Transaction with Savepoints

import { withClient } from '@/lib/db';
import { createResource } from '@db/graph';
import { logAction } from '@db/audit';

const result = await withClient(async (client) => {
    await client.query('BEGIN');
    
    try {
        // Main operation
        const id = await createResource(client, {
            p_type_namespace: 'doc.document',
            p_external_id: 'doc-1'
        });
        
        // Savepoint for optional audit
        await client.query('SAVEPOINT sp1');
        try {
            await logAction(client, {
                p_action: 'created',
                p_resource_id: id
            });
        } catch (auditError) {
            await client.query('ROLLBACK TO SAVEPOINT sp1');
            console.warn('Audit failed, continuing');
        }
        
        await client.query('COMMIT');
        return id;
    } catch (error) {
        await client.query('ROLLBACK');
        throw error;
    }
});

Example 3: Retry on Serialization Failure

async function withRetry<T>(
    operation: () => Promise<T>,
    maxRetries: number = 3
): Promise<T> {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await operation();
        } catch (error) {
            if (error.code === '40001' && attempt < maxRetries) {
                // Serialization failure - retry with backoff
                await new Promise(resolve => 
                    setTimeout(resolve, 100 * attempt)
                );
                continue;
            }
            throw error;
        }
    }
    throw new Error('Max retries exceeded');
}

// Usage
const result = await withRetry(() =>
    withClient(async (client) => {
        await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
        const result = await createResource(client, data);
        await client.query('COMMIT');
        return result;
    })
);

Example 4: Transaction Test

import { test } from '@/test-utils-integration/config/database.context';
import { createResource } from '@db/graph';

test('transaction rollback', async ({ newDbClient }) => {
    await newDbClient.query('BEGIN');
    
    try {
        await createResource(newDbClient, {
            p_type_namespace: 'test.type',
            p_external_id: 'test-id'
        });
        
        // Force rollback
        throw new Error('Simulated error');
    } catch (error) {
        await newDbClient.query('ROLLBACK');
    }
    
    // Verify rollback worked
    const count = await newDbClient.query(
        'SELECT count(*) FROM graph.resource'
    );
    expect(parseInt(count.rows[0].count)).toBe(0);
});