Skip to content

Commit

Permalink
#108: Convert args for prepared statements to expected type (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada authored Jun 19, 2024
1 parent 8d33585 commit 22ad9c3
Show file tree
Hide file tree
Showing 21 changed files with 534 additions and 32 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
strategy:
matrix:
go: ["1.21", "1.22"]
db: ["7.1.25", "8.24.0"]
db: ["7.1.26", "8.27.0"]
env:
DEFAULT_GO: "1.22"
DEFAULT_DB: "8.24.0"
DEFAULT_DB: "8.27.0"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-go-${{ matrix.go }}-db-${{ matrix.db }}
cancel-in-progress: true
Expand Down Expand Up @@ -65,6 +65,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

- name: Run vulnerability check
if: matrix.go == env.DEFAULT_GO && matrix.db == env.DEFAULT_DB
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck -version
govulncheck -format text -mode source -scan symbol -show verbose -test ./...
build:
needs: matrix-build
runs-on: ubuntu-latest
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/dependencies_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Report Security Issues
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * *"

jobs:
report_security_issues:
runs-on: ubuntu-latest
defaults:
run:
shell: "bash"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-report_security_issues
cancel-in-progress: true
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4

- name: Set up Go
id: go
uses: actions/setup-go@v5
with:
go-version: "1.22"
cache: true

- name: Install vulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest

- name: Print vulncheck version
run: govulncheck -version

- name: Run vulnerability check
run: govulncheck -format text -mode source -scan symbol -show verbose -test ./...
5 changes: 3 additions & 2 deletions .project-keeper.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sources:
- type: golang
path: go.mod
version: 1.0.7
version: 1.0.8
excludes:
- "E-PK-CORE-26: 'release_config.yml' exists but must not exist. Reason: Release-droid configuration is replaced by release.yml"
# Releases are done with Release Droid because PK does not yet support release process for Go projects.
- "E-PK-CORE-26: 'release_config.yml' exists but must not exist. Reason: Release-droid configuration is replaced by release.yml"
38 changes: 21 additions & 17 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.generate.finalModifiers": "explicit",
"source.fixAll": "explicit"
},
"go.testTimeout": "240s",
"go.testFlags": ["-p", "1", "-v"],
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
"sonarlint.connectedMode.project": {
"connectionId": "exasol",
"projectKey": "com.exasol:exasol-driver-go"
}
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.generate.finalModifiers": "explicit",
"source.fixAll": "explicit"
},
"go.testTimeout": "240s",
"go.testFlags": [
"-p",
"1",
"-v"
],
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
"sonarlint.connectedMode.project": {
"connectionId": "exasol",
"projectKey": "com.exasol:exasol-driver-go"
}
}
6 changes: 3 additions & 3 deletions dependencies.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/changes/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions doc/changes/changes_1.0.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Exasol Driver go 1.0.8, released 2024-06-19

Code name: Fix inserting double values

## Summary

This release fixes an issue inserting double values with a prepared statement.

Details of the fix:
The driver serializes commands to the database as JSON. When inserting values into a `DOUBLE` column, the database expects the JSON to contain numbers with a decimal point, e.g. `42.0`. When using values like `42` or `42.0` in `stmt.Exec()`, the driver omitted the decimal point. This caused the query to fail with error

```
E-EGOD-11: execution failed with SQL error code '00000' and message 'getDouble: JSON value is not a double
```

## Bugfixes

* #108: Fixed inserting double values with a prepared statement

## Dependency Updates

### Compile Dependency Updates

* Updated `github.com/gorilla/websocket:v1.5.1` to `v1.5.3`
* Updated `github.com/exasol/exasol-test-setup-abstraction-server/go-client:v0.3.6` to `v0.3.9`
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ go 1.21

