Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
710 lines
18 KiB
Go
710 lines
18 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/sqlc-dev/sqlc/internal/constants"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime/trace"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"cel.dev/cel-go/cel"
|
|
"cel.dev/cel-go/ext"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
"github.com/sqlc-dev/sqlc/internal/config"
|
|
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
|
"github.com/sqlc-dev/sqlc/internal/debug"
|
|
"github.com/sqlc-dev/sqlc/internal/migrations"
|
|
"github.com/sqlc-dev/sqlc/internal/opts"
|
|
"github.com/sqlc-dev/sqlc/internal/plugin"
|
|
"github.com/sqlc-dev/sqlc/internal/quickdb"
|
|
"github.com/sqlc-dev/sqlc/internal/shfmt"
|
|
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
|
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
|
"github.com/sqlc-dev/sqlc/internal/vet"
|
|
)
|
|
|
|
var (
|
|
debugDumpExplain = sqlcdebug.New("dumpexplain")
|
|
debugDumpVetEnv = sqlcdebug.New("dumpvetenv")
|
|
debugDatabases = sqlcdebug.New("databases")
|
|
)
|
|
|
|
var ErrFailedChecks = errors.New("failed checks")
|
|
|
|
var pjson = protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
|
|
|
|
func NewCmdVet() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "vet",
|
|
Short: "Vet examines queries",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
defer trace.StartRegion(cmd.Context(), "vet").End()
|
|
stderr := cmd.ErrOrStderr()
|
|
opts := &Options{
|
|
Env: ParseEnv(cmd),
|
|
Stderr: stderr,
|
|
}
|
|
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
|
if err := Vet(cmd.Context(), dir, name, opts); err != nil {
|
|
if !errors.Is(err, ErrFailedChecks) {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func Vet(ctx context.Context, dir, filename string, opts *Options) error {
|
|
e := opts.Env
|
|
stderr := opts.Stderr
|
|
configPath, conf, err := readConfig(stderr, dir, filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
base := filepath.Base(configPath)
|
|
if err := config.Validate(conf); err != nil {
|
|
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
|
return err
|
|
}
|
|
|
|
if err := e.Validate(conf); err != nil {
|
|
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
|
return err
|
|
}
|
|
|
|
env, err := cel.NewEnv(
|
|
cel.StdLib(),
|
|
ext.Strings(ext.StringsVersion(1)),
|
|
cel.Types(
|
|
&vet.Config{},
|
|
&vet.Query{},
|
|
&vet.PostgreSQL{},
|
|
&vet.MySQL{},
|
|
),
|
|
cel.Variable("query",
|
|
cel.ObjectType("vet.Query"),
|
|
),
|
|
cel.Variable("config",
|
|
cel.ObjectType("vet.Config"),
|
|
),
|
|
cel.Variable("postgresql",
|
|
cel.ObjectType("vet.PostgreSQL"),
|
|
),
|
|
cel.Variable("mysql",
|
|
cel.ObjectType("vet.MySQL"),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("new CEL env error: %s", err)
|
|
}
|
|
|
|
rules := map[string]rule{
|
|
constants.QueryRuleDbPrepare: {NeedsPrepare: true},
|
|
}
|
|
|
|
for _, c := range conf.Rules {
|
|
if c.Name == "" {
|
|
return fmt.Errorf("rules require a name")
|
|
}
|
|
if _, found := rules[c.Name]; found {
|
|
return fmt.Errorf("type-check error: a rule with the name '%s' already exists", c.Name)
|
|
}
|
|
if c.Rule == "" {
|
|
return fmt.Errorf("type-check error: %s is empty", c.Name)
|
|
}
|
|
ast, issues := env.Compile(c.Rule)
|
|
if issues != nil && issues.Err() != nil {
|
|
return fmt.Errorf("type-check error: %s %s", c.Name, issues.Err())
|
|
}
|
|
prg, err := env.Program(ast)
|
|
if err != nil {
|
|
return fmt.Errorf("program construction error: %s %s", c.Name, err)
|
|
}
|
|
rule := rule{Program: &prg, Message: c.Msg}
|
|
|
|
// TODO There's probably a nicer way to do this from the ast
|
|
// https://pkg.go.dev/cel.dev/cel-go/common/ast#AllMatcher
|
|
if strings.Contains(c.Rule, "postgresql.explain") ||
|
|
strings.Contains(c.Rule, "mysql.explain") {
|
|
rule.NeedsExplain = true
|
|
}
|
|
|
|
rules[c.Name] = rule
|
|
}
|
|
|
|
c := checker{
|
|
Rules: rules,
|
|
Conf: conf,
|
|
Dir: dir,
|
|
Env: env,
|
|
Stderr: stderr,
|
|
OnlyManagedDB: debugDatabases.Value() == "managed",
|
|
Replacer: shfmt.NewReplacer(nil),
|
|
}
|
|
errored := false
|
|
for _, sql := range conf.SQL {
|
|
if err := c.checkSQL(ctx, sql); err != nil {
|
|
if !errors.Is(err, ErrFailedChecks) {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
}
|
|
errored = true
|
|
}
|
|
}
|
|
if errored {
|
|
return ErrFailedChecks
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type preparer interface {
|
|
Prepare(context.Context, string, string) error
|
|
}
|
|
|
|
type pgxConn struct {
|
|
c *pgx.Conn
|
|
}
|
|
|
|
func (p *pgxConn) Prepare(ctx context.Context, name, query string) error {
|
|
_, err := p.c.Prepare(ctx, name, query)
|
|
return err
|
|
}
|
|
|
|
// Return a default value for a PostgreSQL column based on its type. Returns nil
|
|
// if the type is unknown.
|
|
func pgDefaultValue(col *plugin.Column) any {
|
|
if col == nil {
|
|
return nil
|
|
}
|
|
if col.Type == nil {
|
|
return nil
|
|
}
|
|
typname := strings.TrimPrefix(col.Type.Name, "pg_catalog.")
|
|
switch typname {
|
|
case "any", "void":
|
|
if col.IsArray {
|
|
return []any{}
|
|
} else {
|
|
return nil
|
|
}
|
|
case "anyarray":
|
|
return []any{}
|
|
case "bool", "boolean":
|
|
if col.IsArray {
|
|
return []bool{}
|
|
} else {
|
|
return false
|
|
}
|
|
case "double", "double precision", "real":
|
|
if col.IsArray {
|
|
return []float32{}
|
|
} else {
|
|
return 0.1
|
|
}
|
|
case "json", "jsonb":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return "{}"
|
|
}
|
|
case "citext", "string", "text", "varchar":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return ""
|
|
}
|
|
case "bigint", "bigserial", "integer", "int", "int2", "int4", "int8", "serial":
|
|
if col.IsArray {
|
|
return []int{}
|
|
} else {
|
|
return 1
|
|
}
|
|
case "date", "time", "timestamp", "timestamptz":
|
|
if col.IsArray {
|
|
return []time.Time{}
|
|
} else {
|
|
return time.Time{}
|
|
}
|
|
case "uuid":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return "00000000-0000-0000-0000-000000000000"
|
|
}
|
|
case "numeric", "decimal":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return "0.1"
|
|
}
|
|
case "inet":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return "192.168.0.1/24"
|
|
}
|
|
case "cidr":
|
|
if col.IsArray {
|
|
return []string{}
|
|
} else {
|
|
return "192.168.1/24"
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Return a default value for a MySQL column based on its type. Returns nil
|
|
// if the type is unknown.
|
|
func mysqlDefaultValue(col *plugin.Column) any {
|
|
if col == nil {
|
|
return nil
|
|
}
|
|
if col.Type == nil {
|
|
return nil
|
|
}
|
|
switch col.Type.Name {
|
|
case "any":
|
|
return nil
|
|
case "bool":
|
|
return false
|
|
case "int", "bigint", "mediumint", "smallint", "tinyint", "bit":
|
|
return 1
|
|
case "decimal": // "numeric", "dec", "fixed"
|
|
// No perfect choice here to avoid "Impossible WHERE" but I think
|
|
// 0.1 is decent. It works for all cases where `scale` > 0 which
|
|
// should be the majority. For more information refer to
|
|
// https://dev.mysql.com/doc/refman/8.1/en/fixed-point-types.html.
|
|
return 0.1
|
|
case "float", "double":
|
|
return 0.1
|
|
case "date":
|
|
return "0000-00-00"
|
|
case "datetime", "timestamp":
|
|
return "0000-00-00 00:00:00"
|
|
case "time":
|
|
return "00:00:00"
|
|
case "year":
|
|
return "0000"
|
|
case "char", "varchar", "binary", "varbinary", "tinyblob", "blob",
|
|
"mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext":
|
|
return ""
|
|
case "json":
|
|
return "{}"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (p *pgxConn) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
|
|
eQuery := "EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) " + query
|
|
eArgs := make([]any, len(args))
|
|
for i, a := range args {
|
|
eArgs[i] = pgDefaultValue(a.Column)
|
|
}
|
|
row := p.c.QueryRow(ctx, eQuery, eArgs...)
|
|
var result []json.RawMessage
|
|
if err := row.Scan(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
if debugDumpExplain.Value() == "1" {
|
|
fmt.Println(eQuery, "with args", eArgs)
|
|
fmt.Println(string(result[0]))
|
|
}
|
|
var explain vet.PostgreSQLExplain
|
|
if err := pjson.Unmarshal(result[0], &explain); err != nil {
|
|
return nil, err
|
|
}
|
|
return &vetEngineOutput{PostgreSQL: &vet.PostgreSQL{Explain: &explain}}, nil
|
|
}
|
|
|
|
type dbPreparer struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func (p *dbPreparer) Prepare(ctx context.Context, name, query string) error {
|
|
s, err := p.db.PrepareContext(ctx, query)
|
|
if s != nil {
|
|
s.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
type explainer interface {
|
|
Explain(context.Context, string, ...*plugin.Parameter) (*vetEngineOutput, error)
|
|
}
|
|
|
|
type mysqlExplainer struct {
|
|
*sql.DB
|
|
}
|
|
|
|
func (me *mysqlExplainer) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
|
|
eQuery := "EXPLAIN FORMAT=JSON " + query
|
|
eArgs := make([]any, len(args))
|
|
for i, a := range args {
|
|
eArgs[i] = mysqlDefaultValue(a.Column)
|
|
}
|
|
row := me.QueryRowContext(ctx, eQuery, eArgs...)
|
|
var result json.RawMessage
|
|
if err := row.Scan(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
if debugDumpExplain.Value() == "1" {
|
|
fmt.Println(eQuery, "with args", eArgs)
|
|
fmt.Println(string(result))
|
|
}
|
|
var explain vet.MySQLExplain
|
|
if err := pjson.Unmarshal(result, &explain); err != nil {
|
|
return nil, err
|
|
}
|
|
if explain.QueryBlock.Message != "" {
|
|
return nil, fmt.Errorf("mysql explain: %s", explain.QueryBlock.Message)
|
|
}
|
|
return &vetEngineOutput{MySQL: &vet.MySQL{Explain: &explain}}, nil
|
|
}
|
|
|
|
type rule struct {
|
|
Program *cel.Program
|
|
Message string
|
|
NeedsPrepare bool
|
|
NeedsExplain bool
|
|
}
|
|
|
|
type checker struct {
|
|
Rules map[string]rule
|
|
Conf *config.Config
|
|
Dir string
|
|
Env *cel.Env
|
|
Stderr io.Writer
|
|
OnlyManagedDB bool
|
|
Client dbmanager.Client
|
|
clientOnce sync.Once
|
|
Replacer *shfmt.Replacer
|
|
}
|
|
|
|
// isInMemorySQLite checks if a SQLite URI refers to an in-memory database
|
|
func isInMemorySQLite(uri string) bool {
|
|
if uri == ":memory:" || uri == "" {
|
|
return true
|
|
}
|
|
// Check for file URI with mode=memory parameter
|
|
// e.g., "file:test?mode=memory&cache=shared"
|
|
if strings.Contains(uri, "mode=memory") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *checker) fetchDatabaseUri(ctx context.Context, s config.SQL) (string, func() error, error) {
|
|
cleanup := func() error {
|
|
return nil
|
|
}
|
|
|
|
if s.Database == nil {
|
|
panic("fetch database URI called with nil database")
|
|
}
|
|
if !s.Database.Managed {
|
|
uri, err := c.DSN(s.Database.URI)
|
|
return uri, cleanup, err
|
|
}
|
|
|
|
// Initialize the client exactly once, even if called concurrently
|
|
c.clientOnce.Do(func() {
|
|
c.Client = dbmanager.NewClient(c.Conf.Servers)
|
|
})
|
|
|
|
var ddl []string
|
|
files, err := sqlpath.Glob(s.Schema)
|
|
if err != nil {
|
|
return "", cleanup, err
|
|
}
|
|
for _, schema := range files {
|
|
contents, err := os.ReadFile(schema)
|
|
if err != nil {
|
|
return "", cleanup, fmt.Errorf("read file: %w", err)
|
|
}
|
|
ddl = append(ddl, migrations.RemoveRollbackStatements(string(contents)))
|
|
}
|
|
|
|
resp, err := c.Client.CreateDatabase(ctx, &dbmanager.CreateDatabaseRequest{
|
|
Engine: string(s.Engine),
|
|
Migrations: ddl,
|
|
})
|
|
if err != nil {
|
|
return "", cleanup, fmt.Errorf("managed: create database: %w", err)
|
|
}
|
|
|
|
var uri string
|
|
switch s.Engine {
|
|
case config.EngineMySQL:
|
|
dburi, err := quickdb.MySQLReformatURI(resp.Uri)
|
|
if err != nil {
|
|
return "", cleanup, fmt.Errorf("reformat uri: %w", err)
|
|
}
|
|
uri = dburi
|
|
default:
|
|
uri = resp.Uri
|
|
}
|
|
|
|
return uri, cleanup, nil
|
|
}
|
|
|
|
func (c *checker) DSN(dsn string) (string, error) {
|
|
return c.Replacer.Replace(dsn), nil
|
|
}
|
|
|
|
func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
|
|
// TODO: Create a separate function for this logic so we can
|
|
combo := config.Combine(*c.Conf, s)
|
|
|
|
// TODO: This feels like a hack that will bite us later
|
|
joined := make([]string, 0, len(s.Schema))
|
|
for _, s := range s.Schema {
|
|
joined = append(joined, filepath.Join(c.Dir, s))
|
|
}
|
|
s.Schema = joined
|
|
|
|
joined = make([]string, 0, len(s.Queries))
|
|
for _, q := range s.Queries {
|
|
joined = append(joined, filepath.Join(c.Dir, q))
|
|
}
|
|
s.Queries = joined
|
|
|
|
var name string
|
|
parseOpts := opts.Parser{}
|
|
|
|
result, failed := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr)
|
|
if failed {
|
|
return ErrFailedChecks
|
|
}
|
|
|
|
var prep preparer
|
|
var expl explainer
|
|
if s.Database != nil { // TODO only set up a database connection if a rule evaluation requires it
|
|
if s.Database.URI != "" && c.OnlyManagedDB {
|
|
return fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed")
|
|
}
|
|
dburl, cleanup, err := c.fetchDatabaseUri(ctx, s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := cleanup(); err != nil {
|
|
fmt.Fprintf(c.Stderr, "error cleaning up: %s\n", err)
|
|
}
|
|
}()
|
|
|
|
switch s.Engine {
|
|
case config.EnginePostgreSQL:
|
|
conn, err := pgx.Connect(ctx, dburl)
|
|
if err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
if err := conn.Ping(ctx); err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
defer conn.Close(ctx)
|
|
pConn := &pgxConn{conn}
|
|
prep = pConn
|
|
expl = pConn
|
|
case config.EngineMySQL:
|
|
db, err := sql.Open("mysql", dburl)
|
|
if err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
if err := db.PingContext(ctx); err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
defer db.Close()
|
|
prep = &dbPreparer{db}
|
|
expl = &mysqlExplainer{db}
|
|
case config.EngineSQLite:
|
|
db, err := sql.Open("sqlite3", dburl)
|
|
if err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
if err := db.PingContext(ctx); err != nil {
|
|
return fmt.Errorf("database: connection error: %s", err)
|
|
}
|
|
defer db.Close()
|
|
// For in-memory SQLite databases, apply migrations
|
|
if isInMemorySQLite(dburl) {
|
|
files, err := sqlpath.Glob(s.Schema)
|
|
if err != nil {
|
|
return fmt.Errorf("schema: %w", err)
|
|
}
|
|
for _, schema := range files {
|
|
contents, err := os.ReadFile(schema)
|
|
if err != nil {
|
|
return fmt.Errorf("read schema file: %w", err)
|
|
}
|
|
ddl := migrations.RemoveRollbackStatements(string(contents))
|
|
if _, err := db.ExecContext(ctx, ddl); err != nil {
|
|
return fmt.Errorf("apply schema %s: %w", schema, err)
|
|
}
|
|
}
|
|
}
|
|
prep = &dbPreparer{db}
|
|
// SQLite really doesn't want us to depend on the output of EXPLAIN
|
|
// QUERY PLAN: https://www.sqlite.org/eqp.html
|
|
expl = nil
|
|
default:
|
|
return fmt.Errorf("unsupported database uri: %s", s.Engine)
|
|
}
|
|
}
|
|
|
|
errored := false
|
|
req := codeGenRequest(result, combo)
|
|
cfg := vetConfig(req)
|
|
for i, query := range req.Queries {
|
|
md := result.Queries[i].Metadata
|
|
if md.Flags[constants.QueryFlagSqlcVetDisable] {
|
|
// If the vet disable flag is specified without any rules listed, all rules are ignored.
|
|
if len(md.RuleSkiplist) == 0 {
|
|
if debug.Active {
|
|
log.Printf("Skipping all vet rules for query: %s\n", query.Name)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Rules which are listed to be disabled but not declared in the config file are rejected.
|
|
for r := range md.RuleSkiplist {
|
|
if !slices.Contains(s.Rules, r) {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: rule-check error: rule %q does not exist in the config file\n", query.Filename, query.Name, r)
|
|
errored = true
|
|
}
|
|
}
|
|
}
|
|
|
|
evalMap := map[string]any{
|
|
"query": vetQuery(query),
|
|
"config": cfg,
|
|
}
|
|
|
|
for _, name := range s.Rules {
|
|
if _, skip := md.RuleSkiplist[name]; skip {
|
|
if debug.Active {
|
|
log.Printf("Skipping vet rule %q for query: %s\n", name, query.Name)
|
|
}
|
|
} else {
|
|
rule, ok := c.Rules[name]
|
|
if !ok {
|
|
return fmt.Errorf("type-check error: a rule with the name '%s' does not exist", name)
|
|
}
|
|
|
|
if rule.NeedsPrepare {
|
|
if prep == nil {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: database connection required\n", query.Filename, query.Name, name)
|
|
errored = true
|
|
continue
|
|
}
|
|
prepName := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i)
|
|
if err := prep.Prepare(ctx, prepName, query.Text); err != nil {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, query.Name, name, err)
|
|
errored = true
|
|
continue
|
|
}
|
|
}
|
|
|
|
// short-circuit for "sqlc/db-prepare" rule which doesn't have a CEL program
|
|
if rule.Program == nil {
|
|
continue
|
|
}
|
|
|
|
// Get explain output for this query if we need it
|
|
_, pgsqlOK := evalMap["postgresql"]
|
|
_, mysqlOK := evalMap["mysql"]
|
|
if rule.NeedsExplain && !(pgsqlOK || mysqlOK) {
|
|
if expl == nil {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: database connection required\n", query.Filename, query.Name, name)
|
|
errored = true
|
|
continue
|
|
}
|
|
engineOutput, err := expl.Explain(ctx, query.Text, query.Params...)
|
|
if err != nil {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: %s\n", query.Filename, query.Name, name, err)
|
|
errored = true
|
|
continue
|
|
}
|
|
|
|
evalMap["postgresql"] = engineOutput.PostgreSQL
|
|
evalMap["mysql"] = engineOutput.MySQL
|
|
}
|
|
|
|
if debugDumpVetEnv.Value() == "1" {
|
|
fmt.Printf("vars for rule '%s' evaluating against query '%s':\n", name, query.Name)
|
|
debug.DumpAsJSON(evalMap)
|
|
}
|
|
|
|
out, _, err := (*rule.Program).Eval(evalMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tripped, ok := out.Value().(bool)
|
|
if !ok {
|
|
return fmt.Errorf("expression returned non-bool value: %v", out.Value())
|
|
}
|
|
if tripped {
|
|
// TODO: Get line numbers in the output
|
|
if rule.Message == "" {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s\n", query.Filename, query.Name, name)
|
|
} else {
|
|
fmt.Fprintf(c.Stderr, "%s: %s: %s: %s\n", query.Filename, query.Name, name, rule.Message)
|
|
}
|
|
errored = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if errored {
|
|
return ErrFailedChecks
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func vetConfig(req *plugin.GenerateRequest) *vet.Config {
|
|
return &vet.Config{
|
|
Version: req.Settings.Version,
|
|
Engine: req.Settings.Engine,
|
|
Schema: req.Settings.Schema,
|
|
Queries: req.Settings.Queries,
|
|
}
|
|
}
|
|
|
|
func vetQuery(q *plugin.Query) *vet.Query {
|
|
var params []*vet.Parameter
|
|
for _, p := range q.Params {
|
|
params = append(params, &vet.Parameter{
|
|
Number: p.Number,
|
|
})
|
|
}
|
|
return &vet.Query{
|
|
Sql: q.Text,
|
|
Name: q.Name,
|
|
Cmd: strings.TrimPrefix(q.Cmd, ":"),
|
|
Params: params,
|
|
}
|
|
}
|
|
|
|
type vetEngineOutput struct {
|
|
PostgreSQL *vet.PostgreSQL
|
|
MySQL *vet.MySQL
|
|
}
|