package core import ( "context" "path/filepath" "regexp" "strings" "testing" "time" ) // stampShape is exactly: YYYY-MM-DDTHH:MM:SS.fffZ — three fractional digits and // a literal Z, which is the shape SQLite's strftime('%Y-%m-%dT%H:%M:%fZ','now') // emits. var stampShape = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$`) // (a) Stamp is fixed-width: exactly 3 fractional digits and a trailing Z, even // for a fraction RFC3339Nano would trim. func TestStampFixedWidth(t *testing.T) { tm := time.Date(2026, 10, 11, 12, 0, 0, 500_000_000, time.UTC) got := Stamp(tm) if want := "2026-10-11T12:00:00.500Z"; got != want { t.Fatalf("Stamp = %q, want %q", got, want) } if !strings.HasSuffix(got, "Z") { t.Errorf("Stamp %q does not end in Z", got) } if !stampShape.MatchString(got) { t.Errorf("Stamp %q is not fixed-width with exactly 3 fractional digits", got) } // The premise: RFC3339Nano trims the trailing zeros, which is why it cannot // be used for a lexicographically sortable column. if rfc := tm.Format(time.RFC3339Nano); rfc != "2026-10-11T12:00:00.5Z" { t.Fatalf("RFC3339Nano = %q; test premise (trailing-zero trimming) is wrong", rfc) } } // (b) Two Stamps 1ms apart must compare in the same order as the instants. func TestStampOrdersChronologically(t *testing.T) { a := time.Date(2026, 10, 11, 12, 0, 0, 0, time.UTC) b := a.Add(time.Millisecond) sa, sb := Stamp(a), Stamp(b) if !(sa < sb) { t.Errorf("Stamp %q is not < %q; string order != chronological order", sa, sb) } } // (c) The exact case that was broken: .500s sorts BEFORE .550s as a string. // With RFC3339Nano the opposite happens (".5Z" > ".55Z"). func TestStampBrokenCaseSortsCorrectly(t *testing.T) { a := time.Date(2026, 10, 11, 12, 0, 0, 500_000_000, time.UTC) // .500s b := time.Date(2026, 10, 11, 12, 0, 0, 550_000_000, time.UTC) // .550s sa, sb := Stamp(a), Stamp(b) if !(sa < sb) { t.Errorf("Stamp %q should sort before %q", sa, sb) } // Demonstrate that the old format really was misordered, so this test is // guarding a real regression rather than a tautology. ra, rb := a.Format(time.RFC3339Nano), b.Format(time.RFC3339Nano) if !(ra > rb) { t.Fatalf("premise: RFC3339Nano %q,%q should compare backwards", ra, rb) } } // (d) Stamp's output shape matches SQLite's own default byte-for-byte. func TestStampMatchesSQLiteDefault(t *testing.T) { db, err := OpenDB(filepath.Join(t.TempDir(), "stamp.db")) if err != nil { t.Fatalf("OpenDB: %v", err) } defer db.Close() // A fixed second keeps the comparison deterministic. The format literal is // copied verbatim from the column DEFAULTs in the migrations. var sqliteStamp string if err := db.QueryRowContext(context.Background(), `SELECT strftime('%Y-%m-%dT%H:%M:%fZ','2026-10-11 12:00:00.500')`, ).Scan(&sqliteStamp); err != nil { t.Fatalf("strftime: %v", err) } if !stampShape.MatchString(sqliteStamp) { t.Fatalf("SQLite strftime output %q has an unexpected shape", sqliteStamp) } got := Stamp(time.Date(2026, 10, 11, 12, 0, 0, 500_000_000, time.UTC)) if got != sqliteStamp { t.Fatalf("Stamp = %q, SQLite strftime = %q; they must match byte-for-byte", got, sqliteStamp) } }