aboutsummaryrefslogtreecommitdiff
path: root/benchmark
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-09-17 16:51:17 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-09-17 16:51:17 +0200
commit1825c3040f5ee71ad26185a7a08f2657ede94df5 (patch)
treebf65a15764fae5a947ef1a53d9d6c54a8ccdae11 /benchmark
parent1b8935ec8cc8ebe9ad74ecd12141e8ad115cebfb (diff)
downloadgo-oblast-1825c3040f5ee71ad26185a7a08f2657ede94df5.tar.gz
rebase onto gg@v1.16.0/oblast
Diffstat (limited to 'benchmark')
-rw-r--r--benchmark/.gitignore3
-rw-r--r--benchmark/benchmark_test.go641
-rw-r--r--benchmark/go.mod24
-rw-r--r--benchmark/go.sum48
-rw-r--r--benchmark/internal/oblast_pgx/handle.go89
-rw-r--r--benchmark/internal/oblast_pgx/results.go66
-rw-r--r--benchmark/internal/oblast_pgx/statement.go60
-rw-r--r--benchmark/main.go8
-rw-r--r--benchmark/postgres_test.go393
9 files changed, 0 insertions, 1332 deletions
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
deleted file mode 100644
index ed6d513..0000000
--- a/benchmark/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# artifacts from running e.g. `go test -bench . -benchmem -memprofile mem.out`
-/benchmark.test
-/*.out
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go
deleted file mode 100644
index 487b9c5..0000000
--- a/benchmark/benchmark_test.go
+++ /dev/null
@@ -1,641 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
-// SPDX-License-Identifier: Apache-2.0
-
-package main_test
-
-import (
- "context"
- "crypto/sha256"
- "database/sql"
- "fmt"
- "strconv"
- "testing"
- "time"
-
- "github.com/go-gorp/gorp/v3"
- _ "github.com/mattn/go-sqlite3"
- "go.xyrillian.de/gg/assert"
- "go.xyrillian.de/gg/gsql"
- "go.xyrillian.de/oblast"
- "go.xyrillian.de/oblast/internal/testhelpers/must"
- "gorm.io/driver/sqlite"
- "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 BenchmarkORMHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b *testing.B) {
- for b.Loop() {
- time.Sleep(time.Microsecond)
- }
-}
-
-var (
- totalRecordCountForSelect = 10000
- batchSizesForSelect = []int{1, 10, 100, 1000}
- batchSizesForInsertDelete = []int{1, 2, 4, 8, 16, 100}
- batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100}
-)
-
-func makeSqliteTestDB(t testing.TB, recordCount int) (db *gsql.DB, dsn string) {
- dsn = fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
- db = gsql.NewDB(must.Return(sql.Open("sqlite3", dsn))(t))
- _ = must.Return(db.Exec(`CREATE TABLE entries (id INTEGER, message TEXT, PRIMARY KEY (id AUTOINCREMENT))`))(t)
-
- if recordCount > 0 {
- // fill in some random-looking, but deterministic data
- stmt := must.Return(db.Prepare(`INSERT INTO entries (id, message) VALUES (?, ?)`))(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, dsn
-}
-
-type OblastEntry struct {
- ID int `db:"id,auto"`
- Message string `db:"message"`
-}
-
-type GorpEntry struct {
- ID int `db:"id"`
- Message string `db:"message"`
-}
-
-type GormEntry struct {
- ID int `gorm:"primaryKey"`
- Message string
-}
-
-func (GormEntry) TableName() string { return "entries" }
-
-func BenchmarkORMSelectMany(b *testing.B) {
- db, dsn := makeSqliteTestDB(b, totalRecordCountForSelect)
-
- // test with different sizes of resultsets (N=1 is an OLTP-like workload,
- // then the larger N lean more towards the OLAP side of things)
- for _, batchSize := range batchSizesForSelect {
- b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) {
- // prepare the functions that will be benched
- store := oblast.MustNewStore[OblastEntry](
- oblast.SqliteDialect(),
- oblast.TableNameIs("entries"),
- oblast.PrimaryKeyIs("id"),
- )
- gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
- gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
- partialQuery := `id < ` + strconv.Itoa(batchSize)
- query := `SELECT * FROM entries WHERE ` + partialQuery
- precomputedQuery := store.MustPrepareSelectQueryWhere(partialQuery)
-
- selectWithOblast := func(b *testing.B) {
- records := must.Return(store.Select(noctx, db, query).Collect())(b)
- assert.Equal(b, len(records), batchSize)
- }
-
- selectWithOblastWhere := func(b *testing.B) {
- records := must.Return(precomputedQuery.Select(noctx, db).Collect())(b)
- assert.Equal(b, len(records), batchSize)
- }
-
- selectWithGorp := func(b *testing.B) {
- var records []GorpEntry
- _ = must.Return(gorpDB.Select(&records, query))(b)
- assert.Equal(b, len(records), batchSize)
- }
-
- selectWithGorm := func(b *testing.B) {
- records := must.Return(gorm.G[GormEntry](gormDB).Where(partialQuery).Find(b.Context()))(b)
- assert.Equal(b, len(records), batchSize)
- }
-
- selectWithSqlite := func(b *testing.B) {
- var count int
- rows := must.Return(db.Query(query))(b) //nolint:rowserrcheck // false positive
- var (
- id int64
- message string
- )
- for rows.Next() {
- must.Succeed(b, rows.Scan(&id, &message))
- if id != 20000 && message != "" { // always true; ensures that values are not optimized away
- count++
- }
- }
- must.Succeed(b, rows.Close())
- assert.Equal(b, count, batchSize)
- }
-
- // run once to prewarm caches (if any)
- selectWithOblast(b)
- selectWithGorp(b)
- selectWithGorm(b)
- if b.Failed() {
- b.FailNow()
- }
-
- // run actual benchmark
- b.Run("via Gorm using Find", func(b *testing.B) {
- for b.Loop() {
- selectWithGorm(b)
- }
- })
- b.Run("via Gorp using Select", func(b *testing.B) {
- for b.Loop() {
- selectWithGorp(b)
- }
- })
- b.Run("via Oblast using Select", func(b *testing.B) {
- for b.Loop() {
- selectWithOblast(b)
- }
- })
- b.Run("via Oblast using SelectWhere", func(b *testing.B) {
- for b.Loop() {
- selectWithOblastWhere(b)
- }
- })
- b.Run("just SQLite", func(b *testing.B) {
- for b.Loop() {
- selectWithSqlite(b)
- }
- })
- })
- }
-}
-
-func BenchmarkORMSelectManyValues(b *testing.B) {
- db, _ := makeSqliteTestDB(b, totalRecordCountForSelect)
-
- // test with different sizes of resultsets (N=1 is an OLTP-like workload,
- // then the larger N lean more towards the OLAP side of things)
- for _, batchSize := range batchSizesForSelect {
- b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) {
- // prepare the functions that will be benched
- query := `SELECT message FROM entries WHERE id < ` + strconv.Itoa(batchSize)
- selectWithOblast := func(b *testing.B) {
- messages := must.Return(oblast.Select[string](noctx, db, query).Collect())(b)
- assert.Equal(b, len(messages), batchSize)
- }
- selectWithSqlite := func(b *testing.B) {
- var count int
- rows := must.Return(db.Query(query))(b) //nolint:rowserrcheck // false positive
- var message string
- for rows.Next() {
- must.Succeed(b, rows.Scan(&message))
- count++
- }
- must.Succeed(b, rows.Close())
- assert.Equal(b, count, batchSize)
- }
-
- // run actual benchmark
- b.Run("via Oblast", func(b *testing.B) {
- for b.Loop() {
- selectWithOblast(b)
- }
- })
- b.Run("just SQLite", func(b *testing.B) {
- for b.Loop() {
- selectWithSqlite(b)
- }
- })
- })
- }
-}
-
-func BenchmarkORMSelectOne(b *testing.B) {
- db, dsn := makeSqliteTestDB(b, totalRecordCountForSelect)
-
- // grab a "random" record from the DB, not just the first or the last
- recordID := min(totalRecordCountForSelect*2/3, totalRecordCountForSelect)
-
- // prepare the functions that will be benched
- store := oblast.MustNewStore[OblastEntry](
- oblast.SqliteDialect(),
- oblast.TableNameIs("entries"),
- oblast.PrimaryKeyIs("id"),
- )
- gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
- gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
- partialQuery := `id = ` + strconv.Itoa(recordID)
- query := `SELECT * FROM entries WHERE ` + partialQuery
- precomputedQuery := store.MustPrepareSelectQueryWhere(partialQuery)
-
- selectWithOblast := func(b *testing.B) {
- r := must.Return(store.SelectOne(noctx, db, query))(b)
- assert.Equal(b, r.ID, recordID)
- }
-
- selectWithOblastWhere := func(b *testing.B) {
- r := must.Return(precomputedQuery.SelectOne(noctx, db))(b)
- assert.Equal(b, r.ID, recordID)
- }
-
- selectWithGorp := func(b *testing.B) {
- var r GorpEntry
- must.Succeed(b, gorpDB.SelectOne(&r, query))
- assert.Equal(b, r.ID, recordID)
- }
-
- selectWithGorm := func(b *testing.B) {
- r := must.Return(gorm.G[GormEntry](gormDB).Where(partialQuery).First(b.Context()))(b)
- assert.Equal(b, r.ID, recordID)
- }
-
- selectWithSqlite := func(b *testing.B) {
- var (
- id int64
- message string
- )
- must.Succeed(b, db.QueryRow(query).Scan(&id, &message))
- assert.Equal(b, id, int64(recordID))
- }
-
- // run once to prewarm caches (if any)
- selectWithOblast(b)
- selectWithGorp(b)
- selectWithGorm(b)
- if b.Failed() {
- b.FailNow()
- }
-
- // run actual benchmark
- b.Run("via Gorm using First", func(b *testing.B) {
- for b.Loop() {
- selectWithGorm(b)
- }
- })
- b.Run("via Gorp using SelectOne", func(b *testing.B) {
- for b.Loop() {
- selectWithGorp(b)
- }
- })
- b.Run("via Oblast using SelectOne", func(b *testing.B) {
- for b.Loop() {
- selectWithOblast(b)
- }
- })
- b.Run("via Oblast using SelectOneWhere", func(b *testing.B) {
- for b.Loop() {
- selectWithOblastWhere(b)
- }
- })
- b.Run("just SQLite", func(b *testing.B) {
- for b.Loop() {
- selectWithSqlite(b)
- }
- })
-}
-
-func BenchmarkORMSelectOneValue(b *testing.B) {
- db, _ := makeSqliteTestDB(b, totalRecordCountForSelect)
-
- // grab a "random" record from the DB, not just the first or the last
- recordID := min(totalRecordCountForSelect*2/3, totalRecordCountForSelect)
-
- // prepare the functions that will be benched
- query := `SELECT message FROM entries WHERE id = ` + strconv.Itoa(recordID)
- selectWithOblast := func(b *testing.B) {
- message := must.Return(oblast.SelectOne[string](noctx, db, query))(b)
- assert.Equal(b, len(message), 71)
- }
- selectWithSqlite := func(b *testing.B) {
- var message string
- must.Succeed(b, db.QueryRow(query).Scan(&message))
- assert.Equal(b, len(message), 71)
- }
-
- // run actual benchmark
- b.Run("via Oblast", func(b *testing.B) {
- for b.Loop() {
- selectWithOblast(b)
- }
- })
- b.Run("just SQLite", func(b *testing.B) {
- for b.Loop() {
- selectWithSqlite(b)
- }
- })
-}
-
-func BenchmarkORMInsertAndDelete(b *testing.B) {
- db, dsn := makeSqliteTestDB(b, 0)
-
- store := oblast.MustNewStore[OblastEntry](
- oblast.SqliteDialect(),
- oblast.TableNameIs("entries"),
- oblast.PrimaryKeyIs("id"),
- )
- gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
- gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
- gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
-
- // test with different amounts of records
- for _, batchSize := range batchSizesForInsertDelete {
- b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) {
- // prepare the functions that will be benched
- insertAndDeleteWithOblast := func(b *testing.B) {
- 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, db, recordsForInsert...))
- for _, r := range records {
- if r.ID == 0 {
- b.Errorf("ID was not filled!")
- }
- }
- must.Succeed(b, store.Delete(noctx, db, records...))
- }
- if batchSize == 1 {
- insertAndDeleteWithOblast = func(b *testing.B) {
- record := OblastEntry{Message: "hello"}
- must.Succeed(b, store.Insert(noctx, db, &record))
- if record.ID == 0 {
- b.Errorf("ID was not filled!")
- }
- must.Succeed(b, store.Delete(noctx, db, record))
- }
- }
-
- insertAndDeleteWithGorp := func(b *testing.B) {
- records := make([]any, batchSize)
- for idx := range records {
- records[idx] = &GorpEntry{Message: "hello"}
- }
- must.Succeed(b, gorpDB.Insert(records...))
- for _, r := range records {
- if r.(*GorpEntry).ID == 0 {
- b.Errorf("ID was not filled!")
- }
- }
- _ = must.Return(gorpDB.Delete(records...))(b)
- }
- if batchSize == 1 {
- insertAndDeleteWithGorp = func(b *testing.B) {
- record := GorpEntry{Message: "hello"}
- must.Succeed(b, gorpDB.Insert(&record))
- if record.ID == 0 {
- b.Errorf("ID was not filled!")
- }
- _ = must.Return(gorpDB.Delete(&record))(b)
- }
- }
-
- insertAndDeleteWithGorm := func(b *testing.B) {
- records := make([]GormEntry, batchSize)
- for idx := range records {
- records[idx] = GormEntry{Message: "hello"}
- }
- must.Succeed(b, gorm.G[GormEntry](gormDB).CreateInBatches(b.Context(), &records, batchSize))
- for _, r := range records {
- if r.ID == 0 {
- b.Errorf("ID was not filled!")
- }
- }
- result := gormDB.Delete(&records)
- assert.ErrEqual(b, result.Error, nil)
- assert.Equal(b, result.RowsAffected, int64(batchSize))
- }
- if batchSize == 1 {
- insertAndDeleteWithGorm = func(b *testing.B) {
- record := GormEntry{Message: "hello"}
- must.Succeed(b, gorm.G[GormEntry](gormDB).Create(b.Context(), &record))
- result := gormDB.Delete(&record)
- assert.ErrEqual(b, result.Error, nil)
- assert.Equal(b, result.RowsAffected, 1)
- }
- }
-
- insertAndDeleteWithStraightExec := func(b *testing.B) {
- ids := make([]int64, batchSize)
- for idx := range ids {
- result := must.Return(db.Exec(`INSERT INTO entries (message) VALUES (?)`, "hello"))(b)
- ids[idx] = must.Return(result.LastInsertId())(b)
- }
- for _, id := range ids {
- _ = must.Return(db.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
- }
- }
-
- insertAndDeleteWithPreparedExec := func(b *testing.B) {
- ids := make([]int64, batchSize)
- stmtInsert := must.Return(db.Prepare(`INSERT INTO entries (message) VALUES (?)`))(b)
- defer stmtInsert.Close()
- for idx := range ids {
- result := must.Return(stmtInsert.Exec("hello"))(b)
- ids[idx] = must.Return(result.LastInsertId())(b)
- }
- stmtDelete := must.Return(db.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
- defer stmtDelete.Close()
- for _, id := range ids {
- _ = must.Return(stmtDelete.Exec(id))(b)
- }
- }
-
- insertAndDeleteWithStraightQueryRow := func(b *testing.B) {
- ids := make([]int64, batchSize)
- for idx := range ids {
- must.Succeed(b, db.QueryRow(`INSERT INTO entries (message) VALUES (?) RETURNING id`, "hello").Scan(&ids[idx]))
- }
- for _, id := range ids {
- _ = must.Return(db.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
- }
- }
-
- insertAndDeleteWithPreparedQueryRow := func(b *testing.B) {
- ids := make([]int64, batchSize)
- stmtInsert := must.Return(db.Prepare(`INSERT INTO entries (message) VALUES (?) RETURNING id`))(b)
- defer stmtInsert.Close()
- for idx := range ids {
- must.Succeed(b, stmtInsert.QueryRow("hello").Scan(&ids[idx]))
- }
- stmtDelete := must.Return(db.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
- defer stmtDelete.Close()
- for _, id := range ids {
- _ = must.Return(stmtDelete.Exec(id))(b)
- }
- }
-
- // run once to prewarm caches (if any)
- insertAndDeleteWithOblast(b)
- insertAndDeleteWithGorp(b)
- insertAndDeleteWithGorm(b)
-
- b.Run("via Gorm", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithGorm(b)
- }
- })
- b.Run("via Gorp", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithGorp(b)
- }
- })
- b.Run("via Oblast", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithOblast(b)
- }
- })
- b.Run("just SQLite (straight Exec)", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithStraightExec(b)
- }
- })
- b.Run("just SQLite (prepared Exec)", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithPreparedExec(b)
- }
- })
- b.Run("just SQLite (straight QueryRow)", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithStraightQueryRow(b)
- }
- })
- b.Run("just SQLite (prepared QueryRow)", func(b *testing.B) {
- for b.Loop() {
- insertAndDeleteWithPreparedQueryRow(b)
- }
- })
- })
- }
-}
-
-func BenchmarkORMUpdate(b *testing.B) {
- db, dsn := makeSqliteTestDB(b, 0)
-
- store := oblast.MustNewStore[OblastEntry](
- oblast.SqliteDialect(),
- oblast.TableNameIs("entries"),
- oblast.PrimaryKeyIs("id"),
- )
- gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
- gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
- gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
-
- // test with different amounts of records
- for _, batchSize := range batchSizesForUpdate {
- 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(db.Exec(`DELETE FROM entries`))
- recordsForOblast := make([]OblastEntry, batchSize)
- recordsForOblastForInsert := make([]*OblastEntry, batchSize)
- for idx := range recordsForOblast {
- recordsForOblast[idx] = OblastEntry{Message: "hello"}
- recordsForOblastForInsert[idx] = &recordsForOblast[idx]
- }
- must.Succeed(b, store.Insert(noctx, db, recordsForOblastForInsert...))
- recordsForGorp := make([]any, batchSize)
- for idx, r := range recordsForOblast {
- recordsForGorp[idx] = new(GorpEntry(r))
- }
- recordsForGorm := make([]GormEntry, batchSize)
- for idx, r := range recordsForOblast {
- recordsForGorm[idx] = GormEntry(r)
- }
-
- // prepare the functions that will be benched
- updateWithOblast := func(b *testing.B, message string) {
- for idx := range recordsForOblast {
- recordsForOblast[idx].Message = message
- }
- must.Succeed(b, store.Update(noctx, db, recordsForOblast...))
- }
- updateWithGorp := func(b *testing.B, message string) {
- for _, r := range recordsForGorp {
- r.(*GorpEntry).Message = message
- }
- _ = must.Return(gorpDB.Update(recordsForGorp...))(b)
- }
- updateWithGorm := func(b *testing.B, message string) {
- for idx := range recordsForGorm {
- recordsForGorm[idx].Message = message
- }
- result := gormDB.Save(&recordsForGorm)
- assert.ErrEqual(b, result.Error, nil)
- assert.Equal(b, result.RowsAffected, int64(batchSize))
- }
- updateWithStraightSqlite := func(b *testing.B, message string) {
- for _, r := range recordsForOblast {
- _ = must.Return(db.Exec(`UPDATE entries SET message = ? WHERE id = ?`, message, r.ID))(b)
- }
- }
- updateWithPreparedSqlite := func(b *testing.B, message string) {
- stmt := must.Return(db.Prepare(`UPDATE entries SET message = ? WHERE id = ?`))(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
- must.Succeed(b, db.QueryRow(`SELECT COUNT(*) FROM entries WHERE message = ?`, message).Scan(&count))
- assert.Equal(b, count, int64(batchSize))
- }
-
- // run once to prewarm caches (if any)
- updateWithGorm(b, "warming up")
- updateWithGorp(b, "warming up")
- updateWithOblast(b, "warming up")
-
- b.Run("via Gorm", func(b *testing.B) {
- idx := 0
- for b.Loop() {
- idx++
- message := fmt.Sprintf("round %d", idx)
- updateWithGorm(b, message)
- checkRecordsUpdated(b, message)
- }
- })
- b.Run("via Gorp", func(b *testing.B) {
- idx := 0
- for b.Loop() {
- idx++
- message := fmt.Sprintf("round %d", idx)
- updateWithGorp(b, message)
- checkRecordsUpdated(b, message)
- }
- })
- b.Run("via Oblast", func(b *testing.B) {
- idx := 0
- for b.Loop() {
- idx++
- message := fmt.Sprintf("round %d", idx)
- updateWithOblast(b, message)
- checkRecordsUpdated(b, message)
- }
- })
- b.Run("just SQLite (straight)", func(b *testing.B) {
- idx := 0
- for b.Loop() {
- idx++
- message := fmt.Sprintf("round %d", idx)
- updateWithStraightSqlite(b, message)
- checkRecordsUpdated(b, message)
- }
- })
- b.Run("just SQLite (prepared)", func(b *testing.B) {
- idx := 0
- for b.Loop() {
- idx++
- message := fmt.Sprintf("round %d", idx)
- updateWithPreparedSqlite(b, message)
- checkRecordsUpdated(b, message)
- }
- })
- })
- }
-}
diff --git a/benchmark/go.mod b/benchmark/go.mod
deleted file mode 100644
index be67a60..0000000
--- a/benchmark/go.mod
+++ /dev/null
@@ -1,24 +0,0 @@
-module go.xyrillian.de/oblast/benchmark
-
-go 1.26.0
-
-require (
- github.com/go-gorp/gorp/v3 v3.1.0
- github.com/jackc/pgx/v5 v5.10.0
- github.com/lib/pq v1.12.3
- github.com/mattn/go-sqlite3 v1.14.48
- go.xyrillian.de/gg v1.14.0
- go.xyrillian.de/oblast v0.11.0
- gorm.io/driver/sqlite v1.6.0
- gorm.io/gorm v1.31.2
-)
-
-require (
- github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
- github.com/jackc/puddle/v2 v2.2.2 // indirect
- github.com/jinzhu/inflection v1.0.0 // indirect
- github.com/jinzhu/now v1.1.5 // indirect
- golang.org/x/sync v0.22.0 // indirect
- golang.org/x/text v0.40.0 // indirect
-)
diff --git a/benchmark/go.sum b/benchmark/go.sum
deleted file mode 100644
index 2b2755a..0000000
--- a/benchmark/go.sum
+++ /dev/null
@@ -1,48 +0,0 @@
-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.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
-github.com/jackc/pgx/v5 v5.10.0/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=
-github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
-github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
-github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
-github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-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/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.14.0 h1:S19Jk3V1dcF9WdXQi7OGWjboVj8/40I4+/G1lZ6i4TI=
-go.xyrillian.de/gg v1.14.0/go.mod h1:DoO4fQSWIrBRlNlCjVyrYM0kAEBt/Jg2GkMH+cGRZ0k=
-go.xyrillian.de/oblast v0.11.0 h1:ZqHsxoQW/LPgtQ/jXlG1X3DLTYkgxf9zzrDYyykXJG4=
-go.xyrillian.de/oblast v0.11.0/go.mod h1:sYCzxyVFzzL43EglZJn7Vtd8kfHYBwKff/Us4yF8k+Q=
-golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
-golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
-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=
-gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
-gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
-gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
diff --git a/benchmark/internal/oblast_pgx/handle.go b/benchmark/internal/oblast_pgx/handle.go
deleted file mode 100644
index 4bd72bd..0000000
--- a/benchmark/internal/oblast_pgx/handle.go
+++ /dev/null
@@ -1,89 +0,0 @@
-// 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"
- "github.com/jackc/pgx/v5/pgxpool"
- "go.xyrillian.de/gg/gsql"
-)
-
-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 = &pgxpool.Conn{}
- _ Handle = pgx.Tx(&pgxpool.Tx{})
-)
-
-func Wrap(h Handle) gsql.Handle {
- switch h := h.(type) {
- case *pgx.Conn:
- return wrappedHandle{h}
- case *pgxpool.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
-}
-
-// GSQLPrepare implements the [gsql.Handle] interface.
-func (h wrappedHandle) GSQLPrepare(ctx context.Context, query string, repeated bool) (gsql.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 *pgxpool.Conn:
- // pgxpool.Conn does not have Prepare()
- return wrappedUnpreparedStatement{query, h.inner}, nil
- 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 *pgxpool.Conn:
- panic("unreachable") // because func GSQLPrepare() does not return a wrappedPreparedStatement for this underlying type
- case pgx.Tx:
- return h.Conn().Deallocate(ctx, stmt.Name)
- default:
- panic("unreachable") // because of the check in func Wrap()
- }
-}
-
-// GSQLQuery implements the [gsql.Handle] interface.
-func (h wrappedHandle) GSQLQuery(ctx context.Context, query string, args []any) (gsql.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
deleted file mode 100644
index f842d36..0000000
--- a/benchmark/internal/oblast_pgx/results.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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/gg/gsql"
-)
-
-type wrappedRows struct {
- inner pgx.Rows
-}
-
-var _ gsql.Rows = wrappedRows{}
-
-// Columns implements the [gsql.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 [gsql.Rows] interface.
-func (r wrappedRows) Close() error {
- r.inner.Close()
- return nil
-}
-
-// Err implements the [gsql.Rows] interface.
-func (r wrappedRows) Err() error {
- return r.inner.Err()
-}
-
-// Next implements the [gsql.Rows] interface.
-func (r wrappedRows) Next() bool {
- return r.inner.Next()
-}
-
-// Scan implements the [gsql.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
deleted file mode 100644
index 0a33c73..0000000
--- a/benchmark/internal/oblast_pgx/statement.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// 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/gg/gsql"
-)
-
-type wrappedPreparedStatement struct {
- ctx context.Context
- statement *pgconn.StatementDescription
- handle Handle
-}
-
-type wrappedUnpreparedStatement struct {
- query string
- handle Handle
-}
-
-var (
- _ gsql.Statement = wrappedPreparedStatement{}
- _ gsql.Statement = wrappedUnpreparedStatement{}
-)
-
-// Close implements the [gsql.Statement] interface.
-func (s wrappedPreparedStatement) Close() error {
- return deallocate(s.ctx, s.handle, s.statement)
-}
-
-// Close implements the [gsql.Statement] interface.
-func (s wrappedUnpreparedStatement) Close() error {
- return nil
-}
-
-// Exec implements the [gsql.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 [gsql.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 [gsql.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 [gsql.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/main.go b/benchmark/main.go
deleted file mode 100644
index e80c2cd..0000000
--- a/benchmark/main.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-func main() {
- panic("run with `go test -bench`")
-}
diff --git a/benchmark/postgres_test.go b/benchmark/postgres_test.go
deleted file mode 100644
index dba7ca8..0000000
--- a/benchmark/postgres_test.go
+++ /dev/null
@@ -1,393 +0,0 @@
-// 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/gg/assert"
- "go.xyrillian.de/gg/gsql"
- "go.xyrillian.de/oblast"
- "go.xyrillian.de/oblast/benchmark/internal/oblast_pgx"
- "go.xyrillian.de/oblast/internal/testhelpers/must"
-)
-
-// NOTE: In this file, we benchmark different PostgreSQL database drivers against each other with or without Oblast in between.
-// 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) *gsql.DB {
- dsn := cmp.Or(os.Getenv("BENCHMARK_POSTGRES_DSN"), defaultPostgresDSN)
- db := gsql.NewDB(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
- query := `INSERT INTO entries (id, message) VALUES ($1, $2)`
- stmt := must.Return(conn.Prepare(ctx, query, query))(t)
- for idx := range recordCount {
- buf := sha256.Sum256([]byte(strconv.Itoa(idx)))
- _ = must.Return(conn.Exec(ctx, query, 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)
- 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, pqDB, query).Collect())(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).Collect())(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)
- 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)
- 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, pqDB))(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)
- 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 gsql.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, pqDB)
- }
- })
-
- 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)
- 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, pqDB, 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 gsql.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, pqDB, 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))
- })
- })
- })
- }
-}