ent/ent · error
expect exactly 1 edge-spec per table, but got %d
Error message
expect exactly 1 edge-spec per table, but got %d
What it means
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.
Source
Thrown at dialect/sql/sqlgraph/graph.go:1749
// can hold different values on different insertions (e.g. time.Now() or uuid.New()).
if len(edges[0].Target.Fields) == 0 {
insert.OnConflict(sql.DoNothing())
}
query, args := insert.Query()
if err := g.tx.Exec(ctx, query, args, nil); err != nil {
return fmt.Errorf("add m2m edge for table %s: %w", table, err)
}
}
return nil
}
func (g *graph) batchAddM2M(ctx context.Context, spec *BatchCreateSpec) error {
tables := make(map[string]*sql.InsertBuilder)
for _, node := range spec.Nodes {
edges := EdgeSpecs(node.Edges).FilterRel(M2M)
for name, edges := range edges.GroupTable() {
if len(edges) != 1 {
return fmt.Errorf("expect exactly 1 edge-spec per table, but got %d", len(edges))
}
edge := edges[0]
insert, ok := tables[name]
if !ok {
columns := edge.Columns
// Additional fields, such as edge-schema fields.
for _, f := range edge.Target.Fields {
columns = append(columns, f.Column)
}
insert = g.builder.Insert(name).Columns(columns...)
if edge.Schema != "" {
// If the Schema field was provided to the EdgeSpec (by the
// generated code), it should be the same for all EdgeSpecs.
insert.Schema(edge.Schema)
}
// Ignore conflicts only if edges do not contain extra fields, because these fields
// can hold different values on different insertions (e.g. time.Now() or uuid.New()).
if len(edge.Target.Fields) == 0 {View on GitHub (pinned to 69d5d4deb1)
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.
Example fix
// before client.Post.CreateBulk(client.Post.Create().AddTagIDs(1, 2), client.Post.Create().AddTagIDs(2, 3)).Save(ctx) // after: one spec per relation per node client.Post.CreateBulk(client.Post.Create().AddTagIDs(1, 2), client.Post.Create().AddTagIDs(2, 3)).Save(ctx) // keep each builder's AddTagIDs called once // merged form: client.Post.Create().AddTagIDs(1, 2, 3)
Defensive patterns
Strategy: validation
Validate before calling
// one setter per m2m relation per CreateBulk node ids := dedupe(append(a, b...)) client.Post.CreateBulk(client.Post.Create().AddTagIDs(ids...)).Save(ctx)
Try / catch
err := bulk.Save(ctx)
if err != nil && strings.Contains(err.Error(), "expect exactly 1 edge-spec per table") {
return fmt.Errorf("builder misuse: multiple edge specs for one m2m table: %w", err)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- sql/sqlgraph: update edge schema table %q cannot update exte
- remove m2m edge for table %s: %w
- add m2m edge for table %s: %w
- {{ $pkg }}: missing options for {{ $builder }}.OnConflict
- entsql: view query should not contain arguments. got: %d
AI-assisted analysis of ent/ent@69d5d4deb1 (2026-09-03).
Data as JSON: /api/errors/079e0749dc737321.
Report an issue: GitHub.