Development Guide
Everything needed to build, test, and contribute to DPG.
Prerequisites
| Tool | Minimum version | Required for | Notes |
|---|---|---|---|
| Go | 1.25.6 | Building | Must match go.mod; download from go.dev |
| GCC or Clang | any | Building | CGo required by pg_query_go (libpg_query C parser) |
| Git | 2.x | Everything | |
| Docker | 20.x | Integration tests | Daemon must be running; pulls postgres:16-alpine |
| Hugo extended | 0.147.0 | Docs site | Must be the extended variant for Sass/SCSS |
| Node.js | 20+ | Docs site | PostCSS dependency for Docsy theme |
| npm | 10+ | Docs site | Ships with Node.js |
| staticcheck | latest | make lint | go install honnef.co/go/tools/cmd/staticcheck@latest |
Why CGo?
pg_query_gowraps libpg_query — the real PostgreSQL C parser extracted into a standalone library. DPG uses it so that every.dpgfile is parsed by the exact same grammar as PostgreSQL itself. This rules out pure-Go cross-compilation; each target platform must be built natively on that architecture. The release CI uses platform-matched runners (ubuntu-24.04-armfor linux/arm64,macos-latestfor darwin/arm64).
Quick Setup
The setup script installs every mandatory tool and verifies the build. Safe to re-run.
git clone https://github.com/thec1oud/dpg
cd dpg
bash scripts/setup.sh
Script flags
bash scripts/setup.sh --check # check versions, install nothing
bash scripts/setup.sh --no-docs # skip Hugo + Node (schema tooling only)
Manual Setup
Linux — Ubuntu / Debian
# System packages
sudo apt-get update
sudo apt-get install -y git gcc build-essential curl
# Go (replace 1.25.6 with the version in go.mod if it changes)
curl -fsSL https://go.dev/dl/go1.25.6.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
echo 'export PATH="/usr/local/go/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
# Log out and back in, then: docker run hello-world
# Hugo extended
HUGO_VER=0.147.0
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VER}/hugo_extended_${HUGO_VER}_linux-amd64.tar.gz" \
| sudo tar -C /usr/local/bin -xz hugo
# Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# staticcheck
go install honnef.co/go/tools/cmd/staticcheck@latest
Linux — Fedora / RHEL
sudo dnf install -y git gcc gcc-c++ make curl
# Go
curl -fsSL https://go.dev/dl/go1.25.6.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
echo 'export PATH="/usr/local/go/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
# Docker
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
# Hugo extended (same as Ubuntu)
HUGO_VER=0.147.0
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VER}/hugo_extended_${HUGO_VER}_linux-amd64.tar.gz" \
| sudo tar -C /usr/local/bin -xz hugo
# Node.js 20
sudo dnf module install -y nodejs:20/common
go install honnef.co/go/tools/cmd/staticcheck@latest
Linux — Arch
sudo pacman -S --needed git gcc base-devel docker nodejs npm
# Go (AUR has latest, or download directly)
curl -fsSL https://go.dev/dl/go1.25.6.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
echo 'export PATH="/usr/local/go/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
HUGO_VER=0.147.0
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VER}/hugo_extended_${HUGO_VER}_linux-amd64.tar.gz" \
| sudo tar -C /usr/local/bin -xz hugo
go install honnef.co/go/tools/cmd/staticcheck@latest
macOS
# Homebrew (installs Xcode CLT which provides clang)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Core tools
brew install go git node@20
brew link --overwrite node@20
# Docker Desktop (provides the Docker daemon)
brew install --cask docker
# Open Docker.app from Applications to start the daemon.
# Hugo extended
HUGO_VER=0.147.0
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/arm64/arm64/')
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VER}/hugo_extended_${HUGO_VER}_darwin-${ARCH}.tar.gz" \
| sudo tar -C /usr/local/bin -xz hugo
go install honnef.co/go/tools/cmd/staticcheck@latest
Windows
Windows is a supported build target but the development environment is Linux/macOS. To develop on Windows:
- Install WSL2 with Ubuntu, then follow the Linux instructions inside WSL.
- Install Docker Desktop for Windows (WSL2 backend).
- Alternatively, build release binaries using the CI pipeline.
Repository Layout
dpg/
├── cmd/dpg/ # CLI entry point and all subcommands
├── internal/
│ ├── ast/ # Shared AST node definitions
│ ├── blockparser/ # Part-2 { } block parser
│ ├── compiler/ # Pipeline orchestration
│ ├── config/ # dpg.toml parsing
│ ├── diff/ # Desired IR vs snapshot → DiffOps
│ ├── emit/ # DiffOps → Migration SQL text
│ ├── executor/ # Execute migration against live PG (pgx)
│ ├── format/ # .dpg formatter
│ ├── graph/ # Topological sort + circular FK resolution
│ ├── introspect/ # Live catalog introspection
│ ├── ir/ # IR types and builder
│ ├── linter/ # Built-in lint rules
│ ├── merger/ # Multi-file merge (RFC §2.7)
│ ├── pgparser/ # pg_query_go wrapper
│ ├── pipeline/ # Interfaces, registry, shared types
│ ├── portability/ # PG-specific construct analysis
│ ├── project/ # Project discovery (dpg.toml walk)
│ ├── scanner/ # Source file tokenizer
│ ├── secrets/ # env:/link: secret resolution
│ ├── snapshot/ # Snapshot JSON store
│ ├── testpg/ # PostgreSQL container for integration tests
│ ├── ui/ # Terminal output helpers
│ └── version/ # Embedded version metadata
├── pkg/dpg/ # Stable public API (plugin authors import this)
├── tools/gendocs/ # Standalone CLI doc generator (cobra/doc)
├── internal/docssite/ # Embedded Hugo site server
├── site/ # Hugo/Docsy source for the documentation site
│ ├── config/_default/ # Hugo configuration
│ ├── content/ # Markdown pages
│ │ └── docs/ # Getting started, reference, CLI, extending
│ ├── layouts/ # Custom Hugo templates
│ └── static/ # Static assets (CSS, etc.)
├── rfc/ # Language specification (mounted into site/content/rfc)
├── examples/ # Runnable examples and plugin tests
├── scripts/ # Development tooling
├── Makefile # All build/test/docs targets
├── go.mod / go.sum # Module definition
└── CLAUDE.md # AI assistant instructions
Build Targets
make build # Fast dev build → build/dpg (docs NOT embedded)
make build-full # Full build → build/dpg (docs embedded; runs Hugo first)
make install # go install to $GOPATH/bin (docs not embedded)
make install-full # go install with embedded docs
Distribution
make dist # All platforms with embedded docs → dist/
make dist-linux # linux/amd64 (arm64 skipped unless running on ARM64 host)
make dist-darwin # darwin/amd64 + darwin/arm64
make dist-windows # windows/amd64
make release # make dist + tar/zip archives
All dist targets run make docs-site first, then compile with -tags embeddata.
linux/arm64 requires a native ARM64 host due to the CGo dependency. Locally it is skipped on non-ARM64 machines; the release CI builds it on ubuntu-24.04-arm.
About make build vs make build-full
make build is designed for daily development. It skips Hugo entirely so the iteration cycle is edit → go build → test. The resulting binary returns an error on dpg docs.
make build-full is the release path. It runs the full Hugo pipeline (docs-cli → npm install → hugo --minify) and then compiles with -tags embeddata, embedding internal/docssite/public/ into the binary. This requires Hugo extended, Node.js, and npm.
Testing
Unit tests
make test # go test ./...
make test-verbose # go test ./... -v
make vet # go vet ./...
make lint # staticcheck ./...
Unit tests have no external dependencies. They run on every push via CI.
Integration tests
make test-integration
# equivalent: go test -tags integration -count=1 -timeout 5m ./...
Integration tests require Docker. They spin up a postgres:16-alpine container via testcontainers-go and exercise the full compile → plan → apply → introspect → zero-drift roundtrip.
The container is started once per test binary via testpg.Start(t) in internal/testpg/testpg.go. t.Cleanup stops and removes it automatically.
# Verify Docker is running before attempting integration tests:
docker info
docker pull postgres:16-alpine # optional: pre-pull to speed up first run
Plugin / example tests
make test-examples
# equivalent: go test ./examples/... -v
The examples/plugin/ package demonstrates how to register a custom linter against pkg/dpg. These tests have no external dependencies and run entirely in-process.
Documentation
Serving locally
make docs-serve
# Opens http://localhost:1313 with live reload.
# Requires: Hugo extended 0.147.0, Node.js 20, npm.
The CLI reference pages (site/content/docs/cli/*.md) are generated automatically by make docs-serve before Hugo starts.
Building a static copy
make docs-site
# Output: internal/docssite/public/
# This is also what make build-full embeds into the binary.
Keeping CLI docs in sync
The generated CLI docs in site/content/docs/cli/ are produced by tools/gendocs/main.go, which mirrors the command tree from cmd/dpg without importing pipeline stages. When you add or change a flag, also update its mirror in tools/gendocs/main.go.
make docs-cli # regenerate only the CLI markdown files
Code Organisation Principles
Offline-first. plan and diff never open a database connection. All database access is isolated to executor (apply), introspect (verify/dump), and testpg (integration tests). The compile → diff → emit path has no network I/O.
Pipeline + registry. Every processing stage is an interface in internal/pipeline. Concrete implementations register themselves into pipeline.Default via init(). The CLI resolves implementations from the registry at startup, never by importing concrete packages directly.
Public API surface. pkg/dpg re-exports selected types and functions via type aliases. Plugin authors import only pkg/dpg. Never import internal/ from outside the module.
Build tags for integration. Files that import testcontainers-go carry //go:build integration. This keeps go test ./... fast and dependency-free.
Common Workflows
Add a new CLI flag
- Add the flag in
cmd/dpg/<command>.go - Mirror it in
tools/gendocs/main.go(sameUse,Short,Long, flags) - Run
make docs-clito regenerate the markdown
Add a new lint rule
- Implement in
internal/linter/(satisfiespipeline.Linteror extendBuiltinLinter) - Add a unit test
- Document in
site/content/docs/migrations/linting.md
Add a new IR object type
- Define the struct in
internal/ir/ - Register a
KindXxxconstant ininternal/pipeline/types.go - Add IR builder logic in
internal/ir/ - Add differ logic in
internal/diff/ - Add emitter logic in
internal/emit/ - Export via type alias in
pkg/dpg/dpg.go - Add integration test in
examples/orinternal/with//go:build integration - Document in
site/content/docs/schema-objects/
Troubleshooting
cgo: C compiler "gcc" not found
# Ubuntu/Debian
sudo apt-get install -y gcc build-essential
# macOS
xcode-select --install
go: cannot find main module
Run all make and go commands from the repository root (where go.mod lives), not from a subdirectory.
hugo: command not found or hugo: not extended
The standard Hugo binary does not include the Sass/SCSS compiler. Download the extended variant explicitly:
HUGO_VER=0.147.0
ARCH=$(uname -m | sed 's/x86_64/amd64/')
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VER}/hugo_extended_${HUGO_VER}_linux-${ARCH}.tar.gz" \
| sudo tar -C /usr/local/bin -xz hugo
hugo version # must say "extended"
Integration tests time out or fail to pull the Docker image
docker info # confirm daemon is running
docker pull postgres:16-alpine # pre-pull the test image
make test-integration
If inside a corporate proxy, configure Docker’s proxy settings and ensure registry-1.docker.io is reachable.
dpg docs returns “documentation is not embedded in this build”
The dev build (make build) intentionally does not embed docs. Use:
make build-full # embeds docs, requires Hugo + Node
# or use a release binary from GitHub releases
Version Metadata
The binary embeds three values at link time:
# Build with an explicit version tag
make build v0.5.5-alpha.6=v0.5.5-alpha.6
# The binary reports version info:
dpg --version
# dpg version v0.5.5-alpha.6 (commit: a3f7c91, built: 2026-04-27T00:00:00Z)Without VERSION, it defaults to git describe --tags --always --dirty or dev if git is unavailable. The three values are injected via -ldflags into internal/version/.
Compiler Error Codes
DPG errors use structured codes of the form DPG-Ennn. The full reference:
| Code | Name | Description |
|---|---|---|
| DPG-E001 | unknown_config_key | Unknown key in dpg.toml. |
| DPG-E002 | ambiguous_connection | Both url and link set in cluster config. |
| DPG-E003 | no_connection_configured | Command requires a connection but neither url nor link is set. |
| DPG-E004 | reserved_name_conflict | A database directory name matches the cluster objects directory name. |
| DPG-E005 | conflicting_set_member | Same-named set-valued property has conflicting definitions across files. |
| DPG-E006 | forbidden_verb | CREATE, ALTER, or DROP at declaration level in a .dpg file. |
| DPG-E007 | macro_inside_block | MACRO declaration found inside a block. |
| DPG-E008 | paren_macro_in_block | Paren-body macro spread inside a { } block. |
| DPG-E009 | brace_macro_in_paren | Brace-body macro spread inside a ( ) list. |
| DPG-E010 | undefined_macro | Spread of undefined macro name. |
| DPG-E011 | duplicate_macro | Macro name redeclared in the same file. |
| DPG-E012 | circular_macro | Circular macro reference detected. |
| DPG-E013 | enum_migration_data_remains | Rows still hold a removed ENUM value after the MIGRATE REMOVE DML ran. |
| DPG-E014 | unguarded_enum_removal | ENUM value removed without a MIGRATE REMOVE block. |
| DPG-E015 | invalid_virtual_type_directive | VIRTUAL TYPE { } block contains a directive other than COMMENT. |
| DPG-E016 | not_valid_in_paren_list | NOT VALID used in a column or constraint inside the ( ) list. |
| DPG-E017 | unresolvable_cycle | Circular FK dependency with no DEFERRABLE FK to break the cycle. |
| DPG-E018 | unknown_column_reference | COLUMN name { } block references a column not in the ( ) list. |
| DPG-E019 | stale_column_name_in_index | Index or constraint references old column name after a rename. |
| DPG-E020 | statistics_target_out_of_range | STATISTICS n value is outside [-1, 10000]. |
| DPG-E021 | stale_renamed_from | RENAMED FROM references a name absent from both source and snapshot. |
| DPG-E022 | protected_drop_attempt | Diff would drop a PROTECTED object. |
| DPG-E023 | temporary_table_declared | TEMPORARY TABLE keyword found in a .dpg file. |
| DPG-E024 | unknown_block_directive | Unknown directive for this object kind in a { } block. |
| DPG-E025 | destructive_ops_blocked | Migration contains DESTRUCTIVE ops but --allow-destructive not passed. |
| DPG-E026 | multiple_clusters_no_flag | Multiple clusters found; --cluster required. |
| DPG-E027 | cluster_not_found | --cluster value does not match any cluster. |
| DPG-E028 | multiple_databases_no_flag | Multiple databases found; --database required. |
| DPG-E029 | database_not_found | --database value does not match any database. |