Savvi Studio

Migration Examples

These examples demonstrate database migration patterns and SQL file organization.

Referenced by: migrations.md, schema-organization.md

Example 1: Add Table Migration

-- db/316_graph_my_feature.sql

-- Purpose: Add my_feature to graph
-- Dependencies: 303_graph_node.sql

BEGIN;

-- Add new table
CREATE TABLE graph.my_feature (
    id BIGSERIAL PRIMARY KEY,
    node_id BIGINT NOT NULL REFERENCES graph.node(id),
    feature_data JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Add index
CREATE INDEX idx_my_feature_node 
    ON graph.my_feature(node_id);

-- Add function
CREATE FUNCTION graph.get_my_feature(
    p_node_id BIGINT
) RETURNS TABLE (
    id BIGINT,
    feature_data JSONB
) AS $$
BEGIN
    RETURN QUERY
    SELECT id, feature_data
    FROM graph.my_feature
    WHERE node_id = p_node_id;
END;
$$ LANGUAGE plpgsql;

COMMIT;

Example 2: Add Column Migration

-- Add column with default
ALTER TABLE schema.existing_table
ADD COLUMN new_column TEXT DEFAULT 'default_value';

-- Update existing rows if needed
UPDATE schema.existing_table
SET new_column = 'computed_value'
WHERE new_column IS NULL;

-- Make NOT NULL after data populated
ALTER TABLE schema.existing_table
ALTER COLUMN new_column SET NOT NULL;

Example 3: Add Function Migration

CREATE OR REPLACE FUNCTION schema.new_function(
    p_param1 TEXT,
    p_param2 INT DEFAULT 10
) RETURNS TABLE (
    result_col1 TEXT,
    result_col2 INT
) AS $$
BEGIN
    RETURN QUERY
    SELECT col1, col2
    FROM schema.table
    WHERE col1 = p_param1
    LIMIT p_param2;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION schema.new_function IS
    'Function description and usage';

Example 4: Add Index Migration

-- Regular index
CREATE INDEX idx_table_column 
    ON schema.table(column);

-- Composite index
CREATE INDEX idx_table_multi 
    ON schema.table(col1, col2);

-- Partial index
CREATE INDEX idx_table_active 
    ON schema.table(column)
    WHERE status = 'active';

-- Check if exists first
CREATE INDEX IF NOT EXISTS idx_table_column
    ON schema.table(column);

Example 5: Data Migration with Verification

-- Wrap in transaction
BEGIN;

-- Transform data
UPDATE schema.table
SET new_format = 
    (old_format::jsonb || '{"version": 2}')::jsonb
WHERE old_format IS NOT NULL;

-- Verify
DO $$
DECLARE
    unmigrated_count INT;
BEGIN
    SELECT COUNT(*) INTO unmigrated_count
    FROM schema.table
    WHERE old_format IS NOT NULL 
        AND new_format IS NULL;
        
    IF unmigrated_count > 0 THEN
        RAISE EXCEPTION 'Migration incomplete: % rows', unmigrated_count;
    END IF;
END $$;

COMMIT;

Example 6: Rollback Migration

-- Create reverse migration file
-- db/316_rollback_my_feature.sql

BEGIN;

DROP FUNCTION IF EXISTS graph.get_my_feature(BIGINT);
DROP TABLE IF EXISTS graph.my_feature CASCADE;

COMMIT;

Example 7: Test Migration Success

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

test('migration adds table', async ({ newDbClient }) => {
    const result = await newDbClient.query(`
        SELECT EXISTS (
            SELECT FROM pg_tables
            WHERE schemaname = 'graph'
            AND tablename = 'my_feature'
        ) as exists
    `);
    
    expect(result.rows[0].exists).toBe(true);
});

Example 8: Test Data Migration

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

test('migration transforms data', async ({ newDbClient }) => {
    // Insert test data
    await newDbClient.query(`
        INSERT INTO schema.table (old_format)
        VALUES ('{"version": 1}')
    `);
    
    // Run migration
    await newDbClient.query(migrationSQL);
    
    // Verify transformation
    const result = await newDbClient.query(`
        SELECT new_format
        FROM schema.table
        WHERE old_format IS NOT NULL
    `);
    
    expect(result.rows[0].new_format.version).toBe(2);
});