cmd/entfix: add utility binary entfix (#4306)

* cmd/entfix: add utility binary entfix

First command is 'entfix globalid' which will convert an existing ent_types table to the globalid ent feature.

* CR
This commit is contained in:
Jannik Clausen
2025-01-20 15:07:31 +01:00
committed by GitHub
parent f7fed7316b
commit 4478d3ccb3
5 changed files with 295 additions and 1 deletions

106
cmd/entfix/entfix.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/schema"
"entgo.io/ent/entc/gen"
"github.com/alecthomas/kong"
)
type (
// App configures the entfix CLI.
App struct {
GlobalID GlobalID `cmd:"" name:"globalid" help:"Migrate unique global id ent_types to ent global feature"`
}
// GlobalID represents the 'entfix globalid' command.
GlobalID struct {
Dialect string `name:"dialect" help:"Database dialect" required:"" enum:"mysql,postgres,sqlite3"`
DSN string `name:"dsn" help:"Data source name" required:""`
Path string `name:"path" help:"Path to the generated ent code" required:""`
}
)
func main() {
// Ensure to stop execution on Interrupt signal.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
app := kong.Parse(
new(App),
kong.BindTo(ctx, (*context.Context)(nil)),
kong.UsageOnError(),
)
app.FatalIfErrorf(func() error {
err := app.Run()
if err := context.Cause(ctx); err != nil {
return err
}
if errors.Is(err, context.Canceled) {
return nil
}
return err
}())
}
func (cmd *GlobalID) Run(ctx context.Context) error {
fmt.Print(`IMPORTANT INFORMATION
'entfix globalid' will convert the allocated id ranges for your nodes from the
database stored 'ent_types' table to the new static configuration on the ent
schema itself.
Please note, that the 'ent_types' table might differ between different environments
where your app is deployed. This is especially true if you are using
auto-migration instead of versioned migrations.
Please check, that all 'ent_types' tables for all deployments are equal!
Only 'yes' will be accepted to approve.
Enter a value: `)
switch c, err := bufio.NewReader(os.Stdin).ReadString('\n'); {
case err != nil:
return err
case strings.TrimSpace(c) != "yes":
fmt.Println("\nAborted.")
return nil
}
db, err := sql.Open(cmd.Dialect, cmd.DSN)
if err != nil {
return err
}
rows := &sql.Rows{}
query, args := sql.Select("type").
From(sql.Table(schema.TypeTable)).
OrderBy(sql.Asc("id")).
Query()
if err := db.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
var ts []string
if err := sql.ScanSlice(rows, &ts); err != nil {
return err
}
is := make(gen.IncrementStarts, len(ts))
for i, t := range ts {
is[t] = int64(i << 32)
}
if err := is.WriteToDisk(cmd.Path); err != nil {
return err
}
fmt.Println("\nSuccess! Please run code generation to complete the process.")
return nil
}

89
cmd/entfix/entfix_test.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"context"
"database/sql"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/schema"
"github.com/stretchr/testify/require"
_ "github.com/mattn/go-sqlite3"
)
func TestEntfix(t *testing.T) {
ctx := context.Background()
stdout, err := os.CreateTemp(t.TempDir(), "")
require.NoError(t, err)
oldStdout := os.Stdout
os.Stdout = stdout
t.Cleanup(func() {
os.Stdout = oldStdout
require.NoError(t, stdout.Close())
})
cli := &GlobalID{
Dialect: dialect.SQLite,
DSN: fmt.Sprintf("file:%s?mode=memory&cache=shared&_fk=1", t.Name()),
Path: t.TempDir(),
}
// Prints details, requires confirmation.
os.Stdin = mockStdin(t, "no\n")
require.NoError(t, cli.Run(ctx))
buf, err := os.ReadFile(stdout.Name())
require.NoError(t, err)
require.True(t, strings.HasPrefix(string(buf), "IMPORTANT INFORMATION\n\n"))
require.True(t, strings.HasSuffix(string(buf), "Aborted.\n"))
f, err := os.Open(cli.Path)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, f.Close()) })
_, err = f.Readdirnames(1)
require.ErrorIs(t, err, io.EOF)
// If approved, converts the 'ent_types' table.
db, err := sql.Open(cli.Dialect, cli.DSN)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, db.Close()) })
drv := entsql.OpenDB(dialect.SQLite, db)
m, err := schema.NewMigrate(drv, schema.WithGlobalUniqueID(true))
require.NoError(t, err)
require.NoError(t, m.Create(ctx))
_, err = db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (type) VALUES ('z'), ('y'), ('a'), ('b')", schema.TypeTable))
require.NoError(t, err)
os.Stdin = mockStdin(t, "yes\n")
require.NoError(t, cli.Run(ctx))
buf, err = os.ReadFile(stdout.Name())
require.NoError(t, err)
require.True(t, strings.HasSuffix(string(buf), "Success! Please run code generation to complete the process.\n"))
c, err := os.ReadFile(filepath.Join(cli.Path, "internal", "globalid.go"))
require.NoError(t, err)
require.Contains(t,
string(c),
fmt.Sprintf(`const IncrementStarts = "{\"a\":%d,\"b\":%d,\"y\":%d,\"z\":%d}"`, 2<<32, 3<<32, 1<<32, 0),
)
}
func mockStdin(t *testing.T, content string) *os.File {
t.Helper()
stdin, err := os.CreateTemp(t.TempDir(), "")
require.NoError(t, err)
_, err = stdin.WriteString(content)
require.NoError(t, err)
_, err = stdin.Seek(0, 0)
require.NoError(t, err)
oldStdin := os.Stdin
t.Cleanup(func() {
os.Stdin = oldStdin
require.NoError(t, stdin.Close())
})
return stdin
}

32
cmd/entfix/go.mod Normal file
View File

@@ -0,0 +1,32 @@
module entgo.io/entfix
go 1.23.4
require (
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6
github.com/alecthomas/kong v1.6.1
github.com/go-sql-driver/mysql v1.8.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
github.com/stretchr/testify v1.8.2
)
require (
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

67
cmd/entfix/go.sum Normal file
View File

@@ -0,0 +1,67 @@
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 h1:tqUmxmE7r2qqWmgczyNZNOyhzXLqmdUpRhVSuvCUqTU=
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1/go.mod h1:cpxrs2J9HTrjMrfNKFmUpONY9L0vzAHZ1pY++nTtVw0=
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6 h1:7H+NesJeDdcNtZxp0s5Ip/IYLkuUfoRFTIAC0fGcezg=
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6/go.mod h1:nPwpHTcyAjBMt+hT6q7Y/SdF41e6IJudQPbTj1P5nYY=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8=
github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
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/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=

View File

@@ -120,7 +120,7 @@ func (a *Atlas) NamedDiff(ctx context.Context, name string, tables ...*Table) er
if a.dir == nil {
return errors.New("no migration directory given")
}
opts := []migrate.PlannerOption{migrate.WithFormatter(a.fmt)}
opts := []migrate.PlannerOption{migrate.PlanFormat(a.fmt)}
// Validate the migration directory before proceeding.
if err := migrate.Validate(a.dir); err != nil {
return fmt.Errorf("validating migration directory: %w", err)