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

  1. Use only one setter per m2m relation per node in CreateBulk — merge the IDs into a single AddXIDs(...) call.
  2. Remove duplicated/aliased edge definitions from schema hooks or custom mutators that append extra EdgeSpecs for the same table.
  3. 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

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


AI-assisted analysis of ent/ent@69d5d4deb1 (2026-09-03). Data as JSON: /api/errors/079e0749dc737321. Report an issue: GitHub.