snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Atlas CI Public / lint (push) Failing after 13s
Atlas CI Public / sync (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:12:36 +05:00
commit 4b3ad0b611
2680 changed files with 719472 additions and 0 deletions

360
cmd/internal/base/base.go Normal file
View File

@@ -0,0 +1,360 @@
// Copyright 2019-present Facebook Inc. 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 base defines shared basic pieces of the ent command.
package base
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"unicode"
"entgo.io/ent/cmd/internal/printer"
"entgo.io/ent/dialect/sql/schema"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/ent/schema/field"
"github.com/spf13/cobra"
)
// IDType is a custom ID implementation for pflag.
type IDType field.Type
// Set implements the Set method of the flag.Value interface.
func (t *IDType) Set(s string) error {
switch s {
case field.TypeInt.String():
*t = IDType(field.TypeInt)
case field.TypeInt64.String():
*t = IDType(field.TypeInt64)
case field.TypeUint.String():
*t = IDType(field.TypeUint)
case field.TypeUint64.String():
*t = IDType(field.TypeUint64)
case field.TypeString.String():
*t = IDType(field.TypeString)
default:
return fmt.Errorf("invalid type %q", s)
}
return nil
}
// Type returns the type representation of the id option for help command.
func (IDType) Type() string {
return fmt.Sprintf("%v", []field.Type{
field.TypeInt,
field.TypeInt64,
field.TypeUint,
field.TypeUint64,
field.TypeString,
})
}
// String returns the default value for the help command.
func (IDType) String() string {
return field.TypeInt.String()
}
// InitCmd returns the init command for ent/c packages.
func InitCmd() *cobra.Command {
c := NewCmd()
c.Use = "init [flags] [schemas]"
c.Short = "initialize an environment with zero or more schemas"
c.Example = examples(
"ent init Example",
"ent init --target entv1/schema User Group",
"ent init --template ./path/to/file.tmpl User",
)
c.Deprecated = `use "ent new" instead`
return c
}
// NewCmd returns the new command for ent/c packages.
func NewCmd() *cobra.Command {
var target, tmplPath string
cmd := &cobra.Command{
Use: "new [flags] [schemas]",
Short: "initialize a new environment with zero or more schemas",
Example: examples(
"ent new Example",
"ent new --target entv1/schema User Group",
"ent new --template ./path/to/file.tmpl User",
),
Args: func(_ *cobra.Command, names []string) error {
for _, name := range names {
if !unicode.IsUpper(rune(name[0])) {
return errors.New("schema names must begin with uppercase")
}
}
return nil
},
Run: func(cmd *cobra.Command, names []string) {
var (
err error
tmpl *template.Template
)
if tmplPath != "" {
tmpl = template.New(filepath.Base(tmplPath)).Funcs(gen.Funcs)
tmpl, err = tmpl.ParseFiles(tmplPath)
} else {
tmpl = template.New("schema").Funcs(gen.Funcs)
tmpl, err = tmpl.Parse(defaultTemplate)
}
if err != nil {
log.Fatalln(fmt.Errorf("ent/new: could not parse template %w", err))
}
if err := newEnv(target, names, tmpl); err != nil {
log.Fatalln(fmt.Errorf("ent/new: %w", err))
}
},
}
cmd.Flags().StringVar(&target, "target", defaultSchema, "target directory for schemas")
cmd.Flags().StringVar(&tmplPath, "template", "", "template to use for new schemas")
return cmd
}
// DescribeCmd returns the describe command for ent/c packages.
func DescribeCmd() *cobra.Command {
return &cobra.Command{
Use: "describe [flags] path",
Short: "print a description of the graph schema",
Example: examples(
"ent describe ./ent/schema",
"ent describe github.com/a8m/x",
),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, path []string) {
graph, err := entc.LoadGraph(path[0], &gen.Config{})
if err != nil {
log.Fatalln(err)
}
printer.Fprint(os.Stdout, graph)
},
}
}
// GenerateCmd returns the generate command for ent/c packages.
func GenerateCmd(postRun ...func(*gen.Config)) *cobra.Command {
var (
cfg gen.Config
storage string
features []string
templates []string
idtype = IDType(field.TypeInt)
cmd = &cobra.Command{
Use: "generate [flags] path",
Short: "generate go code for the schema directory",
Example: examples(
"ent generate ./ent/schema",
"ent generate github.com/a8m/x",
),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, path []string) {
opts := []entc.Option{
entc.Storage(storage),
entc.FeatureNames(features...),
}
for _, tmpl := range templates {
typ := "dir"
if parts := strings.SplitN(tmpl, "=", 2); len(parts) > 1 {
typ, tmpl = parts[0], parts[1]
}
switch typ {
case "dir":
opts = append(opts, entc.TemplateDir(tmpl))
case "file":
opts = append(opts, entc.TemplateFiles(tmpl))
case "glob":
opts = append(opts, entc.TemplateGlob(tmpl))
default:
log.Fatalln("unsupported template type", typ)
}
}
// If the target directory is not inferred from
// the schema path, resolve its package path.
if cfg.Target != "" {
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
if err != nil {
log.Fatalln(err)
}
cfg.Package = pkgPath
}
cfg.IDType = &field.TypeInfo{Type: field.Type(idtype)}
if err := entc.Generate(path[0], &cfg, opts...); err != nil {
log.Fatalln(err)
}
for _, fn := range postRun {
fn(&cfg)
}
},
}
)
cmd.Flags().Var(&idtype, "idtype", "type of the id field")
cmd.Flags().StringVar(&storage, "storage", "sql", "storage driver to support in codegen")
cmd.Flags().StringVar(&cfg.Header, "header", "", "override codegen header")
cmd.Flags().StringVar(&cfg.Target, "target", "", "target directory for codegen")
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
cmd.Flags().StringSliceVarP(&templates, "template", "", nil, "external templates to execute")
// The --idtype flag predates the field.<Type>("id") option.
// See, https://entgo.io/docs/schema-fields#id-field.
cobra.CheckErr(cmd.Flags().MarkHidden("idtype"))
return cmd
}
// SchemaCmd returns DDL to use Ent as an Atlas schema loader.
func SchemaCmd() *cobra.Command {
var (
cfg gen.Config
dlct, version string
features, buildTags []string
hashSymbols bool
cmd = &cobra.Command{
Use: "schema [flags] path",
Short: "dump the DDL for the schema directory",
Example: examples(
"ent schema ./ent/schema --dialect mysql --version 5.6",
"ent schema ./ent/schema --dialect sqlite3",
"ent schema github.com/a8m/x --dialect postgres --version 15",
),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, path []string) {
for _, o := range []entc.Option{
entc.FeatureNames(features...),
entc.BuildTags(buildTags...),
} {
if err := o(&cfg); err != nil {
log.Fatalln(err)
}
}
// If the target directory is not inferred from
// the schema path, resolve its package path.
if cfg.Target != "" {
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
if err != nil {
log.Fatalln(err)
}
cfg.Package = pkgPath
}
g, err := entc.LoadGraph(path[0], &cfg)
if err != nil {
log.Fatalln(err)
}
t, err := g.Tables()
if err != nil {
log.Fatalln(err)
}
v, err := g.Views()
if err != nil {
log.Fatalln(err)
}
ddl, err := schema.DDL(cmd.Context(), schema.DDLArgs{
Dialect: dlct,
Version: version,
HashSymbols: hashSymbols,
Tables: append(t, v...),
})
if err != nil {
log.Fatalln(err)
}
fmt.Println(ddl)
},
}
)
cmd.Flags().StringVar(&dlct, "dialect", "", "database dialect to use")
cmd.Flags().StringVar(&version, "version", "", "database version to assume")
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
cmd.Flags().StringSliceVarP(&buildTags, "build-tags", "", nil, "go build tags to use when loading the schema graph")
cmd.Flags().BoolVar(&hashSymbols, "hash-symbols", false, "whether to hash long symbols")
cobra.CheckErr(cmd.MarkFlagRequired("dialect"))
return cmd
}
// newEnv create a new environment for ent codegen.
func newEnv(target string, names []string, tmpl *template.Template) error {
if err := createDir(target); err != nil {
return fmt.Errorf("create dir %s: %w", target, err)
}
for _, name := range names {
if err := gen.ValidSchemaName(name); err != nil {
return fmt.Errorf("new schema %s: %w", name, err)
}
if fileExists(target, name) {
return fmt.Errorf("new schema %s: already exists", name)
}
b := bytes.NewBuffer(nil)
if err := tmpl.Execute(b, name); err != nil {
return fmt.Errorf("executing template %s: %w", name, err)
}
newFileTarget := filepath.Join(target, strings.ToLower(name+".go"))
if err := os.WriteFile(newFileTarget, b.Bytes(), 0644); err != nil {
return fmt.Errorf("writing file %s: %w", newFileTarget, err)
}
}
return nil
}
func createDir(target string) error {
_, err := os.Stat(target)
if err == nil || !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(target, os.ModePerm); err != nil {
return fmt.Errorf("creating schema directory: %w", err)
}
if target != defaultSchema {
return nil
}
if err := os.WriteFile("ent/generate.go", []byte(genFile), 0644); err != nil {
return fmt.Errorf("creating generate.go file: %w", err)
}
return nil
}
func fileExists(target, name string) bool {
var _, err = os.Stat(filepath.Join(target, strings.ToLower(name+".go")))
return err == nil
}
const (
// default schema package path.
defaultSchema = "ent/schema"
// ent/generate.go file used for "go generate" command.
genFile = "package ent\n\n//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema\n"
// schema template for the "init" command.
defaultTemplate = `package schema
import "entgo.io/ent"
// {{ . }} holds the schema definition for the {{ . }} entity.
type {{ . }} struct {
ent.Schema
}
// Fields of the {{ . }}.
func ({{ . }}) Fields() []ent.Field {
return nil
}
// Edges of the {{ . }}.
func ({{ . }}) Edges() []ent.Edge {
return nil
}
`
)
// examples formats the given examples to the cli.
func examples(ex ...string) string {
for i := range ex {
ex[i] = " " + ex[i] // indent each row with 2 spaces.
}
return strings.Join(ex, "\n")
}

