Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add plan cmd and plan generator #1539

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
92 changes: 92 additions & 0 deletions router/cmd/plan-generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"flag"
"log"
"os"
"path/filepath"
"slices"
"strings"
"time"

"github.com/wundergraph/cosmo/router/core"
)

var (
executionConfigFilePath = flag.String("execution-config", "config.json", "execution config file location")
sourceOperationFoldersPath = flag.String("operations", "operations", "source operations folder location")
plansOutPath = flag.String("plans", "plans", "output plans folder location")
operationFilterFilePath = flag.String("filter", "", "operation filter file location which should contain file names of operations to include")
)

func main() {
flag.Parse()

queriesPath, err := filepath.Abs(*sourceOperationFoldersPath)
if err != nil {
log.Fatalf("failed to get absolute path for queries: %v", err)
}

outPath, err := filepath.Abs(*plansOutPath)
if err != nil {
log.Fatalf("failed to get absolute path for output: %v", err)
}
if err := os.MkdirAll(outPath, 0755); err != nil {
log.Fatalf("failed to create output directory: %v", err)
}

supergraphConfigPath, err := filepath.Abs(*executionConfigFilePath)
log.Println("supergraphPath:", supergraphConfigPath)
if err != nil {
log.Fatalf("failed to get absolute path for supergraph: %v", err)
}

queries, err := os.ReadDir(queriesPath)
if err != nil {
log.Fatalf("failed to read queries directory: %v", err)
}

var filter []string
if *operationFilterFilePath != "" {
filterContent, err := os.ReadFile(*operationFilterFilePath)
if err != nil {
log.Fatalf("failed to read filter file: %v", err)
}

filter = strings.Split(string(filterContent), "\n")
}

pg, err := core.NewPlanGenerator(supergraphConfigPath)
if err != nil {
log.Fatalf("failed to create plan generator: %v", err)
}

t := time.Now()

for i, queryFile := range queries {
if filepath.Ext(queryFile.Name()) != ".graphql" {
continue
}

if len(filter) > 0 && !slices.Contains(filter, queryFile.Name()) {
continue
}

log.Println("Running query #", i, " name:", queryFile.Name())

queryFilePath := filepath.Join(queriesPath, queryFile.Name())

outContent, err := pg.PlanOperation(queryFilePath)
if err != nil {
log.Printf("failed operation #%d: %s err: %v\n", i, queryFile.Name(), err.Error())
}

outFileName := filepath.Join(outPath, queryFile.Name())
err = os.WriteFile(outFileName, []byte(outContent), 0644)
if err != nil {
log.Fatalf("failed to write file: %v", err)
}
}

log.Println("Total planning time:", time.Since(t))
}
164 changes: 164 additions & 0 deletions router/core/plan_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package core

import (
"context"
"errors"
"fmt"
"net/http"
"os"

log "github.com/jensneuse/abstractlogger"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/graphql_datasource"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/postprocess"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport"

"github.com/wundergraph/cosmo/router/pkg/execution_config"
)

type PlanGenerator struct {
planConfiguration *plan.Configuration
planner *plan.Planner
definition *ast.Document
}

func NewPlanGenerator(configFilePath string) (*PlanGenerator, error) {
pg := &PlanGenerator{}
if err := pg.loadConfiguration(configFilePath); err != nil {
return nil, err
}

planner, err := plan.NewPlanner(*pg.planConfiguration)
if err != nil {
return nil, fmt.Errorf("failed to create planner: %w", err)
}
pg.planner = planner

return pg, nil
}

func (pg *PlanGenerator) PlanOperation(operationFilePath string) (string, error) {
operation, err := pg.parseOperation(operationFilePath)
if err != nil {
return "", fmt.Errorf("failed to parse operation: %w", err)
}

rawPlan, err := pg.planOperation(operation)
if err != nil {
return "", fmt.Errorf("failed to plan operation: %w", err)
}

return rawPlan.PrettyPrint(), nil
}

func (pg *PlanGenerator) planOperation(operation *ast.Document) (*resolve.FetchTreeQueryPlanNode, error) {
report := operationreport.Report{}

var operationName []byte

for i := range operation.RootNodes {
if operation.RootNodes[i].Kind == ast.NodeKindOperationDefinition {
operationName = operation.OperationDefinitionNameBytes(operation.RootNodes[i].Ref)
break
}
}

if operationName == nil {
return nil, errors.New("operation name not found")
}

astnormalization.NormalizeNamedOperation(operation, pg.definition, operationName, &report)

// create and postprocess the plan
preparedPlan := pg.planner.Plan(operation, pg.definition, string(operationName), &report, plan.IncludeQueryPlanInResponse())
if report.HasErrors() {
return nil, errors.New(report.Error())
}
post := postprocess.NewProcessor()
post.Process(preparedPlan)

if p, ok := preparedPlan.(*plan.SynchronousResponsePlan); ok {
return p.Response.Fetches.QueryPlan(), nil
}

return &resolve.FetchTreeQueryPlanNode{}, nil
}

func (pg *PlanGenerator) parseOperation(operationFilePath string) (*ast.Document, error) {
content, err := os.ReadFile(operationFilePath)
if err != nil {
return nil, err
}

doc, report := astparser.ParseGraphqlDocumentBytes(content)
if report.HasErrors() {
return nil, errors.New(report.Error())
}

return &doc, nil
}

func (pg *PlanGenerator) loadConfiguration(configFilePath string) error {
routerConfig, err := execution_config.FromFile(configFilePath)
if err != nil {
return err
}

var netPollConfig graphql_datasource.NetPollConfiguration
netPollConfig.ApplyDefaults()

subscriptionClient := graphql_datasource.NewGraphQLSubscriptionClient(
http.DefaultClient,
http.DefaultClient,
context.Background(),
graphql_datasource.WithLogger(log.NoopLogger),
graphql_datasource.WithNetPollConfiguration(netPollConfig),
)

loader := NewLoader(false, &DefaultFactoryResolver{
engineCtx: context.Background(),
httpClient: http.DefaultClient,
streamingClient: http.DefaultClient,
subscriptionClient: subscriptionClient,
})

// this generates the plan configuration using the data source factories from the config package
planConfig, err := loader.Load(routerConfig.GetEngineConfig(), routerConfig.GetSubgraphs(), &RouterEngineConfiguration{})
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

planConfig.Debug = plan.DebugConfiguration{
PrintOperationTransformations: false,
PrintOperationEnableASTRefs: false,
PrintPlanningPaths: false,
PrintQueryPlans: false,
PrintNodeSuggestions: false,
ConfigurationVisitor: false,
PlanningVisitor: false,
DatasourceVisitor: false,
}

// this is the GraphQL Schema that we will expose from our API
definition, report := astparser.ParseGraphqlDocumentString(routerConfig.EngineConfig.GraphqlSchema)
if report.HasErrors() {
return fmt.Errorf("failed to parse graphql schema from engine config: %w", report)
}

// we need to merge the base schema, it contains the __schema and __type queries
// these are not usually part of a regular GraphQL schema
// the engine needs to have them defined, otherwise it cannot resolve such fields
err = asttransform.MergeDefinitionWithBaseSchema(&definition)
if err != nil {
return fmt.Errorf("failed to merge graphql schema with base schema: %w", err)
}

pg.planConfiguration = planConfig
pg.definition = &definition
return nil
}
Loading