package testsuite import ( "context" "database/sql" "errors" "fmt" "math/rand" "strconv" "strings" "sync" "time" "git.hexq.cn/tiglog/mydb" detectrace "github.com/ipfs/go-detect-race" "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" ) type artistType struct { ID int64 `db:"id,omitempty"` Name string `db:"name"` } type itemWithCompoundKey struct { Code string `db:"code"` UserID string `db:"user_id"` SomeVal string `db:"some_val"` } type customType struct { Val []byte } type artistWithCustomType struct { Custom customType `db:"name"` } func (f customType) String() string { return fmt.Sprintf("foo: %s", string(f.Val)) } func (f customType) MarshalDB() (interface{}, error) { return f.String(), nil } func (f *customType) UnmarshalDB(in interface{}) error { switch t := in.(type) { case []byte: f.Val = t case string: f.Val = []byte(t) } return nil } var ( _ = mydb.Marshaler(&customType{}) _ = mydb.Unmarshaler(&customType{}) ) type SQLTestSuite struct { suite.Suite Helper } func (s *SQLTestSuite) AfterTest(suiteName, testName string) { err := s.TearDown() s.NoError(err) } func (s *SQLTestSuite) BeforeTest(suiteName, testName string) { err := s.TearUp() s.NoError(err) sess := s.Session() // Creating test data artist := sess.Collection("artist") artistNames := []string{"Ozzie", "Flea", "Slash", "Chrono"} for _, artistName := range artistNames { _, err := artist.Insert(map[string]string{ "name": artistName, }) s.NoError(err) } } func (s *SQLTestSuite) TestPreparedStatementsCache() { sess := s.Session() sess.SetPreparedStatementCache(true) defer sess.SetPreparedStatementCache(false) var tMu sync.Mutex tFatal := func(err error) { tMu.Lock() defer tMu.Unlock() s.T().Errorf("tmu: %v", err) } // This limit was chosen because, by default, MySQL accepts 16k statements // and dies. See https://github.com/upper/db/issues/287 limit := 20000 if detectrace.WithRace() { // When running this test under the Go race detector we quickly reach the limit // of 8128 alive goroutines it can handle, so we set it to a safer number. // // Note that in order to fully stress this feature you'll have to run this // test without the race detector. limit = 100 } var wg sync.WaitGroup for i := 0; i < limit; i++ { wg.Add(1) go func(i int) { defer wg.Done() // This query is different on each iteration and generates a new // prepared statement everytime it's called. res := sess.Collection("artist").Find().Select(mydb.Raw(fmt.Sprintf("count(%d) AS c", i))) var count map[string]uint64 err := res.One(&count) if err != nil { tFatal(err) } }(i) } wg.Wait() // Concurrent Insert can open many connections on MySQL / PostgreSQL, this // sets a limit on them. sess.SetMaxOpenConns(90) switch s.Adapter() { case "ql": limit = 1000 } for i := 0; i < limit; i++ { wg.Add(1) go func(i int) { defer wg.Done() // The same prepared query on every iteration. _, err := sess.Collection("artist").Insert(artistType{ Name: fmt.Sprintf("artist-%d", i), }) if err != nil { tFatal(err) } }(i) } wg.Wait() // Insert returning creates a transaction. for i := 0; i < limit; i++ { wg.Add(1) go func(i int) { defer wg.Done() // The same prepared query on every iteration. artist := artistType{ Name: fmt.Sprintf("artist-%d", i), } err := sess.Collection("artist").InsertReturning(&artist) if err != nil { tFatal(err) } }(i) } wg.Wait() // Removing the limit. sess.SetMaxOpenConns(0) } func (s *SQLTestSuite) TestTruncateAllCollections() { sess := s.Session() collections, err := sess.Collections() s.NoError(err) s.True(len(collections) > 0) for _, col := range collections { if ok, _ := col.Exists(); ok { if err = col.Truncate(); err != nil { s.NoError(err) } } } } func (s *SQLTestSuite) TestQueryLogger() { logLevel := mydb.LC().Level() mydb.LC().SetLogger(logrus.New()) mydb.LC().SetLevel(mydb.LogLevelDebug) defer func() { mydb.LC().SetLogger(nil) mydb.LC().SetLevel(logLevel) }() sess := s.Session() _, err := sess.Collection("artist").Find().Count() s.Equal(nil, err) _, err = sess.Collection("artist_x").Find().Count() s.NotEqual(nil, err) } func (s *SQLTestSuite) TestExpectCursorError() { sess := s.Session() artist := sess.Collection("artist") res := artist.Find(-1) c, err := res.Count() s.Equal(uint64(0), c) s.NoError(err) var item map[string]interface{} err = res.One(&item) s.Error(err) } func (s *SQLTestSuite) TestInsertDefault() { if s.Adapter() == "ql" { s.T().Skip("Currently not supported.") } sess := s.Session() artist := sess.Collection("artist") err := artist.Truncate() s.NoError(err) id, err := artist.Insert(&artistType{}) s.NoError(err) s.NotNil(id) err = artist.Truncate() s.NoError(err) id, err = artist.Insert(nil) s.NoError(err) s.NotNil(id) } func (s *SQLTestSuite) TestInsertReturning() { sess := s.Session() artist := sess.Collection("artist") err := artist.Truncate() s.NoError(err) itemMap := map[string]string{ "name": "Ozzie", } s.Zero(itemMap["id"], "Must be zero before inserting") err = artist.InsertReturning(&itemMap) s.NoError(err) s.NotZero(itemMap["id"], "Must not be zero after inserting") itemStruct := struct { ID int `db:"id,omitempty"` Name string `db:"name"` }{ 0, "Flea", } s.Zero(itemStruct.ID, "Must be zero before inserting") err = artist.InsertReturning(&itemStruct) s.NoError(err) s.NotZero(itemStruct.ID, "Must not be zero after inserting") count, err := artist.Find().Count() s.NoError(err) s.Equal(uint64(2), count, "Expecting 2 elements") itemStruct2 := struct { ID int `db:"id,omitempty"` Name string `db:"name"` }{ 0, "Slash", } s.Zero(itemStruct2.ID, "Must be zero before inserting") err = artist.InsertReturning(itemStruct2) s.Error(err, "Should not happen, using a pointer should be enforced") s.Zero(itemStruct2.ID, "Must still be zero because there was no insertion") itemMap2 := map[string]string{ "name": "Janus", } s.Zero(itemMap2["id"], "Must be zero before inserting") err = artist.InsertReturning(itemMap2) s.Error(err, "Should not happen, using a pointer should be enforced") s.Zero(itemMap2["id"], "Must still be zero because there was no insertion") // Counting elements, must be exactly 2 elements. count, err = artist.Find().Count() s.NoError(err) s.Equal(uint64(2), count, "Expecting 2 elements") } func (s *SQLTestSuite) TestInsertReturningWithinTransaction() { sess := s.Session() err := sess.Collection("artist").Truncate() s.NoError(err) err = sess.Tx(func(tx mydb.Session) error { artist := tx.Collection("artist") itemMap := map[string]string{ "name": "Ozzie", } s.Zero(itemMap["id"], "Must be zero before inserting") err = artist.InsertReturning(&itemMap) s.NoError(err) s.NotZero(itemMap["id"], "Must not be zero after inserting") itemStruct := struct { ID int `db:"id,omitempty"` Name string `db:"name"` }{ 0, "Flea", } s.Zero(itemStruct.ID, "Must be zero before inserting") err = artist.InsertReturning(&itemStruct) s.NoError(err) s.NotZero(itemStruct.ID, "Must not be zero after inserting") count, err := artist.Find().Count() s.NoError(err) s.Equal(uint64(2), count, "Expecting 2 elements") itemStruct2 := struct { ID int `db:"id,omitempty"` Name string `db:"name"` }{ 0, "Slash", } s.Zero(itemStruct2.ID, "Must be zero before inserting") err = artist.InsertReturning(itemStruct2) s.Error(err, "Should not happen, using a pointer should be enforced") s.Zero(itemStruct2.ID, "Must still be zero because there was no insertion") itemMap2 := map[string]string{ "name": "Janus", } s.Zero(itemMap2["id"], "Must be zero before inserting") err = artist.InsertReturning(itemMap2) s.Error(err, "Should not happen, using a pointer should be enforced") s.Zero(itemMap2["id"], "Must still be zero because there was no insertion") // Counting elements, must be exactly 2 elements. count, err = artist.Find().Count() s.NoError(err) s.Equal(uint64(2), count, "Expecting 2 elements") return fmt.Errorf("rolling back for no reason") }) s.Error(err) // Expecting no elements. count, err := sess.Collection("artist").Find().Count() s.NoError(err) s.Equal(uint64(0), count, "Expecting 0 elements, everything was rolled back!") } func (s *SQLTestSuite) TestInsertIntoArtistsTable() { sess := s.Session() artist := sess.Collection("artist") err := artist.Truncate() s.NoError(err) itemMap := map[string]string{ "name": "Ozzie", } record, err := artist.Insert(itemMap) s.NoError(err) s.NotNil(record) if pk, ok := record.ID().(int64); !ok || pk == 0 { s.T().Errorf("Expecting an ID.") } // Attempt to append a struct. itemStruct := struct { Name string `db:"name"` }{ "Flea", } record, err = artist.Insert(itemStruct) s.NoError(err) s.NotNil(record) if pk, ok := record.ID().(int64); !ok || pk == 0 { s.T().Errorf("Expecting an ID.") } // Attempt to append a tagged struct. itemStruct2 := struct { ArtistName string `db:"name"` }{ "Slash", } record, err = artist.Insert(&itemStruct2) s.NoError(err) s.NotNil(record) if pk, ok := record.ID().(int64); !ok || pk == 0 { s.T().Errorf("Expecting an ID.") } itemStruct3 := artistType{ Name: "Janus", } record, err = artist.Insert(&itemStruct3) s.NoError(err) if s.Adapter() != "ql" { s.NotZero(record) // QL always inserts an ID. } // Counting elements, must be exactly 4 elements. count, err := artist.Find().Count() s.NoError(err) s.Equal(uint64(4), count) count, err = artist.Find(mydb.Cond{"name": mydb.Eq("Ozzie")}).Count() s.NoError(err) s.Equal(uint64(1), count) count, err = artist.Find("name", "Ozzie").And("name", "Flea").Count() s.NoError(err) s.Equal(uint64(0), count) count, err = artist.Find(mydb.Or(mydb.Cond{"name": "Ozzie"}, mydb.Cond{"name": "Flea"})).Count() s.NoError(err) s.Equal(uint64(2), count) count, err = artist.Find(mydb.And(mydb.Cond{"name": "Ozzie"}, mydb.Cond{"name": "Flea"})).Count() s.NoError(err) s.Equal(uint64(0), count) count, err = artist.Find(mydb.Cond{"name": "Ozzie"}).And(mydb.Cond{"name": "Flea"}).Count() s.NoError(err) s.Equal(uint64(0), count) } func (s *SQLTestSuite) TestQueryNonExistentCollection() { sess := s.Session() count, err := sess.Collection("doesnotexist").Find().Count() s.Error(err) s.Zero(count) } func (s *SQLTestSuite) TestGetOneResult() { sess := s.Session() artist := sess.Collection("artist") for i := 0; i < 5; i++ { _, err := artist.Insert(map[string]string{ "name": fmt.Sprintf("Artist %d", i), }) s.NoError(err) } // Fetching one struct. var someArtist artistType err := artist.Find().Limit(1).One(&someArtist) s.NoError(err) s.NotZero(someArtist.Name) if s.Adapter() != "ql" { s.NotZero(someArtist.ID) } // Fetching a pointer to a pointer. var someArtistObj *artistType err = artist.Find().Limit(1).One(&someArtistObj) s.NoError(err) s.NotZero(someArtist.Name) if s.Adapter() != "ql" { s.NotZero(someArtist.ID) } } func (s *SQLTestSuite) TestGetWithOffset() { sess := s.Session() artist := sess.Collection("artist") // Fetching one struct. var artists []artistType err := artist.Find().Offset(1).All(&artists) s.NoError(err) s.Equal(3, len(artists)) } func (s *SQLTestSuite) TestGetResultsOneByOne() { sess := s.Session() artist := sess.Collection("artist") rowMap := map[string]interface{}{} res := artist.Find() err := res.Err() s.NoError(err) for res.Next(&rowMap) { s.NotZero(rowMap["id"]) s.NotZero(rowMap["name"]) } err = res.Err() s.NoError(err) err = res.Close() s.NoError(err) // Dumping into a tagged struct. rowStruct2 := struct { Value1 int64 `db:"id"` Value2 string `db:"name"` }{} res = artist.Find() for res.Next(&rowStruct2) { s.NotZero(rowStruct2.Value1) s.NotZero(rowStruct2.Value2) } err = res.Err() s.NoError(err) err = res.Close() s.NoError(err) // Dumping into a slice of maps. allRowsMap := []map[string]interface{}{} res = artist.Find() err = res.All(&allRowsMap) s.NoError(err) s.Equal(4, len(allRowsMap)) for _, singleRowMap := range allRowsMap { if fmt.Sprintf("%d", singleRowMap["id"]) == "0" { s.T().Errorf("Expecting a not null ID.") } } // Dumping into a slice of structs. allRowsStruct := []struct { ID int64 `db:"id,omitempty"` Name string `db:"name"` }{} res = artist.Find() if err = res.All(&allRowsStruct); err != nil { s.T().Errorf("%v", err) } s.Equal(4, len(allRowsStruct)) for _, singleRowStruct := range allRowsStruct { s.NotZero(singleRowStruct.ID) } // Dumping into a slice of tagged structs. allRowsStruct2 := []struct { Value1 int64 `db:"id"` Value2 string `db:"name"` }{} res = artist.Find() err = res.All(&allRowsStruct2) s.NoError(err) s.Equal(4, len(allRowsStruct2)) for _, singleRowStruct := range allRowsStruct2 { s.NotZero(singleRowStruct.Value1) } } func (s *SQLTestSuite) TestGetAllResults() { sess := s.Session() artist := sess.Collection("artist") total, err := artist.Find().Count() s.NoError(err) s.NotZero(total) // Fetching all artists into struct artists := []artistType{} res := artist.Find() err = res.All(&artists) s.NoError(err) s.Equal(len(artists), int(total)) s.NotZero(artists[0].Name) s.NotZero(artists[0].ID) // Fetching all artists into struct pointers artistObjs := []*artistType{} res = artist.Find() err = res.All(&artistObjs) s.NoError(err) s.Equal(len(artistObjs), int(total)) s.NotZero(artistObjs[0].Name) s.NotZero(artistObjs[0].ID) } func (s *SQLTestSuite) TestInlineStructs() { type reviewTypeDetails struct { Name string `db:"name"` Comments string `db:"comments"` Created time.Time `db:"created"` } type reviewType struct { ID int64 `db:"id,omitempty"` PublicationID int64 `db:"publication_id"` Details reviewTypeDetails `db:",inline"` } sess := s.Session() review := sess.Collection("review") err := review.Truncate() s.NoError(err) rec := reviewType{ PublicationID: 123, Details: reviewTypeDetails{ Name: "..name..", Comments: "..comments..", }, } testTimeZone := time.UTC switch s.Adapter() { case "mysql": // MySQL uses a global time zone testTimeZone = defaultTimeLocation } createdAt := time.Date(2016, time.January, 1, 2, 3, 4, 0, testTimeZone) rec.Details.Created = createdAt record, err := review.Insert(rec) s.NoError(err) s.NotZero(record.ID().(int64)) rec.ID = record.ID().(int64) var recChk reviewType res := review.Find() err = res.One(&recChk) s.NoError(err) s.Equal(rec, recChk) } func (s *SQLTestSuite) TestUpdate() { sess := s.Session() artist := sess.Collection("artist") _, err := artist.Insert(map[string]string{ "name": "Ozzie", }) s.NoError(err) // Defining destination struct value := struct { ID int64 `db:"id,omitempty"` Name string `db:"name"` }{} // Getting the first artist. cond := mydb.Cond{"id !=": mydb.NotEq(0)} if s.Adapter() == "ql" { cond = mydb.Cond{"id() !=": 0} } res := artist.Find(cond).Limit(1) err = res.One(&value) s.NoError(err) res = artist.Find(value.ID) // Updating set with a map rowMap := map[string]interface{}{ "name": strings.ToUpper(value.Name), } err = res.Update(rowMap) s.NoError(err) // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying. s.Equal(value.Name, rowMap["name"]) if s.Adapter() != "ql" { // Updating using raw if err = res.Update(map[string]interface{}{"name": mydb.Raw("LOWER(name)")}); err != nil { s.T().Errorf("%v", err) } // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying. s.Equal(value.Name, strings.ToLower(rowMap["name"].(string))) // Updating using raw if err = res.Update(struct { Name *mydb.RawExpr `db:"name"` }{mydb.Raw(`UPPER(name)`)}); err != nil { s.T().Errorf("%v", err) } // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying. s.Equal(value.Name, strings.ToUpper(rowMap["name"].(string))) // Updating using raw if err = res.Update(struct { Name *mydb.FuncExpr `db:"name"` }{mydb.Func("LOWER", mydb.Raw("name"))}); err != nil { s.T().Errorf("%v", err) } // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying. s.Equal(value.Name, strings.ToLower(rowMap["name"].(string))) } // Updating set with a struct rowStruct := struct { Name string `db:"name"` }{strings.ToLower(value.Name)} err = res.Update(rowStruct) s.NoError(err) // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying s.Equal(value.Name, rowStruct.Name) // Updating set with a tagged struct rowStruct2 := struct { Value1 string `db:"name"` }{"john"} err = res.Update(rowStruct2) s.NoError(err) // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying s.Equal(value.Name, rowStruct2.Value1) // Updating set with a tagged object rowStruct3 := &struct { Value1 string `db:"name"` }{"anderson"} err = res.Update(rowStruct3) s.NoError(err) // Pulling it again. err = res.One(&value) s.NoError(err) // Verifying s.Equal(value.Name, rowStruct3.Value1) } func (s *SQLTestSuite) TestFunction() { sess := s.Session() rowStruct := struct { ID int64 Name string }{} artist := sess.Collection("artist") cond := mydb.Cond{"id NOT IN": []int{0, -1}} if s.Adapter() == "ql" { cond = mydb.Cond{"id() NOT IN": []int{0, -1}} } res := artist.Find(cond) err := res.One(&rowStruct) s.NoError(err) total, err := res.Count() s.NoError(err) s.Equal(uint64(4), total) // Testing conditions cond = mydb.Cond{"id NOT IN": []interface{}{0, -1}} if s.Adapter() == "ql" { cond = mydb.Cond{"id() NOT IN": []interface{}{0, -1}} } res = artist.Find(cond) err = res.One(&rowStruct) s.NoError(err) total, err = res.Count() s.NoError(err) s.Equal(uint64(4), total) res = artist.Find().Select("name") var rowMap map[string]interface{} err = res.One(&rowMap) s.NoError(err) total, err = res.Count() s.NoError(err) s.Equal(uint64(4), total) res = artist.Find().Select("name") err = res.One(&rowMap) s.NoError(err) total, err = res.Count() s.NoError(err) s.Equal(uint64(4), total) } func (s *SQLTestSuite) TestNullableFields() { sess := s.Session() type testType struct { ID int64 `db:"id,omitempty"` NullStringTest sql.NullString `db:"_string"` NullInt64Test sql.NullInt64 `db:"_int64"` NullFloat64Test sql.NullFloat64 `db:"_float64"` NullBoolTest sql.NullBool `db:"_bool"` } col := sess.Collection(`data_types`) err := col.Truncate() s.NoError(err) // Testing insertion of invalid nulls. test := testType{ NullStringTest: sql.NullString{String: "", Valid: false}, NullInt64Test: sql.NullInt64{Int64: 0, Valid: false}, NullFloat64Test: sql.NullFloat64{Float64: 0.0, Valid: false}, NullBoolTest: sql.NullBool{Bool: false, Valid: false}, } id, err := col.Insert(testType{}) s.NoError(err) // Testing fetching of invalid nulls. err = col.Find(id).One(&test) s.NoError(err) s.False(test.NullInt64Test.Valid) s.False(test.NullFloat64Test.Valid) s.False(test.NullBoolTest.Valid) // Testing insertion of valid nulls. test = testType{ NullStringTest: sql.NullString{String: "", Valid: true}, NullInt64Test: sql.NullInt64{Int64: 0, Valid: true}, NullFloat64Test: sql.NullFloat64{Float64: 0.0, Valid: true}, NullBoolTest: sql.NullBool{Bool: false, Valid: true}, } id, err = col.Insert(test) s.NoError(err) // Testing fetching of valid nulls. err = col.Find(id).One(&test) s.NoError(err) s.True(test.NullInt64Test.Valid) s.True(test.NullBoolTest.Valid) s.True(test.NullStringTest.Valid) } func (s *SQLTestSuite) TestGroup() { sess := s.Session() type statsType struct { Numeric int `db:"numeric"` Value int `db:"value"` } stats := sess.Collection("stats_test") err := stats.Truncate() s.NoError(err) // Adding row append. for i := 0; i < 100; i++ { numeric, value := rand.Intn(5), rand.Intn(100) _, err := stats.Insert(statsType{numeric, value}) s.NoError(err) } // Testing GROUP BY res := stats.Find().Select( "numeric", mydb.Raw("count(1) AS counter"), mydb.Raw("sum(value) AS total"), ).GroupBy("numeric") var results []map[string]interface{} err = res.All(&results) s.NoError(err) s.Equal(5, len(results)) } func (s *SQLTestSuite) TestInsertAndDelete() { sess := s.Session() artist := sess.Collection("artist") res := artist.Find() total, err := res.Count() s.NoError(err) s.Greater(total, uint64(0)) err = res.Delete() s.NoError(err) total, err = res.Count() s.NoError(err) s.Equal(uint64(0), total) } func (s *SQLTestSuite) TestCompositeKeys() { if s.Adapter() == "ql" { s.T().Skip("Currently not supported.") } sess := s.Session() compositeKeys := sess.Collection("composite_keys") { n := rand.Intn(100000) item := itemWithCompoundKey{ "ABCDEF", strconv.Itoa(n), "Some value", } id, err := compositeKeys.Insert(&item) s.NoError(err) s.NotZero(id) var item2 itemWithCompoundKey s.NotEqual(item2.SomeVal, item.SomeVal) // Finding by ID err = compositeKeys.Find(id).One(&item2) s.NoError(err) s.Equal(item2.SomeVal, item.SomeVal) } { n := rand.Intn(100000) item := itemWithCompoundKey{ "ABCDEF", strconv.Itoa(n), "Some value", } err := compositeKeys.InsertReturning(&item) s.NoError(err) } } // Attempts to test database transactions. func (s *SQLTestSuite) TestTransactionsAndRollback() { if s.Adapter() == "ql" { s.T().Skip("Currently not supported.") } sess := s.Session() err := sess.Tx(func(tx mydb.Session) error { artist := tx.Collection("artist") err := artist.Truncate() s.NoError(err) _, err = artist.Insert(artistType{1, "First"}) s.NoError(err) return nil }) s.NoError(err) err = sess.Tx(func(tx mydb.Session) error { artist := tx.Collection("artist") _, err = artist.Insert(artistType{2, "Second"}) s.NoError(err) // Won't fail. _, err = artist.Insert(artistType{3, "Third"}) s.NoError(err) // Will fail. _, err = artist.Insert(artistType{1, "Duplicated"}) s.Error(err) return err }) s.Error(err) // Let's verify we still have one element. artist := sess.Collection("artist") count, err := artist.Find().Count() s.NoError(err) s.Equal(uint64(1), count) err = sess.Tx(func(tx mydb.Session) error { artist := tx.Collection("artist") // Won't fail. _, err = artist.Insert(artistType{2, "Second"}) s.NoError(err) // Won't fail. _, err = artist.Insert(artistType{3, "Third"}) s.NoError(err) return fmt.Errorf("rollback for no reason") }) s.Error(err) // Let's verify we still have one element. artist = sess.Collection("artist") count, err = artist.Find().Count() s.NoError(err) s.Equal(uint64(1), count) // Attempt to add some rows. err = sess.Tx(func(tx mydb.Session) error { artist = tx.Collection("artist") // Won't fail. _, err = artist.Insert(artistType{2, "Second"}) s.NoError(err) // Won't fail. _, err = artist.Insert(artistType{3, "Third"}) s.NoError(err) return nil }) s.NoError(err) // Let's verify we have 3 rows. artist = sess.Collection("artist") count, err = artist.Find().Count() s.NoError(err) s.Equal(uint64(3), count) } func (s *SQLTestSuite) TestDataTypes() { if s.Adapter() == "ql" { s.T().Skip("Currently not supported.") } type testValuesStruct struct { Uint uint `db:"_uint"` Uint8 uint8 `db:"_uint8"` Uint16 uint16 `db:"_uint16"` Uint32 uint32 `db:"_uint32"` Uint64 uint64 `db:"_uint64"` Int int `db:"_int"` Int8 int8 `db:"_int8"` Int16 int16 `db:"_int16"` Int32 int32 `db:"_int32"` Int64 int64 `db:"_int64"` Float32 float32 `db:"_float32"` Float64 float64 `db:"_float64"` Bool bool `db:"_bool"` String string `db:"_string"` Blob []byte `db:"_blob"` Date time.Time `db:"_date"` DateN *time.Time `db:"_nildate"` DateP *time.Time `db:"_ptrdate"` DateD *time.Time `db:"_defaultdate,omitempty"` Time int64 `db:"_time"` } sess := s.Session() // Getting a pointer to the "data_types" collection. dataTypes := sess.Collection("data_types") // Removing all data. err := dataTypes.Truncate() s.NoError(err) testTimeZone := time.Local switch s.Adapter() { case "mysql", "postgresql": // MySQL uses a global time zone testTimeZone = defaultTimeLocation } ts := time.Date(2011, 7, 28, 1, 2, 3, 0, testTimeZone) tnz := ts.In(time.UTC) switch s.Adapter() { case "mysql": // MySQL uses a global timezone tnz = ts.In(defaultTimeLocation) } testValues := testValuesStruct{ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 1.337, 1.337, true, "Hello world!", []byte("Hello world!"), ts, // Date nil, // DateN &tnz, // DateP nil, // DateD int64(time.Second * time.Duration(7331)), } id, err := dataTypes.Insert(testValues) s.NoError(err) s.NotNil(id) // Defining our set. cond := mydb.Cond{"id": id} if s.Adapter() == "ql" { cond = mydb.Cond{"id()": id} } res := dataTypes.Find(cond) count, err := res.Count() s.NoError(err) s.NotZero(count) // Trying to dump the subject into an empty structure of the same type. var item testValuesStruct err = res.One(&item) s.NoError(err) s.NotNil(item.DateD) s.NotNil(item.Date) // Copy the default date (this value is set by the database) testValues.DateD = item.DateD item.Date = item.Date.In(testTimeZone) s.Equal(testValues.Date, item.Date) s.Equal(testValues.DateN, item.DateN) s.Equal(testValues.DateP, item.DateP) s.Equal(testValues.DateD, item.DateD) // The original value and the test subject must match. s.Equal(testValues, item) } func (s *SQLTestSuite) TestUpdateWithNullColumn() { sess := s.Session() artist := sess.Collection("artist") err := artist.Truncate() s.NoError(err) type Artist struct { ID int64 `db:"id,omitempty"` Name *string `db:"name"` } name := "José" id, err := artist.Insert(Artist{0, &name}) s.NoError(err) var item Artist err = artist.Find(id).One(&item) s.NoError(err) s.NotEqual(nil, item.Name) s.Equal(name, *item.Name) err = artist.Find(id).Update(Artist{Name: nil}) s.NoError(err) var item2 Artist err = artist.Find(id).One(&item2) s.NoError(err) s.Equal((*string)(nil), item2.Name) } func (s *SQLTestSuite) TestBatchInsert() { sess := s.Session() for batchSize := 0; batchSize < 17; batchSize++ { err := sess.Collection("artist").Truncate() s.NoError(err) q := sess.SQL().InsertInto("artist").Columns("name") switch s.Adapter() { case "postgresql", "cockroachdb": q = q.Amend(func(query string) string { return query + ` ON CONFLICT DO NOTHING` }) } batch := q.Batch(batchSize) totalItems := int(rand.Int31n(21)) go func() { defer batch.Done() for i := 0; i < totalItems; i++ { batch.Values(fmt.Sprintf("artist-%d", i)) } }() err = batch.Wait() s.NoError(err) s.NoError(batch.Err()) c, err := sess.Collection("artist").Find().Count() s.NoError(err) s.Equal(uint64(totalItems), c) for i := 0; i < totalItems; i++ { c, err := sess.Collection("artist").Find(mydb.Cond{"name": fmt.Sprintf("artist-%d", i)}).Count() s.NoError(err) s.Equal(uint64(1), c) } } } func (s *SQLTestSuite) TestBatchInsertNoColumns() { sess := s.Session() for batchSize := 0; batchSize < 17; batchSize++ { err := sess.Collection("artist").Truncate() s.NoError(err) batch := sess.SQL().InsertInto("artist").Batch(batchSize) totalItems := int(rand.Int31n(21)) go func() { defer batch.Done() for i := 0; i < totalItems; i++ { value := struct { Name string `db:"name"` }{fmt.Sprintf("artist-%d", i)} batch.Values(value) } }() err = batch.Wait() s.NoError(err) s.NoError(batch.Err()) c, err := sess.Collection("artist").Find().Count() s.NoError(err) s.Equal(uint64(totalItems), c) for i := 0; i < totalItems; i++ { c, err := sess.Collection("artist").Find(mydb.Cond{"name": fmt.Sprintf("artist-%d", i)}).Count() s.NoError(err) s.Equal(uint64(1), c) } } } func (s *SQLTestSuite) TestBatchInsertReturningKeys() { switch s.Adapter() { case "postgresql", "cockroachdb": // pass default: s.T().Skip("Currently not supported.") return } sess := s.Session() err := sess.Collection("artist").Truncate() s.NoError(err) batchSize, totalItems := 7, 12 batch := sess.SQL().InsertInto("artist").Columns("name").Returning("id").Batch(batchSize) go func() { defer batch.Done() for i := 0; i < totalItems; i++ { batch.Values(fmt.Sprintf("artist-%d", i)) } }() var keyMap []struct { ID int `db:"id"` } for batch.NextResult(&keyMap) { // Each insertion must produce new keys. s.True(len(keyMap) > 0) s.True(len(keyMap) <= batchSize) // Find the elements we've just inserted keys := make([]int, 0, len(keyMap)) for i := range keyMap { keys = append(keys, keyMap[i].ID) } // Make sure count matches. c, err := sess.Collection("artist").Find(mydb.Cond{"id": keys}).Count() s.NoError(err) s.Equal(uint64(len(keyMap)), c) } s.NoError(batch.Err()) // Count all new elements c, err := sess.Collection("artist").Find().Count() s.NoError(err) s.Equal(uint64(totalItems), c) } func (s *SQLTestSuite) TestPaginator() { sess := s.Session() err := sess.Collection("artist").Truncate() s.NoError(err) batch := sess.SQL().InsertInto("artist").Batch(100) go func() { defer batch.Done() for i := 0; i < 999; i++ { value := struct { Name string `db:"name"` }{fmt.Sprintf("artist-%d", i)} batch.Values(value) } }() err = batch.Wait() s.NoError(err) s.NoError(batch.Err()) q := sess.SQL().SelectFrom("artist") if s.Adapter() == "ql" { q = sess.SQL().SelectFrom(sess.SQL().Select("id() AS id", "name").From("artist")) } const pageSize = 13 cursorColumn := "id" paginator := q.Paginate(pageSize) var zerothPage []artistType err = paginator.Page(0).All(&zerothPage) s.NoError(err) s.Equal(pageSize, len(zerothPage)) var firstPage []artistType err = paginator.Page(1).All(&firstPage) s.NoError(err) s.Equal(pageSize, len(firstPage)) s.Equal(zerothPage, firstPage) var secondPage []artistType err = paginator.Page(2).All(&secondPage) s.NoError(err) s.Equal(pageSize, len(secondPage)) totalPages, err := paginator.TotalPages() s.NoError(err) s.NotZero(totalPages) s.Equal(uint(77), totalPages) totalEntries, err := paginator.TotalEntries() s.NoError(err) s.NotZero(totalEntries) s.Equal(uint64(999), totalEntries) var lastPage []artistType err = paginator.Page(totalPages).All(&lastPage) s.NoError(err) s.Equal(11, len(lastPage)) var beyondLastPage []artistType err = paginator.Page(totalPages + 1).All(&beyondLastPage) s.NoError(err) s.Equal(0, len(beyondLastPage)) var hundredthPage []artistType err = paginator.Page(100).All(&hundredthPage) s.NoError(err) s.Equal(0, len(hundredthPage)) for i := uint(0); i < totalPages; i++ { current := paginator.Page(i + 1) var items []artistType err := current.All(&items) if err != nil { s.T().Errorf("%v", err) } s.NoError(err) if len(items) < 1 { s.Equal(totalPages+1, i) break } for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", int64(pageSize*int(i)+j)), items[j].Name) } } paginator = paginator.Cursor(cursorColumn) { current := paginator.Page(1) for i := 0; ; i++ { var items []artistType err := current.All(&items) if err != nil { s.T().Errorf("%v", err) } if len(items) < 1 { s.Equal(int(totalPages), i) break } for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", int64(pageSize*int(i)+j)), items[j].Name) } current = current.NextPage(items[len(items)-1].ID) } } { current := paginator.Page(totalPages) for i := totalPages; ; i-- { var items []artistType err := current.All(&items) s.NoError(err) if len(items) < 1 { s.Equal(uint(0), i) break } for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", pageSize*int(i-1)+j), items[j].Name) } current = current.PrevPage(items[0].ID) } } if s.Adapter() == "ql" { s.T().Skip("Unsupported, see https://github.com/cznic/ql/issues/182") return } { result := sess.Collection("artist").Find() fifteenResults := 15 resultPaginator := result.Paginate(uint(fifteenResults)) count, err := resultPaginator.TotalPages() s.Equal(uint(67), count) s.NoError(err) var items []artistType fifthPage := 5 err = resultPaginator.Page(uint(fifthPage)).All(&items) s.NoError(err) for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", int(fifteenResults)*(fifthPage-1)+j), items[j].Name) } resultPaginator = resultPaginator.Cursor(cursorColumn).Page(1) for i := 0; ; i++ { var items []artistType err = resultPaginator.All(&items) s.NoError(err) if len(items) < 1 { break } for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", fifteenResults*i+j), items[j].Name) } resultPaginator = resultPaginator.NextPage(items[len(items)-1].ID) } resultPaginator = resultPaginator.Cursor(cursorColumn).Page(count) for i := count; ; i-- { var items []artistType err = resultPaginator.All(&items) s.NoError(err) if len(items) < 1 { s.Equal(uint(0), i) break } for j := 0; j < len(items); j++ { s.Equal(fmt.Sprintf("artist-%d", fifteenResults*(int(i)-1)+j), items[j].Name) } resultPaginator = resultPaginator.PrevPage(items[0].ID) } } { // Testing page size 0. paginator := q.Paginate(0) totalPages, err := paginator.TotalPages() s.NoError(err) s.Equal(uint(1), totalPages) totalEntries, err := paginator.TotalEntries() s.NoError(err) s.Equal(uint64(999), totalEntries) var allItems []artistType err = paginator.Page(0).All(&allItems) s.NoError(err) s.Equal(totalEntries, uint64(len(allItems))) } } func (s *SQLTestSuite) TestPaginator_Issue607() { sess := s.Session() err := sess.Collection("artist").Truncate() s.NoError(err) // Add first batch { batch := sess.SQL().InsertInto("artist").Batch(50) go func() { defer batch.Done() for i := 0; i < 49; i++ { value := struct { Name string `db:"name"` }{fmt.Sprintf("artist-1.%d", i)} batch.Values(value) } }() err = batch.Wait() s.NoError(err) s.NoError(batch.Err()) } artists := []*artistType{} paginator := sess.SQL().Select("name").From("artist").Paginate(10) err = paginator.Page(1).All(&artists) s.NoError(err) { totalPages, err := paginator.TotalPages() s.NoError(err) s.NotZero(totalPages) s.Equal(uint(5), totalPages) } // Add second batch { batch := sess.SQL().InsertInto("artist").Batch(50) go func() { defer batch.Done() for i := 0; i < 49; i++ { value := struct { Name string `db:"name"` }{fmt.Sprintf("artist-2.%d", i)} batch.Values(value) } }() err = batch.Wait() s.NoError(err) s.NoError(batch.Err()) } { totalPages, err := paginator.TotalPages() s.NoError(err) s.NotZero(totalPages) s.Equal(uint(10), totalPages, "expect number of pages to change") } artists = []*artistType{} cond := mydb.Cond{"name": mydb.Like("artist-1.%")} if s.Adapter() == "ql" { cond = mydb.Cond{"name": mydb.Like("artist-1.")} } paginator = sess.SQL().Select("name").From("artist").Where(cond).Paginate(10) err = paginator.Page(1).All(&artists) s.NoError(err) { totalPages, err := paginator.TotalPages() s.NoError(err) s.NotZero(totalPages) s.Equal(uint(5), totalPages, "expect same 5 pages from the first batch") } } func (s *SQLTestSuite) TestSession() { sess := s.Session() var all []map[string]interface{} err := sess.Collection("artist").Truncate() s.NoError(err) _, err = sess.SQL().InsertInto("artist").Values(struct { Name string `db:"name"` }{"Rinko Kikuchi"}).Exec() s.NoError(err) // Using explicit iterator. iter := sess.SQL().SelectFrom("artist").Iterator() err = iter.All(&all) s.NoError(err) s.NotZero(all) // Using explicit iterator to fetch one item. var item map[string]interface{} iter = sess.SQL().SelectFrom("artist").Iterator() err = iter.One(&item) s.NoError(err) s.NotZero(item) // Using explicit iterator and NextScan. iter = sess.SQL().SelectFrom("artist").Iterator() var id int var name string if s.Adapter() == "ql" { err = iter.NextScan(&name) id = 1 } else { err = iter.NextScan(&id, &name) } s.NoError(err) s.NotZero(id) s.NotEmpty(name) s.NoError(iter.Close()) err = iter.NextScan(&id, &name) s.Error(err) // Using explicit iterator and ScanOne. iter = sess.SQL().SelectFrom("artist").Iterator() id, name = 0, "" if s.Adapter() == "ql" { err = iter.ScanOne(&name) id = 1 } else { err = iter.ScanOne(&id, &name) } s.NoError(err) s.NotZero(id) s.NotEmpty(name) err = iter.ScanOne(&id, &name) s.Error(err) // Using explicit iterator and Next. iter = sess.SQL().SelectFrom("artist").Iterator() var artist map[string]interface{} for iter.Next(&artist) { if s.Adapter() != "ql" { s.NotZero(artist["id"]) } s.NotEmpty(artist["name"]) } // We should not have any error after finishing successfully exiting a Next() loop. s.Empty(iter.Err()) for i := 0; i < 5; i++ { // But we'll get errors if we attempt to continue using Next(). s.False(iter.Next(&artist)) s.Error(iter.Err()) } // Using implicit iterator. q := sess.SQL().SelectFrom("artist") err = q.All(&all) s.NoError(err) s.NotZero(all) err = sess.Tx(func(tx mydb.Session) error { q := tx.SQL().SelectFrom("artist") s.NotZero(iter) err = q.All(&all) s.NoError(err) s.NotZero(all) return nil }) s.NoError(err) } func (s *SQLTestSuite) TestExhaustConnectionPool() { if s.Adapter() == "ql" { s.T().Skip("Currently not supported.") return } sess := s.Session() errRolledBack := errors.New("rolled back") var wg sync.WaitGroup for i := 0; i < 100; i++ { s.T().Logf("Tx %d: Pending", i) wg.Add(1) go func(wg *sync.WaitGroup, i int) { defer wg.Done() // Requesting a new transaction session. start := time.Now() s.T().Logf("Tx: %d: NewTx", i) expectError := false if i%2 == 1 { expectError = true } err := sess.Tx(func(tx mydb.Session) error { s.T().Logf("Tx %d: OK (time to connect: %v)", i, time.Since(start)) // Let's suppose that we do a bunch of complex stuff and that the // transaction lasts 3 seconds. time.Sleep(time.Second * 3) if expectError { if _, err := tx.SQL().DeleteFrom("artist").Exec(); err != nil { return err } return errRolledBack } var account map[string]interface{} if err := tx.Collection("artist").Find().One(&account); err != nil { return err } return nil }) if expectError { s.Error(err) s.True(errors.Is(err, errRolledBack)) } else { s.NoError(err) } }(&wg, i) } wg.Wait() } func (s *SQLTestSuite) TestCustomType() { // See https://github.com/upper/db/issues/332 sess := s.Session() artist := sess.Collection("artist") err := artist.Truncate() s.NoError(err) id, err := artist.Insert(artistWithCustomType{ Custom: customType{Val: []byte("some name")}, }) s.NoError(err) s.NotNil(id) var bar artistWithCustomType err = artist.Find(id).One(&bar) s.NoError(err) s.Equal("foo: some name", string(bar.Custom.Val)) } func (s *SQLTestSuite) Test_Issue565() { s.Session().Collection("birthdays").Insert(&birthday{ Name: "Lucy", Born: time.Now(), }) parentCtx := context.WithValue(s.Session().Context(), "carry", 1) s.NotZero(parentCtx.Value("carry")) { ctx, cancel := context.WithTimeout(parentCtx, time.Nanosecond) defer cancel() sess := s.Session() sess = sess.WithContext(ctx) var result birthday err := sess.Collection("birthdays").Find().Select("name").One(&result) s.Error(err) s.Zero(result.Name) s.NotZero(ctx.Value("carry")) } { ctx, cancel := context.WithTimeout(parentCtx, time.Second*10) cancel() // cancel before passing sess := s.Session().WithContext(ctx) var result birthday err := sess.Collection("birthdays").Find().Select("name").One(&result) s.Error(err) s.Zero(result.Name) s.NotZero(ctx.Value("carry")) } { ctx, cancel := context.WithTimeout(parentCtx, time.Second) defer cancel() sess := s.Session().WithContext(ctx) var result birthday err := sess.Collection("birthdays").Find().Select("name").One(&result) s.NoError(err) s.NotZero(result.Name) s.NotZero(ctx.Value("carry")) } } func (s *SQLTestSuite) TestSelectFromSubquery() { sess := s.Session() { var artists []artistType q := sess.SQL().SelectFrom( sess.SQL().SelectFrom("artist").Where(mydb.Cond{ "name": mydb.IsNotNull(), }), ).As("_q") err := q.All(&artists) s.NoError(err) s.NotZero(len(artists)) } { var artists []artistType q := sess.SQL().SelectFrom( sess.Collection("artist").Find(mydb.Cond{ "name": mydb.IsNotNull(), }), ).As("_q") err := q.All(&artists) s.NoError(err) s.NotZero(len(artists)) } }