Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislas-m committed Dec 1, 2019
2 parents af91c62 + d81c752 commit 1d70017
Show file tree
Hide file tree
Showing 83 changed files with 359 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ bin/*
gin-bin
*.sqlite
tsoda
migrations/schema.sql
testdata/migrations/schema.sql
.grifter/
vendor/
go.mod
Expand Down
8 changes: 0 additions & 8 deletions Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion associations/has_many_association.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (a *hasManyAssociation) AfterSetup() error {
fval.Set(reflect.ValueOf(ownerID))
}
} else {
return fmt.Errorf("could not set '%s' in '%s'", ownerID, fval)
return fmt.Errorf("could not set field '%s' in table '%s' to value '%s' for 'has_many' relation", a.ownerName+"ID", a.tableName, ownerID)
}
}
return nil
Expand Down
6 changes: 3 additions & 3 deletions azure-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ steps:
workingDirectory: "$(modulePath)"
displayName: "Install soda"
- script: |
$(GOBIN)/soda drop -e $(SODA_DIALECT)
$(GOBIN)/soda create -e $(SODA_DIALECT)
$(GOBIN)/soda migrate -e $(SODA_DIALECT)
$(GOBIN)/soda drop -e $(SODA_DIALECT) -p ./testdata/migrations
$(GOBIN)/soda create -e $(SODA_DIALECT) -p ./testdata/migrations
$(GOBIN)/soda migrate -e $(SODA_DIALECT) -p ./testdata/migrations
workingDirectory: "$(modulePath)"
displayName: "Create DB & run migrations"
- script: |
Expand Down
14 changes: 14 additions & 0 deletions callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ func (m *Model) beforeDestroy(c *Connection) error {
return nil
}

// BeforeValidateable callback will be called before a record is
// validated during
// ValidateAndCreate, ValidateAndUpdate, or ValidateAndSave
type BeforeValidateable interface {
BeforeValidate(*Connection) error
}

func (m *Model) beforeValidate(c *Connection) error {
if x, ok := m.Value.(BeforeValidateable); ok {
return x.BeforeValidate(c)
}
return nil
}

// AfterDestroyable callback will be called after a record is
// destroyed in the database.
type AfterDestroyable interface {
Expand Down
5 changes: 5 additions & 0 deletions callbacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func Test_Callbacks(t *testing.T) {
BeforeC: "BC",
BeforeU: "BU",
BeforeD: "BD",
BeforeV: "BV",
AfterS: "AS",
AfterC: "AC",
AfterU: "AU",
Expand Down Expand Up @@ -50,6 +51,10 @@ func Test_Callbacks(t *testing.T) {
r.Equal("BeforeDestroy", user.BeforeD)
r.Equal("AfterDestroy", user.AfterD)

verrs, err := tx.ValidateAndSave(user)
r.False(verrs.HasAny())
r.NoError(err)
r.Equal("BeforeValidate", user.BeforeV)
})
}

Expand Down
30 changes: 30 additions & 0 deletions dialect_mariadb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pop

import (
"github.com/gobuffalo/fizz"
"github.com/gobuffalo/fizz/translators"
)

const nameMariaDB = "mariadb"

func init() {
AvailableDialects = append(AvailableDialects, nameMariaDB)
urlParser[nameMariaDB] = urlParserMySQL
finalizer[nameMariaDB] = finalizerMySQL
newConnection[nameMariaDB] = newMySQL
}

var _ dialect = &mariaDB{}

type mariaDB struct {
mysql
}

func (m *mariaDB) Name() string {
return nameMariaDB
}

func (m *mariaDB) FizzTranslator() fizz.Translator {
t := translators.NewMariaDB(m.URL(), m.Details().Database)
return t
}
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)
}
11 changes: 10 additions & 1 deletion executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func (q *Query) ExecWithCount() (int, error) {
// If model is a slice, each item of the slice is validated then saved in the database.
func (c *Connection) ValidateAndSave(model interface{}, excludeColumns ...string) (*validate.Errors, error) {
sm := &Model{Value: model}
if err := sm.beforeValidate(c); err != nil {
return nil, err
}
verrs, err := sm.validateSave(c)
if err != nil {
return verrs, err
Expand Down Expand Up @@ -93,6 +96,9 @@ func (c *Connection) Save(model interface{}, excludeColumns ...string) error {
// If model is a slice, each item of the slice is validated then created in the database.
func (c *Connection) ValidateAndCreate(model interface{}, excludeColumns ...string) (*validate.Errors, error) {
sm := &Model{Value: model}
if err := sm.beforeValidate(c); err != nil {
return nil, err
}
verrs, err := sm.validateCreate(c)
if err != nil {
return verrs, err
Expand Down Expand Up @@ -313,6 +319,9 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error {
// If model is a slice, each item of the slice is validated then updated in the database.
func (c *Connection) ValidateAndUpdate(model interface{}, excludeColumns ...string) (*validate.Errors, error) {
sm := &Model{Value: model}
if err := sm.beforeValidate(c); err != nil {
return nil, err
}
verrs, err := sm.validateUpdate(c)
if err != nil {
return verrs, err
Expand Down Expand Up @@ -364,7 +373,7 @@ func (c *Connection) Update(model interface{}, excludeColumns ...string) error {

// UpdateColumns writes changes from an entry to the database, including only the given columns
// or all columns if no column names are provided.
// It updates the `updated_at` column automatically.
// It updates the `updated_at` column automatically if you include `updated_at` in columnNames.
//
// If model is a slice, each item of the slice is updated in the database.
func (c *Connection) UpdateColumns(model interface{}, columnNames ...string) error {
Expand Down
23 changes: 23 additions & 0 deletions executors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,29 @@ func Test_UpdateColumns(t *testing.T) {
})
}

func Test_UpdateColumns_UpdatedAt(t *testing.T) {
if PDB == nil {
t.Skip("skipping integration tests")
}
transaction(func(tx *Connection) {
r := require.New(t)

user := User{Name: nulls.NewString("Foo")}
tx.Create(&user)

r.NotZero(user.CreatedAt)
r.NotZero(user.UpdatedAt)
updatedAtBefore := user.UpdatedAt

user.Name.String = "Bar"
err := tx.UpdateColumns(&user, "name", "updated_at") // Update name and updated_at
r.NoError(err)

r.NoError(tx.Reload(&user))
r.NotEqual(user.UpdatedAt, updatedAtBefore) // UpdatedAt should be updated automatically
})
}

func Test_UpdateColumns_MultipleColumns(t *testing.T) {
if PDB == nil {
t.Skip("skipping integration tests")
Expand Down

0 comments on commit 1d70017

Please sign in to comment.