diff --git a/dialect/sql/builder.go b/dialect/sql/builder.go index 88c57a937..6e16ae0b2 100644 --- a/dialect/sql/builder.go +++ b/dialect/sql/builder.go @@ -1924,7 +1924,7 @@ func (b *Builder) Quote(ident string) string { // If it was quoted with the wrong // identifier character. if strings.Contains(ident, "`") { - return strings.Replace(ident, "`", `"`, -1) + return strings.ReplaceAll(ident, "`", `"`) } return strconv.Quote(ident) // An identifier for unknown dialect. @@ -1944,7 +1944,7 @@ func (b *Builder) Ident(s string) *Builder { case (isFunc(s) || isModifier(s)) && b.postgres(): // Modifiers and aggregation functions that // were called without dialect information. - b.WriteString(strings.Replace(s, "`", `"`, -1)) + b.WriteString(strings.ReplaceAll(s, "`", `"`)) default: b.WriteString(s) } diff --git a/dialect/sql/schema/schema.go b/dialect/sql/schema/schema.go index 6096200e8..38d326eef 100644 --- a/dialect/sql/schema/schema.go +++ b/dialect/sql/schema/schema.go @@ -249,7 +249,7 @@ func (c *Column) defaultValue(b *sql.ColumnBuilder) { attr += strconv.FormatBool(v) case string: // Escape single quote by replacing each with 2. - attr += fmt.Sprintf("'%s'", strings.Replace(v, "'", "''", -1)) + attr += fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", "''")) default: attr += fmt.Sprint(v) } @@ -361,7 +361,7 @@ func (r ReferenceOption) ConstName() string { if r == NoAction { return "" } - return strings.Replace(strings.Title(strings.ToLower(string(r))), " ", "", -1) + return strings.ReplaceAll(strings.Title(strings.ToLower(string(r))), " ", "") } // Index definition for table index. diff --git a/entc/entc.go b/entc/entc.go index 25f5d40f7..57dd3236c 100644 --- a/entc/entc.go +++ b/entc/entc.go @@ -7,6 +7,7 @@ package entc import ( + "errors" "fmt" "go/token" "path" @@ -14,7 +15,10 @@ import ( "strings" "github.com/facebook/ent/entc/gen" + "github.com/facebook/ent/entc/internal" "github.com/facebook/ent/entc/load" + + "golang.org/x/tools/go/packages" ) // LoadGraph loads the schema package from the given schema path, @@ -76,14 +80,7 @@ func Generate(schemaPath string, cfg *gen.Config, options ...Option) (err error) _ = undo() } }() - graph, err := LoadGraph(schemaPath, cfg) - if err != nil { - return err - } - if err := normalizePkg(cfg); err != nil { - return err - } - return graph.Gen() + return generate(schemaPath, cfg) } func normalizePkg(c *gen.Config) error { @@ -163,3 +160,35 @@ func templateOption(next func(t *gen.Template) (*gen.Template, error)) Option { return nil } } + +// generate loads the given schema and run codegen. +func generate(schemaPath string, cfg *gen.Config) error { + graph, err := LoadGraph(schemaPath, cfg) + if err != nil { + if err := mayRecover(err, schemaPath, cfg); err != nil { + return err + } + if graph, err = LoadGraph(schemaPath, cfg); err != nil { + return err + } + } + if err := normalizePkg(cfg); err != nil { + return err + } + return graph.Gen() +} + +func mayRecover(err error, schemaPath string, cfg *gen.Config) error { + if enabled, _ := cfg.FeatureEnabled(gen.FeatureSnapshot.Name); !enabled { + return err + } + if errors.As(err, &packages.Error{}) || !internal.IsBuildError(err) { + return err + } + // If the build error comes from the schema package. + if err := internal.CheckDir(schemaPath); err != nil { + return fmt.Errorf("schema failure: %w", err) + } + target := filepath.Join(cfg.Target, "internal/schema.go") + return (&internal.Snapshot{Path: target, Config: cfg}).Restore() +} diff --git a/entc/gen/feature.go b/entc/gen/feature.go index 53c6ef7d5..5c7592cd6 100644 --- a/entc/gen/feature.go +++ b/entc/gen/feature.go @@ -32,10 +32,22 @@ var ( }, } + // FeatureSnapshot stores a snapshot of ent/schema and auto-solve merge-conflict (issue #852). + FeatureSnapshot = Feature{ + Name: "schema/snapshot", + Stage: Experimental, + Default: false, + Description: "Schema snapshot stores a snapshot of ent/schema and auto-solve merge-conflict (issue #852)", + cleanup: func(c *Config) error { + return os.RemoveAll(filepath.Join(c.Target, "internal")) + }, + } + // AllFeatures holds a list of all feature-flags. AllFeatures = []Feature{ FeaturePrivacy, FeatureEntQL, + FeatureSnapshot, } ) diff --git a/entc/gen/graph.go b/entc/gen/graph.go index 8b6151039..8eb056fd2 100644 --- a/entc/gen/graph.go +++ b/entc/gen/graph.go @@ -7,6 +7,7 @@ package gen import ( "bytes" + "encoding/json" "fmt" "go/parser" "go/token" @@ -442,6 +443,35 @@ func (g *Graph) SupportMigrate() bool { return g.Storage.SchemaMode.Support(Migrate) } +// Snapshot holds the information for storing the schema snapshot. +type Snapshot struct { + Schema string + Package string + Schemas []*load.Schema + Features []string +} + +// MarshalSchema returns a JSON string represents the graph schema in loadable format. +func (g *Graph) SchemaSnapshot() (string, error) { + schemas := make([]*load.Schema, len(g.Nodes)) + for i := range g.Nodes { + schemas[i] = g.Nodes[i].schema + } + snap := Snapshot{ + Schema: g.Schema, + Package: g.Package, + Schemas: schemas, + } + for _, feat := range g.Features { + snap.Features = append(snap.Features, feat.Name) + } + out, err := json.Marshal(snap) + if err != nil { + return "", err + } + return string(out), nil +} + func (g *Graph) typ(name string) (*Type, bool) { for _, n := range g.Nodes { if name == n.Name { diff --git a/entc/gen/internal/bindata.go b/entc/gen/internal/bindata.go index e6b921d18..aa129cd86 100644 --- a/entc/gen/internal/bindata.go +++ b/entc/gen/internal/bindata.go @@ -44,6 +44,7 @@ // template/header.tmpl // template/hook.tmpl // template/import.tmpl +// template/internal.tmpl // template/meta.tmpl // template/migrate/migrate.tmpl // template/migrate/schema.tmpl @@ -1009,6 +1010,26 @@ func templateImportTmpl() (*asset, error) { return a, nil } +var _templateInternalTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x8f\xc1\xae\xd3\x30\x10\x45\xf7\xfe\x8a\xab\x27\x56\x0f\x70\x1e\xdd\xb1\x60\x51\xb5\x45\x54\x42\x2d\x52\xfb\x01\x75\xec\x49\x6d\xd5\x78\x22\x7b\x52\x54\x45\xfe\x77\x94\xa4\x20\xc4\xca\xf2\x1c\x5f\xcf\xb9\xe3\xd8\xbc\xaa\x0d\xf7\x8f\x1c\xae\x5e\xb0\x7a\xfb\xf4\xf9\x63\x9f\xa9\x50\x12\x7c\x35\x96\x5a\xe6\x1b\xf6\xc9\x6a\xac\x63\xc4\xfc\xa8\x60\xe2\xf9\x4e\x4e\xab\xb3\x0f\x05\x85\x87\x6c\x09\x96\x1d\x21\x14\xc4\x60\x29\x15\x72\x18\x92\xa3\x0c\xf1\x84\x75\x6f\xac\x27\xac\xf4\xdb\x1f\x8a\x8e\x87\xe4\x54\x48\x33\xff\xbe\xdf\xec\x0e\xa7\x1d\xba\x10\x09\xcf\x59\x66\x16\xb8\x90\xc9\x0a\xe7\x07\xb8\x83\xfc\xb3\x4c\x32\x91\x56\xaf\x4d\xad\x4a\x8d\x23\x1c\x75\x21\x11\x5e\x42\x12\xca\xc9\xc4\xa6\x58\x4f\x3f\xcd\x0b\x9e\xfc\x57\x10\x8f\x77\xfa\x1b\x99\x49\xa9\xd6\x71\x84\x5e\x0e\x8a\x85\x50\x6b\xd3\x60\x33\xf9\x5f\x29\x51\x36\x42\x0e\xed\x03\x94\xc4\x7e\xc0\xf6\x88\xc3\xf1\x8c\xdd\x76\x7f\xd6\x53\x20\xb9\xf9\xd7\xa6\xc1\xfb\x76\x08\xd1\x41\x98\x63\x99\x07\x3f\x8c\xbd\x99\xeb\x54\x61\xd1\x80\xe7\xe8\x0a\x0c\x22\x1b\x67\xda\x48\xb8\x53\x2e\x81\xd3\x52\x87\x10\x8d\x50\x11\x2c\xb6\x5a\xf5\xff\xe5\x95\xb2\x9c\x8a\xe0\x34\x73\x7c\xc1\x65\x12\x5f\x6e\xa7\x64\xfa\xe2\x59\x50\xeb\x45\xfd\xf5\xfa\x1d\x00\x00\xff\xff\x05\x64\x78\xff\xd0\x01\x00\x00") + +func templateInternalTmplBytes() ([]byte, error) { + return bindataRead( + _templateInternalTmpl, + "template/internal.tmpl", + ) +} + +func templateInternalTmpl() (*asset, error) { + bytes, err := templateInternalTmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "template/internal.tmpl", size: 464, mode: os.FileMode(420), modTime: time.Unix(1, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templateMetaTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\x5f\x6f\xe3\xb8\x11\x7f\x96\x3f\xc5\x40\xd0\x02\x71\x90\xc8\x7b\xf7\xd6\x00\x7e\xd8\x26\xd9\x4b\xd0\x43\x70\xc0\x26\xf7\x52\x14\x05\x2d\x8e\x2c\x22\x14\xe9\x23\xa9\xe4\x52\xc1\xdf\xbd\xe0\x90\x92\xa8\xd8\xde\xdd\xdb\xa2\x2f\x86\x44\x0e\xe7\xcf\x8f\x33\xbf\x19\xb9\xef\x57\xe7\x8b\x6b\xbd\x7b\x33\x62\xdb\x38\xf8\xf9\xe3\x4f\x7f\xbb\xdc\x19\xb4\xa8\x1c\x7c\x66\x15\x6e\xb4\x7e\x86\x7b\x55\x95\xf0\x49\x4a\x20\x21\x0b\x7e\xdf\xbc\x20\x2f\x17\x8f\x8d\xb0\x60\x75\x67\x2a\x84\x4a\x73\x04\x61\x41\x8a\x0a\x95\x45\x0e\x9d\xe2\x68\xc0\x35\x08\x9f\x76\xac\x6a\x10\x7e\x2e\x3f\x0e\xbb\x50\xeb\x4e\xf1\x85\x50\xb4\xff\xeb\xfd\xf5\xed\xc3\x97\x5b\xa8\x85\x44\x88\x6b\x46\x6b\x07\x5c\x18\xac\x9c\x36\x6f\xa0\x6b\x70\x89\x31\x67\x10\xcb\xc5\xf9\x6a\xbf\x5f\x2c\xfa\x1e\x38\xd6\x42\x21\xe4\x2d\x3a\x96\x43\x58\xbc\x84\x57\xe1\x1a\xc0\x3f\x1d\x2a\x0e\x05\xe4\xbf\xb1\xea\x99\x6d\x31\x87\xa2\x8c\x8f\x70\xb9\xdf\x2f\xb2\xbe\x07\x87\xed\x4e\x32\x87\x90\x37\xc8\x38\x9a\x1c\x4a\xaf\xa5\xef\xc1\x9f\x8d\x46\x26\x21\xd1\xee\xb4\x71\x39\x14\xb4\x55\x69\x65\x1d\x9c\x2d\xb2\xd5\x0a\x7e\x65\x1b\x94\xd0\x68\xc9\x2d\x45\x61\x9d\x11\x6a\x0b\x92\x96\x39\x2a\xed\xfc\xab\xdf\xe9\x7b\x90\xfa\x15\x0d\x14\xe5\x03\x6b\x11\xf6\x7b\x70\x6f\xbb\x31\x7c\xce\x1c\xdb\x30\x8b\xe5\x22\x0b\x3a\xd7\x90\xf7\x3d\x14\x65\x78\xdb\xef\x73\xb2\x47\x4b\xf7\x37\xe5\xb5\xf7\x81\x29\xe7\xd5\x1c\x58\x9f\xd9\x15\x1c\x6a\x81\x92\x1f\x31\x74\x4c\xd9\x60\xf6\xfe\xa6\xfc\xe2\xb4\x61\x5b\xfc\x07\xbe\x05\xf3\x1e\x62\xc3\xd4\x16\xa1\xa8\xe1\x6a\x0d\x45\xf9\xd9\x2b\xb6\x1e\x94\x8c\x76\x8b\x60\xc9\xef\xd5\xa9\xd6\x45\x36\xf8\x1e\x04\xbe\xe9\xf4\x04\x56\x3d\xa2\x75\x2a\x8a\x6c\xa6\x37\xfa\x5f\x1f\xf5\x7e\xb8\x5c\x7f\x24\x46\x82\x21\x92\x5b\xbe\xc5\x34\x10\xe4\xdb\xb0\x83\xc7\xe3\xa0\xfd\xbf\x10\x06\x8e\x61\xd0\x49\xe5\x5f\x84\x82\xb6\x73\xcc\x09\xad\xec\x10\xc7\xa0\x37\x86\x31\x1e\x3b\x12\x40\xe1\xda\x9d\xf4\x3e\xee\x8c\x50\xae\x86\x9c\x0b\x26\xb1\x72\xab\x0f\x76\xe5\xeb\x62\x55\x45\xc7\xad\xaf\x80\x08\x07\xc4\x02\xf8\x73\x4c\xee\xa0\x86\x32\x7b\x49\x69\x1f\x16\x4e\xab\x7d\x61\x46\xb0\x8d\xc4\xf7\x6a\xfb\x1e\x44\x0d\x0d\xb3\x8f\x73\xd5\x5f\xb3\x38\x2f\xb8\x6f\x59\xae\x3b\x55\x11\x5a\xdf\x6f\x19\x00\xe0\xbb\x8c\xaf\xce\xe1\x8e\x59\x60\x0e\x24\x32\xeb\x40\x2b\x8c\x19\x77\xa6\xb4\x03\x54\x5d\xbb\x0c\x04\xc3\xb1\x66\x9d\x74\xf0\xc2\x64\x87\x40\x94\x34\x66\xa0\x7d\x57\x17\xc1\x33\xaa\xa6\x27\x8b\xe6\x86\x68\x8b\x87\x8d\xe1\xc4\x1a\xd8\x6e\x47\x94\x15\x17\xbc\x78\x10\x89\xee\x79\xe1\x86\xd9\x9b\x68\xf8\x6a\x0d\x35\x93\x16\x83\xcc\xac\x22\xeb\xb9\x61\x46\x5a\xcb\xe1\x20\x45\x52\xd4\xe5\xbd\xbd\xa5\x70\x82\x1b\x89\xe6\x35\x38\xd3\x61\x6a\xfb\x3d\x46\xbf\xa0\x42\xe3\x71\xdc\x4a\xbd\x61\x12\xc6\x64\x80\x5a\x1b\x68\xb4\x7e\xb6\x17\x1e\x19\xc1\x99\xd3\xc6\x92\x07\x3b\x2d\x45\xf5\x06\x55\x83\xd5\x33\x1a\x3b\x42\x26\x6a\xd0\x66\x66\xbf\x28\xef\x98\xfd\x7d\x3a\x5d\x94\x0f\x5d\x7b\xe7\x95\x86\xc7\xdf\x82\xa6\xf1\x5e\x2f\xa1\x50\x83\x00\x01\x3f\x8a\x27\x22\x74\x01\x07\x87\x0f\x15\xac\x81\x71\x9e\xbc\xff\x94\x2a\x89\x20\x64\x83\x42\x95\x18\x22\x52\x78\xd0\x0e\xc1\x35\xcc\x51\xe1\x4f\xb0\x6c\x50\xea\x57\x60\xc6\x97\xbb\x70\x82\x49\xf1\x1f\xe4\xb0\x79\x0b\x3d\xaf\x53\x4e\xb4\x18\x34\xec\x62\x8f\xd2\x81\xe1\x46\x71\x22\x88\xd0\x0f\xd1\xa7\x8a\x14\x15\x2d\x95\xf0\xd8\xa0\xc1\x5a\x1b\xbc\x08\x1a\x84\x03\xdb\xe8\x4e\x72\xd8\x20\x84\x9e\x85\x23\x63\xb6\x4c\x28\x60\xfe\x9e\xa4\xd4\xaf\xf6\x8a\x8e\xd0\x4f\x16\x44\xe1\xdf\x91\xfa\xaf\xb5\xaa\xc5\x76\xec\x99\xfb\xfd\x2a\xfa\x99\xc7\x33\x29\x20\x2f\xcc\xf8\x56\x78\x02\x98\x2c\x3c\xff\xd3\xeb\x4d\x76\xfe\x85\xca\x95\xfe\x25\x1e\x1c\x94\x65\xc7\xef\x2b\xcb\xb2\xf8\xe2\xcf\x85\xc7\x63\x27\xff\x9f\x35\x98\x1d\xb6\xbf\x3a\xe9\x7e\x83\xe7\xdf\xac\x38\x2f\x1b\x9c\xe5\x53\x39\x4f\x27\x22\xdd\x93\x54\x6c\x35\x83\xdc\xac\xdb\xcc\x49\x48\x2b\xa8\x0c\x86\x44\xf1\x75\x18\x7b\xcf\xfb\xe6\x59\x46\xe3\x33\x9d\x53\x21\x7a\x37\x1f\x45\x8b\xe1\xe9\xe9\x89\x10\xf0\x9c\x7b\xb6\x84\x94\x10\x8a\xba\x7c\xf4\x93\xcb\x14\xf8\x88\xd1\x78\x81\x75\xf9\xb4\xe3\xcc\xe1\xcd\x68\xe8\x54\xe0\x33\xb9\x1f\x0e\xbf\x23\x2d\x3f\x18\xfc\x14\xf9\x0f\xc5\x4b\x5d\xa1\xa8\xcb\x84\xb8\xd2\x70\xa9\xd7\x87\x58\x47\x89\x99\x00\x8d\x81\x57\x6b\x18\xfb\x9e\xf7\x01\xce\x3e\xd8\x25\xa0\x31\xda\xe4\x83\x07\xa9\x1b\x03\x3c\x2a\xc6\x28\x2c\xb0\x89\x78\x47\x20\xf2\x19\x12\x79\x84\x02\xee\x9d\x3f\x50\x31\x29\x27\x32\xda\x74\x42\x72\xcf\xcf\x1b\xe2\x14\xb0\xec\x05\x27\xd0\x06\x3b\xd4\xa6\x4f\xa0\x91\xbe\x2c\x0f\xda\x6b\x9c\xd9\xab\xce\x3a\xdd\x86\xd9\xd7\x7b\xe9\x3b\x2b\xc4\x52\x1a\x3a\xc3\x7c\xca\xf4\xa5\x93\x4c\x9a\x34\x17\xf9\x43\x01\xd3\x11\x13\xbf\x6e\xb0\x42\xf1\x82\xc6\xef\x8d\xcf\x45\x5d\xfe\x3d\xc4\xf6\x39\x4e\x89\x24\x1c\x2e\xfe\x8e\xd9\x5f\xf4\x84\xeb\xb8\x3e\x4f\xdc\x30\xf2\x07\x2c\xe7\xa9\x0a\xa3\x3b\xe9\xf0\x19\x65\x7e\xa7\xf4\xa4\xe9\x2d\x4b\xa8\xc4\x3f\x86\xfe\x4d\xeb\xab\x73\xd0\xad\x08\x8d\x63\x68\x02\x04\x77\x6d\x3c\x50\x0d\x12\x58\x65\x40\x27\x9b\xe2\xf7\xdd\x5a\xb4\x03\x4d\x0f\x39\xf2\x25\xcc\xa1\x45\xc2\xdf\xc9\xd8\x1a\x1d\x0d\x77\x61\x47\xe5\x27\x0a\x67\xba\x1b\x9f\x08\x24\x98\x6a\x09\x23\xef\x62\x91\x96\xfd\x1c\x37\xbf\xbe\x3a\x07\xa8\x85\xe2\xa4\x9f\x8e\x52\x9b\x3c\x51\xcc\x3e\xcc\xf0\x99\x36\x63\xdc\xa1\x82\x7c\x2e\xcc\xca\x4b\xd4\x80\x7f\xf8\x41\x39\x60\x7d\x88\x3d\x49\x8e\xf1\x8f\xa1\x89\xb9\xed\x24\xac\x90\xf3\x5f\xbb\xf2\xf5\x5c\xd7\xe8\xcb\x9c\x16\x8e\x95\xc5\xe1\x4d\x50\xd0\x34\xfe\x8f\x9f\x95\xdf\x13\x78\x1a\xca\x91\x0c\x1c\xe0\x08\xa9\x47\xfa\x26\x7f\x96\xde\x8d\xc0\x31\xb3\x9a\x99\xab\x5a\x42\xc8\xa4\xb3\xe5\xf0\x69\xd3\x7b\x55\x06\x5d\x67\x54\x5c\x7a\x7f\x7e\xb9\xc8\xb2\x98\xdf\x31\xde\xc5\x44\x1e\xc7\x28\xf0\x7f\x20\xb1\x90\x4a\x11\xbe\xbf\x42\x68\x14\x79\x62\xf5\xeb\x20\x10\x03\x53\xe8\xf6\x55\xb8\xaa\x81\x03\x69\xe2\x07\x66\xa9\x34\xe2\xa5\x89\x8b\xc3\x8b\x0b\xcc\xa2\xfc\x2e\x7c\x84\xfd\xfe\x22\x6d\x31\x87\x5c\xf4\xfe\x1a\x27\xce\x98\x5d\xfe\xe1\xa0\x7e\x45\x19\x12\xaf\x49\x09\xe9\x5f\x63\x96\xcf\xb6\xea\xd6\x95\xb7\x3e\xb8\xfa\x2c\x8c\x7d\x13\x5f\x5c\x81\x50\x74\x0b\x09\xc6\x74\x19\x47\xda\xea\x15\x7c\xf8\x23\xbf\x78\x8f\x4a\x4c\x84\xd3\xff\xa8\xd0\xf7\x1c\xe3\x5c\xf8\x99\x85\xc9\xe1\xaf\x95\xbe\x8f\xdd\xd4\x7f\xa5\xd1\x20\xd7\x32\x57\x35\x8f\xa7\xce\xad\xce\xf3\x81\x51\x23\xf4\xc3\x47\x71\xd4\x30\x9b\xf6\x8f\x7f\x06\x66\xb3\x0f\xad\xc4\xdb\x59\xf7\xfa\x34\x39\x4f\xf4\x55\x31\xe5\xa7\x6c\xfd\x82\xc6\x08\xce\x51\xf9\x39\x5b\x1b\xfa\x07\x4c\xd3\x97\xc4\xe4\x65\xf8\xab\x6c\xc8\x66\xa2\xd1\xc8\xf3\xe5\xd8\xf2\xd2\x7f\xb4\x66\xc0\xa4\x43\xe8\x7f\x03\x00\x00\xff\xff\x1b\xdd\x29\x94\xbe\x13\x00\x00") func templateMetaTmplBytes() ([]byte, error) { @@ -1285,6 +1306,7 @@ var _bindata = map[string]func() (*asset, error){ "template/header.tmpl": templateHeaderTmpl, "template/hook.tmpl": templateHookTmpl, "template/import.tmpl": templateImportTmpl, + "template/internal.tmpl": templateInternalTmpl, "template/meta.tmpl": templateMetaTmpl, "template/migrate/migrate.tmpl": templateMigrateMigrateTmpl, "template/migrate/schema.tmpl": templateMigrateSchemaTmpl, @@ -1385,12 +1407,13 @@ var _bintree = &bintree{nil, map[string]*bintree{ "update.tmpl": &bintree{templateDialectSqlUpdateTmpl, map[string]*bintree{}}, }}, }}, - "ent.tmpl": &bintree{templateEntTmpl, map[string]*bintree{}}, - "enttest.tmpl": &bintree{templateEnttestTmpl, map[string]*bintree{}}, - "header.tmpl": &bintree{templateHeaderTmpl, map[string]*bintree{}}, - "hook.tmpl": &bintree{templateHookTmpl, map[string]*bintree{}}, - "import.tmpl": &bintree{templateImportTmpl, map[string]*bintree{}}, - "meta.tmpl": &bintree{templateMetaTmpl, map[string]*bintree{}}, + "ent.tmpl": &bintree{templateEntTmpl, map[string]*bintree{}}, + "enttest.tmpl": &bintree{templateEnttestTmpl, map[string]*bintree{}}, + "header.tmpl": &bintree{templateHeaderTmpl, map[string]*bintree{}}, + "hook.tmpl": &bintree{templateHookTmpl, map[string]*bintree{}}, + "import.tmpl": &bintree{templateImportTmpl, map[string]*bintree{}}, + "internal.tmpl": &bintree{templateInternalTmpl, map[string]*bintree{}}, + "meta.tmpl": &bintree{templateMetaTmpl, map[string]*bintree{}}, "migrate": &bintree{nil, map[string]*bintree{ "migrate.tmpl": &bintree{templateMigrateMigrateTmpl, map[string]*bintree{}}, "schema.tmpl": &bintree{templateMigrateSchemaTmpl, map[string]*bintree{}}, diff --git a/entc/gen/template.go b/entc/gen/template.go index 3ef32c6dd..f0ec1e18a 100644 --- a/entc/gen/template.go +++ b/entc/gen/template.go @@ -152,6 +152,13 @@ var ( Name: "runtime/pkg", Format: "runtime/runtime.go", }, + { + Name: "internal/schema", + Format: "internal/schema.go", + Skip: func(g *Graph) bool { + return !g.featureEnabled(FeatureSnapshot) + }, + }, } // templates holds the Go templates for the code generation. templates *Template diff --git a/entc/gen/template/internal.tmpl b/entc/gen/template/internal.tmpl new file mode 100644 index 000000000..f85097101 --- /dev/null +++ b/entc/gen/template/internal.tmpl @@ -0,0 +1,17 @@ +{{/* +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. +*/}} + +{{ define "internal/schema" }} + +{{ with $.Header }}{{ . }}{{ else }}// Code generated by entc, DO NOT EDIT.{{ end }} + +// +build tools + +// Package internal holds a loadable version of the latest schema. +package internal + +const Schema = `{{ .SchemaSnapshot }}` +{{ end }} \ No newline at end of file diff --git a/entc/integration/privacy/ent/generate.go b/entc/integration/privacy/ent/generate.go index 7f1583128..eacf92312 100644 --- a/entc/integration/privacy/ent/generate.go +++ b/entc/integration/privacy/ent/generate.go @@ -4,4 +4,4 @@ package ent -//go:generate go run github.com/facebook/ent/cmd/entc generate --feature privacy,entql --header "// Copyright 2019-present Facebook Inc. All rights reserved.\n// This source code is licensed under the Apache 2.0 license found\n// in the LICENSE file in the root directory of this source tree.\n\n// Code generated by entc, DO NOT EDIT." ./schema +//go:generate go run github.com/facebook/ent/cmd/entc generate --feature privacy,entql,schema/snapshot --header "// Copyright 2019-present Facebook Inc. All rights reserved.\n// This source code is licensed under the Apache 2.0 license found\n// in the LICENSE file in the root directory of this source tree.\n\n// Code generated by entc, DO NOT EDIT." ./schema diff --git a/entc/integration/privacy/ent/internal/schema.go b/entc/integration/privacy/ent/internal/schema.go new file mode 100644 index 000000000..5a091cf40 --- /dev/null +++ b/entc/integration/privacy/ent/internal/schema.go @@ -0,0 +1,12 @@ +// 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. + +// Code generated by entc, DO NOT EDIT. + +// +build tools + +// Package internal holds a loadable version of the latest schema. +package internal + +const Schema = `{"Schema":"github.com/facebook/ent/entc/integration/privacy/ent/schema","Package":"github.com/facebook/ent/entc/integration/privacy/ent","Schemas":[{"name":"Task","config":{"Table":""},"edges":[{"name":"teams","type":"Team"},{"name":"owner","type":"User","ref_name":"tasks","unique":true,"inverse":true}],"fields":[{"name":"title","type":{"Type":7,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"validators":1,"position":{"Index":0,"MixedIn":false,"MixinIndex":0}},{"name":"description","type":{"Type":7,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"optional":true,"position":{"Index":1,"MixedIn":false,"MixinIndex":0}},{"name":"status","type":{"Type":6,"Ident":"task.Status","PkgPath":"","Nillable":false,"RType":null},"enums":[{"N":"planned","V":"planned"},{"N":"in_progress","V":"in_progress"},{"N":"closed","V":"closed"}],"default":true,"default_value":"planned","position":{"Index":2,"MixedIn":false,"MixinIndex":0}}],"hooks":[{"Index":0,"MixedIn":false,"MixinIndex":0}],"policy":[{"Index":0,"MixedIn":true,"MixinIndex":0},{"Index":0,"MixedIn":true,"MixinIndex":1},{"Index":0,"MixedIn":false,"MixinIndex":0}]},{"name":"Team","config":{"Table":""},"edges":[{"name":"tasks","type":"Task","ref_name":"teams","inverse":true},{"name":"users","type":"User","ref_name":"teams","inverse":true}],"fields":[{"name":"name","type":{"Type":7,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"validators":1,"position":{"Index":0,"MixedIn":false,"MixinIndex":0}}],"policy":[{"Index":0,"MixedIn":true,"MixinIndex":0},{"Index":0,"MixedIn":false,"MixinIndex":0}]},{"name":"User","config":{"Table":""},"edges":[{"name":"teams","type":"Team"},{"name":"tasks","type":"Task"}],"fields":[{"name":"name","type":{"Type":7,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"unique":true,"immutable":true,"validators":1,"position":{"Index":0,"MixedIn":false,"MixinIndex":0}},{"name":"age","type":{"Type":16,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"optional":true,"position":{"Index":1,"MixedIn":false,"MixinIndex":0}}],"policy":[{"Index":0,"MixedIn":true,"MixinIndex":0},{"Index":0,"MixedIn":true,"MixinIndex":1},{"Index":0,"MixedIn":false,"MixinIndex":0}]}],"Features":["privacy","entql","schema/snapshot"]}` diff --git a/entc/internal/snapshot.go b/entc/internal/snapshot.go new file mode 100644 index 000000000..6a56ddf62 --- /dev/null +++ b/entc/internal/snapshot.go @@ -0,0 +1,236 @@ +// 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 internal + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/facebook/ent/entc/gen" + "github.com/facebook/ent/entc/load" +) + +// Snapshot describes the schema snapshot restore. +type Snapshot struct { + Path string // Path to snapshot. + Config *gen.Config // Config of codegen. +} + +// Restore restores the generated package from the latest schema snapshot. +// If there is a conflict between upstream and local snapshots, it is merged +// before running the code generation. +func (s *Snapshot) Restore() error { + buf, err := ioutil.ReadFile(s.Path) + if err != nil { + return fmt.Errorf("unable to read snapshot schema %w", err) + } + snap, err := s.parseSnapshot(buf) + if err != nil { + return err + } + s.Config.Schema = snap.Schema + s.Config.Package = snap.Package + s.addFeatures(snap) + graph, err := gen.NewGraph(s.Config, snap.Schemas...) + if err != nil { + return err + } + return graph.Gen() +} + +// schemaIdent holds the schema identifier in snapshot file. +const schemaIdent = "const Schema" + +// parseSnapshot parses the given buffer and extract the generated snapshot. +// If it encounters a merge-conflict, it will resolve it by merging the relevant +// parts for the codegen. +func (s *Snapshot) parseSnapshot(buf []byte) (*gen.Snapshot, error) { + var ( + conflict bool + matches = make([][]byte, 0, 2) + lines = bytes.Split(buf, []byte("\n")) + ) + for i := 0; i < len(lines); i++ { + switch line := lines[i]; { + case bytes.HasPrefix(line, []byte(schemaIdent)): + matches = append(matches, line) + case bytes.HasPrefix(line, []byte(conflictMarker)): + conflict = true + } + } + switch n := len(matches); { + case n == 0: + return nil, fmt.Errorf("schema snapshot was not found in %s", s.Path) + case n > 1 && !conflict: + return nil, fmt.Errorf("expect to have exactly 1 snapshot in %s", s.Path) + } + line, err := trim(matches[0]) + if err != nil { + return nil, err + } + local := &gen.Snapshot{} + if err := json.Unmarshal(line, &local); err != nil { + return nil, fmt.Errorf("unmarshal snapshot %v: %w", local, err) + } + if !conflict || len(matches) == 1 { + return local, nil + } + // In case of merge-conflict, we merge the 2 schemas. + line, err = trim(matches[0]) + if err != nil { + return nil, err + } + other := &gen.Snapshot{} + if err := json.Unmarshal(line, &other); err != nil { + return nil, fmt.Errorf("unmarshal snapshot %v: %w", local, err) + } + merge(local, other) + return local, nil +} + +// addFeatures adds the features in the snapshot to the codegen config. +func (s *Snapshot) addFeatures(snap *gen.Snapshot) { + add := make(map[string]gen.Feature) + for _, name := range snap.Features { + for _, feat := range gen.AllFeatures { + if name == feat.Name { + add[name] = feat + } + } + } + for _, feat := range s.Config.Features { + delete(add, feat.Name) + } + for _, feat := range add { + s.Config.Features = append(s.Config.Features, feat) + } +} + +// merge the "other"/"upstream" snapshot to the "local" version. +func merge(local, other *gen.Snapshot) { + if local.Schema == "" { + local.Schema = other.Schema + } + if local.Package == "" { + local.Package = other.Package + } + locals := make(map[string]*load.Schema, len(local.Schemas)) + for _, schema := range local.Schemas { + locals[schema.Name] = schema + } + // Merge "other" schemas. + for _, schema := range other.Schemas { + switch match, ok := locals[schema.Name]; { + case !ok: + local.Schemas = append(local.Schemas, schema) + case ok: + mergeSchema(match, schema) + } + } + // Merge codegen features. + features := make(map[string]struct{}, len(local.Features)) + for _, feat := range local.Features { + features[feat] = struct{}{} + } + for _, feat := range other.Features { + if _, ok := features[feat]; !ok { + local.Features = append(local.Features, feat) + } + } +} + +// mergeSchema merges to "local" the additional information in +// the "other" schema, that may be necessary for code-generation. +func mergeSchema(local, other *load.Schema) { + if local.Config.Table == "" { + local.Config.Table = other.Config.Table + } + if local.Annotations == nil && other.Annotations != nil { + local.Annotations = make(map[string]interface{}) + } + for ant := range other.Annotations { + if _, ok := local.Annotations[ant]; !ok { + local.Annotations[ant] = other.Annotations[ant] + } + } + fields := make(map[string]*load.Field, len(local.Fields)) + for _, f := range local.Fields { + fields[f.Name] = f + } + for _, f := range other.Fields { + switch match, ok := fields[f.Name]; { + case !ok: + local.Fields = append(local.Fields, f) + case ok: + mergeField(match, f) + } + } + edges := make(map[string]*load.Edge, len(local.Edges)) + for _, e := range local.Edges { + edges[e.Name] = e + } + for _, e := range other.Edges { + switch match, ok := edges[e.Name]; { + case !ok: + local.Edges = append(local.Edges, e) + case ok: + mergeEdge(match, e) + } + } +} + +// mergeField merges to "local" the additional information in +// the "other" field, that may be necessary for code-generation. +func mergeField(local, other *load.Field) { + if local.Annotations == nil && other.Annotations != nil { + local.Annotations = make(map[string]interface{}) + } + for ant := range other.Annotations { + if _, ok := local.Annotations[ant]; !ok { + local.Annotations[ant] = other.Annotations[ant] + } + } + if !local.Immutable && other.Immutable { + local.Immutable = other.Immutable + } +} + +// mergeEdge merges to "local" the additional information in +// the "other" edge, that may be necessary for code-generation. +func mergeEdge(local, other *load.Edge) { + if local.Annotations == nil && other.Annotations != nil { + local.Annotations = make(map[string]interface{}) + } + for ant := range other.Annotations { + if _, ok := local.Annotations[ant]; !ok { + local.Annotations[ant] = other.Annotations[ant] + } + } +} + +// IsBuildError reports if the given error is an error from the Go command (e.g. syntax error). +func IsBuildError(err error) bool { + if strings.HasPrefix(err.Error(), "entc/load: #") { + return true + } + for _, s := range []string{"syntax error", "previous declaration", "invalid character"} { + if strings.Contains(err.Error(), s) { + return true + } + } + return false +} + +func trim(line []byte) ([]byte, error) { + start := bytes.IndexByte(line, '`') + end := bytes.LastIndexByte(line, '`') + if start == -1 || start >= end { + return nil, fmt.Errorf("unexpected snapshot line %s", line) + } + return line[start+1 : end], nil +} diff --git a/entc/internal/snapshot_test.go b/entc/internal/snapshot_test.go new file mode 100644 index 000000000..1f6a8dcbb --- /dev/null +++ b/entc/internal/snapshot_test.go @@ -0,0 +1,71 @@ +// 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 internal + +import ( + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/facebook/ent/entc/gen" + + "github.com/stretchr/testify/require" +) + +func TestSnapshot_Restore(t *testing.T) { + t.Log("Running snapshot-restore integration test") + const testPackage = "../integration/privacy/ent" + err := addConflicts(testPackage) + require.NoError(t, err) + storage, err := gen.NewStorage("sql") + require.NoError(t, err) + snap := &Snapshot{ + Path: filepath.Join(testPackage, "internal/schema.go"), + Config: &gen.Config{ + Storage: storage, + Target: testPackage, + Schema: filepath.Join(testPackage, "schema"), + Header: ` + // 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. + + // Code generated by entc, DO NOT EDIT. + `, + }} + require.NoError(t, snap.Restore()) + err = exec.Command("go", "generate", testPackage).Run() + require.NoError(t, err) +} + +// addConflicts adds VCS conflicts to the files that match the given patterns. +func addConflicts(dir string) error { + rand.Seed(time.Now().UnixNano()) + infos, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + for _, info := range infos { + if info.IsDir() || info.Name() == "generate.go" { + continue + } + path := filepath.Join(dir, info.Name()) + fi, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + if _, err := fi.WriteString("\n" + conflictMarker); err != nil { + return err + } + if err := fi.Close(); err != nil { + return err + } + } + return nil +} diff --git a/entc/gen/internal/vcs.go b/entc/internal/vcs.go similarity index 100% rename from entc/gen/internal/vcs.go rename to entc/internal/vcs.go diff --git a/entc/gen/internal/vcs_test.go b/entc/internal/vcs_test.go similarity index 100% rename from entc/gen/internal/vcs_test.go rename to entc/internal/vcs_test.go diff --git a/entc/load/load.go b/entc/load/load.go index a27da74f8..05c4fcf56 100644 --- a/entc/load/load.go +++ b/entc/load/load.go @@ -42,7 +42,7 @@ type Config struct { // Path is the path for the schema package. Path string // Names are the schema names to run the code generation on. - // Empty means all schemas in the directory. + // Empty means all schema in the directory. Names []string } @@ -50,7 +50,7 @@ type Config struct { func (c *Config) Load() (*SchemaSpec, error) { pkgPath, err := c.load() if err != nil { - return nil, fmt.Errorf("load schemas dir: %v", err) + return nil, fmt.Errorf("load schema dir: %w", err) } if len(c.Names) == 0 { return nil, fmt.Errorf("no schema found in: %s", c.Path) @@ -97,7 +97,7 @@ var entInterface = reflect.TypeOf(struct{ ent.Interface }{}).Field(0).Type func (c *Config) load() (string, error) { pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax}, c.Path, entInterface.PkgPath()) if err != nil { - return "", fmt.Errorf("loading package: %v", err) + return "", fmt.Errorf("loading package: %w", err) } if len(pkgs) < 2 { return "", fmt.Errorf("missing package information for: %s", c.Path) @@ -181,7 +181,7 @@ func schemaTemplates() ([]string, error) { } func filename(pkg string) string { - name := strings.Replace(pkg, "/", "_", -1) + name := strings.ReplaceAll(pkg, "/", "_") return fmt.Sprintf("entc_%s_%d", name, time.Now().Unix()) }