Skip to main content

Contributing to pgtofu

Thank you for your interest in contributing to pgtofu! This guide will help you get started.

Getting Started

Prerequisites

  • Go 1.25 or later
  • Docker (for running PostgreSQL locally)
  • Git

Setting Up Development Environment

# Clone the repository
git clone https://github.com/accented-ai/pgtofu.git
cd pgtofu

# Install dependencies
go mod download

# Run tests to verify setup
go test ./...

Running PostgreSQL Locally

# Start PostgreSQL with Docker
docker run -d \
  --name pgtofu-postgres \
  -e POSTGRES_USER=dev \
  -e POSTGRES_PASSWORD=dev \
  -e POSTGRES_DB=testdb \
  -p 5432:5432 \
  postgres:15

# For TimescaleDB testing
docker run -d \
  --name pgtofu-timescale \
  -e POSTGRES_USER=dev \
  -e POSTGRES_PASSWORD=dev \
  -e POSTGRES_DB=testdb \
  -p 5433:5432 \
  timescale/timescaledb:latest-pg15

Development Workflow

Making Changes

  1. Create a branch:
    git checkout -b feature/your-feature-name
    
  2. Make your changes
  3. Run tests:
    go test -v ./...
    
  4. Run linters:
    golangci-lint run
    
  5. Build:
    go build -o pgtofu ./cmd/pgtofu
    

Running Specific Tests

# Run tests for a specific package
go test -v ./internal/differ/...

# Run a specific test
go test -v ./internal/differ/... -run TestTableComparator

# Run with race detection
go test -race ./...

# Run with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Code Style

Go Style Guidelines

We follow standard Go conventions with some additions:
  • Use gofmt for formatting
  • Line length: max 100 characters (enforced by golines)
  • Add comments for exported functions and types
  • Use meaningful variable names

Linter Configuration

The project uses golangci-lint with 95+ linters. Key settings:
# .golangci.yaml highlights
linters-settings:
  golines:
    max-len: 100
  cyclop:
    max-complexity: 25
  gocognit:
    min-complexity: 40
Run the linter:
golangci-lint run

# Auto-fix some issues
golangci-lint run --fix

Project Structure

pgtofu/
├── cmd/pgtofu/           # CLI entry point
│   └── main.go
├── internal/
│   ├── cli/              # Command implementations
│   ├── schema/           # Core data models
│   ├── extractor/        # Database extraction
│   ├── parser/           # SQL parsing
│   ├── differ/           # Schema comparison
│   ├── generator/        # Migration generation
│   ├── graph/            # Dependency resolution
│   └── util/             # Utilities
├── pkg/database/         # Database connectivity
├── docs/                 # Documentation (Mintlify)
└── .github/workflows/    # CI/CD

Key Packages

PackagePurpose
internal/cliCobra-based CLI commands
internal/schemaJSON-serializable data models
internal/extractorPostgreSQL system catalog queries
internal/parserSQL statement parsing
internal/differSchema comparison logic
internal/generatorDDL generation
internal/graphTopological sorting

Adding Features

Adding a New Change Type

  1. Define the change type in internal/differ/types.go:
    const (
        // ...existing types...
        ChangeTypeYourNewChange ChangeType = "YOUR_NEW_CHANGE"
    )
    
  2. Implement detection in the relevant comparator (e.g., table_comparator.go):
    func (c *TableComparator) detectYourChange(current, desired *schema.Table) []Change {
        // Detection logic
    }
    
  3. Register the DDL builder in internal/generator/ddl_registry.go:
    func init() {
        RegisterBuilder(ChangeTypeYourNewChange, buildYourNewChange)
    }
    
  4. Implement the builder in appropriate ddl_*.go:
    func buildYourNewChange(change Change) DDLStatement {
        return DDLStatement{
            SQL:         "-- Your DDL here",
            Description: change.Description,
        }
    }
    
  5. Add tests in the tests/ subdirectory.

Adding a New CLI Command

  1. Create command file in internal/cli/:
    // internal/cli/yourcommand.go
    package cli
    
    import "github.com/spf13/cobra"
    
    func newYourCommand() *cobra.Command {
        cmd := &cobra.Command{
            Use:   "yourcommand",
            Short: "Short description",
            Long:  "Long description",
            RunE:  runYourCommand,
        }
        // Add flags
        return cmd
    }
    
    func runYourCommand(cmd *cobra.Command, args []string) error {
        // Implementation
        return nil
    }
    
  2. Register in cli.go:
    func NewRootCommand() *cobra.Command {
        // ...
        rootCmd.AddCommand(newYourCommand())
        // ...
    }
    

Adding SQL Parser Support

  1. Add statement detection in internal/parser/statement.go
  2. Create parser in internal/parser/your_object.go
  3. Register in parser.go statement handling
  4. Add tests in internal/parser/tests/

Testing

Test Organization

Tests are in tests/ subdirectories:
internal/differ/
├── differ.go
├── table_comparator.go
└── tests/
    ├── differ_table_test.go
    └── differ_helpers_test.go

Writing Tests

Use table-driven tests:
func TestYourFunction(t *testing.T) {
    t.Parallel()

    tests := []struct {
        name     string
        input    string
        expected string
        wantErr  bool
    }{
        {
            name:     "valid input",
            input:    "test",
            expected: "TEST",
            wantErr:  false,
        },
        {
            name:     "empty input",
            input:    "",
            expected: "",
            wantErr:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()

            result, err := YourFunction(tt.input)

            if tt.wantErr {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tt.expected, result)
        })
    }
}

Test Utilities

Use testify for assertions:
import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

// require - fails immediately
require.NoError(t, err)

// assert - continues execution
assert.Equal(t, expected, actual)

Pull Request Process

Before Submitting

  1. Tests pass:
    go test -race ./...
    
  2. Linters pass:
    golangci-lint run
    
  3. Documentation updated (if applicable)
  4. Commit messages are clear:
    Add support for EXCLUDE constraints
    
    - Add ChangeTypeAddExcludeConstraint
    - Implement detection in constraint_comparator
    - Add DDL builder for EXCLUDE
    - Add tests for exclusion constraint handling
    

PR Description

Include in your PR description:
  • What: Summary of changes
  • Why: Motivation/issue being fixed
  • How: Implementation approach
  • Testing: How you tested the changes

Review Process

  1. CI checks must pass
  2. At least one maintainer review
  3. Address review feedback
  4. Squash and merge

Release Process

Releases are automated via GitHub Actions:
  1. Tag with semantic version:
    git tag v1.2.3
    git push origin v1.2.3
    
  2. GitHub Actions builds and publishes:
    • Docker image to Docker Hub
    • GitHub release

Getting Help

Code of Conduct

Be respectful and inclusive. We follow the Contributor Covenant.