init
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) nikola@petrovv.com
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
87
README.md
Normal file
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# go-migrate
|
||||||
|
|
||||||
|
A PostgreSQL schema migration tool that generates and applies migrations by comparing your desired schema (SQL files) with the current database state.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Schema comparison**: Compare your desired schema against the live database
|
||||||
|
- **Migration generation**: Automatically generate SQL migration statements
|
||||||
|
- **Migration application**: Apply migrations directly to the database
|
||||||
|
- **Type support**: Handles tables, columns, indexes, foreign keys, enums, and constraints
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### As a Library
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.petrovv.com/go-migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Tool
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.petrovv.com/go-migrate/cmd/go-migrate@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Library
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.petrovv.com/go-migrate"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db, _ := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/db")
|
||||||
|
|
||||||
|
// Generate migrations
|
||||||
|
migrations, err := migrate.GetMigrations(ctx, db, "schema/")
|
||||||
|
for _, m := range migrations {
|
||||||
|
fmt.Println(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or use LoadDesiredSchema and CompareSchemas for more control
|
||||||
|
desired, _ := migrate.LoadDesiredSchema("schema/")
|
||||||
|
current, _ := migrate.GetCurrentSchema(ctx, db)
|
||||||
|
migrations = migrate.CompareSchemas(current, desired)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate migration SQL
|
||||||
|
go-migrate -cmd=generate -dir=schema
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
go-migrate -cmd=apply -dir=schema
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
- `-dir`: Directory containing schema SQL files (default: `schema`)
|
||||||
|
- `-cmd`: `generate` or `apply`
|
||||||
|
|
||||||
|
## Schema Files
|
||||||
|
|
||||||
|
Place your desired schema SQL files in a directory (e.g., `schema/`). Files are loaded alphabetically.
|
||||||
|
|
||||||
|
Example `schema/01_users.sql`:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_users_name ON users (name);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
For the CLI, database connection can be configured via environment variables or `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASS=password
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=mydb
|
||||||
|
```
|
||||||
105
cmd/go-migrate/main.go
Normal file
105
cmd/go-migrate/main.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.petrovv.com/nikola/go-migrate"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse flags
|
||||||
|
schemaDir := flag.String("dir", "schema", "Directory containing desired schema SQL files")
|
||||||
|
cmd := flag.String("cmd", "", "Command: generate or apply")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *cmd == "" {
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
fmt.Println(" go run main.go -cmd=generate -dir=schema - Compare schema and generate migrations")
|
||||||
|
fmt.Println(" go run main.go -cmd=apply -dir=schema - Apply pending migrations")
|
||||||
|
fmt.Println("\nFlags:")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *cmd {
|
||||||
|
case "generate":
|
||||||
|
generateMigrations(*schemaDir)
|
||||||
|
case "apply":
|
||||||
|
applyMigrations(*schemaDir)
|
||||||
|
default:
|
||||||
|
fmt.Println("Unknown command:", *cmd)
|
||||||
|
fmt.Println("Use 'generate' or 'apply'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMigrations(schemaDir string) {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
db := getDBConnection()
|
||||||
|
defer db.Close()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
migrations, err := migrate.GetMigrations(ctx, db, schemaDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(migrations) == 0 {
|
||||||
|
fmt.Println("Database schema is up to date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("-- Migration SQL")
|
||||||
|
for _, m := range migrations {
|
||||||
|
fmt.Println(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyMigrations(schemaDir string) {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
db := getDBConnection()
|
||||||
|
defer db.Close()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
migrations, err := migrate.GetMigrations(ctx, db, schemaDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(migrations) == 0 {
|
||||||
|
fmt.Println("Database schema is up to date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Applying migrations...")
|
||||||
|
for _, m := range migrations {
|
||||||
|
fmt.Printf("Executing: %s\n", m)
|
||||||
|
_, err := db.Exec(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to execute %s: %v", m, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("All migrations applied successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDBConnection() *pgxpool.Pool {
|
||||||
|
dbUser := os.Getenv("DB_USER")
|
||||||
|
dbPass := os.Getenv("DB_PASS")
|
||||||
|
dbHost := os.Getenv("DB_HOST")
|
||||||
|
dbPort := os.Getenv("DB_PORT")
|
||||||
|
dbName := os.Getenv("DB_NAME")
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbUser, dbPass, dbHost, dbPort, dbName)
|
||||||
|
|
||||||
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
}
|
||||||
17
go.mod
Normal file
17
go.mod
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module git.petrovv.com/nikola/go-migrate
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
30
go.sum
Normal file
30
go.sum
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
1006
migrate.go
Normal file
1006
migrate.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user