diff --git a/specs/aggregate.go b/specs/aggregate.go new file mode 100644 index 0000000..1736282 --- /dev/null +++ b/specs/aggregate.go @@ -0,0 +1,63 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +// Aggregate tests count specifications. +func Aggregate(t *testing.T, repo rel.Repository) { + // preparte tests data + var ( + user = User{Name: "name1", Gender: "male", Age: 10} + ) + + repo.MustInsert(ctx, &user) + + waitForReplication() + + tests := []rel.Query{ + rel.From("users").Where(where.Eq("id", user.ID)), + rel.From("users").Where(where.Eq("name", "name1")), + rel.From("users").Where(where.Eq("age", 10)), + rel.From("users").Where(where.Eq("id", user.ID), where.Eq("name", "name1")), + rel.From("users").Where(where.Eq("id", user.ID), where.Eq("name", "name1"), where.Eq("age", 10)), + rel.From("users").Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1")), + rel.From("users").Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1"), where.Eq("age", 10)), + rel.From("users").Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1")).OrWhere(where.Eq("age", 10)), + rel.From("users").Where(where.Ne("gender", "male")), + rel.From("users").Where(where.Gt("age", 59)), + rel.From("users").Where(where.Gte("age", 60)), + rel.From("users").Where(where.Lt("age", 11)), + rel.From("users").Where(where.Lte("age", 10)), + rel.From("users").Where(where.Nil("note")), + rel.From("users").Where(where.NotNil("name")), + rel.From("users").Where(where.In("id", 1, 2, 3)), + rel.From("users").Where(where.Nin("id", 1, 2, 3)), + rel.From("users").Where(where.Like("name", "name%")), + rel.From("users").Where(where.NotLike("name", "noname%")), + rel.From("users").Where(where.Fragment("id > 0")), + rel.From("users").Where(where.Not(where.Eq("id", 1), where.Eq("name", "name1"), where.Eq("age", 10))), + // this query is not supported. + // group query is automatically removed. + // use all instead for complex aggregation. + rel.From("users").Limit(10), + rel.From("users").Group("gender"), + rel.From("users").Group("age").Having(where.Gt("age", 10)), + } + + for _, query := range tests { + t.Run("Aggregate", func(t *testing.T) { + count, err := repo.Aggregate(ctx, query, "count", "id") + assert.Nil(t, err) + assert.NotZero(t, count) + + sum, err := repo.Aggregate(ctx, query, "sum", "id") + assert.Nil(t, err) + assert.NotZero(t, sum) + }) + } +} diff --git a/specs/constraint.go b/specs/constraint.go new file mode 100644 index 0000000..3b0522d --- /dev/null +++ b/specs/constraint.go @@ -0,0 +1,78 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" +) + +func createExtra(repo rel.Repository, slug string) Extra { + var user User + repo.MustInsert(ctx, &user) + + extra := Extra{Slug: &slug, UserID: user.ID} + repo.MustInsert(ctx, &extra) + return extra +} + +// UniqueConstraintOnInsert tests unique constraint specifications on insert. +func UniqueConstraintOnInsert(t *testing.T, repo rel.Repository) { + var ( + existing = createExtra(repo, "unique-insert") + err = repo.Insert(ctx, &Extra{Slug: existing.Slug}) + ) + + assertConstraint(t, err, rel.UniqueConstraint, "slug") +} + +// UniqueConstraintOnUpdate tests unique constraint specifications on insert. +func UniqueConstraintOnUpdate(t *testing.T, repo rel.Repository) { + var ( + record = createExtra(repo, "unique-record") + existing = createExtra(repo, "unique-update-existing") + err = repo.Update(ctx, &Extra{ID: record.ID, Slug: existing.Slug}) + ) + + assertConstraint(t, err, rel.UniqueConstraint, "slug") +} + +// ForeignKeyConstraintOnInsert tests foreign key constraint specifications on insert. +func ForeignKeyConstraintOnInsert(t *testing.T, repo rel.Repository) { + var ( + err = repo.Insert(ctx, &Extra{UserID: 1000}) + ) + + assertConstraint(t, err, rel.ForeignKeyConstraint, "user_id") +} + +// ForeignKeyConstraintOnUpdate tests foreign key constraint specifications on update. +func ForeignKeyConstraintOnUpdate(t *testing.T, repo rel.Repository) { + var ( + record = createExtra(repo, "fk-slug") + ) + + record.UserID = 1000 + err := repo.Update(ctx, &record) + assertConstraint(t, err, rel.ForeignKeyConstraint, "user_id") +} + +// CheckConstraintOnInsert tests foreign key constraint specifications on insert. +func CheckConstraintOnInsert(t *testing.T, repo rel.Repository) { + var ( + err = repo.Insert(ctx, &Extra{Score: 150}) + ) + + assertConstraint(t, err, rel.CheckConstraint, "score") +} + +// CheckConstraintOnUpdate tests foreign key constraint specifications. +func CheckConstraintOnUpdate(t *testing.T, repo rel.Repository) { + var ( + record = createExtra(repo, "check-slug") + ) + + // updating + record.Score = 150 + err := repo.Update(ctx, &record) + assertConstraint(t, err, rel.CheckConstraint, "score") +} diff --git a/specs/delete.go b/specs/delete.go new file mode 100644 index 0000000..61da29a --- /dev/null +++ b/specs/delete.go @@ -0,0 +1,171 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +// Delete tests delete specifications. +func Delete(t *testing.T, repo rel.Repository) { + var ( + address = Address{ + Name: "address", + User: User{Name: "user", Age: 100}, + } + ) + + repo.MustInsert(ctx, &address) + assert.NotEqual(t, 0, address.ID) + assert.NotEqual(t, 0, address.User.ID) + + assert.Nil(t, repo.Delete(ctx, &address)) + + waitForReplication() + + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &Address{}, where.Eq("id", address.ID))) + // not deleted because cascade disabled + assert.Nil(t, repo.Find(ctx, &User{}, where.Eq("id", address.User.ID))) +} + +// DeleteAll tests delete specifications. +func DeleteAll(t *testing.T, repo rel.Repository) { + var ( + addresses = []Address{ + {Name: "address1"}, + {Name: "address2"}, + } + ) + + repo.MustInsertAll(ctx, &addresses) + assert.NotEqual(t, 0, addresses[0].ID) + assert.NotEqual(t, 0, addresses[1].ID) + + assert.Nil(t, repo.DeleteAll(ctx, &addresses)) + + waitForReplication() + assert.Zero(t, repo.MustCount(ctx, "addresses", where.In("id", addresses[0].ID, addresses[1].ID))) +} + +// DeleteBelongsTo tests delete specifications. +func DeleteBelongsTo(t *testing.T, repo rel.Repository) { + var ( + address = Address{ + Name: "address", + User: User{Name: "user", Age: 100}, + } + ) + + repo.MustInsert(ctx, &address) + assert.NotEqual(t, 0, address.ID) + assert.NotEqual(t, 0, address.User.ID) + + assert.Nil(t, repo.Delete(ctx, &address, rel.Cascade(true))) + + waitForReplication() + + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &Address{}, where.Eq("id", address.ID))) + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &User{}, where.Eq("id", address.User.ID))) +} + +// DeleteHasOne tests delete specifications. +func DeleteHasOne(t *testing.T, repo rel.Repository) { + var ( + user = User{ + Name: "user", + Age: 100, + PrimaryAddress: &Address{Name: "primary address"}, + } + ) + + repo.MustInsert(ctx, &user) + assert.NotEqual(t, 0, user.ID) + assert.NotEqual(t, 0, user.PrimaryAddress.ID) + + assert.Nil(t, repo.Delete(ctx, &user, rel.Cascade(true))) + + waitForReplication() + + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &User{}, where.Eq("id", user.ID))) + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &Address{}, where.Eq("id", user.PrimaryAddress.ID))) +} + +// DeleteHasMany tests delete specifications. +func DeleteHasMany(t *testing.T, repo rel.Repository) { + tests := []struct { + name string + user User + }{ + { + name: "with empty has many", + user: User{ + Name: "user", + Age: 100, + Addresses: []Address{}, + }, + }, + { + name: "with non-empty has many", + user: User{ + Name: "user", + Age: 100, + Addresses: []Address{ + {Name: "address 1"}, + {Name: "address 2"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo.MustInsert(ctx, &tt.user) + assert.NotEqual(t, 0, tt.user.ID) + for _, addr := range tt.user.Addresses { + assert.NotEqual(t, 0, addr.ID) + } + + assert.Nil(t, repo.Delete(ctx, &tt.user, rel.Cascade(true))) + + waitForReplication() + + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &User{}, where.Eq("id", tt.user.ID))) + for _, addr := range tt.user.Addresses { + assert.Equal(t, rel.NotFoundError{}, repo.Find(ctx, &Address{}, where.Eq("id", addr.ID))) + } + }) + } +} + +// DeleteAny tests delete all specifications. +func DeleteAny(t *testing.T, repo rel.Repository) { + repo.MustInsert(ctx, &User{Name: "delete", Age: 100}) + repo.MustInsert(ctx, &User{Name: "delete", Age: 100}) + repo.MustInsert(ctx, &User{Name: "other delete", Age: 110}) + + waitForReplication() + + tests := []rel.Query{ + rel.From("users").Where(where.Eq("name", "delete")), + rel.From("users").Where(where.Eq("name", "other delete"), where.Gt("age", 100)), + } + + for _, query := range tests { + var result []User + t.Run("DeleteAny", func(t *testing.T) { + assert.Nil(t, repo.FindAll(ctx, &result, query)) + assert.NotEqual(t, 0, len(result)) + + deletedCount, err := repo.DeleteAny(ctx, query) + assert.Nil(t, err) + assert.NotZero(t, deletedCount) + + waitForReplication() + + assert.Nil(t, repo.FindAll(ctx, &result, query)) + assert.Zero(t, len(result)) + }) + } +} diff --git a/specs/insert.go b/specs/insert.go new file mode 100644 index 0000000..c66936b --- /dev/null +++ b/specs/insert.go @@ -0,0 +1,275 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +// Insert tests specification for database insertion. +func Insert(t *testing.T, repo rel.Repository) { + var ( + note = "swordsman" + user = User{ + Name: "insert", + Gender: "male", + Age: 23, + Note: ¬e, + } + ) + + err := repo.Insert(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "insert", user.Name) + assert.Equal(t, "male", user.Gender) + assert.Equal(t, 23, user.Age) + assert.Equal(t, ¬e, user.Note) + + waitForReplication() + + var ( + queried User + ) + + user.Addresses = nil + err = repo.Find(ctx, &queried, where.Eq("id", user.ID)) + assert.Nil(t, err) + assert.Equal(t, user, queried) +} + +// InsertHasMany tests specification insertion with has many association. +func InsertHasMany(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "insert has many", + Gender: "male", + Age: 23, + Addresses: []Address{ + {Name: "primary"}, + {Name: "work"}, + }, + } + ) + + err := repo.Insert(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "insert has many", user.Name) + assert.Equal(t, "male", user.Gender) + assert.Equal(t, 23, user.Age) + + assert.Len(t, user.Addresses, 2) + assert.NotZero(t, user.Addresses[0].ID) + assert.NotZero(t, user.Addresses[1].ID) + assert.Equal(t, user.ID, *user.Addresses[0].UserID) + assert.Equal(t, user.ID, *user.Addresses[1].UserID) + assert.Equal(t, "primary", user.Addresses[0].Name) + assert.Equal(t, "work", user.Addresses[1].Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "addresses") + + assert.Equal(t, result, user) +} + +// InsertHasOne tests specification for insertion with has one association. +func InsertHasOne(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "insert has one", + Gender: "male", + Age: 23, + PrimaryAddress: &Address{Name: "primary"}, + } + ) + + err := repo.Insert(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "insert has one", user.Name) + assert.Equal(t, "male", user.Gender) + assert.Equal(t, 23, user.Age) + + assert.NotZero(t, user.PrimaryAddress.ID) + assert.Equal(t, user.ID, *user.PrimaryAddress.UserID) + assert.Equal(t, "primary", user.PrimaryAddress.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "primary_address") + + assert.Equal(t, result, user) +} + +// InsertBelongsTo tests specification for insertion with belongs to association. +func InsertBelongsTo(t *testing.T, repo rel.Repository) { + var ( + result Address + address = Address{ + Name: "insert belongs to", + User: User{ + Name: "zoro", + Gender: "male", + Age: 23, + }, + } + ) + + err := repo.Insert(ctx, &address) + assert.Nil(t, err) + + assert.NotZero(t, address.ID) + assert.Equal(t, address.User.ID, *address.UserID) + assert.Equal(t, "insert belongs to", address.Name) + + assert.NotZero(t, address.User.ID) + assert.Equal(t, "zoro", address.User.Name) + assert.Equal(t, "male", address.User.Gender) + assert.Equal(t, 23, address.User.Age) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", address.ID)) + repo.MustPreload(ctx, &result, "user") + + assert.Equal(t, result, address) +} + +// Inserts tests insert specifications. +func Inserts(t *testing.T, repo rel.Repository) { + var ( + user User + note = "note" + ) + + repo.MustInsert(ctx, &user) + + tests := []interface{}{ + &User{}, + &User{Name: "insert", Age: 100}, + &User{Name: "insert", Age: 100, Note: ¬e}, + &User{Note: ¬e}, + &User{ID: 123, Name: "insert", Age: 100, Note: ¬e}, + &Address{}, + &Address{Name: "work"}, + &Address{UserID: &user.ID}, + &Address{Name: "work", UserID: &user.ID}, + &Address{ID: 123, Name: "work", UserID: &user.ID}, + &Composite{Primary1: 1, Primary2: 2, Data: "data-1-2"}, + } + + for _, record := range tests { + t.Run("Insert", func(t *testing.T) { + assert.Nil(t, repo.Insert(ctx, record)) + + waitForReplication() + assertRecord(t, repo, record) + }) + } +} + +func assertRecord(t *testing.T, repo rel.Repository, record interface{}) { + switch v := record.(type) { + case *User: + var found User + repo.MustFind(ctx, &found, where.Eq("id", v.ID)) + assert.Equal(t, found, *v) + case *Address: + var found Address + repo.MustFind(ctx, &found, where.Eq("id", v.ID)) + assert.Equal(t, found, *v) + case *Composite: + var found Composite + repo.MustFind(ctx, &found, where.Eq("primary1", v.Primary1).AndEq("primary2", v.Primary2)) + assert.Equal(t, found, *v) + } +} + +// InsertAll tests insert multiple specifications. +func InsertAll(t *testing.T, repo rel.Repository) { + var ( + user User + note = "note" + ) + + repo.MustInsert(ctx, &user) + + tests := []interface{}{ + &[]User{{}}, + &[]User{{Name: "insert", Age: 100}}, + &[]User{{Name: "insert", Age: 100, Note: ¬e}}, + &[]User{{Note: ¬e}}, + &[]User{{Name: "insert", Age: 100}, {Name: "insert too"}}, + &[]User{{ID: 224, Name: "insert", Age: 100}, {ID: 234, Name: "insert too"}}, + &[]Address{{}}, + &[]Address{{Name: "work"}}, + &[]Address{{UserID: &user.ID}}, + &[]Address{{Name: "work", UserID: &user.ID}}, + &[]Address{{Name: "work"}, {Name: "home"}}, + &[]Address{{ID: 233, Name: "work"}, {ID: 235, Name: "home"}}, + } + + for _, record := range tests { + t.Run("InsertAll", func(t *testing.T) { + assert.Nil(t, repo.InsertAll(ctx, record)) + + waitForReplication() + assertRecords(t, repo, record) + }) + } +} + +// InsertAllPartialCustomPrimary tests insert multiple specifications. +func InsertAllPartialCustomPrimary(t *testing.T, repo rel.Repository) { + tests := []interface{}{ + &[]User{{ID: 300, Name: "insert 300", Age: 100}, {Name: "insert 300+?"}}, + &[]User{{Name: "insert 305-?", Age: 100}, {ID: 305, Name: "insert 305+?"}}, + &[]User{{Name: "insert 310-?"}, {ID: 310, Name: "insert 310", Age: 100}, {Name: "insert 300+?"}}, + } + + for _, record := range tests { + t.Run("InsertAll", func(t *testing.T) { + assert.Nil(t, repo.InsertAll(ctx, record)) + + waitForReplication() + assertRecords(t, repo, record) + }) + } +} + +func assertRecords(t *testing.T, repo rel.Repository, records interface{}) { + switch v := records.(type) { + case *[]User: + var ( + found []User + ids = make([]int, len(*v)) + ) + + for i := range *v { + ids[i] = int((*v)[i].ID) + } + + repo.MustFindAll(ctx, &found, where.InInt("id", ids)) + assert.Equal(t, found, *v) + case *[]Address: + var ( + found []Address + ids = make([]int, len(*v)) + ) + + for i := range *v { + ids[i] = int((*v)[i].ID) + } + + repo.MustFindAll(ctx, &found, where.InInt("id", ids)) + assert.Equal(t, found, *v) + } +} diff --git a/specs/migration.go b/specs/migration.go new file mode 100644 index 0000000..f93eb79 --- /dev/null +++ b/specs/migration.go @@ -0,0 +1,257 @@ +package specs + +import ( + "time" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/migrator" +) + +var m migrator.Migrator + +// Setup database for specs execution. +func Setup(repo rel.Repository) func() { + m = migrator.New(repo) + m.Register(1, + func(schema *rel.Schema) { + schema.CreateTable("users", func(t *rel.Table) { + t.ID("id") + t.String("slug", rel.Limit(30)) + t.String("name", rel.Limit(30), rel.Default("")) + t.String("gender", rel.Limit(10), rel.Default("")) + t.Int("age", rel.Required(true), rel.Default(0)) + t.String("note", rel.Limit(50)) + t.DateTime("created_at") + t.DateTime("updated_at") + }) + + schema.CreateUniqueIndex("users", "unique_slug", []string{"slug"}) + }, + func(schema *rel.Schema) { + schema.DropIndex("users", "unique_slug") + schema.DropTable("users") + }, + ) + + m.Register(2, + func(schema *rel.Schema) { + schema.CreateTable("addresses", func(t *rel.Table) { + t.ID("id") + t.Int("user_id", rel.Unsigned(true)) + t.String("name", rel.Limit(60), rel.Required(true), rel.Default("")) + t.DateTime("created_at") + t.DateTime("updated_at") + + t.ForeignKey("user_id", "users", "id") + }) + }, + func(schema *rel.Schema) { + schema.DropTable("addresses") + }, + ) + + m.Register(3, + func(schema *rel.Schema) { + schema.CreateTable("extras", func(t *rel.Table) { + t.ID("id") + t.Int("user_id", rel.Unsigned(true)) + t.String("slug", rel.Limit(30)) + t.Int("score", rel.Default(0)) + + t.ForeignKey("user_id", "users", "id") + t.Unique([]string{"slug"}) + t.Fragment("CONSTRAINT extras_score_check CHECK (score>=0 AND score<=100)") + }) + }, + func(schema *rel.Schema) { + schema.DropTable("extras") + }, + ) + + m.Register(4, + func(schema *rel.Schema) { + schema.CreateTable("composites", func(t *rel.Table) { + t.Int("primary1") + t.Int("primary2") + t.String("data") + + t.PrimaryKeys([]string{"primary1", "primary2"}) + }) + }, + func(schema *rel.Schema) { + schema.DropTable("composites") + }, + ) + + m.Migrate(ctx) + + return func() { + for i := 0; i < 4; i++ { + m.Rollback(ctx) + } + } +} + +// Migrate specs. +func Migrate(flags ...Flag) { + m.Register(5, + func(schema *rel.Schema) { + schema.CreateTable("dummies", func(t *rel.Table) { + t.BigID("id") + t.Bool("bool1") + t.Bool("bool2", rel.Default(true)) + t.Int("int1") + t.Int("int2", rel.Default(8), rel.Unsigned(true), rel.Limit(10)) + t.Int("int3", rel.Unique(true)) + t.BigInt("bigint1") + t.BigInt("bigint2", rel.Default(8), rel.Unsigned(true), rel.Limit(200)) + t.Float("float1") + t.Float("float2", rel.Default(10.00), rel.Precision(2)) + t.Decimal("decimal1") + t.Decimal("decimal2", rel.Default(10.00), rel.Precision(6), rel.Scale(2)) + t.String("string1") + t.String("string2", rel.Default("string"), rel.Limit(100)) + t.Text("text") + t.Date("date1") + t.Date("date2", rel.Default(time.Now())) + t.DateTime("datetime1") + t.DateTime("datetime2", rel.Default(time.Now())) + t.Time("time1") + t.Time("time2", rel.Default(time.Now())) + + t.Unique([]string{"int2"}) + t.Unique([]string{"bigint1", "bigint2"}) + }) + }, + func(schema *rel.Schema) { + schema.DropTable("dummies") + }, + ) + defer m.Rollback(ctx) + + m.Register(6, + func(schema *rel.Schema) { + schema.AlterTable("dummies", func(t *rel.AlterTable) { + t.Bool("new_column") + }) + schema.AddColumn("dummies", "new_column1", rel.Int, rel.Unsigned(true)) + }, + func(schema *rel.Schema) { + if SkipDropColumn.disabled(flags) { + schema.AlterTable("dummies", func(t *rel.AlterTable) { + t.DropColumn("new_column") + }) + schema.DropColumn("dummies", "new_column1") + } + }, + ) + defer m.Rollback(ctx) + + if SkipRenameColumn.disabled(flags) { + m.Register(7, + func(schema *rel.Schema) { + schema.AlterTable("dummies", func(t *rel.AlterTable) { + t.RenameColumn("text", "teks") + t.RenameColumn("date2", "date3") + }) + schema.RenameColumn("dummies", "decimal1", "decimal0") + }, + func(schema *rel.Schema) { + schema.AlterTable("dummies", func(t *rel.AlterTable) { + t.RenameColumn("teks", "text") + t.RenameColumn("date3", "date2") + }) + schema.RenameColumn("dummies", "decimal0", "decimal1") + }, + ) + defer m.Rollback(ctx) + } + + m.Register(8, + func(schema *rel.Schema) { + schema.CreateIndex("dummies", "int1_idx", []string{"int1"}) + schema.CreateIndex("dummies", "string1_string2_idx", []string{"string1", "string2"}) + }, + func(schema *rel.Schema) { + schema.DropIndex("dummies", "int1_idx") + schema.DropIndex("dummies", "string1_string2_idx") + }, + ) + defer m.Rollback(ctx) + + m.Register(9, + func(schema *rel.Schema) { + schema.RenameTable("dummies", "new_dummies") + }, + func(schema *rel.Schema) { + schema.RenameTable("new_dummies", "dummies") + }, + ) + defer m.Rollback(ctx) + + m.Register(10, + func(schema *rel.Schema) { + schema.CreateTableIfNotExists("dummies2", func(t *rel.Table) { + t.ID("id") + }) + }, + func(schema *rel.Schema) { + schema.DropTableIfExists("dummies2") + }, + ) + defer m.Rollback(ctx) + + m.Register(11, + func(schema *rel.Schema) { + schema.CreateTableIfNotExists("dummies2", func(t *rel.Table) { + t.ID("id") + t.Int("field1") + t.Int("field2") + }) + }, + func(schema *rel.Schema) { + schema.DropTableIfExists("dummies2") + }, + ) + defer m.Rollback(ctx) + + m.Register(12, + func(schema *rel.Schema) { + tm := time.Now() + schema.CreateTableIfNotExists("dummies3", func(t *rel.Table) { + t.ID("id") + t.Int("field1") + t.Float("field2", rel.Default(float32(1.337))) + t.DateTime("created_at", rel.Default(tm)) + t.DateTime("updated_at", rel.Default(tm)) + t.Bool("is_active", rel.Default(true)) + }) + schema.CreateUniqueIndex("dummies3", "dummies3_field1_active_uq", []string{"field1"}, rel.Eq("is_active", true)) + schema.CreateUniqueIndex("dummies3", "dummies3_field2_uq", []string{"field2"}, rel.Gt("field1", 12)) + schema.CreateUniqueIndex("dummies3", "dummies3_field1_time_uq", []string{"field2"}, rel.Gt("created_at", &tm)) + }, + func(schema *rel.Schema) { + schema.DropIndex("dummies3", "dummies3_field1_time_uq") + schema.DropIndex("dummies3", "dummies3_field2_uq") + schema.DropIndex("dummies3", "dummies3_field1_active_uq") + schema.DropTableIfExists("dummies3") + }, + ) + defer m.Rollback(ctx) + + m.Register(13, + func(schema *rel.Schema) { + schema.CreateTableIfNotExists("options", func(t *rel.Table) { + t.ID("id") + t.String("name") + t.JSON("value") + }) + }, + func(schema *rel.Schema) { + schema.DropTableIfExists("options") + }, + ) + defer m.Rollback(ctx) + + m.Migrate(ctx) +} diff --git a/specs/preload.go b/specs/preload.go new file mode 100644 index 0000000..7cba889 --- /dev/null +++ b/specs/preload.go @@ -0,0 +1,197 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +func createPreloadUser(repo rel.Repository) User { + var ( + user = User{ + Name: "preload", + Gender: "male", + Age: 25, + Addresses: []Address{ + {Name: "primary"}, + {Name: "home"}, + {Name: "work"}, + }, + } + ) + + repo.MustInsert(ctx, &user) + + return user +} + +// PreloadHasMany tests specification for preloading has many association. +func PreloadHasMany(t *testing.T, repo rel.Repository) { + var ( + result User + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "addresses") + assert.Nil(t, err) + assert.Equal(t, user, result) +} + +// PreloadHasManyWithQuery tests specification for preloading has many association. +func PreloadHasManyWithQuery(t *testing.T, repo rel.Repository) { + var ( + result User + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "addresses", where.Eq("name", "primary")) + assert.Nil(t, err) + assert.Equal(t, 1, len(result.Addresses)) + assert.Equal(t, user.Addresses[0], result.Addresses[0]) +} + +// PreloadHasManySlice tests specification for preloading has many association from multiple records. +func PreloadHasManySlice(t *testing.T, repo rel.Repository) { + var ( + result []User + users = []User{ + createPreloadUser(repo), + createPreloadUser(repo), + } + ) + + waitForReplication() + + err := repo.FindAll(ctx, &result, where.In("id", users[0].ID, users[1].ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "addresses") + assert.Nil(t, err) + assert.Equal(t, users, result) +} + +// PreloadHasOne tests specification for preloading has one association. +func PreloadHasOne(t *testing.T, repo rel.Repository) { + var ( + result User + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "primary_address") + assert.Nil(t, err) + assert.NotNil(t, result.PrimaryAddress) +} + +// PreloadHasOneWithQuery tests specification for preloading has one association. +func PreloadHasOneWithQuery(t *testing.T, repo rel.Repository) { + var ( + result User + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "primary_address", where.Eq("name", "primary")) + assert.Nil(t, err) + assert.Equal(t, user.Addresses[0], *result.PrimaryAddress) +} + +// PreloadHasOneSlice tests specification for preloading has one association from multiple records. +func PreloadHasOneSlice(t *testing.T, repo rel.Repository) { + var ( + result []User + users = []User{ + createPreloadUser(repo), + createPreloadUser(repo), + } + ) + + waitForReplication() + + err := repo.FindAll(ctx, &result, where.In("id", users[0].ID, users[1].ID)) + assert.Nil(t, err) + + err = repo.Preload(ctx, &result, "primary_address") + assert.Nil(t, err) + assert.NotNil(t, result[0].PrimaryAddress) + assert.NotNil(t, result[1].PrimaryAddress) +} + +// PreloadBelongsTo tests specification for preloading belongs to association. +func PreloadBelongsTo(t *testing.T, repo rel.Repository) { + var ( + result Address + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.Addresses[0].ID)) + assert.Nil(t, err) + + user.Addresses = nil + + err = repo.Preload(ctx, &result, "user") + assert.Nil(t, err) + assert.Equal(t, user, result.User) +} + +// PreloadBelongsToWithQuery tests specification for preloading belongs to association. +func PreloadBelongsToWithQuery(t *testing.T, repo rel.Repository) { + var ( + result Address + user = createPreloadUser(repo) + ) + + waitForReplication() + + err := repo.Find(ctx, &result, where.Eq("id", user.Addresses[0].ID)) + assert.Nil(t, err) + + user.Addresses = nil + + err = repo.Preload(ctx, &result, "user", where.Eq("name", "not exists")) + assert.Nil(t, err) + assert.Zero(t, result.User) +} + +// PreloadBelongsToSlice tests specification for preloading belongs to association from multiple records. +func PreloadBelongsToSlice(t *testing.T, repo rel.Repository) { + var ( + user = createPreloadUser(repo) + result = user.Addresses + resultLen = len(result) + ) + + waitForReplication() + + user.Addresses = nil + + err := repo.Preload(ctx, &result, "user") + assert.Nil(t, err) + assert.Len(t, result, resultLen) + + for i := range result { + assert.Equal(t, user, result[i].User) + } +} diff --git a/specs/query.go b/specs/query.go new file mode 100644 index 0000000..084695e --- /dev/null +++ b/specs/query.go @@ -0,0 +1,143 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/sort" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +// Query tests query specifications without join. +func Query(t *testing.T, repo rel.Repository) { + // preparte tests data + var ( + user = User{Name: "name1", Gender: "male", Age: 10} + ) + + repo.MustInsert(ctx, &user) + repo.MustInsert(ctx, &User{Name: "name2", Gender: "male", Age: 20}) + repo.MustInsert(ctx, &User{Name: "name3", Gender: "male", Age: 30}) + repo.MustInsert(ctx, &User{Name: "name4", Gender: "female", Age: 40}) + repo.MustInsert(ctx, &User{Name: "name5", Gender: "female", Age: 50}) + repo.MustInsert(ctx, &User{Name: "name6", Gender: "female", Age: 60}) + + repo.MustInsert(ctx, &Address{Name: "address1", UserID: &user.ID}) + repo.MustInsert(ctx, &Address{Name: "address2", UserID: &user.ID}) + repo.MustInsert(ctx, &Address{Name: "address3", UserID: &user.ID}) + + waitForReplication() + + tests := []rel.Querier{ + where.Eq("id", user.ID), + rel.Where(where.Eq("id", user.ID)), + rel.Where(where.Eq("name", "name1")), + rel.Where(where.Eq("age", 10)), + rel.Where(where.Eq("id", user.ID), where.Eq("name", "name1")), + rel.Where(where.Eq("id", user.ID), where.Eq("name", "name1"), where.Eq("age", 10)), + rel.Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1")), + rel.Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1"), where.Eq("age", 10)), + rel.Where(where.Eq("id", user.ID)).OrWhere(where.Eq("name", "name1")).OrWhere(where.Eq("age", 10)), + rel.Where(where.Ne("gender", "male")), + rel.Where(where.Gt("age", 59)), + rel.Where(where.Gte("age", 60)), + rel.Where(where.Lt("age", 11)), + rel.Where(where.Lte("age", 10)), + rel.Where(where.Nil("note")), + rel.Where(where.NotNil("name")), + rel.Where(where.In("id", 1, 2, 3)), + rel.Where(where.Nin("id", 1, 2, 3)), + rel.Where(where.Like("name", "name%")), + rel.Where(where.NotLike("name", "noname%")), + rel.Where(where.Fragment("id > 0")), + rel.Where(where.Not(where.Eq("id", 1), where.Eq("name", "name1"), where.Eq("age", 10))), + sort.Asc("name"), + sort.Desc("name"), + rel.Select().SortAsc("name").SortDesc("age"), + rel.Select("gender", "COUNT(id) AS count").Group("gender"), + rel.Select("age", "COUNT(id) AS count").Group("age").Having(where.Gt("age", 10)), + rel.Limit(5), + rel.Select().Limit(5), + rel.Select().Limit(5).Offset(5), + rel.Select("name").Where(where.Eq("id", 1)), + rel.Select("name", "age").Where(where.Eq("id", 1)), + rel.Select().Distinct().Where(where.Eq("id", 1)), + rel.SQL("SELECT 1"), + rel.SQL("SELECT 1;"), + } + + run(t, repo, tests) +} + +// QueryJoin tests query specifications with join. +func QueryJoin(t *testing.T, repo rel.Repository) { + tests := []rel.Querier{ + rel.From("addresses").Join("users"), + rel.From("addresses").JoinOn("users", "addresses.user_id", "users.id"), + rel.From("addresses").Join("users").Where(where.Eq("addresses.id", 1)), + rel.From("addresses").Join("users").Where(where.Eq("addresses.name", "address1")), + rel.From("addresses").Join("users").Where(where.Eq("addresses.name", "address1")).SortAsc("addresses.name"), + rel.From("addresses").JoinWith("LEFT JOIN", "users", "addresses.user_id", "users.id"), + } + + run(t, repo, tests) +} + +// Query tests query specifications without join. +func QueryWhereSubQuery(t *testing.T, repo rel.Repository, flags ...Flag) { + tests := []rel.Querier{ + rel.Where(where.Lte("age", rel.Select("AVG(age)").From("users"))), + rel.Where(where.Lte("age", rel.Select("MAX(age)").From("users").Where(where.Eq("gender", "male")))), + rel.Where(where.Gte("age", rel.Select("MIN(age)").From("users").Where(where.Eq("gender", "male")))), + } + + if SkipAllAndAnyKeyword.disabled(flags) { + additionalTests := []rel.Querier{ + rel.Where(where.Lte("age", rel.All(rel.Select("age").From("users").Where(where.Eq("gender", "male"))))), + rel.Where(where.Gte("age", rel.Any(rel.Select("age").From("users").Where(where.Eq("gender", "male"))))), + } + + tests = append(tests, additionalTests...) + } + + run(t, repo, tests) +} + +// QueryNotFound tests query specifications when no result found. +func QueryNotFound(t *testing.T, repo rel.Repository) { + t.Run("NotFound", func(t *testing.T) { + var ( + user User + err = repo.Find(ctx, &user, where.Eq("id", 0)) + ) + + // find user error not found + assert.Equal(t, rel.NotFoundError{}, err) + }) +} + +func run(t *testing.T, repo rel.Repository, queriers []rel.Querier) { + for _, query := range queriers { + t.Run("FindAll", func(t *testing.T) { + var ( + users []User + err = repo.FindAll(ctx, &users, query) + ) + + assert.Nil(t, err) + assert.NotEqual(t, 0, len(users)) + }) + } + + for _, query := range queriers { + t.Run("Find", func(t *testing.T) { + var ( + user User + err = repo.Find(ctx, &user, query) + ) + + assert.Nil(t, err) + }) + } +} diff --git a/specs/specs.go b/specs/specs.go new file mode 100644 index 0000000..e0b66f8 --- /dev/null +++ b/specs/specs.go @@ -0,0 +1,95 @@ +// Package specs defines test specifications for rel's adapter. +package specs + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/adapter/sql" + "github.com/stretchr/testify/assert" +) + +var ctx = context.TODO() + +// Flag for configuration. +type Flag int + +func (f Flag) disabled(flags []Flag) bool { + for i := range flags { + if f&flags[i] == 0 { + return true + } + } + + return false +} + +const ( + // SkipDropColumn spec. + SkipDropColumn Flag = 1 << iota + // SkipRenameColumn spec. + SkipRenameColumn + // SkipAllAndAnyKeyword spec. + SkipAllAndAnyKeyword +) + +// User defines users schema. +type User struct { + ID int64 + Name string + Gender string + Age int + Note *string + Addresses []Address `autosave:"true"` + PrimaryAddress *Address `autosave:"true"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// Address defines addresses schema. +type Address struct { + ID int64 + User User `autosave:"true"` + UserID *int64 + Name string + CreatedAt time.Time + UpdatedAt time.Time +} + +// Extra defines extra schema. +type Extra struct { + ID uint + Slug *string + Score int + UserID int64 +} + +// Composite primaries example. +type Composite struct { + Primary1 int `db:",primary"` + Primary2 int `db:",primary"` + Data string +} + +var ( + config = sql.Config{ + Placeholder: "?", + EscapeChar: "`", + } +) + +func assertConstraint(t *testing.T, err error, ctype rel.ConstraintType, key string) { + assert.NotNil(t, err) + cerr, ok := err.(rel.ConstraintError) + assert.True(t, ok) + assert.True(t, strings.Contains(cerr.Key, key)) + assert.Equal(t, ctype, cerr.Type) +} + +func waitForReplication() { + // wait for replication + time.Sleep(5 * time.Millisecond) +} diff --git a/specs/update.go b/specs/update.go new file mode 100644 index 0000000..5880788 --- /dev/null +++ b/specs/update.go @@ -0,0 +1,417 @@ +package specs + +import ( + "testing" + + "github.com/go-rel/rel" + "github.com/go-rel/rel/where" + "github.com/stretchr/testify/assert" +) + +// Update tests specification for updating a record. +func Update(t *testing.T, repo rel.Repository) { + var ( + note = "swordsman" + user = User{ + Name: "update", + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update" + user.Gender = "male" + user.Age = 23 + user.Note = ¬e + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update", user.Name) + assert.Equal(t, "male", user.Gender) + assert.Equal(t, 23, user.Age) + assert.Equal(t, ¬e, user.Note) + + // update unchanged + assert.Nil(t, repo.Update(ctx, &user)) + + var ( + queried User + ) + + waitForReplication() + + user.Addresses = nil + err = repo.Find(ctx, &queried, where.Eq("id", user.ID)) + assert.Nil(t, err) + assert.Equal(t, user, queried) +} + +// UpdateNotFound tests specification for updating a not found record. +func UpdateNotFound(t *testing.T, repo rel.Repository) { + var ( + user = User{ + ID: 0, + Name: "update", + } + ) + + // update unchanged + assert.Equal(t, rel.NotFoundError{}, repo.Update(ctx, &user)) +} + +// UpdateHasManyInsert tests specification for updating a record and inserting has many association. +func UpdateHasManyInsert(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "update init", + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update insert has many" + user.Addresses = []Address{ + {Name: "primary"}, + {Name: "work"}, + } + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update insert has many", user.Name) + + assert.Len(t, user.Addresses, 2) + assert.NotZero(t, user.Addresses[0].ID) + assert.NotZero(t, user.Addresses[1].ID) + assert.Equal(t, user.ID, *user.Addresses[0].UserID) + assert.Equal(t, user.ID, *user.Addresses[1].UserID) + assert.Equal(t, "primary", user.Addresses[0].Name) + assert.Equal(t, "work", user.Addresses[1].Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "addresses") + + assert.Equal(t, result, user) +} + +// UpdateHasManyUpdate tests specification for updating a record and updating has many association. +func UpdateHasManyUpdate(t *testing.T, repo rel.Repository) { + var ( + user = User{ + Name: "update init", + Addresses: []Address{ + {Name: "old address"}, + }, + } + result User + ) + + repo.MustInsert(ctx, &user) + assert.NotZero(t, user.Addresses[0].ID) + + user.Name = "update insert has many" + user.Addresses[0].Name = "new address" + + assert.Nil(t, repo.Update(ctx, &user)) + assert.NotZero(t, user.ID) + assert.Equal(t, "update insert has many", user.Name) + + assert.Len(t, user.Addresses, 1) + assert.NotZero(t, user.Addresses[0].ID) + assert.Equal(t, user.ID, *user.Addresses[0].UserID) + assert.Equal(t, "new address", user.Addresses[0].Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "addresses") + + assert.Equal(t, result, user) +} + +// UpdateHasManyReplace tests specification for updating a record and replacing has many association. +func UpdateHasManyReplace(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "update init", + Addresses: []Address{ + {Name: "old address"}, + }, + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update insert has many" + user.Addresses = []Address{ + {Name: "primary"}, + {Name: "work"}, + } + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update insert has many", user.Name) + + assert.Len(t, user.Addresses, 2) + assert.NotZero(t, user.Addresses[0].ID) + assert.NotZero(t, user.Addresses[1].ID) + assert.Equal(t, user.ID, *user.Addresses[0].UserID) + assert.Equal(t, user.ID, *user.Addresses[1].UserID) + assert.Equal(t, "primary", user.Addresses[0].Name) + assert.Equal(t, "work", user.Addresses[1].Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "addresses") + + assert.Equal(t, result, user) +} + +// UpdateHasOneInsert tests specification for updating a record and inserting has many association. +func UpdateHasOneInsert(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "update init", + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update insert has one" + user.PrimaryAddress = &Address{Name: "primary"} + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update insert has one", user.Name) + + assert.NotZero(t, user.PrimaryAddress.ID) + assert.Equal(t, user.ID, *user.PrimaryAddress.UserID) + assert.Equal(t, "primary", user.PrimaryAddress.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "primary_address") + + assert.Equal(t, result, user) +} + +// UpdateHasOneUpdate tests specification for updating a record and updating has one association. +func UpdateHasOneUpdate(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "update init", + PrimaryAddress: &Address{Name: "primary"}, + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update update has one" + user.PrimaryAddress.Name = "updated primary" + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update update has one", user.Name) + + assert.NotZero(t, user.PrimaryAddress.ID) + assert.Equal(t, user.ID, *user.PrimaryAddress.UserID) + assert.Equal(t, "updated primary", user.PrimaryAddress.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "primary_address") + + assert.Equal(t, result, user) +} + +// UpdateHasOneReplace tests specification for updating a record and replacing has one association. +func UpdateHasOneReplace(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{ + Name: "update init", + PrimaryAddress: &Address{Name: "primary"}, + } + ) + + repo.MustInsert(ctx, &user) + + user.Name = "update replace has one" + user.PrimaryAddress = &Address{Name: "replaced primary"} + + err := repo.Update(ctx, &user) + assert.Nil(t, err) + assert.NotZero(t, user.ID) + assert.Equal(t, "update replace has one", user.Name) + + assert.NotZero(t, user.PrimaryAddress.ID) + assert.Equal(t, user.ID, *user.PrimaryAddress.UserID) + assert.Equal(t, "replaced primary", user.PrimaryAddress.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + repo.MustPreload(ctx, &result, "primary_address") + + assert.Equal(t, result, user) +} + +// UpdateBelongsToInsert tests specification for updating a record and inserting belongs to association. +func UpdateBelongsToInsert(t *testing.T, repo rel.Repository) { + var ( + result Address + address = Address{Name: "address init"} + ) + + repo.MustInsert(ctx, &address) + + address.Name = "update address belongs to" + address.User = User{Name: "inserted user"} + + err := repo.Update(ctx, &address) + assert.Nil(t, err) + assert.NotZero(t, address.ID) + assert.Equal(t, "update address belongs to", address.Name) + + assert.NotZero(t, address.User.ID) + assert.Equal(t, *address.UserID, address.User.ID) + assert.Equal(t, "inserted user", address.User.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", address.ID)) + repo.MustPreload(ctx, &result, "user") + + assert.Equal(t, result, address) +} + +// UpdateBelongsToUpdate tests specification for updating a record and updating belongs to association. +func UpdateBelongsToUpdate(t *testing.T, repo rel.Repository) { + var ( + result Address + address = Address{ + Name: "address init", + User: User{Name: "user"}, + } + ) + + repo.MustInsert(ctx, &address) + + address.Name = "update address belongs to" + address.User.Name = "updated user" + + err := repo.Update(ctx, &address) + assert.Nil(t, err) + assert.NotZero(t, address.ID) + assert.Equal(t, "update address belongs to", address.Name) + + assert.NotZero(t, address.User.ID) + assert.Equal(t, *address.UserID, address.User.ID) + assert.Equal(t, "updated user", address.User.Name) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", address.ID)) + repo.MustPreload(ctx, &result, "user") + + assert.Equal(t, result, address) +} + +// UpdateAtomic tests increment and decerement operation when updating a record. +func UpdateAtomic(t *testing.T, repo rel.Repository) { + var ( + result User + user = User{Name: "update", Age: 10} + ) + + repo.MustInsert(ctx, &user) + + assert.Nil(t, repo.Update(ctx, &user, rel.Inc("age"))) + assert.Equal(t, 11, user.Age) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + assert.Equal(t, result, user) + + assert.Nil(t, repo.Update(ctx, &user, rel.Dec("age"))) + assert.Equal(t, 10, user.Age) + + waitForReplication() + + repo.MustFind(ctx, &result, where.Eq("id", user.ID)) + assert.Equal(t, result, user) +} + +// Updates tests update specifications. +func Updates(t *testing.T, repo rel.Repository) { + var ( + note = "note" + user = User{Name: "update"} + address = Address{Name: "update"} + ) + + repo.MustInsert(ctx, &user) + repo.MustInsert(ctx, &address) + + tests := []interface{}{ + &User{ID: user.ID, Name: "changed", Age: 100}, + &User{ID: user.ID, Name: "changed", Age: 100, Note: ¬e}, + &User{ID: user.ID, Note: ¬e}, + &Address{ID: address.ID, Name: "address"}, + &Address{ID: address.ID, UserID: &user.ID}, + &Address{ID: address.ID, Name: "address", UserID: &user.ID}, + } + + for _, record := range tests { + t.Run("Update", func(t *testing.T) { + assert.Nil(t, repo.Update(ctx, record)) + }) + } +} + +// UpdateAny tests update all specifications. +func UpdateAny(t *testing.T, repo rel.Repository) { + repo.MustInsert(ctx, &User{Name: "update", Age: 100}) + repo.MustInsert(ctx, &User{Name: "update", Age: 100}) + repo.MustInsert(ctx, &User{Name: "other update", Age: 110}) + + tests := []rel.Query{ + rel.From("users").Where(where.Eq("name", "update")), + rel.From("users").Where(where.Eq("name", "other update"), where.Gt("age", 100)), + } + + for _, query := range tests { + t.Run("UpdateAny", func(t *testing.T) { + var ( + result []User + name = "all updated" + ) + + updatedCount, err := repo.UpdateAny(ctx, query, rel.Set("name", name)) + assert.Nil(t, err) + assert.NotZero(t, updatedCount) + + waitForReplication() + + assert.Nil(t, repo.FindAll(ctx, &result, query)) + assert.Zero(t, len(result)) + for i := range result { + assert.Equal(t, name, result[i].Name) + } + }) + } +}