// 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 gen import ( "fmt" "os" "path/filepath" "reflect" "slices" "testing" "entgo.io/ent/dialect/entsql" "entgo.io/ent/dialect/sql/schema" "entgo.io/ent/entc/load" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "github.com/stretchr/testify/require" ) var ( T1 = &load.Schema{ Name: "T1", Fields: []*load.Field{ {Name: "age", Info: &field.TypeInfo{Type: field.TypeInt}, Optional: true}, {Name: "expired_at", Info: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Optional: true}, {Name: "name", Info: &field.TypeInfo{Type: field.TypeString}, Default: true}, }, Edges: []*load.Edge{ {Name: "t2", Type: "T2", Required: true}, {Name: "t1", Type: "T1", Unique: true}, // Bidirectional unique edge (unique/"has-a" in both sides). {Name: "t2_o2o", Type: "T2", Unique: true}, // Unidirectional non-unique edge ("has-many"). The reference is on the "many" side. // For example: A user "has-many" books, but a book "has-an" owner (and only one). {Name: "o2m", Type: "T2"}, // Unidirectional unique edge ("has-one"). // For example: A user "has-an" address (and only one), but an address "has-many" users. {Name: "m2o", Type: "T2", Unique: true}, // Bidirectional unique edge ("has-one" in T1 side, and "has-many" in T2 side). {Name: "t2_m2o", Type: "T2", Unique: true}, // Bidirectional non-unique edge ("has-many" in T1 side, and "has-one" in T2 side). {Name: "t2_o2m", Type: "T2"}, // Bidirectional non-unique edge ("has-many" in both side). {Name: "t2_m2m", Type: "T2"}, // Unidirectional non-unique edge for the same type. {Name: "t1_m2m", Type: "T1"}, }, } T2 = &load.Schema{ Name: "T2", Annotations: dict("GQL", map[string]string{"Name": "T2"}), Fields: []*load.Field{ {Name: "active", Info: &field.TypeInfo{Type: field.TypeBool}}, }, Edges: []*load.Edge{ {Name: "t1", Type: "T1", RefName: "t2", Inverse: true}, {Name: "t1_o2o", Type: "T1", RefName: "t2_o2o", Unique: true, Inverse: true}, {Name: "t1_o2m", Type: "T1", RefName: "t2_m2o", Inverse: true}, {Name: "t1_m2o", Type: "T1", RefName: "t2_o2m", Unique: true, Inverse: true}, {Name: "t1_m2m", Type: "T1", RefName: "t2_m2m", Inverse: true}, {Name: "t2_m2m_from", Type: "T2", Ref: &load.Edge{Name: "t2_m2m_to", Type: "T2", Annotations: dict("GQL", map[string]string{"Name": "To"})}, Inverse: true, Annotations: dict("GQL", map[string]string{"Name": "From"})}, }, } ) func TestNewGraph(t *testing.T) { require := require.New(t) _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, T1) require.Error(err, "should fail due to missing types") graph, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, T1, T2) require.NoError(err) require.NotNil(graph) require.Len(graph.Nodes, 2) t1 := graph.Nodes[0] // check fields. require.Equal("T1", t1.Name) require.Len(t1.Fields, 3) for i, name := range []string{"age", "expired_at", "name"} { require.Equal(name, t1.Fields[i].Name) } for i, typ := range []string{"int", "time.Time", "string"} { require.Equal(typ, t1.Fields[i].Type.String()) } for i, optional := range []bool{true, true, false} { require.Equal(optional, t1.Fields[i].Optional) } for i, nullable := range []bool{false, true, false} { require.Equal(nullable, t1.Fields[i].Nillable) } for i, value := range []bool{false, false, true} { require.Equal(value, t1.Fields[i].Default) } // check edges. require.Len(t1.Edges, 9) for i, name := range []string{"t2", "t1"} { require.Equal(name, t1.Edges[i].Name) } for i, typ := range []*Type{graph.Nodes[1], graph.Nodes[0]} { require.Equal(typ, t1.Edges[i].Type, "edge should point to the right type") } for i, optional := range []bool{false, true} { require.Equal(optional, t1.Edges[i].Optional) } for i, unique := range []bool{false, true} { require.Equal(unique, t1.Edges[i].Unique) } for i, inverse := range []bool{false, false} { require.Equal(inverse, t1.Edges[i].IsInverse()) } t2 := graph.Nodes[1] require.Equal(map[string]string{"Name": "T2"}, t2.Annotations["GQL"]) f1, e1 := t2.Fields[0], t2.Edges[0] require.Equal("bool", f1.Type.String()) require.Equal("active", f1.Name) require.Equal("t1", e1.Name) require.True(e1.IsInverse()) require.Equal("t2", e1.Inverse) require.Equal(graph.Nodes[0], e1.Type) require.Equal("t2_m2m_from", t2.Edges[5].Name) require.Equal("t2_m2m_to", t2.Edges[5].Inverse) require.Equal("t2_m2m_to", t2.Edges[6].Name) require.Empty(t2.Edges[6].Inverse) require.Equal(t2.Edges[6], t2.Edges[5].Ref) require.Equal(t2.Edges[5], t2.Edges[6].Ref) require.Equal(map[string]string{"Name": "From"}, t2.Edges[5].Annotations["GQL"]) require.Equal(map[string]string{"Name": "To"}, t2.Edges[6].Annotations["GQL"]) } func TestNewGraphRequiredLoop(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "T1", Edges: []*load.Edge{ {Name: "parent", Type: "T1", Unique: true, Required: true}, {Name: "children", Type: "T1", Inverse: true, RefName: "parent", Required: true}, }, }) require.Error(t, err, "require loop") _, err = NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet", Required: true}, }, }, &load.Schema{ Name: "Pet", Edges: []*load.Edge{ {Name: "owner", Type: "User", Inverse: true, RefName: "pets", Unique: true, Required: true}, }, }) require.Error(t, err, "require loop") } func TestNewGraphBadInverse(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet"}, {Name: "groups", Type: "Group"}, }, }, &load.Schema{ Name: "Pet", Edges: []*load.Edge{ {Name: "owner", Type: "User", Unique: true, Required: true, RefName: "pets", Inverse: true}, }, }, &load.Schema{ Name: "Group", Edges: []*load.Edge{ {Name: "users", Type: "User", RefName: "pets", Inverse: true}, }, }) require.Errorf(t, err, "mismatch type for back-reference") } func TestNewGraphDuplicateEdges(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "groups", Type: "Group"}, {Name: "groups", Type: "Group", RefName: "owner", Inverse: true}, }, }, &load.Schema{ Name: "Group", Edges: []*load.Edge{ {Name: "users", Type: "User", RefName: "groups", Inverse: true}, {Name: "owner", Type: "User", Unique: true}, }, }) require.EqualError(t, err, `entc/gen: User schema contains multiple "groups" edges`) } func TestNewGraphDuplicateEdgeField(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "User", Fields: []*load.Field{ {Name: "parent", Info: &field.TypeInfo{Type: field.TypeInt}}, }, Edges: []*load.Edge{ {Name: "parent", Type: "User"}, }, }) require.EqualError(t, err, `entc/gen: User schema cannot contain field and edge with the same name "parent"`) } func TestNewGraphThroughUndefinedType(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "T1", Edges: []*load.Edge{ {Name: "groups", Type: "T1", Required: true, Through: &struct{ N, T string }{N: "groups_edge", T: "T2"}}, }, }) require.EqualError(t, err, `entc/gen: resolving edges: edge T1.groups defined with Through("groups_edge", T2.Type), but type T2 was not found`) } func TestNewGraphThroughInvalidRel(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "T1", Edges: []*load.Edge{ {Name: "groups", Type: "T1", Unique: true, Required: true, Through: &struct{ N, T string }{N: "groups_edge", T: "T2"}}, }, }) require.EqualError(t, err, `entc/gen: resolving edges: edge T1.groups Through("groups_edge", T2.Type) is allowed only on M2M edges, but got: "O2O"`) } func TestNewGraphThroughDuplicates(t *testing.T) { _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "groups", Type: "Group", Through: &struct{ N, T string }{N: "group_edges", T: "T1"}}, {Name: "group_edges", Type: "Group"}, }, }, &load.Schema{ Name: "Group", Edges: []*load.Edge{ {Name: "users", Type: "User", Inverse: true, RefName: "groups", Through: &struct{ N, T string }{N: "user_edges", T: "T1"}}, }, }, &load.Schema{ Name: "T1", }, ) require.EqualError(t, err, `entc/gen: resolving edges: edge User.groups defined with Through("group_edges", T1.Type), but schema User already has an edge named group_edges`) } func TestRelation(t *testing.T) { require := require.New(t) _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, T1) require.Error(err, "should fail due to missing types") graph, err := NewGraph(&Config{Package: "entc/gen"}, T1, T2) require.NoError(err) require.NotNil(graph) require.Len(graph.Nodes, 2) t1, t2 := graph.Nodes[0], graph.Nodes[1] // unidirectional one 2 one. require.Equal(O2O, t1.Edges[1].Rel.Type) // bidirectional one to one. require.Equal(O2O, t1.Edges[2].Rel.Type) require.Equal(O2O, t2.Edges[1].Rel.Type) // unidirectional one 2 many. require.Equal(O2M, t1.Edges[3].Rel.Type) // unidirectional many 2 one. require.Equal(M2O, t1.Edges[4].Rel.Type) // bidirectional many 2 one. require.Equal(M2O, t1.Edges[5].Rel.Type) require.Equal(O2M, t2.Edges[2].Rel.Type) // bidirectional one 2 many. require.Equal(O2M, t1.Edges[6].Rel.Type) require.Equal(M2O, t2.Edges[3].Rel.Type) // bidirectional many 2 many. require.Equal(M2M, t1.Edges[7].Rel.Type) require.Equal(M2M, t2.Edges[4].Rel.Type) // unidirectional many 2 many. require.Equal(M2M, t1.Edges[8].Rel.Type) } func TestFKColumns(t *testing.T) { user := &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet"}, {Name: "pet", Type: "Pet", Unique: true}, {Name: "parent", Type: "User", Unique: true}, }, } require := require.New(t) graph, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, &load.Schema{Name: "Pet"}) require.NoError(err) t1 := graph.Nodes[0] for i, r := range []Relation{ {Type: O2M, Table: "pets", Columns: []string{"user_pets"}}, {Type: M2O, Table: "users", Columns: []string{"user_pet"}}, {Type: O2O, Table: "users", Columns: []string{"user_parent"}}, } { require.Equal(r.Type, t1.Edges[i].Rel.Type) require.Equal(r.Table, t1.Edges[i].Rel.Table) require.Equal(r.Columns, t1.Edges[i].Rel.Columns) } // Adding inverse edges. graph, err = NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, &load.Schema{ Name: "Pet", Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "pets", Inverse: true, Unique: true}, {Name: "team", Type: "User", RefName: "pet", Inverse: true}, }, }, ) require.NoError(err) t1, t2 := graph.Nodes[0], graph.Nodes[1] for i, r := range []Relation{ {Type: O2M, Table: "pets", Columns: []string{"user_pets"}}, {Type: M2O, Table: "users", Columns: []string{"user_pet"}}, } { require.Equal(r.Type, t1.Edges[i].Rel.Type) require.Equal(r.Table, t1.Edges[i].Rel.Table) require.Equal(r.Columns, t1.Edges[i].Rel.Columns) } for i, r := range []Relation{ {Type: M2O, Table: "pets", Columns: []string{"user_pets"}}, {Type: O2M, Table: "users", Columns: []string{"user_pet"}}, } { require.Equal(r.Type, t2.Edges[i].Rel.Type) require.Equal(r.Table, t2.Edges[i].Rel.Table) require.Equal(r.Columns, t2.Edges[i].Rel.Columns) } } func TestBlobDualWriteGoType(t *testing.T) { require := require.New(t) doc := &load.Schema{ Name: "Doc", Fields: []*load.Field{ { Name: "config", Info: &field.TypeInfo{ Type: field.TypeBlob, Ident: "*Config", PkgPath: "example.com/app", PkgName: "app", RType: &field.RType{ Name: "Config", Ident: "Config", Kind: reflect.Struct, PkgPath: "example.com/app", }, }, ValueScanner: true, BlobDualWrite: true, }, }, } g, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, doc) require.NoError(err) require.Len(g.Nodes, 1) f := g.Nodes[0].Fields[0] require.True(f.IsBlob()) require.False(f.IsBlobNoColumn()) // After graph initialization, the blob field should be TypeBytes but preserve GoType info. require.Equal(field.TypeBytes, f.Type.Type) require.True(f.HasGoType(), "GoType should be preserved for DualWrite blob fields") require.Equal("*Config", f.Type.String(), "Type.String() should return the custom GoType") require.Equal("example.com/app", f.Type.PkgPath) require.NotNil(f.Type.RType) require.Equal("Config", f.Type.RType.Name) } func TestAbortDuplicateFK(t *testing.T) { var ( user = &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet", StorageKey: &edge.StorageKey{Symbols: []string{"owner_id"}}}, {Name: "cars", Type: "Car", StorageKey: &edge.StorageKey{Symbols: []string{"owner_id"}}}, }, } pet = &load.Schema{ Name: "Pet", Fields: []*load.Field{ {Name: "owner_id", Info: &field.TypeInfo{Type: field.TypeInt}, Nillable: true, Optional: true}, }, Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "pets", Inverse: true, Unique: true}, }, } car = &load.Schema{ Name: "Car", Fields: []*load.Field{ {Name: "owner_id", Info: &field.TypeInfo{Type: field.TypeInt}, Nillable: true, Optional: true}, }, Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "cars", Inverse: true, Unique: true}, }, } ) g, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, pet, car) require.NoError(t, err) _, err = g.Tables() require.EqualError(t, err, `duplicate foreign-key symbol "owner_id" found in tables "cars" and "pets"`) } func TestPosition(t *testing.T) { antFn := func(s string) map[string]any { return map[string]any{entsql.Annotation{}.Name(): map[string]string{"schema": s}} } var ( user = &load.Schema{ Name: "User", Pos: "user.go:1", Edges: []*load.Edge{ {Name: "pets", Type: "Pet"}, {Name: "cars", Type: "Car", Through: &struct{ N, T string }{N: "car_edge", T: "CarOwner"}}, }, Annotations: antFn("one"), } pet = &load.Schema{ Name: "Pet", Pos: "pet.go:10", Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "pets", Inverse: true}, }, Annotations: antFn("two"), } petView = &load.Schema{ View: true, Name: "PetView", Pos: "pet_view.go:10", Annotations: antFn("two"), } car = &load.Schema{ Name: "Car", Pos: "car.go:100", Edges: []*load.Edge{ {Name: "owners", Type: "User", RefName: "cars", Inverse: true}, }, Annotations: antFn("two"), } carOwner = &load.Schema{ Name: "CarOwner", Pos: "car_owner.go:1000", Fields: []*load.Field{ {Name: "user_id", Info: &field.TypeInfo{Type: field.TypeInt}}, {Name: "car_id", Info: &field.TypeInfo{Type: field.TypeInt}}, }, Edges: []*load.Edge{ {Name: "owner", Type: "User", Field: "user_id", Unique: true, Required: true}, {Name: "car", Type: "User", Field: "car_id", Unique: true, Required: true}, }, Annotations: antFn("two"), } ) g, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, pet, petView, car, carOwner) require.NoError(t, err) ts, err := g.Tables() require.NoError(t, err) require.Len(t, ts, 5) require.Equal(t, ts[0].Pos, "user.go:1") require.Equal(t, ts[1].Pos, "pet.go:10") require.Equal(t, ts[2].Pos, "car.go:100") require.Equal(t, ts[3].Pos, "car_owner.go:1000") // edge schema has its own position require.Equal(t, ts[4].Pos, "user.go:1") // user owns the pet edge -> user position vs, err := g.Views() require.NoError(t, err) require.Len(t, vs, 1) require.Equal(t, vs[0].Pos, "pet_view.go:10") } func TestMultiSchemaAnnotation(t *testing.T) { antFn := func(s string) map[string]any { return map[string]any{entsql.Annotation{}.Name(): map[string]string{"schema": s}} } var ( user = &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet"}, {Name: "cars", Type: "Car", Annotations: antFn("two")}, }, Annotations: antFn("one"), } pet = &load.Schema{ Name: "Pet", Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "pets", Inverse: true}, }, Annotations: antFn("two"), } car = &load.Schema{ Name: "Car", Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "cars", Inverse: true}, }, Annotations: antFn("two"), } ) g, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, pet, car) require.NoError(t, err) ts, err := g.Tables() require.NoError(t, err) require.Len(t, ts, 5) require.Equal(t, "one", ts[0].Schema) // user require.Equal(t, "two", ts[1].Schema) // pet require.Equal(t, "two", ts[2].Schema) // car require.Equal(t, "one", ts[3].Schema) // user<>pets join table user lives in owner schema require.Equal(t, "two", ts[4].Schema) // user<>cars edge has annotation and lives in specified schema } func TestEnsureCorrectFK(t *testing.T) { var ( user = &load.Schema{ Name: "User", Edges: []*load.Edge{ {Name: "pets", Type: "Pet", StorageKey: &edge.StorageKey{Columns: []string{"owner_id"}}}, }, } pet = &load.Schema{ Name: "Pet", Fields: []*load.Field{ {Name: "owner_id", Info: &field.TypeInfo{Type: field.TypeInt}, Nillable: true, Optional: true}, }, Edges: []*load.Edge{ {Name: "owner", Type: "User", RefName: "pets", Inverse: true, Unique: true}, }, } ) _, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, pet) require.EqualError(t, err, `entc/gen: set "User" foreign-keys: column "owner_id" definition on edge "pets" should be replaced with Field("owner_id") on its reference "owner"`) user.Edges[0].StorageKey = nil pet.Edges[0].Field = "owner_id" _, err = NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, pet) require.NoError(t, err) } func TestGraph_Gen(t *testing.T) { require := require.New(t) target := filepath.Join(t.TempDir(), "ent") external := MustParse(NewTemplate("external").Parse("package external")) skipped := MustParse(NewTemplate("skipped").SkipIf(func(*Graph) bool { return true }).Parse("package external")) schemas := []*load.Schema{ { Name: "T1", Fields: []*load.Field{ {Name: "age", Info: &field.TypeInfo{Type: field.TypeInt}, Optional: true}, {Name: "expired_at", Info: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Optional: true}, {Name: "name", Info: &field.TypeInfo{Type: field.TypeString}}, }, Edges: []*load.Edge{ {Name: "t1", Type: "T1", Unique: true}, }, }, {Name: "T2"}, {Name: "T3"}, } graph, err := NewGraph(&Config{ Package: "entc/gen", Target: target, Storage: drivers[0], Templates: []*Template{external, skipped}, IDType: &field.TypeInfo{Type: field.TypeInt}, Features: AllFeatures, }, schemas...) require.NoError(err) require.NotNil(graph) require.NoError(graph.Gen()) // Ensure globalid feature added annotations. a := IncrementStarts{"t1s": 0, "t2s": 1 << 32, "t3s": 2 << 32} require.Equal(a, graph.Annotations[a.Name()]) for i, n := range graph.Nodes { require.Equal(i<<32, *n.EntSQL().IncrementStart) } // Ensure graph files were generated. for _, name := range []string{"ent", "client"} { _, err := os.Stat(fmt.Sprintf("%s/%s.go", target, name)) require.NoError(err) } // Ensure entity files were generated. for _, format := range []string{"%s", "%s_create", "%s_update", "%s_delete", "%s_query"} { _, err := os.Stat(fmt.Sprintf(fmt.Sprintf("%s/%s.go", target, format), "t1")) require.NoError(err) _, err = os.Stat(fmt.Sprintf(fmt.Sprintf("%s/%s.go", target, format), "t2")) require.NoError(err) } _, err = os.Stat(filepath.Join(target, "external.go")) require.NoError(err) _, err = os.Stat(filepath.Join(target, "skipped.go")) require.True(os.IsNotExist(err)) // Generated feature templates. _, err = os.Stat(filepath.Join(target, "internal", "schema.go")) require.NoError(err) _, err = os.Stat(filepath.Join(target, "internal", "schemaconfig.go")) require.NoError(err) c, err := os.ReadFile(filepath.Join(target, "internal", "globalid.go")) require.NoError(err) require.Contains(string(c), fmt.Sprintf(`"{\"t1s\":0,\"t2s\":%d,\"t3s\":%d}"`, 1<<32, 2<<32)) // Rerun codegen with only one feature-flag. graph.Features = []Feature{FeatureSnapshot} require.NoError(graph.Gen()) // Generated feature templates. _, err = os.Stat(filepath.Join(target, "internal", "schema.go")) require.NoError(err) _, err = os.Stat(filepath.Join(target, "internal", "schemaconfig.go")) require.True(os.IsNotExist(err)) _, err = os.Stat(filepath.Join(target, "internal", "globalid.go")) require.True(os.IsNotExist(err)) // Rerun codegen without any feature-flags. graph.Features = nil require.NoError(graph.Gen()) _, err = os.Stat(filepath.Join(target, "internal")) require.True(os.IsNotExist(err)) schemas = schemas[:1] graph, err = NewGraph(&Config{ Package: "entc/gen", Target: target, Storage: drivers[0], Templates: []*Template{external, skipped}, IDType: &field.TypeInfo{Type: field.TypeInt}, Features: AllFeatures, }, schemas...) require.NoError(err) require.NotNil(graph) require.NoError(graph.Gen()) // Ensure entity files were generated. for _, format := range []string{"%s", "%s_create", "%s_update", "%s_delete", "%s_query"} { _, err := os.Stat(fmt.Sprintf(fmt.Sprintf("%s/%s.go", target, format), "t1")) require.NoError(err) _, err = os.Stat(fmt.Sprintf(fmt.Sprintf("%s/%s.go", target, format), "t2")) require.Error(err) } } func ensureStructTag(name string) Hook { return func(next Generator) Generator { return GenerateFunc(func(g *Graph) error { // Ensure all fields have a specific tag. for _, node := range g.Nodes { for _, f := range node.Fields { tag := reflect.StructTag(f.StructTag) if _, ok := tag.Lookup(name); !ok { return fmt.Errorf("struct tag %q is missing for field %s.%s", name, node.Name, f.Name) } } } return next.Generate(g) }) } } func TestGraph_Hooks(t *testing.T) { require := require.New(t) graph, err := NewGraph(&Config{ Package: "entc/gen", Storage: drivers[0], IDType: &field.TypeInfo{Type: field.TypeInt}, Hooks: []Hook{ensureStructTag("yaml")}, }, &load.Schema{ Name: "T1", Fields: []*load.Field{ {Name: "age", Info: &field.TypeInfo{Type: field.TypeInt}, Optional: true}, {Name: "expired_at", Info: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Optional: true}, {Name: "name", Info: &field.TypeInfo{Type: field.TypeString}}, }, Edges: []*load.Edge{ {Name: "t1", Type: "T1", Unique: true}, }, }) require.NoError(err) require.NotNil(graph) require.EqualError(graph.Gen(), `struct tag "yaml" is missing for field T1.age`) } func TestDependencyAnnotation_Build(t *testing.T) { tests := []struct { typ *field.TypeInfo field string }{ { typ: &field.TypeInfo{ Ident: "*http.Client", }, field: "HTTPClient", }, { typ: &field.TypeInfo{ Ident: "[]*http.Client", RType: &field.RType{ Kind: reflect.Slice, }, }, field: "HTTPClients", }, { typ: &field.TypeInfo{ Ident: "[]*url.URL", RType: &field.RType{ Kind: reflect.Slice, }, }, field: "URLs", }, { typ: &field.TypeInfo{ Ident: "*net.Conn", }, field: "NetConn", }, } for _, tt := range tests { d := &Dependency{Type: tt.typ} require.NoError(t, d.Build()) require.Equal(t, tt.field, d.Field) } } func TestEdgeFieldCollation(t *testing.T) { var ( user = &load.Schema{ Name: "User", Fields: []*load.Field{ {Name: "id", Info: &field.TypeInfo{Type: field.TypeString}, Annotations: dict("EntSQL", dict("collation", "utf8mb4_bin"))}, }, } post = &load.Schema{ Name: "Post", Fields: []*load.Field{ {Name: "author_id", Info: &field.TypeInfo{Type: field.TypeString}, Annotations: dict("EntSQL", dict("collation", "utf8mb4_bin"))}, }, Edges: []*load.Edge{ {Name: "author", Type: "User", Field: "author_id", Unique: true, Required: true}, }, } ) g, err := NewGraph(&Config{Package: "entc/gen", Storage: drivers[0]}, user, post) require.NoError(t, err) tables, err := g.Tables() require.NoError(t, err) // Find the post table idx := slices.IndexFunc(tables, func(t *schema.Table) bool { return t.Name == "posts" }) require.NotEqual(t, -1, idx, "posts table should exist") postTable := tables[idx] // Verify author_id column preserves collation from field annotation col, ok := postTable.Column("author_id") require.True(t, ok) require.Equal(t, "utf8mb4_bin", col.Collation) }