View File

@@ -0,0 +1,55 @@
// Copyright 2019-present Facebook Inc. 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 base
import (
"fmt"
"os"
"path"
"path/filepath"
"golang.org/x/tools/go/packages"
)
// DefaultConfig for loading Go base.
var DefaultConfig = &packages.Config{Mode: packages.NeedName}
// PkgPath returns the Go package name for given target path.
// Even if the existing path is not exist yet in the filesystem.
//
// If base.Config is nil, DefaultConfig will be used to load base.
func PkgPath(config *packages.Config, target string) (string, error) {
if config == nil {
config = DefaultConfig
}
pathCheck, err := filepath.Abs(target)
if err != nil {
return "", err
}
var parts []string
if _, err := os.Stat(pathCheck); os.IsNotExist(err) {
parts = append(parts, filepath.Base(pathCheck))
pathCheck = filepath.Dir(pathCheck)
}
// Try maximum 2 directories above the given
// target to find the root package or module.
for i := 0; i < 2; i++ {
pkgs, err := packages.Load(config, pathCheck)
if err != nil {
return "", fmt.Errorf("load package info: %w", err)
}
if len(pkgs) == 0 || len(pkgs[0].Errors) != 0 {
parts = append(parts, filepath.Base(pathCheck))
pathCheck = filepath.Dir(pathCheck)
continue
}
pkgPath := pkgs[0].PkgPath
for j := len(parts) - 1; j >= 0; j-- {
pkgPath = path.Join(pkgPath, parts[j])
}
return pkgPath, nil
}
return "", fmt.Errorf("root package or module was not found for: %s", target)
}

