luna-duclos / instrumentedsql

A sql driver that will wrap any other driver and log/trace all its calls
MIT License
173 stars 44 forks source link

instrumentedsql breaks go-oci8 v0.0.9 and above #41

Open nvx opened 3 years ago

nvx commented 3 years ago

The following code works with v0.0.8 of go-oci8, but breaks under v0.0.9 of go-oci8. The commit that triggers the issue is https://github.com/mattn/go-oci8/commit/4a4497791f93ed91967ff2feb680aec865e804da

stmt.NumInput() returning -1 is valid for a driver to return.

The issue stems that NumInput returning -1 is fine when driver.NamedValueChecker is used, but not fine when driver.ColumnConverter is used. Normally when both are implemented, only the NamedValueChecker is used, but when the driver NamedValueChecker returns driver.ErrSkip and the driver also implements the ColumnConverter interface, database/sql will use a wrapper (database/sql.ccChecker) to expose the ColumnConverter as if it were a NamedValueChecker. The problem arises that when NumInput returns -1 this causes the wrapper to return nil (without calling the drivers ColumnConverter) and no further functions are called. By comparison if the NamedValueChecker returns driver.ErrSkip and ColumnConverter is not implemented this causes database/sql.defaultCheckNamedValue to be called which performs a required conversion step.

Example application that exhibits the issue, runs fine with go-oci8 v0.0.8 (where NumInput() returns >= 0), fails on v0.0.9 or newer (where NumInput() returns -1). Swapping the comment on the wrappedDriver := line allows the code to work even on v0.0.9 of go-oci8 driver.

Additionally commenting out the wrappedStmt.ColumnConverter func in instrumentedsql also allows the code to run without issue.

package main

import (
    "context"
    "database/sql"
    "database/sql/driver"
    "flag"
    "fmt"
    "github.com/luna-duclos/instrumentedsql"
    "github.com/mattn/go-oci8"
)

type dsnConnector struct {
    driver driver.Driver
    dsn    string
}

func (t dsnConnector) Connect(_ context.Context) (driver.Conn, error) {
    return t.driver.Open(t.dsn)
}

func (t dsnConnector) Driver() driver.Driver {
    return t.driver
}

func main() {
    var (
        dbDSN = flag.String("db-dsn", "", "DB DSN")
    )
    flag.Parse()

    // switching the following two lines results in the code working
    wrappedDriver := instrumentedsql.WrapDriver(oci8.Driver)
    //wrappedDriver := oci8.Driver
    db := sql.OpenDB(dsnConnector{driver: wrappedDriver, dsn: *dbDSN})

    var err error
    var out int64

    err = db.QueryRow( `SELECT 1 FROM DUAL`).Scan(&out)
    if err != nil {
        fmt.Println("Error 0:", err) // This case doesn't error
    } else {
        fmt.Println("No error 0:", out)
    }

    err = db.QueryRow( `SELECT 1 FROM DUAL WHERE :1=0`, 0).Scan(&out)
    if err != nil {
        fmt.Println("Error 1:", err) // binary read for column 0 - error: binary.Write: invalid type int
    } else {
        fmt.Println("No error 1:", out)
    }

    err = db.QueryRow( `SELECT 1 FROM DUAL WHERE 1 >= :1`, sql.NullInt64{Int64: 1, Valid: true}).Scan(&out)
    if err != nil {
        fmt.Println("Error 2:", err) // ORA-01722: invalid number
    } else {
        fmt.Println("No error 2:", out)
    }

    err = db.QueryRow( `SELECT 1 FROM DUAL WHERE 1 >= :1`, sql.NullFloat64{Float64: 1, Valid: true}).Scan(&out)
    if err != nil {
        fmt.Println("Error 3:", err) // ORA-01722: invalid number
    } else {
        fmt.Println("No error 3:", out)
    }
}

pseudocode of logic differences when using go-oci8 vs wrapped in instrumentedsql:

oci8: implements NamedValueChecker but not ColumnConverter
    database/sql.driverArgsConnLocked logic
        calls NamedValueChecker
            NamedValueChecker returns driver.ErrSkip
        calls database/sql.defaultCheckNamedValue
            calls driver.DefaultParameterConverter.ConvertValue(nv.Value)

