carlos / platform Public

db --at: an explicit year-1 time is before the history, not the newest copy (#262)

Astra's review (P2): --at 0001-01-01T00:00:00Z parsed to Go's zero time,
which RestoreAsOf and litestream read as 'no cutoff', so the newest copy
came back for a time before everything. parseDBAt and RestoreAsOf now move
any time before the Unix epoch (or that rounds down to zero) to the epoch,
which gets the before-history answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Human

Prompt

Yes, go ahead.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

Clamps pre-epoch --at times to the epoch in both layers.
claude agent, operated by paul@keymail.dev 26c65c10f4cc994f27411295f9a590f7c306879e parent af256c3
4 files changed, +44 −0
  • db.go +6 −0
  • db_at_test.go +7 −0
  • internal/dbdoor/restore.go +11 −0
  • internal/dbdoor/restore_asof_test.go +20 −0

CI passed — run details

diff --git a/db.go b/db.go
index 12c4bb267..596afd821 100644
--- a/db.go
+++ b/db.go
@@ -131,6 +131,12 @@ func parseDBAt(raw string, now time.Time) (time.Time, error) {
if at.After(now) {
return time.Time{}, fmt.Errorf("--at %s is in the future. Leave --at out to read the newest copy", raw)
}
+ // 0001-01-01T00:00:00Z parses to Go's zero time, which means "no
+ // --at". It is before any history, so it is asked as the earliest
+ // time instead, and gets the before-history answer.
+ if at.Before(dbdoor.EarliestAsOf) {
+ at = dbdoor.EarliestAsOf
+ }
return at, nil
}
diff --git a/db_at_test.go b/db_at_test.go
index ecb0acd59..78e64ee43 100644
--- a/db_at_test.go
+++ b/db_at_test.go
@@ -18,6 +18,13 @@ func TestParseDBAt(t *testing.T) {
t.Errorf("parseDBAt(%q) = %v, want before %v", ok, got, now)
}
}
+ // An explicit time that is Go's zero time must not read as "no --at"
+ // (found in Astra's review).
+ for _, early := range []string{"0001-01-01T00:00:00Z", "0001-01-01T01:00:00+01:00"} {
+ if got, err := parseDBAt(early, now); err != nil || got.IsZero() {
+ t.Errorf("parseDBAt(%q) = %v, %v, want a non-zero time before any history", early, got, err)
+ }
+ }
if got, _ := parseDBAt("", now); !got.IsZero() {
t.Errorf("parseDBAt(\"\") = %v, want zero (the newest copy)", got)
}
diff --git a/internal/dbdoor/restore.go b/internal/dbdoor/restore.go
index 9f5690d5e..2c78ffb37 100644
--- a/internal/dbdoor/restore.go
+++ b/internal/dbdoor/restore.go
@@ -36,6 +36,11 @@ var (
ErrNoTimes = errors.New("dbdoor: the replica's listing carries no times, so it cannot be read as of a time")
)
+// EarliestAsOf is the earliest time RestoreAsOf asks litestream for. Any
+// earlier request is moved to it: it is before every replica's history,
+// and unlike Go's zero time it cannot mean "the newest copy".
+var EarliestAsOf = time.Unix(0, 0).UTC()
+
// BeforeHistoryError is ErrBeforeHistory with the oldest point the replica
// keeps: the earliest time an as-of restore can ask for, the first whole
// second after the oldest file a restore could start from was listed.
@@ -97,6 +102,12 @@ func RestoreTo(ctx context.Context, c *Client, parent string) (*Restored, error)
// why). So the copy can be older than at, never newer. Restored.At
// says what it actually holds.
func RestoreAsOf(ctx context.Context, c *Client, parent string, at time.Time) (*Restored, error) {
+ // A requested time that rounds down to Go's zero time would read as
+ // "the newest copy" to litestream. Anything that early is before every
+ // replica's history, so it is asked as the Unix epoch, which still is.
+ if !at.IsZero() && at.Truncate(time.Second).Before(EarliestAsOf) {
+ at = EarliestAsOf
+ }
dir, err := os.MkdirTemp(parent, "carlos-db-")
if err != nil {
return nil, err
diff --git a/internal/dbdoor/restore_asof_test.go b/internal/dbdoor/restore_asof_test.go
index 77a4ecebd..3143a4d1f 100644
--- a/internal/dbdoor/restore_asof_test.go
+++ b/internal/dbdoor/restore_asof_test.go
@@ -186,3 +186,23 @@ func TestRestoreAsOfWithSecondResolutionTimes(t *testing.T) {
t.Fatalf("copy as of %v holds %d rows, want %d: a write after T is in it", at, rows, 2*dbdoortest.Rows)
}
}
+
+// Go's zero time means "the newest copy" to RestoreAsOf and to litestream.
+// A requested time at or near it (year 1, or a fraction of a second into
+// it that rounds down to zero) must still be a time before the history,
+// never the newest copy. Found in Astra's review. The zero instant itself,
+// in any zone, is "the newest copy" by contract; parseDBAt keeps a typed
+// one from reaching here.
+func TestRestoreAsOfNeverTreatsAnEarlyTimeAsTheNewest(t *testing.T) {
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 2)
+ for _, at := range []time.Time{
+ time.Time{}.Add(500 * time.Millisecond),
+ time.Time{}.Add(time.Hour),
+ } {
+ _, err := RestoreAsOf(context.Background(), NewClient(newTestDoor(t, dir).do), t.TempDir(), at)
+ if !errors.Is(err, ErrBeforeHistory) {
+ t.Errorf("RestoreAsOf(%v) = %v, want ErrBeforeHistory", at, err)
+ }
+ }
+}