Skip to content

Commit

Permalink
Make SQLite URL parser aware of options (#481)
Browse files Browse the repository at this point in the history
This patch improves the SQLite URL parser and adds the ability
to specify conneciton options.

Also uses `_fk=true` per default to allow propagation of foreign
keys (disabled by default)

Signed-off-by: aeneasr <[email protected]>
  • Loading branch information
aeneasr authored and stanislas-m committed Nov 30, 2019
1 parent 6e21180 commit 2a2287e
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 5 deletions.
60 changes: 56 additions & 4 deletions dialect_sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package pop
import (
"fmt"
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand All @@ -14,11 +15,12 @@ import (

"github.com/gobuffalo/fizz"
"github.com/gobuffalo/fizz/translators"
_ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver
"github.com/pkg/errors"

"github.com/gobuffalo/pop/columns"
"github.com/gobuffalo/pop/internal/defaults"
"github.com/gobuffalo/pop/logging"
_ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver
"github.com/pkg/errors"
)

const nameSQLite3 = "sqlite3"
Expand All @@ -28,6 +30,7 @@ func init() {
dialectSynonyms["sqlite"] = nameSQLite3
urlParser[nameSQLite3] = urlParserSQLite3
newConnection[nameSQLite3] = newSQLite
finalizer[nameSQLite3] = finalizerSQLite
}

var _ dialect = &sqlite{}
Expand All @@ -47,7 +50,8 @@ func (m *sqlite) Details() *ConnectionDetails {
}

func (m *sqlite) URL() string {
return m.ConnectionDetails.Database + "?_busy_timeout=5000"
c := m.ConnectionDetails
return c.Database + "?" + c.OptionsString("")
}

func (m *sqlite) MigrationURL() string {
Expand Down Expand Up @@ -223,6 +227,54 @@ func newSQLite(deets *ConnectionDetails) (dialect, error) {

func urlParserSQLite3(cd *ConnectionDetails) error {
db := strings.TrimPrefix(cd.URL, "sqlite://")
cd.Database = strings.TrimPrefix(db, "sqlite3://")
db = strings.TrimPrefix(db, "sqlite3://")

dbparts := strings.Split(db, "?")
cd.Database = dbparts[0]

if len(dbparts) != 2 {
return nil
}

q, err := url.ParseQuery(dbparts[1])
if err != nil {
return errors.Wrapf(err, "unable to parse sqlite query")
}

if cd.Options == nil { // prevent panic
cd.Options = make(map[string]string)
}
for k := range q {
cd.Options[k] = q.Get(k)
}

return nil
}

func finalizerSQLite(cd *ConnectionDetails) {
defs := map[string]string{
"_busy_timeout": "5000",
}
forced := map[string]string{
"_fk": "true",
}
if cd.Options == nil { // prevent panic
cd.Options = make(map[string]string)
}

for k, v := range defs {
cd.Options[k] = defaults.String(cd.Options[k], v)
}

for k, v := range forced {
// respect user specified options but print warning!
cd.Options[k] = defaults.String(cd.Options[k], v)
if cd.Options[k] != v { // when user-defined option exists
log(logging.Warn, "IMPORTANT! '%s: %s' option is required to work properly but your current setting is '%v: %v'.", k, v, k, cd.Options[k])
log(logging.Warn, "It is highly recommended to remove '%v: %v' option from your config!", k, cd.Options[k])
} // or override with `cd.Options[k] = v`?
if cd.URL != "" && !strings.Contains(cd.URL, k+"="+v) {
log(logging.Warn, "IMPORTANT! '%s=%s' option is required to work properly. Please add it to the database URL in the config!", k, v)
} // or fix user specified url?
}
}
37 changes: 36 additions & 1 deletion dialect_sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/require"
)

var sqliteDefaultOptions = map[string]string{"_busy_timeout": "5000", "_fk": "true"}

func Test_ConnectionDetails_Finalize_SQLite_URL_Only(t *testing.T) {
r := require.New(t)

Expand All @@ -21,6 +23,20 @@ func Test_ConnectionDetails_Finalize_SQLite_URL_Only(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: N/A")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite3:///tmp/foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: sqlite3:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_OverrideOptions_URL_Only(t *testing.T) {
r := require.New(t)

cd := &ConnectionDetails{
URL: "sqlite3:///tmp/foo.db?_fk=false&foo=bar",
}
err := cd.Finalize() // calls withURL() and urlParserSQLite3(cd)
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: N/A")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite3:///tmp/foo.db?_fk=false&foo=bar")
r.EqualValues(map[string]string{"_fk": "false", "foo": "bar", "_busy_timeout": "5000"}, cd.Options, "given url: sqlite3:///tmp/foo.db?_fk=false&foo=bar")
}

func Test_ConnectionDetails_Finalize_SQLite_SynURL_Only(t *testing.T) {
Expand All @@ -33,6 +49,7 @@ func Test_ConnectionDetails_Finalize_SQLite_SynURL_Only(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: N/A")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite:///tmp/foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: sqlite3:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_Dialect_URL(t *testing.T) {
Expand All @@ -45,6 +62,7 @@ func Test_ConnectionDetails_Finalize_SQLite_Dialect_URL(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: sqlite3")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite3:///tmp/foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: sqlite3:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_Dialect_SynURL(t *testing.T) {
Expand All @@ -57,6 +75,7 @@ func Test_ConnectionDetails_Finalize_SQLite_Dialect_SynURL(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: sqlite3")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite:///tmp/foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: sqlite3:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_Synonym_URL(t *testing.T) {
Expand All @@ -69,6 +88,7 @@ func Test_ConnectionDetails_Finalize_SQLite_Synonym_URL(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: sqlite")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite3:///tmp/foo.db")
r.Equal(sqliteDefaultOptions, cd.Options, "given url: sqlite3:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_Synonym_SynURL(t *testing.T) {
Expand All @@ -81,6 +101,7 @@ func Test_ConnectionDetails_Finalize_SQLite_Synonym_SynURL(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: sqlite")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite:///tmp/foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: sqlite:///tmp/foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_Synonym_Path(t *testing.T) {
Expand All @@ -93,6 +114,20 @@ func Test_ConnectionDetails_Finalize_SQLite_Synonym_Path(t *testing.T) {
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: sqlite")
r.Equal("./foo.db", cd.Database, "given database: ./foo.db")
r.EqualValues(sqliteDefaultOptions, cd.Options, "given url: ./foo.db")
}

func Test_ConnectionDetails_Finalize_SQLite_OverrideOptions_Synonym_Path(t *testing.T) {
r := require.New(t)

cd := &ConnectionDetails{
URL: "sqlite3:///tmp/foo.db?_fk=false&foo=bar",
}
err := cd.Finalize() // calls withURL() and urlParserSQLite3(cd)
r.NoError(err)
r.Equal("sqlite3", cd.Dialect, "given dialect: N/A")
r.Equal("/tmp/foo.db", cd.Database, "given url: sqlite3:///tmp/foo.db")
r.EqualValues(map[string]string{"_fk": "false", "foo": "bar", "_busy_timeout": "5000"}, cd.Options, "given url: sqlite3:///tmp/foo.db?_fk=false&foo=bar")
}

func Test_ConnectionDetails_FinalizeOSPath(t *testing.T) {
Expand All @@ -107,5 +142,5 @@ func Test_ConnectionDetails_FinalizeOSPath(t *testing.T) {
}
r.NoError(cd.Finalize())
r.Equal("sqlite3", cd.Dialect)
r.Equal(p, cd.Database)
r.EqualValues(p, cd.Database)
}

0 comments on commit 2a2287e

Please sign in to comment.