instrumentedsql wrapping oci8: implements NamedValueChecker and ColumnConverter
    database/sql.driverArgsConnLocked logic
        calls NamedValueChecker
            calls oci8.NamedValueChecker
                oci8.NamedValueChecker returns driver.ErrSkip
        calls database/sql.ccChecker ColumnConverter wrapper wrapping instrumentedsql.ColumnConverter
            // c.want is -1 in oci8 v0.0.9 and newer
            if c.want <= index {
                // ColumnConverter only accepts an index, so the wrapper bails out here returning nil
                return nil
            }
            // note that instrumentedsql.ColumnConverter is never called, just the existence of this func is enough to cause the issue
        since nil was returned not driver.ErrSkip, database/sql.defaultCheckNamedValue is not called -> this causes an error

A fix would be to only implement NamedValueChecker and not ColumnConverter on go1.9+ to avoid the side effect of ColumnConverter existing.

If a driver only implements ColumnConverter and not NamedValueChecker on go1.19+, or if a driver implements both and NamedValueChecker returns driver.ErrSkip, instrumentedsql can perform wrapping of ColumnConverter as a NamedValueChecker (a copy paste of database/sql.ccChecker would work, unfortunately it's not public to be able to use it as-is). The important distinction vs what happens now is that the wrapper must only be used if the underlying driver implements ColumnConverter. I suspect that NumInput returning -1 when ColumnConverter is implemented is not a valid condition.

nvx commented 3 years ago

I've implemented the fix mentioned here in PR #42. The fix will make instrumentedsql behave more transparently (in this instance it will replicate the behaviour as if it is not being used) - which for a tracing library makes sense that it shouldn't affect behaviour.

There is another question which is if https://github.com/golang/go/blob/0efbd1015774a2d894138519f1efcf7704bb2d95/src/database/sql/convert.go#L56-L58 returning nil at that point is actually the right option, or if calling defaultCheckNamedValue at that point would make more sense. Of course that's out of scope of this repo and there may be some compatibility issues with the older interface to consider that requires it be handled that way, but something to think about. That said even if the behaviour of database/sql is changed, I still think it makes sense to use the fix I implemented (although the ccChecker should be updated to reflect any such changes) as it makes the way database/sql interacts with the wrapped driver more closely aligned to how database/sql would interact with the unwrapped driver.

nvx commented 3 years ago

I just noticed this is still open. I've been running the merged version from PR #42 for a while now without issues. Is it worth cutting a point release and marking this closed

luna-duclos commented 3 years ago

Yes! I will do so next week when I get to the office

fho commented 3 years ago

Is this issue fixed? The PR was merged a while ago.

fho commented 3 years ago

I ran into a similar issue with the pq driver.

According to https://github.com/golang/go/blob/f865b5fefeafaf76ce6ff99e44d41c52cb6b884b/src/database/sql/driver/driver.go#L275:

// The sql package checks for value checkers in the following order, // stopping at the first found match: Stmt.NamedValueChecker, Conn.NamedValueChecker, // Stmt.ColumnConverter, DefaultParameterConverter.

instrumentedsql in master currently does not call the methods in the descripted order. Conn.NamedValueChecker and DefaultParameterConverter are not explicitly called. Are they called implicitly in somewhere or is that missing in instrumentedsql?

I also discovered https://github.com/ngrok/sqlmw/pull/3. It fixes a similar issue. There Conn.NamedValueChecker and DefaultParameterConverter are called but Stmt.ColumnConverter isn't

nvx commented 3 years ago

Is this issue fixed? The PR was merged a while ago.

It is, but the latest tagged version (v1.1.3) does not include the fix. If you point to the current commit at the tip of the master branch it works fine though.

fho commented 3 years ago

@nvx thanks, do you maybe know if https://github.com/luna-duclos/instrumentedsql/issues/41#issuecomment-883247352 is an issue?

nvx commented 3 years ago

@nvx thanks, do you maybe know if #41 (comment) is an issue?

I wouldn't be surprised if v1.1.3 isn't working, have you tried updating to the current master commit and retesting? I suspect that'll fix it.

fho commented 3 years ago

@nvx Yes, the particular issue that I had related to lib/pq + value conversion is fixed in master.

It looks to me that the current implementation from your PR (https://github.com/luna-duclos/instrumentedsql/issues/41) does not implement the value checker order as described in the stdlib driver package (https://github.com/golang/go/blob/f865b5fefeafaf76ce6ff99e44d41c52cb6b884b/src/database/sql/driver/driver.go#L275) though. I'm wondering if that is really the case or If I'm misunderstand something.

If it does not follow the same value checker order in the stdlib, it might cause issues with driver that I'm currently missing in my test scenario.

nvx commented 3 years ago

It was a copy paste of the stdlib implementation at the time. That said stdlib might have changed since then and there's probably value in keeping it consistent.