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{