View File

@@ -0,0 +1,49 @@
// Copyright 2019-present Facebook Inc. 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 base
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/packages/packagestest"
)
func TestPkgPath(t *testing.T) { packagestest.TestAll(t, testPkgPath) }
func testPkgPath(t *testing.T, x packagestest.Exporter) {
e := packagestest.Export(t, x, []packagestest.Module{
{
Name: "golang.org/x",
Files: map[string]any{
"x.go": "package x",
"y/y.go": "package y",
},
},
})
defer e.Cleanup()
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "x.go"))
target := filepath.Join(e.Config.Dir, "ent")
pkgPath, err := PkgPath(e.Config, target)
require.NoError(t, err)
require.Equal(t, "golang.org/x/ent", pkgPath)
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "y/y.go"))
target = filepath.Join(e.Config.Dir, "ent")
pkgPath, err = PkgPath(e.Config, target)
require.NoError(t, err)
require.Equal(t, "golang.org/x/y/ent", pkgPath)
target = filepath.Join(e.Config.Dir, "z/ent")
pkgPath, err = PkgPath(e.Config, target)
require.NoError(t, err)
require.Equal(t, "golang.org/x/y/z/ent", pkgPath)
target = filepath.Join(e.Config.Dir, "z/e/n/t")
pkgPath, err = PkgPath(e.Config, target)
require.Error(t, err)
require.Empty(t, pkgPath)
}

