aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-05-08 22:56:18 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-05-08 22:56:18 +0200
commit5d655f04f8ab0dfa018430620caa2f56fcd9450a (patch)
treea34c94bafbf31cca3751eb826751e3d80103df99
parent9b456c354ec23ae85f21054e1326683ebccca86a (diff)
downloadgo-oblast-5d655f04f8ab0dfa018430620caa2f56fcd9450a.tar.gz
add type RuntimeIndex
-rw-r--r--CHANGELOG.md6
-rw-r--r--runtimeindex.go57
-rw-r--r--runtimeindex_test.go72
3 files changed, 135 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d316aba..ff08023 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@ SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
SPDX-License-Identifier: Apache-2.0
-->
+# v0.6.0 (TBD)
+
+API changes:
+
+- Add `type RuntimeIndex`.
+
# v0.5.0 (2026-05-08)
API changes:
diff --git a/runtimeindex.go b/runtimeindex.go
new file mode 100644
index 0000000..a5f1f67
--- /dev/null
+++ b/runtimeindex.go
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package oblast
+
+// RuntimeIndex provides methods for sorting records (R) by some type of key (K) at runtime.
+// It is most commonly used with the result of [Store.Select] or [Store.SelectWhere], to build a lookup table for or partition of the retrieved records.
+type RuntimeIndex[R any, K comparable] func(R) K
+
+// NewRuntimeIndex casts a function into type [RuntimeIndex].
+//
+// In practice, this is more compact than writing the cast directly
+// because type arguments can be inferred for function calls, but not type casts.
+func NewRuntimeIndex[R any, K comparable](f func(R) K) RuntimeIndex[R, K] {
+ return RuntimeIndex[R, K](f)
+}
+
+// Index builds a lookup table of the provided records.
+//
+// This should only be used when the index yields unique values for each record.
+// If there can be duplicates, use [RuntimeIndex.Partition] instead.
+func (i RuntimeIndex[R, K]) Index(records []R) map[K]R {
+ result := make(map[K]R, len(records))
+ for _, r := range records {
+ result[i(r)] = r
+ }
+ return result
+}
+
+// IndexFrom is like Index, but can directly wrap a [Store.Select] or [Store.SelectWhere] call.
+// If there is an error, it is passed through unchanged.
+func (i RuntimeIndex[R, K]) IndexFrom(records []R, err error) (map[K]R, error) {
+ if err != nil {
+ return nil, err
+ }
+ return i.Index(records), nil
+}
+
+// Partition builds a partition of the resulting records by their index value.
+// Within each partition, the original order of records is retained.
+func (i RuntimeIndex[R, K]) Partition(records []R) map[K][]R {
+ result := make(map[K][]R, len(records))
+ for _, r := range records {
+ key := i(r)
+ result[key] = append(result[key], r)
+ }
+ return result
+}
+
+// PartitionFrom is like Partition, but can directly wrap a [Store.Select] or [Store.SelectWhere] call.
+// If there is an error, it is passed through unchanged.
+func (i RuntimeIndex[R, K]) PartitionFrom(records []R, err error) (map[K][]R, error) {
+ if err != nil {
+ return nil, err
+ }
+ return i.Partition(records), nil
+}
diff --git a/runtimeindex_test.go b/runtimeindex_test.go
new file mode 100644
index 0000000..ba16fd9
--- /dev/null
+++ b/runtimeindex_test.go
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package oblast_test
+
+import (
+ "database/sql"
+ "testing"
+
+ "go.xyrillian.de/oblast"
+ "go.xyrillian.de/oblast/internal/testhelpers/assert"
+ "go.xyrillian.de/oblast/internal/testhelpers/mock"
+ "go.xyrillian.de/oblast/internal/testhelpers/must"
+)
+
+func TestRuntimeIndex(t *testing.T) {
+ ctx := t.Context()
+ md := mock.NewDriver()
+ db := sql.OpenDB(md)
+
+ type basicRecord struct {
+ ID int64 `db:"id"`
+ Name string `db:"name"`
+ }
+ store := oblast.MustNewStore[basicRecord](
+ oblast.SqliteDialect(),
+ oblast.TableNameIs("basic_records"),
+ oblast.PrimaryKeyIs("id"),
+ )
+
+ commonSetup := func() {
+ md.ForQuery(`SELECT "id", "name" FROM "basic_records" WHERE id > 0`).
+ ExpectQueryWithArgs().
+ AndReturnColumns("id", "name").
+ WithRow(1, "foo").
+ WithRow(2, "bar").
+ WithRow(3, "baz")
+ }
+
+ t.Run("Index", func(t *testing.T) {
+ byName := oblast.NewRuntimeIndex(func(r basicRecord) string { return r.Name })
+
+ // test success path
+ commonSetup()
+ result := must.Return(byName.IndexFrom(store.SelectWhere(ctx, db, `id > 0`)))(t)
+ assert.DeepEqual(t, result, map[string]basicRecord{
+ "foo": {1, "foo"},
+ "bar": {2, "bar"},
+ "baz": {3, "baz"},
+ })
+
+ // test error path
+ _, err := byName.IndexFrom(store.SelectWhere(ctx, db, `id = 1`))
+ assert.ErrEqual(t, err, `during Query(): unexpected query: SELECT "id", "name" FROM "basic_records" WHERE id = 1`)
+ })
+
+ t.Run("Partition", func(t *testing.T) {
+ byFirstLetter := oblast.NewRuntimeIndex(func(r basicRecord) string { return r.Name[0:1] })
+
+ // test success path
+ commonSetup()
+ result := must.Return(byFirstLetter.PartitionFrom(store.SelectWhere(ctx, db, `id > 0`)))(t)
+ assert.DeepEqual(t, result, map[string][]basicRecord{
+ "f": {{1, "foo"}},
+ "b": {{2, "bar"}, {3, "baz"}},
+ })
+
+ // test error path
+ _, err := byFirstLetter.PartitionFrom(store.SelectWhere(ctx, db, `id = 1`))
+ assert.ErrEqual(t, err, `during Query(): unexpected query: SELECT "id", "name" FROM "basic_records" WHERE id = 1`)
+ })
+}