aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-04-10 15:56:22 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-04-10 15:56:22 +0200
commite0fb5aa0acc1983648ab1480f22114aead234eeb (patch)
treebc70cbac9a3f33b596d7435576d6a789c6c34144
parentbce3df549ff4ccc8895697a3222269bd14fc22a4 (diff)
downloadgo-oblast-e0fb5aa0acc1983648ab1480f22114aead234eeb.tar.gz
initial MVP for oblast.Select()
-rw-r--r--benchmark/benchmark_test.go131
-rw-r--r--benchmark/go.mod9
-rw-r--r--benchmark/go.sum20
-rw-r--r--benchmark/main.go8
-rw-r--r--db.go52
-rw-r--r--go.work6
-rw-r--r--go.work.sum7
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`")
+}
diff --git a/db.go b/db.go
index a511d6f..8f1a050 100644
--- a/db.go
+++ b/db.go
@@ -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
+}
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..0f59ee6
--- /dev/null
+++ b/go.work
@@ -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=