mydb/internal/testsuite/sql_suite.go
2023-09-18 15:15:42 +08:00

1975 lines
41 KiB
Go

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))
}
}