# Databricks Isv Go SQL Driver

> PWAF-compliant Databricks SQL Driver for Go (databricks-sql-go): PAT, OAuth M2M, OAuth U2M (browser + token-env + custom OAuth app PKCE), WithUserAgentEntry. Use when building or testing integrations that run SQL queries via a Databricks SQL warehouse.

- Skill: `databricks-solutions/databricks-isv-go-sql-driver` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add databricks-solutions/databricks-isv-go-sql-driver`
- Raw SKILL.md: https://api.skillmd.com/api/skills/databricks-solutions/databricks-isv-go-sql-driver/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: databricks-solutions (https://skillmd.com/u/databricks-solutions)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/databricks-solutions/databricks-isv-go-sql-driver

---


<!-- skill-version: 1.0.0 -->

# Databricks SQL Driver for Go (ISV)

Use this skill when implementing or testing **Databricks SQL Driver for Go** (`databricks-sql-go`) integrations for PWAF-compliant SQL query execution via a Databricks SQL warehouse.

## PWAF Documentation Links

| Resource | URL |
|----------|-----|
| **PWAF Home** | https://databrickslabs.github.io/partner-architecture/ |
| **Authentication Best Practices** | https://databrickslabs.github.io/partner-architecture/isv-partners/lakehouse-patterns/access-auth/ |
| **OAuth M2M Guide** | https://databrickslabs.github.io/partner-architecture/isv-partners/lakehouse-patterns/access-auth/oauth-m2m |
| **OAuth U2M Guide** | https://databrickslabs.github.io/partner-architecture/isv-partners/lakehouse-patterns/access-auth/oauth-u2m |
| **User-Agent (SQL Drivers)** | https://databrickslabs.github.io/partner-architecture/isv-partners/telemetry-attribution/sql-drivers |
| **Databricks SQL Driver for Go** | https://docs.databricks.com/aws/en/dev-tools/go-sql-driver |
| **GitHub** | https://github.com/databricks/databricks-sql-go |

## Requirements

- **Driver:** `github.com/databricks/databricks-sql-go` v1.5.2+
- **Go:** 1.20+
- **SQL Warehouse:** Required (`DATABRICKS_HTTP_PATH`)
- **Install:** `go get github.com/databricks/databricks-sql-go@latest`

## Authentication Comparison

| Method | PWAF Status | Auto Token Refresh | Browser Required | Use Case |
|--------|-------------|-------------------|------------------|----------|
| PAT | ⚠️ Limited | No | No | Testing only |
| OAuth M2M | ✅ Recommended | Yes (driver-native) | No | Production/automated |
| U2M Custom OAuth | ✅ Recommended (ISV) | No | Yes | Interactive (custom app) |
| U2M Token-Env | ✅ Supported | No | No | Headless/CI |

**ISV Note:** For user-interactive flows, use **U2M Custom OAuth** with your registered OAuth app (Account Console → App connections). This provides custom branding, audit trails, and scoped permissions. The driver's built-in U2M browser auth uses a generic `databricks-sql-connector` app which is not appropriate for ISV applications.

---

## Host Normalization Helper

The driver's `WithServerHostname` expects a **bare hostname** (no `https://`):

```go
import "strings"

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}
```

---

## Environment Variables Reference

| Variable | Required For | Description |
|----------|-------------|-------------|
| `DATABRICKS_HOST` | All | Workspace URL (e.g., `https://myworkspace.cloud.databricks.com`) |
| `DATABRICKS_HTTP_PATH` | All | SQL warehouse HTTP path (e.g., `/sql/1.0/warehouses/abc123`) |
| `DATABRICKS_TOKEN` | PAT | Personal access token |
| `DATABRICKS_CLIENT_ID` | OAuth M2M | Service principal UUID |
| `DATABRICKS_CLIENT_SECRET` | OAuth M2M | Service principal OAuth secret |
| `DATABRICKS_U2M_CLIENT_ID` | U2M Custom OAuth | Custom OAuth app client ID from App connections |
| `DATABRICKS_U2M_CLIENT_SECRET` | U2M Custom OAuth (optional) | Custom OAuth app client secret |
| `DATABRICKS_REDIRECT_URI` | U2M Custom OAuth (optional) | Custom redirect URI (default: `http://localhost:8040/callback`) |
| `DATABRICKS_ACCESS_TOKEN` | U2M Token-Env | Pre-obtained OAuth access token |
| `DATABRICKS_AUTH_TYPE` | Multi-auth | Auth type selector: `pat`, `oauth_m2m`, `u2m_browser`, `u2m_custom_oauth`, `u2m_token_env` |

