aboutsummaryrefslogtreecommitdiff
path: root/benchmark
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-04-17 22:46:09 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-04-17 22:46:28 +0200
commitc1c8cc2f6b49741d33a83a534d244fc01b0e1b4a (patch)
tree4ec9d8b15a4f528fbf3e64060a19d68fe04a2a48 /benchmark
parentba4f55e75e6b6ebc3ecfc744d94377e6f3417693 (diff)
downloadgo-oblast-c1c8cc2f6b49741d33a83a534d244fc01b0e1b4a.tar.gz
benchmark: add Gorm
Diffstat (limited to 'benchmark')
-rw-r--r--benchmark/benchmark_test.go136
-rw-r--r--benchmark/go.mod10
-rw-r--r--benchmark/go.sum14
3 files changed, 123 insertions, 37 deletions
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go
index 83832ba..c2d27b3 100644
--- a/benchmark/benchmark_test.go
+++ b/benchmark/benchmark_test.go
@@ -15,6 +15,8 @@ import (
"go.xyrillian.de/oblast"
"go.xyrillian.de/oblast/internal/assert"
"go.xyrillian.de/oblast/internal/must"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
)
var (
@@ -24,8 +26,9 @@ var (
batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100}
)
-func makeTestDB(t testing.TB, recordCount int) *sql.DB {
- db := must.Return(sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())))(t)
+func makeTestDB(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)
if recordCount > 0 {
@@ -38,7 +41,7 @@ func makeTestDB(t testing.TB, recordCount int) *sql.DB {
must.Succeed(t, stmt.Close())
}
- return db
+ return db, dsn
}
type OblastEntry struct {
@@ -51,23 +54,28 @@ type GorpEntry struct {
Message string `db:"message"`
}
+type GormEntry struct {
+ ID int `gorm:"primaryKey"`
+ Message string
+}
+
+func (GormEntry) TableName() string { return "entries" }
+
func BenchmarkSelectMany(b *testing.B) {
- db := makeTestDB(b, totalRecordCountForSelect)
+ db, dsn := makeTestDB(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, err := oblast.NewStore[OblastEntry](
+ store := oblast.MustNewStore[OblastEntry](
oblast.SqliteDialect(),
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- if err != nil {
- b.Fatal(err)
- }
- gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{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
@@ -83,7 +91,12 @@ func BenchmarkSelectMany(b *testing.B) {
selectWithGorp := func(b *testing.B) {
var records []GorpEntry
- _ = must.Return(gdb.Select(&records, query))(b)
+ _ = 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)
}
@@ -107,11 +120,17 @@ func BenchmarkSelectMany(b *testing.B) {
// 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() {
+ selectWithGorp(b)
+ }
+ })
b.Run("via Gorp using Select", func(b *testing.B) {
for b.Loop() {
selectWithGorp(b)
@@ -137,21 +156,19 @@ func BenchmarkSelectMany(b *testing.B) {
}
func BenchmarkSelectOne(b *testing.B) {
- db := makeTestDB(b, totalRecordCountForSelect)
+ db, dsn := makeTestDB(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, err := oblast.NewStore[OblastEntry](
+ store := oblast.MustNewStore[OblastEntry](
oblast.SqliteDialect(),
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- if err != nil {
- b.Fatal(err)
- }
- gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{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
@@ -167,7 +184,12 @@ func BenchmarkSelectOne(b *testing.B) {
selectWithGorp := func(b *testing.B) {
var r GorpEntry
- must.Succeed(b, gdb.SelectOne(&r, query))
+ 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)
}
@@ -183,11 +205,17 @@ func BenchmarkSelectOne(b *testing.B) {
// 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)
@@ -211,18 +239,16 @@ func BenchmarkSelectOne(b *testing.B) {
}
func BenchmarkInsertAndDelete(b *testing.B) {
- db := makeTestDB(b, 0)
+ db, dsn := makeTestDB(b, 0)
- store, err := oblast.NewStore[OblastEntry](
+ store := oblast.MustNewStore[OblastEntry](
oblast.SqliteDialect(),
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- if err != nil {
- b.Fatal(err)
- }
- gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
- gdb.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
+ gorpDB := gorp.DbMap{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 {
@@ -247,13 +273,29 @@ func BenchmarkInsertAndDelete(b *testing.B) {
for idx := range records {
records[idx] = &GorpEntry{Message: "hello"}
}
- must.Succeed(b, gdb.Insert(records...))
+ must.Succeed(b, gorpDB.Insert(records...))
for _, r := range records {
if r.(*GorpEntry).ID == 0 {
b.Errorf("ID was not filled!")
}
}
- _ = must.Return(gdb.Delete(records...))(b)
+ _ = must.Return(gorpDB.Delete(records...))(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, "<success>")
+ assert.Equal(b, result.RowsAffected, int64(batchSize))
}
insertAndDeleteWithStraightSqlite := func(b *testing.B) {
@@ -285,7 +327,13 @@ func BenchmarkInsertAndDelete(b *testing.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)
@@ -311,18 +359,16 @@ func BenchmarkInsertAndDelete(b *testing.B) {
}
func BenchmarkUpdate(b *testing.B) {
- db := makeTestDB(b, 0)
+ db, dsn := makeTestDB(b, 0)
- store, err := oblast.NewStore[OblastEntry](
+ store := oblast.MustNewStore[OblastEntry](
oblast.SqliteDialect(),
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- if err != nil {
- b.Fatal(err)
- }
- gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
- gdb.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
+ gorpDB := gorp.DbMap{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 {
@@ -338,6 +384,10 @@ func BenchmarkUpdate(b *testing.B) {
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) {
@@ -350,7 +400,15 @@ func BenchmarkUpdate(b *testing.B) {
for _, r := range recordsForGorp {
r.(*GorpEntry).Message = message
}
- _ = must.Return(gdb.Update(recordsForGorp...))(b)
+ _ = 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, "<success>")
+ assert.Equal(b, result.RowsAffected, int64(batchSize))
}
updateWithStraightSqlite := func(b *testing.B, message string) {
for _, r := range recordsForOblast {
@@ -370,9 +428,19 @@ func BenchmarkUpdate(b *testing.B) {
}
// 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() {
diff --git a/benchmark/go.mod b/benchmark/go.mod
index 01e8126..75f27db 100644
--- a/benchmark/go.mod
+++ b/benchmark/go.mod
@@ -5,5 +5,13 @@ go 1.26.0
require (
github.com/go-gorp/gorp/v3 v3.1.0
github.com/mattn/go-sqlite3 v1.14.42
- go.xyrillian.de/oblast v0.0.0-20260410125639-bce3df549ff4
+ go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b
+ gorm.io/driver/sqlite v1.6.0
+ gorm.io/gorm v1.31.1
+)
+
+require (
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ golang.org/x/text v0.20.0 // indirect
)
diff --git a/benchmark/go.sum b/benchmark/go.sum
index e05abdd..5701940 100644
--- a/benchmark/go.sum
+++ b/benchmark/go.sum
@@ -4,6 +4,10 @@ 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/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.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
@@ -14,7 +18,13 @@ 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-20260410125639-bce3df549ff4 h1:MJpcBoNsrY4e/eRpYfz1Gllc3aqIjwFxSPSyKToa+mQ=
-go.xyrillian.de/oblast v0.0.0-20260410125639-bce3df549ff4/go.mod h1:lo6ekGOHTID0KeSWhNQV1gjYJ2BfhXgenUEBNBnZkBM=
+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=
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.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
+gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=