camel

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2026 License: MIT Imports: 17 Imported by: 0

README

Camel

Explicit database migrations you can actually read.

Camel is a lightweight CLI for teams who want migrations they write, review, and trust — not magic that diffs your database. You declare small intentions in YAML or JSON; Camel turns them into correct SQL for Postgres, MySQL, SQLite, and SQL Server. No framework. No ORM. No surprises.


Install

macOS

Download camel-darwin-arm64 (Apple Silicon) or camel-darwin-amd64 (Intel) from the Releases page, then:

chmod +x camel-darwin-arm64
sudo mv camel-darwin-arm64 /usr/local/bin/camel
camel init
Linux

Download camel-linux-amd64 or camel-linux-arm64, then:

chmod +x camel-linux-amd64
sudo mv camel-linux-amd64 /usr/local/bin/camel
camel init
Windows
  1. Download camel-windows-amd64.exe from the Releases page.
  2. Open PowerShell as Administrator and run:
mkdir C:\tools
Move-Item "$HOME\Downloads\camel-windows-amd64.exe" C:\tools\camel.exe
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\tools", "Machine")
  1. Close and reopen PowerShell, then verify:
camel
Install with Go (requires Go 1.22+)
go install github.com/martin3zra/camel/cmd/camel@latest
Build from source
git clone https://github.com/martin3zra/camel
cd camel
go install ./cmd/camel

A checksums.txt is included in each release to verify your download.


Quick start

No server needed — SQLite works out of the box for local development.

mkdir myproject && cd myproject
camel init                          # creates camel.yaml + database/
camel config                        # confirm driver=sqlite, source=camel.sqlite
camel make create_posts_table       # scaffold a migration
camel migrate --pretend             # preview SQL without touching the DB
camel migrate                       # apply
camel status                        # show applied / pending
camel rollback                      # reverse the last batch

Switch to Postgres, MySQL, or SQL Server by editing camel.yaml — the same migration files run unchanged on every driver.


Migration format

Create a table
up:
  post:
    action: create
    table: posts
    columns:
      id: id
      title: string:150
      slug: string:180 unique
      body: longText nullable
      status: enum:draft,published default:'draft'
      created_at: timestamp useCurrent
      updated_at: timestamp nullable

down:
  post:
    action: drop
    table: posts
Alter a table
up:
  post:
    action: alter
    table: posts
    add_columns:
      view_count: integer default:0
    rename_columns:
      title: headline
    add_indexes:
      - name: posts_headline_idx
        columns: [headline]
    add_foreign:
      - name: posts_author_fk
        columns: [author_id]
        references_table: users
        references_columns: [id]
        on_delete: cascade

down:
  post:
    action: alter
    table: posts
    drop_foreign: [posts_author_fk]
    drop_indexes: [posts_headline_idx]
    rename_columns:
      headline: title
    drop_columns: [view_count]
Foreign keys across tables

Put both tables in one migration file. Camel reads the FK dependencies and creates tables in the right order automatically — and drops them in reverse on rollback, so SQL Server's FK enforcement never blocks a reset.

up:
  user:
    action: create
    table: users
    columns:
      id: id
      email: string:255 unique
  post:
    action: create
    table: posts
    columns:
      id: id
      author_id: bigInteger
    foreign:
      - name: posts_author_fk
        columns: [author_id]
        references_table: users
        references_columns: [id]
        on_delete: cascade

down:
  post:
    action: drop
    table: posts
  user:
    action: drop
    table: users
Raw SQL (escape hatch)

For data migrations, CHECK constraints, or anything the DSL doesn't cover:

up:
  seed:
    action: raw
    statements:
      - "INSERT INTO roles (name) VALUES ('admin'), ('member');"

down:
  seed:
    action: raw
    statements:
      - "DELETE FROM roles WHERE name IN ('admin', 'member');"

Column DSL

type:attribute modifier modifier:value
id: id                                    # BIGSERIAL / BIGINT AUTO_INCREMENT / INTEGER / BIGINT IDENTITY
title: string:150                         # VARCHAR(150) / NVARCHAR(150) / TEXT
slug: string:180 unique                   # unique auto-splits to ADD COLUMN + CREATE UNIQUE INDEX on all drivers
body: longText nullable                   # LONGTEXT / TEXT / NVARCHAR(MAX), allows NULL
price: decimal:8,2 default:0.00
status: enum:draft,published default:'draft'
created_at: timestamp useCurrent

