Permalink
Browse files

Changeset constraint support (#21)

* use separated constraint check table

* fix insert without values

* init foreign key constraint

* add check constraint

* use Kind to differentiate errors

* added constraint definition

* wip constraints

* test for constraint get error

* added more test for transform error

* use example naming convention for test

* refactor sql.New to use function arguments

* reuse builder

* differentiate name

* rename to differentiate record

* Revert "rename to differentiate record"

This reverts commit 2699d3c.

* fix name

* fix typo

* fix repo transaction
  • Loading branch information...
Fs02 committed May 30, 2018
1 parent 60a8169 commit 45cd9066167ad4c9c148a108333b76c076db25e9
Showing with 912 additions and 544 deletions.
  1. +10 −3 adapter/mysql/mysql.go
  2. +24 −11 adapter/mysql/mysql_test.go
  3. +20 −7 adapter/postgres/postgres.go
  4. +21 −8 adapter/postgres/postgres_test.go
  5. +58 −0 adapter/specs/constraint.go
  6. +1 −2 adapter/specs/count.go
  7. +1 −2 adapter/specs/delete.go
  8. +3 −25 adapter/specs/insert.go
  9. +21 −33 adapter/specs/query.go
  10. +1 −2 adapter/specs/save.go
  11. +13 −3 adapter/specs/specs.go
  12. +4 −4 adapter/specs/transaction.go
  13. +5 −30 adapter/specs/update.go
  14. +73 −60 adapter/sql/builder_test.go
  15. +31 −24 adapter/sql/bulder.go
  16. +5 −5 adapter/sql/scan_test.go
  17. +54 −24 adapter/sql/sql.go
  18. +23 −22 adapter/sql/sql_test.go
  19. +13 −4 adapter/sqlite3/sqlite3.go
  20. +26 −11 adapter/sqlite3/sqlite3_test.go
  21. +3 −3 c/condition_test.go
  22. +1 −1 changeset/add_error.go
  23. +1 −1 changeset/apply_string_test.go
  24. +9 −9 changeset/cast_assoc_test.go
  25. +12 −12 changeset/cast_test.go
  26. +11 −5 changeset/changeset.go
  27. +3 −0 changeset/changeset_test.go
  28. +29 −0 changeset/check_constraint.go
  29. +17 −0 changeset/check_constraint_test.go
  30. +47 −0 changeset/constraint.go
  31. +59 −0 changeset/constraint_test.go
  32. +29 −0 changeset/foreign_key_constraint.go
  33. +17 −0 changeset/foreign_key_constraint_test.go
  34. +24 −0 changeset/options.go
  35. +7 −1 changeset/options_test.go
  36. +2 −2 changeset/put_change_test.go
  37. +29 −0 changeset/unique_constraint.go
  38. +17 −0 changeset/unique_constraint_test.go
  39. +2 −2 changeset/validate_exclusion_test.go
  40. +2 −2 changeset/validate_inclusion_test.go
  41. +2 −2 changeset/validate_max_test.go
  42. +2 −2 changeset/validate_min_test.go
  43. +2 −2 changeset/validate_pattern_test.go
  44. +2 −2 changeset/validate_range_test.go
  45. +2 −2 changeset/validate_regexp_test.go
  46. +1 −1 changeset/validate_required_test.go
  47. +22 −74 errors/errors.go
  48. +21 −35 errors/errors_test.go
  49. +2 −2 logger_test.go
  50. +37 −18 query.go
  51. +73 −68 query_test.go
  52. +1 −1 repo.go
  53. +17 −17 repo_test.go
@@ -34,7 +34,7 @@ var _ grimoire.Adapter = (*Adapter)(nil)
func Open(dsn string) (*Adapter, error) {
var err error

adapter := &Adapter{sql.New("?", false, errorFunc, incrementFunc)}
adapter := &Adapter{sql.New(errorFunc, incrementFunc, sql.Placeholder("?"))}
adapter.DB, err = db.Open("mysql", dsn)

return adapter, err
@@ -57,8 +57,15 @@ func incrementFunc(adapter sql.Adapter) int {
func errorFunc(err error) error {
if err == nil {
return nil
} else if e, ok := err.(*mysql.MySQLError); ok && e.Number == 1062 {
return errors.UniqueConstraintError(e.Message, internal.ExtractString(e.Message, "key '", "'"))
}

if e, ok := err.(*mysql.MySQLError); ok {
switch e.Number {
case 1062:
return errors.New(e.Message, internal.ExtractString(e.Message, "key '", "'"), errors.UniqueConstraint)
case 1452:
return errors.New(e.Message, internal.ExtractString(e.Message, "CONSTRAINT `", "`"), errors.ForeignKeyConstraint)
}
}

return err
@@ -14,21 +14,21 @@ func init() {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")

_, _, err = adapter.Exec(`DROP TABLE IF EXISTS extras;`, nil)
paranoid.Panic(err, "failed dropping extras table")
_, _, err = adapter.Exec(`DROP TABLE IF EXISTS addresses;`, nil)
paranoid.Panic(err, "failed dropping addresses table")
_, _, err = adapter.Exec(`DROP TABLE IF EXISTS users;`, nil)
paranoid.Panic(err, "failed dropping users table")

_, _, err = adapter.Exec(`CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(30) DEFAULT NULL,
name VARCHAR(30) NOT NULL,
gender VARCHAR(10) NOT NULL,
age INT NOT NULL,
note varchar(50),
created_at DATETIME,
updated_at DATETIME,
UNIQUE (slug)
updated_at DATETIME
);`, nil)
paranoid.Panic(err, "failed creating users table")

@@ -41,6 +41,16 @@ func init() {
FOREIGN KEY (user_id) REFERENCES users(id)
);`, nil)
paranoid.Panic(err, "failed creating addresses table")

_, _, err = adapter.Exec(`CREATE TABLE extras (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(30) DEFAULT NULL UNIQUE,
user_id INT UNSIGNED,
SCORE INT,
CONSTRAINT extras_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id)
);`, nil)

paranoid.Panic(err, "failed creating extras table")
}

func dsn() string {
@@ -51,7 +61,7 @@ func dsn() string {
return "root@(127.0.0.1:3306)/grimoire_test?charset=utf8&parseTime=True&loc=Local"
}

func TestSpecs(t *testing.T) {
func TestAdapter__specs(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -73,13 +83,11 @@ func TestSpecs(t *testing.T) {
specs.Insert(t, repo)
specs.InsertAll(t, repo)
specs.InsertSet(t, repo)
specs.InsertConstraint(t, repo)

// Update Specs
specs.Update(t, repo)
specs.UpdateWhere(t, repo)
specs.UpdateSet(t, repo)
specs.UpdateConstraint(t, repo)

// Put Specs
specs.SaveInsert(t, repo)
@@ -91,9 +99,14 @@ func TestSpecs(t *testing.T) {

// Transaction specs
specs.Transaction(t, repo)

// Constraint specs
// - Check constraint is not supported by mysql
specs.UniqueConstraint(t, repo)
specs.ForeignKeyConstraint(t, repo)
}

func TestAdapterInsertAllError(t *testing.T) {
func TestAdapter_InsertAll_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -109,23 +122,23 @@ func TestAdapterInsertAllError(t *testing.T) {
assert.NotNil(t, err)
}

func TestAdapterTransactionCommitError(t *testing.T) {
func TestAdapter_Transaction_commitError(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

assert.NotNil(t, adapter.Commit())
}

func TestAdapterTransactionRollbackError(t *testing.T) {
func TestAdapter_Transaction_rollbackError(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

assert.NotNil(t, adapter.Rollback())
}

func TestAdapterQueryError(t *testing.T) {
func TestAdapter_Query_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -136,7 +149,7 @@ func TestAdapterQueryError(t *testing.T) {
assert.NotNil(t, err)
}

func TestAdapterExecError(t *testing.T) {
func TestAdapter_Exec_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -22,26 +22,30 @@ import (
"github.com/lib/pq"
)

// Adapter definition for mysql database.
// Adapter definition for postgrees database.
type Adapter struct {
*sql.Adapter
}

var _ grimoire.Adapter = (*Adapter)(nil)

// Open mysql connection using dsn.
// Open postgrees connection using dsn.
func Open(dsn string) (*Adapter, error) {
var err error

adapter := &Adapter{sql.New("$", true, errorFunc, nil)}
adapter := &Adapter{sql.New(errorFunc, nil,
sql.Placeholder("$"),
sql.Ordinal(true),
sql.InsertDefaultValues(true)),
}
adapter.DB, err = db.Open("postgres", dsn)

return adapter, err
}

// Insert inserts a record to database and returns its id.
func (adapter *Adapter) Insert(query grimoire.Query, changes map[string]interface{}, loggers ...grimoire.Logger) (interface{}, error) {
statement, args := sql.NewBuilder(adapter.Placeholder, adapter.Ordinal).
statement, args := sql.NewBuilder(adapter.Placeholder, adapter.Ordinal, adapter.InsertDefaultValues).
Returning("id").
Insert(query.Collection, changes)

@@ -55,7 +59,7 @@ func (adapter *Adapter) Insert(query grimoire.Query, changes map[string]interfac

// InsertAll inserts multiple records to database and returns its ids.
func (adapter *Adapter) InsertAll(query grimoire.Query, fields []string, allchanges []map[string]interface{}, loggers ...grimoire.Logger) ([]interface{}, error) {
statement, args := sql.NewBuilder(adapter.Placeholder, adapter.Ordinal).Returning("id").InsertAll(query.Collection, fields, allchanges)
statement, args := sql.NewBuilder(adapter.Placeholder, adapter.Ordinal, adapter.InsertDefaultValues).Returning("id").InsertAll(query.Collection, fields, allchanges)

var result []struct {
ID int64
@@ -89,8 +93,17 @@ func (adapter *Adapter) Begin() (grimoire.Adapter, error) {
func errorFunc(err error) error {
if err == nil {
return nil
} else if e, ok := err.(*pq.Error); ok && e.Code == "23505" {
return errors.UniqueConstraintError(e.Message, internal.ExtractString(e.Message, "constraint \"", "\""))
}

if e, ok := err.(*pq.Error); ok {
switch e.Code {
case "23505":
return errors.New(e.Message, internal.ExtractString(e.Message, "constraint \"", "\""), errors.UniqueConstraint)
case "23503":
return errors.New(e.Message, internal.ExtractString(e.Message, "constraint \"", "\""), errors.ForeignKeyConstraint)
case "23514":
return errors.New(e.Message, internal.ExtractString(e.Message, "constraint \"", "\""), errors.CheckConstraint)
}
}

return err
@@ -15,6 +15,8 @@ func init() {
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

_, _, err = adapter.Exec(`DROP TABLE IF EXISTS extras;`, nil)
paranoid.Panic(err, "failed dropping extras table")
_, _, err = adapter.Exec(`DROP TABLE IF EXISTS addresses;`, nil)
paranoid.Panic(err, "failed dropping addresses table")
_, _, err = adapter.Exec(`DROP TABLE IF EXISTS users;`, nil)
@@ -41,6 +43,14 @@ func init() {
updated_at TIMESTAMP
);`, nil)
paranoid.Panic(err, "failed creating addresses table")

_, _, err = adapter.Exec(`CREATE TABLE extras (
id SERIAL NOT NULL PRIMARY KEY,
slug VARCHAR(30) DEFAULT NULL UNIQUE,
user_id INTEGER REFERENCES users(id),
score INTEGER DEFAULT 0 CHECK (score>=0 AND score<=100)
);`, nil)
paranoid.Panic(err, "failed creating extras table")
}

func dsn() string {
@@ -51,7 +61,7 @@ func dsn() string {
return "postgres://postgres@localhost/grimoire_test?sslmode=disable"
}

func TestSpecs(t *testing.T) {
func TestAdapter__specs(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -73,13 +83,11 @@ func TestSpecs(t *testing.T) {
specs.Insert(t, repo)
specs.InsertAll(t, repo)
specs.InsertSet(t, repo)
specs.InsertConstraint(t, repo)

// Update Specs
specs.Update(t, repo)
specs.UpdateWhere(t, repo)
specs.UpdateSet(t, repo)
specs.UpdateConstraint(t, repo)

// Put Specs
specs.SaveInsert(t, repo)
@@ -91,9 +99,14 @@ func TestSpecs(t *testing.T) {

// Transaction specs
specs.Transaction(t, repo)

// Constraint specs
specs.UniqueConstraint(t, repo)
specs.ForeignKeyConstraint(t, repo)
specs.CheckConstraint(t, repo)
}

func TestAdapterInsertAllError(t *testing.T) {
func TestAdapter_InsertAll_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -109,23 +122,23 @@ func TestAdapterInsertAllError(t *testing.T) {
assert.NotNil(t, err)
}

func TestAdapterTransactionCommitError(t *testing.T) {
func TestAdapter_Transaction_commitError(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

assert.NotNil(t, adapter.Commit())
}

func TestAdapterTransactionRollbackError(t *testing.T) {
func TestAdapter_Transaction_rollbackError(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()

assert.NotNil(t, adapter.Rollback())
}

func TestAdapterQueryError(t *testing.T) {
func TestAdapter_Query_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -136,7 +149,7 @@ func TestAdapterQueryError(t *testing.T) {
assert.NotNil(t, err)
}

func TestAdapterExecError(t *testing.T) {
func TestAdapter_Exec_error(t *testing.T) {
adapter, err := Open(dsn())
paranoid.Panic(err, "failed to open database connection")
defer adapter.Close()
@@ -0,0 +1,58 @@
package specs

import (
"testing"

"github.com/Fs02/grimoire"
"github.com/Fs02/grimoire/errors"
)

// UniqueConstraint tests unique constraint specifications.
func UniqueConstraint(t *testing.T, repo grimoire.Repo) {
extra1 := Extra{}
extra2 := Extra{}
repo.From(extras).Set("slug", "slug1").MustInsert(&extra1)
repo.From(extras).Set("slug", "slug2").MustInsert(&extra2)

t.Run("UniqueConstraint", func(t *testing.T) {
// inserting
err := repo.From(extras).Set("slug", extra1.Slug).Insert(nil)
assertConstraint(t, err, errors.UniqueConstraint, "slug")

// updating
err = repo.From(extras).Find(extra2.ID).Set("slug", extra1.Slug).Update(nil)
assertConstraint(t, err, errors.UniqueConstraint, "slug")
})
}

// ForeignKeyConstraint tests foreign key constraint specifications.
func ForeignKeyConstraint(t *testing.T, repo grimoire.Repo) {
extra := Extra{}
repo.From(extras).MustSave(&extra)

t.Run("ForeignKeyConstraint", func(t *testing.T) {
// inserting
err := repo.From(extras).Set("user_id", 1000).Insert(nil)
assertConstraint(t, err, errors.ForeignKeyConstraint, "user_id")

// updating
err = repo.From(extras).Find(extra.ID).Set("user_id", 1000).Update(nil)
assertConstraint(t, err, errors.ForeignKeyConstraint, "user_id")
})
}

// CheckConstraint tests foreign key constraint specifications.
func CheckConstraint(t *testing.T, repo grimoire.Repo) {
extra := Extra{}
repo.From(extras).MustSave(&extra)

t.Run("CheckConstraint", func(t *testing.T) {
// inserting
err := repo.From(extras).Set("score", 150).Insert(nil)
assertConstraint(t, err, errors.CheckConstraint, "score")

// updating
err = repo.From(extras).Find(extra.ID).Set("score", 150).Update(nil)
assertConstraint(t, err, errors.CheckConstraint, "score")
})
}
@@ -4,7 +4,6 @@ import (
"testing"

"github.com/Fs02/grimoire"
"github.com/Fs02/grimoire/adapter/sql"
"github.com/Fs02/grimoire/c"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +39,7 @@ func Count(t *testing.T, repo grimoire.Repo) {
}

for _, query := range tests {
statement, _ := sql.NewBuilder("?", false).Find(query.Select("COUNT(*) AS count"))
statement, _ := builder.Find(query.Select("COUNT(*) AS count"))
t.Run("Count|"+statement, func(t *testing.T) {
_, err := query.Count()
assert.Nil(t, err)
@@ -4,7 +4,6 @@ import (
"testing"

"github.com/Fs02/grimoire"
"github.com/Fs02/grimoire/adapter/sql"
"github.com/Fs02/grimoire/c"
"github.com/stretchr/testify/assert"
)
@@ -24,7 +23,7 @@ func Delete(t *testing.T, repo grimoire.Repo) {
}

for _, query := range tests {
statement, _ := sql.NewBuilder("?", false).Delete(query.Collection, query.Condition)
statement, _ := builder.Delete(query.Collection, query.Condition)
t.Run("Delete|"+statement, func(t *testing.T) {
var result []User
assert.Nil(t, query.All(&result))
Oops, something went wrong.

0 comments on commit 45cd906

Please sign in to comment.