From 7017cbc898e8734920c5116a694606124eda7618 Mon Sep 17 00:00:00 2001 From: Jannik Clausen <12862103+masseelch@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:10:15 +0200 Subject: [PATCH] dialect/sql/schema: file based type store (#2644) * dialect/sql/schema: file based type store This PR adds support for a file based type storage when using versioned migrations. The file called `.ent_types` is written to the migration directory alongside the migration files and will be kept in sync for every migration file generation run. In order to not break existing code, where the type storage might differ for different deployment, global unique ID mut be enabled by using a new option. This will also be raised as an error to the user when attempting to use versioned migrations and global unique ID. Documentation will be added to this PR once feedback on the code is gathered. * apply CR * fix tests * change format of types file to exclude it from atlas.sum file * docs and drift test * apply CR --- dialect/sql/schema/atlas.go | 152 +++++++++++++++++++++-- dialect/sql/schema/atlas_test.go | 33 +++++ dialect/sql/schema/migrate.go | 59 ++++++--- dialect/sql/schema/migrate_test.go | 106 +++++++++++++++- dialect/sql/schema/mysql.go | 7 ++ dialect/sql/schema/postgres.go | 7 ++ dialect/sql/schema/sqlite.go | 7 ++ doc/md/migrate.md | 9 ++ doc/md/versioned-migrations.md | 45 +++++-- entc/integration/migrate/migrate_test.go | 17 ++- go.mod | 2 +- go.sum | 4 + 12 files changed, 411 insertions(+), 37 deletions(-) create mode 100644 dialect/sql/schema/atlas_test.go diff --git a/dialect/sql/schema/atlas.go b/dialect/sql/schema/atlas.go index ea1f123acf..838aa1a48e 100644 --- a/dialect/sql/schema/atlas.go +++ b/dialect/sql/schema/atlas.go @@ -9,6 +9,8 @@ import ( "database/sql" "errors" "fmt" + "io/fs" + "io/ioutil" "sort" "strings" @@ -271,16 +273,32 @@ func WithSumFile() MigrateOption { } } +// WithUniversalID instructs atlas to use a file based type store when +// global unique ids are enabled. For more information see the setupAtlas method on Migrate. +// +// ATTENTION: +// The file based PK range store is not backward compatible, since the allocated ranges were computed +// dynamically when computing the diff between a deployed database and the current schema. In cases where there +// exist multiple deployments, the allocated ranges for the same type might be different from each other, +// depending on when the deployment took part. +func WithUniversalID() MigrateOption { + return func(m *Migrate) { + m.universalID = true + m.atlas.typeStoreConsent = true + } +} + type ( // atlasOptions describes the options for atlas. atlasOptions struct { - enabled bool - diff []DiffHook - apply []ApplyHook - skip ChangeKind - dir migrate.Dir - fmt migrate.Formatter - genSum bool + enabled bool + diff []DiffHook + apply []ApplyHook + skip ChangeKind + dir migrate.Dir + fmt migrate.Formatter + genSum bool + typeStoreConsent bool } // atBuilder must be implemented by the different drivers in @@ -293,9 +311,12 @@ type ( atIncrementC(*schema.Table, *schema.Column) atIncrementT(*schema.Table, int64) atIndex(*Index, *schema.Table, *schema.Index) error + atTypeRangeSQL(t ...string) string } ) +var errConsent = errors.New("sql/schema: use WithUniversalID() instead of WithGlobalUniqueID(true) when using WithDir(): https://entgo.io/docs/migrate#universal-ids") + func (m *Migrate) setupAtlas() error { // Using one of the Atlas options, opt-in to Atlas migration. if !m.atlas.enabled && (m.atlas.skip != NoChange || len(m.atlas.diff) > 0 || len(m.atlas.apply) > 0) || m.atlas.dir != nil { @@ -326,6 +347,16 @@ func (m *Migrate) setupAtlas() error { if m.atlas.dir != nil && m.atlas.fmt == nil { m.atlas.fmt = sqltool.GolangMigrateFormatter } + if m.universalID && m.atlas.dir != nil { + // If global unique ids and a migration directory is given, enable the file based type store for pk ranges. + m.typeStore = &dirTypeStore{dir: m.atlas.dir} + // To guard the user against a possible bug due to backward incompatibility, the file based type store must + // be enabled by an option. For more information see the comment of WithUniversalID function. + if !m.atlas.typeStoreConsent { + return errConsent + } + m.atlas.diff = append(m.atlas.diff, m.ensureTypeTable) + } return nil } @@ -537,6 +568,54 @@ func (m *Migrate) aIndexes(b atBuilder, t1 *Table, t2 *schema.Table) error { return nil } +func (m *Migrate) ensureTypeTable(next Differ) Differ { + return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) { + // If there is a types table but no types file yet, the user most likely + // switched from online migration to migration files. + if len(m.dbTypeRanges) == 0 { + var ( + at = schema.NewTable(TypeTable) + et = NewTable(TypeTable). + AddPrimary(&Column{Name: "id", Type: field.TypeUint, Increment: true}). + AddColumn(&Column{Name: "type", Type: field.TypeString, Unique: true}) + ) + m.atTable(et, at) + if err := m.aColumns(m, et, at); err != nil { + return nil, err + } + if err := m.aIndexes(m, et, at); err != nil { + return nil, err + } + desired.Tables = append(desired.Tables, at) + } + // If there is a drift between the types stored in the database and the ones stored in the file, + // stop diffing, as this is potentially destructive. This will most likely happen on the first diffing + // after moving from online-migration to versioned migrations if the "old" ent types are not in sync with + // the deterministic ones computed by the new engine. + if len(m.dbTypeRanges) > 0 && len(m.fileTypeRanges) > 0 && !equal(m.fileTypeRanges, m.dbTypeRanges) { + return nil, fmt.Errorf( + "type allocation range drift detected: %v <> %v: see %s for more information", + m.dbTypeRanges, m.fileTypeRanges, + "https://entgo.io/docs/versioned-migrations#moving-from-auto-migration-to-versioned-migrations", + ) + } + changes, err := next.Diff(current, desired) + if err != nil { + return nil, err + } + if len(m.dbTypeRanges) > 0 && len(m.fileTypeRanges) == 0 { + // Override the types file created in the diff process with the "old" allocated types ranges. + if err := m.typeStore.(*dirTypeStore).save(m.dbTypeRanges); err != nil { + return nil, err + } + // Change the type range allocations since they will be added to the migration files when + // writing the migration plan to migration files. + m.typeRanges = m.dbTypeRanges + } + return changes, nil + }) +} + func setAtChecks(t1 *Table, t2 *schema.Table) { if check := t1.Annotation.Check; check != "" { t2.AddChecks(&schema.Check{ @@ -574,3 +653,62 @@ func descIndexes(idx *Index) map[string]bool { } return descs } + +const entTypes = ".ent_types" + +// dirTypeStore stores and read pk information from a text file stored alongside generated versioned migrations. +// This behaviour is enabled automatically when using versioned migrations. +type dirTypeStore struct { + dir migrate.Dir +} + +const atlasDirective = "atlas:sum ignore\n" + +// load the types from the types file. +func (s *dirTypeStore) load(context.Context, dialect.ExecQuerier) ([]string, error) { + f, err := s.dir.Open(entTypes) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("reading types file: %w", err) + } + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } + defer f.Close() + c, err := ioutil.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("reading types file: %w", err) + } + return strings.Split(strings.TrimPrefix(string(c), atlasDirective), ","), nil +} + +// add a new type entry to the types file. +func (s *dirTypeStore) add(ctx context.Context, conn dialect.ExecQuerier, t string) error { + ts, err := s.load(ctx, conn) + if err != nil { + return fmt.Errorf("adding type %q: %w", t, err) + } + return s.save(append(ts, t)) +} + +// save takes the given allocation range and writes them to the types file. +// The types file will be overridden. +func (s *dirTypeStore) save(ts []string) error { + if err := s.dir.WriteFile(entTypes, []byte(atlasDirective+strings.Join(ts, ","))); err != nil { + return fmt.Errorf("writing types file: %w", err) + } + return nil +} + +var _ typeStore = (*dirTypeStore)(nil) + +func equal(s1, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} diff --git a/dialect/sql/schema/atlas_test.go b/dialect/sql/schema/atlas_test.go new file mode 100644 index 0000000000..4d4b794294 --- /dev/null +++ b/dialect/sql/schema/atlas_test.go @@ -0,0 +1,33 @@ +// Copyright 2019-present Facebook Inc. All rights reserved. +// This source code is licensed under the Apache 2.0 license found +// in the LICENSE file in the root directory of this source tree. + +package schema + +import ( + "context" + "os" + "path/filepath" + "testing" + + "ariga.io/atlas/sql/migrate" + "github.com/stretchr/testify/require" +) + +func TestDirTypeStore(t *testing.T) { + ex := []string{"a", "b", "c"} + p := t.TempDir() + d, err := migrate.NewLocalDir(p) + require.NoError(t, err) + + s := &dirTypeStore{d} + require.NoError(t, s.save(ex)) + require.FileExists(t, filepath.Join(p, entTypes)) + c, err := os.ReadFile(filepath.Join(p, entTypes)) + require.NoError(t, err) + require.Contains(t, string(c), atlasDirective) + + ac, err := s.load(context.Background(), nil) + require.NoError(t, err) + require.Equal(t, ex, ac) +} diff --git a/dialect/sql/schema/migrate.go b/dialect/sql/schema/migrate.go index bac92df874..20447ea0e6 100644 --- a/dialect/sql/schema/migrate.go +++ b/dialect/sql/schema/migrate.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "math" + "strings" "ariga.io/atlas/sql/migrate" "entgo.io/ent/dialect" @@ -114,7 +115,9 @@ type Migrate struct { atlas *atlasOptions // migrate with atlas. typeRanges []string // types order by their range. hooks []Hook // hooks to apply before creation - typeStore typeStore + typeStore typeStore // the typeStore to read and save type ranges + fileTypeRanges []string // used internally by ensureTypeTable hook + dbTypeRanges []string // used internally by ensureTypeTable hook } // NewMigrate create a migration structure for the given SQL driver. @@ -163,18 +166,29 @@ func (m *Migrate) Create(ctx context.Context, tables ...*Table) error { return creator.Create(ctx, tables...) } -// Diff compares the state read from the StateReader with the state defined by Ent. -// Changes will be written to migration files by the configures Planner. +// Diff compares the state read from the connected database with the state defined by Ent. +// Changes will be written to migration files by the configured Planner. func (m *Migrate) Diff(ctx context.Context, tables ...*Table) error { return m.NamedDiff(ctx, "changes", tables...) } -// NamedDiff compares the state read from the StateReader with the state defined by Ent. -// Changes will be written to migration files by the configures Planner. +// NamedDiff compares the state read from the connected database with the state defined by Ent. +// Changes will be written to migration files by the configured Planner. func (m *Migrate) NamedDiff(ctx context.Context, name string, tables ...*Table) error { if m.atlas.dir == nil { return errors.New("no migration directory given") } + opts := []migrate.PlannerOption{ + migrate.WithFormatter(m.atlas.fmt), + } + if m.atlas.genSum { + // Validate the migration directory before proceeding. + if err := migrate.Validate(m.atlas.dir); err != nil { + return fmt.Errorf("validating migration directory: %w", err) + } + } else { + opts = append(opts, migrate.DisableChecksum()) + } if err := m.init(ctx, m); err != nil { return err } @@ -182,26 +196,39 @@ func (m *Migrate) NamedDiff(ctx context.Context, name string, tables ...*Table) if err := m.types(ctx, m); err != nil { return err } + m.fileTypeRanges = m.typeRanges + ex, err := m.tableExist(ctx, m, TypeTable) + if err != nil { + return err + } + if ex { + m.dbTypeRanges, err = (&dbTypeStore{m}).load(ctx, m) + if err != nil { + return err + } + } + defer func() { + m.fileTypeRanges = nil + m.dbTypeRanges = nil + }() } plan, err := m.atDiff(ctx, m, name, tables...) if err != nil { return err } + if m.universalID { + newTypes := m.typeRanges[len(m.dbTypeRanges):] + if len(newTypes) > 0 { + plan.Changes = append(plan.Changes, &migrate.Change{ + Cmd: m.atTypeRangeSQL(newTypes...), + Comment: fmt.Sprintf("add pk ranges for %s tables", strings.Join(newTypes, ",")), + }) + } + } // Skip if the plan has no changes. if len(plan.Changes) == 0 { return nil } - opts := []migrate.PlannerOption{ - migrate.WithFormatter(m.atlas.fmt), - } - if m.atlas.genSum { - // Validate the migration directory before proceeding. - if err := migrate.Validate(m.atlas.dir); err != nil { - return fmt.Errorf("validating migration directory: %w", err) - } - } else { - opts = append(opts, migrate.DisableChecksum()) - } return migrate.NewPlanner(nil, m.atlas.dir, opts...).WritePlan(plan) } diff --git a/dialect/sql/schema/migrate_test.go b/dialect/sql/schema/migrate_test.go index 2862069b7b..365e8b31e7 100644 --- a/dialect/sql/schema/migrate_test.go +++ b/dialect/sql/schema/migrate_test.go @@ -6,13 +6,17 @@ package schema import ( "context" + "fmt" "os" "path/filepath" + "strings" "testing" + "text/template" "time" "ariga.io/atlas/sql/migrate" "ariga.io/atlas/sql/schema" + "entgo.io/ent/schema/field" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" @@ -80,7 +84,7 @@ func TestMigrate_Diff(t *testing.T) { m, err := NewMigrate(db, WithDir(d)) require.NoError(t, err) require.NoError(t, m.Diff(context.Background(), &Table{Name: "users"})) - v := time.Now().Format("20060102150405") + v := time.Now().UTC().Format("20060102150405") requireFileEqual(t, filepath.Join(p, v+"_changes.up.sql"), "-- create \"users\" table\nCREATE TABLE `users` (, PRIMARY KEY ());\n") requireFileEqual(t, filepath.Join(p, v+"_changes.down.sql"), "-- reverse: create \"users\" table\nDROP TABLE `users`;\n") require.NoFileExists(t, filepath.Join(p, "atlas.sum")) @@ -97,6 +101,106 @@ func TestMigrate_Diff(t *testing.T) { require.FileExists(t, filepath.Join(p, "atlas.sum")) require.NoError(t, d.WriteFile("tmp.sql", nil)) require.ErrorIs(t, m.Diff(context.Background(), &Table{Name: "users"}), migrate.ErrChecksumMismatch) + + // Test type store. + idCol := []*Column{{Name: "id", Type: field.TypeInt, Increment: true}} + p = t.TempDir() + d, err = migrate.NewLocalDir(p) + require.NoError(t, err) + f, err := migrate.NewTemplateFormatter( + template.Must(template.New("").Parse("{{ .Name }}.sql")), + template.Must(template.New("").Parse( + `{{ range .Changes }}{{ printf "%s;\n" .Cmd }}{{ end }}`, + )), + ) + require.NoError(t, err) + + // If using global unique ID and versioned migrations, + // consent for the file based type store has to be given explicitly. + _, err = NewMigrate(db, WithDir(d), WithGlobalUniqueID(true)) + require.ErrorIs(t, err, errConsent) + require.Contains(t, err.Error(), "WithUniversalID") + require.Contains(t, err.Error(), "WithGlobalUniqueID") + require.Contains(t, err.Error(), "WithDir") + + m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithUniversalID(), WithSumFile()) + require.NoError(t, err) + require.IsType(t, &dirTypeStore{}, m.typeStore) + require.NoError(t, m.Diff(context.Background(), + &Table{Name: "users", Columns: idCol, PrimaryKey: idCol}, + &Table{Name: "groups", Columns: idCol, PrimaryKey: idCol}, + )) + requireFileEqual(t, filepath.Join(p, ".ent_types"), atlasDirective+"users,groups") + changesSQL := strings.Join([]string{ + "CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT);", + "CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT);", + fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"groups\", %d);", 1<<32), + "CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);", + "CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);", + "INSERT INTO `ent_types` (`type`) VALUES ('users'), ('groups');", "", + }, "\n") + requireFileEqual(t, filepath.Join(p, "changes.sql"), changesSQL) + + // types file cannot be part of the sum file. + require.FileExists(t, filepath.Join(p, "atlas.sum")) + sum, err := os.ReadFile(filepath.Join(p, "atlas.sum")) + require.NoError(t, err) + require.NotContains(t, string(sum), ".ent_types") + + // Adding another node will result in a new entry to the TypeTable (without actually creating it). + _, err = db.ExecContext(context.Background(), changesSQL, nil, nil) + require.NoError(t, err) + require.NoError(t, m.NamedDiff(context.Background(), "changes_2", &Table{Name: "pets", Columns: idCol, PrimaryKey: idCol})) + requireFileEqual(t, filepath.Join(p, ".ent_types"), atlasDirective+"users,groups,pets") + requireFileEqual(t, + filepath.Join(p, "changes_2.sql"), strings.Join([]string{ + "CREATE TABLE `pets` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT);", + fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"pets\", %d);", 2<<32), + "INSERT INTO `ent_types` (`type`) VALUES ('pets');", "", + }, "\n")) + + // types file cannot be part of the sum file. + require.FileExists(t, filepath.Join(p, "atlas.sum")) + sum, err = os.ReadFile(filepath.Join(p, "atlas.sum")) + require.NoError(t, err) + require.NotContains(t, string(sum), ".ent_types") + + // Checksum will be updated as well. + require.NoError(t, migrate.Validate(d)) + + // Running diff against an existing database without having a types file yet + // will result in the types file respect the "old" order of pk allocations. + switchAllocs := func(one, two string) { + for _, stmt := range []string{ + "DELETE FROM `ent_types`;", + fmt.Sprintf("INSERT INTO `ent_types` (`type`) VALUES ('%s'), ('%s');", one, two), + } { + _, err = db.ExecContext(context.Background(), stmt) + require.NoError(t, err) + } + } + switchAllocs("groups", "users") + p = t.TempDir() + d, err = migrate.NewLocalDir(p) + require.NoError(t, err) + m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithUniversalID()) + require.NoError(t, err) + + require.NoError(t, m.Diff(context.Background(), + &Table{Name: "users", Columns: idCol, PrimaryKey: idCol}, + &Table{Name: "groups", Columns: idCol, PrimaryKey: idCol}, + )) + requireFileEqual(t, filepath.Join(p, ".ent_types"), atlasDirective+"groups,users") + require.NoFileExists(t, filepath.Join(p, "changes.sql")) + + // Drifts in the types file and types database will be detected, + switchAllocs("users", "groups") + require.ErrorContains(t, m.Diff(context.Background()), fmt.Sprintf( + "type allocation range drift detected: %v <> %v: see %s for more information", + []string{"users", "groups"}, + []string{"groups", "users"}, + "https://entgo.io/docs/versioned-migrations#moving-from-auto-migration-to-versioned-migrations", + )) } func requireFileEqual(t *testing.T, name, contents string) { diff --git a/dialect/sql/schema/mysql.go b/dialect/sql/schema/mysql.go index f65236b440..fddf1e2da8 100644 --- a/dialect/sql/schema/mysql.go +++ b/dialect/sql/schema/mysql.go @@ -970,3 +970,10 @@ func indexType(idx *Index, d string) (string, bool) { } return "", false } + +func (MySQL) atTypeRangeSQL(ts ...string) string { + for i := range ts { + ts[i] = fmt.Sprintf("('%s')", ts[i]) + } + return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", ")) +} diff --git a/dialect/sql/schema/postgres.go b/dialect/sql/schema/postgres.go index 4a7a92d391..15c6c9b715 100644 --- a/dialect/sql/schema/postgres.go +++ b/dialect/sql/schema/postgres.go @@ -784,3 +784,10 @@ func (d *Postgres) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) er } return nil } + +func (Postgres) atTypeRangeSQL(ts ...string) string { + for i := range ts { + ts[i] = fmt.Sprintf("('%s')", ts[i]) + } + return fmt.Sprintf(`INSERT INTO "%s" ("type") VALUES %s`, TypeTable, strings.Join(ts, ", ")) +} diff --git a/dialect/sql/schema/sqlite.go b/dialect/sql/schema/sqlite.go index ce46631bd4..d97eb48ef5 100644 --- a/dialect/sql/schema/sqlite.go +++ b/dialect/sql/schema/sqlite.go @@ -449,3 +449,10 @@ func (d *SQLite) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) erro } return nil } + +func (SQLite) atTypeRangeSQL(ts ...string) string { + for i := range ts { + ts[i] = fmt.Sprintf("('%s')", ts[i]) + } + return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", ")) +} diff --git a/doc/md/migrate.md b/doc/md/migrate.md index 093543a663..7370bab03d 100755 --- a/doc/md/migrate.md +++ b/doc/md/migrate.md @@ -79,6 +79,15 @@ the object ID to be unique. To enable the Universal-IDs support for your project, pass the `WithGlobalUniqueID` option to the migration. +:::note +Be aware, that `WithGlobalUniqueID` and [versioned migration](versioned-migrations.md) files are not working together. +When using `WithGlobalUniqueID`, the allocated ranges are computed dynamically when creating the diff between a deployed +database and the current schema. In cases where multiple deployments exist, the allocated ranges for the same type might +be different from each other, depending on when the deployment took part. If you only have one deployment or all +deployments have the exact same data in the `ent_types` table, you can use `WithUniversalID` instead. This will enable a +file based type range store instead of a database-table as source of truth. +::: + ```go package main diff --git a/doc/md/versioned-migrations.md b/doc/md/versioned-migrations.md index d4a32a25eb..d2c79b8189 100644 --- a/doc/md/versioned-migrations.md +++ b/doc/md/versioned-migrations.md @@ -163,16 +163,47 @@ migrate -source file://migrations -database mysql://root:pass@tcp(localhost:3306 In case you already have an Ent application in production and want to switch over from auto migration to the new versioned migration, you need to take some extra steps. -1. Create an initial migration file (or several files if you want) reflecting the currently deployed state. +### Create an initial migration file reflecting the currently deployed state - To do this make sure your schema definition is in sync with your deployed version. Then spin up an empty database and - run the diff command once as described above. This will create the statements needed to create the current state of - your schema graph. +To do this make sure your schema definition is in sync with your deployed version. Then spin up an empty database and +run the diff command once as described above. This will create the statements needed to create the current state of +your schema graph. +If you happened to have [universal IDs](migrate.md#universal-ids) enabled before, the above command will create a +file called `.ent_types` containing a list of schema names similar to the following: -2. Configure the tool you use to manage migrations to consider this file as **applied**. +```text title=".ent_types" +atlas.sum ignore +users,groups +``` +Once that has been created, one of the migration files will contain statements to create a table called +`ent_types`, as well as some inserts to it: + +```sql +CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT); +CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT); +INSERT INTO sqlite_sequence (name, seq) VALUES ("groups", 4294967296); +CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL); +CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`); +INSERT INTO `ent_types` (`type`) VALUES ('users'), ('groups'); +``` + +In order to ensure to not break existing code, make sure the contents of that file are equal to the contents in the +table present in the database you created the diff from. For example, if you consider the `.ent_types` file from +above (`users,groups`) but your deployed table looks like the one below (`groups,users`): + +| id | type | +|-----|--------| +| 1 | groups | +| 2 | users | + +You can see, that the order differs. In that case, you have to manually change both the entries in the +`.ent_types` file, as well in the generated migrations file. As a safety feature, Ent will warn you about type +drifts if you attempt to run a migration diff. + +### Configure the tool you use to manage migrations to consider this file as applied - In case of `golang-migrate` this can be done by forcing your database version as - described [here](https://github.com/golang-migrate/migrate/blob/master/GETTING_STARTED.md#forcing-your-database-version). +In case of `golang-migrate` this can be done by forcing your database version as +described [here](https://github.com/golang-migrate/migrate/blob/master/GETTING_STARTED.md#forcing-your-database-version). ## Use a Custom Formatter diff --git a/entc/integration/migrate/migrate_test.go b/entc/integration/migrate/migrate_test.go index 0e95ce92d6..31889fb533 100644 --- a/entc/integration/migrate/migrate_test.go +++ b/entc/integration/migrate/migrate_test.go @@ -67,7 +67,10 @@ func TestMySQL(t *testing.T) { require.NoError(t, err, root.Exec(ctx, "DROP DATABASE IF EXISTS versioned_migrate", []interface{}{}, new(sql.Result))) require.NoError(t, root.Exec(ctx, "CREATE DATABASE IF NOT EXISTS versioned_migrate", []interface{}{}, new(sql.Result))) defer root.Exec(ctx, "DROP DATABASE IF EXISTS versioned_migrate", []interface{}{}, new(sql.Result)) - Versioned(t, drv, versioned.NewClient(versioned.Driver(drv))) + vdrv, err := sql.Open("mysql", fmt.Sprintf("root:pass@tcp(localhost:%d)/versioned_migrate?parseTime=True", port)) + require.NoError(t, err, "connecting to versioned migrate database") + defer vdrv.Close() + Versioned(t, vdrv, versioned.NewClient(versioned.Driver(vdrv))) }) } } @@ -228,11 +231,15 @@ func Versioned(t *testing.T, drv sql.ExecQuerier, client *versioned.Client) { template.Must(template.New("name").Parse(`{{ range .Changes }}{{ printf "%s;\n" .Cmd }}{{ end }}`)), ) require.NoError(t, err) - opts := []schema.MigrateOption{schema.WithDir(dir), schema.WithGlobalUniqueID(true), schema.WithFormatter(format)} + opts := []schema.MigrateOption{ + schema.WithDir(dir), + schema.WithUniversalID(), + schema.WithFormatter(format), + } // Compared to empty database. require.NoError(t, client.Schema.NamedDiff(ctx, "first", opts...)) - require.Equal(t, 1, countFiles(t, dir)) + require.Equal(t, 2, countFiles(t, dir)) // Apply the migrations. fs, err := dir.Files() @@ -242,14 +249,14 @@ func Versioned(t *testing.T, drv sql.ExecQuerier, client *versioned.Client) { for sc.Scan() { if sc.Text() != "" { _, err := drv.ExecContext(ctx, sc.Text()) - require.NoError(t, err) + require.NoError(t, err, sc.Text()) } } } // Diff again - there should not be a new file. require.NoError(t, client.Schema.NamedDiff(ctx, "second", opts...)) - require.Equal(t, 1, countFiles(t, dir)) + require.Equal(t, 2, countFiles(t, dir)) } func V1ToV2(t *testing.T, dialect string, clientv1 *entv1.Client, clientv2 *entv2.Client) { diff --git a/go.mod b/go.mod index 8b6d77504f..6a405cc461 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module entgo.io/ent go 1.17 require ( - ariga.io/atlas v0.4.2-0.20220601084524-93e29909973c + ariga.io/atlas v0.4.3-0.20220615084302-e90a7f969cbd github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/go-openapi/inflect v0.19.0 github.com/go-sql-driver/mysql v1.6.0 diff --git a/go.sum b/go.sum index 65fafde249..6c457af6fb 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ ariga.io/atlas v0.4.2-0.20220524161107-b5b3f75b1034 h1:cjqJPtwk6LhJdYaFgLHI0OuZy ariga.io/atlas v0.4.2-0.20220524161107-b5b3f75b1034/go.mod h1:CKqqlJeTdRfEmnHaCcNPHg8DD6GPx1TxNPZ1NFknKHU= ariga.io/atlas v0.4.2-0.20220601084524-93e29909973c h1:xCquXJslTtTyp4bG9sH5XHAauvE5lgluRdeo65IRduo= ariga.io/atlas v0.4.2-0.20220601084524-93e29909973c/go.mod h1:CKqqlJeTdRfEmnHaCcNPHg8DD6GPx1TxNPZ1NFknKHU= +ariga.io/atlas v0.4.3-0.20220610112612-85980c775013 h1:LBT0bq4tu/btdF/x2V0aoV421yPnXm2rSorJdt4vlYE= +ariga.io/atlas v0.4.3-0.20220610112612-85980c775013/go.mod h1:0eSMrFg/pqF2qZ+bjM3yRvlzKROYaLbKbSVaU0RKr5g= +ariga.io/atlas v0.4.3-0.20220615084302-e90a7f969cbd h1:S/RPkFUkVqIH3abJWE5tP3QyqPjat/bAzXSdT7lZokU= +ariga.io/atlas v0.4.3-0.20220615084302-e90a7f969cbd/go.mod h1:10hc2LWRcvk2SFtdakSD6YKm5wp9DmzcOb6aepwYiG4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=