**Important:** Do not mix M2M and U2M environment variables. Use `env -i` for clean test environments.

---

## Complete Examples

### PAT Authentication (Testing Only)

```go
package main

import (
    "database/sql"
    "fmt"
    "os"
    "strings"

    dbsql "github.com/databricks/databricks-sql-go"
)

func main() {
    host := os.Getenv("DATABRICKS_HOST")
    httpPath := os.Getenv("DATABRICKS_HTTP_PATH")
    token := os.Getenv("DATABRICKS_TOKEN")

    connector, err := dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(token),
        dbsql.WithUserAgentEntry("YourCompany_YourProduct/1.0.0"),
    )
    if err != nil {
        panic(err)
    }

    db := sql.OpenDB(connector)
    defer db.Close()

    rows, err := db.Query("SELECT COUNT(*) FROM samples.nyctaxi.trips")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var count int64
    rows.Next()
    rows.Scan(&count)
    fmt.Printf("PAT OK: %d trips\n", count)
}

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}
```

**Env vars:** `DATABRICKS_HOST`, `DATABRICKS_HTTP_PATH`, `DATABRICKS_TOKEN`

---

### OAuth M2M Authentication (Production Recommended)

Uses the driver's **built-in M2M authenticator** that handles token fetch and refresh:

```go
package main

import (
    "database/sql"
    "fmt"
    "os"
    "strings"

    dbsql "github.com/databricks/databricks-sql-go"
    "github.com/databricks/databricks-sql-go/auth/oauth/m2m"
)

func main() {
    host := os.Getenv("DATABRICKS_HOST")
    httpPath := os.Getenv("DATABRICKS_HTTP_PATH")
    clientID := os.Getenv("DATABRICKS_CLIENT_ID")
    clientSecret := os.Getenv("DATABRICKS_CLIENT_SECRET")

    authenticator := m2m.NewAuthenticator(clientID, clientSecret, serverHostname(host))

    connector, err := dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAuthenticator(authenticator),
        dbsql.WithUserAgentEntry("YourCompany_YourProduct/1.0.0"),
    )
    if err != nil {
        panic(err)
    }

    db := sql.OpenDB(connector)
    defer db.Close()

    rows, err := db.Query("SELECT COUNT(*) FROM samples.nyctaxi.trips")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var count int64
    rows.Next()
    rows.Scan(&count)
    fmt.Printf("OAuth M2M OK: %d trips\n", count)
}

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}
```

**Env vars:** `DATABRICKS_HOST`, `DATABRICKS_HTTP_PATH`, `DATABRICKS_CLIENT_ID`, `DATABRICKS_CLIENT_SECRET`

**Setup:** Create service principal in Account Console → Settings → Service principals. Generate OAuth secret. Grant `CAN_USE` on the SQL warehouse.

---

### U2M Browser Authentication (Driver-Native)

Uses the driver's **built-in U2M authenticator** that opens the user's default browser:

```go
package main

import (
    "database/sql"
    "fmt"
    "os"
    "strings"
    "time"

    dbsql "github.com/databricks/databricks-sql-go"
    "github.com/databricks/databricks-sql-go/auth/oauth/u2m"
)

func main() {
    host := os.Getenv("DATABRICKS_HOST")
    httpPath := os.Getenv("DATABRICKS_HTTP_PATH")

    authenticator, err := u2m.NewAuthenticator(serverHostname(host), 2*time.Minute)
    if err != nil {
        panic(err)
    }

    connector, err := dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAuthenticator(authenticator),
        dbsql.WithUserAgentEntry("YourCompany_YourProduct/1.0.0"),
    )
    if err != nil {
        panic(err)
    }

    db := sql.OpenDB(connector)
    defer db.Close()

    rows, err := db.Query("SELECT COUNT(*) FROM samples.nyctaxi.trips")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var count int64
    rows.Next()
    rows.Scan(&count)
    fmt.Printf("U2M Browser OK: %d trips\n", count)
}

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}
```

**Env vars:** `DATABRICKS_HOST`, `DATABRICKS_HTTP_PATH`

**Notes:**
- Uses Databricks' built-in OAuth app (client_id=`databricks-sql-connector`)
- Do NOT set `DATABRICKS_CLIENT_ID` or `DATABRICKS_TOKEN`
- Driver picks an available localhost port at runtime

---

### U2M Token-Env Authentication (Headless/CI)

Pass a pre-obtained OAuth access token:

```go
package main

import (
    "database/sql"
    "fmt"
    "os"
    "strings"

    dbsql "github.com/databricks/databricks-sql-go"
)

func main() {
    host := os.Getenv("DATABRICKS_HOST")
    httpPath := os.Getenv("DATABRICKS_HTTP_PATH")
    
    token := os.Getenv("DATABRICKS_ACCESS_TOKEN")
    if token == "" {
        token = os.Getenv("DATABRICKS_TOKEN")
    }

    connector, err := dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(token),
        dbsql.WithUserAgentEntry("YourCompany_YourProduct/1.0.0"),
    )
    if err != nil {
        panic(err)
    }

    db := sql.OpenDB(connector)
    defer db.Close()

    rows, err := db.Query("SELECT COUNT(*) FROM samples.nyctaxi.trips")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var count int64
    rows.Next()
    rows.Scan(&count)
    fmt.Printf("U2M Token-Env OK: %d trips\n", count)
}

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}
```

**Env vars:** `DATABRICKS_HOST`, `DATABRICKS_HTTP_PATH`, `DATABRICKS_ACCESS_TOKEN` (or `DATABRICKS_TOKEN`)

---

### U2M Custom OAuth App (PKCE) - Interactive

For user-interactive flows with a custom OAuth app:

```go
package main

import (
    "context"
    "crypto/rand"
    "crypto/sha256"
    "database/sql"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "os/exec"
    "runtime"
    "strings"

    dbsql "github.com/databricks/databricks-sql-go"
)

func main() {
    host := os.Getenv("DATABRICKS_HOST")
    httpPath := os.Getenv("DATABRICKS_HTTP_PATH")
    clientID := os.Getenv("DATABRICKS_U2M_CLIENT_ID")
    clientSecret := os.Getenv("DATABRICKS_U2M_CLIENT_SECRET")
    redirectURI := os.Getenv("DATABRICKS_REDIRECT_URI")
    if redirectURI == "" {
        redirectURI = "http://localhost:8040/callback"
    }

    token, err := runPKCEFlow(host, clientID, clientSecret, redirectURI)
    if err != nil {
        panic(err)
    }

    connector, err := dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(token),
        dbsql.WithUserAgentEntry("YourCompany_YourProduct/1.0.0"),
    )
    if err != nil {
        panic(err)
    }

    db := sql.OpenDB(connector)
    defer db.Close()

    rows, err := db.Query("SELECT COUNT(*) FROM samples.nyctaxi.trips")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var count int64
    rows.Next()
    rows.Scan(&count)
    fmt.Printf("U2M Custom OAuth OK: %d trips\n", count)
}

func runPKCEFlow(host, clientID, clientSecret, redirectURI string) (string, error) {
    verifierBytes := make([]byte, 32)
    rand.Read(verifierBytes)
    codeVerifier := base64.RawURLEncoding.EncodeToString(verifierBytes)
    
    hash := sha256.Sum256([]byte(codeVerifier))
    codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
    
    stateBytes := make([]byte, 32)
    rand.Read(stateBytes)
    state := base64.RawURLEncoding.EncodeToString(stateBytes)

    redirectURL, _ := url.Parse(redirectURI)
    port := redirectURL.Port()
    if port == "" {
        port = "8040"
    }

    hostNorm := serverHostname(host)
    authURL := fmt.Sprintf("https://%s/oidc/v1/authorize?"+
        "client_id=%s&redirect_uri=%s&response_type=code&scope=all-apis&"+
        "code_challenge=%s&code_challenge_method=S256&state=%s",
        hostNorm, clientID, url.QueryEscape(redirectURI), codeChallenge, state)

    codeChan := make(chan string, 1)
    errChan := make(chan error, 1)

    server := &http.Server{Addr: ":" + port}
    http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Query().Get("state") != state {
            errChan <- fmt.Errorf("state mismatch")
            return
        }
        code := r.URL.Query().Get("code")
        if code == "" {
            errChan <- fmt.Errorf("no code in callback")
            return
        }
        w.Header().Set("Content-Type", "text/html")
        w.Write([]byte(`<html><body style="font-family:sans-serif;text-align:center;padding:50px;">
            <h1 style="color:green;">✓ Authentication Successful</h1>
            <p>You can close this tab.</p></body></html>`))
        codeChan <- code
    })

    go server.ListenAndServe()
    defer server.Shutdown(context.Background())

    openBrowser(authURL)
    fmt.Println("Waiting for authentication in browser...")

    var code string
    select {
    case code = <-codeChan:
    case err := <-errChan:
        return "", err
    }

    tokenURL := fmt.Sprintf("https://%s/oidc/v1/token", hostNorm)
    data := url.Values{
        "grant_type":    {"authorization_code"},
        "code":          {code},
        "redirect_uri":  {redirectURI},
        "code_verifier": {codeVerifier},
        "client_id":     {clientID},
    }
    if clientSecret != "" {
        data.Set("client_secret", clientSecret)
    }

    resp, err := http.PostForm(tokenURL, data)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    var result struct {
        AccessToken string `json:"access_token"`
        Error       string `json:"error"`
    }
    json.NewDecoder(resp.Body).Decode(&result)
    if result.Error != "" {
        return "", fmt.Errorf("token error: %s", result.Error)
    }
    return result.AccessToken, nil
}

func serverHostname(host string) string {
    s := strings.TrimPrefix(host, "https://")
    s = strings.TrimPrefix(s, "http://")
    if idx := strings.Index(s, "/"); idx >= 0 {
        s = s[:idx]
    }
    return s
}

func openBrowser(url string) {
    switch runtime.GOOS {
    case "darwin":
        exec.Command("open", url).Start()
    case "linux":
        exec.Command("xdg-open", url).Start()
    case "windows":
        exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
    }
}
```

**Env vars:** `DATABRICKS_HOST`, `DATABRICKS_HTTP_PATH`, `DATABRICKS_U2M_CLIENT_ID`, optional `DATABRICKS_U2M_CLIENT_SECRET`, `DATABRICKS_REDIRECT_URI`

**Setup:** Create OAuth app in Account Console → Settings → App connections. Add redirect URI `http://localhost:8040/callback`.

---

## Environment Variables Reference

| Variable | Used By | Description |
|----------|---------|-------------|
| `DATABRICKS_HOST` | All | Workspace URL |
| `DATABRICKS_HTTP_PATH` | All | SQL Warehouse path (e.g., `/sql/1.0/warehouses/<id>`) |
| `DATABRICKS_TOKEN` | PAT, U2M token-env | Personal access token or pre-obtained OAuth token |
| `DATABRICKS_CLIENT_ID` | OAuth M2M | Service principal UUID |
| `DATABRICKS_CLIENT_SECRET` | OAuth M2M | Service principal OAuth secret |
| `DATABRICKS_U2M_CLIENT_ID` | U2M Custom OAuth | Custom OAuth app client ID |
| `DATABRICKS_U2M_CLIENT_SECRET` | U2M Custom OAuth | Custom OAuth app secret (if confidential) |
| `DATABRICKS_REDIRECT_URI` | U2M Custom OAuth | Redirect URI (default: `http://localhost:8040/callback`) |

