~bigbes/sr-ht-dolt

65be341fc575daaf7f1093b698b40193bf75ab93 — Eugene Blikh 3 days ago cd0b0c0
db: read a database's timestamps out of the store

The repository table has carried created and updated since the first
migration and the projection never selected them, so core.Repo could not
answer when a database was made. Nothing needed it while the surfaces were
pages: a listing sorts by created in SQL and prints none of it.

/query publishes a database as a record rather than as a page, and a record
without its timestamps is a poorer answer for no reason — the columns are
already there. CreateRepo returns the pair it wrote, so a caller need not
re-read the row to learn them.

The test reads them back through all three projections. A column dropped
from repoSelect would otherwise surface as the zero time for every database
on the instance, without failing anywhere.
3 files changed, 58 insertions(+), 2 deletions(-)

M core/models.go
M db/repos.go
M db/repos_test.go
M core/models.go => core/models.go +8 -0
@@ 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

M db/repos.go => db/repos.go +3 -2
@@ 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
}


M db/repos_test.go => db/repos_test.go +47 -0
@@ 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.