Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ touches the same area may be assuming the behaviour we replaced.
| **Create read-back** | `sqlgraph.CreateSpec` carries `Returning`, `ScanValues` and `Assign`, and a create reads back the row it stored: by the INSERT, with `RETURNING`, where the dialect has it, and by a query after it on MySQL or with a conflict clause. `sql/returning` gives a create builder `Stored`, which asks for it, in a `CreateBulk` as well: a batch reads every row back, matched to its node by the Id the caller gave, or by its place when the database made the Ids up. Upstream's INSERT answers with the Id and nothing else. | #9, #11 |
| **Mutation expressions** | A generated mutation assigns a field a SQL expression (`SetFieldExpr`, `FieldExpr`, `ExprFields`; `SetField` hands one over), which an update writes and `Fields`/`Field` report to hooks. A validated, immutable, id or edge field refuses one, as does a create. `sqlgraph` writes a `sql.Querier` in a `FieldSpec` as SQL, even for a JSON column, which upstream would encode. Upstream's only route for an expression is `Modify`, which the mutation never sees. | #1 |
| **Set after clear** | A mutation's `SetX` drops a previous clear of an optional field, so `ClearX().SetX(v)` writes `v` once. Upstream emits `x = NULL, x = ?`, which PostgreSQL refuses. The same fix was offered upstream; drop this row once upstream has it. | #13 |
| **A field named after a mutation method** | A field's `Clear` and `Cleared` are renamed when every mutation has a method of that name, as its `Add`, `Set` and the rest already were -- `ClearFieldField` and `FieldFieldCleared` for a field named `field` -- and everything that calls a field's method calls it by the namer: the dispatchers that take a field by its name (`AddedField`, `AddField`, `ClearField`), the update builders, and an update's check of a required edge (`EdgeEdgeCleared`). `mutMethods` lists every method a generated mutation has beside `ent.Mutation`'s, which upstream's list does not; one of them, `Ids`, is upstream's `IDs`, which a field named `ids` meets only here. Upstream has the rest of this bug; drop the row once it has the fix. | #21 |
| **Time: zone** | Every `time.Time` is UTC at both edges. `bindArgs` moves an argument to UTC; `Rows.Scan` moves a scanned value to UTC; `field.Time`'s `Default`/`UpdateDefault` answer in UTC. | `7d10ad284`, `c6003ab94`, `1d1a9d88b` |
| **Time: precision** | Time columns keep six fractional-second digits by default; `field.Time().Precision(n)` says otherwise. | `cede1c296` |
| **Time: MySql column** | A time field is a `datetime`, not a `timestamp` -- no session `time_zone` conversion, no 2038. | `5d232985b` |
Expand Down
146 changes: 146 additions & 0 deletions entc/gen/mutnames_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2026 Seunghyun Hwang. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.

package gen

import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"strings"
"testing"
"unicode"

"github.com/protobuf-orm/ent/entc/load"
"github.com/protobuf-orm/ent/schema/field"
"github.com/stretchr/testify/require"
)

// fieldMethods are the names of a field's own mutation methods.
var fieldMethods = map[string]func(Field) string{
"MutationGet": Field.MutationGet,
"MutationGetOld": Field.MutationGetOld,
"MutationSet": Field.MutationSet,
"MutationReset": Field.MutationReset,
"MutationClear": Field.MutationClear,
"MutationCleared": Field.MutationCleared,
"MutationAdd": Field.MutationAdd,
"MutationAdded": Field.MutationAdded,
"MutationAppend": Field.MutationAppend,
"MutationAppended": Field.MutationAppended,
}

// TestField_MutationNames names a field after each method every mutation has,
// and after what is left of one once a field method's prefix or suffix is
// taken off it -- `field` for AddField, `edge` for EdgeCleared -- and requires
// that none of that field's own methods is one of them.
//
// One that is would be declared twice, or would be shadowed by the ent
// package's mutation, and the client would not compile: the field method and
// the generic one taking a field by its name are different functions of the
// same name. Two namers did not ask (Clear, Cleared), and the dispatchers
// that call a field's method by name called the generic one instead.
func TestField_MutationNames(t *testing.T) {
typ := &Type{Id: &Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}}}
typ.Id.typ = typ

affixes := [][2]string{
{"", ""}, {"Old", ""}, {"Set", ""}, {"Reset", ""}, {"Clear", ""}, {"", "Cleared"},
{"Add", ""}, {"Added", ""}, {"Append", ""}, {"Appended", ""},
}
tried := map[string]bool{}
for m := range mutMethods {
for _, a := range affixes {
rest, ok := strings.CutPrefix(m, a[0])
if !ok {
continue
}
if rest, ok = strings.CutSuffix(rest, a[1]); !ok || rest == "" || !unicode.IsUpper(rune(rest[0])) {
continue
}
name := snakeWords(rest)
if pascal(name) != rest || tried[name] {
continue
}
tried[name] = true

f := Field{Name: name, typ: typ, Type: &field.TypeInfo{Type: field.TypeInt}}
for namer, method := range fieldMethods {
got := method(f)
require.False(t, mutMethods[got], "%s of a field named %q is %s, which every mutation has", namer, name, got)
}
}
}
for _, name := range []string{"field", "fields", "edge", "ids", "where", "op", "type"} {
require.True(t, tried[name], "%q was not tried", name)
}
}

// snakeWords is a name whose words are the PascalCase words of s, which is
// pascal undone: WhereP is where_p.
func snakeWords(s string) string {
var b strings.Builder
for i, r := range s {
if i > 0 && unicode.IsUpper(r) {
b.WriteByte('_')
}
b.WriteRune(unicode.ToLower(r))
}
return b.String()
}

// TestMutationMethods generates a type with nothing of its own and requires
// that every exported method its mutations have, in the type's package and in
// the ent package, is one the field namers keep clear of.
//
// mutMethods is ent.Mutation's methods and a list of the others, and the list
// is the part that can go stale: it missed Ids, which a field named `ids`
// shadowed, since this fork spells upstream's IDs as a word.
func TestMutationMethods(t *testing.T) {
target := filepath.Join(t.TempDir(), "ent")
graph, err := NewGraph(&Config{
Package: "entc/gen",
Target: target,
Storage: drivers[0],
IdType: &field.TypeInfo{Type: field.TypeInt},
}, &load.Schema{Name: "T1"})
require.NoError(t, err)
require.NoError(t, graph.Gen())

methods := map[string]bool{}
for path, receiver := range map[string]string{
filepath.Join(target, "t1", "mutation.go"): "Mutation",
filepath.Join(target, "mutation.go"): "T1Mutation",
} {
f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
require.NoError(t, err)
for _, d := range f.Decls {
fn, ok := d.(*ast.FuncDecl)
if !ok || fn.Recv == nil || !fn.Name.IsExported() || named(fn.Recv.List[0].Type) != receiver {
continue
}
methods[fn.Name.Name] = true
}
}
require.Contains(t, methods, "Ids", "the generated mutation was not read")
for m := range methods {
// The id's own getter, which no other field can be named after.
if m == "Id" {
continue
}
require.True(t, mutMethods[m], "every mutation has %s, and a field may be given a method of that name", m)
}
}