## Auth Isolation

Run tests with a **clean environment** using `env -i`:

```bash
env -i PATH=$PATH HOME=$HOME \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  DATABRICKS_CLIENT_ID=$DATABRICKS_CLIENT_ID \
  DATABRICKS_CLIENT_SECRET=$DATABRICKS_CLIENT_SECRET \
  ./your_binary
```

## Validation Query

Verify User-Agent telemetry is being recorded correctly:

```sql
SELECT event_time, user_agent, action_name, request_params
FROM system.access.audit
WHERE event_time > current_timestamp() - INTERVAL 1 HOUR
  AND lower(user_agent) LIKE '%yourcompany%'
ORDER BY event_time DESC
LIMIT 10;
```

---

## Required Imports

Quick reference for imports by auth type:

```go
// All examples
import (
    "database/sql"
    "fmt"
    "os"
    "strings"
    
    dbsql "github.com/databricks/databricks-sql-go"
)

// OAuth M2M - add:
import "github.com/databricks/databricks-sql-go/auth/oauth/m2m"

// U2M Browser - add:
import (
    "time"
    "github.com/databricks/databricks-sql-go/auth/oauth/u2m"
)

// U2M Custom OAuth App (PKCE) - add:
import (
    "context"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "net/url"
    "os/exec"
    "runtime"
)

// Multi-auth - add:
import "database/sql/driver"
```

---

## Error Handling Patterns

### Basic Error Handling

```go
import (
    "database/sql"
    "errors"
    "strings"
)

func executeQuery(db *sql.DB, query string) error {
    rows, err := db.Query(query)
    if err != nil {
        return handleSQLError(err)
    }
    defer rows.Close()
    // process rows...
    return nil
}

func handleSQLError(err error) error {
    errMsg := err.Error()
    
    switch {
    case strings.Contains(errMsg, "401"):
        return fmt.Errorf("authentication failed: check credentials")
    case strings.Contains(errMsg, "403"):
        return fmt.Errorf("permission denied: check service principal access")
    case strings.Contains(errMsg, "503"):
        return fmt.Errorf("warehouse unavailable: may be starting up")
    case strings.Contains(errMsg, "429"):
        return fmt.Errorf("rate limited: implement backoff")
    case strings.Contains(errMsg, "invalid httpPath"):
        return fmt.Errorf("invalid HTTP path: check DATABRICKS_HTTP_PATH")
    default:
        return err
    }
}
```

### Retry Pattern with Exponential Backoff

```go
import (
    "context"
    "database/sql"
    "strings"
    "time"
)

// RetryConfig holds retry behavior settings
type RetryConfig struct {
    MaxRetries     int
    InitialBackoff time.Duration
    MaxBackoff     time.Duration
    Multiplier     float64
}

// DefaultRetryConfig returns production-recommended settings
func DefaultRetryConfig() RetryConfig {
    return RetryConfig{
        MaxRetries:     3,
        InitialBackoff: 2 * time.Second,
        MaxBackoff:     30 * time.Second,
        Multiplier:     2.0,
    }
}

func QueryWithRetry(ctx context.Context, db *sql.DB, cfg RetryConfig, query string) (*sql.Rows, error) {
    var lastErr error
    backoff := cfg.InitialBackoff
    
    for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
        rows, err := db.QueryContext(ctx, query)
        if err == nil {
            return rows, nil
        }
        lastErr = err
        
        if !isRetryable(err) || attempt >= cfg.MaxRetries {
            return nil, err
        }
        
        fmt.Printf("[Retry %d/%d] Waiting %v. Error: %v\n", attempt+1, cfg.MaxRetries, backoff, err)
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(backoff):
            backoff = time.Duration(float64(backoff) * cfg.Multiplier)
            if backoff > cfg.MaxBackoff {
                backoff = cfg.MaxBackoff
            }
        }
    }
    return nil, lastErr
}

func isRetryable(err error) bool {
    msg := err.Error()
    // Retryable: server errors, rate limits, transient failures
    if strings.Contains(msg, "503") || strings.Contains(msg, "429") ||
       strings.Contains(msg, "500") || strings.Contains(msg, "502") ||
       strings.Contains(msg, "connection reset") ||
       strings.Contains(msg, "TEMPORARILY_UNAVAILABLE") {
        return true
    }
    // Non-retryable: auth, permissions, syntax errors
    if strings.Contains(msg, "401") || strings.Contains(msg, "403") ||
       strings.Contains(msg, "AnalysisException") || strings.Contains(msg, "ParseException") {
        return false
    }
    return false
}

// Usage:
// cfg := DefaultRetryConfig()
// rows, err := QueryWithRetry(ctx, db, cfg, "SELECT * FROM table")
```

---

## Troubleshooting

### Common Errors

| Error | Cause | Solution |
|-------|-------|----------|
| `dial tcp: connection refused` | Network/host issue | Verify `DATABRICKS_HOST` URL |
| `invalid httpPath` | Missing warehouse path | Set `DATABRICKS_HTTP_PATH=/sql/1.0/warehouses/<id>` |
| `401 Unauthorized` | Invalid credentials | Check token/secret |
| `403 PERMISSION_DENIED` | SP lacks permission | Grant `CAN_USE` on SQL warehouse to service principal |
| `context deadline exceeded` | Query timeout | Increase context timeout or check warehouse status |
| `503 Service Unavailable` | Warehouse starting | Wait and retry with backoff |

### OAuth-Specific Errors

| Error | Cause | Solution |
|-------|-------|----------|
| `OAuth application not available` | Account not enabled | Enable OAuth in account console |
| `redirect_uri mismatch` | URI not registered | Add redirect URI in App connections |
| `invalid_client` | Wrong client ID/secret | Verify credentials match registered OAuth app |
| `invalid_grant` | Auth code expired/reused | Auth codes are single-use; restart OAuth flow |

### U2M Browser Issues

| Symptom | Cause | Solution |
|---------|-------|----------|
| Browser doesn't open | Missing env vars | Pass `HOME`, `USER`, `DISPLAY` via `env -i` |
| `localhost refused to connect` | Stale browser tab | **Normal** if auth succeeded; server shuts down after token exchange |
| Port 8040 in use | Another process | Set `DATABRICKS_REDIRECT_URI=http://localhost:8050/callback` |
| Browser hangs after login | Callback not reached | Check redirect URI matches exactly; try different port |
| `authentication timed out` | User didn't complete login | Retry; ensure browser can reach workspace |

### Host Normalization Issues

| Symptom | Cause | Solution |
|---------|-------|----------|
| `no such host` | `https://` prefix passed to `WithServerHostname` | Use `serverHostname()` helper to strip scheme |
| SSL errors | Incorrect port or scheme | Ensure `WithPort(443)` and bare hostname |

## Multi-Auth Pattern

Support multiple auth types in a single binary using `DATABRICKS_AUTH_TYPE`:

```go
authType := os.Getenv("DATABRICKS_AUTH_TYPE")
if authType == "" {
    authType = "pat"
}

var connector driver.Connector
var err error

switch authType {
case "pat":
    connector, err = dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(os.Getenv("DATABRICKS_TOKEN")),
        dbsql.WithUserAgentEntry(userAgent),
    )
case "oauth_m2m", "m2m":
    auth := m2m.NewAuthenticator(
        os.Getenv("DATABRICKS_CLIENT_ID"),
        os.Getenv("DATABRICKS_CLIENT_SECRET"),
        serverHostname(host),
    )
    connector, err = dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAuthenticator(auth),
        dbsql.WithUserAgentEntry(userAgent),
    )
case "u2m_browser", "u2m":
    auth, err := u2m.NewAuthenticator(serverHostname(host), 2*time.Minute)
    if err != nil {
        panic(err)
    }
    connector, err = dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAuthenticator(auth),
        dbsql.WithUserAgentEntry(userAgent),
    )
case "u2m_token_env":
    token := os.Getenv("DATABRICKS_ACCESS_TOKEN")
    if token == "" {
        token = os.Getenv("DATABRICKS_TOKEN")
    }
    connector, err = dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(token),
        dbsql.WithUserAgentEntry(userAgent),
    )
case "u2m_custom_oauth_app":
    token, err := runPKCEFlow(host, clientID, clientSecret, redirectURI)
    if err != nil {
        panic(err)
    }
    connector, err = dbsql.NewConnector(
        dbsql.WithServerHostname(serverHostname(host)),
        dbsql.WithPort(443),
        dbsql.WithHTTPPath(httpPath),
        dbsql.WithAccessToken(token),
        dbsql.WithUserAgentEntry(userAgent),
    )
}
```