View File

@@ -0,0 +1,141 @@
// Copyright 2019-present Facebook Inc. 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 printer
import (
"fmt"
"io"
"reflect"
"strconv"
"strings"
"entgo.io/ent/entc/gen"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
)
// A Config controls the output of Fprint.
type Config struct {
io.Writer
}
// Print prints a table description of the graph to the given writer.
func (p Config) Print(g *gen.Graph) {
for _, n := range g.Nodes {
p.node(n)
}
}
// Fprint executes "pretty-printer" on the given writer.
func Fprint(w io.Writer, g *gen.Graph) {
Config{Writer: w}.Print(g)
}
// node returns description of a type. The format of the description is:
//
// Type:
// <Fields Table>
//
// <Edges Table>
func (p Config) node(t *gen.Type) {
var (
b strings.Builder
id []*gen.Field
table = tablewriter.NewWriter(&b)
header = []string{"Field", "Type", "Unique", "Optional", "Nillable", "Default", "UpdateDefault", "Immutable", "StructTag", "Validators", "Comment"}
)
b.WriteString(t.Name + ":\n")
table.Options(
tablewriter.WithHeaderConfig(tw.CellConfig{
Padding: tw.CellPadding{
Global: tw.Padding{
Left: tw.Space,
Right: tw.Space,
},
},
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
}),
tablewriter.WithRendition(tw.Rendition{
Symbols: tw.NewSymbols(tw.StyleASCII),
}),
)
table.Header(header)
var alignment = make([]tw.Align, 0)
if t.ID != nil {
id = append(id, t.ID)
}
for _, f := range append(id, t.Fields...) {
v := reflect.ValueOf(*f)
row := make([]string, len(header))
for i := 0; i < len(row)-1; i++ {
field := v.FieldByNameFunc(func(name string) bool {
// The first field is mapped from "Name" to "Field".
return name == "Name" && i == 0 || name == header[i]
})
row[i] = fmt.Sprint(field.Interface())
_, err := strconv.Atoi(row[i])
if err == nil {
alignment = append(alignment, tw.AlignRight)
} else {
alignment = append(alignment, tw.AlignLeft)
}
}
row[len(row)-1] = f.Comment()
_ = table.Append(row)
table.Options(
tablewriter.WithRowAlignmentConfig(
tw.CellAlignment{PerColumn: alignment},
),
)
}
err := table.Render()
if err != nil {
return
}
// Create new table for edges
table = tablewriter.NewWriter(&b)
table.Options(
tablewriter.WithHeaderConfig(tw.CellConfig{
Formatting: tw.CellFormatting{AutoFormat: tw.Off},
Padding: tw.CellPadding{
Global: tw.Padding{
Left: tw.Space,
Right: tw.Space,
},
},
}),
tablewriter.WithRendition(tw.Rendition{
Symbols: tw.NewSymbols(tw.StyleASCII),
}),
)
table.Header([]string{"Edge", "Type", "Inverse", "BackRef", "Relation", "Unique", "Optional", "Comment"})
hasEdges := false
for _, e := range t.Edges {
hasEdges = true
err := table.Append([]string{
e.Name,
e.Type.Name,
strconv.FormatBool(e.IsInverse()),
e.Inverse,
e.Rel.Type.String(),
strconv.FormatBool(e.Unique),
strconv.FormatBool(e.Optional),
e.Comment(),
})
if err != nil {
return
}
}
if hasEdges {
err := table.Render()
if err != nil {
return
}
}
io.WriteString(p, strings.ReplaceAll(b.String(), "\n", "\n\t")+"\n")
}

View File

@@ -0,0 +1,177 @@
// Copyright 2019-present Facebook Inc. 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 printer
import (
"strings"
"testing"
"entgo.io/ent/entc/gen"
"entgo.io/ent/schema/field"
"github.com/stretchr/testify/assert"
)
func TestPrinter_Print(t *testing.T) {
tests := []struct {
input *gen.Graph
out string
}{
{
input: &gen.Graph{
Nodes: []*gen.Type{
{
Name: "User",
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
Fields: []*gen.Field{
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
{Name: "created_at", Type: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Immutable: true},
},
},
},
},
out: `
User:
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| id | int | false | false | false | false | false | false | | 0 | |
| name | string | false | false | false | false | false | false | | 1 | |
| age | int | false | false | true | false | false | false | | 0 | |
| created_at | time.Time | false | false | true | false | false | true | | 0 | |
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
`,
},
{
input: &gen.Graph{
Nodes: []*gen.Type{
{
Name: "User",
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
Edges: []*gen.Edge{
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
},
},
},
},
out: `
User:
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| id | int | false | false | false | false | false | false | | 0 | |
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
+--------+-------+---------+---------+----------+--------+----------+---------+
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
+--------+-------+---------+---------+----------+--------+----------+---------+
| groups | Group | false | | M2M | false | true | |
| spouse | User | false | | O2O | true | false | |
+--------+-------+---------+---------+----------+--------+----------+---------+
`,
},
{
input: &gen.Graph{
Nodes: []*gen.Type{
{
Name: "User",
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
Fields: []*gen.Field{
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
},
Edges: []*gen.Edge{
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
},
},
},
},
out: `
User:
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| id | int | false | false | false | false | false | false | | 0 | |
| name | string | false | false | false | false | false | false | | 1 | |
| age | int | false | false | true | false | false | false | | 0 | |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
+--------+-------+---------+---------+----------+--------+----------+---------+
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
+--------+-------+---------+---------+----------+--------+----------+---------+
| groups | Group | false | | M2M | false | true | |
| spouse | User | false | | O2O | true | false | |
+--------+-------+---------+---------+----------+--------+----------+---------+
`,
},
{
input: &gen.Graph{
Nodes: []*gen.Type{
{
Name: "User",
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
Fields: []*gen.Field{
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
},
Edges: []*gen.Edge{
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
},
},
{
Name: "Group",
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
Fields: []*gen.Field{
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}},
},
Edges: []*gen.Edge{
{Name: "users", Type: &gen.Type{Name: "User"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
},
},
},
},
out: `
User:
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| id | int | false | false | false | false | false | false | | 0 | |
| name | string | false | false | false | false | false | false | | 1 | |
| age | int | false | false | true | false | false | false | | 0 | |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
+--------+-------+---------+---------+----------+--------+----------+---------+
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
+--------+-------+---------+---------+----------+--------+----------+---------+
| groups | Group | false | | M2M | false | true | |
| spouse | User | false | | O2O | true | false | |
+--------+-------+---------+---------+----------+--------+----------+---------+
Group:
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
| id | int | false | false | false | false | false | false | | 0 | |
| name | string | false | false | false | false | false | false | | 0 | |
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
+-------+------+---------+---------+----------+--------+----------+---------+
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
+-------+------+---------+---------+----------+--------+----------+---------+
| users | User | false | | M2M | false | true | |
+-------+------+---------+---------+----------+--------+----------+---------+
`,
},
}
for _, tt := range tests {
b := &strings.Builder{}
Fprint(b, tt.input)
assert.Equal(t, tt.out, "\n"+b.String())
}
}