Skip to content

Commit

Permalink
General cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
kenshaw committed Sep 3, 2021
1 parent 3c07ba6 commit 07c85de
Show file tree
Hide file tree
Showing 12 changed files with 735 additions and 712 deletions.
8 changes: 1 addition & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.17.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Packages
run: |
sudo apt-get -qq update
sudo apt-get install -y netcat-openbsd
- name: Install Go
uses: actions/setup-go@v2
with:
Expand All @@ -20,6 +16,4 @@ jobs:
uses: actions/checkout@v2
- name: Test
run: |
sudo mkdir -p /var/run/postgresql /var/run/mysqld
sudo nc -lkU /var/run/mysqld/mysqld.sock &> /dev/null &
CGO_ENABLED=0 go test -v ./...
35 changes: 17 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@ driver.
[discord]: https://discord.gg/yJKEzc7prt (Discord Discussion)
[discord-status]: https://img.shields.io/discord/829150509658013727.svg?label=Discord&logo=Discord&colorB=7289da&style=flat-square (Discord Discussion)


## Database Connection URL Overview

Supported database connection URLs are of the form:

```
protocol+transport://user:pass@host/dbname?opt1=a&opt2=b
protocol:/path/to/file
```text
protocol+transport://user:pass@host/dbname?opt1=a&opt2=b
protocol:/path/to/file
```

Where:
Expand Down Expand Up @@ -85,19 +84,19 @@ if err != nil { /* ... */ }
The following are example database connection URLs that can be handled by
[`dburl.Parse`][goref-parse] and [`dburl.Open`][goref-open]:

```
postgres://user:pass@localhost/dbname
pg://user:pass@localhost/dbname?sslmode=disable
mysql://user:pass@localhost/dbname
mysql:/var/run/mysqld/mysqld.sock
sqlserver://user:pass@remote-host.com/dbname
mssql://user:pass@remote-host.com/instance/dbname
ms://user:pass@remote-host.com:port/instance/dbname?keepAlive=10
oracle://user:pass@somehost.com/sid
sap://user:pass@localhost/dbname
sqlite:/path/to/file.db
file:myfile.sqlite3?loc=auto
odbc+postgres://user:pass@localhost:port/dbname?option1=
```text
postgres://user:pass@localhost/dbname
pg://user:pass@localhost/dbname?sslmode=disable
mysql://user:pass@localhost/dbname
mysql:/var/run/mysqld/mysqld.sock
sqlserver://user:pass@remote-host.com/dbname
mssql://user:pass@remote-host.com/instance/dbname
ms://user:pass@remote-host.com:port/instance/dbname?keepAlive=10
oracle://user:pass@somehost.com/sid
sap://user:pass@localhost/dbname
sqlite:/path/to/file.db
file:myfile.sqlite3?loc=auto
odbc+postgres://user:pass@localhost:port/dbname?option1=
```

