@@ 4,6 4,8 @@
// depend on it freely.
package core
+import "time"
+
// Visibility mirrors the Postgres `visibility` enum.
type Visibility string
@@ 71,6 73,12 @@ type Repo struct {
OwnerName string
Path string
Visibility Visibility
+ // Created and Updated are the row's timestamps, in UTC. They are read by
+ // the surfaces that publish a database as a record rather than as a page —
+ // /query is the first — and are the zero time for a Repo built in memory
+ // rather than read from the store.
+ Created time.Time
+ Updated time.Time
}
// Caller is the authenticated principal for a request. A nil *Caller is an
@@ 17,7 17,7 @@ import (
// nullable in the schema, so it is coalesced to the empty string.
const repoSelect = `
SELECT r.id, r.name, COALESCE(r.description, ''), r.owner_id,
- COALESCE(u.username, ''), r.path, r.visibility
+ COALESCE(u.username, ''), r.path, r.visibility, r.created, r.updated
FROM repository r
JOIN "user" u ON u.id = r.owner_id`
@@ 27,7 27,7 @@ func scanRepo(sc rowScanner) (*core.Repo, error) {
visibility string
)
if err := sc.Scan(&r.ID, &r.Name, &r.Description, &r.OwnerID,
- &r.OwnerName, &r.Path, &visibility); err != nil {
+ &r.OwnerName, &r.Path, &visibility, &r.Created, &r.Updated); err != nil {
return nil, err
}
r.Visibility = core.Visibility(visibility)
@@ 75,6 75,7 @@ RETURNING id`
}
out := *r
out.ID = id
+ out.Created, out.Updated = now, now
return &out, nil
}
@@ 4,6 4,7 @@ import (
"context"
"errors"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ 72,6 73,52 @@ func TestCreateRepoDuplicateName(t *testing.T) {
}
}
+// The row's timestamps are read, not merely stored: /query publishes them, and
+// a projection that forgot the two columns would answer the zero time for every
+// database on the instance without failing anywhere.
+func TestRepoCarriesItsTimestamps(t *testing.T) {
+ s, sqlDB, cleanup := newTestStore(t)
+ defer cleanup()
+ ctx := context.Background()
+
+ before := time.Now().UTC().Add(-time.Second)
+ insertUser(t, sqlDB, 1, "alice", core.UserTypeUser)
+ created := mkRepo(t, s, ctx, 1, "alice", "widgets", core.VisibilityPublic)
+
+ assert.False(t, created.Created.IsZero(), "CreateRepo must return the row it wrote")
+ assert.Equal(t, created.Created, created.Updated, "a fresh row was never updated")
+
+ for _, tc := range []struct {
+ name string
+ get func() (*core.Repo, error)
+ }{
+ {"by id", func() (*core.Repo, error) { return s.GetRepoByID(ctx, created.ID) }},
+ {"by owner and name", func() (*core.Repo, error) { return s.GetRepoByOwnerAndName(ctx, "alice", "widgets") }},
+ {"through a listing", func() (*core.Repo, error) {
+ repos, err := s.ListReposByOwner(ctx, "alice", nil)
+ if err != nil {
+ return nil, err
+ }
+ return repos[0], nil
+ }},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := tc.get()
+ require.NoError(t, err)
+ assert.False(t, got.Created.IsZero(), "created came back as the zero time")
+ assert.True(t, got.Created.After(before), "created is not the row's own timestamp")
+ assert.False(t, got.Updated.IsZero())
+ })
+ }
+
+ // An update moves Updated and leaves Created where it was.
+ require.NoError(t, s.UpdateRepo(ctx, created.ID, "now with docs", core.VisibilityPrivate))
+ got, err := s.GetRepoByID(ctx, created.ID)
+ require.NoError(t, err)
+ assert.Equal(t, created.Created.Unix(), got.Created.Unix(), "an update rewrote created")
+ assert.False(t, got.Updated.Before(got.Created), "updated went backwards")
+}
+
// A rename moves the name and the on-disk path in one statement: path is
// derived from (owner, name), and a row whose two halves disagree would be
// served out of the wrong store.