Skip to content

Commit

Permalink
Fix mysql for real (#49)
Browse files Browse the repository at this point in the history
* fix problems with mysql.

* use a prepared statement explicitly to make mysql happy when there are no query parameters

* only attempt to use the prepared statement if there are no query parameters, as that's the bug in the mysql driver

* simplify code

The problem with MySQL's Go driver (https://github.com/go-sql-driver/mysql) is that it won't translate numbers in the response when the text protocol is used (which happens when a query is run without any parameters). Multiple issues have been filed against this for years, but the developers have responded with a wontfix due to the dubious claim that it's too much of a performance hit. See go-sql-driver/mysql#861 for an example.

This update checks to see if a query has no query args. If it does, it checks to see if the Querier/ContextQuerier also implements Prepare/PrepareContext and if so, it creates a statement, forcing MySQL to use the binary protocol. (Postgres will also be using a prepared statement unnecessarily, but it probably has minimal performance difference).
  • Loading branch information
jonbodner authored Sep 4, 2020
1 parent e16badc commit fbbe783
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 2 deletions.
59 changes: 59 additions & 0 deletions proteus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,65 @@ func setupMySQL(c context.Context, dao interface{}) (*sql.DB, error) {
return db, err
}

func TestNoParams(t *testing.T) {
type ScannerProduct struct {
Id int `prof:"id"`
Name string `prof:"name"`
}

type ScannerProductDao struct {
Insert func(e Executor) (int64, error) `proq:"insert into product(name) values('hi')"`
FindAll func(e Querier) (ScannerProduct, error) `proq:"select * from product"`
FindAllContext func(ctx context.Context, e ContextQuerier) (ScannerProduct, error) `proq:"select * from product"`
}

doTest := func(t *testing.T, setup setup, create string) {
productDao := ScannerProductDao{}
c := logger.WithLevel(context.Background(), logger.DEBUG)
db, err := setup(c, &productDao)
if err != nil {
t.Fatal(err)
}
defer db.Close()

tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Commit()

_, err = tx.Exec(create)
if err != nil {
t.Fatal(err)
}

_, err = productDao.Insert(tx)
if err != nil {
t.Fatal(err)
}
roundTrip, err := productDao.FindAll(tx)
if err != nil {
t.Fatal(err)
}
if roundTrip.Id != 1 || roundTrip.Name != "hi" {
t.Errorf("Expected {1 hi}, got %v", roundTrip)
}
roundTrip2, err := productDao.FindAllContext(context.Background(), tx)
if err != nil {
t.Fatal(err)
}
if roundTrip2.Id != 1 || roundTrip2.Name != "hi" {
t.Errorf("Expected {1 hi}, got %v", roundTrip2)
}
}
t.Run("postgres", func(t *testing.T) {
doTest(t, setupPostgres, " drop table if exists product; CREATE TABLE product(id SERIAL PRIMARY KEY, name VARCHAR(100), null_field VARCHAR(100))")
})
t.Run("mysql", func(t *testing.T) {
doTest(t, setupMySQL, " drop table if exists product; CREATE TABLE product(id int AUTO_INCREMENT, name VARCHAR(100), null_field VARCHAR(100), PRIMARY KEY(id))")
})
}

func TestUnnamedStructs(t *testing.T) {

type ScannerProduct struct {
Expand Down
36 changes: 34 additions & 2 deletions runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,24 @@ func makeContextQuerierImplementation(c context.Context, funcType reflect.Type,
}

logger.Log(ctx, logger.DEBUG, fmt.Sprintln("calling", finalQuery, "with params", queryArgs))
// going to work around the defective Go MySQL driver, which refuses to convert the text protocol properly.
// It is used when doing a query without parameters.
if len(queryArgs) == 0 {
type ContextPreparer interface {
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}
if cp, ok := querier.(ContextPreparer); ok {
var stmt *sql.Stmt
stmt, err = cp.PrepareContext(ctx, finalQuery)
if err != nil {
return buildRetVals(rows, err)
}
defer stmt.Close()
rows, err = stmt.QueryContext(ctx)
return buildRetVals(rows, err)
}
}
rows, err = querier.QueryContext(ctx, finalQuery, queryArgs...)

return buildRetVals(rows, err)
}, nil
}
Expand Down Expand Up @@ -220,8 +236,24 @@ func makeQuerierImplementation(c context.Context, funcType reflect.Type, query q
}

logger.Log(c, logger.DEBUG, fmt.Sprintln("calling", finalQuery, "with params", queryArgs))
// going to work around the defective Go MySQL driver, which refuses to convert the text protocol properly.
// It is used when doing a query without parameters.
if len(queryArgs) == 0 {
type Preparer interface {
Prepare(query string) (*sql.Stmt, error)
}
if cp, ok := querier.(Preparer); ok {
var stmt *sql.Stmt
stmt, err = cp.Prepare(finalQuery)
if err != nil {
return buildRetVals(rows, err)
}
defer stmt.Close()
rows, err = stmt.Query()
return buildRetVals(rows, err)
}
}
rows, err = querier.Query(finalQuery, queryArgs...)

return buildRetVals(rows, err)
}, nil
}
Expand Down

0 comments on commit fbbe783

Please sign in to comment.