diff --git a/dialect/pgdialect/inspector.go b/dialect/pgdialect/inspector.go index 67fbe96d8..d4061a487 100644 --- a/dialect/pgdialect/inspector.go +++ b/dialect/pgdialect/inspector.go @@ -6,7 +6,7 @@ import ( "github.com/uptrace/bun" "github.com/uptrace/bun/migrate/sqlschema" - "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type ( @@ -32,7 +32,7 @@ func newInspector(db *bun.DB, excludeTables ...string) *Inspector { func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { dbSchema := Schema{ - Tables: make(map[schema.FQN]sqlschema.Table), + Tables: orderedmap.New[string, sqlschema.Table](), ForeignKeys: make(map[sqlschema.ForeignKey]string), } @@ -59,7 +59,7 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { return dbSchema, err } - colDefs := make(map[string]sqlschema.Column) + colDefs := orderedmap.New[string, sqlschema.Column]() uniqueGroups := make(map[string][]string) for _, c := range columns { @@ -70,7 +70,7 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { def = strings.ToLower(def) } - colDefs[c.Name] = &Column{ + colDefs.Set(c.Name, &Column{ Name: c.Name, SQLType: c.DataType, VarcharLen: c.VarcharLen, @@ -78,7 +78,7 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { IsNullable: c.IsNullable, IsAutoIncrement: c.IsSerial, IsIdentity: c.IsIdentity, - } + }) for _, group := range c.UniqueGroups { uniqueGroups[group] = append(uniqueGroups[group], c.Name) @@ -101,14 +101,13 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { } } - fqn := schema.FQN{Schema: table.Schema, Table: table.Name} - dbSchema.Tables[fqn] = &Table{ + dbSchema.Tables.Set(table.Name, &Table{ Schema: table.Schema, Name: table.Name, Columns: colDefs, PrimaryKey: pk, UniqueConstraints: unique, - } + }) } for _, fk := range fks { diff --git a/go.mod b/go.mod index 2359d474e..bfdaff905 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/uptrace/bun -go 1.22 +go 1.23 + +toolchain go1.23.2 require ( github.com/jinzhu/inflection v1.0.0 @@ -9,10 +11,14 @@ require ( github.com/stretchr/testify v1.8.1 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect diff --git a/go.sum b/go.sum index b35837f9f..c08e09e39 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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= @@ -5,9 +9,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -37,6 +44,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/dbtest/go.mod b/internal/dbtest/go.mod index f205f3fd2..ebe262e80 100644 --- a/internal/dbtest/go.mod +++ b/internal/dbtest/go.mod @@ -1,8 +1,8 @@ module github.com/uptrace/bun/internal/dbtest -go 1.22.0 +go 1.23 -toolchain go1.22.6 +toolchain go1.23.2 replace github.com/uptrace/bun => ../.. @@ -42,6 +42,8 @@ require ( ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect @@ -56,6 +58,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgtype v1.7.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect @@ -67,6 +70,7 @@ require ( github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/mod v0.21.0 // indirect diff --git a/internal/dbtest/go.sum b/internal/dbtest/go.sum index 5dfed193e..7983053d0 100644 --- a/internal/dbtest/go.sum +++ b/internal/dbtest/go.sum @@ -24,6 +24,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -32,6 +34,8 @@ github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnL github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk= github.com/brianvoe/gofakeit/v6 v6.4.1 h1:u4lPnxVNr648hEyoIz31A8zrQl5woUQbCgqjAj/n/Y4= github.com/brianvoe/gofakeit/v6 v6.4.1/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -202,6 +206,7 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -226,6 +231,8 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -371,6 +378,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/dbtest/inspect_test.go b/internal/dbtest/inspect_test.go index 9f60af30e..7cb0ea8ec 100644 --- a/internal/dbtest/inspect_test.go +++ b/internal/dbtest/inspect_test.go @@ -12,6 +12,7 @@ import ( "github.com/uptrace/bun/dialect/sqltype" "github.com/uptrace/bun/migrate/sqlschema" "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type Article struct { @@ -93,144 +94,219 @@ func TestDatabaseInspector_Inspect(t *testing.T) { defaultSchema := db.Dialect().DefaultSchema() // Tables come sorted alphabetically by schema and table. - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: "admin", Table: "offices"}: &sqlschema.BaseTable{ - Schema: "admin", - Name: "offices", - Columns: map[string]sqlschema.Column{ - "office_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "publisher_id": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "publisher_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "offices", + Value: &sqlschema.BaseTable{ + Schema: "admin", + Name: "offices", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "office_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("office_name")}, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("office_name")}, }, - {Schema: defaultSchema, Table: "articles"}: &sqlschema.BaseTable{ - Schema: defaultSchema, - Name: "articles", - Columns: map[string]sqlschema.Column{ - "isbn": &sqlschema.BaseColumn{ - SQLType: "bigint", - IsNullable: false, - IsAutoIncrement: false, - IsIdentity: true, - DefaultValue: "", - }, - "editor": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - IsAutoIncrement: false, - IsIdentity: false, - DefaultValue: "john doe", - }, - "title": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - IsAutoIncrement: false, - IsIdentity: false, - DefaultValue: "", - }, - "locale": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - VarcharLen: 5, - IsNullable: true, - IsAutoIncrement: false, - IsIdentity: false, - DefaultValue: "en-GB", - }, - "page_count": &sqlschema.BaseColumn{ - SQLType: "smallint", - IsNullable: false, - IsAutoIncrement: false, - IsIdentity: false, - DefaultValue: "1", - }, - "book_count": &sqlschema.BaseColumn{ - SQLType: "integer", - IsNullable: false, - IsAutoIncrement: true, - IsIdentity: false, - DefaultValue: "", - }, - "publisher_id": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "author_id": &sqlschema.BaseColumn{ - SQLType: "bigint", + orderedmap.Pair[string, sqlschema.Table]{ + Key: "articles", + Value: &sqlschema.BaseTable{ + Schema: defaultSchema, + Name: "articles", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "isbn", + Value: &sqlschema.BaseColumn{ + SQLType: "bigint", + IsNullable: false, + IsAutoIncrement: false, + IsIdentity: true, + DefaultValue: "", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "editor", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + IsAutoIncrement: false, + IsIdentity: false, + DefaultValue: "john doe", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "title", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + IsAutoIncrement: false, + IsIdentity: false, + DefaultValue: "", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "locale", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + VarcharLen: 5, + IsNullable: true, + IsAutoIncrement: false, + IsIdentity: false, + DefaultValue: "en-GB", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "page_count", + Value: &sqlschema.BaseColumn{ + SQLType: "smallint", + IsNullable: false, + IsAutoIncrement: false, + IsIdentity: false, + DefaultValue: "1", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "book_count", + Value: &sqlschema.BaseColumn{ + SQLType: "integer", + IsNullable: false, + IsAutoIncrement: true, + IsIdentity: false, + DefaultValue: "", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "author_id", + Value: &sqlschema.BaseColumn{ + SQLType: "bigint", + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("isbn")}, + UniqueConstraints: []sqlschema.Unique{ + {Columns: sqlschema.NewColumns("editor", "title")}, }, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("isbn")}, - UniqueConstraints: []sqlschema.Unique{ - {Columns: sqlschema.NewColumns("editor", "title")}, - }, }, - {Schema: defaultSchema, Table: "authors"}: &sqlschema.BaseTable{ - Schema: defaultSchema, - Name: "authors", - Columns: map[string]sqlschema.Column{ - "author_id": &sqlschema.BaseColumn{ - SQLType: "bigint", - IsIdentity: true, - }, - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "email": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, + orderedmap.Pair[string, sqlschema.Table]{ + Key: "authors", + Value: &sqlschema.BaseTable{ + Schema: defaultSchema, + Name: "authors", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "author_id", + Value: &sqlschema.BaseColumn{ + SQLType: "bigint", + IsIdentity: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "email", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("author_id")}, + UniqueConstraints: []sqlschema.Unique{ + {Columns: sqlschema.NewColumns("first_name", "last_name")}, + {Columns: sqlschema.NewColumns("email")}, }, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("author_id")}, - UniqueConstraints: []sqlschema.Unique{ - {Columns: sqlschema.NewColumns("first_name", "last_name")}, - {Columns: sqlschema.NewColumns("email")}, - }, }, - {Schema: defaultSchema, Table: "publisher_to_journalists"}: &sqlschema.BaseTable{ - Schema: defaultSchema, - Name: "publisher_to_journalists", - Columns: map[string]sqlschema.Column{ - "publisher_id": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "author_id": &sqlschema.BaseColumn{ - SQLType: "bigint", - }, + orderedmap.Pair[string, sqlschema.Table]{ + Key: "publisher_to_journalists", + Value: &sqlschema.BaseTable{ + Schema: defaultSchema, + Name: "publisher_to_journalists", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "author_id", + Value: &sqlschema.BaseColumn{ + SQLType: "bigint", + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("publisher_id", "author_id")}, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("publisher_id", "author_id")}, }, - {Schema: defaultSchema, Table: "publishers"}: &sqlschema.BaseTable{ - Schema: defaultSchema, - Name: "publishers", - Columns: map[string]sqlschema.Column{ - "publisher_id": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - DefaultValue: "gen_random_uuid()", - }, - "publisher_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - }, - "created_at": &sqlschema.BaseColumn{ - SQLType: "timestamp", - DefaultValue: "current_timestamp", - IsNullable: true, + orderedmap.Pair[string, sqlschema.Table]{ + Key: "publishers", + Value: &sqlschema.BaseTable{ + Schema: defaultSchema, + Name: "publishers", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + DefaultValue: "gen_random_uuid()", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "publisher_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "created_at", + Value: &sqlschema.BaseColumn{ + SQLType: "timestamp", + DefaultValue: "current_timestamp", + IsNullable: true, + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("publisher_id")}, + UniqueConstraints: []sqlschema.Unique{ + {Columns: sqlschema.NewColumns("publisher_id", "publisher_name")}, }, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("publisher_id")}, - UniqueConstraints: []sqlschema.Unique{ - {Columns: sqlschema.NewColumns("publisher_id", "publisher_name")}, - }, }, - } + )) wantFKs := []sqlschema.ForeignKey{ { @@ -294,25 +370,20 @@ func mustCreateSchema(tb testing.TB, ctx context.Context, db *bun.DB, schema str // cmpTables compares table schemas using dialect-specific equivalence checks for column types // and reports the differences as t.Error(). func cmpTables( - tb testing.TB, d sqlschema.InspectorDialect, want, got map[schema.FQN]sqlschema.Table, + tb testing.TB, + d sqlschema.InspectorDialect, + want, got *orderedmap.OrderedMap[string, sqlschema.Table], ) { tb.Helper() require.ElementsMatch(tb, tableNames(want), tableNames(got), "different set of tables") // Now we are guaranteed to have the same tables. - for _, wantTable := range want { - // TODO(dyma): this will be simplified by map[string]Table - var gt sqlschema.Table - for i := range got { - if got[i].GetName() == wantTable.GetName() { - gt = got[i] - break - } - } - - cmpColumns(tb, d, wantTable.GetName(), wantTable.(*sqlschema.BaseTable).Columns, gt.(*sqlschema.BaseTable).Columns) - cmpConstraints(tb, wantTable.(*sqlschema.BaseTable), gt.(*sqlschema.BaseTable)) + for tableName, wantTable := range want.FromOldest() { + gotTable, ok := got.Get(tableName) + require.True(tb, ok) + cmpColumns(tb, d, wantTable.GetName(), wantTable.GetColumns(), gotTable.GetColumns()) + cmpConstraints(tb, wantTable.(*sqlschema.BaseTable), gotTable.(*sqlschema.BaseTable)) } } @@ -321,18 +392,18 @@ func cmpColumns( tb testing.TB, d sqlschema.InspectorDialect, tableName string, - want, got map[string]sqlschema.Column, + want, got *orderedmap.OrderedMap[string, sqlschema.Column], ) { tb.Helper() var errs []string var missing []string - for colName, wantCol := range want { + for colName, wantCol := range want.FromOldest() { errorf := func(format string, args ...interface{}) { errs = append(errs, fmt.Sprintf("[%s.%s] "+format, append([]interface{}{tableName, colName}, args...)...)) } wantCol := wantCol.(*sqlschema.BaseColumn) - gotCol, ok := got[colName].(*sqlschema.BaseColumn) + gotCol, ok := got.Value(colName).(*sqlschema.BaseColumn) if !ok { missing = append(missing, colName) continue @@ -364,8 +435,8 @@ func cmpColumns( } var extra []string - for colName := range got { - if _, ok := want[colName]; !ok { + for colName := range got.FromOldest() { + if _, ok := want.Get(colName); !ok { extra = append(extra, colName) } } @@ -400,9 +471,9 @@ func cmpConstraints(tb testing.TB, want, got *sqlschema.BaseTable) { require.ElementsMatch(tb, stripNames(want.UniqueConstraints), stripNames(got.UniqueConstraints), "table %q does not have expected unique constraints (listA=want, listB=got)", want.Name) } -func tableNames(tables map[schema.FQN]sqlschema.Table) (names []string) { - for fqn := range tables { - names = append(names, fqn.Table) +func tableNames(tables *orderedmap.OrderedMap[string, sqlschema.Table]) (names []string) { + for name := range tables.FromOldest() { + names = append(names, name) } return } @@ -430,24 +501,30 @@ func TestBunModelInspector_Inspect(t *testing.T) { tables.Register((*Model)(nil)) inspector := sqlschema.NewBunModelInspector(tables) - want := map[string]sqlschema.Column{ - "id": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - DefaultValue: "random()", + want := orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + DefaultValue: "random()", + }, }, - "name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - DefaultValue: "'John Doe'", + orderedmap.Pair[string, sqlschema.Column]{ + Key: "name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + DefaultValue: "'John Doe'", + }, }, - } + )) got, err := inspector.Inspect(context.Background()) require.NoError(t, err) - gotTables := got.(sqlschema.BunModelSchema).ModelTables - require.Len(t, gotTables, 1) - for _, table := range gotTables { - cmpColumns(t, dialect.(sqlschema.InspectorDialect), "model", want, table.Columns) + gotTables := got.GetTables() + require.Equal(t, 1, gotTables.Len()) + for _, table := range gotTables.FromOldest() { + cmpColumns(t, dialect.(sqlschema.InspectorDialect), "model", want, table.GetColumns()) return } }) @@ -463,27 +540,36 @@ func TestBunModelInspector_Inspect(t *testing.T) { tables.Register((*Model)(nil)) inspector := sqlschema.NewBunModelInspector(tables) - want := map[string]sqlschema.Column{ - "id": &sqlschema.BaseColumn{ - SQLType: "text", + want := orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "id", + Value: &sqlschema.BaseColumn{ + SQLType: "text", + }, }, - "first_name": &sqlschema.BaseColumn{ - SQLType: "character varying", - VarcharLen: 60, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: "character varying", + VarcharLen: 60, + }, }, - "last_name": &sqlschema.BaseColumn{ - SQLType: "varchar", - VarcharLen: 100, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: "varchar", + VarcharLen: 100, + }, }, - } + )) got, err := inspector.Inspect(context.Background()) require.NoError(t, err) - gotTables := got.(sqlschema.BunModelSchema).ModelTables - require.Len(t, gotTables, 1) - for _, table := range gotTables { - cmpColumns(t, dialect.(sqlschema.InspectorDialect), "model", want, table.Columns) + gotTables := got.GetTables() + require.Equal(t, 1, gotTables.Len()) + for _, table := range gotTables.FromOldest() { + cmpColumns(t, dialect.(sqlschema.InspectorDialect), "model", want, table.GetColumns()) } }) @@ -509,10 +595,10 @@ func TestBunModelInspector_Inspect(t *testing.T) { got, err := inspector.Inspect(context.Background()) require.NoError(t, err) - gotTables := got.(sqlschema.BunModelSchema).ModelTables - require.Len(t, gotTables, 1) - for _, table := range gotTables { - cmpConstraints(t, want, &table.BaseTable) + gotTables := got.GetTables() + require.Equal(t, 1, gotTables.Len()) + for _, table := range gotTables.FromOldest() { + cmpConstraints(t, want, &table.(*sqlschema.BunTable).BaseTable) return } }) @@ -531,11 +617,12 @@ func TestBunModelInspector_Inspect(t *testing.T) { got, err := inspector.Inspect(context.Background()) require.NoError(t, err) - gotTables := got.(sqlschema.BunModelSchema).ModelTables - require.Len(t, gotTables, 1) - for _, table := range gotTables { - require.NotNilf(t, table.PrimaryKey, "did not register primary key, want (%s)", want) - require.Equal(t, want, table.PrimaryKey.Columns, "wrong primary key columns") + gotTables := got.GetTables() + require.Equal(t, 1, gotTables.Len()) + for _, table := range gotTables.FromOldest() { + pk := table.GetPrimaryKey() + require.NotNilf(t, pk, "did not register primary key, want (%s)", want) + require.Equal(t, want, pk.Columns, "wrong primary key columns") return } }) diff --git a/internal/dbtest/migrate_test.go b/internal/dbtest/migrate_test.go index 2b45a0268..9ceca55a6 100644 --- a/internal/dbtest/migrate_test.go +++ b/internal/dbtest/migrate_test.go @@ -14,7 +14,7 @@ import ( "github.com/uptrace/bun/dialect/sqltype" "github.com/uptrace/bun/migrate" "github.com/uptrace/bun/migrate/sqlschema" - "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) const ( @@ -370,8 +370,9 @@ func testRenameTable(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) tables := state.Tables - require.Len(t, tables, 1) - require.Contains(t, tables, schema.FQN{Schema: db.Dialect().DefaultSchema(), Table: "changed"}) + require.Equal(t, 1, tables.Len()) + _, found := tables.Get("changed") + require.True(t, found) } func testCreateDropTable(t *testing.T, db *bun.DB) { @@ -399,8 +400,9 @@ func testCreateDropTable(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) tables := state.Tables - require.Len(t, tables, 1) - require.Contains(t, tables, schema.FQN{Schema: db.Dialect().DefaultSchema(), Table: "createme"}) + require.Equal(t, 1, tables.Len()) + _, found := tables.Get("createme") + require.True(t, found) } func testAlterForeignKeys(t *testing.T, db *bun.DB) { @@ -524,10 +526,10 @@ func testRenamedColumns(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) - require.Len(t, state.Tables, 2) + require.Equal(t, 2, state.Tables.Len()) var renamed, model2 sqlschema.Table - for _, tbl := range state.Tables { + for _, tbl := range state.Tables.FromOldest() { switch tbl.GetName() { case "renamed": renamed = tbl @@ -536,9 +538,9 @@ func testRenamedColumns(t *testing.T, db *bun.DB) { } } - require.Contains(t, renamed.GetColumns(), "count") - require.Contains(t, model2.GetColumns(), "second_column") - require.Contains(t, model2.GetColumns(), "do_not_rename") + require.NotNil(t, renamed.GetColumns().Value("count")) + require.NotNil(t, model2.GetColumns().Value("second_column")) + require.NotNil(t, model2.GetColumns().Value("do_not_rename")) } // testChangeColumnType_AutoCast checks type changes which can be type-casted automatically, @@ -568,46 +570,70 @@ func testChangeColumnType_AutoCast(t *testing.T, db *bun.DB) { // ManyValues []string `bun:",array"` // did not change } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "change_me_own_type"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "change_me_own_type", - Columns: map[string]sqlschema.Column{ - "bigger_int": &sqlschema.BaseColumn{ - SQLType: "bigint", - IsIdentity: true, - }, - "ts": &sqlschema.BaseColumn{ - SQLType: "timestamp", // FIXME(dyma): convert "timestamp with time zone" to sqltype.Timestamp - DefaultValue: "current_timestamp", // FIXME(dyma): Convert driver-specific value to common "expressions" (e.g. CURRENT_TIMESTAMP == current_timestamp) OR lowercase all types. - IsNullable: true, - }, - "default_expr": &sqlschema.BaseColumn{ - SQLType: "varchar", - IsNullable: true, - DefaultValue: "random()", - }, - "empty_default": &sqlschema.BaseColumn{ - SQLType: "varchar", - IsNullable: true, - DefaultValue: "", // NOT "''" - }, - "not_null": &sqlschema.BaseColumn{ - SQLType: "varchar", - IsNullable: false, - }, - "type_override": &sqlschema.BaseColumn{ - SQLType: "varchar", - IsNullable: true, - VarcharLen: 200, - }, - // "many_values": { - // SQLType: "array", - // }, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "change_me_own_type", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "change_me_own_type", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "bigger_int", + Value: &sqlschema.BaseColumn{ + SQLType: "bigint", + IsIdentity: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "ts", + Value: &sqlschema.BaseColumn{ + SQLType: "timestamp", // FIXME(dyma): convert "timestamp with time zone" to sqltype.Timestamp + DefaultValue: "current_timestamp", // FIXME(dyma): Convert driver-specific value to common "expressions" (e.g. CURRENT_TIMESTAMP == current_timestamp) OR lowercase all types. + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "default_expr", + Value: &sqlschema.BaseColumn{ + SQLType: "varchar", + IsNullable: true, + DefaultValue: "random()", + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "empty_default", + Value: &sqlschema.BaseColumn{ + SQLType: "varchar", + IsNullable: true, + DefaultValue: "", // NOT "''" + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "not_null", + Value: &sqlschema.BaseColumn{ + SQLType: "varchar", + IsNullable: false, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "type_override", + Value: &sqlschema.BaseColumn{ + SQLType: "varchar", + IsNullable: true, + VarcharLen: 200, + }, + }, + // orderedmap.Pair[string, sqlschema.Column]{ + // Key: "many_values", + // Value: &sqlschema.BaseColumn{ + // SQLType: "array", + // }, + // }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("bigger_int")}, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("bigger_int")}, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) @@ -619,7 +645,7 @@ func testChangeColumnType_AutoCast(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) - cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.Tables) + cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.GetTables()) } func testIdentity(t *testing.T, db *bun.DB) { @@ -635,22 +661,31 @@ func testIdentity(t *testing.T, db *bun.DB) { B int64 `bun:",notnull,identity"` } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "bourne_identity"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "bourne_identity", - Columns: map[string]sqlschema.Column{ - "a": &sqlschema.BaseColumn{ - SQLType: sqltype.BigInt, - IsIdentity: false, // <- drop IDENTITY - }, - "b": &sqlschema.BaseColumn{ - SQLType: sqltype.BigInt, - IsIdentity: true, // <- add IDENTITY - }, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "bourne_identity", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "bourne_identity", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "a", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.BigInt, + IsIdentity: false, // <- drop IDENTITY + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "b", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.BigInt, + IsIdentity: true, // <- add IDENTITY + }, + }, + )), }, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) @@ -662,7 +697,7 @@ func testIdentity(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) - cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.Tables) + cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.GetTables()) } func testAddDropColumn(t *testing.T, db *bun.DB) { @@ -678,22 +713,31 @@ func testAddDropColumn(t *testing.T, db *bun.DB) { AddMe bool `bun:"addme"` } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "column_madness"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "column_madness", - Columns: map[string]sqlschema.Column{ - "do_not_touch": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "addme": &sqlschema.BaseColumn{ - SQLType: sqltype.Boolean, - IsNullable: true, - }, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "column_madness", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "column_madness", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "do_not_touch", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "addme", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.Boolean, + IsNullable: true, + }, + }, + )), }, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) @@ -705,7 +749,7 @@ func testAddDropColumn(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) - cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.Tables) + cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.GetTables()) } func testUnique(t *testing.T, db *bun.DB) { @@ -731,48 +775,72 @@ func testUnique(t *testing.T, db *bun.DB) { PetBreed string `bun:"pet_breed"` // shrink "pet" unique group } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "uniqlo_stores"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "uniqlo_stores", - Columns: map[string]sqlschema.Column{ - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "middle_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "birthday": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "email": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "uniqlo_stores", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "uniqlo_stores", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "middle_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "birthday", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "email", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "pet_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "pet_breed", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + )), + UniqueConstraints: []sqlschema.Unique{ + {Columns: sqlschema.NewColumns("email")}, + {Columns: sqlschema.NewColumns("pet_name")}, + // We can only be sure of the user-defined index name + {Name: "full_name", Columns: sqlschema.NewColumns("first_name", "middle_name", "last_name")}, }, - "pet_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "pet_breed": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - }, - UniqueConstraints: []sqlschema.Unique{ - {Columns: sqlschema.NewColumns("email")}, - {Columns: sqlschema.NewColumns("pet_name")}, - // We can only be sure of the user-defined index name - {Name: "full_name", Columns: sqlschema.NewColumns("first_name", "middle_name", "last_name")}, }, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) @@ -784,7 +852,7 @@ func testUnique(t *testing.T, db *bun.DB) { // Assert state := inspect(ctx) - cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.Tables) + cmpTables(t, db.Dialect().(sqlschema.InspectorDialect), wantTables, state.GetTables()) } func testUniqueRenamedTable(t *testing.T, db *bun.DB) { @@ -809,39 +877,57 @@ func testUniqueRenamedTable(t *testing.T, db *bun.DB) { PetBreed string `bun:"pet_breed,unique"` } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "after"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "after", - Columns: map[string]sqlschema.Column{ - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "after", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "after", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "birthday", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "pet_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "pet_breed", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + )), + UniqueConstraints: []sqlschema.Unique{ + {Columns: sqlschema.NewColumns("pet_name")}, + {Columns: sqlschema.NewColumns("pet_breed")}, + {Name: "full_name", Columns: sqlschema.NewColumns("first_name", "last_name", "birthday")}, }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "birthday": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "pet_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "pet_breed": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - }, - UniqueConstraints: []sqlschema.Unique{ - {Columns: sqlschema.NewColumns("pet_name")}, - {Columns: sqlschema.NewColumns("pet_breed")}, - {Name: "full_name", Columns: sqlschema.NewColumns("first_name", "last_name", "birthday")}, }, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) @@ -904,57 +990,87 @@ func testUpdatePrimaryKeys(t *testing.T, db *bun.DB) { LastName string `bun:"last_name,pk"` } - wantTables := map[schema.FQN]sqlschema.Table{ - {Schema: db.Dialect().DefaultSchema(), Table: "drop_your_pks"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "drop_your_pks", - Columns: map[string]sqlschema.Column{ - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - }, + wantTables := orderedmap.New[string, sqlschema.Table](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Table]{ + Key: "drop_your_pks", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "drop_your_pks", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + }, + }, + )), }, }, - {Schema: db.Dialect().DefaultSchema(), Table: "add_new_pk"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "add_new_pk", - Columns: map[string]sqlschema.Column{ - "new_id": &sqlschema.BaseColumn{ - SQLType: sqltype.BigInt, - IsNullable: false, - IsIdentity: true, - }, - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: true, - }, + orderedmap.Pair[string, sqlschema.Table]{ + Key: "add_new_pk", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "add_new_pk", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "new_id", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.BigInt, + IsNullable: false, + IsIdentity: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: true, + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("new_id")}, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("new_id")}, }, - {Schema: db.Dialect().DefaultSchema(), Table: "change_pk"}: &sqlschema.BaseTable{ - Schema: db.Dialect().DefaultSchema(), - Name: "change_pk", - Columns: map[string]sqlschema.Column{ - "first_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - }, - "last_name": &sqlschema.BaseColumn{ - SQLType: sqltype.VarChar, - IsNullable: false, - }, + orderedmap.Pair[string, sqlschema.Table]{ + Key: "change_pk", + Value: &sqlschema.BaseTable{ + Schema: db.Dialect().DefaultSchema(), + Name: "change_pk", + Columns: orderedmap.New[string, sqlschema.Column](orderedmap.WithInitialData( + orderedmap.Pair[string, sqlschema.Column]{ + Key: "first_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + }, + }, + orderedmap.Pair[string, sqlschema.Column]{ + Key: "last_name", + Value: &sqlschema.BaseColumn{ + SQLType: sqltype.VarChar, + IsNullable: false, + }, + }, + )), + PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("first_name", "last_name")}, }, - PrimaryKey: &sqlschema.PrimaryKey{Columns: sqlschema.NewColumns("first_name", "last_name")}, }, - } + )) ctx := context.Background() inspect := inspectDbOrSkip(t, db) diff --git a/migrate/diff.go b/migrate/diff.go index b9149b303..5c93a33e0 100644 --- a/migrate/diff.go +++ b/migrate/diff.go @@ -23,24 +23,24 @@ func diff(got, want sqlschema.Database, opts ...diffOption) *changeset { } func (d *detector) detectChanges() *changeset { - currentTables := d.mapNameToTable(d.current) - targetTables := d.mapNameToTable(d.target) + currentTables := d.current.GetTables() + targetTables := d.target.GetTables() RenameCreate: - for wantName, wantTable := range targetTables { + for wantName, wantTable := range targetTables.FromOldest() { // A table with this name exists in the database. We assume that schema objects won't // be renamed to an already existing name, nor do we support such cases. // Simply check if the table definition has changed. - if haveTable, ok := currentTables[wantName]; ok { + if haveTable, ok := currentTables.Get(wantName); ok { d.detectColumnChanges(haveTable, wantTable, true) d.detectConstraintChanges(haveTable, wantTable) continue } // Find all renamed tables. We assume that renamed tables have the same signature. - for haveName, haveTable := range currentTables { - if _, exists := targetTables[haveName]; !exists && d.canRename(haveTable, wantTable) { + for haveName, haveTable := range currentTables.FromOldest() { + if _, exists := targetTables.Get(haveName); !exists && d.canRename(haveTable, wantTable) { d.changes.Add(&RenameTableOp{ FQN: haveTable.GetFQN(), NewName: wantName, @@ -51,7 +51,7 @@ RenameCreate: // We need not check wantTable any further. d.detectColumnChanges(haveTable, wantTable, false) d.detectConstraintChanges(haveTable, wantTable) - delete(currentTables, haveName) + currentTables.Delete(haveName) continue RenameCreate } } @@ -66,8 +66,8 @@ RenameCreate: } // Drop any remaining "current" tables which do not have a model. - for name, table := range currentTables { - if _, keep := targetTables[name]; !keep { + for name, table := range currentTables.FromOldest() { + if _, keep := targetTables.Get(name); !keep { d.changes.Add(&DropTableOp{ FQN: table.GetFQN(), }) @@ -100,16 +100,16 @@ RenameCreate: // detechColumnChanges finds renamed columns and, if checkType == true, columns with changed type. func (d *detector) detectColumnChanges(current, target sqlschema.Table, checkType bool) { - currentColumns := d.mapNameToColumn(current) - targetColumns := d.mapNameToColumn(target) + currentColumns := current.GetColumns() + targetColumns := target.GetColumns() ChangeRename: - for tName, tCol := range targetColumns { + for tName, tCol := range targetColumns.FromOldest() { // This column exists in the database, so it hasn't been renamed, dropped, or added. // Still, we should not delete(columns, thisColumn), because later we will need to // check that we do not try to rename a column to an already a name that already exists. - if cCol, ok := currentColumns[tName]; ok { + if cCol, ok := currentColumns.Get(tName); ok { if checkType && !d.equalColumns(cCol, tCol) { d.changes.Add(&ChangeColumnTypeOp{ FQN: target.GetFQN(), @@ -123,9 +123,9 @@ ChangeRename: // Column tName does not exist in the database -- it's been either renamed or added. // Find renamed columns first. - for cName, cCol := range currentColumns { + for cName, cCol := range currentColumns.FromOldest() { // Cannot rename if a column with this name already exists or the types differ. - if _, exists := targetColumns[cName]; exists || !d.equalColumns(tCol, cCol) { + if _, exists := targetColumns.Get(cName); exists || !d.equalColumns(tCol, cCol) { continue } d.changes.Add(&RenameColumnOp{ @@ -134,7 +134,7 @@ ChangeRename: NewName: tName, }) d.refMap.RenameColumn(target.GetFQN(), cName, tName) - delete(currentColumns, cName) // no need to check this column again + currentColumns.Delete(cName) // no need to check this column again // Update primary key definition to avoid superficially recreating the constraint. current.GetPrimaryKey().Columns.Replace(cName, tName) @@ -150,8 +150,8 @@ ChangeRename: } // Drop columns which do not exist in the target schema and were not renamed. - for cName, cCol := range currentColumns { - if _, keep := targetColumns[cName]; !keep { + for cName, cCol := range currentColumns.FromOldest() { + if _, keep := targetColumns.Get(cName); !keep { d.changes.Add(&DropColumnOp{ FQN: target.GetFQN(), Column: cName, @@ -295,22 +295,6 @@ func (d detector) makeTargetColDef(current, target sqlschema.Column) sqlschema.C return target } -func (d *detector) mapNameToTable(s sqlschema.Database) map[string]sqlschema.Table { - m := make(map[string]sqlschema.Table) - for _, t := range s.GetTables() { - m[t.GetName()] = t - } - return m -} - -func (d *detector) mapNameToColumn(t sqlschema.Table) map[string]sqlschema.Column { - m := make(map[string]sqlschema.Column) - for _, c := range t.GetColumns() { - m[c.GetName()] = c - } - return m -} - type TypeEquivalenceFunc func(sqlschema.Column, sqlschema.Column) bool // equalSignatures determines if two tables have the same "signature". @@ -342,7 +326,7 @@ func newSignature(t sqlschema.Table, eq TypeEquivalenceFunc) signature { // scan iterates over table's field and counts occurrences of each unique column definition. func (s *signature) scan(t sqlschema.Table) { - for _, icol := range t.GetColumns() { + for _, icol := range t.GetColumns().FromOldest() { scanCol := icol.(*sqlschema.BaseColumn) // This is slightly more expensive than if the columns could be compared directly // and we always did s.underlying[col]++, but we get type-equivalence in return. diff --git a/migrate/sqlschema/database.go b/migrate/sqlschema/database.go index 66bdff19e..9800306db 100644 --- a/migrate/sqlschema/database.go +++ b/migrate/sqlschema/database.go @@ -5,10 +5,11 @@ import ( "strings" "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type Database interface { - GetTables() []Table + GetTables() *orderedmap.OrderedMap[string, Table] GetForeignKeys() map[ForeignKey]string } @@ -19,16 +20,12 @@ var _ Database = (*BaseDatabase)(nil) // Dialects and only dialects can use it to implement the Database interface. // Other packages must use the Database interface. type BaseDatabase struct { - Tables map[schema.FQN]Table + Tables *orderedmap.OrderedMap[string, Table] ForeignKeys map[ForeignKey]string } -func (ds BaseDatabase) GetTables() []Table { - var tables []Table - for i := range ds.Tables { - tables = append(tables, ds.Tables[i]) - } - return tables +func (ds BaseDatabase) GetTables() *orderedmap.OrderedMap[string, Table] { + return ds.Tables } func (ds BaseDatabase) GetForeignKeys() map[ForeignKey]string { diff --git a/migrate/sqlschema/inspector.go b/migrate/sqlschema/inspector.go index 74d791330..087a7f9f9 100644 --- a/migrate/sqlschema/inspector.go +++ b/migrate/sqlschema/inspector.go @@ -8,6 +8,7 @@ import ( "github.com/uptrace/bun" "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" ) type InspectorDialect interface { @@ -59,15 +60,11 @@ func NewBunModelInspector(tables *schema.Tables) *BunModelInspector { type BunModelSchema struct { BaseDatabase - ModelTables map[schema.FQN]*BunTable + Tables *orderedmap.OrderedMap[string, Table] } -func (ms BunModelSchema) GetTables() []Table { - var tables []Table - for _, t := range ms.ModelTables { - tables = append(tables, t) - } - return tables +func (ms BunModelSchema) GetTables() *orderedmap.OrderedMap[string, Table] { + return ms.Tables } // BunTable provides additional table metadata that is only accessible from scanning bun models. @@ -83,17 +80,17 @@ func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) { BaseDatabase: BaseDatabase{ ForeignKeys: make(map[ForeignKey]string), }, - ModelTables: make(map[schema.FQN]*BunTable), + Tables: orderedmap.New[string, Table](), } for _, t := range bmi.tables.All() { - columns := make(map[string]Column) + columns := orderedmap.New[string, Column]() for _, f := range t.Fields { sqlType, length, err := parseLen(f.CreateTableSQLType) if err != nil { - return state, fmt.Errorf("parse length in %q: %w", f.CreateTableSQLType, err) + return nil, fmt.Errorf("parse length in %q: %w", f.CreateTableSQLType, err) } - columns[f.Name] = &BaseColumn{ + columns.Set(f.Name, &BaseColumn{ Name: f.Name, SQLType: strings.ToLower(sqlType), // TODO(dyma): maybe this is not necessary after Column.Eq() VarcharLen: length, @@ -101,7 +98,7 @@ func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) { IsNullable: !f.NotNull, IsAutoIncrement: f.AutoIncrement, IsIdentity: f.Identity, - } + }) } var unique []Unique @@ -132,8 +129,7 @@ func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) { pk = &PrimaryKey{Columns: NewColumns(columns...)} } - fqn := schema.FQN{Schema: t.Schema, Table: t.Name} - state.ModelTables[fqn] = &BunTable{ + state.Tables.Set(t.Name, &BunTable{ BaseTable: BaseTable{ Schema: t.Schema, Name: t.Name, @@ -142,7 +138,7 @@ func (bmi *BunModelInspector) Inspect(ctx context.Context) (Database, error) { PrimaryKey: pk, }, Model: t.ZeroIface, - } + }) for _, rel := range t.Relations { // These relations are nominal and do not need a foreign key to be declared in the current table. diff --git a/migrate/sqlschema/table.go b/migrate/sqlschema/table.go index 44667e799..e5e71479c 100644 --- a/migrate/sqlschema/table.go +++ b/migrate/sqlschema/table.go @@ -1,11 +1,14 @@ package sqlschema -import "github.com/uptrace/bun/schema" +import ( + "github.com/uptrace/bun/schema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) type Table interface { GetSchema() string GetName() string - GetColumns() []Column + GetColumns() *orderedmap.OrderedMap[string, Column] GetPrimaryKey() *PrimaryKey GetUniqueConstraints() []Unique GetFQN() schema.FQN @@ -22,8 +25,7 @@ type BaseTable struct { Name string // ColumnDefinitions map each column name to the column definition. - // TODO: this must be an ordered map so the order of columns is preserved - Columns map[string]Column + Columns *orderedmap.OrderedMap[string, Column] // PrimaryKey holds the primary key definition. // A nil value means that no primary key is defined for the table. @@ -47,13 +49,8 @@ func (td *BaseTable) GetName() string { return td.Name } -func (td *BaseTable) GetColumns() []Column { - var columns []Column - // FIXME: columns will be returned in a random order - for colName := range td.Columns { - columns = append(columns, td.Columns[colName]) - } - return columns +func (td *BaseTable) GetColumns() *orderedmap.OrderedMap[string, Column] { + return td.Columns } func (td *BaseTable) GetPrimaryKey() *PrimaryKey {