Add store helpers: transactions, ULIDs and time conventions
- Store.WithTx runs fn on the writer, committing on nil and rolling back on error, panic or goroutine exit; panics propagate unchanged. - NewID returns a monotonic ULID string (oklog/ulid/v2, crypto/rand), strictly increasing within the process. - Timestamp stores UTC RFC 3339 TEXT at fixed millisecond precision. - LocalDate stores YYYY-MM-DD TEXT and rejects invalid or non-canonical dates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func openTxTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
s := openTestStore(t)
|
||||
if _, err := s.Writer.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func countRows(t *testing.T, s *Store) int {
|
||||
t.Helper()
|
||||
var n int
|
||||
if err := s.Reader.QueryRow("SELECT count(*) FROM t").Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func insertRow(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO t (id) VALUES (1)")
|
||||
return err
|
||||
}
|
||||
|
||||
func TestWithTxCommitsOnNil(t *testing.T) {
|
||||
s := openTxTestStore(t)
|
||||
if err := s.WithTx(context.Background(), insertRow); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := countRows(t, s); n != 1 {
|
||||
t.Fatalf("rows after commit = %d, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTxRollsBackOnError(t *testing.T) {
|
||||
s := openTxTestStore(t)
|
||||
errBoom := errors.New("boom")
|
||||
err := s.WithTx(context.Background(), func(tx *sql.Tx) error {
|
||||
if err := insertRow(tx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return errBoom
|
||||
})
|
||||
if !errors.Is(err, errBoom) {
|
||||
t.Fatalf("WithTx error = %v, want %v", err, errBoom)
|
||||
}
|
||||
if n := countRows(t, s); n != 0 {
|
||||
t.Fatalf("rows after rollback = %d, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTxRollsBackOnPanic(t *testing.T) {
|
||||
s := openTxTestStore(t)
|
||||
const value = "kaboom"
|
||||
func() {
|
||||
defer func() {
|
||||
if p := recover(); p != value {
|
||||
t.Fatalf("recovered %v, want panic %q to propagate", p, value)
|
||||
}
|
||||
}()
|
||||
_ = s.WithTx(context.Background(), func(tx *sql.Tx) error {
|
||||
if err := insertRow(tx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
panic(value)
|
||||
})
|
||||
t.Fatal("WithTx returned instead of panicking")
|
||||
}()
|
||||
if n := countRows(t, s); n != 0 {
|
||||
t.Fatalf("rows after panic = %d, want 0", n)
|
||||
}
|
||||
// The single writer connection must have been released.
|
||||
if err := s.WithTx(context.Background(), insertRow); err != nil {
|
||||
t.Fatalf("WithTx after panic: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user