Skip to main content

generator Package

The generator package converts detected changes into DDL statements and produces migration files compatible with golang-migrate.
import "github.com/accented-ai/pgtofu/internal/generator"

Generator

Main type for migration generation.
type Generator struct {
    options Options
}

func New(options Options) *Generator

Options

type Options struct {
    // OutputDir - directory for migration files
    OutputDir string

    // StartVersion - starting migration version number
    StartVersion int

    // PreviewMode - don't write files, just return content
    PreviewMode bool

    // TransactionMode - how to wrap statements
    TransactionMode TransactionMode

    // Idempotent - use IF EXISTS/IF NOT EXISTS
    Idempotent bool

    // GenerateDownMigrations - create down migrations
    GenerateDownMigrations bool

    // MaxOperationsPerFile - limit changes per file
    MaxOperationsPerFile int

    // IncludeComments - add explanatory comments
    IncludeComments bool
}

func DefaultOptions() Options

TransactionMode

type TransactionMode string

const (
    TransactionAuto   TransactionMode = "auto"   // Smart wrapping
    TransactionAlways TransactionMode = "always" // Always wrap
    TransactionNever  TransactionMode = "never"  // Never wrap
)

Generate

Generate migration files from diff result:
func (g *Generator) Generate(diff *differ.DiffResult) (*GenerateResult, error)

Example Usage

import "github.com/accented-ai/pgtofu/internal/generator"

func generateMigrations(diff *differ.DiffResult) error {
    opts := generator.Options{
        OutputDir:              "./migrations",
        StartVersion:           1,
        PreviewMode:            false,
        Idempotent:             true,
        GenerateDownMigrations: true,
    }
    gen := generator.New(opts)

    result, err := gen.Generate(diff)
    if err != nil {
        return fmt.Errorf("generating migrations: %w", err)
    }

    fmt.Printf("Generated %d migration pairs\n", len(result.Pairs))
    for _, pair := range result.Pairs {
        fmt.Printf("  %s\n", pair.UpFile.FileName)
    }

    return nil
}

GenerateResult

type GenerateResult struct {
    Pairs    []MigrationPair
    Warnings []string
}

MigrationPair

type MigrationPair struct {
    Version     int
    Description string
    UpFile      *MigrationFile
    DownFile    *MigrationFile
}

MigrationFile

type MigrationFile struct {
    Version     int
    Description string
    Direction   Direction // "up" or "down"
    FileName    string
    Content     string
}

type Direction string

const (
    DirectionUp   Direction = "up"
    DirectionDown Direction = "down"
)

DDL Builders

The generator uses a registry of builders for each change type:

Builder Registration

type DDLBuilder func(change differ.Change) DDLStatement

func RegisterBuilder(changeType differ.ChangeType, builder DDLBuilder)

DDLStatement

type DDLStatement struct {
    SQL         string // The DDL statement
    Description string // Human-readable description
    IsUnsafe    bool   // Requires manual review
    RequiresTx  bool   // Must run in transaction
    CannotUseTx bool   // Cannot run in transaction
}

Built-in Builders

Change TypeBuilder
ADD_TABLECreates full CREATE TABLE with columns, constraints
DROP_TABLEDROP TABLE IF EXISTS with warning
ADD_COLUMNALTER TABLE ADD COLUMN
DROP_COLUMNALTER TABLE DROP COLUMN with warning
MODIFY_COLUMN_TYPEALTER TABLE ALTER COLUMN TYPE
ADD_INDEXCREATE INDEX IF NOT EXISTS
DROP_INDEXDROP INDEX IF EXISTS
ADD_VIEWCREATE OR REPLACE VIEW
DROP_VIEWDROP VIEW IF EXISTS
ADD_FUNCTIONCREATE OR REPLACE FUNCTION
ADD_TRIGGERCREATE TRIGGER

Custom Builder Example

func init() {
    generator.RegisterBuilder(
        differ.ChangeTypeCustom,
        func(change differ.Change) generator.DDLStatement {
            return generator.DDLStatement{
                SQL:         "-- Custom DDL here",
                Description: change.Description,
                IsUnsafe:    false,
            }
        },
    )
}

File Naming

Migration files follow golang-migrate convention:
func FormatFileName(version int, description string, direction Direction) string
Pattern: {version:06d}_{description}.{direction}.sql Examples:
  • 000001_add_users_table.up.sql
  • 000001_add_users_table.down.sql

Version Detection

Auto-detect next version from existing migrations:
func DetectNextVersion(directory string) (int, error)
Scans directory for highest version number and returns next.

DDL Templates

Table Creation

func buildCreateTable(table *schema.Table) string
Output:
CREATE TABLE IF NOT EXISTS schema.table (
    column1 type1 constraints,
    column2 type2 constraints,
    CONSTRAINT pk PRIMARY KEY (...)
);

Index Creation

func buildCreateIndex(index *schema.Index) string
Output:
CREATE INDEX IF NOT EXISTS name
ON schema.table USING btree (columns)
INCLUDE (include_columns)
WHERE condition;

Function Creation

func buildCreateFunction(function *schema.Function) string
Output:
CREATE OR REPLACE FUNCTION schema.name(args)
RETURNS return_type
LANGUAGE plpgsql
AS $function$
body
$function$;

Identifier Quoting

All identifiers are properly quoted:
func QuoteIdentifier(name string) string
func QualifiedName(schema, name string) string
Examples:
  • users"users"
  • public, users"public"."users"
  • User Table"User Table"

Transaction Handling

Auto Mode

Wraps statements in transaction when safe:
BEGIN;
-- DDL statements
COMMIT;

Cannot Use Transaction

Some operations cannot run in transactions:
func cannotUseTransaction(change differ.Change) bool
Examples:
  • CREATE INDEX CONCURRENTLY
  • DROP INDEX CONCURRENTLY
  • CREATE DATABASE
These get separate migration files without transaction wrapping.

See Also