From 51e185459a1efec3fe751f9849a1a23aafffce4c Mon Sep 17 00:00:00 2001 From: Jannik Clausen <12862103+masseelch@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:32:25 +0200 Subject: [PATCH] dialect/sql/schema: add option to control symbol hashing logic for schema dump (#4411) --- cmd/internal/base/base.go | 9 +- dialect/sql/schema/atlas.go | 4 + dialect/sql/schema/schema.go | 48 +++++-- dialect/sql/schema/schema_test.go | 202 +++++++++++++++++------------- 4 files changed, 167 insertions(+), 96 deletions(-) diff --git a/cmd/internal/base/base.go b/cmd/internal/base/base.go index a9294fa66..8eff8b037 100644 --- a/cmd/internal/base/base.go +++ b/cmd/internal/base/base.go @@ -215,6 +215,7 @@ func SchemaCmd() *cobra.Command { cfg gen.Config dlct, version string features, buildTags []string + hashSymbols bool cmd = &cobra.Command{ Use: "schema [flags] path", Short: "dump the DDL for the schema directory", @@ -254,7 +255,12 @@ func SchemaCmd() *cobra.Command { if err != nil { log.Fatalln(err) } - ddl, err := schema.Dump(cmd.Context(), dlct, version, append(t, v...)) + ddl, err := schema.DDL(cmd.Context(), schema.DDLArgs{ + Dialect: dlct, + Version: version, + HashSymbols: hashSymbols, + Tables: append(t, v...), + }) if err != nil { log.Fatalln(err) } @@ -266,6 +272,7 @@ func SchemaCmd() *cobra.Command { cmd.Flags().StringVar(&version, "version", "", "database version to assume") cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features") cmd.Flags().StringSliceVarP(&buildTags, "build-tags", "", nil, "go build tags to use when loading the schema graph") + cmd.Flags().BoolVar(&hashSymbols, "hash-symbols", false, "whether to hash long symbols") cobra.CheckErr(cmd.MarkFlagRequired("dialect")) return cmd } diff --git a/dialect/sql/schema/atlas.go b/dialect/sql/schema/atlas.go index 7e7db8e69..d1704ca19 100644 --- a/dialect/sql/schema/atlas.go +++ b/dialect/sql/schema/atlas.go @@ -38,6 +38,7 @@ type Atlas struct { dropColumns bool // drop deleted columns dropIndexes bool // drop deleted indexes withForeignKeys bool // with foreign keys + hashSymbols bool // whether to use a hash for too long symbols, only for StateReader mode Mode hooks []Hook // hooks to apply before creation diffHooks []DiffHook // diff hooks to run when diffing current and desired @@ -528,6 +529,9 @@ func (a *Atlas) StateReader(tables ...*Table) migrate.StateReaderFunc { } a.sqlDialect = drv } + if a.hashSymbols { + a.setupTables(tables) + } return a.realm(tables) } } diff --git a/dialect/sql/schema/schema.go b/dialect/sql/schema/schema.go index e30e59a4f..5bd04245c 100644 --- a/dialect/sql/schema/schema.go +++ b/dialect/sql/schema/schema.go @@ -587,17 +587,45 @@ var drivers = func(v string) map[string]driver { } } +type DDLArgs struct { + // Dialect and Version of the target database. + Dialect, Version string + // HashSymbols indicates whether to hash long symbols in the DDL. + HashSymbols bool + // Tables to dump. + Tables []*Table + // Options to pass to the migration plan engine. + Options []migrate.PlanOption +} + // Dump the schema DDL for the given tables. +// +// Deprecated: use DDL instead. func Dump(ctx context.Context, dialect, version string, tables []*Table, opts ...migrate.PlanOption) (string, error) { - opts = append([]migrate.PlanOption{func(o *migrate.PlanOptions) { + return DDL(ctx, DDLArgs{ + Dialect: dialect, + Version: version, + Tables: tables, + Options: opts, + }) +} + +// DDL the schema DDL for the given tables. +func DDL(ctx context.Context, args DDLArgs) (string, error) { + args.Options = append([]migrate.PlanOption{func(o *migrate.PlanOptions) { o.Mode = migrate.PlanModeDump o.Indent = " " - }}, opts...) - d, ok := drivers(version)[dialect] + }}, args.Options...) + d, ok := drivers(args.Version)[args.Dialect] if !ok { - return "", fmt.Errorf("unsupported dialect %q", dialect) + return "", fmt.Errorf("unsupported dialect %q", args.Dialect) } - r, err := (&Atlas{sqlDialect: d, dialect: dialect}).StateReader(tables...).ReadState(ctx) + a := &Atlas{ + sqlDialect: d, + dialect: args.Dialect, + hashSymbols: args.HashSymbols, + } + r, err := a.StateReader(args.Tables...).ReadState(ctx) if err != nil { return "", err } @@ -609,7 +637,7 @@ func Dump(ctx context.Context, dialect, version string, tables []*Table, opts .. s.Views = nil } var c schema.Changes - if slices.ContainsFunc(tables, func(t *Table) bool { return t.Schema != "" }) { + if slices.ContainsFunc(args.Tables, func(t *Table) bool { return t.Schema != "" }) { c, err = d.RealmDiff(&schema.Realm{}, r) } else { c, err = d.SchemaDiff(&schema.Schema{}, r.Schemas[0]) @@ -617,17 +645,17 @@ func Dump(ctx context.Context, dialect, version string, tables []*Table, opts .. if err != nil { return "", err } - p, err := d.PlanChanges(ctx, "dump", c, opts...) + p, err := d.PlanChanges(ctx, "dump", c, args.Options...) if err != nil { return "", err } for _, v := range vs { - q, _ := sql.Dialect(dialect). + q, _ := sql.Dialect(args.Dialect). CreateView(v.Name). Schema(v.Schema.Name). Columns(func(cols []*schema.Column) (bs []*sql.ColumnBuilder) { for _, c := range cols { - bs = append(bs, sql.Dialect(dialect).Column(c.Name).Type(c.Type.Raw)) + bs = append(bs, sql.Dialect(args.Dialect).Column(c.Name).Type(c.Type.Raw)) } return }(v.Columns)...). @@ -638,7 +666,7 @@ func Dump(ctx context.Context, dialect, version string, tables []*Table, opts .. Comment: fmt.Sprintf("Add %q view", v.Name), }) } - for _, t := range tables { + for _, t := range args.Tables { p.Directives = append(p.Directives, fmt.Sprintf( "-- atlas:pos %s%s[type=%s] %s", func() string { diff --git a/dialect/sql/schema/schema_test.go b/dialect/sql/schema/schema_test.go index 76fce0073..fa640f4f5 100644 --- a/dialect/sql/schema/schema_test.go +++ b/dialect/sql/schema/schema_test.go @@ -6,6 +6,7 @@ package schema import ( "context" + "fmt" "strings" "testing" @@ -154,69 +155,77 @@ func TestCopyTables(t *testing.T) { require.Equal(t, tables, copyT) } -func TestDump(t *testing.T) { - users := &Table{ - Name: "users", - Pos: "users.go:15", - Columns: []*Column{ - {Name: "id", Type: field.TypeInt}, - {Name: "name", Type: field.TypeString}, - {Name: "spouse_id", Type: field.TypeInt}, - }, +func TestDDL(t *testing.T) { + const ( + hash = "249590215c5bc8be0106146e65d7fa7f" + idx = "super_duper_mega_ultra_hyper_giga_colossal_unbelievably_long_index_name" + hashMY = "super_duper_mega_ultra_hyper_gi_249590215c5bc8be0106146e65d7fa7f" + hashPG = "super_duper_mega_ultra_hyper_g_249590215c5bc8be0106146e65d7fa7f" + ) + tbls := func() []*Table { + users := &Table{ + Name: "users", + Pos: "users.go:15", + Columns: []*Column{ + {Name: "id", Type: field.TypeInt}, + {Name: "name", Type: field.TypeString}, + {Name: "spouse_id", Type: field.TypeInt}, + }, + } + users.PrimaryKey = users.Columns[:1] + users.Indexes = append(users.Indexes, &Index{ + Name: "name", + Columns: users.Columns[1:2], + }) + users.AddForeignKey(&ForeignKey{ + Columns: users.Columns[2:], + RefTable: users, + RefColumns: users.Columns[:1], + OnUpdate: SetDefault, + }) + users.SetAnnotation(&entsql.Annotation{Table: "Users"}) + pets := &Table{ + Name: "pets", + Pos: "pets.go:15", + Columns: []*Column{ + {Name: "id", Type: field.TypeInt}, + {Name: "name", Type: field.TypeString}, + {Name: "fur_color", Type: field.TypeEnum, Enums: []string{"black", "white"}}, + {Name: "owner_id", Type: field.TypeInt}, + }, + } + pets.Indexes = append(pets.Indexes, &Index{ + Name: idx, + Unique: true, + Columns: pets.Columns[1:2], + Annotation: entsql.Desc(), + }) + pets.AddForeignKey(&ForeignKey{ + Columns: pets.Columns[3:], + RefTable: users, + RefColumns: users.Columns[:1], + OnDelete: SetDefault, + }) + petsWithoutFur := &Table{ + Name: "pets_without_fur", + Pos: "pets.go:30", + View: true, + Columns: append(pets.Columns[:2], pets.Columns[3]), + Annotation: entsql.View("SELECT id, name, owner_id FROM pets"), + } + petNames := &Table{ + Name: "pet_names", + Pos: "pets.go:45", + View: true, + Columns: pets.Columns[1:1], + Annotation: entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) { + s.Select("name").From(sql.Table("pets")) + }), + } + return []*Table{users, pets, petsWithoutFur, petNames} } - users.PrimaryKey = users.Columns[:1] - users.Indexes = append(users.Indexes, &Index{ - Name: "name", - Columns: users.Columns[1:2], - }) - users.AddForeignKey(&ForeignKey{ - Columns: users.Columns[2:], - RefTable: users, - RefColumns: users.Columns[:1], - OnUpdate: SetDefault, - }) - users.SetAnnotation(&entsql.Annotation{Table: "Users"}) - pets := &Table{ - Name: "pets", - Pos: "pets.go:15", - Columns: []*Column{ - {Name: "id", Type: field.TypeInt}, - {Name: "name", Type: field.TypeString}, - {Name: "fur_color", Type: field.TypeEnum, Enums: []string{"black", "white"}}, - {Name: "owner_id", Type: field.TypeInt}, - }, - } - pets.Indexes = append(pets.Indexes, &Index{ - Name: "name", - Unique: true, - Columns: pets.Columns[1:2], - Annotation: entsql.Desc(), - }) - pets.AddForeignKey(&ForeignKey{ - Columns: pets.Columns[3:], - RefTable: users, - RefColumns: users.Columns[:1], - OnDelete: SetDefault, - }) - petsWithoutFur := &Table{ - Name: "pets_without_fur", - Pos: "pets.go:30", - View: true, - Columns: append(pets.Columns[:2], pets.Columns[3]), - Annotation: entsql.View("SELECT id, name, owner_id FROM pets"), - } - petNames := &Table{ - Name: "pet_names", - Pos: "pets.go:45", - View: true, - Columns: pets.Columns[1:1], - Annotation: entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) { - s.Select("name").From(sql.Table("pets")) - }), - } - tables = []*Table{users, pets, petsWithoutFur, petNames} - - my := strings.ReplaceAll(`-- Add new schema named "s1" + my := func(length int, idx string) string { + return fmt.Sprintf(strings.ReplaceAll(`-- Add new schema named "s1" CREATE DATABASE $s1$; -- Add new schema named "s2" CREATE DATABASE $s2$; @@ -225,7 +234,7 @@ CREATE DATABASE $s3$; -- Create "users" table CREATE TABLE $s1$.$users$ ( $id$ bigint NOT NULL, - $name$ varchar(255) NOT NULL, + $name$ varchar(%d) NOT NULL, $spouse_id$ bigint NOT NULL, PRIMARY KEY ($id$), INDEX $name$ ($name$), @@ -234,17 +243,19 @@ CREATE TABLE $s1$.$users$ ( -- Create "pets" table CREATE TABLE $s2$.$pets$ ( $id$ bigint NOT NULL, - $name$ varchar(255) NOT NULL, + $name$ varchar(%d) NOT NULL, $owner_id$ bigint NOT NULL, $owner_id$ bigint NOT NULL, - UNIQUE INDEX $name$ ($name$ DESC), + UNIQUE INDEX $%s$ ($name$ DESC), FOREIGN KEY ($owner_id$) REFERENCES $s1$.$users$ ($id$) ON DELETE SET DEFAULT ) CHARSET utf8mb4 COLLATE utf8mb4_bin; -- Add "pets_without_fur" view CREATE VIEW $s3$.$pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets; -`, "$", "`") +`, "$", "`"), length, length, idx) + } - pg := `-- Add new schema named "s1" + pg := func(idx string) string { + return fmt.Sprintf(`-- Add new schema named "s1" CREATE SCHEMA "s1"; -- Add new schema named "s2" CREATE SCHEMA "s2"; @@ -268,18 +279,22 @@ CREATE TABLE "s2"."pets" ( "owner_id" bigint NOT NULL, FOREIGN KEY ("owner_id") REFERENCES "s1"."users" ("id") ON DELETE SET DEFAULT ); --- Create index "name" to table: "pets" -CREATE UNIQUE INDEX "name" ON "s2"."pets" ("name" DESC); +-- Create index "%s" to table: "pets" +CREATE UNIQUE INDEX "%s" ON "s2"."pets" ("name" DESC); -- Add "pets_without_fur" view CREATE VIEW "s3"."pets_without_fur" ("id", "name", "owner_id") AS SELECT id, name, owner_id FROM pets; -- Add "pet_names" view CREATE VIEW "s3"."pet_names" AS SELECT "name" FROM "pets"; -` +`, idx, idx) + } - for _, tt := range []struct{ dialect, version, expected string }{ + for _, tt := range []struct { + dialect, version, expected string + hash bool + }{ { dialect.SQLite, "", - strings.ReplaceAll(`-- Create "users" table + fmt.Sprintf(strings.ReplaceAll(`-- Create "users" table CREATE TABLE $users$ ( $id$ integer NOT NULL, $name$ text NOT NULL, @@ -297,19 +312,20 @@ CREATE TABLE $pets$ ( $owner_id$ integer NOT NULL, FOREIGN KEY ($owner_id$) REFERENCES $users$ ($id$) ON DELETE SET DEFAULT ); --- Create index "name" to table: "pets" -CREATE UNIQUE INDEX $name$ ON $pets$ ($name$ DESC); +-- Create index "%s" to table: "pets" +CREATE UNIQUE INDEX $%s$ ON $pets$ ($name$ DESC); -- Add "pets_without_fur" view CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets; -`, "$", "`"), +`, "$", "`"), idx, idx), false, }, - {dialect.MySQL, "5.6", my}, - {dialect.MySQL, "5.7", my}, - {dialect.MySQL, "8", my}, - {dialect.Postgres, "12", pg}, - {dialect.Postgres, "13", pg}, - {dialect.Postgres, "14", pg}, - {dialect.Postgres, "15", pg}, + {dialect.MySQL, "5.6", my(255, idx), false}, + {dialect.MySQL, "5.6", my(191, hashMY), true}, + {dialect.MySQL, "5.7", my(255, idx), false}, + {dialect.MySQL, "8", my(255, hashMY), true}, + {dialect.Postgres, "12", pg(idx), false}, + {dialect.Postgres, "13", pg(idx), false}, + {dialect.Postgres, "14", pg(hashPG), true}, + {dialect.Postgres, "15", pg(hashPG), true}, } { n := tt.dialect if tt.version != "" { @@ -321,6 +337,7 @@ CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, ow -- atlas:pos pet_names[type=view] pets.go:45 ` + tables := tbls() if tt.dialect != dialect.SQLite { tables[0].Schema = "s1" tables[1].Schema = "s2" @@ -334,12 +351,22 @@ CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, ow ` } t.Run(n, func(t *testing.T) { - ac, err := Dump(context.Background(), tt.dialect, tt.version, tables) + ac, err := DDL(context.Background(), DDLArgs{ + Dialect: tt.dialect, + Version: tt.version, + Tables: tables, + HashSymbols: tt.hash, + }) require.NoError(t, err) require.Equal(t, pos+tt.expected, ac) }) t.Run(n+" single schema", func(t *testing.T) { - ac, err := Dump(context.Background(), tt.dialect, tt.version, tables[0:1]) + ac, err := DDL(context.Background(), DDLArgs{ + Dialect: tt.dialect, + Version: tt.version, + Tables: tables[0:1], + HashSymbols: tt.hash, + }) require.NoError(t, err) if tt.dialect != dialect.SQLite { require.Contains(t, ac, "s1[type=schema].") @@ -349,7 +376,12 @@ CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, ow }) t.Run(n+" no schema", func(t *testing.T) { tables[0].Schema = "" - ac, err := Dump(context.Background(), tt.dialect, tt.version, tables[0:1]) + ac, err := DDL(context.Background(), DDLArgs{ + Dialect: tt.dialect, + Version: tt.version, + Tables: tables[0:1], + HashSymbols: tt.hash, + }) require.NoError(t, err) require.NotContains(t, ac, "[type=schema].") require.Contains(t, ac, "[type=table]")