package pg_test
import (
"fmt"
"math/rand"
"strconv"
"sync"
"testing"
"time"
"github.com/go-pg/pg"
"github.com/go-pg/pg/orm"
"github.com/go-pg/pg/types"
)
func benchmarkDB() *pg.DB {
return pg.Connect(&pg.Options{
User: "postgres",
Database: "postgres",
DialTimeout: 30 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
PoolSize: 10,
PoolTimeout: 30 * time.Second,
})
}
func BenchmarkQueryRowsGopgDiscard(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := db.Query(pg.Discard, `SELECT * FROM records LIMIT 100`)
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkQueryRowsGopgOptimized(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var rs OptRecords
_, err := db.Query(&rs, `SELECT * FROM records LIMIT 100`)
if err != nil {
b.Fatal(err)
}
if len(rs.C) != 100 {
b.Fatalf("got %d, wanted 100", len(rs.C))
}
}
})
}
func BenchmarkQueryRowsGopgReflect(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var rs []Record
_, err := db.Query(&rs, `SELECT * FROM records LIMIT 100`)
if err != nil {
b.Fatal(err)
}
if len(rs) != 100 {
b.Fatalf("got %d, wanted 100", len(rs))
}
}
})
}
func BenchmarkQueryRowsGopgORM(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var rs []Record
err := db.Model(&rs).Limit(100).Select()
if err != nil {
b.Fatal(err)
}
if len(rs) != 100 {
b.Fatalf("got %d, wanted 100", len(rs))
}
}
})
}
func BenchmarkModelHasOneGopg(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var books []Book
err := db.Model(&books).Column("book.*", "Author").Limit(100).Select()
if err != nil {
b.Fatal(err)
}
if len(books) != 100 {
b.Fatalf("got %d, wanted 100", len(books))
}
}
})
}
func BenchmarkModelHasManyGopg(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var books []Book
err := db.Model(&books).Column("book.*", "Translations").Limit(100).Select()
if err != nil {
b.Fatal(err)
}
if len(books) != 100 {
b.Fatalf("got %d, wanted 100", len(books))
}
for _, book := range books {
if len(book.Translations) != 10 {
b.Fatalf("got %d, wanted 10", len(book.Translations))
}
}
}
})
}
func BenchmarkModelHasMany2ManyGopg(b *testing.B) {
seedDB()
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var books []Book
err := db.Model(&books).
Column("book.*", "Genres").
Limit(100).
Select()
if err != nil {
b.Fatal(err)
}
if len(books) != 100 {
b.Fatalf("got %d, wanted 100", len(books))
}
for _, book := range books {
if len(book.Genres) != 10 {
b.Fatalf("got %d, wanted 10", len(book.Genres))
}
}
}
})
}
func BenchmarkQueryRow(b *testing.B) {
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst numLoader
_, err := db.QueryOne(&dst, `SELECT ?::bigint AS num`, 1)
if err != nil {
b.Fatal(err)
}
if dst.Num != 1 {
b.Fatalf("got %d, wanted 1", dst.Num)
}
}
}
func BenchmarkQueryRowStmt(b *testing.B) {
db := benchmarkDB()
defer db.Close()
stmt, err := db.Prepare(`SELECT $1::bigint AS num`)
if err != nil {
b.Fatal(err)
}
defer stmt.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst numLoader
_, err := stmt.QueryOne(&dst, 1)
if err != nil {
b.Fatal(err)
}
if dst.Num != 1 {
b.Fatalf("got %d, wanted 1", dst.Num)
}
}
}
func BenchmarkQueryRowScan(b *testing.B) {
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var n int64
_, err := db.QueryOne(pg.Scan(&n), `SELECT ? AS num`, 1)
if err != nil {
b.Fatal(err)
}
if n != 1 {
b.Fatalf("got %d, wanted 1", n)
}
}
})
}
func BenchmarkQueryRowStmtScan(b *testing.B) {
db := benchmarkDB()
defer db.Close()
stmt, err := db.Prepare(`SELECT $1::bigint AS num`)
if err != nil {
b.Fatal(err)
}
defer stmt.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var n int64
_, err := stmt.QueryOne(pg.Scan(&n), 1)
if err != nil {
b.Fatal(err)
}
if n != 1 {
b.Fatalf("got %d, wanted 1", n)
}
}
}
func BenchmarkExec(b *testing.B) {
db := benchmarkDB()
defer db.Close()
qs := []string{
`DROP TABLE IF EXISTS exec_test`,
`CREATE TABLE exec_test(id bigint, name varchar(500))`,
}
for _, q := range qs {
_, err := db.Exec(q)
if err != nil {
b.Fatal(err)
}
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := db.Exec(`INSERT INTO exec_test (id, name) VALUES (?, ?)`, 1, "hello world")
if err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkExecWithError(b *testing.B) {
db := benchmarkDB()
defer db.Close()
qs := []string{
`DROP TABLE IF EXISTS exec_with_error_test`,
`CREATE TABLE exec_with_error_test(id bigint PRIMARY KEY, name varchar(500))`,
}
for _, q := range qs {
_, err := db.Exec(q)
if err != nil {
b.Fatal(err)
}
}
_, err := db.Exec(`
INSERT INTO exec_with_error_test(id, name) VALUES(?, ?)
`, 1, "hello world")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := db.Exec(`INSERT INTO exec_with_error_test(id) VALUES(?)`, 1)
if err == nil {
b.Fatalf("got nil error, expected integrity violation")
} else if pgErr, ok := err.(pg.Error); !ok || !pgErr.IntegrityViolation() {
b.Fatalf("got %s, expected integrity violation", err)
}
}
})
}
func BenchmarkExecStmt(b *testing.B) {
db := benchmarkDB()
defer db.Close()
_, err := db.Exec(`CREATE TEMP TABLE statement_exec(id bigint, name varchar(500))`)
if err != nil {
b.Fatal(err)
}
stmt, err := db.Prepare(`INSERT INTO statement_exec (id, name) VALUES ($1, $2)`)
if err != nil {
b.Fatal(err)
}
defer stmt.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := stmt.Exec(1, "hello world")
if err != nil {
b.Fatal(err)
}
}
}
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randSeq(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
type Record struct {
Num1, Num2, Num3 int64
Str1, Str2, Str3 string
}
func (r *Record) GetNum1() int64 {
return r.Num1
}
func (r *Record) GetNum2() int64 {
return r.Num2
}
func (r *Record) GetNum3() int64 {
return r.Num3
}
func (r *Record) GetStr1() string {
return r.Str1
}
func (r *Record) GetStr2() string {
return r.Str2
}
func (r *Record) GetStr3() string {
return r.Str3
}
type OptRecord struct {
Num1, Num2, Num3 int64
Str1, Str2, Str3 string
}
var _ orm.ColumnScanner = (*OptRecord)(nil)
func (r *OptRecord) ScanColumn(colIdx int, colName string, rd types.Reader, n int) error {
tmp, err := rd.ReadFullTemp()
if err != nil {
return err
}
switch colName {
case "num1":
r.Num1, err = strconv.ParseInt(string(tmp), 10, 64)
case "num2":
r.Num2, err = strconv.ParseInt(string(tmp), 10, 64)
case "num3":
r.Num3, err = strconv.ParseInt(string(tmp), 10, 64)
case "str1":
r.Str1 = string(tmp)
case "str2":
r.Str2 = string(tmp)
case "str3":
r.Str3 = string(tmp)
default:
return fmt.Errorf("unknown column: %q", colName)
}
return err
}
type OptRecords struct {
C []OptRecord
}
var _ orm.HooklessModel = (*OptRecords)(nil)
func (rs *OptRecords) Init() error {
return nil
}
func (rs *OptRecords) NewModel() orm.ColumnScanner {
rs.C = append(rs.C, OptRecord{})
return &rs.C[len(rs.C)-1]
}
func (OptRecords) AddModel(_ orm.ColumnScanner) error {
return nil
}
var seedDBOnce sync.Once
func seedDB() {
seedDBOnce.Do(func() {
if err := _seedDB(); err != nil {
panic(err)
}
})
}
func _seedDB() error {
db := benchmarkDB()
defer db.Close()
_, err := db.Exec(`DROP TABLE IF EXISTS records`)
if err != nil {
return err
}
_, err = db.Exec(`
CREATE TABLE records(
num1 serial,
num2 serial,
num3 serial,
str1 text,
str2 text,
str3 text
)
`)
if err != nil {
return err
}
for i := 0; i < 1000; i++ {
_, err = db.Exec(`
INSERT INTO records (str1, str2, str3) VALUES (?, ?, ?)
`, randSeq(100), randSeq(200), randSeq(300))
if err != nil {
return err
}
}
err = createTestSchema(db)
if err != nil {
return err
}
for i := 1; i < 100; i++ {
genre := Genre{
Id: i,
Name: fmt.Sprintf("genre %d", i),
}
err = db.Insert(&genre)
if err != nil {
return err
}
author := Author{
ID: i,
Name: fmt.Sprintf("author %d", i),
}
err = db.Insert(&author)
if err != nil {
return err
}
}
for i := 1; i <= 1000; i++ {
err = db.Insert(&Book{
Id: i,
Title: fmt.Sprintf("book %d", i),
AuthorID: rand.Intn(99) + 1,
CreatedAt: time.Now(),
})
if err != nil {
return err
}
for j := 1; j <= 10; j++ {
err = db.Insert(&BookGenre{
BookId: i,
GenreId: j,
})
if err != nil {
return err
}
err = db.Insert(&Translation{
BookId: i,
Lang: fmt.Sprintf("%d", j),
})
if err != nil {
return err
}
}
}
return nil
}
func BenchmarkForEachReal(b *testing.B) {
const N = 100000
type Model struct {
Id int
_ [1000]byte
}
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var i int
err := db.Model().
TableExpr("generate_series(1, ?) as id", N).
ForEach(func(m *Model) error {
i++
return nil
})
if err != nil {
b.Fatal(err)
}
if i != N {
b.Fatalf("got %d, wanted %d", i, N)
}
}
}
func BenchmarkForEachInMemory(b *testing.B) {
const N = 100000
type Model struct {
Id int
_ [1000]byte
}
db := benchmarkDB()
defer db.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var model []Model
err := db.Model().
TableExpr("generate_series(1, ?) as id", N).
Select(&model)
if err != nil {
b.Fatal(err)
}
if len(model) != N {
b.Fatalf("got %d, wanted %d", len(model), N)
}
}
}