diff options
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | select.go | 45 |
2 files changed, 42 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b56ec45..781348e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,19 +5,23 @@ SPDX-License-Identifier: Apache-2.0 # v0.3.0 (TBD) -Changes: +API changes: - All `Store` methods that run DB operations now take a `context.Context` argument. The previous decision to avoid `ctx` arguments was based on benchmarking using `b.Context()`, where it turns out that we just benchmarked those particular stacked contexts being inefficient. - `Store.Insert()` now takes its arguments by-pointer. This is probably slightly less efficient, but significantly safer because autogenerated field values cannot be disregarded by accident. -- Add `Store.Update()`. +- Add `Store.Upsert()`. - Removed support for SQL dialects that rely on LastInsertId() for ID columns. Using a RETURNING clause to collect autogenerated field values is better in many ways, and has been supported by both MariaDB and SQLite for at least six years. In practice, this only drops support specifically for Oracle MySQL. +Other changes: + +- Removed an unnecessary memory allocation and copy within `Select()` and `SelectWhere()`. + # v0.2.0 (2026-04-18) Changes: @@ -27,17 +27,44 @@ func (s Store[R]) Select(ctx context.Context, db Handle, query string, args ...a var result []R slots := make([]any, len(indexes)) for rows.Next() { - var target R - err = collectRow(rows, s.plan, reflect.ValueOf(&target).Elem(), slots, indexes) + var target *R + result, target = growRecordSlice(result) + err = collectRow(rows, s.plan, reflect.ValueOf(target).Elem(), slots, indexes) if err != nil { return nil, err } - result = append(result, target) } return result, newIOError(err, "Rows.Err", rows.Err()) } +// Appends an empty R to the slice and returns a pointer to it, as well as the updated slice. +// It is more efficient to write: +// +// var result []R +// for rows.Next() { +// var target *R +// result, target = growRecordSlice(result) +// doSomethingWith(rows, reflect.ValueOf(target).Elem()) +// } +// +// Instead of the more obvious: +// +// var result []R +// for rows.Next() { +// var target R +// doSomethingWith(rows, reflect.ValueOf(&target).Elem()) +// result = append(result, target) +// } +// +// In the second phrasing, `target` escapes to the heap because of `reflect.ValueOf(&target)`, +// causing an additional allocation for `target` as well as a memcpy of `target` during `append()`. +func growRecordSlice[R any](records []R) (newRecords []R, target *R) { + var zero R + newRecords = append(records, zero) + return newRecords, &newRecords[len(newRecords)-1] +} + // SelectWhere is like [Store.Select], but you only provide the part of the SELECT query that comes after the WHERE. // The initial part ("SELECT ... FROM ... WHERE") is autogenerated and prepended to partialQuery. // This has two benefits: @@ -63,12 +90,12 @@ func (s Store[R]) SelectWhere(ctx context.Context, db Handle, partialQuery strin var result []R slots := make([]any, len(indexes)) for rows.Next() { - var target R - err = collectRow(rows, s.plan, reflect.ValueOf(&target).Elem(), slots, indexes) + var target *R + result, target = growRecordSlice(result) + err = collectRow(rows, s.plan, reflect.ValueOf(target).Elem(), slots, indexes) if err != nil { return nil, err } - result = append(result, target) } return result, newIOError(err, "Rows.Err", rows.Err()) @@ -232,12 +259,12 @@ func (q PreparedSelectQuery[R]) Select(ctx context.Context, db Handle, args ...a var result []R slots := make([]any, len(indexes)) for rows.Next() { - var target R - err = collectRow(rows, q.store.plan, reflect.ValueOf(&target).Elem(), slots, indexes) + var target *R + result, target = growRecordSlice(result) + err = collectRow(rows, q.store.plan, reflect.ValueOf(target).Elem(), slots, indexes) if err != nil { return nil, err } - result = append(result, target) } return result, newIOError(err, "Rows.Err", rows.Err()) |