## Protocol Schemes and Aliases
Expand Down Expand Up @@ -271,7 +270,7 @@ func main() {
`dburl` was built primarily to support these projects:

* [usql][usql] - a universal command-line interface for SQL databases
* [xo][xo] - a command-line tool to generate Go code from a database schema
* [xo][xo] - a command-line tool to generate code for SQL databases

[go-project]: https://golang.org/project
[goref-open]: https://pkg.go.dev/github.com/xo/dburl#Open
Expand Down
230 changes: 215 additions & 15 deletions dburl.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,227 @@
//
// Related Projects
//
// This package was written mainly to support xo (https://github.com/xo/xo)
// and usql (https://github.com/xo/usql).
// This package was written mainly to support these projects:
//
// https://github.com/xo/usql - a universal command-line interface for SQL databases
// https://github.com/xo/xo - a command-line tool to generate code for SQL databases
//
package dburl

import (
"database/sql"
"net/url"
"strings"
)

// Error is a dburl error.
// Open takes a URL like "protocol+transport://user:pass@host/dbname?option1=a&option2=b"
// and opens a standard sql.DB connection.
//
// See Parse for information on formatting URLs to work properly with Open.
func Open(urlstr string) (*sql.DB, error) {
u, err := Parse(urlstr)
if err != nil {
return nil, err
}
return sql.Open(u.Driver, u.DSN)
}

// URL wraps the standard net/url.URL type, adding OriginalScheme, Transport,
// Driver, and DSN strings.
type URL struct {
// URL is the base net/url/URL.
url.URL
// OriginalScheme is the original parsed scheme (ie, "sq", "mysql+unix", "sap", etc).
OriginalScheme string
// Transport is the specified transport protocol (ie, "tcp", "udp",
// "unix", ...), if provided.
Transport string
// Driver is the non-aliased SQL driver name that should be used in a call
// to sql/Open.
Driver string
// Unaliased is the unaliased driver name.
Unaliased string
// DSN is the built connection "data source name" that can be used in a
// call to sql/Open.
DSN string
// hostPortDB will be set by Gen*() funcs after determining the host, port,
// database.
//
// when empty, indicates that these values are not special, and can be
// retrieved as the host, port, and path[1:] as usual.
hostPortDB []string
}

// Parse parses a URL, similar to the standard url.Parse.
//
// Handles parsing OriginalScheme, Transport, Driver, Unaliased, and DSN
// fields.
//
// Note: if the URL has a Opaque component (ie, URLs not specified as
// "scheme://" but "scheme:"), and the database scheme does not support opaque
// components, then Parse will attempt to re-process the URL as
// "scheme://<opaque>".
func Parse(urlstr string) (*URL, error) {
// parse url
v, err := url.Parse(urlstr)
if err != nil {
return nil, err
}
if v.Scheme == "" {
return nil, ErrInvalidDatabaseScheme
}
// create url
u := &URL{
URL: *v,
OriginalScheme: urlstr[:len(v.Scheme)],
Transport: "tcp",
}
// check for +transport in scheme
var checkTransport bool
if i := strings.IndexRune(u.Scheme, '+'); i != -1 {
u.Transport = urlstr[i+1 : len(v.Scheme)]
u.Scheme = u.Scheme[:i]
checkTransport = true
}
// get dsn generator
scheme, ok := schemeMap[u.Scheme]
if !ok {
return nil, ErrUnknownDatabaseScheme
}
// if scheme does not understand opaque URLs, retry parsing after making a fully
// qualified URL
if !scheme.Opaque && u.Opaque != "" {
var q string
if u.RawQuery != "" {
q = "?" + u.RawQuery
}
var f string
if u.Fragment != "" {
f = "#" + u.Fragment
}
return Parse(u.OriginalScheme + "://" + u.Opaque + q + f)
}
if scheme.Opaque && u.Opaque == "" {
// force Opaque
u.Opaque, u.Host, u.Path, u.RawPath = u.Host+u.Path, "", "", ""
} else if u.Host == "." || (u.Host == "" && strings.TrimPrefix(u.Path, "/") != "") {
// force unix proto
u.Transport = "unix"
}
// check proto
if checkTransport || u.Transport != "tcp" {
if scheme.Transport == TransportNone {
return nil, ErrInvalidTransportProtocol
}
switch {
case scheme.Transport&TransportAny != 0 && u.Transport != "",
scheme.Transport&TransportTCP != 0 && u.Transport == "tcp",
scheme.Transport&TransportUDP != 0 && u.Transport == "udp",
scheme.Transport&TransportUnix != 0 && u.Transport == "unix":
default:
return nil, ErrInvalidTransportProtocol
}
}
// set driver
u.Driver, u.Unaliased = scheme.Driver, scheme.Driver
if scheme.Override != "" {
u.Driver = scheme.Override
}
// generate dsn
if u.DSN, err = scheme.Generator(u); err != nil {
return nil, err
}
return u, nil
}

// String satisfies the stringer interface.
func (u *URL) String() string {
p := &url.URL{
Scheme: u.OriginalScheme,
Opaque: u.Opaque,
User: u.User,
Host: u.Host,
Path: u.Path,
RawPath: u.RawPath,
RawQuery: u.RawQuery,
Fragment: u.Fragment,
}
return p.String()
}

// Short provides a short description of the user, host, and database.
func (u *URL) Short() string {
if u.Scheme == "" {
return ""
}
s := schemeMap[u.Scheme].Aliases[0]
if u.Scheme == "odbc" || u.Scheme == "oleodbc" {
n := u.Transport
if v, ok := schemeMap[n]; ok {
n = v.Aliases[0]
}
s += "+" + n
} else if u.Transport != "tcp" {
s += "+" + u.Transport
}
s += ":"
if u.User != nil {
if n := u.User.Username(); n != "" {
s += n + "@"
}
}
if u.Host != "" {
s += u.Host
}
if u.Path != "" && u.Path != "/" {
s += u.Path
}
if u.Opaque != "" {
s += u.Opaque
}
return s
}

// Normalize returns the driver, host, port, database, and user name of a URL,
// joined with sep, populating blank fields with empty.
func (u *URL) Normalize(sep, empty string, cut int) string {
s := []string{u.Unaliased, "", "", "", ""}
if u.Transport != "tcp" && u.Transport != "unix" {
s[0] += "+" + u.Transport
}
// set host port dbname fields
if u.hostPortDB == nil {
if u.Opaque != "" {
u.hostPortDB = []string{u.Opaque, "", ""}
} else {
u.hostPortDB = []string{u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/")}
}
}
copy(s[1:], u.hostPortDB)
// set user
if u.User != nil {
s[4] = u.User.Username()
}
// replace blank entries ...
for i := 0; i < len(s); i++ {
if s[i] == "" {
s[i] = empty
}
}
if cut > 0 {
// cut to only populated fields
i := len(s) - 1
for ; i > cut; i-- {
if s[i] != "" {
break
}
}
s = s[:i]
}
return strings.Join(s, sep)
}

// Error is an error.
type Error string

// Error satisfies the error interface.
Expand All @@ -227,15 +439,3 @@ const (
// ErrMissingUser is the missing user error.
ErrMissingUser Error = "missing user"
)

// Open takes a urlstr like "protocol+transport://user:pass@host/dbname?option1=a&option2=b"
// and creates a standard sql.DB connection.
//
// See Parse for information on formatting URLs to work properly with Open.
func Open(urlstr string) (*sql.DB, error) {
u, err := Parse(urlstr)
if err != nil {
return nil, err
}
return sql.Open(u.Driver, u.DSN)
}
33 changes: 28 additions & 5 deletions dburl_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
package dburl

import (
"io/fs"
"os"
"testing"
"time"
)

type stat fs.FileMode

func (mode stat) Name() string { return "" }
func (mode stat) Size() int64 { return 1 }
func (mode stat) Mode() fs.FileMode { return fs.FileMode(mode) }
func (mode stat) ModTime() time.Time { return time.Now() }
func (mode stat) IsDir() bool { return fs.FileMode(mode)&fs.ModeDir != 0 }
func (mode stat) Sys() interface{} { return nil }

func init() {
Stat = func(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/postgresql":
return stat(fs.ModeDir), nil
case "/var/run/mysqld/mysqld.sock":
return stat(fs.ModeSocket), nil
}
return nil, fs.ErrNotExist
}
}

func TestBadParse(t *testing.T) {
tests := []struct {
s string
Expand Down Expand Up @@ -51,11 +74,11 @@ func TestBadParse(t *testing.T) {
for i, test := range tests {
_, err := Parse(test.s)
if err == nil {
t.Errorf("test %d expected error parsing `%s`, got: nil", i, test.s)
t.Errorf("test %d expected error parsing %q", i, test.s)
continue
}
if err != test.exp {
t.Errorf("test %d expected error parsing `%s`: `%v`, got: `%v`", i, test.s, test.exp, err)
t.Errorf("test %d expected error parsing %q: expected: %v got: %v", i, test.s, test.exp, err)
}
}
}
Expand Down Expand Up @@ -176,14 +199,14 @@ func TestParse(t *testing.T) {
continue
}
if u.Driver != test.d {
t.Errorf("test %d expected driver `%s`, got: `%s`", i, test.d, u.Driver)
t.Errorf("test %d expected driver %q, got: %q", i, test.d, u.Driver)
}
if u.DSN != test.exp {
_, err := os.Stat(test.path)
if test.path != "" && err != nil && os.IsNotExist(err) {
t.Logf("test %d expected DSN `%s`, got: `%s` -- ignoring because `%s` does not exist", i, test.exp, u.DSN, test.path)
t.Logf("test %d expected dsn %q, got: %q -- ignoring because `%s` does not exist", i, test.exp, u.DSN, test.path)
} else {
t.Errorf("test %d expected DSN `%s`, got: `%s`", i, test.exp, u.DSN)
t.Errorf("test %d expected dsn %q, got: %q", i, test.exp, u.DSN)
}
}
}
Expand Down
Loading

0 comments on commit 07c85de

Please sign in to comment.