See DOCS.md for the full type and modifier reference, per-driver SQL output, and driver-specific behavior.


SQL migrations (procedures, functions, triggers)

For anything the YAML DSL can't express — stored procedures, functions, triggers, complex views — scaffold a plain .sql file:

camel make create_backfill_proc --format sql

The generated file has -- up and -- down sections. Use GO on its own line as the statement separator when the body contains semicolons:

-- up
CREATE PROCEDURE backfill_slugs()
BEGIN
  UPDATE posts SET slug = LOWER(title) WHERE slug IS NULL;
END
GO

-- down
DROP PROCEDURE IF EXISTS backfill_slugs
GO

camel migrate, camel rollback, and camel status treat .sql files the same as .yaml files. The -- down section gives rollback a real inverse.

.sql files are driver-specific. You own dialect correctness; Camel passes statements through verbatim.


Squashing migrations

As a project matures, the database/ directory can accumulate hundreds of files. camel dump condenses them:

camel dump           # write schema.sql at the project root (reference only)
camel dump --prune   # squash applied migrations into one SQL file

After --prune:

database/
├── 00000000000000_schema_dump.sql   ← full schema, applied first on a fresh DB
└── 20261220_add_something.yaml      ← pending migrations survive untouched

On a fresh database camel migrate picks up 00000000000000_schema_dump.sql automatically — no extra commands needed.

The generated SQL is dialect-specific. Regenerate the dump if you switch database drivers.


Commands

Command Description
camel init Create camel.yaml and the migrations directory
camel config Print resolved configuration
camel make <name> [--format yaml|json|sql] Scaffold a migration file
camel plan Preview SQL for all pending migrations
camel migrate Apply all pending migrations
camel migrate --pretend Preview SQL without touching the DB
camel migrate --dry-run Run pending migrations for real against the configured DB, in a transaction that's always rolled back — catches state-dependent failures (existing column, NOT NULL add on a non-empty table, missing FK target) that --pretend can't see. Refuses on MySQL, whose DDL auto-commits and can't be rolled back.
camel rollback Reverse the last batch
camel rollback --step N Reverse the last N migrations
camel rollback --all Reverse every applied migration
camel reset Roll everything back
camel status List migrations with applied / pending state
camel dump Write current schema to schema.sql
camel dump --prune Squash applied migrations into one SQL file

Configuration

camel.yaml in the project root:

db:
  driver: "sqlite"     # postgres | mysql | sqlite | mssql
  source: "camel.sqlite"

migration:
  directory: "database"
  pattern: "*.yaml"
  table: "camel_migrations"

Precedence (highest first): process env → .env file → camel.yaml → defaults.

Environment variables: DB_DRIVER / DATABASE_DRIVER, DB_SOURCE / DATABASE_URL.

Connection strings
# PostgreSQL
source: "postgres://user:password@localhost:5432/mydb?sslmode=disable"

# MySQL
source: "user:password@tcp(localhost:3306)/mydb?parseTime=true"

# SQL Server
source: "sqlserver://sa:Password@localhost:1433?database=mydb&encrypt=disable&TrustServerCertificate=true"

# SQLite
source: "mydb.sqlite"

Supported drivers

Driver driver value Notes
PostgreSQL postgres
MySQL mysql RENAME COLUMN requires MySQL 8.0+
SQLite sqlite No ALTER COLUMN; FKs declared at create time
SQL Server mssql azure-sql-edge supported on ARM64

Why explicit migrations

  • No surprises — Camel runs the files you wrote. It never diffs your live database or generates migrations behind your back.
  • Reviewable — a migration is a few lines of intent, readable and diff-friendly in PRs.
  • Portable — one file, correct SQL on all four drivers. FK ordering, unique index splitting, and driver quirks are handled automatically.
  • Transactional — each migration is all-or-nothing. A failure rolls back and is never recorded as applied.
  • Full lifecycle — schema, data, procedures, functions, triggers — all versioned in the same migrations directory.

