ent / ent/ent

[Bug] Nil Pointer Dereference when Multiple Schemas Use entsql.Skip()

Open
#4,455 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
17.2k
Forks
1k
PR merge metrics
No merged PRs in 30d

Description

- [x] The issue is present in the latest release.
- [x] I have searched the [issues](https://github.com/ent/ent/issues) of this repository and believe that this is not a duplicate.

## Current Behavior

When two or more schemas have `entsql.Skip()` annotation, code generation fails with a nil pointer dereference error:

```
execute template "schema": template: schema.tmpl:26:18: executing "schema" at <$.Tables>:
error calling Tables: runtime error: invalid memory address or nil pointer dereference
exit status 1
```

**Workaround**: Only one schema can use `entsql.Skip()` at a time:
- If only one schema has `entsql.Skip()`, generation succeeds ✅
- If **multiple** schemas have `entsql.Skip()`, generation fails ❌

## Expected Behavior

Code generation should succeed when multiple schemas use `entsql.Skip()`, allowing multiple tables to be skipped from migration while still generating their entity code.

## Steps to Reproduce

**Note**: The bug occurs when multiple schemas use `entsql.Skip()` **and** there are edges (relationships) referencing those schemas.

1. Create two schemas with `entsql.Skip()` and one schema with edges to them:

```go
// ent/schema/product.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
"entgo.io/ent/schema/field"
)

type Product struct {
ent.Schema
}

func (Product) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Skip(), // Skip this table from migration
}
}

func (Product) Fields() []ent.Field {
return []ent.Field{
field.Int("id"),
field.String("name"),
}
}
```

```go
// ent/schema/category.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
"entgo.io/ent/schema/field"
)

type Category struct {
ent.Schema
}

func (Category) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Skip(), // Skip this table from migration
}
}

func (Category) Fields() []ent.Field {
return []ent.Field{
field.Int("id"),
field.String("name"),
}
}
```

```go
// ent/schema/order.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)

type Order struct {
ent.Schema
}

func (Order) Fields() []ent.Field {
return []ent.Field{
field.Int("id"),
field.String("order_number"),
}
}

func (Order) Edges() []ent.Edge {
return []ent.Edge{
edge.To("product", Product.Type),
edge.To("category", Category.Type),
}
}
```

2. Run code generation:

```bash
go generate ./ent
```

3. The error occurs.

## Root Cause

The `Tables()` method in `entc/gen/graph.go` has two loops:

**First loop** (lines 639-669): Creates tables and skips those with `entsql.Skip()`:
```go
for _, n := range g.MutableNodes() {
// ...
switch ant := n.EntSQL(); {
case ant == nil:
case ant.Skip:
continue // ← Skipped tables are NOT added to 'tables' map
default:
table.SetAnnotation(ant).SetSchema(ant.Schema)
}
// ...
tables[table.Name] = table // Only non-skipped tables added
}
```

**Second loop** (lines 671+): Processes edges for ALL nodes (including skipped ones):
```go
for _, n := range g.Nodes { // ← ALL nodes, including skipped
for _, e := range n.Edges {
// ...
switch e.Rel.Type {
case O2O, O2M:
owner, ref := tables[e.Rel.Table], tables[n.Table()] // ← nil if skipped
column := fkColumn(e, owner, ref.PrimaryKey[0]) // ← panic!
// ...
}
}
}
```

When a table is skipped, it doesn't exist in the `tables` map, causing `tables[...]` to return `nil`. Accessing `ref.PrimaryKey[0]` or calling methods on `nil` causes the panic.

## Proposed Fix

Add nil checks in the second loop before accessing table properties:

```go
for _, n := range g.Nodes {
// Skip if this node is marked as Skip
if ant := n.EntSQL(); ant != nil && ant.Skip {
continue
}

for _, e := range n.Edges {
if e.IsInverse() {
continue
}
switch e.Rel.Type {
case O2O, O2M:
owner, ref := tables[e.Rel.Table], tables[n.Table()]
if owner == nil || ref == nil {
continue // Skip edge if either table is skipped
}
column := fkColumn(e, owner, ref.PrimaryKey[0])
// ... rest of the code ...

case M2O:
ref, owner := tables[e.Type.Table()], tables[e.Rel.Table]
if ref == nil || owner == nil {
continue
}
column := fkColumn(e, owner, ref.PrimaryKey[0])
// ... rest of the code ...

case M2M:
t1, t2 := tables[n.Table()], tables[e.Type.Table()]
if t1 == nil || t2 == nil {
continue
}
// ... rest of the code ...
}
}
}
```

## Additional Notes

- **Verified on v0.14.4** through testing
- The bug was introduced in v0.14.1 when `entsql.Skip()` annotation was added (PR #4156)
- **Important**: The bug only manifests when edges reference skipped schemas
- Without edges, multiple `entsql.Skip()` annotations work correctly

## Your Environment

| Tech | Version |
| ----------- | ------- |
| Go | 1.25.1 |
| Ent | v0.14.4 |
| Database | - |
| Driver | - |

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.