From 5d655f04f8ab0dfa018430620caa2f56fcd9450a Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Fri, 8 May 2026 22:56:18 +0200 Subject: add type RuntimeIndex --- CHANGELOG.md | 6 +++++ runtimeindex.go | 57 +++++++++++++++++++++++++++++++++++++++++ runtimeindex_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 runtimeindex.go create mode 100644 runtimeindex_test.go 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 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 +// 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 +// 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`) + }) +} -- cgit v1.2.3