{"record":{"id":"079e0749dc737321","repo":"ent/ent","slug":"expect-exactly-1-edge-spec-per-table-but-got-d","errorCode":null,"errorMessage":"expect exactly 1 edge-spec per table, but got %d","messagePattern":"expect exactly 1 edge-spec per table, but got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"dialect/sql/sqlgraph/graph.go","lineNumber":1749,"sourceCode":"\t\t// can hold different values on different insertions (e.g. time.Now() or uuid.New()).\n\t\tif len(edges[0].Target.Fields) == 0 {\n\t\t\tinsert.OnConflict(sql.DoNothing())\n\t\t}\n\t\tquery, args := insert.Query()\n\t\tif err := g.tx.Exec(ctx, query, args, nil); err != nil {\n\t\t\treturn fmt.Errorf(\"add m2m edge for table %s: %w\", table, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *graph) batchAddM2M(ctx context.Context, spec *BatchCreateSpec) error {\n\ttables := make(map[string]*sql.InsertBuilder)\n\tfor _, node := range spec.Nodes {\n\t\tedges := EdgeSpecs(node.Edges).FilterRel(M2M)\n\t\tfor name, edges := range edges.GroupTable() {\n\t\t\tif len(edges) != 1 {\n\t\t\t\treturn fmt.Errorf(\"expect exactly 1 edge-spec per table, but got %d\", len(edges))\n\t\t\t}\n\t\t\tedge := edges[0]\n\t\t\tinsert, ok := tables[name]\n\t\t\tif !ok {\n\t\t\t\tcolumns := edge.Columns\n\t\t\t\t// Additional fields, such as edge-schema fields.\n\t\t\t\tfor _, f := range edge.Target.Fields {\n\t\t\t\t\tcolumns = append(columns, f.Column)\n\t\t\t\t}\n\t\t\t\tinsert = g.builder.Insert(name).Columns(columns...)\n\t\t\t\tif edge.Schema != \"\" {\n\t\t\t\t\t// If the Schema field was provided to the EdgeSpec (by the\n\t\t\t\t\t// generated code), it should be the same for all EdgeSpecs.\n\t\t\t\t\tinsert.Schema(edge.Schema)\n\t\t\t\t}\n\t\t\t\t// Ignore conflicts only if edges do not contain extra fields, because these fields\n\t\t\t\t// can hold different values on different insertions (e.g. time.Now() or uuid.New()).\n\t\t\t\tif len(edge.Target.Fields) == 0 {","sourceCodeStart":1731,"sourceCodeEnd":1767,"githubUrl":"https://github.com/ent/ent/blob/69d5d4deb19599f129166634e09d33addcf3f2cc/dialect/sql/sqlgraph/graph.go#L1731-L1767","documentation":"In batchAddM2M, ent groups edge specs per join table and expects exactly one EdgeSpec per table per batch node. If GroupTable() returns more than one spec for the same table, the builder cannot merge them into a single multi-row INSERT and fails fast with this count in the message.","triggerScenarios":"Using batch creation (client.<Entity>.CreateBulk(...).Save(ctx)) where a node has two separate edge specs targeting the same m2m join table — typically by setting both AddXIDs(...) and AddX(...) with overlapping/aliased edges in one builder.","commonSituations":"Mixing edge setters on the same relation in a CreateBulk (e.g. .AddTagIDs(1) plus a custom through-edge-spec); generated-code misuse or hand-written specs in custom hooks/mutators that add extra EdgeSpecs for the same table.","solutions":["Use only one setter per m2m relation per node in CreateBulk — merge the IDs into a single AddXIDs(...) call.","Remove duplicated/aliased edge definitions from schema hooks or custom mutators that append extra EdgeSpecs for the same table.","If batching is not required, fall back to individual Create().Save(ctx) calls, which route through addM2MEdges instead."],"exampleFix":"// before\nclient.Post.CreateBulk(client.Post.Create().AddTagIDs(1, 2), client.Post.Create().AddTagIDs(2, 3)).Save(ctx)\n// after: one spec per relation per node\nclient.Post.CreateBulk(client.Post.Create().AddTagIDs(1, 2), client.Post.Create().AddTagIDs(2, 3)).Save(ctx) // keep each builder's AddTagIDs called once\n// merged form: client.Post.Create().AddTagIDs(1, 2, 3)","handlingStrategy":"validation","validationCode":"// one setter per m2m relation per CreateBulk node\nids := dedupe(append(a, b...))\nclient.Post.CreateBulk(client.Post.Create().AddTagIDs(ids...)).Save(ctx)","typeGuard":null,"tryCatchPattern":"err := bulk.Save(ctx)\nif err != nil && strings.Contains(err.Error(), \"expect exactly 1 edge-spec per table\") {\n  return fmt.Errorf(\"builder misuse: multiple edge specs for one m2m table: %w\", err)\n}","preventionTips":["Call each edge setter at most once per builder; merge ID lists first.","Audit hooks/mutators that inject extra EdgeSpecs for the same table.","Review CreateBulk usage after refactoring edge definitions.","Deduplicate edges in custom mutation code before Save."],"tags":["ent","sql","m2m","batch","createbulk"],"backgroundTag":"invalid-edge-spec","analyzedSha":"69d5d4deb19599f129166634e09d33addcf3f2cc","analyzedAt":"2026-09-03T16:51:39.799Z","contentChangedAt":"2026-09-03T16:51:39.799Z","schemaVersion":2},"datasetVersion":"2026-09-11T00:17:11.886Z"}