-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# DSN | ||
|
||
A data source name (DSN) contains information about how to connect to a database in the form of a single URL string. This information makes it easy to provide connection information in a single data item without requiring multiple elements for the connection provider. This package provides parsing and handling of DSNs for database connections including both server and embedded database connections. | ||
|
||
A typical DSN for a server is something like: | ||
|
||
``` | ||
provider[+driver]://username[:password]@host:port/db?option1=value1&option2=value2 | ||
``` | ||
|
||
Whereas an embedded database usually just includes the provider and the path: | ||
|
||
``` | ||
provider:///relative/path/to/file.db | ||
provider:////absolute/path/to/file.db | ||
``` | ||
|
||
Use the `dsn.Parse` method to parse this provider so that you can pass the connection details easily into your connection manager of choice. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package dsn | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
var ( | ||
ErrCannotParseDSN = errors.New("could not parse dsn: missing provider or path") | ||
ErrCannotParseProvider = errors.New("could not parse dsn: incorrect provider") | ||
ErrCannotParsePort = errors.New("could not parse dsn: invalid port number") | ||
) | ||
|
||
// DSN (data source name) contains information about how to connect to a database. It | ||
// serves both as a mechanism to connect to the local database storage engine and as a | ||
// mechanism to connect to the server databases from external clients. A typical DSN is: | ||
// | ||
// provider[+driver]://username[:password]@host:port/db?option1=value1&option2=value2 | ||
// | ||
// This DSN provides connection to both server and embedded datbases. An embedded | ||
// database DSN needs to specify relative vs absolute paths. Ensure an extra / is | ||
// included for absolute paths to disambiguate the path and host portion. | ||
type DSN struct { | ||
Provider string // The provider indicates the database being connected to. | ||
Driver string // An additional component of the provider, separated by a + - it indicates what dirver to use. | ||
User *UserInfo // The username and password (must be URL encoded for special chars) | ||
Host string // The hostname of the database to connect to. | ||
Port uint16 // The port of the database to connect on. | ||
Path string // The path to the database (or the database name) including the directory. | ||
Options Options // Any additional connection options for the database. | ||
} | ||
|
||
// Contains user or machine login credentials. | ||
type UserInfo struct { | ||
Username string | ||
Password string | ||
} | ||
|
||
// Additional options for establishing a database connection. | ||
type Options map[string]string | ||
|
||
func Parse(dsn string) (_ *DSN, err error) { | ||
var uri *url.URL | ||
if uri, err = url.Parse(dsn); err != nil { | ||
return nil, ErrCannotParseDSN | ||
} | ||
|
||
if uri.Scheme == "" || uri.Path == "" { | ||
return nil, ErrCannotParseDSN | ||
} | ||
|
||
d := &DSN{ | ||
Host: uri.Hostname(), | ||
Path: strings.TrimPrefix(uri.Path, "/"), | ||
} | ||
|
||
scheme := strings.Split(uri.Scheme, "+") | ||
switch len(scheme) { | ||
case 1: | ||
d.Provider = scheme[0] | ||
case 2: | ||
d.Provider = scheme[0] | ||
d.Driver = scheme[1] | ||
default: | ||
return nil, ErrCannotParseProvider | ||
} | ||
|
||
if user := uri.User; user != nil { | ||
d.User = &UserInfo{ | ||
Username: user.Username(), | ||
} | ||
d.User.Password, _ = user.Password() | ||
} | ||
|
||
if port := uri.Port(); port != "" { | ||
var pnum uint64 | ||
if pnum, err = strconv.ParseUint(port, 10, 16); err != nil { | ||
return nil, ErrCannotParsePort | ||
} | ||
d.Port = uint16(pnum) | ||
} | ||
|
||
if params := uri.Query(); len(params) > 0 { | ||
d.Options = make(Options, len(params)) | ||
for key := range params { | ||
d.Options[key] = params.Get(key) | ||
} | ||
} | ||
|
||
return d, nil | ||
} | ||
|
||
func (d *DSN) String() string { | ||
u := &url.URL{ | ||
Scheme: d.scheme(), | ||
User: d.userinfo(), | ||
Host: d.hostport(), | ||
Path: d.Path, | ||
RawQuery: d.rawquery(), | ||
} | ||
|
||
if d.Host == "" { | ||
u.Path = "/" + d.Path | ||
} | ||
|
||
return u.String() | ||
} | ||
|
||
func (d *DSN) scheme() string { | ||
switch { | ||
case d.Provider != "" && d.Driver != "": | ||
return d.Provider + "+" + d.Driver | ||
case d.Provider != "": | ||
return d.Provider | ||
case d.Driver != "": | ||
return d.Driver | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
func (d *DSN) hostport() string { | ||
if d.Port != 0 { | ||
return fmt.Sprintf("%s:%d", d.Host, d.Port) | ||
} | ||
return d.Host | ||
} | ||
|
||
func (d *DSN) userinfo() *url.Userinfo { | ||
if d.User != nil { | ||
if d.User.Password != "" { | ||
return url.UserPassword(d.User.Username, d.User.Password) | ||
} | ||
if d.User.Username != "" { | ||
return url.User(d.User.Username) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (d *DSN) rawquery() string { | ||
if len(d.Options) > 0 { | ||
query := make(url.Values) | ||
for key, val := range d.Options { | ||
query.Add(key, val) | ||
} | ||
return query.Encode() | ||
} | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package dsn_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"go.rtnl.ai/x/assert" | ||
"go.rtnl.ai/x/dsn" | ||
) | ||
|
||
func TestParse(t *testing.T) { | ||
t.Run("Valid", func(t *testing.T) { | ||
testCases := []struct { | ||
uri string | ||
expected *dsn.DSN | ||
}{ | ||
{ | ||
"sqlite3:///path/to/test.db", | ||
&dsn.DSN{Provider: "sqlite3", Path: "path/to/test.db"}, | ||
}, | ||
{ | ||
"sqlite3:////absolute/path/test.db", | ||
&dsn.DSN{Provider: "sqlite3", Path: "/absolute/path/test.db"}, | ||
}, | ||
{ | ||
"leveldb:///path/to/db", | ||
&dsn.DSN{Provider: "leveldb", Path: "path/to/db"}, | ||
}, | ||
{ | ||
"leveldb:////absolute/path/db", | ||
&dsn.DSN{Provider: "leveldb", Path: "/absolute/path/db"}, | ||
}, | ||
{ | ||
"postgresql://janedoe:mypassword@localhost:5432/mydb?schema=sample", | ||
&dsn.DSN{Provider: "postgresql", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 5432, Path: "mydb", Options: dsn.Options{"schema": "sample"}}, | ||
}, | ||
{ | ||
"postgresql+psycopg2://janedoe:mypassword@localhost:5432/mydb?schema=sample", | ||
&dsn.DSN{Provider: "postgresql", Driver: "psycopg2", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 5432, Path: "mydb", Options: dsn.Options{"schema": "sample"}}, | ||
}, | ||
{ | ||
"mysql://janedoe:mypassword@localhost:3306/mydb", | ||
&dsn.DSN{Provider: "mysql", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 3306, Path: "mydb"}, | ||
}, | ||
{ | ||
"mysql+odbc://janedoe:mypassword@localhost:3306/mydb", | ||
&dsn.DSN{Provider: "mysql", Driver: "odbc", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 3306, Path: "mydb"}, | ||
}, | ||
{ | ||
"cockroachdb+postgresql://janedoe:mypassword@localhost:26257/mydb?schema=public", | ||
&dsn.DSN{Provider: "cockroachdb", Driver: "postgresql", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 26257, Path: "mydb", Options: dsn.Options{"schema": "public"}}, | ||
}, | ||
{ | ||
"mongodb+srv://root:[email protected]/myDatabase?retryWrites=true&w=majority", | ||
&dsn.DSN{Provider: "mongodb", Driver: "srv", User: &dsn.UserInfo{Username: "root", Password: "password"}, Host: "cluster0.ab1cd.mongodb.net", Path: "myDatabase", Options: dsn.Options{"retryWrites": "true", "w": "majority"}}, | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
actual, err := dsn.Parse(tc.uri) | ||
assert.Ok(t, err, "test case %d failed", i) | ||
assert.Equal(t, tc.expected, actual, "test case %d failed", i) | ||
} | ||
|
||
}) | ||
|
||
t.Run("Invalid", func(t *testing.T) { | ||
testCases := []struct { | ||
uri string | ||
err error | ||
}{ | ||
{"", dsn.ErrCannotParseDSN}, | ||
{"sqlite3://", dsn.ErrCannotParseDSN}, | ||
{"postgresql://jdoe:<mypassword>@localhost:foo/mydb", dsn.ErrCannotParseDSN}, | ||
{"postgresql://localhost:foo/mydb", dsn.ErrCannotParseDSN}, | ||
{"mysql+odbc+sand://jdoe:mypassword@localhost:3306/mydb", dsn.ErrCannotParseProvider}, | ||
{"postgresql://jdoe:mypassword@localhost:656656/mydb", dsn.ErrCannotParsePort}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
_, err := dsn.Parse(tc.uri) | ||
assert.ErrorIs(t, err, tc.err, "test case %d failed", i) | ||
} | ||
}) | ||
} | ||
|
||
func TestString(t *testing.T) { | ||
testCases := []struct { | ||
expected string | ||
uri *dsn.DSN | ||
}{ | ||
{ | ||
"sqlite3:///path/to/test.db", | ||
&dsn.DSN{Provider: "sqlite3", Path: "path/to/test.db"}, | ||
}, | ||
{ | ||
"sqlite3:////absolute/path/test.db", | ||
&dsn.DSN{Provider: "sqlite3", Path: "/absolute/path/test.db"}, | ||
}, | ||
{ | ||
"leveldb:///path/to/db", | ||
&dsn.DSN{Provider: "leveldb", Path: "path/to/db"}, | ||
}, | ||
{ | ||
"leveldb:////absolute/path/db", | ||
&dsn.DSN{Provider: "leveldb", Path: "/absolute/path/db"}, | ||
}, | ||
{ | ||
"postgresql://localhost:5432/mydb?schema=sample", | ||
&dsn.DSN{Provider: "postgresql", Host: "localhost", Port: 5432, Path: "mydb", Options: dsn.Options{"schema": "sample"}}, | ||
}, | ||
{ | ||
"postgresql+psycopg2://janedoe:mypassword@localhost:5432/mydb?schema=sample", | ||
&dsn.DSN{Provider: "postgresql", Driver: "psycopg2", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 5432, Path: "mydb", Options: dsn.Options{"schema": "sample"}}, | ||
}, | ||
{ | ||
"mysql://janedoe:mypassword@localhost:3306/mydb", | ||
&dsn.DSN{Provider: "mysql", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 3306, Path: "mydb"}, | ||
}, | ||
{ | ||
"mysql+odbc://janedoe@localhost:3306/mydb", | ||
&dsn.DSN{Provider: "mysql", Driver: "odbc", User: &dsn.UserInfo{Username: "janedoe"}, Host: "localhost", Port: 3306, Path: "mydb"}, | ||
}, | ||
{ | ||
"cockroachdb+postgresql://janedoe:mypassword@localhost:26257/mydb?schema=public", | ||
&dsn.DSN{Provider: "cockroachdb", Driver: "postgresql", User: &dsn.UserInfo{Username: "janedoe", Password: "mypassword"}, Host: "localhost", Port: 26257, Path: "mydb", Options: dsn.Options{"schema": "public"}}, | ||
}, | ||
{ | ||
"mongodb+srv://root:[email protected]/myDatabase?retryWrites=true&w=majority", | ||
&dsn.DSN{Provider: "mongodb", Driver: "srv", User: &dsn.UserInfo{Username: "root", Password: "password"}, Host: "cluster0.ab1cd.mongodb.net", Path: "myDatabase", Options: dsn.Options{"retryWrites": "true", "w": "majority"}}, | ||
}, | ||
{ | ||
"cockroachdb://localhost:26257/mydb", | ||
&dsn.DSN{Driver: "cockroachdb", Host: "localhost", Port: 26257, Path: "mydb"}, | ||
}, | ||
{ | ||
"//localhost:26257/mydb", | ||
&dsn.DSN{Host: "localhost", Port: 26257, Path: "mydb"}, | ||
}, | ||
{ | ||
"/mydb", | ||
&dsn.DSN{Path: "mydb"}, | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
actual := tc.uri.String() | ||
assert.Equal(t, tc.expected, actual, "test case %d failed", i) | ||
} | ||
} |