entc/gen: support type-based matching for edge-schema columns (#3306)

In case of no match by foreign-keys, we search by edge-type. This can happen if the type (edge owner)
is named T, but the edge-schema E names its edge field as u_id. We consider it as a match if
there is only one usage of T in E.
This commit is contained in:
Ariel Mashraki
2023-02-07 10:58:20 +02:00
committed by GitHub
parent 939c7cff1a
commit baec350ed4
38 changed files with 9050 additions and 62 deletions

View File

@@ -422,13 +422,15 @@ func (g *Graph) resolve(t *Type) error {
if c1 == c2 {
c2 = rules.Singularize(e.Name) + "_id"
}
// Share the same backing array for the relation columns so
// that any changes to one will be reflected in both edges.
e.Rel.Columns = []string{c1, c2}
ref.Rel.Columns = []string{c1, c2}
ref.Rel.Columns = e.Rel.Columns
}
e.Rel.Table, ref.Rel.Table = table, table
if !e.M2M() {
e.Rel.Columns = []string{column}
ref.Rel.Columns = []string{column}
ref.Rel.Columns = e.Rel.Columns
}
// Assoc with uninitialized relation.
case !e.IsInverse() && e.Rel.Type == Unk:
@@ -470,49 +472,75 @@ func (g *Graph) edgeSchemas() error {
if !e.M2M() {
return fmt.Errorf("edge %s.%s Through(%q, %s.Type) is allowed only on M2M edges, but got: %q", n.Name, e.Name, e.def.Through.N, e.def.Through.T, e.Rel.Type)
}
typ, ok := g.typ(e.def.Through.T)
edgeT, ok := g.typ(e.def.Through.T)
switch {
case !ok:
return fmt.Errorf("edge %s.%s defined with Through(%q, %s.Type), but type %[4]s was not found", n.Name, e.Name, e.def.Through.N, e.def.Through.T, e.def.Through.T)
case typ == n:
case edgeT == n:
return fmt.Errorf("edge %s.%s defined with Through(%q, %s.Type), but edge cannot go through itself", n.Name, e.Name, e.def.Through.N, e.def.Through.T)
case e.def.Through.N == "" || n.hasEdge(e.def.Through.N):
return fmt.Errorf("edge %s.%s defined with Through(%q, %s.Type), but schema %[1]s already has an edge named %[3]s", n.Name, e.Name, e.def.Through.N, e.def.Through.T)
case e.IsInverse():
if typ.EdgeSchema.From != nil {
return fmt.Errorf("type %s is already used as an edge schema by other edge.From: %s.%s", typ.Name, typ.EdgeSchema.From.Name, typ.EdgeSchema.From.Owner.Name)
if edgeT.EdgeSchema.From != nil {
return fmt.Errorf("type %s is already used as an edge-schema by other edge.From: %s.%s", edgeT.Name, edgeT.EdgeSchema.From.Name, edgeT.EdgeSchema.From.Owner.Name)
}
e.Through = typ
typ.EdgeSchema.From = e
if to, from := typ.EdgeSchema.To, typ.EdgeSchema.From; to != nil && from.Ref != to {
return fmt.Errorf("mismtached edge.From(%q, %s.Type) and edge.To(%q, %s.Type) for edge schema %s", from.Name, from.Type.Name, to.Name, to.Type.Name, typ.Name)
e.Through = edgeT
edgeT.EdgeSchema.From = e
if to, from := edgeT.EdgeSchema.To, edgeT.EdgeSchema.From; to != nil && from.Ref != to {
return fmt.Errorf("mismtached edge.From(%q, %s.Type) and edge.To(%q, %s.Type) for edge schema %s", from.Name, from.Type.Name, to.Name, to.Type.Name, edgeT.Name)
}
default: // Assoc.
if typ.EdgeSchema.To != nil {
return fmt.Errorf("type %s is already used as an edge schema by other edge.To: %s.%s", typ.Name, typ.EdgeSchema.From.Name, typ.EdgeSchema.From.Owner.Name)
if edgeT.EdgeSchema.To != nil {
return fmt.Errorf("type %s is already used as an edge schema by other edge.To: %s.%s", edgeT.Name, edgeT.EdgeSchema.From.Name, edgeT.EdgeSchema.From.Owner.Name)
}
e.Through = typ
typ.EdgeSchema.To = e
if to, from := typ.EdgeSchema.To, typ.EdgeSchema.From; from != nil && from.Ref != to {
return fmt.Errorf("mismtached edge.To(%q, %s.Type) and edge.From(%q, %s.Type) for edge schema %s", from.Name, from.Type.Name, to.Name, to.Type.Name, typ.Name)
e.Through = edgeT
edgeT.EdgeSchema.To = e
if to, from := edgeT.EdgeSchema.To, edgeT.EdgeSchema.From; from != nil && from.Ref != to {
return fmt.Errorf("mismtached edge.To(%q, %s.Type) and edge.From(%q, %s.Type) for edge schema %s", from.Name, from.Type.Name, to.Name, to.Type.Name, edgeT.Name)
}
}
e.Rel.Table = typ.Table()
// Update both Assoc/To and Inverse/From
// relation tables to the edge schema table.
e.Rel.Table = edgeT.Table()
if e.Ref != nil {
e.Ref.Rel.Table = edgeT.Table()
}
var ref *Edge
for i, c := range e.Rel.Columns {
r, ok := func() (*Edge, bool) {
for _, fk := range typ.ForeignKeys {
// Search first for matching by edge-field.
for _, fk := range edgeT.ForeignKeys {
if fk.Field.Name == c {
return fk.Edge, true
}
}
// In case of no match, search by edge-type. This can happen if the type (edge owner)
// is named "T", but the edge-schema "E" names its edge field as "u_id". We consider
// it as a match if there is only one usage of "T" in "E".
var (
matches []*Edge
matchOn = n
)
if i == 0 && e.IsInverse() || i == 1 && !e.IsInverse() {
matchOn = e.Type
}
for _, e2 := range edgeT.Edges {
if e2.Type == matchOn && e2.Field() != nil {
matches = append(matches, e2)
}
}
if len(matches) == 1 {
// Ensure the M2M foreign key is updated accordingly.
e.Rel.Columns[i] = matches[0].Field().Name
return matches[0], true
}
return nil, false
}()
if !ok {
return fmt.Errorf("missing edge-field %s.%s for edge schema used by %s.%s in Through(%q, %s.Type)", typ.Name, c, n.Name, e.Name, e.def.Through.N, typ.Name)
return fmt.Errorf("missing edge-field %s.%s for edge schema used by %s.%s in Through(%q, %s.Type)", edgeT.Name, c, n.Name, e.Name, e.def.Through.N, edgeT.Name)
}
if r.Optional {
return fmt.Errorf("edge-schema %s is missing a Required() attribute for its reference edge %q", typ.Name, e.Name)
return fmt.Errorf("edge-schema %s is missing a Required() attribute for its reference edge %q", edgeT.Name, e.Name)
}
if !e.IsInverse() && i == 0 || e.IsInverse() && i == 1 {
ref = r
@@ -523,7 +551,7 @@ func (g *Graph) edgeSchemas() error {
n.Edges = append(n.Edges, &Edge{
def: &load.Edge{},
Name: e.def.Through.N,
Type: typ,
Type: edgeT,
Inverse: ref.Name,
Ref: ref,
Owner: n,
@@ -538,21 +566,21 @@ func (g *Graph) edgeSchemas() error {
},
})
// Edge schema contains a composite primary key, and it was not resolved in previous iterations.
if ant := fieldAnnotate(typ.Annotations); ant != nil && len(ant.ID) > 0 && len(typ.EdgeSchema.ID) == 0 {
if ant := fieldAnnotate(edgeT.Annotations); ant != nil && len(ant.ID) > 0 && len(edgeT.EdgeSchema.ID) == 0 {
r1, r2 := e.Rel.Columns[0], e.Rel.Columns[1]
if len(ant.ID) != 2 || ant.ID[0] != r1 || ant.ID[1] != r2 {
return fmt.Errorf(`edge schema primary key can only be defined on "id" or (%q, %q) in the same order`, r1, r2)
}
typ.ID = nil
edgeT.ID = nil
for _, f := range ant.ID {
typ.EdgeSchema.ID = append(typ.EdgeSchema.ID, typ.fields[f])
edgeT.EdgeSchema.ID = append(edgeT.EdgeSchema.ID, edgeT.fields[f])
}
}
if typ.HasCompositeID() {
if edgeT.HasCompositeID() {
continue
}
hasI := func() bool {
for _, idx := range typ.Indexes {
for _, idx := range edgeT.Indexes {
if !idx.Unique || len(idx.Columns) != 2 {
continue
}
@@ -565,7 +593,7 @@ func (g *Graph) edgeSchemas() error {
return false
}()
if !hasI {
if err := typ.AddIndex(&load.Index{Unique: true, Fields: e.Rel.Columns}); err != nil {
if err := edgeT.AddIndex(&load.Index{Unique: true, Fields: e.Rel.Columns}); err != nil {
return err
}
}