diff options
Diffstat (limited to 'benchmark')
| -rw-r--r-- | benchmark/.gitignore | 3 | ||||
| -rw-r--r-- | benchmark/benchmark_test.go | 641 | ||||
| -rw-r--r-- | benchmark/go.mod | 24 | ||||
| -rw-r--r-- | benchmark/go.sum | 48 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/handle.go | 89 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/results.go | 66 | ||||
| -rw-r--r-- | benchmark/internal/oblast_pgx/statement.go | 60 | ||||
| -rw-r--r-- | benchmark/main.go | 8 | ||||
| -rw-r--r-- | benchmark/postgres_test.go | 393 |
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)) - }) - }) - }) - } -} |
