From 36c82004cdfff01b7521edb067e31ed96aba25b1 Mon Sep 17 00:00:00 2001 From: Sebastian Beisch Date: Wed, 23 Jun 2021 19:31:52 +0200 Subject: [PATCH] feat(#6): add dsn builder --- README.md | 20 +++++++ connection.go | 6 +-- connector.go | 2 +- driver.go | 9 ++-- driver_test.go | 3 +- dsn.go | 127 ++++++++++++++++++++++++++++++++++++++------ dsn_test.go | 42 +++++++++++---- helper.go | 7 +++ integration_test.go | 47 ++++++++-------- websocket.go | 9 +++- 10 files changed, 214 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index d2a3078..932f72e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ This library uses the standard Golang [SQL driver interface](https://golang.org/ ### Create Connection +#### with exasol dsn + ```go package main @@ -23,6 +25,23 @@ func main() { } ``` +#### with exasol config + +```go +package main + +import ( + "database/sql" + + "github.com/exasol/exasol-driver-go" +) + +func main() { + db, err := sql.Open("exasol", exasol.NewConfig("username", "password").Port(8563).String()) + ... +} +``` + ### Execute Statement ```go @@ -98,6 +117,7 @@ Limitations: Only single ips or dns is supported | clientversion | string | | Tell the server the version of the application. | | compression | 0=off, 1=on | 0 | Switch data compression on or off. | | encryption | 0=off, 1=on | 1 | Switch automatic encryption on or off. | +| insecure | 0=off, 1=on | 0 | Disable TLS/SSL verification. Use if you want to use a self-signed or invalid certificate (server side) | | fetchsize | numeric, >0 | 128*1024 | Amount of data in kB which should be obtained by Exasol during a fetch. The JVM can run out of memory if the value is too high. | | password | string | | Exasol password. | | resultsetmaxrows | numeric | | Set the max amount of rows in the result set. | diff --git a/connection.go b/connection.go index 13c8793..a508c40 100644 --- a/connection.go +++ b/connection.go @@ -17,12 +17,8 @@ import ( "github.com/gorilla/websocket" ) -var ( - defaultDialer = *websocket.DefaultDialer -) - type connection struct { - config *Config + config *config websocket *websocket.Conn ctx context.Context isClosed bool diff --git a/connector.go b/connector.go index b3c6407..4ae471f 100644 --- a/connector.go +++ b/connector.go @@ -6,7 +6,7 @@ import ( ) type connector struct { - config *Config + config *config } func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { diff --git a/driver.go b/driver.go index 4b753f2..60ed9de 100644 --- a/driver.go +++ b/driver.go @@ -9,11 +9,11 @@ import ( type ExasolDriver struct{} -type Config struct { +type config struct { User string Password string Host string - Port string + Port int Params map[string]string // Connection parameters ApiVersion int ClientName string @@ -25,6 +25,7 @@ type Config struct { ResultSetMaxRows int Timeout time.Time Encryption bool + Insecure bool } func init() { @@ -32,7 +33,7 @@ func init() { } func (e ExasolDriver) Open(dsn string) (driver.Conn, error) { - config, err := ParseDSN(dsn) + config, err := parseDSN(dsn) if err != nil { return nil, err } @@ -43,7 +44,7 @@ func (e ExasolDriver) Open(dsn string) (driver.Conn, error) { } func (e ExasolDriver) OpenConnector(dsn string) (driver.Connector, error) { - config, err := ParseDSN(dsn) + config, err := parseDSN(dsn) if err != nil { return nil, err } diff --git a/driver_test.go b/driver_test.go index 6a3e06c..7c9f401 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1,9 +1,10 @@ package exasol import ( - "github.com/stretchr/testify/suite" "strings" "testing" + + "github.com/stretchr/testify/suite" ) type DriverTestSuite struct { diff --git a/dsn.go b/dsn.go index 45b46fa..7790624 100644 --- a/dsn.go +++ b/dsn.go @@ -8,22 +8,108 @@ import ( "strings" ) -func ParseDSN(dsn string) (*Config, error) { +type DSNConfig struct { + host string + port int + user string + password string + autocommit *bool + encryption *bool + compression *bool + clientName string + clientVersion string + fetchSize int + insecure *bool +} + +func NewConfig(user, password string) *DSNConfig { + return &DSNConfig{ + host: "localhost", + port: 8563, + user: user, + password: password, + } +} + +func (c *DSNConfig) Compression(enabled bool) *DSNConfig { + c.compression = &enabled + return c +} +func (c *DSNConfig) Encryption(enabled bool) *DSNConfig { + c.encryption = &enabled + return c +} +func (c *DSNConfig) Autocommit(enabled bool) *DSNConfig { + c.autocommit = &enabled + return c +} +func (c *DSNConfig) Insecure(enabled bool) *DSNConfig { + c.insecure = &enabled + return c +} +func (c *DSNConfig) FetchSize(size int) *DSNConfig { + c.fetchSize = size + return c +} +func (c *DSNConfig) ClientName(name string) *DSNConfig { + c.clientName = name + return c +} +func (c *DSNConfig) ClientVersion(version string) *DSNConfig { + c.clientVersion = version + return c +} +func (c *DSNConfig) Host(host string) *DSNConfig { + c.host = host + return c +} +func (c *DSNConfig) Port(port int) *DSNConfig { + c.port = port + return c +} + +func (c *DSNConfig) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("exa:%s:%d;user=%s;password=%s;", c.host, c.port, c.user, c.password)) + if c.autocommit != nil { + sb.WriteString(fmt.Sprintf("autocommit=%d;", boolToInt(*c.autocommit))) + } + if c.compression != nil { + sb.WriteString(fmt.Sprintf("compression=%d;", boolToInt(*c.compression))) + } + if c.encryption != nil { + sb.WriteString(fmt.Sprintf("encryption=%d;", boolToInt(*c.encryption))) + } + if c.insecure != nil { + sb.WriteString(fmt.Sprintf("insecure=%d;", boolToInt(*c.insecure))) + } + if c.fetchSize != 0 { + sb.WriteString(fmt.Sprintf("fetchsize=%d;", c.fetchSize)) + } + if c.clientName != "" { + sb.WriteString(fmt.Sprintf("clientname=%s;", c.clientName)) + } + if c.clientVersion != "" { + sb.WriteString(fmt.Sprintf("clientversion=%s;", c.clientVersion)) + } + return strings.TrimRight(sb.String(), ";") +} + +func parseDSN(dsn string) (*config, error) { if !strings.HasPrefix(dsn, "exa:") { return nil, fmt.Errorf("invalid connection string, must start with 'exa:'") } splitDsn := splitIntoConnectionStringAndParameters(dsn) - hostPort := extractHostAndPort(splitDsn[0]) - - if len(hostPort) != 2 { - return nil, fmt.Errorf("invalid host or port, expected format: :") + host, port, err := extractHostAndPort(splitDsn[0]) + if err != nil { + return nil, err } if len(splitDsn) < 2 { - return getBasicConfig(hostPort), nil + return getDefaultConfig(host, port), nil } else { - return getConfigWithParameters(hostPort, splitDsn[1]) + return getConfigWithParameters(host, port, splitDsn[1]) } } @@ -32,26 +118,35 @@ func splitIntoConnectionStringAndParameters(dsn string) []string { return strings.SplitN(cleanDsn, ";", 2) } -func extractHostAndPort(connectionString string) []string { - return strings.Split(connectionString, ":") +func extractHostAndPort(connectionString string) (string, int, error) { + hostPort := strings.Split(connectionString, ":") + if len(hostPort) != 2 { + return "", 0, fmt.Errorf("invalid host or port, expected format: :") + } + port, err := strconv.Atoi(hostPort[1]) + if err != nil { + return "", 0, fmt.Errorf("invalid `port` value, numeric port expected") + } + return hostPort[0], port, nil } -func getBasicConfig(hostPort []string) *Config { - return &Config{ - Host: hostPort[0], - Port: hostPort[1], +func getDefaultConfig(host string, port int) *config { + return &config{ + Host: host, + Port: port, ApiVersion: 2, Autocommit: true, Encryption: true, Compression: false, + Insecure: false, ClientName: "Go client", Params: map[string]string{}, FetchSize: 128 * 1024, } } -func getConfigWithParameters(hostPort []string, parametersString string) (*Config, error) { - config := getBasicConfig(hostPort) +func getConfigWithParameters(host string, port int, parametersString string) (*config, error) { + config := getDefaultConfig(host, port) parameters := extractParameters(parametersString) for _, parameter := range parameters { parameter = strings.TrimRight(parameter, ";") @@ -71,6 +166,8 @@ func getConfigWithParameters(hostPort []string, parametersString string) (*Confi config.Autocommit = value == "1" case "encryption": config.Encryption = value == "1" + case "insecure": + config.Insecure = value == "1" case "compression": config.Compression = value == "1" case "clientname": diff --git a/dsn_test.go b/dsn_test.go index a5160b3..1db7cc3 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -1,9 +1,10 @@ package exasol import ( - "github.com/stretchr/testify/suite" "testing" "time" + + "github.com/stretchr/testify/suite" ) type DsnTestSuite struct { @@ -15,7 +16,7 @@ func TestDsnSuite(t *testing.T) { } func (suite *DsnTestSuite) TestParseValidDsnWithoutParameters() { - dsn, err := ParseDSN("exa:localhost:1234") + dsn, err := parseDSN("exa:localhost:1234") suite.NoError(err) suite.Equal(dsn.User, "") suite.Equal(dsn.Password, "") @@ -35,7 +36,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithoutParameters() { } func (suite *DsnTestSuite) TestParseValidDsnWithParameters() { - dsn, err := ParseDSN( + dsn, err := parseDSN( "exa:localhost:1234;user=sys;password=exasol;" + "autocommit=0;" + "encryption=0;" + @@ -64,7 +65,7 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters() { } func (suite *DsnTestSuite) TestParseValidDsnWithParameters2() { - dsn, err := ParseDSN( + dsn, err := parseDSN( "exa:localhost:1234;user=sys;password=exasol;autocommit=1;encryption=1;compression=0") suite.NoError(err) suite.Equal(dsn.User, "sys") @@ -77,31 +78,54 @@ func (suite *DsnTestSuite) TestParseValidDsnWithParameters2() { } func (suite *DsnTestSuite) TestInvalidPrefix() { - dsn, err := ParseDSN("exaa:localhost:1234") + dsn, err := parseDSN("exaa:localhost:1234") suite.Nil(dsn) suite.EqualError(err, "invalid connection string, must start with 'exa:'") } func (suite *DsnTestSuite) TestInvalidHostPortFormat() { - dsn, err := ParseDSN("exa:localhost") + dsn, err := parseDSN("exa:localhost") suite.Nil(dsn) suite.EqualError(err, "invalid host or port, expected format: :") } func (suite *DsnTestSuite) TestInvalidParameter() { - dsn, err := ParseDSN("exa:localhost:1234;user") + dsn, err := parseDSN("exa:localhost:1234;user") suite.Nil(dsn) suite.EqualError(err, "invalid parameter user, expected format =") } func (suite *DsnTestSuite) TestInvalidFetchsize() { - dsn, err := ParseDSN("exa:localhost:1234;fetchsize=size") + dsn, err := parseDSN("exa:localhost:1234;fetchsize=size") suite.Nil(dsn) suite.EqualError(err, "invalid `fetchsize` value, numeric expected") } func (suite *DsnTestSuite) TestInvalidResultsetmaxrows() { - dsn, err := ParseDSN("exa:localhost:1234;resultsetmaxrows=size") + dsn, err := parseDSN("exa:localhost:1234;resultsetmaxrows=size") suite.Nil(dsn) suite.EqualError(err, "invalid `resultsetmaxrows` value, numeric expected") } + +func (suite *DriverTestSuite) TestConfigToDsnDefaultValues() { + suite.Equal("exa:localhost:8563;user=sys;password=exasol", NewConfig("sys", "exasol").String()) + dsn, err := parseDSN( + "exa:localhost:1234;user=sys;password=exasol;autocommit=1;encryption=1;compression=0") + suite.NoError(err) + suite.Equal(dsn.User, "sys") + suite.Equal(dsn.Password, "exasol") + suite.Equal(dsn.Host, "localhost") + suite.Equal(dsn.Port, 1234) + suite.Equal(dsn.Autocommit, true) + suite.Equal(dsn.Encryption, true) + suite.Equal(dsn.Compression, false) +} + +func (suite *DriverTestSuite) TestConfigToDsn() { + config := NewConfig("sys", "exasol") + config.Compression(true) + config.Encryption(true) + config.Autocommit(true) + config.Insecure(true) + suite.Equal("exa:localhost:8563;user=sys;password=exasol;autocommit=1;compression=1;encryption=1;insecure=1", config.String()) +} diff --git a/helper.go b/helper.go index e117df0..432747e 100644 --- a/helper.go +++ b/helper.go @@ -14,3 +14,10 @@ func namedValuesToValues(namedValues []driver.NamedValue) ([]driver.Value, error } return values, nil } + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/integration_test.go b/integration_test.go index 4ea53f6..e142d80 100644 --- a/integration_test.go +++ b/integration_test.go @@ -3,21 +3,23 @@ package exasol_test import ( "context" "database/sql" - _ "github.com/exasol/exasol-driver-go" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" "log" "strings" "testing" "time" + + "github.com/exasol/exasol-driver-go" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" ) type IntegrationTestSuite struct { suite.Suite ctx context.Context exasolContainer testcontainers.Container - port string + port int } func TestIntegrationSuite(t *testing.T) { @@ -31,31 +33,34 @@ func (suite *IntegrationTestSuite) SetupSuite() { } func (suite *IntegrationTestSuite) TestConnect() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") - rows, _ := exasol.Query("SELECT 2 FROM DUAL") + if testing.Short() { + suite.T().Skip("skipping integration test") + } + db, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) + rows, _ := db.Query("SELECT 2 FROM DUAL") columns, _ := rows.Columns() suite.Equal("2", columns[0]) } func (suite *IntegrationTestSuite) TestConnectWithWrongPort() { - exasol, _ := sql.Open("exasol", "exa:localhost:1234;user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", "exa:localhost:1234;user=sys;password=exasol") err := exasol.Ping() suite.Error(err) suite.Contains(err.Error(), "connect: connection refuse") } func (suite *IntegrationTestSuite) TestConnectWithWrongUsername() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=wrongUser;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("wronguser", "exasol").Insecure(true).Port(suite.port).String()) suite.EqualError(exasol.Ping(), "[08004] Connection exception - authentication failed.") } func (suite *IntegrationTestSuite) TestConnectWithWrongPassword() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=wrongPassword;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "wrongpassword").Insecure(true).Port(suite.port).String()) suite.EqualError(exasol.Ping(), "[08004] Connection exception - authentication failed.") } func (suite *IntegrationTestSuite) TestExecAndQuery() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) schemaName := "TEST_SCHEMA_1" _, _ = exasol.Exec("CREATE SCHEMA " + schemaName) _, _ = exasol.Exec("CREATE TABLE " + schemaName + ".TEST_TABLE(x INT)") @@ -65,14 +70,14 @@ func (suite *IntegrationTestSuite) TestExecAndQuery() { } func (suite *IntegrationTestSuite) TestExecuteWithError() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) _, err := exasol.Exec("CREATE SCHEMAA TEST_SCHEMA") suite.Error(err) suite.True(strings.Contains(err.Error(), "syntax error")) } func (suite *IntegrationTestSuite) TestQueryWithError() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) schemaName := "TEST_SCHEMA_2" _, _ = exasol.Exec("CREATE SCHEMA " + schemaName) _, err := exasol.Query("SELECT x FROM " + schemaName + ".TEST_TABLE") @@ -81,7 +86,7 @@ func (suite *IntegrationTestSuite) TestQueryWithError() { } func (suite *IntegrationTestSuite) TestPreparedStatement() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) schemaName := "TEST_SCHEMA_3" _, _ = exasol.Exec("CREATE SCHEMA " + schemaName) _, _ = exasol.Exec("CREATE TABLE " + schemaName + ".TEST_TABLE(x INT)") @@ -93,7 +98,7 @@ func (suite *IntegrationTestSuite) TestPreparedStatement() { } func (suite *IntegrationTestSuite) TestBeginAndCommit() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0;autocommit=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Autocommit(false).Port(suite.port).String()) schemaName := "TEST_SCHEMA_4" transaction, _ := exasol.Begin() _, _ = transaction.Exec("CREATE SCHEMA " + schemaName) @@ -105,7 +110,7 @@ func (suite *IntegrationTestSuite) TestBeginAndCommit() { } func (suite *IntegrationTestSuite) TestBeginAndRollback() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0;autocommit=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Autocommit(false).Port(suite.port).String()) schemaName := "TEST_SCHEMA_5" transaction, _ := exasol.Begin() _, _ = transaction.Exec("CREATE SCHEMA " + schemaName) @@ -118,14 +123,14 @@ func (suite *IntegrationTestSuite) TestBeginAndRollback() { } func (suite *IntegrationTestSuite) TestPingWithContext() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) suite.NoError(exasol.PingContext(ctx)) cancel() } func (suite *IntegrationTestSuite) TestExecuteAndQueryWithContext() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).String()) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) schemaName := "TEST_SCHEMA_6" _, _ = exasol.ExecContext(ctx, "CREATE SCHEMA "+schemaName) @@ -137,7 +142,7 @@ func (suite *IntegrationTestSuite) TestExecuteAndQueryWithContext() { } func (suite *IntegrationTestSuite) TestBeginWithCancelledContext() { - exasol, _ := sql.Open("exasol", "exa:localhost:"+suite.port+";user=sys;password=exasol;encryption=0;autocommit=0") + exasol, _ := sql.Open("exasol", exasol.NewConfig("sys", "exasol").Insecure(true).Port(suite.port).Autocommit(false).String()) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) schemaName := "TEST_SCHEMA_7" transaction, _ := exasol.BeginTx(ctx, nil) @@ -178,10 +183,10 @@ func runExasolContainer(ctx context.Context) testcontainers.Container { return exasolContainer } -func getExasolPort(exasolContainer testcontainers.Container, ctx context.Context) string { +func getExasolPort(exasolContainer testcontainers.Container, ctx context.Context) int { port, err := exasolContainer.MappedPort(ctx, "8563") onError(err) - return port.Port() + return port.Int() } func onError(err error) { diff --git a/websocket.go b/websocket.go index c93ecb4..290e3d7 100644 --- a/websocket.go +++ b/websocket.go @@ -4,6 +4,7 @@ import ( "bytes" "compress/zlib" "context" + "crypto/tls" "database/sql/driver" "encoding/json" "fmt" @@ -14,7 +15,7 @@ import ( ) func (c *connection) connect() error { - uri := fmt.Sprintf("%s:%s", c.config.Host, c.config.Port) + uri := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port) scheme := "ws" if c.config.Encryption { @@ -26,10 +27,14 @@ func (c *connection) connect() error { Host: uri, } log.Printf("Connect to %s", u.String()) - ws, _, err := defaultDialer.DialContext(c.ctx, u.String(), nil) + dialer := *websocket.DefaultDialer + dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: c.config.Insecure} + + ws, _, err := dialer.DialContext(c.ctx, u.String(), nil) if err != nil { return err } + c.websocket = ws c.websocket.EnableWriteCompression(false) return nil