Skip to main content

Automatic Schema Migration Planning for Bun using Atlas

TL;DR

  • Bun is a fast and simple SQL client and ORM for Go.
  • Atlas is an open-source tool for inspecting, planning, linting and executing schema changes to your database.
  • Developers using Bun can use Atlas to automatically plan schema migrations for them based on the desired state of their schema, rather than crafting them by hand.

Automatic migration planning for Bun

Bun is a fast and simple SQL client and ORM for Go that supports PostgreSQL, MySQL, SQLite, and SQL Server. Bun provides excellent performance and a clean API for working with databases.

Bun already includes a powerful migration system that supports both Go-based and SQL-based migrations. However, while Bun's migration system is excellent for executing migrations, it requires developers to manually write the migration logic.

Atlas complements Bun's migration system by automatically generating the migration content based on schema changes in your models. Instead of manually writing CREATE TABLE, ALTER TABLE, and other DDL statements, Atlas can:

  • Automatically plan migrations by comparing your Bun models againt the current database state
  • Detect schema drift between your models and production databases
  • Lint migrations to catch potential issues before deployment
  • Provide CI/CD integration with automated schema validation

Atlas can generate migration files that work seamlessly with Bun's existing migration framework, giving you the best of both worlds: automatic planning with Atlas and robust execution with Bun.

Atlas works by calculating the diff between the current state of the database and the desired state defined by your Bun models. The desired schema is provided to Atlas via an External Schema Datasource - a program that reads your Bun models and outputs the corresponding SQL schema.

The Bun Atlas Provider handles this integration, allowing Atlas to understand your Bun models and generate appropriate migration files.

In this guide, we will show how to use Atlas to automatically generate migration content that you can integrate with your existing Bun project.

Prerequisites

  • A local Bun project - if you don't have a Bun project handy, you can use Bun's example projects as a starting point.

The Bun Atlas Provider works by creating a temporary Go program, then compiling and running it to extract the schema of your Bun project. Therefore, you will need to have Go installed on your machine.

Using the Bun Atlas Provider

Installation

Install Atlas from macOS or Linux by running:

curl -sSf https://atlasgo.sh | sh

See atlasgo.io for more installation options.

Install the provider by running:

go get -u ariga.io/atlas-provider-bun

Standalone vs Go Program mode

The Atlas Bun Provider can be used in two modes:

  • Standalone - If all of your Bun models exist in a single package, you can use the provider directly to load your Bun schema into Atlas.
  • Go Program - In other cases, you can use the provider as a library directly in a Go program to load your Bun schema into Atlas.

Standalone mode

In your project directory, create a new file named atlas.hcl with the following contents:

data "external_schema" "bun" {
program = [
"go",
"run",
"-mod=mod",
"ariga.io/atlas-provider-bun",
"load",
"--path", "./path/to/models",
"--dialect", "mysql", // | postgres | sqlite | sqlserver
]
}

env "bun" {
src = data.external_schema.bun.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}

Be sure to replace ./path/to/models with the path to the package that contains your Bun models.

Pinning Go dependencies

To prevent the Go Modules system from dropping this dependency from our go.mod file, let's follow its official recommendation for tracking dependencies of tools by adding a file named tools.go with the following contents:

tools.go
//go:build tools
package main

import _ "ariga.io/atlas-provider-bun/bunschema"

Finally, to tidy things up, run:

go mod tidy

Go Program mode

Create a new program named loader/main.go with the following contents:

package main

import (
"fmt"
"io"
"os"

"ariga.io/atlas-provider-bun/bunschema"
_ "ariga.io/atlas-go-sdk/recordriver"
"github.com/<yourorg>/<yourrepo>/path/to/models"
)

func main() {
stmts, err := bunschema.New(bunschema.DialectMySQL).Load(
&models.User{},
&models.Post{},
)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load bun schema: %v\n", err)
os.Exit(1)
}
io.WriteString(os.Stdout, stmts)
}

