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,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func openTypesTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
s := openTestStore(t)
|
||||
if _, err := s.Writer.Exec("CREATE TABLE v (val TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// roundTrip writes in through the writer, then reads it back into out and
|
||||
// returns the stored SQLite type and raw text.
|
||||
func roundTrip(t *testing.T, s *Store, in, out any) (typ, raw string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if _, err := s.Writer.ExecContext(ctx, "DELETE FROM v"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.Writer.ExecContext(ctx, "INSERT INTO v (val) VALUES (?)", in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Reader.QueryRowContext(ctx, "SELECT typeof(val), val, val FROM v").Scan(&typ, &raw, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return typ, raw
|
||||
}
|
||||
|
||||
func TestTimestampRoundTrip(t *testing.T) {
|
||||
s := openTypesTestStore(t)
|
||||
zone := time.FixedZone("EDT", -4*60*60)
|
||||
in := Timestamp{time.Date(2026, 10, 3, 8, 37, 5, 123456789, zone)}
|
||||
|
||||
var out Timestamp
|
||||
typ, raw := roundTrip(t, s, in, &out)
|
||||
if typ != "text" || raw != "2026-10-03T12:37:05.123Z" {
|
||||
t.Fatalf("stored %s %q, want text %q", typ, raw, "2026-10-03T12:37:05.123Z")
|
||||
}
|
||||
if want := in.Truncate(time.Millisecond); !out.Equal(want) {
|
||||
t.Fatalf("read back %v, want %v", out, want)
|
||||
}
|
||||
if out.Location() != time.UTC {
|
||||
t.Fatalf("read back location %v, want UTC", out.Location())
|
||||
}
|
||||
|
||||
now := Now()
|
||||
roundTrip(t, s, now, &out)
|
||||
if out != now {
|
||||
t.Fatalf("Now() round trip = %#v, want %#v", out, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampRejects(t *testing.T) {
|
||||
if _, err := (Timestamp{}).Value(); err == nil {
|
||||
t.Error("zero Timestamp Value succeeded")
|
||||
}
|
||||
for _, src := range []any{nil, int64(0), []byte("2026-10-03T12:37:05.123Z"), "2026-10-03T12:37:05Z", "2026-10-03T08:37:05.123-04:00", "garbage"} {
|
||||
var ts Timestamp
|
||||
if err := ts.Scan(src); err == nil {
|
||||
t.Errorf("Scan(%#v) succeeded", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalDateRoundTrip(t *testing.T) {
|
||||
s := openTypesTestStore(t)
|
||||
for _, str := range []string{"2026-10-03", "2028-02-29", "0001-01-01", "9999-12-31"} {
|
||||
in, err := ParseLocalDate(str)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLocalDate(%q): %v", str, err)
|
||||
}
|
||||
if in.String() != str {
|
||||
t.Fatalf("String() = %q, want %q", in.String(), str)
|
||||
}
|
||||
var out LocalDate
|
||||
typ, raw := roundTrip(t, s, in, &out)
|
||||
if typ != "text" || raw != str {
|
||||
t.Fatalf("stored %s %q, want text %q", typ, raw, str)
|
||||
}
|
||||
if out != in {
|
||||
t.Fatalf("read back %v, want %v", out, in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalDateRejectsInvalid(t *testing.T) {
|
||||
for _, str := range []string{
|
||||
"", "2026-02-30", "2026-02-29", "2026-13-01", "2026-00-10", "2026-10-00",
|
||||
"2026-10-32", "2026-1-03", "2026-10-3", "26-10-03", "2026/10/03",
|
||||
"2026-10-03T00:00:00Z", " 2026-10-03", "20261003", "not-a-date",
|
||||
} {
|
||||
if d, err := ParseLocalDate(str); err == nil {
|
||||
t.Errorf("ParseLocalDate(%q) = %v, want error", str, d)
|
||||
}
|
||||
var d LocalDate
|
||||
if err := d.Scan(str); err == nil {
|
||||
t.Errorf("Scan(%q) succeeded", str)
|
||||
}
|
||||
}
|
||||
if _, err := (LocalDate{}).Value(); err == nil {
|
||||
t.Error("zero LocalDate Value succeeded")
|
||||
}
|
||||
var d LocalDate
|
||||
if err := d.Scan(nil); err == nil {
|
||||
t.Error("Scan(nil) succeeded")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user