Skip to content

Commit

Permalink
#89 Add query timeout (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada authored Aug 3, 2023
1 parent 64b6766 commit d84792e
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 12 deletions.
7 changes: 5 additions & 2 deletions doc/changes/changes_1.0.1.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Exasol Driver go 1.0.1, released 2023-??-??
# Exasol Driver go 1.0.1, released 2023-08-03

Code name: Test with Exasol v8

## Summary

This release adds integration tests with Exasol v8. Exasol version 7.0 is deprecated and not supported any more.
This release adds support for specifying the query timeout in seconds when connecting to an Exasol database. The default timeout is 0, i.e. no timeout.

The release also adds integration tests with Exasol v8. Exasol version 7.0 is deprecated and no longer supported.

## Features

* #92: Added tests with Exasol v8
* #89: Added query timeout to DSN

## Dependency Updates

Expand Down
5 changes: 2 additions & 3 deletions driver_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package exasol

import (
"strings"
"testing"

"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -31,14 +30,14 @@ func (suite *DriverTestSuite) TestOpen() {
exasolDriver := ExasolDriver{}
_, err := exasolDriver.Open("exa:localhost:1234")
suite.Error(err)
suite.True(strings.Contains(err.Error(), "connection refused"))
suite.ErrorContains(err, "connection refused")
}

func (suite *DriverTestSuite) TestOpenBadDsn() {
exasolDriver := ExasolDriver{}
_, err := exasolDriver.Open("")
suite.Error(err)
suite.True(strings.Contains(err.Error(), "invalid connection string"))
suite.ErrorContains(err, "invalid connection string")
}

