feat(bun): generate native Go array slices for PostgreSQL array columns
Bun's pgdialect scans/appends native slices directly, so array columns
(text[], integer[], uuid[], ...) always generate as plain []string,
[]int32, etc. with an explicit "array" bun tag, regardless of --types
(sqltypes/stdlib/baselib). The SqlXxxArray wrapper types are no longer
used for Bun array columns (gorm is unaffected and keeps using them).
Adds --array-nullable pointer_slice to represent nullable array columns
as *[]T instead of []T, so callers can distinguish SQL NULL (nil) from
'{}' (pointer to an empty slice). Verified end-to-end against a live
PostgreSQL instance for NULL/{}/populated arrays in every --types mode.
Closes #13
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MigrationsOption configures a Migrations instance.
|
||||
type MigrationsOption func(m *Migrations)
|
||||
|
||||
// WithMigrationsDirectory sets the directory where migration files are stored.
|
||||
func WithMigrationsDirectory(directory string) MigrationsOption {
|
||||
return func(m *Migrations) {
|
||||
m.explicitDirectory = directory
|
||||
}
|
||||
}
|
||||
|
||||
// Migrations is a collection of registered migrations.
|
||||
type Migrations struct {
|
||||
ms MigrationSlice
|
||||
|
||||
explicitDirectory string
|
||||
implicitDirectory string
|
||||
}
|
||||
|
||||
// NewMigrations creates a new collection of migrations.
|
||||
func NewMigrations(opts ...MigrationsOption) *Migrations {
|
||||
m := new(Migrations)
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
}
|
||||
m.implicitDirectory = filepath.Dir(migrationFile())
|
||||
return m
|
||||
}
|
||||
|
||||
// Sorted returns a copy of the migrations sorted by name in ascending order.
|
||||
func (m *Migrations) Sorted() MigrationSlice {
|
||||
migrations := make(MigrationSlice, len(m.ms))
|
||||
copy(migrations, m.ms)
|
||||
sortAsc(migrations)
|
||||
return migrations
|
||||
}
|
||||
|
||||
// MustRegister is like Register but panics on error.
|
||||
func (m *Migrations) MustRegister(up, down MigrationFunc) {
|
||||
if err := m.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register registers up and down migration functions derived from the caller's file name.
|
||||
func (m *Migrations) Register(up, down MigrationFunc) error {
|
||||
fpath := migrationFile()
|
||||
name, comment, err := extractMigrationName(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Add(Migration{
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
Up: wrapGoMigrationFunc(up),
|
||||
Down: wrapGoMigrationFunc(down),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add appends a migration to the collection.
|
||||
func (m *Migrations) Add(migration Migration) {
|
||||
if migration.Name == "" {
|
||||
panic("migration name is required")
|
||||
}
|
||||
m.ms = append(m.ms, migration)
|
||||
}
|
||||
|
||||
// DiscoverCaller discovers SQL migration files in the caller's directory.
|
||||
func (m *Migrations) DiscoverCaller() error {
|
||||
dir := filepath.Dir(migrationFile())
|
||||
return m.Discover(os.DirFS(dir))
|
||||
}
|
||||
|
||||
// Discover discovers SQL migration files in the given filesystem.
|
||||
func (m *Migrations) Discover(fsys fs.FS) error {
|
||||
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".up.sql") && !strings.HasSuffix(path, ".down.sql") {
|
||||
return nil
|
||||
}
|
||||
|
||||
name, comment, err := extractMigrationName(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migration := m.getOrCreateMigration(name)
|
||||
migration.Comment = comment
|
||||
migrationFunc := newSQLMigrationFunc(fsys, path)
|
||||
|
||||
if strings.HasSuffix(path, ".up.sql") {
|
||||
migration.Up = migrationFunc
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, ".down.sql") {
|
||||
migration.Down = migrationFunc
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("migrate: not reached")
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Migrations) getOrCreateMigration(name string) *Migration {
|
||||
for i := range m.ms {
|
||||
mig := &m.ms[i]
|
||||
if mig.Name == name {
|
||||
return mig
|
||||
}
|
||||
}
|
||||
|
||||
m.ms = append(m.ms, Migration{Name: name})
|
||||
return &m.ms[len(m.ms)-1]
|
||||
}
|
||||
|
||||
func (m *Migrations) getDirectory() string {
|
||||
if m.explicitDirectory != "" {
|
||||
return m.explicitDirectory
|
||||
}
|
||||
if m.implicitDirectory != "" {
|
||||
return m.implicitDirectory
|
||||
}
|
||||
return filepath.Dir(migrationFile())
|
||||
}
|
||||
|
||||
func migrationFile() string {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(1, pcs[:])
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
|
||||
for {
|
||||
f, ok := frames.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if !strings.Contains(f.Function, "/bun/migrate.") {
|
||||
return f.File
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var fnameRE = regexp.MustCompile(`^(\d{1,14})_([0-9a-z_\-]+)\.`)
|
||||
|
||||
func extractMigrationName(fpath string) (string, string, error) {
|
||||
fname := filepath.Base(fpath)
|
||||
|
||||
matches := fnameRE.FindStringSubmatch(fname)
|
||||
if matches == nil {
|
||||
return "", "", fmt.Errorf("migrate: unsupported migration name format: %q", fname)
|
||||
}
|
||||
|
||||
return matches[1], matches[2], nil
|
||||
}
|
||||
Reference in New Issue
Block a user