1405 lines
30 KiB
Go
1405 lines
30 KiB
Go
package postgresql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.hexq.cn/tiglog/mydb"
|
|
"git.hexq.cn/tiglog/mydb/internal/testsuite"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type customJSONBObjectArray []customJSONB
|
|
|
|
func (customJSONBObjectArray) ConvertValue(in interface{}) interface {
|
|
sql.Scanner
|
|
driver.Valuer
|
|
} {
|
|
return &JSONB{in}
|
|
}
|
|
|
|
type customJSONBObjectMap map[string]customJSONB
|
|
|
|
func (c customJSONBObjectMap) Value() (driver.Value, error) {
|
|
return JSONBValue(c)
|
|
}
|
|
|
|
func (c *customJSONBObjectMap) Scan(src interface{}) error {
|
|
return ScanJSONB(c, src)
|
|
}
|
|
|
|
type customJSONB struct {
|
|
N string `json:"name"`
|
|
V float64 `json:"value"`
|
|
|
|
*JSONBConverter
|
|
}
|
|
|
|
type int64Compat int64
|
|
|
|
type uintCompat uint
|
|
|
|
type stringCompat string
|
|
|
|
type uint8Compat uint8
|
|
|
|
type uint8CompatArray []uint8Compat
|
|
|
|
func (ua uint8CompatArray) Value() (driver.Value, error) {
|
|
v := make([]byte, len(ua))
|
|
for i := range ua {
|
|
v[i] = byte(ua[i])
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func (ua *uint8CompatArray) Scan(src interface{}) error {
|
|
decoded := Bytea{}
|
|
if err := decoded.Scan(src); err != nil {
|
|
return nil
|
|
}
|
|
if len(decoded) < 1 {
|
|
*ua = nil
|
|
return nil
|
|
}
|
|
*ua = make([]uint8Compat, len(decoded))
|
|
for i := range decoded {
|
|
(*ua)[i] = uint8Compat(decoded[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type int64CompatArray []int64Compat
|
|
|
|
func (i64a int64CompatArray) Value() (driver.Value, error) {
|
|
v := make(Int64Array, len(i64a))
|
|
for i := range i64a {
|
|
v[i] = int64(i64a[i])
|
|
}
|
|
return v.Value()
|
|
}
|
|
|
|
func (i64a *int64CompatArray) Scan(src interface{}) error {
|
|
s := Int64Array{}
|
|
if err := s.Scan(src); err != nil {
|
|
return err
|
|
}
|
|
dst := make([]int64Compat, len(s))
|
|
for i := range s {
|
|
dst[i] = int64Compat(s[i])
|
|
}
|
|
if len(dst) < 1 {
|
|
return nil
|
|
}
|
|
*i64a = dst
|
|
return nil
|
|
}
|
|
|
|
type uintCompatArray []uintCompat
|
|
|
|
type AdapterTests struct {
|
|
testsuite.Suite
|
|
}
|
|
|
|
func (s *AdapterTests) SetupSuite() {
|
|
s.Helper = &Helper{}
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue469_BadConnection() {
|
|
sess := s.Session()
|
|
|
|
// Ask the PostgreSQL server to disconnect sessions that remain inactive for more
|
|
// than 1 second.
|
|
_, err := sess.SQL().Exec(`SET SESSION idle_in_transaction_session_timeout=1000`)
|
|
s.NoError(err)
|
|
|
|
// Remain inactive for 2 seconds.
|
|
time.Sleep(time.Second * 2)
|
|
|
|
// A query should start a new connection, even if the server disconnected us.
|
|
_, err = sess.Collection("artist").Find().Count()
|
|
s.NoError(err)
|
|
|
|
// This is a new session, ask the PostgreSQL server to disconnect sessions that
|
|
// remain inactive for more than 1 second.
|
|
_, err = sess.SQL().Exec(`SET SESSION idle_in_transaction_session_timeout=1000`)
|
|
s.NoError(err)
|
|
|
|
// Remain inactive for 2 seconds.
|
|
time.Sleep(time.Second * 2)
|
|
|
|
// At this point the server should have disconnected us. Let's try to create
|
|
// a transaction anyway.
|
|
err = sess.Tx(func(sess mydb.Session) error {
|
|
var err error
|
|
|
|
_, err = sess.Collection("artist").Find().Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
s.NoError(err)
|
|
|
|
// This is a new session, ask the PostgreSQL server to disconnect sessions that
|
|
// remain inactive for more than 1 second.
|
|
_, err = sess.SQL().Exec(`SET SESSION idle_in_transaction_session_timeout=1000`)
|
|
s.NoError(err)
|
|
|
|
err = sess.Tx(func(sess mydb.Session) error {
|
|
var err error
|
|
|
|
// This query should succeed.
|
|
_, err = sess.Collection("artist").Find().Count()
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
// Remain inactive for 2 seconds.
|
|
time.Sleep(time.Second * 2)
|
|
|
|
// This query should fail because the server disconnected us in the middle
|
|
// of a transaction.
|
|
_, err = sess.Collection("artist").Find().Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
s.Error(err, "Expecting an error (can't recover from this)")
|
|
}
|
|
|
|
func testPostgreSQLTypes(t *testing.T, sess mydb.Session) {
|
|
type PGTypeInline struct {
|
|
IntegerArrayPtr *Int64Array `db:"integer_array_ptr,omitempty"`
|
|
StringArrayPtr *StringArray `db:"string_array_ptr,omitempty"`
|
|
JSONBMapPtr *JSONBMap `db:"jsonb_map_ptr,omitempty"`
|
|
}
|
|
|
|
type PGTypeAutoInline struct {
|
|
AutoIntegerArray []int64 `db:"auto_integer_array"`
|
|
AutoStringArray []string `db:"auto_string_array"`
|
|
AutoJSONBMap map[string]interface{} `db:"auto_jsonb_map"`
|
|
AutoJSONBMapString map[string]interface{} `db:"auto_jsonb_map_string"`
|
|
AutoJSONBMapInteger map[string]interface{} `db:"auto_jsonb_map_integer"`
|
|
}
|
|
|
|
type PGType struct {
|
|
ID int64 `db:"id,omitempty"`
|
|
|
|
UInt8Value uint8Compat `db:"uint8_value"`
|
|
UInt8ValueArray uint8CompatArray `db:"uint8_value_array"`
|
|
|
|
Int64Value int64Compat `db:"int64_value"`
|
|
Int64ValueArray *int64CompatArray `db:"int64_value_array"`
|
|
|
|
IntegerArray Int64Array `db:"integer_array"`
|
|
StringArray StringArray `db:"string_array,stringarray"`
|
|
JSONBMap JSONBMap `db:"jsonb_map"`
|
|
|
|
RawJSONBMap *json.RawMessage `db:"raw_jsonb_map,omitempty"`
|
|
RawJSONBText *json.RawMessage `db:"raw_jsonb_text,omitempty"`
|
|
|
|
PGTypeInline `db:",inline"`
|
|
|
|
PGTypeAutoInline `db:",inline"`
|
|
|
|
JSONBObject JSONB `db:"jsonb_object"`
|
|
JSONBArray JSONBArray `db:"jsonb_array"`
|
|
|
|
CustomJSONBObject customJSONB `db:"custom_jsonb_object"`
|
|
AutoCustomJSONBObject customJSONB `db:"auto_custom_jsonb_object"`
|
|
|
|
CustomJSONBObjectPtr *customJSONB `db:"custom_jsonb_object_ptr,omitempty"`
|
|
AutoCustomJSONBObjectPtr *customJSONB `db:"auto_custom_jsonb_object_ptr,omitempty"`
|
|
|
|
AutoCustomJSONBObjectArray *customJSONBObjectArray `db:"auto_custom_jsonb_object_array"`
|
|
AutoCustomJSONBObjectMap *customJSONBObjectMap `db:"auto_custom_jsonb_object_map"`
|
|
|
|
StringValue string `db:"string_value"`
|
|
IntegerValue int64 `db:"integer_value"`
|
|
VarcharValue string `db:"varchar_value"`
|
|
DecimalValue float64 `db:"decimal_value"`
|
|
|
|
Int64CompatValue int64Compat `db:"integer_compat_value"`
|
|
UIntCompatValue uintCompat `db:"uinteger_compat_value"`
|
|
StringCompatValue stringCompat `db:"string_compat_value"`
|
|
|
|
Int64CompatValueJSONBArray JSONBArray `db:"integer_compat_value_jsonb_array"`
|
|
UIntCompatValueJSONBArray JSONBArray `db:"uinteger_compat_value_jsonb_array"`
|
|
StringCompatValueJSONBArray JSONBArray `db:"string_compat_value_jsonb_array"`
|
|
|
|
StringValuePtr *string `db:"string_value_ptr,omitempty"`
|
|
IntegerValuePtr *int64 `db:"integer_value_ptr,omitempty"`
|
|
VarcharValuePtr *string `db:"varchar_value_ptr,omitempty"`
|
|
DecimalValuePtr *float64 `db:"decimal_value_ptr,omitempty"`
|
|
|
|
UUIDValueString *string `db:"uuid_value_string,omitempty"`
|
|
}
|
|
|
|
integerValue := int64(10)
|
|
stringValue := string("ten")
|
|
decimalValue := float64(10.0)
|
|
|
|
uuidStringValue := "52356d08-6a16-4839-9224-75f0a547e13c"
|
|
|
|
integerArrayValue := Int64Array{1, 2, 3, 4}
|
|
stringArrayValue := StringArray{"a", "b", "c"}
|
|
jsonbMapValue := JSONBMap{"Hello": "World"}
|
|
rawJSONBMap := json.RawMessage(`{"foo": "bar"}`)
|
|
rawJSONBText := json.RawMessage(`{"age": [{">": "1h"}]}`)
|
|
|
|
testValue := "Hello world!"
|
|
|
|
origPgTypeTests := []PGType{
|
|
PGType{
|
|
UUIDValueString: &uuidStringValue,
|
|
},
|
|
PGType{
|
|
UInt8Value: 7,
|
|
UInt8ValueArray: uint8CompatArray{1, 2, 3, 4, 5, 6},
|
|
},
|
|
PGType{
|
|
Int64Value: -1,
|
|
Int64ValueArray: &int64CompatArray{1, 2, 3, -4, 5, 6},
|
|
},
|
|
PGType{
|
|
UInt8Value: 1,
|
|
UInt8ValueArray: uint8CompatArray{1, 2, 3, 4, 5, 6},
|
|
},
|
|
PGType{
|
|
Int64Value: 1,
|
|
Int64ValueArray: &int64CompatArray{7, 7, 7},
|
|
},
|
|
PGType{
|
|
Int64Value: 1,
|
|
},
|
|
PGType{
|
|
Int64Value: 99,
|
|
Int64ValueArray: nil,
|
|
},
|
|
PGType{
|
|
Int64CompatValue: -5,
|
|
UIntCompatValue: 3,
|
|
StringCompatValue: "abc",
|
|
},
|
|
PGType{
|
|
Int64CompatValueJSONBArray: JSONBArray{1.0, -2.0, 3.0, -4.0},
|
|
UIntCompatValueJSONBArray: JSONBArray{1.0, 2.0, 3.0, 4.0},
|
|
StringCompatValueJSONBArray: JSONBArray{"a", "b", "", "c"},
|
|
},
|
|
PGType{
|
|
Int64CompatValueJSONBArray: JSONBArray(nil),
|
|
UIntCompatValueJSONBArray: JSONBArray(nil),
|
|
StringCompatValueJSONBArray: JSONBArray(nil),
|
|
},
|
|
PGType{
|
|
IntegerValuePtr: &integerValue,
|
|
StringValuePtr: &stringValue,
|
|
DecimalValuePtr: &decimalValue,
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoJSONBMapString: map[string]interface{}{"a": "x", "b": "67"},
|
|
AutoJSONBMapInteger: map[string]interface{}{"a": 12.0, "b": 13.0},
|
|
},
|
|
},
|
|
PGType{
|
|
RawJSONBMap: &rawJSONBMap,
|
|
RawJSONBText: &rawJSONBText,
|
|
},
|
|
PGType{
|
|
IntegerValue: integerValue,
|
|
StringValue: stringValue,
|
|
DecimalValue: decimalValue,
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{1, 2, 3, 4},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoIntegerArray: Int64Array{1, 2, 3, 4},
|
|
AutoStringArray: nil,
|
|
},
|
|
},
|
|
PGType{
|
|
AutoCustomJSONBObjectArray: &customJSONBObjectArray{
|
|
customJSONB{
|
|
N: "Hello",
|
|
},
|
|
customJSONB{
|
|
N: "World",
|
|
},
|
|
},
|
|
AutoCustomJSONBObjectMap: &customJSONBObjectMap{
|
|
"a": customJSONB{
|
|
N: "Hello",
|
|
},
|
|
"b": customJSONB{
|
|
N: "World",
|
|
},
|
|
},
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoJSONBMap: map[string]interface{}{
|
|
"Hello": "world",
|
|
"Roses": "red",
|
|
},
|
|
},
|
|
JSONBArray: JSONBArray{float64(1), float64(2), float64(3), float64(4)},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoIntegerArray: nil,
|
|
},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoJSONBMap: map[string]interface{}{},
|
|
},
|
|
JSONBArray: JSONBArray{},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoJSONBMap: map[string]interface{}(nil),
|
|
},
|
|
JSONBArray: JSONBArray(nil),
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoStringArray: []string{"aaa", "bbb", "ccc"},
|
|
},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoStringArray: nil,
|
|
},
|
|
},
|
|
PGType{
|
|
PGTypeAutoInline: PGTypeAutoInline{
|
|
AutoJSONBMap: map[string]interface{}{"hello": "world!"},
|
|
},
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{1, 2, 3, 4},
|
|
StringArray: []string{"a", "boo", "bar"},
|
|
},
|
|
PGType{
|
|
StringValue: stringValue,
|
|
DecimalValue: decimalValue,
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{},
|
|
},
|
|
PGType{
|
|
StringArray: []string{},
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{},
|
|
StringArray: []string{},
|
|
},
|
|
PGType{},
|
|
PGType{
|
|
IntegerArray: []int64{1},
|
|
StringArray: []string{"a"},
|
|
},
|
|
PGType{
|
|
PGTypeInline: PGTypeInline{
|
|
IntegerArrayPtr: &integerArrayValue,
|
|
StringArrayPtr: &stringArrayValue,
|
|
JSONBMapPtr: &jsonbMapValue,
|
|
},
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{0, 0, 0, 0},
|
|
StringValue: testValue,
|
|
CustomJSONBObject: customJSONB{
|
|
N: "Hello",
|
|
},
|
|
AutoCustomJSONBObject: customJSONB{
|
|
N: "World",
|
|
},
|
|
StringArray: []string{"", "", "", ``, `""`},
|
|
},
|
|
PGType{
|
|
CustomJSONBObject: customJSONB{},
|
|
AutoCustomJSONBObject: customJSONB{},
|
|
},
|
|
PGType{
|
|
CustomJSONBObject: customJSONB{
|
|
N: "Hello 1",
|
|
},
|
|
AutoCustomJSONBObject: customJSONB{
|
|
N: "World 2",
|
|
},
|
|
},
|
|
PGType{
|
|
CustomJSONBObjectPtr: nil,
|
|
AutoCustomJSONBObjectPtr: nil,
|
|
},
|
|
PGType{
|
|
CustomJSONBObjectPtr: &customJSONB{},
|
|
AutoCustomJSONBObjectPtr: &customJSONB{},
|
|
},
|
|
PGType{
|
|
CustomJSONBObjectPtr: &customJSONB{
|
|
N: "Hello 3",
|
|
},
|
|
AutoCustomJSONBObjectPtr: &customJSONB{
|
|
N: "World 4",
|
|
},
|
|
},
|
|
PGType{
|
|
StringValue: testValue,
|
|
},
|
|
PGType{
|
|
IntegerValue: integerValue,
|
|
IntegerValuePtr: &integerValue,
|
|
CustomJSONBObject: customJSONB{
|
|
V: 4.4,
|
|
},
|
|
},
|
|
PGType{
|
|
StringArray: []string{"a", "boo", "bar"},
|
|
},
|
|
PGType{
|
|
StringArray: []string{"a", "boo", "bar", `""`},
|
|
CustomJSONBObject: customJSONB{},
|
|
},
|
|
PGType{
|
|
IntegerArray: []int64{0},
|
|
StringArray: []string{""},
|
|
},
|
|
PGType{
|
|
CustomJSONBObject: customJSONB{
|
|
N: "Peter",
|
|
V: 5.56,
|
|
},
|
|
},
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
pgTypeTests := make([]PGType, len(origPgTypeTests))
|
|
perm := rand.Perm(len(origPgTypeTests))
|
|
for i, v := range perm {
|
|
pgTypeTests[v] = origPgTypeTests[i]
|
|
}
|
|
|
|
for i := range pgTypeTests {
|
|
record, err := sess.Collection("pg_types").Insert(pgTypeTests[i])
|
|
assert.NoError(t, err)
|
|
|
|
var actual PGType
|
|
err = sess.Collection("pg_types").Find(record.ID()).One(&actual)
|
|
assert.NoError(t, err)
|
|
|
|
expected := pgTypeTests[i]
|
|
expected.ID = record.ID().(int64)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
for i := range pgTypeTests {
|
|
row, err := sess.SQL().InsertInto("pg_types").Values(pgTypeTests[i]).Returning("id").QueryRow()
|
|
assert.NoError(t, err)
|
|
|
|
var id int64
|
|
err = row.Scan(&id)
|
|
assert.NoError(t, err)
|
|
|
|
var actual PGType
|
|
err = sess.Collection("pg_types").Find(id).One(&actual)
|
|
assert.NoError(t, err)
|
|
|
|
expected := pgTypeTests[i]
|
|
expected.ID = id
|
|
|
|
assert.Equal(t, expected, actual)
|
|
|
|
var actual2 PGType
|
|
err = sess.SQL().SelectFrom("pg_types").Where("id = ?", id).One(&actual2)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expected, actual2)
|
|
}
|
|
|
|
inserter := sess.SQL().InsertInto("pg_types")
|
|
for i := range pgTypeTests {
|
|
inserter = inserter.Values(pgTypeTests[i])
|
|
}
|
|
_, err := inserter.Exec()
|
|
assert.NoError(t, err)
|
|
|
|
err = sess.Collection("pg_types").Truncate()
|
|
assert.NoError(t, err)
|
|
|
|
batch := sess.SQL().InsertInto("pg_types").Batch(50)
|
|
go func() {
|
|
defer batch.Done()
|
|
for i := range pgTypeTests {
|
|
batch.Values(pgTypeTests[i])
|
|
}
|
|
}()
|
|
|
|
err = batch.Wait()
|
|
assert.NoError(t, err)
|
|
|
|
var values []PGType
|
|
err = sess.SQL().SelectFrom("pg_types").All(&values)
|
|
assert.NoError(t, err)
|
|
|
|
for i := range values {
|
|
expected := pgTypeTests[i]
|
|
expected.ID = values[i].ID
|
|
|
|
assert.Equal(t, expected, values[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) TestOptionTypes() {
|
|
sess := s.Session()
|
|
|
|
optionTypes := sess.Collection("option_types")
|
|
err := optionTypes.Truncate()
|
|
s.NoError(err)
|
|
|
|
// TODO: lets do some benchmarking on these auto-wrapped option types..
|
|
|
|
// TODO: add nullable jsonb field mapped to a []string
|
|
|
|
// A struct with wrapped option types defined in the struct tags
|
|
// for postgres string array and jsonb types
|
|
type optionType struct {
|
|
ID int64 `db:"id,omitempty"`
|
|
Name string `db:"name"`
|
|
Tags []string `db:"tags"`
|
|
Settings map[string]interface{} `db:"settings"`
|
|
}
|
|
|
|
// Item 1
|
|
item1 := optionType{
|
|
Name: "Food",
|
|
Tags: []string{"toronto", "pizza"},
|
|
Settings: map[string]interface{}{"a": 1, "b": 2},
|
|
}
|
|
|
|
record, err := optionTypes.Insert(item1)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item1Chk optionType
|
|
err = optionTypes.Find(record).One(&item1Chk)
|
|
s.NoError(err)
|
|
|
|
s.Equal(float64(1), item1Chk.Settings["a"])
|
|
s.Equal("toronto", item1Chk.Tags[0])
|
|
|
|
// Item 1 B
|
|
item1b := &optionType{
|
|
Name: "Golang",
|
|
Tags: []string{"love", "it"},
|
|
Settings: map[string]interface{}{"go": 1, "lang": 2},
|
|
}
|
|
|
|
record, err = optionTypes.Insert(item1b)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item1bChk optionType
|
|
err = optionTypes.Find(record).One(&item1bChk)
|
|
s.NoError(err)
|
|
|
|
s.Equal(float64(1), item1bChk.Settings["go"])
|
|
s.Equal("love", item1bChk.Tags[0])
|
|
|
|
// Item 1 C
|
|
item1c := &optionType{
|
|
Name: "Sup", Tags: []string{}, Settings: map[string]interface{}{},
|
|
}
|
|
|
|
record, err = optionTypes.Insert(item1c)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item1cChk optionType
|
|
err = optionTypes.Find(record).One(&item1cChk)
|
|
s.NoError(err)
|
|
|
|
s.Zero(len(item1cChk.Tags))
|
|
s.Zero(len(item1cChk.Settings))
|
|
|
|
// An option type to pointer jsonb field
|
|
type optionType2 struct {
|
|
ID int64 `db:"id,omitempty"`
|
|
Name string `db:"name"`
|
|
Tags StringArray `db:"tags"`
|
|
Settings *JSONBMap `db:"settings"`
|
|
}
|
|
|
|
item2 := optionType2{
|
|
Name: "JS", Tags: []string{"hi", "bye"}, Settings: nil,
|
|
}
|
|
|
|
record, err = optionTypes.Insert(item2)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item2Chk optionType2
|
|
res := optionTypes.Find(record)
|
|
err = res.One(&item2Chk)
|
|
s.NoError(err)
|
|
|
|
s.Equal(record.ID().(int64), item2Chk.ID)
|
|
|
|
s.Equal(item2Chk.Name, item2.Name)
|
|
|
|
s.Equal(item2Chk.Tags[0], item2.Tags[0])
|
|
s.Equal(len(item2Chk.Tags), len(item2.Tags))
|
|
|
|
// Update the value
|
|
m := JSONBMap{}
|
|
m["lang"] = "javascript"
|
|
m["num"] = 31337
|
|
item2.Settings = &m
|
|
err = res.Update(item2)
|
|
s.NoError(err)
|
|
|
|
err = res.One(&item2Chk)
|
|
s.NoError(err)
|
|
|
|
s.Equal(float64(31337), (*item2Chk.Settings)["num"].(float64))
|
|
|
|
s.Equal("javascript", (*item2Chk.Settings)["lang"])
|
|
|
|
// An option type to pointer string array field
|
|
type optionType3 struct {
|
|
ID int64 `db:"id,omitempty"`
|
|
Name string `db:"name"`
|
|
Tags *StringArray `db:"tags"`
|
|
Settings JSONBMap `db:"settings"`
|
|
}
|
|
|
|
item3 := optionType3{
|
|
Name: "Julia",
|
|
Tags: nil,
|
|
Settings: JSONBMap{"girl": true, "lang": true},
|
|
}
|
|
|
|
record, err = optionTypes.Insert(item3)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item3Chk optionType2
|
|
err = optionTypes.Find(record).One(&item3Chk)
|
|
s.NoError(err)
|
|
}
|
|
|
|
type Settings struct {
|
|
Name string `json:"name"`
|
|
Num int64 `json:"num"`
|
|
}
|
|
|
|
func (s *Settings) Scan(src interface{}) error {
|
|
return ScanJSONB(s, src)
|
|
}
|
|
func (s Settings) Value() (driver.Value, error) {
|
|
return JSONBValue(s)
|
|
}
|
|
|
|
func (s *AdapterTests) TestOptionTypeJsonbStruct() {
|
|
sess := s.Session()
|
|
|
|
optionTypes := sess.Collection("option_types")
|
|
|
|
err := optionTypes.Truncate()
|
|
s.NoError(err)
|
|
|
|
type OptionType struct {
|
|
ID int64 `db:"id,omitempty"`
|
|
Name string `db:"name"`
|
|
Tags StringArray `db:"tags"`
|
|
Settings Settings `db:"settings"`
|
|
}
|
|
|
|
item1 := &OptionType{
|
|
Name: "Hi",
|
|
Tags: []string{"aah", "ok"},
|
|
Settings: Settings{Name: "a", Num: 123},
|
|
}
|
|
|
|
record, err := optionTypes.Insert(item1)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var item1Chk OptionType
|
|
err = optionTypes.Find(record).One(&item1Chk)
|
|
s.NoError(err)
|
|
|
|
s.Equal(2, len(item1Chk.Tags))
|
|
s.Equal("aah", item1Chk.Tags[0])
|
|
s.Equal("a", item1Chk.Settings.Name)
|
|
s.Equal(int64(123), item1Chk.Settings.Num)
|
|
}
|
|
|
|
func (s *AdapterTests) TestSchemaCollection() {
|
|
sess := s.Session()
|
|
|
|
col := sess.Collection("test_schema.test")
|
|
_, err := col.Insert(map[string]int{"id": 9})
|
|
s.Equal(nil, err)
|
|
|
|
var dump []map[string]int
|
|
err = col.Find().All(&dump)
|
|
s.Nil(err)
|
|
s.Equal(1, len(dump))
|
|
s.Equal(9, dump[0]["id"])
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue340_MaxOpenConns() {
|
|
sess := s.Session()
|
|
|
|
sess.SetMaxOpenConns(5)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 30; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
|
|
_, err := sess.SQL().Exec(fmt.Sprintf(`SELECT pg_sleep(1.%d)`, i))
|
|
if err != nil {
|
|
s.T().Errorf("%v", err)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
sess.SetMaxOpenConns(0)
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue370_InsertUUID() {
|
|
sess := s.Session()
|
|
|
|
{
|
|
type itemT struct {
|
|
ID *uuid.UUID `db:"id"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
newUUID := uuid.New()
|
|
|
|
item1 := itemT{
|
|
ID: &newUUID,
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_370")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = col.InsertReturning(&item1)
|
|
s.NoError(err)
|
|
|
|
var item2 itemT
|
|
err = col.Find(item1.ID).One(&item2)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item2.Name)
|
|
|
|
var item3 itemT
|
|
err = col.Find(mydb.Cond{"id": item1.ID}).One(&item3)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item3.Name)
|
|
}
|
|
|
|
{
|
|
type itemT struct {
|
|
ID uuid.UUID `db:"id"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
item1 := itemT{
|
|
ID: uuid.New(),
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_370")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = col.InsertReturning(&item1)
|
|
s.NoError(err)
|
|
|
|
var item2 itemT
|
|
err = col.Find(item1.ID).One(&item2)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item2.Name)
|
|
|
|
var item3 itemT
|
|
err = col.Find(mydb.Cond{"id": item1.ID}).One(&item3)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item3.Name)
|
|
}
|
|
|
|
{
|
|
type itemT struct {
|
|
ID Int64Array `db:"id"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
item1 := itemT{
|
|
ID: Int64Array{1, 2, 3},
|
|
Name: "Vojtech",
|
|
}
|
|
|
|
col := sess.Collection("issue_370_2")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = col.InsertReturning(&item1)
|
|
s.NoError(err)
|
|
|
|
var item2 itemT
|
|
err = col.Find(item1.ID).One(&item2)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item2.Name)
|
|
|
|
var item3 itemT
|
|
err = col.Find(mydb.Cond{"id": item1.ID}).One(&item3)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item3.Name)
|
|
}
|
|
}
|
|
|
|
type issue602Organization struct {
|
|
ID string `json:"id" db:"id,omitempty"`
|
|
Name string `json:"name" db:"name"`
|
|
CreatedAt time.Time `json:"created_at,omitempty" db:"created_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at,omitempty"`
|
|
}
|
|
|
|
type issue602OrganizationStore struct {
|
|
mydb.Store
|
|
}
|
|
|
|
func (r *issue602Organization) BeforeUpdate(mydb.Session) error {
|
|
return nil
|
|
}
|
|
|
|
func (r *issue602Organization) Store(sess mydb.Session) mydb.Store {
|
|
return issue602OrganizationStore{sess.Collection("issue_602_organizations")}
|
|
}
|
|
|
|
var _ interface {
|
|
mydb.Record
|
|
mydb.BeforeUpdateHook
|
|
} = &issue602Organization{}
|
|
|
|
func (s *AdapterTests) Test_Issue602_IncorrectBinaryFormat() {
|
|
settingsWithBinaryMode := ConnectionURL{
|
|
Database: settings.Database,
|
|
User: settings.User,
|
|
Password: settings.Password,
|
|
Host: settings.Host,
|
|
Options: map[string]string{
|
|
"timezone": testsuite.TimeZone,
|
|
//"binary_parameters": "yes",
|
|
},
|
|
}
|
|
|
|
sess, err := Open(settingsWithBinaryMode)
|
|
if err != nil {
|
|
s.T().Errorf("%v", err)
|
|
}
|
|
|
|
{
|
|
item := issue602Organization{
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_602_organizations")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = sess.Save(&item)
|
|
s.NoError(err)
|
|
}
|
|
|
|
{
|
|
item := issue602Organization{
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_602_organizations")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = col.InsertReturning(&item)
|
|
s.NoError(err)
|
|
}
|
|
|
|
{
|
|
newUUID := uuid.New()
|
|
|
|
item := issue602Organization{
|
|
ID: newUUID.String(),
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_602_organizations")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
id, err := col.Insert(item)
|
|
s.NoError(err)
|
|
s.NotZero(id)
|
|
}
|
|
|
|
{
|
|
item := issue602Organization{
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("issue_602_organizations")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = sess.Save(&item)
|
|
s.NoError(err)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) TestInsertVarcharPrimaryKey() {
|
|
sess := s.Session()
|
|
|
|
{
|
|
type itemT struct {
|
|
Address string `db:"address"`
|
|
Name string `db:"name"`
|
|
}
|
|
|
|
item1 := itemT{
|
|
Address: "1234",
|
|
Name: "Jonny",
|
|
}
|
|
|
|
col := sess.Collection("varchar_primary_key")
|
|
err := col.Truncate()
|
|
s.NoError(err)
|
|
|
|
err = col.InsertReturning(&item1)
|
|
s.NoError(err)
|
|
|
|
var item2 itemT
|
|
err = col.Find(mydb.Cond{"address": item1.Address}).One(&item2)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item2.Name)
|
|
|
|
var item3 itemT
|
|
err = col.Find(mydb.Cond{"address": item1.Address}).One(&item3)
|
|
s.NoError(err)
|
|
s.Equal(item1.Name, item3.Name)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue409_TxOptions() {
|
|
sess := s.Session()
|
|
|
|
{
|
|
err := sess.TxContext(context.Background(), func(tx mydb.Session) error {
|
|
col := tx.Collection("publication")
|
|
|
|
row := map[string]interface{}{
|
|
"title": "foo",
|
|
"author_id": 1,
|
|
}
|
|
err := col.InsertReturning(&row)
|
|
s.Error(err)
|
|
|
|
return err
|
|
}, &sql.TxOptions{
|
|
ReadOnly: true,
|
|
})
|
|
s.Error(err)
|
|
s.True(strings.Contains(err.Error(), "read-only transaction"))
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) TestEscapeQuestionMark() {
|
|
sess := s.Session()
|
|
|
|
var val bool
|
|
|
|
{
|
|
res, err := sess.SQL().QueryRow(`SELECT '{"mykey":["val1", "val2"]}'::jsonb->'mykey' ?? ?`, "val2")
|
|
s.NoError(err)
|
|
|
|
err = res.Scan(&val)
|
|
s.NoError(err)
|
|
s.Equal(true, val)
|
|
}
|
|
|
|
{
|
|
res, err := sess.SQL().QueryRow(`SELECT ?::jsonb->'mykey' ?? ?`, `{"mykey":["val1", "val2"]}`, `val2`)
|
|
s.NoError(err)
|
|
|
|
err = res.Scan(&val)
|
|
s.NoError(err)
|
|
s.Equal(true, val)
|
|
}
|
|
|
|
{
|
|
res, err := sess.SQL().QueryRow(`SELECT ?::jsonb->? ?? ?`, `{"mykey":["val1", "val2"]}`, `mykey`, `val2`)
|
|
s.NoError(err)
|
|
|
|
err = res.Scan(&val)
|
|
s.NoError(err)
|
|
s.Equal(true, val)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue391_TextMode() {
|
|
testPostgreSQLTypes(s.T(), s.Session())
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue391_BinaryMode() {
|
|
settingsWithBinaryMode := ConnectionURL{
|
|
Database: settings.Database,
|
|
User: settings.User,
|
|
Password: settings.Password,
|
|
Host: settings.Host,
|
|
Options: map[string]string{
|
|
"timezone": testsuite.TimeZone,
|
|
//"binary_parameters": "yes",
|
|
},
|
|
}
|
|
|
|
sess, err := Open(settingsWithBinaryMode)
|
|
if err != nil {
|
|
s.T().Errorf("%v", err)
|
|
}
|
|
defer sess.Close()
|
|
|
|
testPostgreSQLTypes(s.T(), sess)
|
|
}
|
|
|
|
func (s *AdapterTests) TestStringAndInt64Array() {
|
|
sess := s.Session()
|
|
driver := sess.Driver().(*sql.DB)
|
|
|
|
defer func() {
|
|
_, _ = driver.Exec(`DROP TABLE IF EXISTS array_types`)
|
|
}()
|
|
|
|
if _, err := driver.Exec(`
|
|
CREATE TABLE array_types (
|
|
id serial primary key,
|
|
integers bigint[] DEFAULT NULL,
|
|
strings varchar(64)[]
|
|
)`); err != nil {
|
|
s.NoError(err)
|
|
}
|
|
|
|
arrayTypes := sess.Collection("array_types")
|
|
err := arrayTypes.Truncate()
|
|
s.NoError(err)
|
|
|
|
type arrayType struct {
|
|
ID int64 `db:"id,pk"`
|
|
Integers Int64Array `db:"integers"`
|
|
Strings StringArray `db:"strings"`
|
|
}
|
|
|
|
tt := []arrayType{
|
|
// Test nil arrays.
|
|
arrayType{
|
|
ID: 1,
|
|
Integers: nil,
|
|
Strings: nil,
|
|
},
|
|
|
|
// Test empty arrays.
|
|
arrayType{
|
|
ID: 2,
|
|
Integers: []int64{},
|
|
Strings: []string{},
|
|
},
|
|
|
|
// Test non-empty arrays.
|
|
arrayType{
|
|
ID: 3,
|
|
Integers: []int64{1, 2, 3},
|
|
Strings: []string{"1", "2", "3"},
|
|
},
|
|
}
|
|
|
|
for _, item := range tt {
|
|
record, err := arrayTypes.Insert(item)
|
|
s.NoError(err)
|
|
|
|
if pk, ok := record.ID().(int64); !ok || pk == 0 {
|
|
s.T().Errorf("Expecting an ID.")
|
|
}
|
|
|
|
var itemCheck arrayType
|
|
err = arrayTypes.Find(record).One(&itemCheck)
|
|
s.NoError(err)
|
|
s.Len(itemCheck.Integers, len(item.Integers))
|
|
s.Len(itemCheck.Strings, len(item.Strings))
|
|
|
|
s.Equal(item, itemCheck)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue210() {
|
|
list := []string{
|
|
`DROP TABLE IF EXISTS testing123`,
|
|
`DROP TABLE IF EXISTS hello`,
|
|
`CREATE TABLE IF NOT EXISTS testing123 (
|
|
ID INT PRIMARY KEY NOT NULL,
|
|
NAME TEXT NOT NULL
|
|
)
|
|
`,
|
|
`CREATE TABLE IF NOT EXISTS hello (
|
|
ID INT PRIMARY KEY NOT NULL,
|
|
NAME TEXT NOT NULL
|
|
)`,
|
|
}
|
|
|
|
sess := s.Session()
|
|
|
|
err := sess.Tx(func(tx mydb.Session) error {
|
|
for i := range list {
|
|
_, err := tx.SQL().Exec(list[i])
|
|
s.NoError(err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
s.NoError(err)
|
|
|
|
_, err = sess.Collection("testing123").Find().Count()
|
|
s.NoError(err)
|
|
|
|
_, err = sess.Collection("hello").Find().Count()
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *AdapterTests) TestPreparedStatements() {
|
|
sess := s.Session()
|
|
|
|
var val int
|
|
|
|
{
|
|
stmt, err := sess.SQL().Prepare(`SELECT 1`)
|
|
s.NoError(err)
|
|
s.NotNil(stmt)
|
|
|
|
q, err := stmt.Query()
|
|
s.NoError(err)
|
|
s.NotNil(q)
|
|
s.True(q.Next())
|
|
|
|
err = q.Scan(&val)
|
|
s.NoError(err)
|
|
|
|
err = q.Close()
|
|
s.NoError(err)
|
|
|
|
s.Equal(1, val)
|
|
|
|
err = stmt.Close()
|
|
s.NoError(err)
|
|
}
|
|
|
|
{
|
|
err := sess.Tx(func(tx mydb.Session) error {
|
|
stmt, err := tx.SQL().Prepare(`SELECT 2`)
|
|
s.NoError(err)
|
|
s.NotNil(stmt)
|
|
|
|
q, err := stmt.Query()
|
|
s.NoError(err)
|
|
s.NotNil(q)
|
|
s.True(q.Next())
|
|
|
|
err = q.Scan(&val)
|
|
s.NoError(err)
|
|
|
|
err = q.Close()
|
|
s.NoError(err)
|
|
|
|
s.Equal(2, val)
|
|
|
|
err = stmt.Close()
|
|
s.NoError(err)
|
|
|
|
return nil
|
|
})
|
|
s.NoError(err)
|
|
}
|
|
|
|
{
|
|
stmt, err := sess.SQL().Select(3).Prepare()
|
|
s.NoError(err)
|
|
s.NotNil(stmt)
|
|
|
|
q, err := stmt.Query()
|
|
s.NoError(err)
|
|
s.NotNil(q)
|
|
s.True(q.Next())
|
|
|
|
err = q.Scan(&val)
|
|
s.NoError(err)
|
|
|
|
err = q.Close()
|
|
s.NoError(err)
|
|
|
|
s.Equal(3, val)
|
|
|
|
err = stmt.Close()
|
|
s.NoError(err)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) TestNonTrivialSubqueries() {
|
|
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)
|
|
}
|
|
|
|
{
|
|
q, err := sess.SQL().Query(`WITH test AS (?) ?`,
|
|
sess.SQL().Select("id AS foo").From("artist"),
|
|
sess.SQL().Select("foo").From("test").Where("foo > ?", 0),
|
|
)
|
|
|
|
s.NoError(err)
|
|
s.NotNil(q)
|
|
|
|
s.True(q.Next())
|
|
|
|
var number int
|
|
s.NoError(q.Scan(&number))
|
|
|
|
s.Equal(1, number)
|
|
s.NoError(q.Close())
|
|
}
|
|
|
|
{
|
|
builder := sess.SQL()
|
|
row, err := builder.QueryRow(`WITH test AS (?) ?`,
|
|
builder.Select("id AS foo").From("artist"),
|
|
builder.Select("foo").From("test").Where("foo > ?", 0),
|
|
)
|
|
|
|
s.NoError(err)
|
|
s.NotNil(row)
|
|
|
|
var number int
|
|
s.NoError(row.Scan(&number))
|
|
|
|
s.Equal(1, number)
|
|
}
|
|
|
|
{
|
|
res, err := sess.SQL().Exec(
|
|
`UPDATE artist a1 SET id = ?`,
|
|
sess.SQL().Select(mydb.Raw("id + 5")).
|
|
From("artist a2").
|
|
Where("a2.id = a1.id"),
|
|
)
|
|
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
}
|
|
|
|
{
|
|
builder := sess.SQL()
|
|
|
|
q, err := builder.Query(mydb.Raw(`WITH test AS (?) ?`,
|
|
builder.Select("id AS foo").From("artist"),
|
|
builder.Select("foo").From("test").Where("foo > ?", 0).OrderBy("foo"),
|
|
))
|
|
|
|
s.NoError(err)
|
|
s.NotNil(q)
|
|
|
|
s.True(q.Next())
|
|
|
|
var number int
|
|
s.NoError(q.Scan(&number))
|
|
|
|
s.Equal(6, number)
|
|
s.NoError(q.Close())
|
|
}
|
|
|
|
{
|
|
res, err := sess.SQL().Exec(mydb.Raw(`UPDATE artist a1 SET id = ?`,
|
|
sess.SQL().Select(mydb.Raw("id + 7")).From("artist a2").Where("a2.id = a1.id"),
|
|
))
|
|
|
|
s.NoError(err)
|
|
s.NotNil(res)
|
|
}
|
|
}
|
|
|
|
func (s *AdapterTests) Test_Issue601_ErrorCarrying() {
|
|
var items []interface{}
|
|
var err error
|
|
|
|
sess := s.Session()
|
|
|
|
_, err = sess.SQL().Exec(`DROP TABLE IF EXISTS issue_601`)
|
|
s.NoError(err)
|
|
|
|
err = sess.Collection("issue_601").Find().All(&items)
|
|
s.Error(err)
|
|
|
|
_, err = sess.SQL().Exec(`CREATE TABLE issue_601 (id INTEGER PRIMARY KEY)`)
|
|
s.NoError(err)
|
|
|
|
err = sess.Collection("issue_601").Find().All(&items)
|
|
s.NoError(err)
|
|
}
|
|
|
|
func TestAdapter(t *testing.T) {
|
|
suite.Run(t, &AdapterTests{})
|
|
}
|