diff --git a/auth/auth.go b/auth/auth.go
index 6ee8b8f..efbbcb7 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -1,7 +1,40 @@
package auth
-import "net/http"
+import (
+ "net/http"
+ "strings"
+)
type Authenticator interface {
Authenticate(*http.Request) error
}
+
+type AuthType int
+
+const (
+ AuthTypeUnknown AuthType = iota
+ AuthTypePat
+ AuthTypeOauthU2M
+ AuthTypeOauthM2M
+)
+
+var authTypeNames []string = []string{"Unknown", "Pat", "OauthU2M", "OauthM2M"}
+
+func (at AuthType) String() string {
+ if at >= 0 && int(at) < len(authTypeNames) {
+ return authTypeNames[at]
+ }
+
+ return authTypeNames[0]
+}
+
+func ParseAuthType(typeString string) AuthType {
+ typeString = strings.ToLower(typeString)
+ for i, n := range authTypeNames {
+ if strings.ToLower(n) == typeString {
+ return AuthType(i)
+ }
+ }
+
+ return AuthTypeUnknown
+}
diff --git a/auth/oauth/m2m/m2m.go b/auth/oauth/m2m/m2m.go
new file mode 100644
index 0000000..aa72eb9
--- /dev/null
+++ b/auth/oauth/m2m/m2m.go
@@ -0,0 +1,91 @@
+package m2m
+
+// clientid e92aa085-4875-42fe-ad75-ba38fb3c9706
+// secretid vUdzecmn4aUi2jRDamaBOy3qThu9LSgeV_BW4UnQ
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "sync"
+
+ "github.com/databricks/databricks-sql-go/auth"
+ "github.com/databricks/databricks-sql-go/auth/oauth"
+ "github.com/rs/zerolog/log"
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/clientcredentials"
+)
+
+func NewAuthenticator(clientID, clientSecret, hostName string) auth.Authenticator {
+ scopes := oauth.GetScopes(hostName, []string{})
+ return &authClient{
+ clientID: clientID,
+ clientSecret: clientSecret,
+ hostName: hostName,
+ scopes: scopes,
+ }
+}
+
+type authClient struct {
+ clientID string
+ clientSecret string
+ hostName string
+ scopes []string
+ tokenSource oauth2.TokenSource
+ mx sync.Mutex
+}
+
+// Auth will start the OAuth Authorization Flow to authenticate the cli client
+// using the users credentials in the browser. Compatible with SSO.
+func (c *authClient) Authenticate(r *http.Request) error {
+ c.mx.Lock()
+ defer c.mx.Unlock()
+ if c.tokenSource != nil {
+ token, err := c.tokenSource.Token()
+ if err != nil {
+ return err
+ }
+ token.SetAuthHeader(r)
+ return nil
+ }
+
+ config, err := GetConfig(context.Background(), c.hostName, c.clientID, c.clientSecret, c.scopes)
+ if err != nil {
+ return fmt.Errorf("unable to generate clientCredentials.Config: %w", err)
+ }
+
+ c.tokenSource = GetTokenSource(config)
+ token, err := c.tokenSource.Token()
+ log.Info().Msgf("token fetched successfully")
+ if err != nil {
+ log.Err(err).Msg("failed to get token")
+
+ return err
+ }
+ token.SetAuthHeader(r)
+
+ return nil
+
+}
+
+func GetTokenSource(config clientcredentials.Config) oauth2.TokenSource {
+ tokenSource := config.TokenSource(context.Background())
+ return tokenSource
+}
+
+func GetConfig(ctx context.Context, issuerURL, clientID, clientSecret string, scopes []string) (clientcredentials.Config, error) {
+ // Get the endpoint based on the host name
+ endpoint, err := oauth.GetEndpoint(ctx, issuerURL)
+ if err != nil {
+ return clientcredentials.Config{}, fmt.Errorf("could not lookup provider details: %w", err)
+ }
+
+ config := clientcredentials.Config{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ TokenURL: endpoint.TokenURL,
+ Scopes: scopes,
+ }
+
+ return config, nil
+}
diff --git a/auth/oauth/oauth.go b/auth/oauth/oauth.go
new file mode 100644
index 0000000..82c952d
--- /dev/null
+++ b/auth/oauth/oauth.go
@@ -0,0 +1,122 @@
+package oauth
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+const (
+ azureTenantId = "4a67d088-db5c-48f1-9ff2-0aace800ae68"
+)
+
+func GetEndpoint(ctx context.Context, hostName string) (oauth2.Endpoint, error) {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
+ cloud := InferCloudFromHost(hostName)
+
+ if cloud == Unknown {
+ return oauth2.Endpoint{}, errors.New("unsupported cloud type")
+ }
+
+ if cloud == Azure {
+ authURL := fmt.Sprintf("https://%s/oidc/oauth2/v2.0/authorize", hostName)
+ tokenURL := fmt.Sprintf("https://%s/oidc/oauth2/v2.0/token", hostName)
+ return oauth2.Endpoint{AuthURL: authURL, TokenURL: tokenURL}, nil
+ }
+
+ issuerURL := fmt.Sprintf("https://%s/oidc", hostName)
+ ctx = oidc.InsecureIssuerURLContext(ctx, issuerURL)
+ provider, err := oidc.NewProvider(ctx, issuerURL)
+ if err != nil {
+ return oauth2.Endpoint{}, err
+ }
+
+ endpoint := provider.Endpoint()
+
+ return endpoint, err
+}
+
+func GetScopes(hostName string, scopes []string) []string {
+ for _, s := range []string{oidc.ScopeOfflineAccess} {
+ if !hasScope(scopes, s) {
+ scopes = append(scopes, s)
+ }
+ }
+
+ cloudType := InferCloudFromHost(hostName)
+ if cloudType == Azure {
+ userImpersonationScope := fmt.Sprintf("%s/user_impersonation", azureTenantId)
+ if !hasScope(scopes, userImpersonationScope) {
+ scopes = append(scopes, userImpersonationScope)
+ }
+ } else {
+ if !hasScope(scopes, "sql") {
+ scopes = append(scopes, "sql")
+ }
+ }
+
+ return scopes
+}
+
+func hasScope(scopes []string, scope string) bool {
+ for _, s := range scopes {
+ if s == scope {
+ return true
+ }
+ }
+ return false
+}
+
+var databricksAWSDomains []string = []string{
+ ".cloud.databricks.com",
+ ".dev.databricks.com",
+}
+
+var databricksAzureDomains []string = []string{
+ ".azuredatabricks.net",
+ ".databricks.azure.cn",
+ ".databricks.azure.us",
+}
+
+type CloudType int
+
+const (
+ AWS = iota
+ Azure
+ Unknown
+)
+
+func (cl CloudType) String() string {
+ switch cl {
+ case AWS:
+ return "AWS"
+ case Azure:
+ return "Azure"
+ }
+
+ return "Unknown"
+}
+
+func InferCloudFromHost(hostname string) CloudType {
+
+ for _, d := range databricksAzureDomains {
+ if strings.Contains(hostname, d) {
+ return Azure
+ }
+ }
+
+ for _, d := range databricksAWSDomains {
+ if strings.Contains(hostname, d) {
+ return AWS
+ }
+ }
+
+ return Unknown
+}
diff --git a/auth/oauth/pkce/pkce.go b/auth/oauth/pkce/pkce.go
new file mode 100644
index 0000000..b9f3368
--- /dev/null
+++ b/auth/oauth/pkce/pkce.go
@@ -0,0 +1,47 @@
+package pkce
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "io"
+
+ "golang.org/x/oauth2"
+)
+
+// Generate generates a new random PKCE code.
+func Generate() (Code, error) { return generate(rand.Reader) }
+
+func generate(rand io.Reader) (Code, error) {
+ // From https://tools.ietf.org/html/rfc7636#section-4.1:
+ // code_verifier = high-entropy cryptographic random STRING using the
+ // unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
+ // from Section 2.3 of [RFC3986], with a minimum length of 43 characters
+ // and a maximum length of 128 characters.
+ var buf [32]byte
+ if _, err := io.ReadFull(rand, buf[:]); err != nil {
+ return "", fmt.Errorf("could not generate PKCE code: %w", err)
+ }
+ return Code(hex.EncodeToString(buf[:])), nil
+}
+
+// Code implements the basic options required for RFC 7636: Proof Key for Code Exchange (PKCE).
+type Code string
+
+// Challenge returns the OAuth2 auth code parameter for sending the PKCE code challenge.
+func (p *Code) Challenge() oauth2.AuthCodeOption {
+ b := sha256.Sum256([]byte(*p))
+ return oauth2.SetAuthURLParam("code_challenge", base64.RawURLEncoding.EncodeToString(b[:]))
+}
+
+// Method returns the OAuth2 auth code parameter for sending the PKCE code challenge method.
+func (p *Code) Method() oauth2.AuthCodeOption {
+ return oauth2.SetAuthURLParam("code_challenge_method", "S256")
+}
+
+// Verifier returns the OAuth2 auth code parameter for sending the PKCE code verifier.
+func (p *Code) Verifier() oauth2.AuthCodeOption {
+ return oauth2.SetAuthURLParam("code_verifier", string(*p))
+}
diff --git a/auth/oauth/u2m/authenticator.go b/auth/oauth/u2m/authenticator.go
new file mode 100644
index 0000000..29a973c
--- /dev/null
+++ b/auth/oauth/u2m/authenticator.go
@@ -0,0 +1,268 @@
+package u2m
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/pkg/browser"
+
+ "github.com/databricks/databricks-sql-go/auth"
+ "github.com/databricks/databricks-sql-go/auth/oauth"
+ "github.com/rs/zerolog/log"
+ "golang.org/x/oauth2"
+)
+
+const (
+ azureClientId = "96eecda7-19ea-49cc-abb5-240097d554f5"
+ azureRedirctURL = "localhost:8030"
+
+ awsClientId = "databricks-sql-connector"
+ awsRedirctURL = "localhost:8030"
+)
+
+func NewAuthenticator(hostName string, timeout time.Duration) (auth.Authenticator, error) {
+
+ cloud := oauth.InferCloudFromHost(hostName)
+
+ var clientID, redirectURL string
+ if cloud == oauth.AWS {
+ clientID = awsClientId
+ redirectURL = awsRedirctURL
+ } else if cloud == oauth.Azure {
+ clientID = azureClientId
+ redirectURL = azureRedirctURL
+ } else {
+ return nil, errors.New("unhandled cloud type: " + cloud.String())
+ }
+
+ // Get an oauth2 config
+ config, err := GetConfig(context.Background(), hostName, clientID, "", redirectURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("unable to generate oauth2.Config: %w", err)
+ }
+
+ tsp, err := GetTokenSourceProvider(context.Background(), config, timeout)
+
+ return &u2mAuthenticator{
+ clientID: clientID,
+ hostName: hostName,
+ tsp: tsp,
+ }, err
+}
+
+type u2mAuthenticator struct {
+ clientID string
+ hostName string
+ // scopes []string
+ tokenSource oauth2.TokenSource
+ tsp *tokenSourceProvider
+ mx sync.Mutex
+}
+
+// Auth will start the OAuth Authorization Flow to authenticate the cli client
+// using the users credentials in the browser. Compatible with SSO.
+func (c *u2mAuthenticator) Authenticate(r *http.Request) error {
+ c.mx.Lock()
+ defer c.mx.Unlock()
+ if c.tokenSource != nil {
+ token, err := c.tokenSource.Token()
+ if err == nil {
+ token.SetAuthHeader(r)
+ return nil
+ } else if !strings.Contains(err.Error(), "invalid_grant") {
+ return err
+ }
+
+ token.SetAuthHeader(r)
+ return nil
+ }
+
+ tokenSource, err := c.tsp.GetTokenSource()
+ if err != nil {
+ return fmt.Errorf("unable to get token source: %w", err)
+ }
+ c.tokenSource = tokenSource
+
+ token, err := tokenSource.Token()
+ if err != nil {
+ return fmt.Errorf("unable to get token source: %w", err)
+ }
+
+ token.SetAuthHeader(r)
+
+ return nil
+}
+
+type authResponse struct {
+ err string
+ details string
+ state string
+ code string
+}
+
+type tokenSourceProvider struct {
+ timeout time.Duration
+ state string
+ sigintCh chan os.Signal
+ authDoneCh chan authResponse
+ redirectURL *url.URL
+ config oauth2.Config
+}
+
+func (tsp *tokenSourceProvider) GetTokenSource() (oauth2.TokenSource, error) {
+ state, err := randString(16)
+ if err != nil {
+ err = fmt.Errorf("unable to generate random number: %w", err)
+ return nil, err
+ }
+
+ challenge, challengeMethod, verifier, err := GetAuthCodeOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ loginURL := tsp.config.AuthCodeURL(state, challenge, challengeMethod)
+ tsp.state = state
+
+ log.Info().Msgf("listening on %s://%s/", tsp.redirectURL.Scheme, tsp.redirectURL.Host)
+ listener, err := net.Listen("tcp", tsp.redirectURL.Host)
+ if err != nil {
+ return nil, err
+ }
+ defer listener.Close()
+
+ srv := &http.Server{
+ ReadHeaderTimeout: 3 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ }
+
+ defer srv.Close()
+
+ // Start local server to wait for callback
+ go func() {
+ err := srv.Serve(listener)
+
+ // in case port is in use
+ if err != nil && err != http.ErrServerClosed {
+ tsp.authDoneCh <- authResponse{err: err.Error()}
+ }
+ }()
+
+ fmt.Printf("\nOpen URL in Browser to Continue: %s\n\n", loginURL)
+ err = browser.OpenURL(loginURL)
+ if err != nil {
+ fmt.Println("Unable to open browser automatically. Please open manually: ", loginURL)
+ }
+
+ ctx := context.Background()
+ // Wait for callback to be received, Wait for either the callback to finish, SIGINT to be received or up to 2 minutes
+ select {
+ case authResponse := <-tsp.authDoneCh:
+ if authResponse.err != "" {
+ return nil, fmt.Errorf("identity provider error: %s: %s", authResponse.err, authResponse.details)
+ }
+ token, err := tsp.config.Exchange(ctx, authResponse.code, verifier)
+ if err != nil {
+ return nil, fmt.Errorf("failed to exchange token: %w", err)
+ }
+
+ return tsp.config.TokenSource(ctx, token), nil
+
+ case <-tsp.sigintCh:
+ return nil, errors.New("interrupted while waiting for auth callback")
+
+ case <-time.After(tsp.timeout):
+ return nil, errors.New("timed out waiting for response from provider")
+ }
+}
+
+func (tsp *tokenSourceProvider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ resp := authResponse{
+ err: r.URL.Query().Get("error"),
+ details: r.URL.Query().Get("error_description"),
+ state: r.URL.Query().Get("state"),
+ code: r.URL.Query().Get("code"),
+ }
+
+ // Send the response back to the to cli
+ defer func() { tsp.authDoneCh <- resp }()
+
+ // Do some checking of the response here to show more relevant content
+ if resp.err != "" {
+ log.Error().Msg(resp.err)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err := w.Write([]byte(errorHTML("Identity Provider returned an error: " + resp.err)))
+ if err != nil {
+ log.Error().Err(err).Msg("unable to write error response")
+ }
+ return
+ }
+ if resp.state != tsp.state && r.URL.String() != "/favicon.ico" {
+ msg := "Authentication state received did not match original request. Please try to login again."
+ log.Error().Msg(msg)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err := w.Write([]byte(errorHTML(msg)))
+ if err != nil {
+ log.Error().Err(err).Msg("unable to write error response")
+ }
+ return
+ }
+
+ _, err := w.Write([]byte(infoHTML("CLI Login Success", "You may close this window anytime now and go back to terminal")))
+ if err != nil {
+ log.Error().Err(err).Msg("unable to write success response")
+ }
+}
+
+var register sync.Once = sync.Once{}
+
+func GetTokenSourceProvider(ctx context.Context, config oauth2.Config, timeout time.Duration) (*tokenSourceProvider, error) {
+ if timeout == 0 {
+ timeout = 2 * time.Minute
+ }
+
+ // handle ctrl-c while waiting for the callback
+ sigintCh := make(chan os.Signal, 1)
+ signal.Notify(sigintCh, os.Interrupt)
+
+ // receive auth callback response
+ authDoneCh := make(chan authResponse)
+
+ u, _ := url.Parse(config.RedirectURL)
+ if u.Path == "" {
+ u.Path = "/"
+ }
+
+ tsp := &tokenSourceProvider{
+ timeout: timeout,
+ sigintCh: sigintCh,
+ authDoneCh: authDoneCh,
+ redirectURL: u,
+ config: config,
+ }
+
+ f := func() { http.Handle(u.Path, tsp) }
+ register.Do(f)
+
+ return tsp, nil
+}
+
+func randString(nByte int) (string, error) {
+ b := make([]byte, nByte)
+ if _, err := io.ReadFull(rand.Reader, b); err != nil {
+ return "", err
+ }
+ return base64.RawURLEncoding.EncodeToString(b), nil
+}
diff --git a/auth/oauth/u2m/html_template.go b/auth/oauth/u2m/html_template.go
new file mode 100644
index 0000000..69107b3
--- /dev/null
+++ b/auth/oauth/u2m/html_template.go
@@ -0,0 +1,56 @@
+package u2m
+
+import (
+ "bytes"
+ _ "embed"
+ "html/template"
+)
+
+type SimplePage struct {
+ Title string
+ Heading string
+ Content string
+ Action ActionLink
+ Code string
+}
+
+type ActionLink struct {
+ Label string
+ Link string
+}
+
+var (
+ //go:embed templates/simple.html
+ simpleHtmlPage string
+)
+
+func renderHTML(data SimplePage) (string, error) {
+ var out bytes.Buffer
+ tmpl, err := template.New("name").Parse(simpleHtmlPage)
+ if err != nil {
+ return "", err
+ }
+ err = tmpl.Execute(&out, data)
+ return out.String(), err
+}
+
+func infoHTML(title, content string) string {
+ data := SimplePage{
+ Title: "Authentication Success",
+ Heading: title,
+ Content: content,
+ }
+ out, _ := renderHTML(data)
+ return out
+}
+
+func errorHTML(msg string) string {
+ data := SimplePage{
+ Title: "Authentication Error",
+ Heading: "Ooops!",
+ Content: "Sorry, Databricks could not authenticate to your account due to some server errors. Please try it later.",
+ Code: msg,
+ }
+ out, _ := renderHTML(data)
+ return out
+}
diff --git a/auth/oauth/u2m/templates/simple.html b/auth/oauth/u2m/templates/simple.html
new file mode 100644
index 0000000..71b0336
--- /dev/null
+++ b/auth/oauth/u2m/templates/simple.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+ {{ .Title }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/auth/oauth/u2m/u2m.go b/auth/oauth/u2m/u2m.go
new file mode 100644
index 0000000..456e369
--- /dev/null
+++ b/auth/oauth/u2m/u2m.go
@@ -0,0 +1,51 @@
+package u2m
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/databricks/databricks-sql-go/auth/oauth"
+ "github.com/databricks/databricks-sql-go/auth/oauth/pkce"
+ "golang.org/x/oauth2"
+)
+
+func GetConfig(ctx context.Context, hostName, clientID, clientSecret, callbackURL string, scopes []string) (oauth2.Config, error) {
+ // Add necessary scopes for AWS or Azure
+ scopes = oauth.GetScopes(hostName, scopes)
+
+ // Get the endpoint based on the host name
+ endpoint, err := oauth.GetEndpoint(ctx, hostName)
+ if err != nil {
+ return oauth2.Config{}, fmt.Errorf("could not lookup provider details: %w", err)
+ }
+
+ if !strings.HasPrefix(callbackURL, "http") {
+ callbackURL = fmt.Sprintf("http://%s", callbackURL)
+ }
+
+ config := oauth2.Config{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ Endpoint: endpoint,
+ RedirectURL: callbackURL,
+ Scopes: scopes,
+ }
+
+ return config, nil
+}
+
+func GetAuthCodeOptions() (challenge, challengeMethod, verifier oauth2.AuthCodeOption, err error) {
+ code, err := pkce.Generate()
+ if err != nil {
+ return
+ }
+
+ return code.Challenge(), code.Method(), code.Verifier(), err
+}
+
+func GetLoginURL(config oauth2.Config, state string, options ...oauth2.AuthCodeOption) string {
+ loginURL := config.AuthCodeURL(state, options...)
+
+ return loginURL
+}
diff --git a/connector.go b/connector.go
index 03a3bec..96a8831 100644
--- a/connector.go
+++ b/connector.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/databricks/databricks-sql-go/auth"
+ "github.com/databricks/databricks-sql-go/auth/oauth/m2m"
"github.com/databricks/databricks-sql-go/auth/pat"
"github.com/databricks/databricks-sql-go/driverctx"
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
@@ -259,3 +260,13 @@ func WithMaxDownloadThreads(numThreads int) connOption {
c.MaxDownloadThreads = numThreads
}
}
+
+// Setup of Oauth M2m authentication
+func WithClientCredentials(clientID, clientSecret string) connOption {
+ return func(c *config.Config) {
+ if clientID != "" && clientSecret != "" {
+ authr := m2m.NewAuthenticator(clientID, clientSecret, c.Host)
+ c.Authenticator = authr
+ }
+ }
+}
diff --git a/doc.go b/doc.go
index 3fb432b..cdbcb13 100644
--- a/doc.go
+++ b/doc.go
@@ -39,6 +39,10 @@ Supported optional connection parameters can be specified in param=value and inc
- userAgentEntry: Used to identify partners. Set as a string with format
- useCloudFetch: Used to enable cloud fetch for the query execution. Default is false
- maxDownloadThreads: Sets up the max number of concurrent workers for cloud fetch. Default is 10
+ - authType: Specifies the desired authentication type. Valid values are: Pat, OauthM2M, OauthU2M
+ - accessToken: Personal access token. Required if authType set to Pat
+ - clientID: Specifies the client ID to use with OauthM2M
+ - clientSecret: Specifies the client secret to use with OauthM2M
Supported optional session parameters can be specified in param=value and include:
@@ -83,6 +87,8 @@ Supported functional options include:
- WithUserAgentEntry( string). Used to identify partners. Optional
- WithCloudFetch (bool). Used to enable cloud fetch for the query execution. Default is false. Optional
- WithMaxDownloadThreads ( int). Sets up the max number of concurrent workers for cloud fetch. Default is 10. Optional
+ - WithAuthenticator ( auth.Authenticator). Sets up authentication. Required if neither access token or client credentials are provided.
+ - WithClientCredentials( string, string). Sets up Oauth M2M authentication.
# Query cancellation and timeout
diff --git a/errors/errors.go b/errors/errors.go
index 5b4339b..a551853 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -22,12 +22,12 @@ const (
ErrInvalidURL = "invalid URL"
ErrNoAuthenticationMethod = "no authentication method set"
+ ErrNoDefaultAuthenticator = "unable to create default authenticator"
ErrInvalidDSNFormat = "invalid DSN: invalid format"
ErrInvalidDSNPort = "invalid DSN: invalid DSN port"
ErrInvalidDSNPATIsEmpty = "invalid DSN: empty token"
ErrBasicAuthNotSupported = "invalid DSN: basic auth not enabled"
- ErrInvalidDSNMaxRows = "invalid DSN: maxRows param is not an integer"
- ErrInvalidDSNTimeout = "invalid DSN: timeout param is not an integer"
+ ErrInvalidDSNM2m = "invalid DSN: clientId and clientSecret params required"
// Execution error messages (query failure)
ErrQueryExecution = "failed to execute query"
diff --git a/examples/oauth/main.go b/examples/oauth/main.go
new file mode 100644
index 0000000..7393fbd
--- /dev/null
+++ b/examples/oauth/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ dbsql "github.com/databricks/databricks-sql-go"
+ "github.com/databricks/databricks-sql-go/auth/oauth/u2m"
+ "github.com/joho/godotenv"
+)
+
+func main() {
+ err := godotenv.Load()
+
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+
+ authenticator, err := u2m.NewAuthenticator(os.Getenv("DATABRICKS_HOST"), 1*time.Minute)
+
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+
+ connector, err := dbsql.NewConnector(
+ dbsql.WithServerHostname(os.Getenv("DATABRICKS_HOST")),
+ dbsql.WithHTTPPath(os.Getenv("DATABRICKS_HTTPPATH")),
+ dbsql.WithAuthenticator(authenticator),
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ db := sql.OpenDB(connector)
+ defer db.Close()
+
+ // Pinging should require logging in
+ if err := db.Ping(); err != nil {
+ fmt.Println(err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+ defer cancel()
+
+ var res int
+
+ // Running query should not require logging in as we should have a token
+ // from when ping was called.
+ err1 := db.QueryRowContext(ctx, `select 1`).Scan(&res)
+
+ if err1 != nil {
+ if err1 == sql.ErrNoRows {
+ fmt.Println("not found")
+ return
+ } else {
+ fmt.Printf("err: %v\n", err1)
+ }
+ }
+ fmt.Println(res)
+}
diff --git a/go.mod b/go.mod
index d5a232d..e3862a8 100644
--- a/go.mod
+++ b/go.mod
@@ -5,9 +5,12 @@ go 1.19
require (
github.com/apache/arrow/go/v12 v12.0.1
github.com/apache/thrift v0.17.0
+ github.com/coreos/go-oidc/v3 v3.5.0
github.com/joho/godotenv v1.4.0
github.com/mattn/go-isatty v0.0.19
+ github.com/pierrec/lz4/v4 v4.1.15
github.com/stretchr/testify v1.8.1
+ golang.org/x/oauth2 v0.7.0
gotest.tools/gotestsum v1.8.2
)
@@ -17,7 +20,9 @@ require (
github.com/dnephin/pflag v1.0.7 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.8+incompatible // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -29,20 +34,24 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
- github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
+ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/mod v0.8.0 // indirect
+ golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
- golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
+ golang.org/x/term v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/hashicorp/go-retryablehttp v0.7.1
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.28.0
- golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/sys v0.7.0 // indirect
)
diff --git a/go.sum b/go.sum
index 677161e..568bb27 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@@ -5,6 +6,8 @@ github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/
github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw=
github.com/apache/thrift v0.17.0 h1:cMd2aj52n+8VoAtvSvLn4kDC3aZ6IAkBuqWQ2IDu7wo=
github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q=
+github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
+github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -16,13 +19,20 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -43,7 +53,10 @@ github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQan
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -59,6 +72,8 @@ github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8D
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -72,33 +87,49 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
+golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -110,24 +141,35 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -137,7 +179,15 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/internal/client/client.go b/internal/client/client.go
index 0d39a57..cb6c961 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -256,7 +256,7 @@ func InitThriftClient(cfg *config.Config, httpclient *http.Client) (*ThriftServi
case "http":
if httpclient == nil {
if cfg.Authenticator == nil {
- return nil, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrNoAuthenticationMethod, nil)
+ return nil, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrNoDefaultAuthenticator, nil)
}
httpclient = RetryableClient(cfg)
}
diff --git a/internal/config/config.go b/internal/config/config.go
index eda2555..b6ccace 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -15,6 +15,8 @@ import (
"github.com/databricks/databricks-sql-go/auth"
"github.com/databricks/databricks-sql-go/auth/noop"
+ "github.com/databricks/databricks-sql-go/auth/oauth/m2m"
+ "github.com/databricks/databricks-sql-go/auth/oauth/u2m"
"github.com/databricks/databricks-sql-go/auth/pat"
"github.com/databricks/databricks-sql-go/internal/cli_service"
dbsqlerrint "github.com/databricks/databricks-sql-go/internal/errors"
@@ -218,87 +220,72 @@ func ParseDSN(dsn string) (UserConfig, error) {
return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPort, err)
}
ucfg.Port = port
- name := parsedURL.User.Username()
- if name == "token" {
- pass, ok := parsedURL.User.Password()
- if pass == "" {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPATIsEmpty, err)
- }
- if ok {
- ucfg.AccessToken = pass
- pat := &pat.PATAuth{
- AccessToken: pass,
- }
- ucfg.Authenticator = pat
- }
- } else {
- if name != "" {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrBasicAuthNotSupported, err)
- }
- }
+
ucfg.HTTPPath = parsedURL.Path
- params := parsedURL.Query()
- maxRowsStr := params.Get("maxRows")
- if maxRowsStr != "" {
- maxRows, err := strconv.Atoi(maxRowsStr)
+
+ // Any params that are not specifically handled are assumed to be session params.
+ // Use extractableParams so that the processed values are deleted as we go.
+ params := &extractableParams{Values: parsedURL.Query()}
+
+ // Create an authenticator based on the url and params
+ err = makeAuthenticator(parsedURL, params, &ucfg)
+ if err != nil {
+ return UserConfig{}, err
+ }
+
+ if maxRows, ok, err := params.extractAsInt("maxRows"); ok {
if err != nil {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNMaxRows, err)
+ return UserConfig{}, err
}
- // we should always have at least some page size
- if maxRows != 0 {
+ if maxRows > 0 {
ucfg.MaxRows = maxRows
}
}
- params.Del("maxRows")
- timeoutStr := params.Get("timeout")
- if timeoutStr != "" {
- timeoutSeconds, err := strconv.Atoi(timeoutStr)
+ if timeoutSeconds, ok, err := params.extractAsInt("timeout"); ok {
if err != nil {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNTimeout, err)
+ return UserConfig{}, err
}
ucfg.QueryTimeout = time.Duration(timeoutSeconds) * time.Second
}
- params.Del("timeout")
- if params.Has("catalog") {
- ucfg.Catalog = params.Get("catalog")
- params.Del("catalog")
+
+ if catalog, ok := params.extract("catalog"); ok {
+ ucfg.Catalog = catalog
}
- if params.Has("userAgentEntry") {
- ucfg.UserAgentEntry = params.Get("userAgentEntry")
+ if userAgent, ok := params.extract("userAgentEntry"); ok {
+ ucfg.UserAgentEntry = userAgent
params.Del("userAgentEntry")
}
- if params.Has("schema") {
- ucfg.Schema = params.Get("schema")
- params.Del("schema")
+ if schema, ok := params.extract("schema"); ok {
+ ucfg.Schema = schema
}
// Cloud Fetch parameters
- if params.Has("useCloudFetch") {
- useCloudFetch, err := strconv.ParseBool(params.Get("useCloudFetch"))
+ if useCloudFetch, ok, err := params.extractAsBool("useCloudFetch"); ok {
if err != nil {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat("useCloudFetch", params.Get("useCloudFetch"), "bool"), err)
+ return UserConfig{}, err
}
ucfg.UseCloudFetch = useCloudFetch
}
- params.Del("useCloudFetch")
- if params.Has("maxDownloadThreads") {
- numThreads, err := strconv.Atoi(params.Get("maxDownloadThreads"))
+
+ if numThreads, ok, err := params.extractAsInt("maxDownloadThreads"); ok {
if err != nil {
- return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat("maxDownloadThreads", params.Get("maxDownloadThreads"), "int"), err)
+ return UserConfig{}, err
}
ucfg.MaxDownloadThreads = numThreads
}
- params.Del("maxDownloadThreads")
- for k := range params {
- if strings.ToLower(k) == "timezone" {
- ucfg.Location, err = time.LoadLocation(params.Get("timezone"))
- }
+ // for timezone we do a case insensitive key match.
+ // We use getNoCase because we want to leave timezone in the params so that it will also
+ // be used as a session param.
+ if timezone, ok := params.getNoCase("timezone"); ok {
+ ucfg.Location, err = time.LoadLocation(timezone)
}
- if len(params) > 0 {
+
+ // any left over params are treated as session params
+ if len(params.Values) > 0 {
sessionParams := make(map[string]string)
- for k := range params {
+ for k := range params.Values {
sessionParams[k] = params.Get(k)
}
ucfg.SessionParams = sessionParams
@@ -307,6 +294,149 @@ func ParseDSN(dsn string) (UserConfig, error) {
return ucfg, err
}
+// update the config with an authenticator based on the value from the parsed DSN
+func makeAuthenticator(parsedURL *url.URL, params *extractableParams, config *UserConfig) error {
+ name := parsedURL.User.Username()
+ // if the user name is set to 'token' we will interpret the password as an acess token
+ if name == "token" {
+ pass, _ := parsedURL.User.Password()
+ return addPatAuthenticator(pass, config)
+ } else if name != "" {
+ // Currently don't support user name/password authentication
+ return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrBasicAuthNotSupported, nil)
+ } else {
+ // Process parameters that specify the authentication type. They are removed from params
+ // Get the optional authentication type param
+ authTypeS, _ := params.extract("authType")
+ authType := auth.ParseAuthType(authTypeS)
+
+ // Get optional parameters for creating an authenticator
+ clientId, hasClientId := params.extract("clientId")
+ if !hasClientId {
+ clientId, hasClientId = params.extract("clientID")
+ }
+
+ clientSecret, hasClientSecret := params.extract("clientSecret")
+ accessToken, hasAccessToken := params.extract("accessToken")
+
+ switch authType {
+ case auth.AuthTypeUnknown:
+ // if no authentication type is specified create an authenticator based on which
+ // params have values
+ if hasAccessToken {
+ return addPatAuthenticator(accessToken, config)
+ }
+
+ if hasClientId || hasClientSecret {
+ return addOauthM2MAuthenticator(clientId, clientSecret, config)
+ }
+ case auth.AuthTypePat:
+ return addPatAuthenticator(accessToken, config)
+ case auth.AuthTypeOauthM2M:
+ return addOauthM2MAuthenticator(clientId, clientSecret, config)
+ case auth.AuthTypeOauthU2M:
+ return addOauthU2MAuthenticator(config)
+ }
+
+ }
+
+ return nil
+}
+
+func addPatAuthenticator(accessToken string, config *UserConfig) error {
+ if accessToken == "" {
+ return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPATIsEmpty, nil)
+ }
+ config.AccessToken = accessToken
+ pat := &pat.PATAuth{
+ AccessToken: accessToken,
+ }
+ config.Authenticator = pat
+ return nil
+}
+
+func addOauthM2MAuthenticator(clientId, clientSecret string, config *UserConfig) error {
+ if clientId == "" || clientSecret == "" {
+ return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNM2m, nil)
+ }
+
+ m2m := m2m.NewAuthenticator(clientId, clientSecret, config.Host)
+ config.Authenticator = m2m
+ return nil
+}
+
+func addOauthU2MAuthenticator(config *UserConfig) error {
+ u2m, err := u2m.NewAuthenticator(config.Host, 0)
+ if err == nil {
+ config.Authenticator = u2m
+ }
+ return err
+}
+
+type extractableParams struct {
+ url.Values
+}
+
+// returns the value corresponding to the key, if any, and a bool flag indicating if
+// there was a set value and it is not the empty string
+// deletes the key/value from params
+func (params *extractableParams) extract(key string) (string, bool) {
+ return extractParam(key, params, false, true)
+}
+
+func (params *extractableParams) extractAsInt(key string) (int, bool, error) {
+ if intString, ok := extractParam(key, params, false, true); ok {
+ i, err := strconv.Atoi(intString)
+ if err != nil {
+ return 0, true, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat(key, intString, "int"), err)
+ }
+
+ return i, true, nil
+ }
+
+ return 0, false, nil
+}
+
+func (params *extractableParams) extractAsBool(key string) (bool, bool, error) {
+ if boolString, ok := extractParam(key, params, false, true); ok {
+ b, err := strconv.ParseBool(boolString)
+ if err != nil {
+ return false, true, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat(key, boolString, "bool"), err)
+ }
+
+ return b, true, nil
+ }
+ return false, false, nil
+}
+
+// returns the value corresponding to the key using case insensitive key matching and a bool flag
+// indicating if the value was set and is not the empty string
+func (params *extractableParams) getNoCase(key string) (string, bool) {
+ return extractParam(key, params, true, false)
+}
+
+func extractParam(key string, params *extractableParams, ignoreCase bool, delValue bool) (string, bool) {
+ if ignoreCase {
+ key = strings.ToLower(key)
+ }
+
+ for k := range params.Values {
+ kc := k
+ if ignoreCase {
+ kc = strings.ToLower(k)
+ }
+ if kc == key {
+ val := params.Get(k)
+ if delValue {
+ params.Del(k)
+ }
+ return val, val != ""
+ }
+ }
+
+ return "", false
+}
+
type ArrowConfig struct {
UseArrowBatches bool
UseArrowNativeDecimal bool
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index d891c36..df5e4bd 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -2,11 +2,13 @@ package config
import (
"crypto/tls"
+ "fmt"
"reflect"
"testing"
"time"
"github.com/databricks/databricks-sql-go/auth/noop"
+ "github.com/databricks/databricks-sql-go/auth/oauth/m2m"
"github.com/databricks/databricks-sql-go/auth/pat"
"github.com/databricks/databricks-sql-go/internal/cli_service"
)
@@ -15,6 +17,9 @@ func TestParseConfig(t *testing.T) {
type args struct {
dsn string
}
+
+ defCloudConfig := CloudFetchConfig{}.WithDefaults()
+
tz, _ := time.LoadLocation("America/Vancouver")
tests := []struct {
name string
@@ -28,23 +33,18 @@ func TestParseConfig(t *testing.T) {
name: "base case",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 443,
- MaxRows: defaultMaxRows,
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- AccessToken: "supersecret",
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 443,
+ MaxRows: defaultMaxRows,
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ AccessToken: "supersecret",
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -53,23 +53,18 @@ func TestParseConfig(t *testing.T) {
name: "with https scheme",
args: args{dsn: "https://token:supersecret@example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 443,
- MaxRows: defaultMaxRows,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 443,
+ MaxRows: defaultMaxRows,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -78,22 +73,17 @@ func TestParseConfig(t *testing.T) {
name: "with http scheme",
args: args{dsn: "http://localhost:8080/sql/1.0/endpoints/12346a5b5b0e123a"},
wantCfg: UserConfig{
- Protocol: "http",
- Host: "localhost",
- Port: 8080,
- MaxRows: defaultMaxRows,
- Authenticator: &noop.NoopAuth{},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "http",
+ Host: "localhost",
+ Port: 8080,
+ MaxRows: defaultMaxRows,
+ Authenticator: &noop.NoopAuth{},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantErr: false,
wantURL: "http://localhost:8080/sql/1.0/endpoints/12346a5b5b0e123a",
@@ -102,21 +92,16 @@ func TestParseConfig(t *testing.T) {
name: "with localhost",
args: args{dsn: "http://localhost:8080"},
wantCfg: UserConfig{
- Protocol: "http",
- Host: "localhost",
- Port: 8080,
- Authenticator: &noop.NoopAuth{},
- MaxRows: defaultMaxRows,
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "http",
+ Host: "localhost",
+ Port: 8080,
+ Authenticator: &noop.NoopAuth{},
+ MaxRows: defaultMaxRows,
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantErr: false,
wantURL: "http://localhost:8080",
@@ -125,24 +110,19 @@ func TestParseConfig(t *testing.T) {
name: "with query params",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?timeout=100&maxRows=1000"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 8000,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- QueryTimeout: 100 * time.Second,
- MaxRows: 1000,
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -151,25 +131,20 @@ func TestParseConfig(t *testing.T) {
name: "with query params and session params",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?timeout=100&maxRows=1000&timezone=America/Vancouver"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 8000,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- QueryTimeout: 100 * time.Second,
- MaxRows: 1000,
- Location: tz,
- SessionParams: map[string]string{"timezone": "America/Vancouver"},
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ Location: tz,
+ SessionParams: map[string]string{"timezone": "America/Vancouver"},
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -178,22 +153,17 @@ func TestParseConfig(t *testing.T) {
name: "bare",
args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Authenticator: &noop.NoopAuth{},
- Port: 8000,
- MaxRows: defaultMaxRows,
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Authenticator: &noop.NoopAuth{},
+ Port: 8000,
+ MaxRows: defaultMaxRows,
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -202,24 +172,19 @@ func TestParseConfig(t *testing.T) {
name: "with catalog",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b?catalog=default"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 8000,
- MaxRows: defaultMaxRows,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b",
- Catalog: "default",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ MaxRows: defaultMaxRows,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b",
+ Catalog: "default",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b",
wantErr: false,
@@ -228,24 +193,19 @@ func TestParseConfig(t *testing.T) {
name: "with user agent entry",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b?userAgentEntry=partner-name"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 8000,
- MaxRows: defaultMaxRows,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b",
- UserAgentEntry: "partner-name",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ MaxRows: defaultMaxRows,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b",
+ UserAgentEntry: "partner-name",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b",
wantErr: false,
@@ -254,24 +214,19 @@ func TestParseConfig(t *testing.T) {
name: "with schema",
args: args{dsn: "token:supersecret2@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?schema=system"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 8000,
- MaxRows: defaultMaxRows,
- AccessToken: "supersecret2",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
- HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
- Schema: "system",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ MaxRows: defaultMaxRows,
+ AccessToken: "supersecret2",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ Schema: "system",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
wantErr: false,
@@ -295,7 +250,6 @@ func TestParseConfig(t *testing.T) {
UseCloudFetch: true,
MaxDownloadThreads: 10,
MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
},
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b",
@@ -320,7 +274,6 @@ func TestParseConfig(t *testing.T) {
UseCloudFetch: true,
MaxDownloadThreads: 15,
MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
},
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b",
@@ -349,7 +302,6 @@ func TestParseConfig(t *testing.T) {
UseCloudFetch: true,
MaxDownloadThreads: 15,
MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
},
},
wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
@@ -359,22 +311,17 @@ func TestParseConfig(t *testing.T) {
name: "missing http path",
args: args{dsn: "token:supersecret@example.cloud.databricks.com:443"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 443,
- MaxRows: defaultMaxRows,
- AccessToken: "supersecret",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 443,
+ MaxRows: defaultMaxRows,
+ AccessToken: "supersecret",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret"},
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:443",
wantErr: false,
@@ -384,25 +331,20 @@ func TestParseConfig(t *testing.T) {
name: "missing http path 2",
args: args{dsn: "token:supersecret2@example.cloud.databricks.com:443?catalog=default&schema=system&timeout=100&maxRows=1000"},
wantCfg: UserConfig{
- Protocol: "https",
- Host: "example.cloud.databricks.com",
- Port: 443,
- AccessToken: "supersecret2",
- Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
- QueryTimeout: 100 * time.Second,
- MaxRows: 1000,
- Catalog: "default",
- Schema: "system",
- SessionParams: make(map[string]string),
- RetryMax: 4,
- RetryWaitMin: 1 * time.Second,
- RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{
- UseCloudFetch: false,
- MaxDownloadThreads: 10,
- MaxFilesInMemory: 10,
- MinTimeToExpiry: 0 * time.Second,
- },
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 443,
+ AccessToken: "supersecret2",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ Catalog: "default",
+ Schema: "system",
+ SessionParams: make(map[string]string),
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://example.cloud.databricks.com:443",
wantErr: false,
@@ -444,7 +386,6 @@ func TestParseConfig(t *testing.T) {
wantCfg: UserConfig{},
wantErr: true,
},
-
{
name: "missing host",
args: args{dsn: "token:supersecret2@:443?catalog=default&schema=system&timeout=100&maxRows=1000"},
@@ -461,24 +402,158 @@ func TestParseConfig(t *testing.T) {
RetryMax: 4,
RetryWaitMin: 1 * time.Second,
RetryWaitMax: 30 * time.Second,
- CloudFetchConfig: CloudFetchConfig{}.WithDefaults(),
+ CloudFetchConfig: defCloudConfig,
},
wantURL: "https://:443",
wantErr: false,
wantEndpointErr: true,
},
+ {
+ name: "with accessToken param",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2"},
+ wantCfg: UserConfig{
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ AccessToken: "supersecret2",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ UserAgentEntry: "partner-name",
+ Catalog: "default",
+ Schema: "system",
+ SessionParams: map[string]string{"ANSI_MODE": "true"},
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
+ },
+ wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
+ wantErr: false,
+ },
+ {
+ name: "with accessToken param and client id/secret params",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2&clientId=client_id&clientSecret=client_secret"},
+ wantCfg: UserConfig{
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ AccessToken: "supersecret2",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ UserAgentEntry: "partner-name",
+ Catalog: "default",
+ Schema: "system",
+ SessionParams: map[string]string{"ANSI_MODE": "true"},
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
+ },
+ wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
+ wantErr: false,
+ },
+ {
+ name: "authType unknown with accessTokenParam",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2&clientId=client_id&clientSecret=client_secret"},
+ wantCfg: UserConfig{
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ AccessToken: "supersecret2",
+ Authenticator: &pat.PATAuth{AccessToken: "supersecret2"},
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ UserAgentEntry: "partner-name",
+ Catalog: "default",
+ Schema: "system",
+ SessionParams: map[string]string{"ANSI_MODE": "true"},
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
+ },
+ wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
+ wantErr: false,
+ },
+ {
+ name: "client id no secret",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientId=client_id&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
+ {
+ name: "client secret no id",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
+ {
+ name: "authType Pat, no accessToken",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=pat&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
+ {
+ name: "authType m2m, no id",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
+ {
+ name: "authType m2m, no secret",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&clientId=client_id&client_secret=&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
+ {
+ name: "authType unknown with client id/secret",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientId=client_id&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{
+ Protocol: "https",
+ Host: "example.cloud.databricks.com",
+ Port: 8000,
+ Authenticator: m2m.NewAuthenticator("client_id", "client_secret", "example.cloud.databricks.com"),
+ HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a",
+ QueryTimeout: 100 * time.Second,
+ MaxRows: 1000,
+ UserAgentEntry: "partner-name",
+ Catalog: "default",
+ Schema: "system",
+ SessionParams: map[string]string{"ANSI_MODE": "true"},
+ RetryMax: 4,
+ RetryWaitMin: 1 * time.Second,
+ RetryWaitMax: 30 * time.Second,
+ CloudFetchConfig: defCloudConfig,
+ },
+ wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a",
+ wantErr: false,
+ },
+ {
+ name: "authType m2m with accessToken",
+ args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&accessToken=supersecret2&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"},
+ wantCfg: UserConfig{},
+ wantErr: true,
+ },
}
- for _, tt := range tests {
+ for i, tt := range tests {
+ fmt.Println(i)
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDSN(tt.args.dsn)
if (err != nil) != tt.wantErr {
t.Errorf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
+
if !reflect.DeepEqual(got, tt.wantCfg) {
t.Errorf("ParseConfig() = %v, want %v", got, tt.wantCfg)
return
}
+
if err == nil {
cfg := &Config{UserConfig: got}
gotUrl, err := cfg.ToEndpointURL()