Be sure to replace github.com/<yourorg>/<yourrepo>/path/to/models with the import path to your Bun models. In addition, replace the model types (e.g models.User, models.Post) with the types of your Bun models.

Next, in your project directory, create a new file named atlas.hcl with the following contents:

data "external_schema" "bun" {
program = [
"go",
"run",
"-mod=mod",
"./loader",
]
}

env "bun" {
src = data.external_schema.bun.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}

Usage

Atlas can generate SQL migration files that you can integrate with Bun's migration system. You can use the atlas migrate diff command to automatically generate a migration file based on changes to your Bun models.

The generated SQL can then be used in Bun's SQL-based migrations or incorporated into Go-based migrations.

Suppose we have the following Bun models in our models package:

package models

import (
"time"
"github.com/uptrace/bun"
)

type User struct {
bun.BaseModel `bun:"table:users,alias:u"`

ID int64 `bun:"id,pk,autoincrement"`
Name string `bun:"name,notnull"`
Email string `bun:"email,unique,notnull"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
Posts []Post `bun:"rel:has-many,join:id=user_id"`
}

type Post struct {
bun.BaseModel `bun:"table:posts,alias:p"`

ID int64 `bun:"id,pk,autoincrement"`
Title string `bun:"title,notnull"`
Content string `bun:"content,type:text"`
UserID int64 `bun:"user_id,notnull"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`

User *User `bun:"rel:belongs-to,join:user_id=id"`
}

Using the Standalone mode configuration file for the provider, we can generate a migration file by running this command:

atlas migrate diff --env bun 

Running this command will generate files in the migrations directory similar to this:

migrations
├── 20250618083251.sql
└── atlas.sum

1 directory, 2 files

Examining the contents of 20250618083251.sql:

20250618083251.sql
-- Create "users" table
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `email` (`email`)
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Create "posts" table
CREATE TABLE `posts` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`content` text NULL,
`user_id` bigint NOT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `user_id` (`user_id`),
CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

Atlas automatically generated a migration file that will create the users and posts tables in our database.

Next, let's alter the User struct to add a new field:

type User struct {
bun.BaseModel `bun:"table:users,alias:u"`

ID int64 `bun:"id,pk,autoincrement"`
Name string `bun:"name,notnull"`
Email string `bun:"email,unique,notnull"`
Bio string `bun:"bio,type:text"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
Posts []Post `bun:"rel:has-many,join:id=user_id"`
}

Re-run this command:

atlas migrate diff --env bun 

Observe a new migration file is generated under the migrations directory:

20250618083619.sql
-- Modify "users" table
ALTER TABLE `users` ADD COLUMN `bio` text NULL;

Many-to-Many Relations

The provider fully supports many-to-many relations, but ONLY when it is used in Go Program mode. The standalone mode cannot automatically discover join tables.

When working with many-to-many relations:

  1. Register the join table via the bunschema.WithJoinTable option.
  2. Pass the join table model together with the related models to the Load function.

For example:

stmts, err := bunschema.New("mysql",
bunschema.WithJoinTable(&models.OrderToItem{}),
).Load(
&models.Item{},
&models.Order{},
&models.OrderToItem{},
)
if err != nil {
log.Fatal(err)
}
fmt.Println(stmts)

When to Use Atlas with Bun

Consider using Atlas with Bun when you need:

  • Automatic migration generation - Save time by generating DDL statements from model changes
  • Schema drift detection - Ensure your production database matches your models
  • Advanced linting - Catch potential migration issues before deployment
  • Complex schema changes - Handle intricate migrations that are error-prone to write manually

Conclusion

In this guide we demonstrated how projects using Bun can leverage Atlas to automatically generate migration content based on model changes, while still using Bun's robust ORM system. This combination gives you automated planning with proven execution.

Have questions? Feedback? Find our team on our Discord server.