qustavo/sqlhooks

runtime error: comparing uncomparable type mssql.Error

tochka opened this issue · 7 comments

I've got a panic when using your library (2.1.0) with github.com/microsoft/go-mssqldb@v1.7.2 sql driver.

Stack trace

/usr/local/go/src/runtime/panic.go:770 +0x124
github.com/qustavo/sqlhooks/v2.composed.OnError({0x14000a5a650?, 0x2?, 0x104ad1740?}, {0x1051a44d8, 0x140009884e0}, {0x10517a120, 0x14000acade0}, {0x1043e2b1a, 0x23}, {0x1400073c940, ...})
    /Users/vvinogradov/go/pkg/mod/github.com/qustavo/sqlhooks/v2@v2.1.0/compose.go:52 +0xfc
github.com/qustavo/sqlhooks/v2.handlerErr({0x1051a44d8?, 0x140009884e0?}, {0x1051851f0?, 0x14000915908?}, {0x10517a120, 0x14000acade0}, {0x1043e2b1a?, 0x23?}, {0x1400073c940?, 0x2?, ...})
    /Users/vvinogradov/go/pkg/mod/github.com/qustavo/sqlhooks/v2@v2.1.0/sqlhooks.go:32 +0x8c
github.com/qustavo/sqlhooks/v2.(*Stmt).QueryContext(0x140009884b0, {0x1051a4430, 0x106abf780}, {0x14000534a50, 0x2, 0x2})
    /Users/vvinogradov/go/pkg/mod/github.com/qustavo/sqlhooks/v2@v2.1.0/sqlhooks.go:312 +0x1c4
database/sql.ctxDriverStmtQuery({0x1051a4430, 0x106abf780}, {0x1051a56c8, 0x140009884b0}, {0x14000534a50, 0x2, 0x2?})
    /usr/local/go/src/database/sql/ctxutil.go:82 +0x9c
database/sql.rowsiFromStatement({0x1051a4430, 0x106abf780}, {0x1051935e0, 0x1400073c900}, 0x1400095d640, {0x14000a91828, 0x2, 0x2})
    /usr/local/go/src/database/sql/sql.go:2836 +0x118
database/sql.(*DB).queryDC(0x14000856a90?, {0x1051a4430, 0x106abf780}, {0x0, 0x0}, 0x14000d8abd0, 0x14000c71e40, {0x1043e2b1a, 0x23}, {0x14000a91828, ...})
    /usr/local/go/src/database/sql/sql.go:1806 +0x26c
database/sql.(*DB).query(0x14000856a90, {0x1051a4430, 0x106abf780}, {0x1043e2b1a, 0x23}, {0x14000a91828, 0x2, 0x2}, 0x0?)
    /usr/local/go/src/database/sql/sql.go:1754 +0xb4
database/sql.(*DB).QueryContext.func1(0xf8?)
    /usr/local/go/src/database/sql/sql.go:1732 +0x54
database/sql.(*DB).retry(0x58?, 0x14000a91630)
    /usr/local/go/src/database/sql/sql.go:1566 +0x4c
database/sql.(*DB).QueryContext(0x104e3c2e0?, {0x1051a4430?, 0x106abf780?}, {0x1043e2b1a?, 0x1400014af00?}, {0x14000a91828?, 0x14000a91718?, 0x104312878?})
    /usr/local/go/src/database/sql/sql.go:1731 +0x94
database/sql.(*DB).Query(...)
    /usr/local/go/src/database/sql/sql.go:1745

can you provide the code you are using?

package mssql_test

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"net"
	"testing"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/go-connections/nat"
	gomssql "github.com/microsoft/go-mssqldb"
	"github.com/qustavo/sqlhooks/v2"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

const ddlTestFunc = `create procedure test_proc  AS
begin
	DECLARE @ErrorMsg varchar(7000)

	SET NOCOUNT ON;
	select @ErrorMsg = 'Test error'
	raiserror(@ErrorMsg, 16, 1)
	return
end;`

func TestMsSQL(t *testing.T) {
	ctx := context.Background()
	req := testcontainers.ContainerRequest{
		Image:        "mcr.microsoft.com/mssql/server:2019-latest",
		ExposedPorts: []string{"1433/tcp"},
		SkipReaper:   true,
		Env: map[string]string{
			"ACCEPT_EULA":       "Y",
			"MSSQL_PID":         "Evaluation",
			"MSSQL_SA_PASSWORD": "Test87654321",
		},
		WaitingFor: wait.ForSQL("1433/tcp", "sqlserver", func(host string, port nat.Port) string {
			return fmt.Sprintf("sqlserver://sa:Test87654321@%s/master", net.JoinHostPort(host, port.Port()))
		}),
		HostConfigModifier: func(hc *container.HostConfig) {
			hc.AutoRemove = true
		},
	}
	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
		Logger:           testcontainers.TestLogger(t),
	})
	require.NoError(t, err)

	t.Cleanup(func() {
		container.Stop(ctx, nil)
	})

	hostPort, err := container.PortEndpoint(ctx, "1433/tcp", "")
	require.NoError(t, err)

	connStr := fmt.Sprintf("sqlserver://sa:Test87654321@%s/master", hostPort)
	t.Log(connStr)

	db := openDB(connStr, []sqlhooks.Hooks{simpleHook{}})

	_, err = db.Exec(ddlTestFunc)
	require.NoError(t, err)

	_, err = db.Exec(`EXEC test_proc`)
	require.Error(t, err, "error is: %v", err)
}

type simpleHook struct{}