License

MIT

Documentation

Index

Constants

View Source
const DefaultConfigFile = "camel.yaml"

Variables

This section is empty.

Functions

func CreateMigrationFile

func CreateMigrationFile(dir string, cfg Config, name string, format string) (string, error)

func DefaultConfigYAML

func DefaultConfigYAML(env map[string]string) string

func GenerateSQL

func GenerateSQL(key string, intent TableIntent, driver string) ([]string, error)

func InitProject

func InitProject(dir string) error

func InstallDBScaffoldSkill

func InstallDBScaffoldSkill(dir string, force bool) (written, skipped []string, err error)

InstallDBScaffoldSkill writes the bundled db-scaffold skill into dir (a target project root). Files that already exist are left untouched and reported as skipped unless force is true.

func ReadDotEnv

func ReadDotEnv(path string) (map[string]string, error)

func StatementsFor

func StatementsFor(migration Migration, direction Direction, driver string) ([]string, error)

Types

type AppliedMigration

type AppliedMigration struct {
	Name  string
	Batch int
}

AppliedMigration is one recorded row in the migration tracking table.

type Column

type Column struct {
	Name          string
	Type          string
	Attributes    []string
	Nullable      bool
	Primary       bool
	Unique        bool
	AutoIncrement bool
	Unsigned      bool
	Default       *string
	UseCurrent    bool
}

func ParseColumn

func ParseColumn(name string, definition string) (Column, error)

type Config

type Config struct {
	DB        DBConfig        `json:"db" yaml:"db"`
	Migration MigrationConfig `json:"migration" yaml:"migration"`
}

func DefaultConfig

func DefaultConfig() Config

func LoadConfig

func LoadConfig(dir string) (Config, error)

type DBConfig

type DBConfig struct {
	Driver string `json:"driver" yaml:"driver"`
	Source string `json:"source" yaml:"source"`
}

type Direction

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

type ForeignIntent

type ForeignIntent struct {
	Name       string   `json:"name" yaml:"name"`
	Columns    []string `json:"columns" yaml:"columns"`
	RefTable   string   `json:"references_table" yaml:"references_table"`
	RefColumns []string `json:"references_columns" yaml:"references_columns"`
	OnDelete   string   `json:"on_delete,omitempty" yaml:"on_delete,omitempty"`
	OnUpdate   string   `json:"on_update,omitempty" yaml:"on_update,omitempty"`
}

ForeignIntent describes a foreign-key constraint added via ALTER.

type IndexIntent

type IndexIntent struct {
	Name    string   `json:"name" yaml:"name"`
	Columns []string `json:"columns" yaml:"columns"`
	Unique  bool     `json:"unique,omitempty" yaml:"unique,omitempty"`
}

IndexIntent describes a CREATE INDEX.

type Migration

type Migration struct {
	Path string
	Name string
	File MigrationFile
}

func ListMigrations

func ListMigrations(cfg Config, dir string) ([]Migration, error)

func LoadMigration

func LoadMigration(path string) (Migration, error)

type MigrationConfig

type MigrationConfig struct {
	Directory string `json:"directory" yaml:"directory"`
	Pattern   string `json:"pattern" yaml:"pattern"`
	Table     string `json:"table" yaml:"table"`
}

type MigrationFile

type MigrationFile struct {
	Up   map[string]TableIntent `json:"up" yaml:"up"`
	Down map[string]TableIntent `json:"down" yaml:"down"`
}

type MigrationStatus

type MigrationStatus struct {
	Name    string
	Applied bool
}

type RollbackOptions

type RollbackOptions struct {
	Steps   int  // reverse the last N migrations; 0 means "last batch"
	All     bool // reverse every applied migration; overrides Steps
	Pretend bool // print SQL without executing
}

RollbackOptions controls how many applied migrations Rollback reverses.

With the zero value, Rollback reverses the most recent batch (every migration sharing the highest batch number), matching the grouping that a single `migrate` run records. Steps overrides that to reverse exactly the last N applied migrations regardless of batch. All reverses everything (reset).

type Runner

