diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-05-12 23:32:28 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-05-12 23:32:28 +0200 |
| commit | a86a346ecceb7ad409f116474c1593b201012cf2 (patch) | |
| tree | 267505a9e6bba398f7a379a046df64a8aec45b1c /benchmark | |
| parent | 23fa77bbe1286b55e2526c0a965da1a4c3048415 (diff) | |
| download | go-oblast-a86a346ecceb7ad409f116474c1593b201012cf2.tar.gz | |
add PostgreSQL benchmark, comparing lib/pq against pgx both with and w/o Oblast
Diffstat (limited to 'benchmark')
| -rw-r--r-- | benchmark/benchmark_test.go | 24 | ||||
| -rw-r--r-- | benchmark/go.mod | 9 | ||||
| -rw-r--r-- | benchmark/go.sum | 30 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/handle.go | 81 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/results.go | 66 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/statement.go | 60 | ||||
| -rw-r--r-- | benchmark/postgres_test.go | 396 |
7 files changed, 648 insertions, 18 deletions
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go index f0967b9..da764b2 100644 --- a/benchmark/benchmark_test.go +++ b/benchmark/benchmark_test.go @@ -21,13 +21,16 @@ import ( "gorm.io/gorm" ) +// NOTE: In this file, we benchmark different ORMs against each other and against hand-written operations using plain database/sql. +// All benchmarks are called "BenchmarkORM...". + // Do not use b.Context() within benchmarks, or you will merely demonstrate that using a deep stack of Context objects is expensive. var noctx = context.Background() // This is not a real benchmark (obviously). // Its purpose is to be the first line that is printed, while having one of the longest names, // so that all other results are aligned with it and the table looks nice. -func BenchmarkHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b *testing.B) { +func BenchmarkORMHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b *testing.B) { for b.Loop() { time.Sleep(time.Microsecond) } @@ -40,7 +43,7 @@ var ( batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100} ) -func makeTestDB(t testing.TB, recordCount int) (db *sql.DB, dsn string) { +func makeSqliteTestDB(t testing.TB, recordCount int) (db *sql.DB, dsn string) { dsn = fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db = must.Return(sql.Open("sqlite3", dsn))(t) _ = must.Return(db.Exec(`CREATE TABLE entries (id INTEGER, message TEXT, PRIMARY KEY (id AUTOINCREMENT))`))(t) @@ -75,8 +78,8 @@ type GormEntry struct { func (GormEntry) TableName() string { return "entries" } -func BenchmarkSelectMany(b *testing.B) { - db, dsn := makeTestDB(b, totalRecordCountForSelect) +func BenchmarkORMSelectMany(b *testing.B) { + db, dsn := makeSqliteTestDB(b, totalRecordCountForSelect) dbh := oblast.Wrap(db) // test with different sizes of resultsets (N=1 is an OLTP-like workload, @@ -171,8 +174,8 @@ func BenchmarkSelectMany(b *testing.B) { } } -func BenchmarkSelectOne(b *testing.B) { - db, dsn := makeTestDB(b, totalRecordCountForSelect) +func BenchmarkORMSelectOne(b *testing.B) { + db, dsn := makeSqliteTestDB(b, totalRecordCountForSelect) dbh := oblast.Wrap(db) // grab a "random" record from the DB, not just the first or the last @@ -256,8 +259,8 @@ func BenchmarkSelectOne(b *testing.B) { }) } -func BenchmarkInsertAndDelete(b *testing.B) { - db, dsn := makeTestDB(b, 0) +func BenchmarkORMInsertAndDelete(b *testing.B) { + db, dsn := makeSqliteTestDB(b, 0) dbh := oblast.Wrap(db) store := oblast.MustNewStore[OblastEntry]( @@ -442,8 +445,8 @@ func BenchmarkInsertAndDelete(b *testing.B) { } } -func BenchmarkUpdate(b *testing.B) { - db, dsn := makeTestDB(b, 0) +func BenchmarkORMUpdate(b *testing.B) { + db, dsn := makeSqliteTestDB(b, 0) dbh := oblast.Wrap(db) store := oblast.MustNewStore[OblastEntry]( @@ -507,6 +510,7 @@ func BenchmarkUpdate(b *testing.B) { for _, r := range recordsForOblast { _ = must.Return(stmt.Exec(message, r.ID))(b) } + must.Succeed(b, stmt.Close()) } checkRecordsUpdated := func(b *testing.B, message string) { var count int64 diff --git a/benchmark/go.mod b/benchmark/go.mod index 75f27db..5f981fa 100644 --- a/benchmark/go.mod +++ b/benchmark/go.mod @@ -4,14 +4,19 @@ go 1.26.0 require ( github.com/go-gorp/gorp/v3 v3.1.0 + github.com/jackc/pgx/v5 v5.9.2 + github.com/lib/pq v1.10.7 github.com/mattn/go-sqlite3 v1.14.42 - go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b + go.xyrillian.de/oblast v0.7.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - golang.org/x/text v0.20.0 // indirect + go.xyrillian.de/gg v1.7.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/benchmark/go.sum b/benchmark/go.sum index 5701940..0455a12 100644 --- a/benchmark/go.sum +++ b/benchmark/go.sum @@ -1,9 +1,18 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -16,12 +25,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b h1:TalkSqODn/2HbLiwd/Tgb5KFXmzJa6O1BbassZyLnVU= -go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b/go.mod h1:lo6ekGOHTID0KeSWhNQV1gjYJ2BfhXgenUEBNBnZkBM= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.xyrillian.de/gg v1.7.0 h1:IA0BJaX9TtBD7crH+CSoK4lYmBk5zi7nUQd0YRzPNf0= +go.xyrillian.de/gg v1.7.0/go.mod h1:dj+ZhCwC6JKWyFvImhVNXQAErrRcYMUkXu6vwWYNrzQ= +go.xyrillian.de/oblast v0.7.0 h1:0+TR2De6/OvOm7MpLYukgkkzgZ3HUW/oTtOe3EslsH4= +go.xyrillian.de/oblast v0.7.0/go.mod h1:fR/UKmOOv44L27Xe2hlkrHKs9pl+D/9wWXFH6WiDeYI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= diff --git a/benchmark/internal/oblast_pgx/handle.go b/benchmark/internal/oblast_pgx/handle.go new file mode 100644 index 0000000..6a88e2b --- /dev/null +++ b/benchmark/internal/oblast_pgx/handle.go @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package oblast_pgx + +import ( + "context" + "fmt" + "strconv" + "sync/atomic" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "go.xyrillian.de/oblast/handle" +) + +type Handle interface { + Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row +} + +var ( + _ Handle = &pgx.Conn{} + _ Handle = pgx.Tx(nil) +) + +// TODO: offer wrapping for pgxpool.Pool and pgxpool.Conn? +func Wrap(h Handle) handle.Handle { + switch h := h.(type) { + case *pgx.Conn: + return wrappedHandle{h} + case pgx.Tx: + return wrappedHandle{h} + default: + panic(fmt.Sprintf("unexpected type: %#v", h)) + } +} + +var preparedStatementId atomic.Uint64 + +type wrappedHandle struct { + inner Handle +} + +// Prepare implements the [handle.Handle] interface. +func (h wrappedHandle) Prepare(ctx context.Context, query string, repeated bool) (handle.Statement, error) { + if !repeated { + return wrappedUnpreparedStatement{query, h.inner}, nil + } + + name := "oblast_pgx_" + strconv.FormatUint(preparedStatementId.Add(1), 10) + switch inner := h.inner.(type) { + case *pgx.Conn: + stmt, err := inner.Prepare(ctx, name, query) + return wrappedPreparedStatement{ctx, stmt, h.inner}, err + case pgx.Tx: + stmt, err := inner.Conn().Prepare(ctx, name, query) + return wrappedPreparedStatement{ctx, stmt, h.inner}, err + default: + panic("unreachable") // because of the check in func Wrap() + } +} + +// Releases a prepared statement. +func deallocate(ctx context.Context, h Handle, stmt *pgconn.StatementDescription) error { + switch h := h.(type) { + case *pgx.Conn: + return h.Deallocate(ctx, stmt.Name) + case pgx.Tx: + return h.Conn().Deallocate(ctx, stmt.Name) + default: + panic("unreachable") // because of the check in func Wrap() + } +} + +// Query implements the [handle.Handle] interface. +func (h wrappedHandle) Query(ctx context.Context, query string, args []any) (handle.Rows, error) { + rows, err := h.inner.Query(ctx, query, args...) + return wrappedRows{rows}, err +} diff --git a/benchmark/internal/oblast_pgx/results.go b/benchmark/internal/oblast_pgx/results.go new file mode 100644 index 0000000..3ccb5ce --- /dev/null +++ b/benchmark/internal/oblast_pgx/results.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package oblast_pgx + +import ( + "database/sql" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "go.xyrillian.de/oblast/handle" +) + +type wrappedRows struct { + inner pgx.Rows +} + +var _ handle.Rows = wrappedRows{} + +// Columns implements the [handle.Rows] interface. +func (r wrappedRows) Columns() ([]string, error) { + descriptions := r.inner.FieldDescriptions() + result := make([]string, len(descriptions)) + for idx, desc := range descriptions { + result[idx] = desc.Name + } + return result, nil +} + +// Close implements the [handle.Rows] interface. +func (r wrappedRows) Close() error { + r.inner.Close() + return nil +} + +// Err implements the [handle.Rows] interface. +func (r wrappedRows) Err() error { + return r.inner.Err() +} + +// Next implements the [handle.Rows] interface. +func (r wrappedRows) Next() bool { + return r.inner.Next() +} + +// Scan implements the [handle.Rows] interface. +func (r wrappedRows) Scan(args ...any) error { + return r.inner.Scan(args...) +} + +type wrappedResult struct { + inner pgconn.CommandTag +} + +var _ sql.Result = wrappedResult{} + +// LastInsertId implements the [sql.Result] interface. +func (r wrappedResult) LastInsertId() (int64, error) { + return 0, errors.New("PostgreSQL does not support LastInsertId()") +} + +// LastInsertId implements the [sql.Result] interface. +func (r wrappedResult) RowsAffected() (int64, error) { + return r.inner.RowsAffected(), nil +} diff --git a/benchmark/internal/oblast_pgx/statement.go b/benchmark/internal/oblast_pgx/statement.go new file mode 100644 index 0000000..d81c579 --- /dev/null +++ b/benchmark/internal/oblast_pgx/statement.go @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package oblast_pgx + +import ( + "context" + "database/sql" + + "github.com/jackc/pgx/v5/pgconn" + "go.xyrillian.de/oblast/handle" +) + +type wrappedPreparedStatement struct { + ctx context.Context + statement *pgconn.StatementDescription + handle Handle +} + +type wrappedUnpreparedStatement struct { + query string + handle Handle +} + +var ( + _ handle.Statement = wrappedPreparedStatement{} + _ handle.Statement = wrappedUnpreparedStatement{} +) + +// Close implements the [handle.Statement] interface. +func (s wrappedPreparedStatement) Close() error { + return deallocate(s.ctx, s.handle, s.statement) +} + +// Close implements the [handle.Statement] interface. +func (s wrappedUnpreparedStatement) Close() error { + return nil +} + +// Exec implements the [handle.Statement] interface. +func (s wrappedPreparedStatement) Exec(ctx context.Context, args []any) (sql.Result, error) { + result, err := s.handle.Exec(ctx, s.statement.Name, args...) + return wrappedResult{result}, err +} + +// Exec implements the [handle.Statement] interface. +func (s wrappedUnpreparedStatement) Exec(ctx context.Context, args []any) (sql.Result, error) { + result, err := s.handle.Exec(ctx, s.query, args...) + return wrappedResult{result}, err +} + +// QueryRow implements the [handle.Statement] interface. +func (s wrappedPreparedStatement) QueryRow(ctx context.Context, args, slots []any) error { + return s.handle.QueryRow(ctx, s.statement.Name, args...).Scan(slots...) +} + +// QueryRow implements the [handle.Statement] interface. +func (s wrappedUnpreparedStatement) QueryRow(ctx context.Context, args, slots []any) error { + return s.handle.QueryRow(ctx, s.query, args...).Scan(slots...) +} diff --git a/benchmark/postgres_test.go b/benchmark/postgres_test.go new file mode 100644 index 0000000..320ea2a --- /dev/null +++ b/benchmark/postgres_test.go @@ -0,0 +1,396 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package main_test + +import ( + "cmp" + "crypto/sha256" + "database/sql" + "fmt" + "os" + "strconv" + "testing" + "time" + + "github.com/jackc/pgx/v5" + _ "github.com/lib/pq" + "go.xyrillian.de/oblast" + "go.xyrillian.de/oblast/benchmark/internal/oblast_pgx" + "go.xyrillian.de/oblast/internal/testhelpers/assert" + "go.xyrillian.de/oblast/internal/testhelpers/must" +) + +// NOTE: In this file, we benchmark different PostgreSQL database drivers against each other with or without Oblast inbetween. +// All benchmarks are called "BenchmarkPostgres...". +// To run these benchmarks, you need to have provide a DSN to a PostgreSQL database in $BENCHMARK_POSTGRES_DSN. + +// This is not a real benchmark (obviously). +// Its purpose is to be the first line that is printed, while having one of the longest names, +// so that all other results are aligned with it and the table looks nice. +func BenchmarkPostgresHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b *testing.B) { + for b.Loop() { + time.Sleep(time.Microsecond) + } +} + +const defaultPostgresDSN = "host=localhost user=postgres dbname=oblast_benchmark sslmode=disable" + +func connectToPostgresTestDB(t testing.TB, recordCount int) *sql.DB { + dsn := cmp.Or(os.Getenv("BENCHMARK_POSTGRES_DSN"), defaultPostgresDSN) + db := must.Return(sql.Open("postgres", dsn))(t) + _ = must.Return(db.Exec(`CREATE TEMPORARY TABLE entries (id BIGSERIAL, message TEXT)`))(t) + + if recordCount > 0 { + // fill in some random-looking, but deterministic data + stmt := must.Return(db.Prepare(`INSERT INTO entries (id, message) VALUES ($1, $2)`))(t) + for idx := range recordCount { + buf := sha256.Sum256([]byte(strconv.Itoa(idx))) + _ = must.Return(stmt.Exec(idx, fmt.Sprintf("sha256:%x", buf[:])))(t) + } + must.Succeed(t, stmt.Close()) + } + + return db +} + +func connectToPgxTestDB(t testing.TB, recordCount int) *pgx.Conn { + ctx := t.Context() + dsn := cmp.Or(os.Getenv("BENCHMARK_POSTGRES_DSN"), defaultPostgresDSN) + conn := must.Return(pgx.Connect(ctx, dsn))(t) + _ = must.Return(conn.Exec(ctx, `CREATE TEMPORARY TABLE entries (id BIGSERIAL, message TEXT)`))(t) + + if recordCount > 0 { + // fill in some random-looking, but deterministic data + sql := `INSERT INTO entries (id, message) VALUES ($1, $2)` + stmt := must.Return(conn.Prepare(ctx, sql, sql))(t) + for idx := range recordCount { + buf := sha256.Sum256([]byte(strconv.Itoa(idx))) + _ = must.Return(conn.Exec(ctx, sql, idx, fmt.Sprintf("sha256:%x", buf[:])))(t) + } + must.Succeed(t, conn.Deallocate(ctx, stmt.Name)) + } + + return conn +} + +func BenchmarkPostgresSelect(b *testing.B) { + pqDB := connectToPostgresTestDB(b, totalRecordCountForSelect) + pqDBH := oblast.Wrap(pqDB) + pgxConn := connectToPgxTestDB(b, totalRecordCountForSelect) + pgxConnH := oblast_pgx.Wrap(pgxConn) + + store := oblast.MustNewStore[OblastEntry]( + oblast.PostgresDialect(), + oblast.TableNameIs("entries"), + oblast.PrimaryKeyIs("id"), + ) + + for _, batchSize := range batchSizesForSelect { + b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) { + partialQuery := `id < ` + strconv.Itoa(batchSize) + query := `SELECT * FROM entries WHERE ` + partialQuery + + b.Run("driver=pq/strategy=oblast", func(b *testing.B) { + for b.Loop() { + records := must.Return(store.Select(noctx, pqDBH, query))(b) + assert.Equal(b, len(records), batchSize) + } + }) + + b.Run("driver=pgx/strategy=oblast", func(b *testing.B) { + for b.Loop() { + records := must.Return(store.Select(noctx, pgxConnH, query))(b) + assert.Equal(b, len(records), batchSize) + } + }) + + b.Run("driver=pq/strategy=straight", func(b *testing.B) { + for b.Loop() { + var records []OblastEntry + rows := must.Return(pqDB.Query(query))(b) //nolint:rowserrcheck // false positive + for rows.Next() { + var e OblastEntry + must.Succeed(b, rows.Scan(&e.ID, &e.Message)) + records = append(records, e) + } + must.Succeed(b, rows.Close()) + assert.Equal(b, len(records), batchSize) + } + }) + + b.Run("driver=pgx/strategy=straight", func(b *testing.B) { + for b.Loop() { + var records []OblastEntry + rows := must.Return(pgxConn.Query(noctx, query))(b) //nolint:rowserrcheck // false positive + for rows.Next() { + var e OblastEntry + must.Succeed(b, rows.Scan(&e.ID, &e.Message)) + records = append(records, e) + } + rows.Close() + assert.Equal(b, len(records), batchSize) + } + }) + }) + } +} + +func BenchmarkPostgresSelectOne(b *testing.B) { + pqDB := connectToPostgresTestDB(b, totalRecordCountForSelect) + pqDBH := oblast.Wrap(pqDB) + pgxConn := connectToPgxTestDB(b, totalRecordCountForSelect) + pgxConnH := oblast_pgx.Wrap(pgxConn) + + // grab a "random" record from the DB, not just the first or the last + recordID := min(totalRecordCountForSelect*2/3, totalRecordCountForSelect) + + store := oblast.MustNewStore[OblastEntry]( + oblast.PostgresDialect(), + oblast.TableNameIs("entries"), + oblast.PrimaryKeyIs("id"), + ) + + partialQuery := `id = ` + strconv.Itoa(recordID) + query := `SELECT * FROM entries WHERE ` + partialQuery + precomputedQuery := store.MustPrepareSelectQueryWhere(partialQuery) + + b.Run("driver=pq/strategy=oblast", func(b *testing.B) { + for b.Loop() { + r := must.Return(precomputedQuery.SelectOne(noctx, pqDBH))(b) + assert.Equal(b, r.ID, recordID) + } + }) + + b.Run("driver=pgx/strategy=oblast", func(b *testing.B) { + for b.Loop() { + r := must.Return(precomputedQuery.SelectOne(noctx, pgxConnH))(b) + assert.Equal(b, r.ID, recordID) + } + }) + + b.Run("driver=pq/strategy=straight", func(b *testing.B) { + for b.Loop() { + var ( + id int64 + message string + ) + must.Succeed(b, pqDB.QueryRow(query).Scan(&id, &message)) + assert.Equal(b, id, int64(recordID)) + } + }) + + b.Run("driver=pgx/strategy=straight", func(b *testing.B) { + for b.Loop() { + var ( + id int64 + message string + ) + must.Succeed(b, pgxConn.QueryRow(noctx, query).Scan(&id, &message)) + assert.Equal(b, id, int64(recordID)) + } + }) +} + +func BenchmarkPostgresInsertAndDelete(b *testing.B) { + pqDB := connectToPostgresTestDB(b, 0) + pqDBH := oblast.Wrap(pqDB) + pgxConn := connectToPgxTestDB(b, 0) + pgxConnH := oblast_pgx.Wrap(pgxConn) + + store := oblast.MustNewStore[OblastEntry]( + oblast.PostgresDialect(), + oblast.TableNameIs("entries"), + oblast.PrimaryKeyIs("id"), + ) + + // test with different amounts of records + for _, batchSize := range batchSizesForInsertDelete { + b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) { + insertAndDeleteWithOblast := func(b *testing.B, dbh oblast.Handle) { + records := make([]OblastEntry, batchSize) + recordsForInsert := make([]*OblastEntry, batchSize) + for idx := range records { + records[idx] = OblastEntry{Message: "hello"} + recordsForInsert[idx] = &records[idx] + } + must.Succeed(b, store.Insert(noctx, dbh, recordsForInsert...)) + for _, r := range records { + if r.ID == 0 { + b.Errorf("ID was not filled!") + } + } + must.Succeed(b, store.Delete(noctx, dbh, records...)) + } + + b.Run("driver=pq/strategy=oblast", func(b *testing.B) { + for b.Loop() { + insertAndDeleteWithOblast(b, pqDBH) + } + }) + + b.Run("driver=pgx/strategy=oblast", func(b *testing.B) { + for b.Loop() { + insertAndDeleteWithOblast(b, pgxConnH) + } + }) + + insertQuery := `INSERT INTO entries (message) VALUES ($1) RETURNING id` + deleteQuery := `DELETE FROM entries WHERE id = $1` + + b.Run("driver=pq/strategy=straight", func(b *testing.B) { + for b.Loop() { + ids := make([]int64, batchSize) + for idx := range ids { + must.Succeed(b, pqDB.QueryRow(insertQuery, "hello").Scan(&ids[idx])) + } + for _, id := range ids { + _ = must.Return(pqDB.Exec(deleteQuery, id))(b) + } + } + }) + + b.Run("driver=pgx/strategy=straight", func(b *testing.B) { + for b.Loop() { + ids := make([]int64, batchSize) + for idx := range ids { + must.Succeed(b, pgxConn.QueryRow(noctx, insertQuery, "hello").Scan(&ids[idx])) + } + for _, id := range ids { + _ = must.Return(pgxConn.Exec(noctx, deleteQuery, id))(b) + } + } + }) + + b.Run("driver=pq/strategy=prepared", func(b *testing.B) { + for b.Loop() { + ids := make([]int64, batchSize) + stmtInsert := must.Return(pqDB.Prepare(insertQuery))(b) + defer stmtInsert.Close() + for idx := range ids { + must.Succeed(b, stmtInsert.QueryRow("hello").Scan(&ids[idx])) + } + stmtDelete := must.Return(pqDB.Prepare(deleteQuery))(b) + defer stmtDelete.Close() + for _, id := range ids { + _ = must.Return(stmtDelete.Exec(id))(b) + } + } + }) + + b.Run("driver=pgx/strategy=prepared", func(b *testing.B) { + for b.Loop() { + stmtInsert := must.Return(pgxConn.Prepare(noctx, "my-insert", insertQuery))(b) + ids := make([]int64, batchSize) + for idx := range ids { + must.Succeed(b, pgxConn.QueryRow(noctx, stmtInsert.Name, "hello").Scan(&ids[idx])) + } + must.Succeed(b, pgxConn.Deallocate(noctx, stmtInsert.Name)) + stmtDelete := must.Return(pgxConn.Prepare(noctx, "my-delete", deleteQuery))(b) + for _, id := range ids { + _ = must.Return(pgxConn.Exec(noctx, stmtDelete.Name, id))(b) + } + must.Succeed(b, pgxConn.Deallocate(noctx, stmtDelete.Name)) + } + }) + }) + } +} + +func BenchmarkPostgresUpdate(b *testing.B) { + pqDB := connectToPostgresTestDB(b, 0) + pqDBH := oblast.Wrap(pqDB) + pgxConn := connectToPgxTestDB(b, 0) + pgxConnH := oblast_pgx.Wrap(pgxConn) + + store := oblast.MustNewStore[OblastEntry]( + oblast.PostgresDialect(), + oblast.TableNameIs("entries"), + oblast.PrimaryKeyIs("id"), + ) + + // test with different amounts of records + for _, batchSize := range batchSizesForInsertDelete { + b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) { + // prepare a bunch of records that we can update, in a reproducible way + _ = must.Return(pqDB.Exec(`DELETE FROM entries`)) + _ = must.Return(pgxConn.Exec(noctx, `DELETE FROM entries`)) + pqRecords := make([]OblastEntry, batchSize) + pqRecordsForInsert := make([]*OblastEntry, batchSize) + pgxRecords := make([]OblastEntry, batchSize) + pgxRecordsForInsert := make([]*OblastEntry, batchSize) + for idx := range batchSize { + pqRecords[idx] = OblastEntry{Message: "hello"} + pqRecordsForInsert[idx] = &pqRecords[idx] + pgxRecords[idx] = OblastEntry{Message: "hello"} + pgxRecordsForInsert[idx] = &pgxRecords[idx] + } + must.Succeed(b, store.Insert(noctx, pqDBH, pqRecordsForInsert...)) + must.Succeed(b, store.Insert(noctx, pgxConnH, pgxRecordsForInsert...)) + + // each benchmark will, while looping, write changing values each time in the same way + loop := func(b *testing.B, action func(string)) { + idx := 0 + for b.Loop() { + idx++ + message := fmt.Sprintf("round %d", idx) + action(message) + } + } + + updateWithOblast := func(b *testing.B, dbh oblast.Handle, records []OblastEntry) func(string) { + return func(message string) { + for idx := range records { + records[idx].Message = message + } + must.Succeed(b, store.Update(noctx, dbh, records...)) + } + } + + b.Run("driver=pq/strategy=oblast", func(b *testing.B) { + loop(b, updateWithOblast(b, pqDBH, pqRecords)) + }) + + b.Run("driver=pgx/strategy=oblast", func(b *testing.B) { + loop(b, updateWithOblast(b, pgxConnH, pgxRecords)) + }) + + updateQuery := `UPDATE entries SET message = $1 WHERE id = $2` + + b.Run("driver=pq/strategy=straight", func(b *testing.B) { + loop(b, func(message string) { + for _, r := range pqRecords { + _ = must.Return(pqDB.Exec(updateQuery, message, r.ID))(b) + } + }) + }) + + b.Run("driver=pgx/strategy=straight", func(b *testing.B) { + loop(b, func(message string) { + for _, r := range pgxRecords { + _ = must.Return(pgxConn.Exec(noctx, updateQuery, message, r.ID))(b) + } + }) + }) + + b.Run("driver=pq/strategy=prepared", func(b *testing.B) { + loop(b, func(message string) { + stmt := must.Return(pqDB.Prepare(updateQuery))(b) + for _, r := range pqRecords { + _ = must.Return(stmt.Exec(message, r.ID))(b) + } + }) + }) + + b.Run("driver=pgx/strategy=prepared", func(b *testing.B) { + loop(b, func(message string) { + stmt := must.Return(pgxConn.Prepare(noctx, "my-update", updateQuery))(b) + for _, r := range pgxRecords { + _ = must.Return(pgxConn.Exec(noctx, stmt.Name, message, r.ID))(b) + } + must.Succeed(b, pgxConn.Deallocate(noctx, stmt.Name)) + }) + }) + }) + } +} |
