-- Migration 005: Schema Separation for Dev/Testing/Production Environments -- This migration creates separate schemas to avoid data collision between environments. -- Each service should use the DB_SCHEMA environment variable to set search_path. -- ============================================ -- PART 1: Create Environment Schemas -- ============================================ -- Development schema (default) CREATE SCHEMA IF NOT EXISTS dev; -- Testing/QA schema CREATE SCHEMA IF NOT EXISTS testing; -- Production schema CREATE SCHEMA IF NOT EXISTS prod; -- ============================================ -- PART 2: Grant Permissions -- ============================================ -- Grant usage on all schemas to the application user -- Note: Adjust 'user' to match your DB_USER if different DO $$ BEGIN -- Grant permissions to the default user EXECUTE format('GRANT ALL ON SCHEMA dev TO %I', current_user); EXECUTE format('GRANT ALL ON SCHEMA testing TO %I', current_user); EXECUTE format('GRANT ALL ON SCHEMA prod TO %I', current_user); END $$; -- ============================================ -- PART 3: Create Tables in Each Schema -- ============================================ -- Note: The following creates the base tables in each schema. -- This ensures each environment has isolated data. -- Function to replicate tables from public schema to a target schema CREATE OR REPLACE FUNCTION clone_tables_to_schema(target_schema TEXT) RETURNS VOID AS $$ DECLARE tbl RECORD; sql_stmt TEXT; BEGIN -- Clone each table from public schema FOR tbl IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT LIKE 'pg_%' LOOP -- Create table if not exists (structure only) sql_stmt := format( 'CREATE TABLE IF NOT EXISTS %I.%I (LIKE public.%I INCLUDING ALL)', target_schema, tbl.tablename, tbl.tablename ); EXECUTE sql_stmt; END LOOP; -- Clone sequences FOR tbl IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' LOOP sql_stmt := format( 'CREATE SEQUENCE IF NOT EXISTS %I.%I', target_schema, tbl.sequence_name ); BEGIN EXECUTE sql_stmt; EXCEPTION WHEN duplicate_table THEN -- Sequence already exists, skip END; END LOOP; END; $$ LANGUAGE plpgsql; -- Clone tables to each schema SELECT clone_tables_to_schema('dev'); SELECT clone_tables_to_schema('testing'); SELECT clone_tables_to_schema('prod'); -- Copy existing data from public to dev schema (for development continuity) DO $$ DECLARE tbl RECORD; sql_stmt TEXT; BEGIN FOR tbl IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT LIKE 'pg_%' LOOP -- Copy data only if target table is empty sql_stmt := format( 'INSERT INTO dev.%I SELECT * FROM public.%I WHERE NOT EXISTS (SELECT 1 FROM dev.%I LIMIT 1)', tbl.tablename, tbl.tablename, tbl.tablename ); BEGIN EXECUTE sql_stmt; EXCEPTION WHEN OTHERS THEN -- Log and continue if copy fails RAISE NOTICE 'Could not copy data for table %: %', tbl.tablename, SQLERRM; END; END LOOP; END $$; -- ============================================ -- PART 4: Cleanup -- ============================================ -- Drop the helper function DROP FUNCTION IF EXISTS clone_tables_to_schema(TEXT); -- ============================================ -- USAGE NOTES: -- ============================================ -- To use a specific schema in your Go service, set search_path in the connection: -- -- connStr := fmt.Sprintf( -- "user=%s password=%s dbname=%s host=%s sslmode=%s search_path=%s", -- user, password, name, host, sslMode, schema -- ) -- -- Or execute after connection: -- db.Exec(fmt.Sprintf("SET search_path TO %s", schema)) -- -- Environment Variable: DB_SCHEMA (dev, testing, prod)