type Runner struct {
	Config Config
	Dir    string
	DB     *sql.DB
}

func NewRunner

func NewRunner(cfg Config, dir string) (*Runner, error)

func (*Runner) Close

func (r *Runner) Close() error

func (*Runner) Dump

func (r *Runner) Dump(prune bool) error

Dump writes the SQL for all applied migrations to schema.sql at the project root. With prune=true it also squashes: applied migration files are deleted, a 00000000000000_schema_dump.sql is written into the migrations directory (so `camel migrate` on a fresh DB runs it automatically), and the tracking table is reset so only the dump is recorded as applied.

func (*Runner) EnsureRepository

func (r *Runner) EnsureRepository() error

func (*Runner) Migrate

func (r *Runner) Migrate(pretend bool) error

func (*Runner) MigrateDryRun

func (r *Runner) MigrateDryRun() error

MigrateDryRun runs every pending migration's up statements against the real, configured database — inside a single transaction that is always rolled back — to catch failures that depend on the database's actual current state (a column that already exists, a NOT NULL column added to a table that already has rows, a foreign key whose target isn't really there). camel plan can't see any of that: it only renders SQL from the DSL, it never touches a connection. Nothing is committed and nothing is recorded in the tracking table, regardless of outcome.

Pending migrations run in order on one shared transaction (not one transaction per migration) so a later migration sees the tables/columns earlier ones in the same batch would have created — matching what a real `migrate` run would leave in place, right up until the final rollback.

MySQL is refused unconditionally: its DDL statements auto-commit, so a transaction rollback would not undo them — a "dry run" there would apply the changes for real while leaving the tracking table (and therefore a later `camel migrate`) out of sync with what actually happened.

func (*Runner) Reset

func (r *Runner) Reset(pretend bool) error

Reset rolls back every applied migration, newest first.

func (*Runner) Rollback

func (r *Runner) Rollback(opts RollbackOptions) error

func (*Runner) Status

func (r *Runner) Status() ([]MigrationStatus, error)

type SkillFile

type SkillFile struct {
	Source string
	Dest   string
}

SkillFile is one file the db-scaffold skill installs into a target project, mapping an embedded source path to a destination relative to the project root.

func DBScaffoldFiles

func DBScaffoldFiles() []SkillFile

DBScaffoldFiles lists every file InstallDBScaffoldSkill writes. The same SKILL.md is installed for both Claude Code and Cursor (Cursor supports the identical Agent Skill format); the .mdc is a thin Cursor rule pointing at it, kept as a fallback for Cursor setups that only honor rules.

type TableIntent

type TableIntent struct {
	Table  string `json:"table,omitempty" yaml:"table,omitempty"`
	Action string `json:"action,omitempty" yaml:"action,omitempty"`

	// create
	Columns map[string]string `json:"columns,omitempty" yaml:"columns,omitempty"`
	Indexes []IndexIntent     `json:"indexes,omitempty" yaml:"indexes,omitempty"`
	Foreign []ForeignIntent   `json:"foreign,omitempty" yaml:"foreign,omitempty"`

	// raw: statements executed verbatim, in order (action: raw)
	Statements []string `json:"statements,omitempty" yaml:"statements,omitempty"`

	// alter
	AddColumns    map[string]string `json:"add_columns,omitempty" yaml:"add_columns,omitempty"`
	DropColumns   []string          `json:"drop_columns,omitempty" yaml:"drop_columns,omitempty"`
	ModifyColumns map[string]string `json:"modify_columns,omitempty" yaml:"modify_columns,omitempty"`
	RenameColumns map[string]string `json:"rename_columns,omitempty" yaml:"rename_columns,omitempty"`
	AddIndexes    []IndexIntent     `json:"add_indexes,omitempty" yaml:"add_indexes,omitempty"`
	DropIndexes   []string          `json:"drop_indexes,omitempty" yaml:"drop_indexes,omitempty"`
	AddForeign    []ForeignIntent   `json:"add_foreign,omitempty" yaml:"add_foreign,omitempty"`
	DropForeign   []string          `json:"drop_foreign,omitempty" yaml:"drop_foreign,omitempty"`
}

Directories

Path Synopsis
cmd
camel command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL