Skip to content

Commit

Permalink
feat: add local schema diff
Browse files Browse the repository at this point in the history
If no remote URI is specified for schema diff it will attempt to diff the local project

closes #2174
  • Loading branch information
stuartwdouglas committed Jul 28, 2024
1 parent 48bc91d commit 0b56f8b
Showing 1 changed file with 83 additions and 3 deletions.
86 changes: 83 additions & 3 deletions cmd/ftl/cmd_schema_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,65 @@ package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"

"connectrpc.com/connect"
"github.com/alecthomas/chroma/v2/quick"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/mattn/go-isatty"
"google.golang.org/protobuf/proto"

ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema"
"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/common/projectconfig"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/rpc"
)

type schemaDiffCmd struct {
OtherEndpoint url.URL `arg:"" help:"Other endpoint URL to compare against."`
OtherEndpoint url.URL `arg:"" help:"Other endpoint URL to compare against. If this is not specified then ftl will perform a diff against the local schema." optional:""`
Color bool `help:"Enable colored output regardless of TTY."`
}

func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error {
other, err := schemaForURL(ctx, d.OtherEndpoint)
func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL, projConfig projectconfig.Config) error {
var other *schema.Schema
var err error
sameModulesOnly := false
if d.OtherEndpoint.String() == "" {
sameModulesOnly = true
other, err = localSchema(projConfig)
} else {
other, err = schemaForURL(ctx, d.OtherEndpoint)
}

if err != nil {
return fmt.Errorf("failed to get other schema: %w", err)
}
current, err := schemaForURL(ctx, *currentURL)
if err != nil {
return fmt.Errorf("failed to get current schema: %w", err)
}
if sameModulesOnly {
tempModules := current.Modules
current.Modules = []*schema.Module{}
moduleMap := map[string]*schema.Module{}
for _, i := range tempModules {
moduleMap[i.Name] = i
}
for _, i := range other.Modules {
if mod, ok := moduleMap[i.Name]; ok {
current.Modules = append(current.Modules, mod)
}
}
}

edits := myers.ComputeEdits(span.URIFromPath(""), other.String(), current.String())
diff := fmt.Sprint(gotextdiff.ToUnified(d.OtherEndpoint.String(), currentURL.String(), other.String(), edits))
Expand All @@ -57,6 +83,60 @@ func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error {
return nil
}

func localSchema(moduleDirs projectconfig.Config) (*schema.Schema, error) {
pb := &schemapb.Schema{}
found := false
tried := ""
moduleFound := false
moduleTried := ""
for _, modulePaths := range moduleDirs.AbsModuleDirs() {
entries, err := os.ReadDir(modulePaths)
if err != nil {
return nil, fmt.Errorf("failed to list directory %w", err)
}
for _, module := range entries {
if !module.IsDir() {
continue
}
path := filepath.Join(modulePaths, module.Name(), ".ftl", "schema.pb")
mod, err := loadProtoSchema(path)
if err != nil {
moduleTried += fmt.Sprintf("failed to read schema file %s; did you run ftl build?", path)
} else {
moduleFound = true
found = true
pb.Modules = append(pb.Modules, mod)
}
}
}
if !moduleFound {
tried += moduleTried
}

if !found {
return nil, errors.New(tried)
}

s, err := schema.FromProto(pb)
if err != nil {
return nil, fmt.Errorf("failed to parse local schema: %w", err)
}
return s, nil
}

func loadProtoSchema(file string) (*schemapb.Module, error) {
content, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("failed to read schema file %w", err)
}
module := &schemapb.Module{}
err = proto.Unmarshal(content, module)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal schema %w", err)
}
return module, nil
}

func schemaForURL(ctx context.Context, url url.URL) (*schema.Schema, error) {
client := rpc.Dial(ftlv1connect.NewControllerServiceClient, url.String(), log.Error)
resp, err := client.PullSchema(ctx, connect.NewRequest(&ftlv1.PullSchemaRequest{}))
Expand Down

0 comments on commit 0b56f8b

Please sign in to comment.