| `DATABRICKS_AUTH_TYPE` | Auth flow | Env vars required |
|------------------------|-----------|-------------------|
| `pat` (default) | Personal Access Token | `DATABRICKS_TOKEN` |
| `oauth_m2m` or `m2m` | OAuth M2M (client credentials) | `DATABRICKS_CLIENT_ID`, `DATABRICKS_CLIENT_SECRET` |
| `u2m_browser` or `u2m` | U2M browser sign-in | None (uses built-in OAuth app) |
| `u2m_token_env` | U2M pre-obtained token | `DATABRICKS_ACCESS_TOKEN` or `DATABRICKS_TOKEN` |
| `u2m_custom_oauth_app` | U2M custom OAuth app (PKCE) | `DATABRICKS_U2M_CLIENT_ID`, optional `DATABRICKS_U2M_CLIENT_SECRET` |

**Note:** Unlike the Databricks SDK for Go, the SQL driver does NOT read `DATABRICKS_AUTH_TYPE` internally, so you can safely use it as your app-level auth selector.

---

## U2M Browser Environment Requirements

For U2M browser flows to work, pass these environment variables when running with `env -i`:

```bash
env -i PATH=$PATH HOME=$HOME USER=$USER DISPLAY=$DISPLAY \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  ./your_binary
```

- `HOME` - Required for browser profile access
- `USER` - Required by some browsers
- `DISPLAY` - Required on Linux for GUI browser

---

## Testing with Auth Isolation

Test each auth type with a **clean environment** to avoid "more than one authorization method" conflicts:

```bash
# PAT test
env -i PATH=$PATH HOME=$HOME \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  DATABRICKS_TOKEN=$DATABRICKS_TOKEN \
  DATABRICKS_AUTH_TYPE=pat \
  ./go_sql_all_auth

# OAuth M2M test
env -i PATH=$PATH HOME=$HOME \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  DATABRICKS_CLIENT_ID=$DATABRICKS_CLIENT_ID \
  DATABRICKS_CLIENT_SECRET=$DATABRICKS_CLIENT_SECRET \
  DATABRICKS_AUTH_TYPE=oauth_m2m \
  ./go_sql_all_auth

# U2M Browser test (needs DISPLAY, USER for browser)
env -i PATH=$PATH HOME=$HOME USER=$USER DISPLAY=$DISPLAY \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  DATABRICKS_AUTH_TYPE=u2m_browser \
  ./go_sql_all_auth

# U2M Custom OAuth App test
env -i PATH=$PATH HOME=$HOME USER=$USER DISPLAY=$DISPLAY \
  DATABRICKS_HOST=$DATABRICKS_HOST \
  DATABRICKS_HTTP_PATH=$DATABRICKS_HTTP_PATH \
  DATABRICKS_U2M_CLIENT_ID=$DATABRICKS_U2M_CLIENT_ID \
  DATABRICKS_AUTH_TYPE=u2m_custom_oauth_app \
  ./go_sql_all_auth
```

---

## PKCE Flow with Timeout

Add timeout handling to the PKCE flow for better UX:

```go
func runPKCEFlow(host, clientID, clientSecret, redirectURI string) (string, error) {
    // ... setup code_verifier, code_challenge, state ...

    codeChan := make(chan string, 1)
    errChan := make(chan error, 1)

    server := &http.Server{Addr: ":" + port}
    // ... setup handler ...

    go server.ListenAndServe()
    defer server.Shutdown(context.Background())

    openBrowser(authURL)

    // Timeout for user to complete browser auth
    select {
    case code := <-codeChan:
        return exchangeCodeForToken(code, codeVerifier, clientID, clientSecret, redirectURI, host)
    case err := <-errChan:
        return "", err
    case <-time.After(2 * time.Minute):
        return "", fmt.Errorf("authentication timed out after 2 minutes")
    }
}
```

---

## Key Differences from Databricks SDK for Go

| Aspect | Databricks SQL Driver for Go | Databricks SDK for Go |
|--------|------------------------------|----------------------|
| **Purpose** | SQL queries via warehouse | REST APIs (UC, Jobs, Clusters) |
| **SQL Warehouse** | Required (`DATABRICKS_HTTP_PATH`) | Not needed |
| **Interface** | `database/sql` package | Typed Go methods |
| **Auth Config** | `dbsql.NewConnector()` options | `databricks.Config{}` struct |
| **User-Agent** | `dbsql.WithUserAgentEntry()` (per-connector) | `useragent.WithPartner()` (global) |
| **U2M Browser** | `u2m.NewAuthenticator()` (built-in) | Not built-in (use PKCE helper) |
| **M2M Authenticator** | `m2m.NewAuthenticator()` (built-in) | SDK handles automatically |
| **Auth selector env var** | Can use `DATABRICKS_AUTH_TYPE` | Must use custom (e.g., `APP_AUTH_TYPE`) |

---

## Build Tags for Multiple Examples

When organizing multiple Go example files in the same package, use **build tags** to compile each example separately. This prevents "main redeclared" errors.

### Adding Build Tags

Each example file should have a build tag at the top (before package declaration):

```go
//go:build pat_example

package main
// ... rest of file
```

### Example File Tags

| File | Build Tag |
|------|-----------|
| `pat_example.go` | `//go:build pat_example` |
| `oauth_m2m_example.go` | `//go:build oauth_m2m_example` |
| `u2m_builtin_oauth_example.go` | `//go:build u2m_builtin_oauth_example` |
| `u2m_custom_oauth_app_example.go` | `//go:build u2m_custom_oauth_app_example` |
| `u2m_token_env_example.go` | `//go:build u2m_token_env_example` |
| `go_all_auth_example.go` | `//go:build go_all_auth_example` |
| `error_handling_example.go` | `//go:build error_handling_example` |
| `retry_helper.go` | `//go:build retry_helper_example` |

### Shared Helper Files

For shared helpers (like PKCE helper) that are used by multiple examples, use OR conditions:

```go
//go:build go_all_auth_example || u2m_custom_oauth_app_example || u2m_builtin_oauth_example

package main
// ... shared helper code
```

### Building with Tags

```bash
# Build specific example
go build -tags=pat_example -o pat_example_bin .
go build -tags=oauth_m2m_example -o oauth_m2m_bin .
go build -tags=go_all_auth_example -o all_auth_bin .

# Build and run
go build -tags=u2m_custom_oauth_app_example -o u2m_bin . && ./u2m_bin
```

### Why Build Tags?

Without build tags, Go tries to compile all `.go` files in a package together, causing conflicts when multiple files define `func main()` or the same constants/variables. Build tags solve this by selectively including files during compilation.

---

## Implementation Learnings

1. **Host normalization is critical**: Always use `serverHostname()` helper to strip `https://` before passing to `WithServerHostname()`
2. **Port 443 required**: Always include `WithPort(443)` for HTTPS connections
3. **User-Agent per connector**: Unlike SDK's global setting, each connector needs `WithUserAgentEntry()`
4. **U2M browser requires env vars**: Pass `HOME`, `USER`, `DISPLAY` via `env -i` for browser to launch
5. **M2M auto-refreshes**: The `m2m.NewAuthenticator()` handles token refresh automatically
6. **Build tags prevent conflicts**: When multiple example files exist in same package, use build tags to compile individually
7. **PKCE helper is shared**: The PKCE helper file needs conditional build tags to be included with relevant examples