func (h simpleHook) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
	return ctx, nil
}
func (h simpleHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
	return ctx, nil
}
func (h simpleHook) OnError(ctx context.Context, err error, query string, args ...interface{}) error {
	return err
}

// openDB opens new sql DB pool with an optional hooks and optional tracing.
func openDB(dsn string, hooks []sqlhooks.Hooks) *sql.DB {
	var drv driver.Driver = &gomssql.Driver{}

	if len(hooks) > 0 {
		drv = sqlhooks.Wrap(drv, sqlhooks.Compose(hooks...))
	}

	// Open DB connections pool.
	return sql.OpenDB(&connector{
		driver: drv, dsn: dsn,
	})
}

// connector is a connector with dsn and driver.
type connector struct {
	driver driver.Driver
	dsn    string
}

// Connect returns a connection to the database.
func (c *connector) Connect(_ context.Context) (driver.Conn, error) {
	return c.driver.Open(c.dsn)
}

// Driver returns the underlying Driver of the Connector.
func (c *connector) Driver() driver.Driver {
	return c.driver
}

@qustavo
Do you have any news about the panic?

sorry @tochka for the late reply. Unfortunately I haven't had the capacity nor the possibility to experiment with mssql. Have you tried to debug it?

@qustavo just test this code

package main

import (
	"fmt"
)

type testError struct {
	data []string
}

func (e testError) Error() string {
	return "test error"
}

func main() {
	var err1 error = testError{}
	var err2 error = testError{}
	fmt.Println(err1 == err2)
}

https://go.dev/play/p/SBtKap4-Yki

And more complex example
https://go.dev/play/p/HdVjUopwCRe

Will reproduce with any error containing a slice or map, passed by value, and if sqlhooks.Compose is used.

package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"github.com/qustavo/sqlhooks/v2"
)

// similar to https://github.com/microsoft/go-mssqldb/blob/main/error.go#L13
type mssqlError struct{
	All []string
}

func (m mssqlError) Error() string {
	return ""
}

// fakeHook satisfies the sqlhook.fakeHook interface
type fakeHook struct{}

func (h *fakeHook) OnError(ctx context.Context, err error, query string, args ...interface{}) error {
	return mssqlError{}
}

// Before hook will print the query with it's args and return the context with the timestamp
func (h *fakeHook) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
	return ctx, nil
}

// After hook will get the timestamp registered on the Before hook and print the elapsed time
func (h *fakeHook) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
	return ctx, nil
}

var _ driver.Connector = (*fakeConnector)(nil)
var _ sqlhooks.Hooks = (*fakeHook)(nil)
var _ sqlhooks.OnErrorer = (*fakeHook)(nil)

type fakeConnector struct {
	driver driver.Driver
}

func (f fakeConnector) Connect(ctx context.Context) (driver.Conn, error) {
	return f.driver.Open("fake")
}

func (f fakeConnector) Driver() driver.Driver {
	//TODO implement me
	panic("implement me")
}

type fakeDriver struct{}

func (f fakeDriver) Open(name string) (driver.Conn, error) {
	//TODO implement me
	return &fakeConn{}, nil
}

var _ driver.Driver = (*fakeDriver)(nil)

type fakeTx struct{}

func (f fakeTx) Commit() error {
	//TODO implement me
	panic("implement me")
}

func (f fakeTx) Rollback() error {
	//TODO implement me
	panic("implement me")
}

var _ driver.Tx = (*fakeTx)(nil)

type fakeConn struct{}

func (f fakeConn) Query(query string, args []driver.Value) (driver.Rows, error) {
	return nil, mssqlError{}
}

func (f fakeConn) Ping(ctx context.Context) error {
	//TODO implement me
	return nil
}

func (f fakeConn) Prepare(query string) (driver.Stmt, error) {
	//TODO implement me
	return fakeStmt{}, nil
}

func (f fakeConn) Close() error {
	//TODO implement me
	return nil
}

func (f fakeConn) Begin() (driver.Tx, error) {
	//TODO implement me
	panic("implement me")
}

func (f fakeConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
	//TODO implement me
	panic("implement me")
}

var _ driver.Conn = (*fakeConn)(nil)
var _ driver.ConnBeginTx = (*fakeConn)(nil)
var _ driver.Queryer = (*fakeConn)(nil)

type fakeStmt struct{}

func (f fakeStmt) Close() error {
	//TODO implement me
	return nil
}

func (f fakeStmt) NumInput() int {
	//TODO implement me
	return 0
}

func (f fakeStmt) Exec(args []driver.Value) (driver.Result, error) {
	//TODO implement me
	panic("implement me")
}

func (f fakeStmt) Query(args []driver.Value) (driver.Rows, error) {
	//TODO implement me
	return nil, mssqlError{}
}

var _ driver.Stmt = (*fakeStmt)(nil)

func main() {
	drv := sqlhooks.Wrap(fakeDriver{}, sqlhooks.Compose(&fakeHook{}))

	// Open DB connections pool.
	db := sql.OpenDB(&fakeConnector{
		driver: drv,
	})

	func() {
		defer func() {
			if recovered := recover(); recovered == nil {
				fmt.Println("sqlhooks bug is already fixed. check codebase and upgrade. original issue https://github.com/qustavo/sqlhooks/issues/56")
			} else {
				fmt.Println("panic as expected: ", recovered)
			}
		}()

		_, _ = db.Query("select 1")
	}()

	_ = db.Close()
}

Hi from Testcontainers for Go 👋

I think it could be useful to allow developers to run the integration tests while developing this project. I saw they are skipped unless the DSN env var is set. Using tc-go would help in providing the same experience running the tests on CI and locally.

Please let us know if we can help here 🙏