// named is the name of a receiver's type, through its pointer.
func named(e ast.Expr) string {
if s, ok := e.(*ast.StarExpr); ok {
e = s.X
}
if id, ok := e.(*ast.Ident); ok {
return id.Name
}
return ""
}
2 changes: 1 addition & 1 deletion entc/gen/template/builder/setter.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ in the LICENSE file in the root directory of this source tree.
{{ $func := print "Clear" $f.StructField }}
// {{ $func }} clears the value of the "{{ $f.Name }}" field.
func ({{ $receiver }} *{{ $builder }}) {{ $func }}() *{{ $builder }} {
{{ $receiver }}.mutation.{{ $func }}()
{{ $receiver }}.mutation.{{ $f.MutationClear }}()
return {{ $receiver }}
}
{{ end }}
Expand Down
4 changes: 2 additions & 2 deletions entc/gen/template/builder/update.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func ({{ $receiver }} *{{ $onebuilder }}) ExecX(ctx context.Context) {
func ({{ $receiver }} *{{ $builder }}) defaults() {{ if $runtimeRequired }}error{{ end }}{
{{- range $f := $.Fields }}
{{- if $f.UpdateDefault }}
if _, ok := {{ $mutation }}.{{ $f.MutationGet }}(); !ok {{ if $f.Optional }} && !{{ $mutation }}.{{ $f.StructField }}Cleared() {{ end }} {
if _, ok := {{ $mutation }}.{{ $f.MutationGet }}(); !ok {{ if $f.Optional }} && !{{ $mutation }}.{{ $f.MutationCleared }}() {{ end }} {
{{- /* An expression is what the field was set to, and setting the default would replace it. */}}
{{- if $f.SupportsMutationExpr }}
if _, ok := {{ $mutation }}.FieldExpr({{ $.Package }}.{{ $f.Constant }}); !ok {
Expand Down Expand Up @@ -272,7 +272,7 @@ func ({{ $receiver }} *{{ $onebuilder }}) ExecX(ctx context.Context) {
{{- end }}
{{- range $e := $.Edges }}
{{- if and $e.Unique (not $e.Optional) }}
if {{ $mutation }}.{{ $e.StructField }}Cleared() && len({{ $mutation }}.{{ $e.StructField }}Ids()) > 0 {
if {{ $mutation }}.{{ $e.MutationCleared }}() && len({{ $mutation }}.{{ $e.StructField }}Ids()) > 0 {
return errors.New(`{{ $pkg }}: clearing a required unique edge "{{ $.Name }}.{{ $e.Name }}"`)
}
{{- end }}
Expand Down
2 changes: 1 addition & 1 deletion entc/gen/template/dialect/sql/update.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func ({{ $receiver }} *{{ $builder }}) sqlSave(ctx context.Context) (_node {{ if
{{- end }}
{{- end }}
{{- if $f.Optional }}
if {{ $mutation }}.{{ $f.StructField }}Cleared() {
if {{ $mutation }}.{{ $f.MutationCleared }}() {
_spec.ClearField({{ $.Package }}.{{ $f.Constant }}, field.{{ $f.Type.ConstName }})
}
{{- end }}
Expand Down
6 changes: 3 additions & 3 deletions entc/gen/template/mutation.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ func (m *Mutation) AddedField(name string) (ent.Value, bool) {
{{- range $f := $.Fields }}
{{- if $f.SupportsMutationAdd }}
case {{ $f.Constant }}:
return m.Added{{ $f.StructField }}()
return m.{{ $f.MutationAdded }}()
{{- end }}
{{- end }}
}
Expand All @@ -501,7 +501,7 @@ func (m *Mutation) AddField(name string, value ent.Value) error {
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.Add{{ $f.StructField }}(v)
m.{{ $f.MutationAdd }}(v)
return nil
{{- end }}
{{- end }}
Expand Down Expand Up @@ -543,7 +543,7 @@ func (m *Mutation) ClearField(name string) error {
{{- range $f := $.Fields }}
{{- if $f.Optional }}
case {{ $f.Constant }}:
m.Clear{{ $f.StructField }}()
m.{{ $f.MutationClear }}()
return nil
{{- end }}
{{- end }}
Expand Down
31 changes: 26 additions & 5 deletions entc/gen/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -1175,9 +1175,18 @@ func (f Field) EntSql() *entsql.Annotation {
return sqlAnnotate(f.Annotations)
}

// mutMethods returns the method names of mutation interface.
// mutMethods holds the method names of the mutation interface, and of the
// methods every generated mutation has beside them. A field's own method of
// one of these names would be declared twice, or shadowed by the ent
// package's mutation, which embeds the entity package's. Ids is one: upstream
// spells it IDs, and spelled by this fork's rule it is also the getter of a
// field named "ids".
var mutMethods = func() map[string]bool {
names := map[string]bool{"Client": true, "Tx": true, "Where": true, "SetOp": true}
names := map[string]bool{
"Client": true, "Tx": true, "Ids": true,
"Where": true, "WhereP": true, "Predicates": true, "SetOp": true,
"SetFieldExpr": true, "FieldExpr": true, "ExprFields": true,
}
t := reflect.TypeOf(new(ent.Mutation)).Elem()
for i := 0; i < t.NumMethod(); i++ {
names[t.Method(i).Name] = true
Expand Down Expand Up @@ -1228,14 +1237,26 @@ func (f Field) MutationSet() string {
}

// MutationClear returns the method name for clearing the field value.
// The default name is "Clear<FieldName>". If the method conflicts with
// the mutation methods, suffix the method with "Field".
func (f Field) MutationClear() string {
return "Clear" + f.StructField()
name := "Clear" + f.StructField()
if mutMethods[name] {
name += "Field"
}
return name
}

// MutationCleared returns the method name for indicating if the field
// was cleared in the mutation.
// was cleared in the mutation. The default name is "<FieldName>Cleared".
// If the method conflicts with the mutation methods, add "Field" after
// the field name, as an edge's adds "Edge".
func (f Field) MutationCleared() string {
return f.StructField() + "Cleared"
name := f.StructField() + "Cleared"
if mutMethods[name] {
return f.StructField() + "FieldCleared"
}
return name
}

// MutationAdd returns the method name for adding a value to the field.
Expand Down
Loading
Loading