func (suite *DriverTestSuite) TestConfigToDsnWithBooleanValuesTrue() {
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
Schema string
Autocommit bool
FetchSize int // Fetch size in kB
QueryTimeout int // query timeout in seconds
Compression bool
ResultSetMaxRows int
Encryption bool
Expand Down
14 changes: 11 additions & 3 deletions itest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (suite *IntegrationTestSuite) TestExecuteWithError() {
defer database.Close()
_, err := database.Exec("CREATE SCHEMAA TEST_SCHEMA")
suite.Error(err)
suite.True(strings.Contains(err.Error(), "syntax error"))
suite.ErrorContains(err, "syntax error")
}

func (suite *IntegrationTestSuite) TestQueryWithError() {
Expand All @@ -224,7 +224,7 @@ func (suite *IntegrationTestSuite) TestQueryWithError() {
defer suite.cleanup(database, schemaName)
_, err := database.Query("SELECT x FROM " + schemaName + ".TEST_TABLE")
suite.Error(err)
suite.True(strings.Contains(err.Error(), "object TEST_SCHEMA_2.TEST_TABLE not found"))
suite.ErrorContains(err, "object TEST_SCHEMA_2.TEST_TABLE not found")
}

func (suite *IntegrationTestSuite) TestPreparedStatement() {
Expand Down Expand Up @@ -290,7 +290,7 @@ func (suite *IntegrationTestSuite) TestBeginAndRollback() {
_ = transaction.Rollback()
_, err := database.Query("SELECT x FROM " + schemaName + ".TEST_TABLE")
suite.Error(err)
suite.True(strings.Contains(err.Error(), "object "+schemaName+".TEST_TABLE not found"))
suite.ErrorContains(err, "object "+schemaName+".TEST_TABLE not found")
}

func (suite *IntegrationTestSuite) TestPingWithContext() {
Expand Down Expand Up @@ -534,6 +534,14 @@ func (suite *IntegrationTestSuite) TestClientMetadataWithDefaultClientName() {
suite.Equal(runtime.GOOS, osName)
}

func (suite *IntegrationTestSuite) TestQueryTimeoutExpired() {
database := suite.openConnection(suite.createDefaultConfig().QueryTimeout(1))
defer database.Close()
rows, err := database.Query(`SELECT "$SLEEP"(2)`)
suite.ErrorContains(err, "E-EGOD-11: execution failed with SQL error code 'R0001' and message 'Query terminated because timeout has been reached.")
suite.Nil(rows)
}

func (suite *IntegrationTestSuite) assertSingleValueResult(rows *sql.Rows, expected string) {
rows.Next()
var testValue string
Expand Down
1 change: 1 addition & 0 deletions pkg/connection/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func (c *Connection) preLogin(ctx context.Context, compression bool) (*types.Aut
Autocommit: utils.BoolToPtr(c.Config.Autocommit),
CurrentSchema: c.Config.Schema,
CompressionEnabled: utils.BoolToPtr(compression),
QueryTimeout: c.Config.QueryTimeout,
},
}
if c.Config.AccessToken != "" {
Expand Down
1 change: 1 addition & 0 deletions pkg/dsn/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func ToInternalConfig(dsnConfig *DSNConfig) *config.Config {
Schema: dsnConfig.Schema,
Autocommit: *dsnConfig.Autocommit,
FetchSize: dsnConfig.FetchSize,
QueryTimeout: dsnConfig.QueryTimeout,
Compression: *dsnConfig.Compression,
ResultSetMaxRows: dsnConfig.ResultSetMaxRows,
Encryption: *dsnConfig.Encryption,
Expand Down
60 changes: 60 additions & 0 deletions pkg/dsn/converter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dsn_test

import (
"testing"

"github.com/exasol/exasol-driver-go/internal/config"
"github.com/exasol/exasol-driver-go/pkg/dsn"
"github.com/stretchr/testify/suite"
)

type ConverterTestSuite struct {
suite.Suite
}

func TestConverterSuite(t *testing.T) {
suite.Run(t, new(ConverterTestSuite))
}

func (suite *ConverterTestSuite) TestConvertUserPassword() {
config := suite.convert("exa:localhost:1234;user=sys;password=exasol")
suite.Equal(2, config.ApiVersion)
suite.Equal("sys", config.User)
suite.Equal("exasol", config.Password)
suite.Equal("", config.AccessToken)
suite.Equal("", config.RefreshToken)
}

func (suite *ConverterTestSuite) TestConvertAccessToken() {
config := suite.convert("exa:localhost:1234;accesstoken=token")
suite.Equal(3, config.ApiVersion)
suite.Equal("token", config.AccessToken)
suite.Equal("", config.RefreshToken)
suite.Equal("", config.User)
suite.Equal("", config.Password)
}

func (suite *ConverterTestSuite) TestConvertRefreshToken() {
config := suite.convert("exa:localhost:1234;refreshtoken=token")
suite.Equal(3, config.ApiVersion)
suite.Equal("", config.AccessToken)
suite.Equal("token", config.RefreshToken)
suite.Equal("", config.User)
suite.Equal("", config.Password)
}

func (suite *ConverterTestSuite) TestConvertFetchSize() {
config := suite.convert("exa:localhost:1234;fetchsize=42")
suite.Equal(42, config.FetchSize)
}

func (suite *ConverterTestSuite) TestConvertQueryTimeout() {
config := suite.convert("exa:localhost:1234;querytimeout=42")
suite.Equal(42, config.QueryTimeout)
}

func (suite *ConverterTestSuite) convert(dsnValue string) *config.Config {
config, err := dsn.ParseDSN(dsnValue)
suite.NoError(err)
return dsn.ToInternalConfig(config)
}
17 changes: 17 additions & 0 deletions pkg/dsn/dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type DSNConfig struct {
ClientName string // Client name reported to the database (default: "Go client")
ClientVersion string // Client version reported to the database (default: "")
FetchSize int // Fetch size for results in KiB (default: 2000 KiB)
QueryTimeout int // QueryTimeout sets the query timeout in seconds. If a query runs longer than the specified time, it will be aborted (default: 0)
ValidateServerCertificate *bool // If true, validate the server's TLS certificate (default: true)
CertificateFingerprint string // Expected SHA256 checksum of the server's TLS certificate in Hex format (default: "")
Schema string // Name of the schema to open during connection (default: "")
Expand Down Expand Up @@ -77,6 +78,12 @@ func (c *DSNConfigBuilder) FetchSize(size int) *DSNConfigBuilder {
return c
}

// QueryTimeout sets the query timeout in seconds. If a query runs longer than the specified time, it will be aborted (default: 0, i.e. no timeout).
func (c *DSNConfigBuilder) QueryTimeout(timeout int) *DSNConfigBuilder {
c.Config.QueryTimeout = timeout
return c
}

// ClientName sets the client name reported to the database (default: "Go client")
func (c *DSNConfigBuilder) ClientName(name string) *DSNConfigBuilder {
c.Config.ClientName = name
Expand Down Expand Up @@ -149,6 +156,9 @@ func (c *DSNConfig) ToDSN() string {
if c.FetchSize != 0 {
sb.WriteString(fmt.Sprintf("fetchsize=%d;", c.FetchSize))
}
if c.QueryTimeout != 0 {
sb.WriteString(fmt.Sprintf("querytimeout=%d;", c.QueryTimeout))
}
if c.ClientName != "" {
sb.WriteString(fmt.Sprintf("clientname=%s;", c.ClientName))
}
Expand Down Expand Up @@ -208,6 +218,7 @@ func getDefaultConfig(host string, port int) *DSNConfig {
ClientName: "Go client",
Params: map[string]string{},
FetchSize: 2000,
QueryTimeout: 0,
}
}

Expand Down Expand Up @@ -253,6 +264,12 @@ func getConfigWithParameters(host string, port int, parametersString string) (*D
return nil, errors.NewInvalidConnectionStringInvalidIntParam("fetchsize", value)
}
config.FetchSize = fetchSizeValue
case "querytimeout":
queryTimeoutValue, err := strconv.Atoi(value)
if err != nil {
return nil, errors.NewInvalidConnectionStringInvalidIntParam("querytimeout", value)
}
config.QueryTimeout = queryTimeoutValue
case "resultsetmaxrows":
maxRowsValue, err := strconv.Atoi(value)
if err != nil {
Expand Down
40 changes: 37 additions & 3 deletions pkg/dsn/dsn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithoutParameters() {
suite.Equal("", dsn.Schema)
suite.Equal(true, *dsn.Autocommit)
suite.Equal(2000, dsn.FetchSize)
suite.Equal(0, dsn.QueryTimeout)
suite.Equal(false, *dsn.Compression)
suite.Equal(0, dsn.ResultSetMaxRows)
suite.Equal(true, *dsn.Encryption)
Expand All @@ -40,6 +41,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters() {
"autocommit=0;" +
"encryption=0;" +
"fetchsize=1000;" +
"querytimeout=10;" +
"clientname=Exasol Go client;" +
"clientversion=1.0.0;" +
"schema=MY_SCHEMA;" +
Expand All @@ -57,6 +59,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters() {
suite.Equal("MY_SCHEMA", dsn.Schema)
suite.Equal(false, *dsn.Autocommit)
suite.Equal(1000, dsn.FetchSize)
suite.Equal(10, dsn.QueryTimeout)
suite.Equal(true, *dsn.Compression)
suite.Equal(100, dsn.ResultSetMaxRows)
suite.Equal(false, *dsn.Encryption)
Expand All @@ -66,7 +69,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters() {

func (suite *DsnTestSuite) TestParseValidDsnWithParameters2() {
dsn, err := ParseDSN(
"exa:localhost:1234;user=sys;password=exasol;autocommit=1;encryption=1;compression=0")
"exa:localhost:1234;user=sys;password=exasol;autocommit=1;encryption=1;compression=0;querytimeout=42;fetchsize=17")
suite.NoError(err)
suite.Equal("sys", dsn.User)
suite.Equal("exasol", dsn.Password)
Expand All @@ -75,6 +78,8 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters2() {
suite.Equal(true, *dsn.Autocommit)
suite.Equal(true, *dsn.Encryption)
suite.Equal(false, *dsn.Compression)
suite.Equal(42, dsn.QueryTimeout)
suite.Equal(17, dsn.FetchSize)
}

func (suite *DsnTestSuite) TestParseValidDsnWithSpecialChars() {
Expand Down Expand Up @@ -126,6 +131,12 @@ func (suite *DsnTestSuite) TestInvalidFetchsize() {
suite.EqualError(err, "E-EGOD-25: invalid 'fetchsize' value 'size', numeric expected")
}

func (suite *DsnTestSuite) TestInvalidQueryTimeout() {
dsn, err := ParseDSN("exa:localhost:1234;querytimeout=timeout")
suite.Nil(dsn)
suite.EqualError(err, "E-EGOD-25: invalid 'querytimeout' value 'timeout', numeric expected")
}

func (suite *DsnTestSuite) TestInvalidValidateservercertificateUsesDefaultValue() {
dsn, err := ParseDSN("exa:localhost:1234;validateservercertificate=false")
suite.NoError(err)
Expand All @@ -138,9 +149,9 @@ func (suite *DsnTestSuite) TestInvalidResultsetmaxrows() {
suite.EqualError(err, "E-EGOD-25: invalid 'resultsetmaxrows' value 'size', numeric expected")
}

func (suite *DsnTestSuite) TestConfigToDsnCustomValues() {
func (suite *DsnTestSuite) TestConfigParseDsnCustomValues() {
dsn, err := ParseDSN(
"exa:localhost:1234;user=sys;password=exasol;autocommit=0;encryption=0;compression=1;validateservercertificate=0;certificatefingerprint=fingerprint;clientname=clientName;clientversion=clientVersion")
"exa:localhost:1234;user=sys;password=exasol;autocommit=0;encryption=0;compression=1;validateservercertificate=0;certificatefingerprint=fingerprint;fetchsize=13;querytimeout=42;clientname=clientName;clientversion=clientVersion")
suite.NoError(err)
suite.Equal("sys", dsn.User)
suite.Equal("exasol", dsn.Password)
Expand All @@ -151,10 +162,33 @@ func (suite *DsnTestSuite) TestConfigToDsnCustomValues() {
suite.Equal(true, *dsn.Compression)
suite.Equal(false, *dsn.ValidateServerCertificate)
suite.Equal("fingerprint", dsn.CertificateFingerprint)
suite.Equal(13, dsn.FetchSize)
suite.Equal(42, dsn.QueryTimeout)
suite.Equal("clientName", dsn.ClientName)
suite.Equal("clientVersion", dsn.ClientVersion)
}

func (suite *DsnTestSuite) TestToDsnWithUserPassword() {
const value = "exa:localhost:1234;user=sys;password=exasol;autocommit=0;compression=1;encryption=0;validateservercertificate=0;certificatefingerprint=fingerprint;fetchsize=13;querytimeout=42;clientname=clientName;clientversion=clientVersion;schema=schema"
dsn, err := ParseDSN(value)
suite.NoError(err)
suite.Equal(value, dsn.ToDSN())
}

func (suite *DsnTestSuite) TestToDsnWithAccessToken() {
const value = "exa:localhost:1234;accesstoken=token;autocommit=1;compression=0;encryption=1;validateservercertificate=1;fetchsize=2000;clientname=Go client"
dsn, err := ParseDSN(value)
suite.NoError(err)
suite.Equal(value, dsn.ToDSN())
}

func (suite *DsnTestSuite) TestToDsnWithRefreshToken() {
const value = "exa:localhost:1234;refreshtoken=token;autocommit=1;compression=0;encryption=1;validateservercertificate=1;fetchsize=2000;clientname=Go client"
dsn, err := ParseDSN(value)
suite.NoError(err)
suite.Equal(value, dsn.ToDSN())
}

func (suite *DsnTestSuite) TestParseValidDsnWithAccessToken() {
dsn, err := ParseDSN(
`exa:localhost:1234;accesstoken=TOKEN.JWT.TEST;autocommit=1;encryption=1;compression=0`)
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ sonar.exclusions=*_test.go,examples/**
sonar.tests=.
sonar.test.inclusions=*_test.go
sonar.go.coverage.reportPaths=./coverage.out
sonar.coverage.exclusions=itest/integration_test.go
sonar.coverage.exclusions=itest/integration_test.go,**/*_test.go

0 comments on commit d84792e

Please sign in to comment.