From ca5bbec54f722518e1655cbcdc840d3f7eadb9fb Mon Sep 17 00:00:00 2001 From: Andrew Benton Date: Fri, 28 Jul 2023 22:15:40 -0400 Subject: [PATCH 1/7] feat(vet): add default query parameters for vet explain with MySQL --- internal/cmd/vet.go | 29 +- .../testdata/vet_explain/mysql/db/db.go | 31 ++ .../testdata/vet_explain/mysql/db/models.go | 137 ++++++ .../vet_explain/mysql/db/query.sql.go | 443 ++++++++++++++++++ .../testdata/vet_explain/mysql/query.sql | 290 ++++++++++++ .../testdata/vet_explain/mysql/schema.sql | 38 ++ .../testdata/vet_explain/mysql/sqlc.yaml | 16 + 7 files changed, 983 insertions(+), 1 deletion(-) create mode 100644 internal/endtoend/testdata/vet_explain/mysql/db/db.go create mode 100644 internal/endtoend/testdata/vet_explain/mysql/db/models.go create mode 100644 internal/endtoend/testdata/vet_explain/mysql/db/query.sql.go create mode 100644 internal/endtoend/testdata/vet_explain/mysql/query.sql create mode 100644 internal/endtoend/testdata/vet_explain/mysql/schema.sql create mode 100644 internal/endtoend/testdata/vet_explain/mysql/sqlc.yaml diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 57fee7c397..243fc115b4 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -210,13 +210,40 @@ type mysqlExplainer struct { 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, arg := range args { + switch arg.Column.Type.Name { + case "int", "bigint", "mediumint", "smallint", "tinyint", "bit": // "bool" + eArgs[i] = 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. + eArgs[i] = 0.1 + case "float", "double": + eArgs[i] = 0.1 + case "date": + eArgs[i] = time.Now().Format(time.DateOnly) + case "datetime", "timestamp", "time": + eArgs[i] = time.Now() + case "year": + eArgs[i] = time.Now().Year() + case "char", "varchar", "binary", "varbinary", "tinyblob", "blob", + "mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext": + eArgs[i] = "A" + case "json": + eArgs[i] = "{}" + default: + eArgs[i] = nil + } + } row := me.QueryRowContext(ctx, eQuery, eArgs...) var result json.RawMessage if err := row.Scan(&result); err != nil { return nil, err } if debug.Debug.DumpExplain { - fmt.Println(eQuery) + fmt.Println(eQuery, "with args", eArgs) fmt.Println(string(result)) } var explain vet.MySQLExplain diff --git a/internal/endtoend/testdata/vet_explain/mysql/db/db.go b/internal/endtoend/testdata/vet_explain/mysql/db/db.go new file mode 100644 index 0000000000..f81f9910e6 --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 + +package test + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/vet_explain/mysql/db/models.go b/internal/endtoend/testdata/vet_explain/mysql/db/models.go new file mode 100644 index 0000000000..fc78656322 --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/db/models.go @@ -0,0 +1,137 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 + +package test + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" +) + +type DebugCenum string + +const ( + DebugCenumOne DebugCenum = "one" + DebugCenumTwo DebugCenum = "two" + DebugCenumThree DebugCenum = "three" +) + +func (e *DebugCenum) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = DebugCenum(s) + case string: + *e = DebugCenum(s) + default: + return fmt.Errorf("unsupported scan type for DebugCenum: %T", src) + } + return nil +} + +type NullDebugCenum struct { + DebugCenum DebugCenum + Valid bool // Valid is true if DebugCenum is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullDebugCenum) Scan(value interface{}) error { + if value == nil { + ns.DebugCenum, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.DebugCenum.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullDebugCenum) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.DebugCenum), nil +} + +type DebugCset string + +const ( + DebugCsetOne DebugCset = "one" + DebugCsetTwo DebugCset = "two" + DebugCsetThree DebugCset = "three" +) + +func (e *DebugCset) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = DebugCset(s) + case string: + *e = DebugCset(s) + default: + return fmt.Errorf("unsupported scan type for DebugCset: %T", src) + } + return nil +} + +type NullDebugCset struct { + DebugCset DebugCset + Valid bool // Valid is true if DebugCset is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullDebugCset) Scan(value interface{}) error { + if value == nil { + ns.DebugCset, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.DebugCset.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullDebugCset) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.DebugCset), nil +} + +type Debug struct { + ID int64 + Csmallint int32 + Cint int32 + Cinteger int32 + Cdecimal string + Cnumeric string + Cfloat float64 + Creal float64 + Cdoubleprecision float64 + Cdouble float64 + Cdec string + Cfixed string + Ctinyint int32 + Cbool bool + Cmediumint int32 + Cbit interface{} + Cdate time.Time + Cdatetime time.Time + Ctimestamp time.Time + Ctime time.Time + Cyear int32 + Cchar string + Cvarchar string + Cbinary []byte + Cvarbinary []byte + Ctinyblob []byte + Cblob []byte + Cmediumblob []byte + Clongblob []byte + Ctinytext string + Ctext string + Cmediumtext string + Clongtext string + Cenum NullDebugCenum + Cset DebugCset + Cjson json.RawMessage +} diff --git a/internal/endtoend/testdata/vet_explain/mysql/db/query.sql.go b/internal/endtoend/testdata/vet_explain/mysql/db/query.sql.go new file mode 100644 index 0000000000..d8ced7364d --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/db/query.sql.go @@ -0,0 +1,443 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 +// source: query.sql + +package test + +import ( + "context" + "encoding/json" + "time" +) + +const selectByCbinary = `-- name: SelectByCbinary :one +SELECT id FROM debug +WHERE Cbinary = ? LIMIT 1 +` + +func (q *Queries) SelectByCbinary(ctx context.Context, cbinary []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCbinary, cbinary) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCbit = `-- name: SelectByCbit :one +SELECT id FROM debug +WHERE Cbit = ? LIMIT 1 +` + +func (q *Queries) SelectByCbit(ctx context.Context, cbit interface{}) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCbit, cbit) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCblob = `-- name: SelectByCblob :one +SELECT id FROM debug +WHERE Cblob = ? LIMIT 1 +` + +func (q *Queries) SelectByCblob(ctx context.Context, cblob []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCblob, cblob) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCbool = `-- name: SelectByCbool :one +SELECT id FROM debug +WHERE Cbool = ? LIMIT 1 +` + +func (q *Queries) SelectByCbool(ctx context.Context, cbool bool) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCbool, cbool) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCchar = `-- name: SelectByCchar :one +SELECT id FROM debug +WHERE Cchar = ? LIMIT 1 +` + +func (q *Queries) SelectByCchar(ctx context.Context, cchar string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCchar, cchar) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdate = `-- name: SelectByCdate :one +SELECT id FROM debug +WHERE Cdate = ? LIMIT 1 +` + +func (q *Queries) SelectByCdate(ctx context.Context, cdate time.Time) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdate, cdate) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdatetime = `-- name: SelectByCdatetime :one +SELECT id FROM debug +WHERE Cdatetime = ? LIMIT 1 +` + +func (q *Queries) SelectByCdatetime(ctx context.Context, cdatetime time.Time) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdatetime, cdatetime) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdec = `-- name: SelectByCdec :one +SELECT id FROM debug +WHERE Cdec = ? LIMIT 1 +` + +func (q *Queries) SelectByCdec(ctx context.Context, cdec string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdec, cdec) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdecimal = `-- name: SelectByCdecimal :one +SELECT id FROM debug +WHERE Cdecimal = ? LIMIT 1 +` + +func (q *Queries) SelectByCdecimal(ctx context.Context, cdecimal string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdecimal, cdecimal) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdouble = `-- name: SelectByCdouble :one +SELECT id FROM debug +WHERE Cdouble = ? LIMIT 1 +` + +func (q *Queries) SelectByCdouble(ctx context.Context, cdouble float64) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdouble, cdouble) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCdoubleprecision = `-- name: SelectByCdoubleprecision :one +SELECT id FROM debug +WHERE Cdoubleprecision = ? LIMIT 1 +` + +func (q *Queries) SelectByCdoubleprecision(ctx context.Context, cdoubleprecision float64) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCdoubleprecision, cdoubleprecision) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCenum = `-- name: SelectByCenum :one +SELECT id FROM debug +WHERE Cenum = ? LIMIT 1 +` + +func (q *Queries) SelectByCenum(ctx context.Context, cenum NullDebugCenum) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCenum, cenum) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCfixed = `-- name: SelectByCfixed :one +SELECT id FROM debug +WHERE Cfixed = ? LIMIT 1 +` + +func (q *Queries) SelectByCfixed(ctx context.Context, cfixed string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCfixed, cfixed) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCfloat = `-- name: SelectByCfloat :one +SELECT id FROM debug +WHERE Cfloat = ? LIMIT 1 +` + +func (q *Queries) SelectByCfloat(ctx context.Context, cfloat float64) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCfloat, cfloat) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCint = `-- name: SelectByCint :one +SELECT id FROM debug +WHERE Cint = ? LIMIT 1 +` + +func (q *Queries) SelectByCint(ctx context.Context, cint int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCint, cint) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCinteger = `-- name: SelectByCinteger :one +SELECT id FROM debug +WHERE Cinteger = ? LIMIT 1 +` + +func (q *Queries) SelectByCinteger(ctx context.Context, cinteger int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCinteger, cinteger) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCjson = `-- name: SelectByCjson :one +SELECT id FROM debug +WHERE Cjson = ? LIMIT 1 +` + +func (q *Queries) SelectByCjson(ctx context.Context, cjson json.RawMessage) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCjson, cjson) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByClongblob = `-- name: SelectByClongblob :one +SELECT id FROM debug +WHERE Clongblob = ? LIMIT 1 +` + +func (q *Queries) SelectByClongblob(ctx context.Context, clongblob []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByClongblob, clongblob) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByClongtext = `-- name: SelectByClongtext :one +SELECT id FROM debug +WHERE Clongtext = ? LIMIT 1 +` + +func (q *Queries) SelectByClongtext(ctx context.Context, clongtext string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByClongtext, clongtext) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCmediumblob = `-- name: SelectByCmediumblob :one +SELECT id FROM debug +WHERE Cmediumblob = ? LIMIT 1 +` + +func (q *Queries) SelectByCmediumblob(ctx context.Context, cmediumblob []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCmediumblob, cmediumblob) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCmediumint = `-- name: SelectByCmediumint :one +SELECT id FROM debug +WHERE Cmediumint = ? LIMIT 1 +` + +func (q *Queries) SelectByCmediumint(ctx context.Context, cmediumint int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCmediumint, cmediumint) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCmediumtext = `-- name: SelectByCmediumtext :one +SELECT id FROM debug +WHERE Cmediumtext = ? LIMIT 1 +` + +func (q *Queries) SelectByCmediumtext(ctx context.Context, cmediumtext string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCmediumtext, cmediumtext) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCnumeric = `-- name: SelectByCnumeric :one +SELECT id FROM debug +WHERE Cnumeric = ? LIMIT 1 +` + +func (q *Queries) SelectByCnumeric(ctx context.Context, cnumeric string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCnumeric, cnumeric) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCreal = `-- name: SelectByCreal :one +SELECT id FROM debug +WHERE Creal = ? LIMIT 1 +` + +func (q *Queries) SelectByCreal(ctx context.Context, creal float64) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCreal, creal) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCset = `-- name: SelectByCset :one +SELECT id FROM debug +WHERE Cset = ? LIMIT 1 +` + +func (q *Queries) SelectByCset(ctx context.Context, cset DebugCset) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCset, cset) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCsmallint = `-- name: SelectByCsmallint :one +SELECT id FROM debug +WHERE Csmallint = ? LIMIT 1 +` + +func (q *Queries) SelectByCsmallint(ctx context.Context, csmallint int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCsmallint, csmallint) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtext = `-- name: SelectByCtext :one +SELECT id FROM debug +WHERE Ctext = ? LIMIT 1 +` + +func (q *Queries) SelectByCtext(ctx context.Context, ctext string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtext, ctext) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtime = `-- name: SelectByCtime :one +SELECT id FROM debug +WHERE Ctime = ? LIMIT 1 +` + +func (q *Queries) SelectByCtime(ctx context.Context, ctime time.Time) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtime, ctime) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtimestamp = `-- name: SelectByCtimestamp :one +SELECT id FROM debug +WHERE Ctimestamp = ? LIMIT 1 +` + +func (q *Queries) SelectByCtimestamp(ctx context.Context, ctimestamp time.Time) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtimestamp, ctimestamp) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtinyblob = `-- name: SelectByCtinyblob :one +SELECT id FROM debug +WHERE Ctinyblob = ? LIMIT 1 +` + +func (q *Queries) SelectByCtinyblob(ctx context.Context, ctinyblob []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtinyblob, ctinyblob) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtinyint = `-- name: SelectByCtinyint :one +SELECT id FROM debug +WHERE Ctinyint = ? LIMIT 1 +` + +func (q *Queries) SelectByCtinyint(ctx context.Context, ctinyint int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtinyint, ctinyint) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCtinytext = `-- name: SelectByCtinytext :one +SELECT id FROM debug +WHERE Ctinytext = ? LIMIT 1 +` + +func (q *Queries) SelectByCtinytext(ctx context.Context, ctinytext string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCtinytext, ctinytext) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCvarbinary = `-- name: SelectByCvarbinary :one +SELECT id FROM debug +WHERE Cvarbinary = ? LIMIT 1 +` + +func (q *Queries) SelectByCvarbinary(ctx context.Context, cvarbinary []byte) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCvarbinary, cvarbinary) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCvarchar = `-- name: SelectByCvarchar :one +SELECT id FROM debug +WHERE Cvarchar = ? LIMIT 1 +` + +func (q *Queries) SelectByCvarchar(ctx context.Context, cvarchar string) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCvarchar, cvarchar) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectByCyear = `-- name: SelectByCyear :one +SELECT id FROM debug +WHERE Cyear = ? LIMIT 1 +` + +func (q *Queries) SelectByCyear(ctx context.Context, cyear int32) (int64, error) { + row := q.db.QueryRowContext(ctx, selectByCyear, cyear) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectById = `-- name: SelectById :one +SELECT id FROM debug +WHERE id = ? LIMIT 1 +` + +func (q *Queries) SelectById(ctx context.Context, id int64) (int64, error) { + row := q.db.QueryRowContext(ctx, selectById, id) + err := row.Scan(&id) + return id, err +} diff --git a/internal/endtoend/testdata/vet_explain/mysql/query.sql b/internal/endtoend/testdata/vet_explain/mysql/query.sql new file mode 100644 index 0000000000..9712dd7478 --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/query.sql @@ -0,0 +1,290 @@ +-- name: SelectById :one +SELECT id FROM debug +WHERE id = ? LIMIT 1; + +-- name: SelectByCsmallint :one +SELECT id FROM debug +WHERE Csmallint = ? LIMIT 1; + +-- name: SelectByCint :one +SELECT id FROM debug +WHERE Cint = ? LIMIT 1; + +-- name: SelectByCinteger :one +SELECT id FROM debug +WHERE Cinteger = ? LIMIT 1; + +-- name: SelectByCdecimal :one +SELECT id FROM debug +WHERE Cdecimal = ? LIMIT 1; + +-- name: SelectByCnumeric :one +SELECT id FROM debug +WHERE Cnumeric = ? LIMIT 1; + +-- name: SelectByCfloat :one +SELECT id FROM debug +WHERE Cfloat = ? LIMIT 1; + +-- name: SelectByCreal :one +SELECT id FROM debug +WHERE Creal = ? LIMIT 1; + +-- name: SelectByCdoubleprecision :one +SELECT id FROM debug +WHERE Cdoubleprecision = ? LIMIT 1; + +-- name: SelectByCdouble :one +SELECT id FROM debug +WHERE Cdouble = ? LIMIT 1; + +-- name: SelectByCdec :one +SELECT id FROM debug +WHERE Cdec = ? LIMIT 1; + +-- name: SelectByCfixed :one +SELECT id FROM debug +WHERE Cfixed = ? LIMIT 1; + +-- name: SelectByCtinyint :one +SELECT id FROM debug +WHERE Ctinyint = ? LIMIT 1; + +-- name: SelectByCbool :one +SELECT id FROM debug +WHERE Cbool = ? LIMIT 1; + +-- name: SelectByCmediumint :one +SELECT id FROM debug +WHERE Cmediumint = ? LIMIT 1; + +-- name: SelectByCbit :one +SELECT id FROM debug +WHERE Cbit = ? LIMIT 1; + +-- name: SelectByCdate :one +SELECT id FROM debug +WHERE Cdate = ? LIMIT 1; + +-- name: SelectByCdatetime :one +SELECT id FROM debug +WHERE Cdatetime = ? LIMIT 1; + +-- name: SelectByCtimestamp :one +SELECT id FROM debug +WHERE Ctimestamp = ? LIMIT 1; + +-- name: SelectByCtime :one +SELECT id FROM debug +WHERE Ctime = ? LIMIT 1; + +-- name: SelectByCyear :one +SELECT id FROM debug +WHERE Cyear = ? LIMIT 1; + +-- name: SelectByCchar :one +SELECT id FROM debug +WHERE Cchar = ? LIMIT 1; + +-- name: SelectByCvarchar :one +SELECT id FROM debug +WHERE Cvarchar = ? LIMIT 1; + +-- name: SelectByCbinary :one +SELECT id FROM debug +WHERE Cbinary = ? LIMIT 1; + +-- name: SelectByCvarbinary :one +SELECT id FROM debug +WHERE Cvarbinary = ? LIMIT 1; + +-- name: SelectByCtinyblob :one +SELECT id FROM debug +WHERE Ctinyblob = ? LIMIT 1; + +-- name: SelectByCblob :one +SELECT id FROM debug +WHERE Cblob = ? LIMIT 1; + +-- name: SelectByCmediumblob :one +SELECT id FROM debug +WHERE Cmediumblob = ? LIMIT 1; + +-- name: SelectByClongblob :one +SELECT id FROM debug +WHERE Clongblob = ? LIMIT 1; + +-- name: SelectByCtinytext :one +SELECT id FROM debug +WHERE Ctinytext = ? LIMIT 1; + +-- name: SelectByCtext :one +SELECT id FROM debug +WHERE Ctext = ? LIMIT 1; + +-- name: SelectByCmediumtext :one +SELECT id FROM debug +WHERE Cmediumtext = ? LIMIT 1; + +-- name: SelectByClongtext :one +SELECT id FROM debug +WHERE Clongtext = ? LIMIT 1; + +-- name: SelectByCenum :one +SELECT id FROM debug +WHERE Cenum = ? LIMIT 1; + +-- name: SelectByCset :one +SELECT id FROM debug +WHERE Cset = ? LIMIT 1; + +-- name: SelectByCjson :one +SELECT id FROM debug +WHERE Cjson = ? LIMIT 1; + +-- +-- + +-- -- name: DeleteById :exec +-- DELETE FROM debug +-- WHERE id = ?; + +-- -- name: DeleteByCsmallint :exec +-- DELETE FROM debug +-- WHERE Csmallint = ? LIMIT 1; + +-- -- name: DeleteByCint :exec +-- DELETE FROM debug +-- WHERE Cint = ? LIMIT 1; + +-- -- name: DeleteByCinteger :exec +-- DELETE FROM debug +-- WHERE Cinteger = ? LIMIT 1; + +-- -- name: DeleteByCdecimal :exec +-- DELETE FROM debug +-- WHERE Cdecimal = ? LIMIT 1; + +-- -- name: DeleteByCnumeric :exec +-- DELETE FROM debug +-- WHERE Cnumeric = ? LIMIT 1; + +-- -- name: DeleteByCfloat :exec +-- DELETE FROM debug +-- WHERE Cfloat = ? LIMIT 1; + +-- -- name: DeleteByCreal :exec +-- DELETE FROM debug +-- WHERE Creal = ? LIMIT 1; + +-- -- name: DeleteByCdoubleprecision :exec +-- DELETE FROM debug +-- WHERE Cdoubleprecision = ? LIMIT 1; + +-- -- name: DeleteByCdouble :exec +-- DELETE FROM debug +-- WHERE Cdouble = ? LIMIT 1; + +-- -- name: DeleteByCdec :exec +-- DELETE FROM debug +-- WHERE Cdec = ? LIMIT 1; + +-- -- name: DeleteByCfixed :exec +-- DELETE FROM debug +-- WHERE Cfixed = ? LIMIT 1; + +-- -- name: DeleteByCtinyint :exec +-- DELETE FROM debug +-- WHERE Ctinyint = ? LIMIT 1; + +-- -- name: DeleteByCbool :exec +-- DELETE FROM debug +-- WHERE Cbool = ? LIMIT 1; + +-- -- name: DeleteByCmediumint :exec +-- DELETE FROM debug +-- WHERE Cmediumint = ? LIMIT 1; + +-- -- name: DeleteByCbit :exec +-- DELETE FROM debug +-- WHERE Cbit = ? LIMIT 1; + +-- -- name: DeleteByCdate :exec +-- DELETE FROM debug +-- WHERE Cdate = ? LIMIT 1; + +-- -- name: DeleteByCdatetime :exec +-- DELETE FROM debug +-- WHERE Cdatetime = ? LIMIT 1; + +-- -- name: DeleteByCtimestamp :exec +-- DELETE FROM debug +-- WHERE Ctimestamp = ? LIMIT 1; + +-- -- name: DeleteByCtime :exec +-- DELETE FROM debug +-- WHERE Ctime = ? LIMIT 1; + +-- -- name: DeleteByCyear :exec +-- DELETE FROM debug +-- WHERE Cyear = ? LIMIT 1; + +-- -- name: DeleteByCchar :exec +-- DELETE FROM debug +-- WHERE Cchar = ? LIMIT 1; + +-- -- name: DeleteByCvarchar :exec +-- DELETE FROM debug +-- WHERE Cvarchar = ?; + +-- -- name: DeleteByCbinary :exec +-- DELETE FROM debug +-- WHERE Cbinary = ? LIMIT 1; + +-- -- name: DeleteByCvarbinary :exec +-- DELETE FROM debug +-- WHERE Cvarbinary = ? LIMIT 1; + +-- -- name: DeleteByCtinyblob :exec +-- DELETE FROM debug +-- WHERE Ctinyblob = ? LIMIT 1; + +-- -- name: DeleteByCblob :exec +-- DELETE FROM debug +-- WHERE Cblob = ? LIMIT 1; + +-- -- name: DeleteByCmediumblob :exec +-- DELETE FROM debug +-- WHERE Cmediumblob = ? LIMIT 1; + +-- -- name: DeleteByClongblob :exec +-- DELETE FROM debug +-- WHERE Clongblob = ? LIMIT 1; + +-- -- name: DeleteByCtinytext :exec +-- DELETE FROM debug +-- WHERE Ctinytext = ? LIMIT 1; + +-- -- name: DeleteByCtext :exec +-- DELETE FROM debug +-- WHERE Ctext = ? LIMIT 1; + +-- -- name: DeleteByCmediumtext :exec +-- DELETE FROM debug +-- WHERE Cmediumtext = ? LIMIT 1; + +-- -- name: DeleteByClongtext :exec +-- DELETE FROM debug +-- WHERE Clongtext = ? LIMIT 1; + +-- -- name: DeleteByCenum :exec +-- DELETE FROM debug +-- WHERE Cenum = ? LIMIT 1; + +-- -- name: DeleteByCset :exec +-- DELETE FROM debug +-- WHERE Cset = ? LIMIT 1; + +-- -- name: DeleteByCjson :exec +-- DELETE FROM debug +-- WHERE Cjson = ? LIMIT 1; diff --git a/internal/endtoend/testdata/vet_explain/mysql/schema.sql b/internal/endtoend/testdata/vet_explain/mysql/schema.sql new file mode 100644 index 0000000000..3a7476b603 --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/schema.sql @@ -0,0 +1,38 @@ +CREATE TABLE debug ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + Csmallint smallint not null, + Cint int not null, + Cinteger integer not null, + Cdecimal decimal(1,1) not null, + Cnumeric numeric(2,1) not null, + Cfloat float not null, + Creal real not null, + Cdoubleprecision double precision not null, + Cdouble double not null, + Cdec dec(3,2) not null, + Cfixed fixed(5,5) not null, + Ctinyint tinyint not null, + Cbool bool not null, + Cmediumint mediumint not null, + Cbit bit not null, + Cdate date not null, + Cdatetime datetime not null, + Ctimestamp timestamp not null, + Ctime time not null, + Cyear year not null, + Cchar char(1) not null, + Cvarchar varchar(1) not null, + Cbinary binary(1) not null, + Cvarbinary varbinary(1) not null, + Ctinyblob tinyblob not null, + Cblob blob not null, + Cmediumblob mediumblob not null, + Clongblob longblob not null, + Ctinytext tinytext NOT NULL, + Ctext text NOT NULL, + Cmediumtext mediumtext NOT NULL, + Clongtext longtext NOT NULL, + Cenum ENUM('one', 'two', 'three'), + Cset SET('one', 'two', 'three') NOT NULL, + Cjson JSON NOT NULL +); \ No newline at end of file diff --git a/internal/endtoend/testdata/vet_explain/mysql/sqlc.yaml b/internal/endtoend/testdata/vet_explain/mysql/sqlc.yaml new file mode 100644 index 0000000000..8e83a1c606 --- /dev/null +++ b/internal/endtoend/testdata/vet_explain/mysql/sqlc.yaml @@ -0,0 +1,16 @@ +version: 2 +sql: + - schema: "schema.sql" + queries: "query.sql" + engine: "mysql" + database: + uri: root:${MYSQL_ROOT_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/test?multiStatements=true&parseTime=true + gen: + go: + package: "test" + out: "db" + rules: + - test +rules: + - name: test + rule: "!has(mysql.explain)" From a0b2549037925b3e6860df8f9be7b8e93bfc7eeb Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sun, 30 Jul 2023 12:53:47 -0700 Subject: [PATCH 2/7] feat(vet): Add default parameter values for PostgreSQL --- internal/cmd/shim.go | 13 +++- internal/cmd/vet.go | 159 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 144 insertions(+), 28 deletions(-) diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 20b68ca178..7958277345 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -297,10 +297,21 @@ func pluginQueryParam(p compiler.Parameter) *plugin.Parameter { } func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.CodeGenRequest { - return &plugin.CodeGenRequest{ + out := &plugin.CodeGenRequest{ Settings: pluginSettings(r, settings), Catalog: pluginCatalog(r.Catalog), Queries: pluginQueries(r), SqlcVersion: info.Version, } + + // TODO: Remove this + if out.Settings.Engine == "mysql" { + for _, query := range out.Queries { + for _, col := range query.Columns { + mysqlDefaultValue(col) + } + } + } + + return out } diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 243fc115b4..97d211966c 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -170,16 +170,145 @@ func (p *pgxConn) Prepare(ctx context.Context, name, query string) error { 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": + t := time.Time{} + return t.Format(time.DateOnly) + case "datetime", "timestamp", "time": + return time.Time{} + case "year": + t := time.Time{} + return t.Year() + 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 debug.Debug.DumpExplain { - fmt.Println(eQuery) + fmt.Println(eQuery, "with args", eArgs) fmt.Println(string(result[0])) } var explain vet.PostgreSQLExplain @@ -210,32 +339,8 @@ type mysqlExplainer struct { 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, arg := range args { - switch arg.Column.Type.Name { - case "int", "bigint", "mediumint", "smallint", "tinyint", "bit": // "bool" - eArgs[i] = 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. - eArgs[i] = 0.1 - case "float", "double": - eArgs[i] = 0.1 - case "date": - eArgs[i] = time.Now().Format(time.DateOnly) - case "datetime", "timestamp", "time": - eArgs[i] = time.Now() - case "year": - eArgs[i] = time.Now().Year() - case "char", "varchar", "binary", "varbinary", "tinyblob", "blob", - "mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext": - eArgs[i] = "A" - case "json": - eArgs[i] = "{}" - default: - eArgs[i] = nil - } + for i, a := range args { + eArgs[i] = mysqlDefaultValue(a.Column) } row := me.QueryRowContext(ctx, eQuery, eArgs...) var result json.RawMessage From eb73cf5e6816db1b90f61fff11b620b35c7520ae Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sun, 30 Jul 2023 12:58:06 -0700 Subject: [PATCH 3/7] cleanup --- internal/cmd/shim.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 7958277345..20b68ca178 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -297,21 +297,10 @@ func pluginQueryParam(p compiler.Parameter) *plugin.Parameter { } func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.CodeGenRequest { - out := &plugin.CodeGenRequest{ + return &plugin.CodeGenRequest{ Settings: pluginSettings(r, settings), Catalog: pluginCatalog(r.Catalog), Queries: pluginQueries(r), SqlcVersion: info.Version, } - - // TODO: Remove this - if out.Settings.Engine == "mysql" { - for _, query := range out.Queries { - for _, col := range query.Columns { - mysqlDefaultValue(col) - } - } - } - - return out } From c0e6f391109d51c4215ae91c88f4daff313dfdbf Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sun, 30 Jul 2023 14:48:35 -0700 Subject: [PATCH 4/7] Mysql date / time zero values --- internal/cmd/vet.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 97d211966c..12b80748f9 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -279,13 +279,13 @@ func mysqlDefaultValue(col *plugin.Column) any { case "float", "double": return 0.1 case "date": - t := time.Time{} - return t.Format(time.DateOnly) - case "datetime", "timestamp", "time": + return "0000-00-00" + case "datetime", "timestamp": return time.Time{} + case "time": + return "00:00:00" case "year": - t := time.Time{} - return t.Year() + return "0000" case "char", "varchar", "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext": return "" From 7e98480e47d1f034e8321dfa5589244a97483743 Mon Sep 17 00:00:00 2001 From: Kyle Gray Date: Sun, 30 Jul 2023 19:48:31 -0700 Subject: [PATCH 5/7] Update internal/cmd/vet.go --- internal/cmd/vet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 12b80748f9..da0189e9d9 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -281,7 +281,7 @@ func mysqlDefaultValue(col *plugin.Column) any { case "date": return "0000-00-00" case "datetime", "timestamp": - return time.Time{} + return "0000-00-00 00:00:00" case "time": return "00:00:00" case "year": From 187345e840992a759eff68dac513905d4fa5edbc Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sun, 30 Jul 2023 20:04:38 -0700 Subject: [PATCH 6/7] Add a few more docs --- docs/howto/vet.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/howto/vet.md b/docs/howto/vet.md index 91e026a613..6345075008 100644 --- a/docs/howto/vet.md +++ b/docs/howto/vet.md @@ -89,6 +89,8 @@ rules: ### Rules using `EXPLAIN ...` output +*Added in v1.20.0* + The CEL expression environment has two variables containing `EXPLAIN ...` output, `postgresql.explain` and `mysql.explain`. `sqlc` only populates the variable associated with your configured database engine, and only when you have a @@ -100,7 +102,7 @@ For the `postgresql` engine, `sqlc` runs EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) ... ``` -where `"..."` is your query string, and parses the output into a `PostgreSQLExplain` proto message. +where `"..."` is your query string, and parses the output into a [`PostgreSQLExplain`](https://buf.build/sqlc/sqlc/docs/main:vet#vet.PostgreSQLExplain) proto message. For the `mysql` engine, `sqlc` runs @@ -108,7 +110,7 @@ For the `mysql` engine, `sqlc` runs EXPLAIN FORMAT=JSON ... ``` -where `"..."` is your query string, and parses the output into a `MySQLExplain` proto message. +where `"..."` is your query string, and parses the output into a [`MySQLExplain`](https://buf.build/sqlc/sqlc/docs/main:vet#vet.MySQLExplain) proto message. These proto message definitions are too long to include here, but you can find them in the `protos` directory within the `sqlc` source tree. From 350e134df74f3d2b94f80b5fddf38b9eb1ef9d47 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sun, 30 Jul 2023 20:11:52 -0700 Subject: [PATCH 7/7] docs: Fix buf link --- docs/howto/vet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/howto/vet.md b/docs/howto/vet.md index 6345075008..1b3d075076 100644 --- a/docs/howto/vet.md +++ b/docs/howto/vet.md @@ -102,7 +102,7 @@ For the `postgresql` engine, `sqlc` runs EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) ... ``` -where `"..."` is your query string, and parses the output into a [`PostgreSQLExplain`](https://buf.build/sqlc/sqlc/docs/main:vet#vet.PostgreSQLExplain) proto message. +where `"..."` is your query string, and parses the output into a [`PostgreSQLExplain`](https://buf.build/sqlc/sqlc/docs/v1.20.0:vet#vet.PostgreSQLExplain) proto message. For the `mysql` engine, `sqlc` runs @@ -110,7 +110,7 @@ For the `mysql` engine, `sqlc` runs EXPLAIN FORMAT=JSON ... ``` -where `"..."` is your query string, and parses the output into a [`MySQLExplain`](https://buf.build/sqlc/sqlc/docs/main:vet#vet.MySQLExplain) proto message. +where `"..."` is your query string, and parses the output into a [`MySQLExplain`](https://buf.build/sqlc/sqlc/docs/v1.20.0:vet#vet.MySQLExplain) proto message. These proto message definitions are too long to include here, but you can find them in the `protos` directory within the `sqlc` source tree.