Skip to content

Commit 90196ef

Browse files
committed
initial commit
1 parent d9f0552 commit 90196ef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+8148
-1
lines changed

README.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,89 @@
1-
# dbx
1+
# dbx
2+
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/swiftcarrot/dbx.svg)](https://pkg.go.dev/github.com/swiftcarrot/dbx)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/swiftcarrot/dbx)](https://goreportcard.com/report/github.com/swiftcarrot/dbx)
5+
[![CI Status](https://github.com/swiftcarrot/dbx/workflows/CI/badge.svg)](https://github.com/swiftcarrot/dbx/actions)
6+
7+
DBX is a powerful database schema migration library for Go that enables programmatic management of database schemas using Go code instead of raw SQL.
8+
9+
## Features
10+
11+
- **Schema-based migrations**: Define your database schema in Go code rather than raw SQL
12+
- **Automatic SQL generation**: Automatically generate SQL statements for schema changes
13+
- **Database inspection**: Introspect existing database schemas
14+
- **Schema comparison**: Compare schemas and generate migration statements
15+
- **Built on `database/sql`**: Works with standard Go database drivers
16+
- **Multiple dialect support**: PostgreSQL and MySQL supported, with more coming soon
17+
18+
## Usage Examples
19+
20+
### Database Inspection
21+
22+
Introspect an existing database schema:
23+
24+
```go
25+
import (
26+
_ "github.com/lib/pq"
27+
"github.com/swiftcarrot/dbx/postgresql"
28+
"github.com/swiftcarrot/dbx/schema"
29+
)
30+
31+
db, err := sql.Open("postgres", "postgres://")
32+
pg := postgresql.New()
33+
source, err := pg.Inspect(db)
34+
```
35+
36+
### Schema Definition and Comparison
37+
38+
Define a target schema and compare with current schema:
39+
40+
```go
41+
target := schema.NewSchema()
42+
target.CreateTable("user", func(t *schema.Table) {
43+
t.Column("name", "text", schema.NotNull)
44+
t.Index("")
45+
})
46+
47+
changes, err := schema.Diff(source, target)
48+
```
49+
50+
### Applying Schema Changes
51+
52+
Generate and execute SQL from schema changes:
53+
54+
```go
55+
for _, change := range changes {
56+
sql := pg.GenerateSQL(change)
57+
_, err := db.Exec(sql)
58+
}
59+
```
60+
61+
## Supported Dialects
62+
63+
### PostgreSQL
64+
65+
```go
66+
import (
67+
_ "github.com/lib/pq"
68+
"github.com/swiftcarrot/dbx/postgresql"
69+
)
70+
71+
pg := postgresql.New()
72+
```
73+
74+
### MySQL
75+
76+
```go
77+
import (
78+
_ "github.com/go-sql-driver/mysql"
79+
"github.com/swiftcarrot/dbx/mysql"
80+
)
81+
82+
my := mysql.New()
83+
```
84+
85+
For other dialect support, feel free to [create an issue](https://github.com/swiftcarrot/dbx/issues/new).
86+
87+
## License
88+
89+
This project is licensed under the Apache License - see the LICENSE file for details.

go.mod

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module github.com/swiftcarrot/dbx
2+
3+
go 1.21.9
4+
5+
require (
6+
github.com/lib/pq v1.10.9
7+
github.com/urfave/cli/v2 v2.27.6
8+
)
9+
10+
require (
11+
filippo.io/edwards25519 v1.1.0 // indirect
12+
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
13+
github.com/go-sql-driver/mysql v1.9.2 // indirect
14+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
15+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
16+
)
17+
18+
require (
19+
github.com/davecgh/go-spew v1.1.1 // indirect
20+
github.com/pmezard/go-difflib v1.0.0 // indirect
21+
github.com/stretchr/testify v1.10.0
22+
gopkg.in/yaml.v3 v3.0.1 // indirect
23+
)

go.sum

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3+
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
4+
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
8+
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
9+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
10+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
14+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
15+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
16+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
17+
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
18+
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
19+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
20+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
21+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
22+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/testutil/compare.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package testutil
2+
3+
import "regexp"
4+
5+
func FormatSQL(sql string) string {
6+
re := regexp.MustCompile(`\s+`)
7+
return re.ReplaceAllString(sql, " ")
8+
}

internal/testutil/mysql.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testutil
2+
3+
import (
4+
"database/sql"
5+
"log"
6+
7+
_ "github.com/go-sql-driver/mysql"
8+
)
9+
10+
const MySQLTestConnString = "root@(localhost:3306)/dbx_test?parseTime=true&multiStatements=true"
11+
12+
func GetMySQLTestConn() (*sql.DB, error) {
13+
db, err := sql.Open("mysql", MySQLTestConnString)
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
err = db.Ping()
18+
if err != nil {
19+
log.Fatal(err)
20+
}
21+
22+
return db, err
23+
}

internal/testutil/postgresql.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testutil
2+
3+
import (
4+
"database/sql"
5+
"log"
6+
7+
_ "github.com/lib/pq"
8+
)
9+
10+
const PGTestConnString = "postgres://postgres:postgres@localhost:5432/dbx_test?sslmode=disable"
11+
12+
func GetPGTestConn() (*sql.DB, error) {
13+
db, err := sql.Open("postgres", PGTestConnString)
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
err = db.Ping()
18+
if err != nil {
19+
log.Fatal(err)
20+
}
21+
22+
return db, err
23+
}

internal/testutil/schema.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package testutil
2+
3+
import (
4+
"github.com/swiftcarrot/dbx/schema"
5+
)
6+
7+
// CreateUsersTableSchema creates a schema with a users table for testing.
8+
func CreateUsersTableSchema() *schema.Schema {
9+
s := schema.NewSchema()
10+
s.CreateTable("users", func(t *schema.Table) {
11+
t.Column("id", "serial", schema.NotNull)
12+
t.Column("username", "varchar(50)", schema.NotNull)
13+
t.Column("email", "varchar(100)", schema.NotNull)
14+
t.Column("created_at", "timestamp", schema.NotNull, schema.Default("CURRENT_TIMESTAMP"))
15+
t.SetPrimaryKey("users_pkey", []string{"id"})
16+
t.Index("users_email_idx", []string{"email"}, schema.Unique)
17+
t.Index("users_username_idx", []string{"username"})
18+
})
19+
return s
20+
}
21+
22+
// CreateUsersTableWithProfileSchema creates a schema with a users table that includes profile fields.
23+
func CreateUsersTableWithProfileSchema() *schema.Schema {
24+
s := CreateUsersTableSchema()
25+
table := s.Tables[0] // users table
26+
table.Column("bio", "text", schema.Nullable)
27+
table.Column("avatar_url", "varchar(255)", schema.Nullable)
28+
return s
29+
}
30+
31+
// CreateUsersAndPostsSchema creates a schema with users and posts tables.
32+
func CreateUsersAndPostsSchema() *schema.Schema {
33+
s := CreateUsersTableSchema()
34+
s.CreateTable("posts", func(t *schema.Table) {
35+
t.Column("id", "serial", schema.NotNull)
36+
t.Column("user_id", "integer", schema.NotNull)
37+
t.Column("title", "varchar(200)", schema.NotNull)
38+
t.Column("content", "text", schema.Nullable)
39+
t.Column("published", "boolean", schema.NotNull, schema.Default("false"))
40+
t.Column("created_at", "timestamp", schema.NotNull, schema.Default("CURRENT_TIMESTAMP"))
41+
t.SetPrimaryKey("posts_pkey", []string{"id"})
42+
t.Index("posts_user_id_idx", []string{"user_id"})
43+
t.Index("posts_published_created_at_idx", []string{"published", "created_at"})
44+
t.ForeignKey("fk_posts_user", []string{"user_id"}, "users", []string{"id"}, schema.OnDelete("CASCADE"))
45+
})
46+
return s
47+
}
48+
49+
// CreateFullSchema creates a schema with users, posts, and comments tables.
50+
func CreateFullSchema() *schema.Schema {
51+
s := CreateUsersAndPostsSchema()
52+
s.CreateTable("comments", func(t *schema.Table) {
53+
t.Column("post_id", "integer", schema.NotNull)
54+
t.Column("user_id", "integer", schema.NotNull)
55+
t.Column("content", "text", schema.NotNull)
56+
t.Column("created_at", "timestamp", schema.NotNull, schema.Default("CURRENT_TIMESTAMP"))
57+
t.SetPrimaryKey("comments_pkey", []string{"post_id", "user_id"})
58+
t.ForeignKey("fk_comments_post", []string{"post_id"}, "posts", []string{"id"}, schema.OnDelete("CASCADE"))
59+
t.ForeignKey("fk_comments_user", []string{"user_id"}, "users", []string{"id"}, schema.OnDelete("CASCADE"))
60+
})
61+
return s
62+
}
63+
64+
// CreateUsersTableWithModifiedColumnSchema creates a users schema with modified columns.
65+
func CreateUsersTableWithModifiedColumnSchema() *schema.Schema {
66+
s := schema.NewSchema()
67+
s.CreateTable("users", func(t *schema.Table) {
68+
t.Column("id", "serial", schema.NotNull)
69+
t.Column("username", "varchar(50)", schema.Nullable) // Changed to nullable
70+
t.Column("email", "varchar(255)", schema.NotNull) // Changed length
71+
t.Column("created_at", "timestamp", schema.NotNull, schema.Default("CURRENT_TIMESTAMP"))
72+
t.SetPrimaryKey("users_pkey", []string{"id"})
73+
t.Index("users_email_idx", []string{"email"}, schema.Unique)
74+
t.Index("users_username_idx", []string{"username"})
75+
})
76+
return s
77+
}

migration/migration.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package migration
2+
3+
import (
4+
"time"
5+
6+
"github.com/swiftcarrot/dbx/schema"
7+
)
8+
9+
// Migration represents a database schema migration similar to Rails migrations.
10+
// It includes version information, name, and methods to define schema changes.
11+
type Migration struct {
12+
// Version represents the migration version (typically a timestamp)
13+
Version string
14+
15+
// Name is a descriptive name for the migration
16+
Name string
17+
18+
// CreatedAt represents when the migration was created
19+
CreatedAt time.Time
20+
21+
// UpFn defines the schema changes for migrating up
22+
UpFn func() *schema.Schema
23+
24+
// DownFn defines the schema changes for rolling back (migrating down)
25+
DownFn func() *schema.Schema
26+
}
27+
28+
// NewMigration creates a new migration with the given version and name
29+
func NewMigration(version, name string, upFn, downFn func() *schema.Schema) *Migration {
30+
return &Migration{
31+
Version: version,
32+
Name: name,
33+
CreatedAt: time.Now(),
34+
UpFn: upFn,
35+
DownFn: downFn,
36+
}
37+
}
38+
39+
// Up returns the schema for migrating up
40+
func (m *Migration) Up() *schema.Schema {
41+
if m.UpFn != nil {
42+
return m.UpFn()
43+
}
44+
return schema.NewSchema()
45+
}
46+
47+
// Down returns the schema for migrating down (rolling back)
48+
func (m *Migration) Down() *schema.Schema {
49+
if m.DownFn != nil {
50+
return m.DownFn()
51+
}
52+
return schema.NewSchema()
53+
}
54+
55+
// FullVersion returns the combined version and name (e.g., "20250504120000_create_users")
56+
func (m *Migration) FullVersion() string {
57+
return m.Version + "_" + m.Name
58+
}

0 commit comments

Comments
 (0)