This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new f83b9b57 feat(arrow/flight/flightsql/driver): accept grpc+tcp, 
grpc+tls and grpc+unix schemes (#1420)
f83b9b57 is described below

commit f83b9b5796e406ea40a35ff4f26c865be0998194
Author: Nirajan Sah <[email protected]>
AuthorDate: Mon Oct 26 23:31:24 2026 +0545

    feat(arrow/flight/flightsql/driver): accept grpc+tcp, grpc+tls and 
grpc+unix schemes (#1420)
    
    ### Rationale for this change
    
    The FlightSQL `database/sql` driver only accepts `flightsql://` DSNs.
    pyarrow, C++, Java and ADBC use `grpc+tcp://`, `grpc+tls://` and
    `grpc+unix://`. Closes #57.
    
    ### What changes are included in this PR?
    
    - `grpc+tcp://host:port` behaves like `flightsql://`.
    - `grpc+tls://host:port` enables TLS by default; `tls=<name>` still
    selects a registered config, and `tls=false`/`disabled` is an error.
    - `grpc+unix:///path` connects to a Unix domain socket (address
    `unix:///path`).
    - `DSN()` emits `grpc+unix://` for socket addresses; other output is
    unchanged.
    - README documents the schemes.
    
    ### Are these changes tested?
    
    Yes. New table cases in `config_test.go` for parsing, errors and
    `DSN()`, and `TestUnixSocketConnection` runs a query against the SQLite
    example server over a Unix socket. All fail on main with `invalid
    scheme`.
    
    - `go test ./arrow/flight/flightsql/driver/ ./arrow/flight/flightsql/
    -count=1 -race`
    - `golangci-lint run` in `arrow/`: 0 issues
    
    ### Are there any user-facing changes?
    
    Yes, three new accepted DSN schemes. Existing `flightsql://` DSNs behave
    the same.
---
 arrow/flight/flightsql/driver/README.md      |  8 ++++
 arrow/flight/flightsql/driver/config.go      | 38 +++++++++++----
 arrow/flight/flightsql/driver/config_test.go | 69 ++++++++++++++++++++++++++++
 arrow/flight/flightsql/driver/driver_test.go | 38 +++++++++++++++
 4 files changed, 145 insertions(+), 8 deletions(-)

diff --git a/arrow/flight/flightsql/driver/README.md 
b/arrow/flight/flightsql/driver/README.md
index 5bd51300..a4bd419c 100644
--- a/arrow/flight/flightsql/driver/README.md
+++ b/arrow/flight/flightsql/driver/README.md
@@ -88,6 +88,14 @@ backend as GRPC Basic-Auth headers. If your backend requires 
a token based
 authentication, please use a `token` parameter (see
 [common parameters](#common-parameters) below).
 
+For consistency with other Flight SQL clients, the following schemes are
+accepted as well:
+
+* `grpc+tcp://` is the same as `flightsql://`.
+* `grpc+tls://` enables TLS by default, see the [`tls` parameter](#tls).
+* `grpc+unix:///path/to/socket` connects to a Unix domain socket at the given
+  absolute path, e.g. `grpc+unix://user:password@/tmp/flightsql.sock`.
+
 **Please note**: All parameters are case-sensitive!
 
 Alternatively to specifying the DSN directly you can use the `DriverConfig`
diff --git a/arrow/flight/flightsql/driver/config.go 
b/arrow/flight/flightsql/driver/config.go
index 9f1d56a3..a6f8c450 100644
--- a/arrow/flight/flightsql/driver/config.go
+++ b/arrow/flight/flightsql/driver/config.go
@@ -17,8 +17,10 @@ package driver
 
 import (
        "crypto/tls"
+       "errors"
        "fmt"
        "net/url"
+       "strings"
        "sync"
        "time"
 
@@ -86,12 +88,23 @@ func NewDriverConfigFromDSN(dsn string) (*DriverConfig, 
error) {
        }
 
        // Sanity checks on the given connection string
-       if u.Scheme != "flightsql" {
+       address := u.Host
+       switch u.Scheme {
+       case "flightsql", "grpc+tcp", "grpc+tls":
+               if u.Path != "" {
+                       return nil, fmt.Errorf("unexpected path %q", u.Path)
+               }
+       case "grpc+unix":
+               if u.Host != "" {
+                       return nil, fmt.Errorf("unexpected host %q", u.Host)
+               }
+               if u.Path == "" {
+                       return nil, errors.New("missing socket path")
+               }
+               address = "unix://" + u.Path
+       default:
                return nil, fmt.Errorf("invalid scheme %q", u.Scheme)
        }
-       if u.Path != "" {
-               return nil, fmt.Errorf("unexpected path %q", u.Path)
-       }
 
        // Extract the settings
        var username, password string
@@ -103,10 +116,11 @@ func NewDriverConfigFromDSN(dsn string) (*DriverConfig, 
error) {
        }
 
        config := &DriverConfig{
-               Address:  u.Host,
-               Username: username,
-               Password: password,
-               Params:   make(map[string]string),
+               Address:    address,
+               Username:   username,
+               Password:   password,
+               Params:     make(map[string]string),
+               TLSEnabled: u.Scheme == "grpc+tls",
        }
 
        // Determine the parameters
@@ -133,6 +147,9 @@ func NewDriverConfigFromDSN(dsn string) (*DriverConfig, 
error) {
                        case "true", "enabled":
                                config.TLSEnabled = true
                        case "false", "disabled":
+                               if u.Scheme == "grpc+tls" {
+                                       return nil, fmt.Errorf("tls=%s 
conflicts with scheme %q", v, u.Scheme)
+                               }
                                config.TLSEnabled = false
                        default:
                                config.TLSEnabled = true
@@ -156,6 +173,11 @@ func (config *DriverConfig) DSN() string {
                Scheme: "flightsql",
                Host:   config.Address,
        }
+       if path, found := strings.CutPrefix(config.Address, "unix://"); found {
+               u.Scheme = "grpc+unix"
+               u.Host = ""
+               u.Path = path
+       }
        if config.Username != "" {
                if config.Password == "" {
                        u.User = url.User(config.Username)
diff --git a/arrow/flight/flightsql/driver/config_test.go 
b/arrow/flight/flightsql/driver/config_test.go
index f68fb47e..1e98bd6e 100644
--- a/arrow/flight/flightsql/driver/config_test.go
+++ b/arrow/flight/flightsql/driver/config_test.go
@@ -100,6 +100,26 @@ func TestConfigFromDSNInvalid(t *testing.T) {
                        dsn:      "flightsql://127.0.0.1:12345?tls=mycfg",
                        expected: "TLS entry not registered",
                },
+               {
+                       name:     "unknown scheme",
+                       dsn:      "grpc+foo://127.0.0.1:12345",
+                       expected: "invalid scheme",
+               },
+               {
+                       name:     "TLS disabled with grpc+tls",
+                       dsn:      "grpc+tls://127.0.0.1:12345?tls=disabled",
+                       expected: "conflicts with scheme",
+               },
+               {
+                       name:     "unix socket with host",
+                       dsn:      "grpc+unix://localhost/tmp/flightsql.sock",
+                       expected: "unexpected host",
+               },
+               {
+                       name:     "unix socket without path",
+                       dsn:      "grpc+unix://",
+                       expected: "missing socket path",
+               },
        }
 
        for _, tt := range testcases {
@@ -238,6 +258,45 @@ func TestConfigFromDSN(t *testing.T) {
                                Params:        make(map[string]string),
                        },
                },
+               {
+                       name: "grpc+tcp scheme",
+                       dsn:  "grpc+tcp://127.0.0.1:12345",
+                       expected: &driver.DriverConfig{
+                               Address: "127.0.0.1:12345",
+                               Params:  make(map[string]string),
+                       },
+               },
+               {
+                       name: "grpc+tls scheme",
+                       dsn:  "grpc+tls://127.0.0.1:12345",
+                       expected: &driver.DriverConfig{
+                               Address:    "127.0.0.1:12345",
+                               TLSEnabled: true,
+                               Params:     make(map[string]string),
+                       },
+               },
+               {
+                       name: "grpc+tls scheme with custom config",
+                       dsn:  "grpc+tls://127.0.0.1:12345?tls=mycfg",
+                       expected: &driver.DriverConfig{
+                               Address:       "127.0.0.1:12345",
+                               TLSEnabled:    true,
+                               TLSConfigName: "mycfg",
+                               TLSConfig:     tlscfg,
+                               Params:        make(map[string]string),
+                       },
+               },
+               {
+                       name: "grpc+unix scheme",
+                       dsn:  
"grpc+unix://peter:parker@/tmp/flightsql.sock?timeout=1s",
+                       expected: &driver.DriverConfig{
+                               Address:  "unix:///tmp/flightsql.sock",
+                               Username: "peter",
+                               Password: "parker",
+                               Timeout:  time.Second,
+                               Params:   make(map[string]string),
+                       },
+               },
        }
 
        for _, tt := range testcases {
@@ -369,6 +428,16 @@ func TestDSNFromConfig(t *testing.T) {
                                Params: make(map[string]string),
                        },
                },
+               {
+                       name:     "unix socket",
+                       expected: 
"grpc+unix://peter@/tmp/flightsql.sock?timeout=1s",
+                       drvcfg: &driver.DriverConfig{
+                               Address:  "unix:///tmp/flightsql.sock",
+                               Username: "peter",
+                               Timeout:  time.Second,
+                               Params:   make(map[string]string),
+                       },
+               },
        }
 
        for _, tt := range testcases {
diff --git a/arrow/flight/flightsql/driver/driver_test.go 
b/arrow/flight/flightsql/driver/driver_test.go
index bfa9c34d..0f19e325 100644
--- a/arrow/flight/flightsql/driver/driver_test.go
+++ b/arrow/flight/flightsql/driver/driver_test.go
@@ -25,7 +25,10 @@ import (
        "errors"
        "fmt"
        "math/rand"
+       "net"
        "os"
+       "path/filepath"
+       "runtime"
        "strings"
        "sync"
        "sync/atomic"
@@ -1688,6 +1691,41 @@ func TestSqliteBackend(t *testing.T) {
        suite.Run(t, s)
 }
 
+func TestUnixSocketConnection(t *testing.T) {
+       if runtime.GOOS == "windows" {
+               t.Skip("windows socket paths cannot be used as a URL path")
+       }
+
+       // keep the socket path short, macOS limits it to 104 bytes
+       dir, err := os.MkdirTemp("", "flightsql")
+       require.NoError(t, err)
+       defer os.RemoveAll(dir)
+       sock := filepath.Join(dir, "server.sock")
+
+       lis, err := net.Listen("unix", sock)
+       require.NoError(t, err)
+
+       db, err := sql.Open("sqlite", ":memory:")
+       require.NoError(t, err)
+       defer db.Close()
+       sqliteServer, err := example.NewSQLiteFlightSQLServer(db)
+       require.NoError(t, err)
+
+       server := flight.NewServerWithMiddleware(nil)
+       server.RegisterFlightService(flightsql.NewFlightServer(sqliteServer))
+       server.InitListener(lis)
+       go server.Serve()
+       defer server.Shutdown()
+
+       client, err := sql.Open("flightsql", "grpc+unix://"+sock+"?timeout=5s")
+       require.NoError(t, err)
+       defer client.Close()
+
+       var value int64
+       require.NoError(t, client.QueryRow("SELECT 42").Scan(&value))
+       require.EqualValues(t, 42, value)
+}
+
 func TestPreparedStatementSchema(t *testing.T) {
        // Setup the expected test
        backend := &MockServer{

Reply via email to