diff --git a/.golangci.yml b/.golangci.yml index a2b7d1b13..709f8b1ad 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -67,4 +67,4 @@ issues: - staticcheck - path: entc/gen/graph.go linters: - - gocritic \ No newline at end of file + - gocritic diff --git a/dialect/sql/schema/schema.go b/dialect/sql/schema/schema.go index bed17f687..10718b223 100644 --- a/dialect/sql/schema/schema.go +++ b/dialect/sql/schema/schema.go @@ -409,9 +409,6 @@ const ( // ConstName returns the constant name of a reference option. It's used by entc for printing the constant name in templates. func (r ReferenceOption) ConstName() string { - if r == NoAction { - return "" - } return strings.ReplaceAll(strings.Title(strings.ToLower(string(r))), " ", "") } diff --git a/doc/md/schema-edges.md b/doc/md/schema-edges.md index b5aba0a9e..a667a24ed 100755 --- a/doc/md/schema-edges.md +++ b/doc/md/schema-edges.md @@ -1034,6 +1034,12 @@ func (Card) Edges() []ent.Edge { If the example above, a card entity cannot be created without its owner. +:::info +Note that, starting with [v0.10](https://github.com/ent/ent/releases/tag/v0.10.0), foreign key columns are created +as `NOT NULL` in the database for required edges that are not [self-reference](#o2m-same-type). In order to migrate +existing foreign key columns, use the [Atlas Migration](migrate.md#atlas-integration) option. +::: + ## StorageKey By default, Ent configures edge storage-keys by the edge-owner (the schema that holds the `edge.To`), and not the by diff --git a/entc/gen/graph.go b/entc/gen/graph.go index 5a991c335..267ec1ddb 100644 --- a/entc/gen/graph.go +++ b/entc/gen/graph.go @@ -456,15 +456,20 @@ func (g *Graph) Tables() (all []*schema.Table, err error) { } switch e.Rel.Type { case O2O, O2M: - // The "owner" is the table that owns the relation (we set the foreign-key on) - // and "ref" is the referenced table. + // The "owner" is the table that owns the relation (we set + // the foreign-key on) and "ref" is the referenced table. owner, ref := tables[e.Rel.Table], tables[n.Table()] pk := ref.PrimaryKey[0] column := &schema.Column{Name: e.Rel.Column(), Size: pk.Size, Type: pk.Type, Unique: e.Rel.Type == O2O, SchemaType: pk.SchemaType, Nullable: true} + // If it's not a circular reference (self-referencing table), + // and the inverse edge is required, make it non-nullable. + if n != e.Type && e.Ref != nil && !e.Ref.Optional { + column.Nullable = false + } mayAddColumn(owner, column) owner.AddForeignKey(&schema.ForeignKey{ RefTable: ref, - OnDelete: deleteAction(e), + OnDelete: deleteAction(e, column), Columns: []*schema.Column{column}, RefColumns: []*schema.Column{ref.PrimaryKey[0]}, Symbol: fkSymbol(e, owner, ref), @@ -473,10 +478,15 @@ func (g *Graph) Tables() (all []*schema.Table, err error) { ref, owner := tables[e.Type.Table()], tables[e.Rel.Table] pk := ref.PrimaryKey[0] column := &schema.Column{Name: e.Rel.Column(), Size: pk.Size, Type: pk.Type, SchemaType: pk.SchemaType, Nullable: true} + // If it's not a circular reference (self-referencing table), + // and the edge is non-optional (required), make it non-nullable. + if n != e.Type && !e.Optional { + column.Nullable = false + } mayAddColumn(owner, column) owner.AddForeignKey(&schema.ForeignKey{ RefTable: ref, - OnDelete: deleteAction(e), + OnDelete: deleteAction(e, column), Columns: []*schema.Column{column}, RefColumns: []*schema.Column{ref.PrimaryKey[0]}, Symbol: fkSymbol(e, owner, ref), @@ -563,8 +573,11 @@ func fkSymbols(e *Edge, c1, c2 *schema.Column) (string, string) { } // deleteAction returns the referential action for DELETE operations of the given edge. -func deleteAction(e *Edge) schema.ReferenceOption { - action := schema.SetNull +func deleteAction(e *Edge, c *schema.Column) schema.ReferenceOption { + action := schema.NoAction + if c.Nullable { + action = schema.SetNull + } if ant := e.EntSQL(); ant != nil && ant.OnDelete != "" { action = schema.ReferenceOption(ant.OnDelete) } diff --git a/entc/integration/cascadelete/ent/migrate/schema.go b/entc/integration/cascadelete/ent/migrate/schema.go index 18e8f5419..37e92cb13 100644 --- a/entc/integration/cascadelete/ent/migrate/schema.go +++ b/entc/integration/cascadelete/ent/migrate/schema.go @@ -16,7 +16,7 @@ var ( CommentsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "text", Type: field.TypeString}, - {Name: "post_id", Type: field.TypeInt, Nullable: true}, + {Name: "post_id", Type: field.TypeInt}, } // CommentsTable holds the schema information for the "comments" table. CommentsTable = &schema.Table{ diff --git a/entc/integration/edgefield/ent/migrate/schema.go b/entc/integration/edgefield/ent/migrate/schema.go index 6af5e24d3..6ba6d87d3 100644 --- a/entc/integration/edgefield/ent/migrate/schema.go +++ b/entc/integration/edgefield/ent/migrate/schema.go @@ -165,8 +165,8 @@ var ( RentalsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "date", Type: field.TypeTime}, - {Name: "car_id", Type: field.TypeUUID, Nullable: true}, - {Name: "user_id", Type: field.TypeInt, Nullable: true, SchemaType: map[string]string{"sqlite3": "integer"}}, + {Name: "car_id", Type: field.TypeUUID}, + {Name: "user_id", Type: field.TypeInt, SchemaType: map[string]string{"sqlite3": "integer"}}, } // RentalsTable holds the schema information for the "rentals" table. RentalsTable = &schema.Table{ @@ -178,13 +178,13 @@ var ( Symbol: "rentals_cars_rentals", Columns: []*schema.Column{RentalsColumns[2]}, RefColumns: []*schema.Column{CarsColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, { Symbol: "rentals_users_rentals", Columns: []*schema.Column{RentalsColumns[3]}, RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, Indexes: []*schema.Index{ diff --git a/entc/integration/ent/migrate/schema.go b/entc/integration/ent/migrate/schema.go index b911a8459..ad47afaaa 100644 --- a/entc/integration/ent/migrate/schema.go +++ b/entc/integration/ent/migrate/schema.go @@ -247,7 +247,7 @@ var ( {Name: "type", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "max_users", Type: field.TypeInt, Nullable: true, Default: 10}, {Name: "name", Type: field.TypeString}, - {Name: "group_info", Type: field.TypeInt, Nullable: true}, + {Name: "group_info", Type: field.TypeInt}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ @@ -259,7 +259,7 @@ var ( Symbol: "groups_group_infos_info", Columns: []*schema.Column{GroupsColumns[6]}, RefColumns: []*schema.Column{GroupInfosColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, } diff --git a/entc/integration/migrate/entv2/car_create.go b/entc/integration/migrate/entv2/car_create.go index 4e3bdadee..7f8dc04d5 100644 --- a/entc/integration/migrate/entv2/car_create.go +++ b/entc/integration/migrate/entv2/car_create.go @@ -8,6 +8,7 @@ package entv2 import ( "context" + "errors" "fmt" "entgo.io/ent/dialect/sql/sqlgraph" @@ -29,14 +30,6 @@ func (cc *CarCreate) SetOwnerID(id int) *CarCreate { return cc } -// SetNillableOwnerID sets the "owner" edge to the User entity by ID if the given value is not nil. -func (cc *CarCreate) SetNillableOwnerID(id *int) *CarCreate { - if id != nil { - cc = cc.SetOwnerID(*id) - } - return cc -} - // SetOwner sets the "owner" edge to the User entity. func (cc *CarCreate) SetOwner(u *User) *CarCreate { return cc.SetOwnerID(u.ID) @@ -112,6 +105,9 @@ func (cc *CarCreate) ExecX(ctx context.Context) { // check runs all checks and user-defined validators on the builder. func (cc *CarCreate) check() error { + if _, ok := cc.mutation.OwnerID(); !ok { + return &ValidationError{Name: "owner", err: errors.New(`entv2: missing required edge "Car.owner"`)} + } return nil } diff --git a/entc/integration/migrate/entv2/car_update.go b/entc/integration/migrate/entv2/car_update.go index 56352c9b8..8156e20a7 100644 --- a/entc/integration/migrate/entv2/car_update.go +++ b/entc/integration/migrate/entv2/car_update.go @@ -38,14 +38,6 @@ func (cu *CarUpdate) SetOwnerID(id int) *CarUpdate { return cu } -// SetNillableOwnerID sets the "owner" edge to the User entity by ID if the given value is not nil. -func (cu *CarUpdate) SetNillableOwnerID(id *int) *CarUpdate { - if id != nil { - cu = cu.SetOwnerID(*id) - } - return cu -} - // SetOwner sets the "owner" edge to the User entity. func (cu *CarUpdate) SetOwner(u *User) *CarUpdate { return cu.SetOwnerID(u.ID) @@ -69,6 +61,9 @@ func (cu *CarUpdate) Save(ctx context.Context) (int, error) { affected int ) if len(cu.hooks) == 0 { + if err = cu.check(); err != nil { + return 0, err + } affected, err = cu.sqlSave(ctx) } else { var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { @@ -76,6 +71,9 @@ func (cu *CarUpdate) Save(ctx context.Context) (int, error) { if !ok { return nil, fmt.Errorf("unexpected mutation type %T", m) } + if err = cu.check(); err != nil { + return 0, err + } cu.mutation = mutation affected, err = cu.sqlSave(ctx) mutation.done = true @@ -116,6 +114,14 @@ func (cu *CarUpdate) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (cu *CarUpdate) check() error { + if _, ok := cu.mutation.OwnerID(); cu.mutation.OwnerCleared() && !ok { + return errors.New(`entv2: clearing a required unique edge "Car.owner"`) + } + return nil +} + func (cu *CarUpdate) sqlSave(ctx context.Context) (n int, err error) { _spec := &sqlgraph.UpdateSpec{ Node: &sqlgraph.NodeSpec{ @@ -194,14 +200,6 @@ func (cuo *CarUpdateOne) SetOwnerID(id int) *CarUpdateOne { return cuo } -// SetNillableOwnerID sets the "owner" edge to the User entity by ID if the given value is not nil. -func (cuo *CarUpdateOne) SetNillableOwnerID(id *int) *CarUpdateOne { - if id != nil { - cuo = cuo.SetOwnerID(*id) - } - return cuo -} - // SetOwner sets the "owner" edge to the User entity. func (cuo *CarUpdateOne) SetOwner(u *User) *CarUpdateOne { return cuo.SetOwnerID(u.ID) @@ -232,6 +230,9 @@ func (cuo *CarUpdateOne) Save(ctx context.Context) (*Car, error) { node *Car ) if len(cuo.hooks) == 0 { + if err = cuo.check(); err != nil { + return nil, err + } node, err = cuo.sqlSave(ctx) } else { var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { @@ -239,6 +240,9 @@ func (cuo *CarUpdateOne) Save(ctx context.Context) (*Car, error) { if !ok { return nil, fmt.Errorf("unexpected mutation type %T", m) } + if err = cuo.check(); err != nil { + return nil, err + } cuo.mutation = mutation node, err = cuo.sqlSave(ctx) mutation.done = true @@ -279,6 +283,14 @@ func (cuo *CarUpdateOne) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (cuo *CarUpdateOne) check() error { + if _, ok := cuo.mutation.OwnerID(); cuo.mutation.OwnerCleared() && !ok { + return errors.New(`entv2: clearing a required unique edge "Car.owner"`) + } + return nil +} + func (cuo *CarUpdateOne) sqlSave(ctx context.Context) (_node *Car, err error) { _spec := &sqlgraph.UpdateSpec{ Node: &sqlgraph.NodeSpec{ diff --git a/entc/integration/migrate/entv2/migrate/schema.go b/entc/integration/migrate/entv2/migrate/schema.go index edf198688..ecbae1aa1 100644 --- a/entc/integration/migrate/entv2/migrate/schema.go +++ b/entc/integration/migrate/entv2/migrate/schema.go @@ -16,7 +16,7 @@ var ( // CarsColumns holds the columns for the "cars" table. CarsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, - {Name: "user_car", Type: field.TypeInt, Nullable: true}, + {Name: "user_car", Type: field.TypeInt}, } // CarsTable holds the schema information for the "cars" table. CarsTable = &schema.Table{ @@ -28,7 +28,7 @@ var ( Symbol: "cars_users_car", Columns: []*schema.Column{CarsColumns[1]}, RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, } diff --git a/entc/integration/migrate/entv2/schema/user.go b/entc/integration/migrate/entv2/schema/user.go index 0bf5ca2de..073010c2c 100644 --- a/entc/integration/migrate/entv2/schema/user.go +++ b/entc/integration/migrate/entv2/schema/user.go @@ -120,7 +120,7 @@ func (User) Indexes() []ent.Index { // this index on MySQL. index.Fields("description"). Annotations(entsql.Prefix(100)), - // deleting old indexes (name, address), + // Deleting old indexes (name, address), // and defining a new one. index.Fields("phone", "age"). Unique(), @@ -135,7 +135,10 @@ func (Car) Edges() []ent.Edge { return []ent.Edge{ edge.From("owner", User.Type). Ref("car"). - Unique(), + Unique(). + // Make a M20 edge from nullable to required. + // Requires column and foreign-key migration. + Required(), } } diff --git a/entc/integration/migrate/migrate_test.go b/entc/integration/migrate/migrate_test.go index fd7f4122a..a8baea10e 100644 --- a/entc/integration/migrate/migrate_test.go +++ b/entc/integration/migrate/migrate_test.go @@ -128,13 +128,14 @@ func TestSQLite(t *testing.T) { ) SanityV2(t, drv.Dialect(), client) - idRange(t, client.Car.Create().SaveX(ctx).ID, 0, 1<<32) + u := client.User.Create().SetAge(1).SetName("x").SetNickname("x'").SetPhone("y").SaveX(ctx) + idRange(t, client.Car.Create().SetOwner(u).SaveX(ctx).ID, 0, 1<<32) idRange(t, client.Conversion.Create().SaveX(ctx).ID, 1<<32-1, 2<<32) idRange(t, client.CustomType.Create().SaveX(ctx).ID, 2<<32-1, 3<<32) idRange(t, client.Group.Create().SaveX(ctx).ID, 3<<32-1, 4<<32) idRange(t, client.Media.Create().SaveX(ctx).ID, 4<<32-1, 5<<32) idRange(t, client.Pet.Create().SaveX(ctx).ID, 5<<32-1, 6<<32) - idRange(t, client.User.Create().SetAge(1).SetName("x").SetNickname("x'").SetPhone("y").SaveX(ctx).ID, 6<<32-1, 7<<32) + idRange(t, u.ID, 6<<32-1, 7<<32) // Override the default behavior of LIKE in SQLite. // https://www.sqlite.org/pragma.html#pragma_case_sensitive_like @@ -163,11 +164,12 @@ func V1ToV2(t *testing.T, dialect string, clientv1 *entv1.Client, clientv2 *entv require.NoError(t, clientv2.Schema.Create(ctx, migratev2.WithGlobalUniqueID(true), schema.WithAtlas(true)), "should not create additional resources on multiple runs") SanityV2(t, dialect, clientv2) - idRange(t, clientv2.Car.Create().SaveX(ctx).ID, 0, 1<<32) + u := clientv2.User.Create().SetAge(1).SetName("foo").SetNickname("nick_foo").SetPhone("phone").SaveX(ctx) + idRange(t, clientv2.Car.Create().SetOwner(u).SaveX(ctx).ID, 0, 1<<32) idRange(t, clientv2.Conversion.Create().SaveX(ctx).ID, 1<<32-1, 2<<32) // Since "users" created in the migration of v1, it will occupy the range of 1<<32-1 ... 2<<32-1, // even though they are ordered differently in the migration of v2 (groups, pets, users). - idRange(t, clientv2.User.Create().SetAge(1).SetName("foo").SetNickname("nick_foo").SetPhone("phone").SaveX(ctx).ID, 3<<32-1, 4<<32) + idRange(t, u.ID, 3<<32-1, 4<<32) idRange(t, clientv2.Group.Create().SaveX(ctx).ID, 4<<32-1, 5<<32) idRange(t, clientv2.Media.Create().SaveX(ctx).ID, 5<<32-1, 6<<32) idRange(t, clientv2.Pet.Create().SaveX(ctx).ID, 6<<32-1, 7<<32) diff --git a/examples/o2o2types/ent/migrate/schema.go b/examples/o2o2types/ent/migrate/schema.go index 0ee5027b7..cfe135b65 100644 --- a/examples/o2o2types/ent/migrate/schema.go +++ b/examples/o2o2types/ent/migrate/schema.go @@ -17,7 +17,7 @@ var ( {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "expired", Type: field.TypeTime}, {Name: "number", Type: field.TypeString}, - {Name: "user_card", Type: field.TypeInt, Unique: true, Nullable: true}, + {Name: "user_card", Type: field.TypeInt, Unique: true}, } // CardsTable holds the schema information for the "cards" table. CardsTable = &schema.Table{ @@ -29,7 +29,7 @@ var ( Symbol: "cards_users_card", Columns: []*schema.Column{CardsColumns[3]}, RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, } diff --git a/examples/privacytenant/ent/migrate/schema.go b/examples/privacytenant/ent/migrate/schema.go index a73850330..9b6cfe344 100644 --- a/examples/privacytenant/ent/migrate/schema.go +++ b/examples/privacytenant/ent/migrate/schema.go @@ -16,7 +16,7 @@ var ( GroupsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "name", Type: field.TypeString, Default: "Unknown"}, - {Name: "group_tenant", Type: field.TypeInt, Nullable: true}, + {Name: "group_tenant", Type: field.TypeInt}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ @@ -28,7 +28,7 @@ var ( Symbol: "groups_tenants_tenant", Columns: []*schema.Column{GroupsColumns[2]}, RefColumns: []*schema.Column{TenantsColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, } @@ -48,7 +48,7 @@ var ( {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "name", Type: field.TypeString, Default: "Unknown"}, {Name: "foods", Type: field.TypeJSON, Nullable: true}, - {Name: "user_tenant", Type: field.TypeInt, Nullable: true}, + {Name: "user_tenant", Type: field.TypeInt}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ @@ -60,7 +60,7 @@ var ( Symbol: "users_tenants_tenant", Columns: []*schema.Column{UsersColumns[3]}, RefColumns: []*schema.Column{TenantsColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, }