require (
github.com/exasol/error-reporting-go v0.2.0
github.com/exasol/exasol-test-setup-abstraction-server/go-client v0.3.6
github.com/gorilla/websocket v1.5.1
github.com/exasol/exasol-test-setup-abstraction-server/go-client v0.3.9
github.com/gorilla/websocket v1.5.3
github.com/stretchr/testify v1.9.0
go.uber.org/goleak v1.3.0
golang.org/x/sync v0.7.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package version

const DriverVersion = "v1.0.7"
const DriverVersion = "v1.0.8"
173 changes: 173 additions & 0 deletions itest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,179 @@ func (suite *IntegrationTestSuite) TestPreparedStatement() {
suite.assertSingleValueResult(rows, "15")
}

var dereferenceString = func(v any) any { return *(v.(*string)) }
var dereferenceFloat64 = func(v any) any { return *(v.(*float64)) }
var dereferenceInt64 = func(v any) any { return *(v.(*int64)) }
var dereferenceInt = func(v any) any { return *(v.(*int)) }
var dereferenceBool = func(v any) any { return *(v.(*bool)) }

func (suite *IntegrationTestSuite) TestQueryDataTypesCast() {
for i, testCase := range []struct {
testDescription string
sqlValue string
sqlType string
scanDest any
expectedValue any
dereference func(any) any
}{
{"decimal to int64", "1", "DECIMAL(18,0)", new(int64), int64(1), dereferenceInt64},
{"decimal to int", "1", "DECIMAL(18,0)", new(int), 1, dereferenceInt},
{"decimal to float", "1", "DECIMAL(18,0)", new(float64), 1.0, dereferenceFloat64},
{"decimal to string", "1", "DECIMAL(18,0)", new(string), "1", dereferenceString},
{"decimal to float64", "2.2", "DECIMAL(18,2)", new(float64), 2.2, dereferenceFloat64},
{"decimal to string", "2.2", "DECIMAL(18,2)", new(string), "2.2", dereferenceString},
{"double to float64", "3.3", "DOUBLE PRECISION", new(float64), 3.3, dereferenceFloat64},
{"double to string", "3.3", "DOUBLE PRECISION", new(string), "3.3", dereferenceString},
{"varchar to string", "'text'", "VARCHAR(10)", new(string), "text", dereferenceString},
{"char to string", "'text'", "CHAR(10)", new(string), "text ", dereferenceString},
{"date to string", "'2024-06-18'", "DATE", new(string), "2024-06-18", dereferenceString},
{"timestamp to string", "'2024-06-18 17:22:13.123456'", "TIMESTAMP", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{"timestamp with local time zone to string", "'2024-06-18 17:22:13.123456'", "TIMESTAMP WITH LOCAL TIME ZONE", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{"geometry to string", "'point(1 2)'", "GEOMETRY", new(string), "POINT (1 2)", dereferenceString},
{"interval ytm to string", "'5-3'", "INTERVAL YEAR TO MONTH", new(string), "+05-03", dereferenceString},
{"interval dts to string", "'2 12:50:10.123'", "INTERVAL DAY TO SECOND", new(string), "+02 12:50:10.123", dereferenceString},
{"hashtype to string", "'550e8400-e29b-11d4-a716-446655440000'", "HASHTYPE", new(string), "550e8400e29b11d4a716446655440000", dereferenceString},
{"bool to bool", "true", "BOOLEAN", new(bool), true, dereferenceBool},
{"bool to string", "false", "BOOLEAN", new(string), "false", dereferenceString},
} {
database := suite.openConnection(suite.createDefaultConfig())
defer database.Close()
suite.Run(fmt.Sprintf("Cast Test %02d %s: %s", i, testCase.testDescription, testCase.sqlType), func() {
rows, err := database.Query(fmt.Sprintf("SELECT CAST(%s AS %s)", testCase.sqlValue, testCase.sqlType))
onError(err)
defer rows.Close()
suite.True(rows.Next(), "should have one row")
onError(rows.Scan(testCase.scanDest))
val := testCase.scanDest
suite.Equal(testCase.expectedValue, testCase.dereference(val))
})
}
}

func (suite *IntegrationTestSuite) TestPreparedStatementArgsConverted() {
for i, testCase := range []struct {
sqlValue any
sqlType string
scanDest any
expectedValue any
dereference func(any) any
}{
{1, "DECIMAL(18,0)", new(int64), int64(1), dereferenceInt64},
{1.1, "DECIMAL(18,0)", new(int64), int64(1), dereferenceInt64},
{1, "DECIMAL(18,0)", new(int), 1, dereferenceInt},
{1, "DECIMAL(18,0)", new(float64), 1.0, dereferenceFloat64},
{2.2, "DECIMAL(18,2)", new(float64), 2.2, dereferenceFloat64},
{2, "DECIMAL(18,2)", new(float64), 2.0, dereferenceFloat64},
{3.3, "DOUBLE PRECISION", new(float64), 3.3, dereferenceFloat64},
{3, "DOUBLE PRECISION", new(float64), 3.0, dereferenceFloat64},
{"text", "VARCHAR(10)", new(string), "text", dereferenceString},
{"text", "CHAR(10)", new(string), "text ", dereferenceString},
{"2024-06-18", "DATE", new(string), "2024-06-18", dereferenceString},
{time.Date(2024, time.June, 18, 0, 0, 0, 0, time.UTC), "DATE", new(string), "2024-06-18", dereferenceString},
{"2024-06-18 17:22:13.123456", "TIMESTAMP", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{time.Date(2024, time.June, 18, 17, 22, 13, 123456789, time.UTC), "TIMESTAMP", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{"2024-06-18 17:22:13.123456", "TIMESTAMP WITH LOCAL TIME ZONE", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{time.Date(2024, time.June, 18, 17, 22, 13, 123456789, time.UTC), "TIMESTAMP WITH LOCAL TIME ZONE", new(string), "2024-06-18 17:22:13.123000", dereferenceString},
{"point(1 2)", "GEOMETRY", new(string), "POINT (1 2)", dereferenceString},
{"5-3", "INTERVAL YEAR TO MONTH", new(string), "+05-03", dereferenceString},
{"2 12:50:10.123", "INTERVAL DAY TO SECOND", new(string), "+02 12:50:10.123", dereferenceString},
{"550e8400-e29b-11d4-a716-446655440000", "HASHTYPE", new(string), "550e8400e29b11d4a716446655440000", dereferenceString},
{true, "BOOLEAN", new(bool), true, dereferenceBool},
} {
database := suite.openConnection(suite.createDefaultConfig().Autocommit(false))
schemaName := "DATATYPE_TEST"
_, err := database.Exec("CREATE SCHEMA " + schemaName)
onError(err)
defer suite.cleanup(database, schemaName)

suite.Run(fmt.Sprintf("%02d Column type %s accepts values of type %T", i, testCase.sqlType, testCase.sqlValue), func() {
tableName := fmt.Sprintf("%s.TAB_%d", schemaName, i)
_, err = database.Exec(fmt.Sprintf("CREATE TABLE %s (col %s)", tableName, testCase.sqlType))
onError(err)
stmt, err := database.Prepare(fmt.Sprintf("insert into %s values (?)", tableName))
onError(err)
_, err = stmt.Exec(testCase.sqlValue)
onError(err)
rows, err := database.Query(fmt.Sprintf("select * from %s", tableName))
onError(err)
defer rows.Close()
suite.True(rows.Next(), "should have one row")
onError(rows.Scan(testCase.scanDest))
val := testCase.scanDest
suite.Equal(testCase.expectedValue, testCase.dereference(val))
})
}
}

func (suite *IntegrationTestSuite) TestPreparedStatementArgsConversionFails() {
database := suite.openConnection(suite.createDefaultConfig().Autocommit(false))
schemaName := "DATATYPE_TEST"
_, err := database.Exec("CREATE SCHEMA " + schemaName)
onError(err)
defer suite.cleanup(database, schemaName)

tableName := fmt.Sprintf("%s.TAB", schemaName)
_, err = database.Exec(fmt.Sprintf("CREATE TABLE %s (col TIMESTAMP)", tableName))
onError(err)
stmt, err := database.Prepare(fmt.Sprintf("insert into %s values (?)", tableName))
onError(err)
_, err = stmt.Exec(true)
suite.EqualError(err, "E-EGOD-30: cannot convert argument 'true' of type 'bool' to 'TIMESTAMP' type")
}

func (suite *IntegrationTestSuite) TestScanTypeUnsupported() {
for i, testCase := range []struct {
testDescription string
sqlValue any
sqlType string
scanDest any
expectedError string
}{
{"timestamp", time.Date(2024, time.June, 18, 17, 22, 13, 123456789, time.UTC), "TIMESTAMP", new(time.Time), `sql: Scan error on column index 0, name "COL": unsupported Scan, storing driver.Value type string into type *time.Time`},
{"timestamp with local time zone", time.Date(2024, time.June, 18, 17, 22, 13, 123456789, time.UTC), "TIMESTAMP WITH LOCAL TIME ZONE", new(time.Time), `sql: Scan error on column index 0, name "COL": unsupported Scan, storing driver.Value type string into type *time.Time`},
} {
database := suite.openConnection(suite.createDefaultConfig().Autocommit(false))
schemaName := "DATATYPE_TEST"
_, err := database.Exec("CREATE SCHEMA " + schemaName)
onError(err)
defer suite.cleanup(database, schemaName)

suite.Run(fmt.Sprintf("Scan fails %02d %s: %s", i, testCase.testDescription, testCase.sqlType), func() {
tableName := fmt.Sprintf("%s.TAB_%d", schemaName, i)
_, err = database.Exec(fmt.Sprintf("CREATE TABLE %s (col %s)", tableName, testCase.sqlType))
onError(err)
stmt, err := database.Prepare(fmt.Sprintf("insert into %s values (?)", tableName))
onError(err)
_, err = stmt.Exec(testCase.sqlValue)
onError(err)
rows, err := database.Query(fmt.Sprintf("select * from %s", tableName))
onError(err)
defer rows.Close()
suite.True(rows.Next(), "should have one row")
err = rows.Scan(testCase.scanDest)
suite.EqualError(err, testCase.expectedError)
})
}
}

// https://github.com/exasol/exasol-driver-go/issues/108
func (suite *IntegrationTestSuite) TestPreparedStatementIntConvertedToFloat() {
database := suite.openConnection(suite.createDefaultConfig())
schemaName := "TEST_SCHEMA_3"
_, err := database.Exec("CREATE SCHEMA " + schemaName)
onError(err)
_, err = database.Exec(fmt.Sprintf("create or replace table %s.dummy (a integer, b float)", schemaName))
onError(err)
defer suite.cleanup(database, schemaName)
stmt, err := database.Prepare(fmt.Sprintf("insert into %s.dummy values(?,?)", schemaName))
onError(err)
_, err = stmt.Exec(1, 2)
onError(err)
rows, err := database.Query(fmt.Sprintf("select a || ':' || b from %s.dummy", schemaName))
onError(err)
suite.assertSingleValueResult(rows, "1:2")
}

func (suite *IntegrationTestSuite) TestQueryWithValuesAndContext() {
database := suite.openConnection(suite.createDefaultConfig())
schemaName := "TEST_SCHEMA_3_2"
Expand Down
Loading

0 comments on commit 22ad9c3

Please sign in to comment.