diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-04-10 15:56:22 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-04-10 15:56:22 +0200 |
| commit | e0fb5aa0acc1983648ab1480f22114aead234eeb (patch) | |
| tree | bc70cbac9a3f33b596d7435576d6a789c6c34144 | |
| parent | bce3df549ff4ccc8895697a3222269bd14fc22a4 (diff) | |
| download | go-oblast-e0fb5aa0acc1983648ab1480f22114aead234eeb.tar.gz | |
initial MVP for oblast.Select()
| -rw-r--r-- | benchmark/benchmark_test.go | 131 | ||||
| -rw-r--r-- | benchmark/go.mod | 9 | ||||
| -rw-r--r-- | benchmark/go.sum | 20 | ||||
| -rw-r--r-- | benchmark/main.go | 8 | ||||
| -rw-r--r-- | db.go | 52 | ||||
| -rw-r--r-- | go.work | 6 | ||||
| -rw-r--r-- | go.work.sum | 7 |
7 files changed, 232 insertions, 1 deletions
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go new file mode 100644 index 0000000..c08361e --- /dev/null +++ b/benchmark/benchmark_test.go @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package main_test + +import ( + "crypto/sha256" + "database/sql" + "fmt" + "strconv" + "testing" + + "github.com/go-gorp/gorp/v3" + _ "github.com/mattn/go-sqlite3" + "go.xyrillian.de/oblast" +) + +func BenchmarkSelect(b *testing.B) { + const ( + totalRecordCount = 10000 + selectedRecordCount = 1 + ) + + db, err := sql.Open("sqlite3", "file:foobar?mode=memory&cache=shared") + if err != nil { + b.Fatal(err) + } + + // fill in some random-looking, but deterministic data + _, err = db.Exec(`CREATE TABLE entries (id INTEGER, message TEXT)`) + if err != nil { + b.Fatal(err) + } + stmt, err := db.Prepare(`INSERT INTO entries (id, message) VALUES (?, ?)`) + if err != nil { + b.Fatal(err) + } + for idx := range totalRecordCount { + buf := sha256.Sum256([]byte(strconv.Itoa(idx))) + _, err = stmt.Exec(idx, fmt.Sprintf("sha256:%x", buf[:])) + if err != nil { + b.Fatal(err) + } + } + err = stmt.Close() + if err != nil { + b.Fatal(err) + } + + // prepare the functions that will be benched + odb := oblast.NewDB(db, oblast.SqliteDialect()) + gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + type record struct { + ID int `db:"id"` + Message string `db:"message"` + } + query := `SELECT * FROM entries WHERE id < ` + strconv.Itoa(selectedRecordCount) + + selectWithOblast := func(b *testing.B) { + records, err := oblast.Select[record](b.Context(), odb, query) + if err != nil { + b.Error(err) + } + if len(records) != selectedRecordCount { + b.Errorf("expected %d, but got %d records", selectedRecordCount, len(records)) + } + } + + selectWithGorp := func(b *testing.B) { + var records []record + _, err := gdb.Select(&records, query) + if err != nil { + b.Error(err) + } + if len(records) != selectedRecordCount { + b.Errorf("expected %d, but got %d records", selectedRecordCount, len(records)) + } + } + + selectWithSqlite := func(b *testing.B) { + var count int64 + rows, err := db.Query(query) + if err != nil { + b.Error(err) + } + var ( + id int64 + message string + ) + for rows.Next() { + err := rows.Scan(&id, &message) + if err != nil { + b.Error(err) + } + if id != 20000 && message != "" { // always true; ensures that values are not optimized away + count++ + } + } + err = rows.Close() + if err != nil { + b.Error(err) + } + if count != selectedRecordCount { + b.Errorf("expected %d, but got %d records", selectedRecordCount, count) + } + } + + // run once to prewarm caches + selectWithOblast(b) + selectWithGorp(b) + if b.Failed() { + b.FailNow() + } + + // run actual benchmark + b.Run("via Gorp", func(b *testing.B) { + for range b.N { + selectWithGorp(b) + } + }) + b.Run("via Oblast", func(b *testing.B) { + for range b.N { + selectWithOblast(b) + } + }) + b.Run("just SQLite", func(b *testing.B) { + for range b.N { + selectWithSqlite(b) + } + }) +} diff --git a/benchmark/go.mod b/benchmark/go.mod new file mode 100644 index 0000000..01e8126 --- /dev/null +++ b/benchmark/go.mod @@ -0,0 +1,9 @@ +module go.xyrillian.de/oblast/benchmark + +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 +) diff --git a/benchmark/go.sum b/benchmark/go.sum new file mode 100644 index 0000000..e05abdd --- /dev/null +++ b/benchmark/go.sum @@ -0,0 +1,20 @@ +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/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= +github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +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/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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/benchmark/main.go b/benchmark/main.go new file mode 100644 index 0000000..e80c2cd --- /dev/null +++ b/benchmark/main.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package main + +func main() { + panic("run with `go test -bench`") +} @@ -6,6 +6,7 @@ package oblast import ( "context" "database/sql" + "fmt" "reflect" "sync" ) @@ -26,9 +27,58 @@ func NewDB(db *sql.DB, dialect Dialect) *DB { } } +// TODO: remove func Keks[T IsTable](ctx context.Context, db *DB) error { _, err := db.getPlan(reflect.TypeFor[T]()) return err } -// TODO: Begin() -> custom Tx type +// TODO: Begin() -> custom Tx type; add interface to allow Select() et all to take either *DB or *Tx + +func Select[T any](ctx context.Context, db *DB, query string, args ...any) ([]T, error) { + // TODO: minimize function body to avoid binary size blowup from monomorphization + // TODO: catch error from rows.Close(), if any + // TODO: add context to errors + + plan, err := db.getPlan(reflect.TypeFor[T]()) + if err != nil { + return nil, err + } + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + columnNames, err := rows.Columns() + if err != nil { + return nil, err + } + indexes := make([][]int, len(columnNames)) + for idx, columnName := range columnNames { + var ok bool + indexes[idx], ok = plan.IndexByColumnName[columnName] + if !ok { + var zero T + return nil, fmt.Errorf("result has column %q in position %d, but no field in %T has `db:%[1]q`", + columnName, idx, zero) + } + } + + var result []T + slots := make([]any, len(indexes)) + for rows.Next() { + var target T + rvalue := reflect.ValueOf(&target).Elem() + for idx, index := range indexes { + slots[idx] = rvalue.FieldByIndex(index).Addr().Interface() + } + err := rows.Scan(slots...) + if err != nil { + return nil, err + } + result = append(result, target) + } + + return result, nil +} @@ -0,0 +1,6 @@ +go 1.26.0 + +use ( + . + ./benchmark +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..847440b --- /dev/null +++ b/go.work.sum @@ -0,0 +1,7 @@ +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/nelsam/hel/v2 v2.3.3/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= +golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |
