internal/postgres: add functionality to get and insert version rows

GetVersion and InsertVersion are implemented. GetVersion takes
a primary key, the name and version, and returns the corresponding
Version if it exists in the Versions table. InsertVersion takes
a Version and inserts it into the Versions table if there are no
pre-existing Versions with the same primary key, failing otherwise.

GetVersion particularly will be used to get the information needed
to write the html header for the discovery site.

Note that the updated_at field in the Versions schema should use a trigger
to automatically update but that will be handled in a future CL.

Fixes b/124338357

Change-Id: If6ecc43a35381814f74df024581a135f16c34771
Reviewed-on: https://team-review.git.corp.google.com/c/417750
Reviewed-by: Katie Hockman <katiehockman@google.com>
This commit is contained in:
Channing Kimble-Brown 2019-02-15 11:32:26 -05:00 коммит произвёл Julie Qiu
Родитель 42a10443c1
Коммит ef655c1f99
5 изменённых файлов: 307 добавлений и 180 удалений

Просмотреть файл

@ -9,23 +9,27 @@ import "time"
// A Series is a group of modules that share the same base path and are assumed
// to be major-version variants.
type Series struct {
Name string
Modules []*Module
Name string
CreatedAt time.Time
Modules []*Module
}
// A Module is a collection of packages that share a common path prefix (the
// module path) and are versioned as a single unit, along with a go.mod file
// listing other required modules.
type Module struct {
Name string
Series *Series
Versions []*Version
Name string
CreatedAt time.Time
Series *Series
Versions []*Version
}
// A Version is a specific, reproducible build of a module.
type Version struct {
Module *Module
Version string
CreatedAt time.Time
UpdatedAt time.Time
Synopsis string
CommitTime time.Time
License *License

Просмотреть файл

@ -6,6 +6,10 @@ package postgres
import (
"database/sql"
"errors"
"time"
"golang.org/x/discovery/internal"
)
type DB struct {
@ -43,3 +47,128 @@ func (db *DB) Transact(txFunc func(*sql.Tx) error) (err error) {
return txFunc(tx)
}
// LatestProxyIndexUpdate reports the last time the Proxy Index Cron
// successfully fetched data from the Module Proxy Index.
func (db *DB) LatestProxyIndexUpdate() (time.Time, error) {
query := `
SELECT created_at
FROM version_logs
WHERE source=$1
ORDER BY created_at DESC
LIMIT 1`
var createdAt time.Time
row := db.QueryRow(query, internal.VersionLogProxyIndex)
switch err := row.Scan(&createdAt); err {
case sql.ErrNoRows:
return time.Time{}, nil
case nil:
return createdAt, nil
default:
return time.Time{}, err
}
}
// InsertVersionLogs inserts a VersionLog into the database and
// insertion fails and returns an error if the VersionLog's primary
// key already exists in the database.
func (db *DB) InsertVersionLogs(logs []*internal.VersionLog) error {
return db.Transact(func(tx *sql.Tx) error {
for _, l := range logs {
if _, err := tx.Exec(
`INSERT INTO version_logs(name, version, created_at, source, error)
VALUES ($1, $2, $3, $4, $5);`,
l.Name, l.Version, l.CreatedAt, l.Source, l.Error,
); err != nil {
return err
}
}
return nil
})
}
// GetVersion fetches a Version from the database with the primary key
// (name, version).
func (db *DB) GetVersion(name string, version string) (*internal.Version, error) {
var synopsis string
var commitTime, createdAt, updatedAt time.Time
var license string
query := `
SELECT
created_at,
updated_at,
synopsis,
commit_time,
license
FROM versions
WHERE name = $1 and version = $2;`
row := db.QueryRow(query, name, version)
if err := row.Scan(&createdAt, &updatedAt, &synopsis, &commitTime, &license); err != nil {
return nil, err
}
return &internal.Version{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Module: &internal.Module{
Name: name,
},
Version: version,
Synopsis: synopsis,
CommitTime: commitTime,
License: &internal.License{
Type: license,
},
}, nil
}
// InsertVersion inserts a Version into the database along with any
// necessary series and modules. Insertion fails and returns an error
// if the Version's primary key already exists in the database.
// Inserting a Version connected to a series or module that already
// exists in the database will not update the existing series or
// module.
func (db *DB) InsertVersion(version *internal.Version) error {
if version == nil {
return errors.New("postgres: cannot insert nil version")
}
return db.Transact(func(tx *sql.Tx) error {
if _, err := tx.Exec(
`INSERT INTO series(name)
VALUES($1)
ON CONFLICT DO NOTHING`,
version.Module.Series.Name); err != nil {
return err
}
if _, err := tx.Exec(
`INSERT INTO modules(name, series_name)
VALUES($1,$2)
ON CONFLICT DO NOTHING`,
version.Module.Name, version.Module.Series.Name); err != nil {
return err
}
// TODO(ckimblebrown, julieqiu): Update Versions schema and insert readmes,
// licenses, dependencies, and packages (the rest of the fields in the
// internal.Version struct)
if _, err := tx.Exec(
`INSERT INTO versions(name, version, synopsis, commit_time, license, deleted)
VALUES($1,$2,$3,$4,$5,$6)`,
version.Module.Name,
version.Version,
version.Synopsis,
version.CommitTime,
version.License.Type,
false,
); err != nil {
return err
}
return nil
})
}

Просмотреть файл

@ -0,0 +1,169 @@
package postgres
import (
"fmt"
"os"
"reflect"
"testing"
"time"
_ "github.com/lib/pq"
"golang.org/x/discovery/internal"
)
var (
user = getEnv("GO_DISCOVERY_DATABASE_TEST_USER", "postgres")
password = getEnv("GO_DISCOVERY_DATABASE_TEST_PASSWORD", "")
host = getEnv("GO_DISCOVERY_DATABASE_TEST_HOST", "localhost")
testdbname = getEnv("GO_DISCOVERY_DATABASE_TEST_NAME", "discovery-database-test")
testdb = fmt.Sprintf("user=%s host=%s dbname=%s sslmode=disable", user, host, testdbname)
)
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func setupCleanDB(t *testing.T) (func(t *testing.T), *DB) {
t.Helper()
db, err := Open(testdb)
if err != nil {
t.Fatalf("Open(%q), error: %v", testdb, err)
}
cleanup := func(t *testing.T) {
db.Exec(`TRUNCATE version_logs;`) // truncates version_logs
db.Exec(`TRUNCATE versions CASCADE;`) // truncates versions and any tables that use versions as a foreign key.
}
return cleanup, db
}
func TestPostgres_ReadAndWriteVersion(t *testing.T) {
var series = &internal.Series{
Name: "myseries",
Modules: []*internal.Module{},
}
var module = &internal.Module{
Name: "valid_module_name",
Series: series,
Versions: []*internal.Version{},
}
var testVersion = &internal.Version{
Module: module,
Version: "v1.0.0",
Synopsis: "This is a synopsis",
License: &internal.License{},
ReadMe: &internal.ReadMe{},
CommitTime: time.Now(),
Packages: []*internal.Package{},
Dependencies: []*internal.Version{},
Dependents: []*internal.Version{},
}
testCases := []struct {
name, moduleName, version string
versionData *internal.Version
wantReadErr, wantWriteErr bool
}{
{
name: "nil_version_write_error",
moduleName: "valid_module_name",
version: "v1.0.0",
wantReadErr: true,
wantWriteErr: true,
},
{
name: "valid_test",
moduleName: "valid_module_name",
version: "v1.0.0",
versionData: testVersion,
},
{
name: "nonexistent_version_test",
moduleName: "valid_module_name",
version: "v1.2.3",
versionData: testVersion,
wantReadErr: true,
},
{
name: "nonexistent_module_test",
moduleName: "nonexistent_module_name",
version: "v1.0.0",
versionData: testVersion,
wantReadErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
teardownTestCase, db := setupCleanDB(t)
defer teardownTestCase(t)
if err := db.InsertVersion(tc.versionData); tc.wantWriteErr != (err != nil) {
t.Errorf("db.InsertVersion(%+v) error: %v, want write error: %t", tc.versionData, err, tc.wantWriteErr)
}
// Test that insertion of duplicate primary key fails when the first insert worked
if err := db.InsertVersion(tc.versionData); err == nil {
t.Errorf("db.InsertVersion(%+v) on duplicate version did not produce error", testVersion)
}
got, err := db.GetVersion(tc.moduleName, tc.version)
if tc.wantReadErr != (err != nil) {
t.Fatalf("db.GetVersion(%q, %q) error: %v, want read error: %t", tc.moduleName, tc.version, err, tc.wantReadErr)
}
if !tc.wantReadErr && got == nil {
t.Fatalf("db.GetVersion(%q, %q) = %v, want %v",
tc.moduleName, tc.version, got, tc.versionData)
}
if !tc.wantReadErr && reflect.DeepEqual(*got, *tc.versionData) {
t.Errorf("db.GetVersion(%q, %q) = %v, want %v",
tc.moduleName, tc.version, got, tc.versionData)
}
})
}
}
func TestPostgress_InsertVersionLogs(t *testing.T) {
teardownTestCase, db := setupCleanDB(t)
defer teardownTestCase(t)
now := time.Now().UTC()
newVersions := []*internal.VersionLog{
&internal.VersionLog{
Name: "testModule",
Version: "v.1.0.0",
CreatedAt: now.Add(-10 * time.Minute),
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule",
Version: "v.1.1.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule/v2",
Version: "v.2.0.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
}
if err := db.InsertVersionLogs(newVersions); err != nil {
t.Errorf("db.InsertVersionLogs(newVersions) error: %v", err)
}
dbTime, err := db.LatestProxyIndexUpdate()
if err != nil {
t.Errorf("db.LatestProxyIndexUpdate error: %v", err)
}
if !dbTime.Equal(now) {
t.Errorf("db.LatestProxyIndexUpdate() = %v, want %v", dbTime, now)
}
}

Просмотреть файл

@ -1,49 +0,0 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package postgres
import (
"database/sql"
"time"
"golang.org/x/discovery/internal"
)
// LatestProxyIndexUpdate reports the last time the Proxy Index Cron
// successfully fetched data from the Module Proxy Index.
func (db *DB) LatestProxyIndexUpdate() (time.Time, error) {
query := `
SELECT created_at
FROM version_logs
WHERE source=$1
ORDER BY created_at DESC
LIMIT 1`
var createdAt time.Time
row := db.QueryRow(query, internal.VersionLogProxyIndex)
switch err := row.Scan(&createdAt); err {
case sql.ErrNoRows:
return time.Time{}, nil
case nil:
return createdAt, nil
default:
return time.Time{}, err
}
}
func (db *DB) InsertVersionLogs(logs []*internal.VersionLog) error {
return db.Transact(func(tx *sql.Tx) error {
for _, l := range logs {
if _, err := tx.Exec(
`INSERT INTO version_logs(name, version, created_at, source, error)
VALUES ($1, $2, $3, $4, $5);`,
l.Name, l.Version, l.CreatedAt, l.Source, l.Error,
); err != nil {
return err
}
}
return nil
})
}

Просмотреть файл

@ -1,126 +0,0 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package postgres
import (
"fmt"
"os"
"testing"
"time"
"golang.org/x/discovery/internal"
_ "github.com/lib/pq"
)
var (
user = getEnv("GO_DISCOVERY_DATABASE_TEST_USER", "postgres")
password = getEnv("GO_DISCOVERY_DATABASE_TEST_PASSWORD", "")
host = getEnv("GO_DISCOVERY_DATABASE_TEST_HOST", "localhost")
dbname = getEnv("GO_DISCOVERY_DATABASE_TEST_NAME", "discovery-database")
testdb = fmt.Sprintf("user=%s host=%s dbname=%s sslmode=disable", user, host, testdbname)
)
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func setupTestCase(t *testing.T) (func(t *testing.T), *DB) {
db, err := Open(testdb)
if err != nil {
t.Fatalf("Open(testdb) error: %v", err)
}
fn := func(t *testing.T) {
db.Exec(`TRUNCATE version_logs;`) // truncates the version_logs table
}
return fn, db
}
func TestLatestProxyIndexUpdateReturnsNilWithNoRows(t *testing.T) {
teardownTestCase, db := setupTestCase(t)
defer teardownTestCase(t)
dbTime, err := db.LatestProxyIndexUpdate()
if err != nil {
t.Errorf("db.LatestProxyIndexUpdate error: %v", err)
}
if !dbTime.IsZero() {
t.Errorf("db.LatestProxyIndexUpdate() = %v, want %v", dbTime, time.Time{})
}
}
func TestLatestProxyIndexUpdateReturnsLatestTimestamp(t *testing.T) {
teardownTestCase, db := setupTestCase(t)
defer teardownTestCase(t)
now := time.Now().UTC()
newVersions := []*internal.VersionLog{
&internal.VersionLog{
Name: "testModule",
Version: "v.1.0.0",
CreatedAt: now.Add(-10 * time.Minute),
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule",
Version: "v.1.1.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule/v2",
Version: "v.2.0.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
}
if err := db.InsertVersionLogs(newVersions); err != nil {
t.Errorf("db.InsertVersionLogs(newVersions) error: %v", err)
}
dbTime, err := db.LatestProxyIndexUpdate()
if err != nil {
t.Errorf("db.LatestProxyIndexUpdate error: %v", err)
}
if !dbTime.Equal(now) {
t.Errorf("db.LatestProxyIndexUpdate() = %v, want %v", dbTime, now)
}
}
func TestInsertVersionLogs(t *testing.T) {
teardownTestCase, db := setupTestCase(t)
defer teardownTestCase(t)
now := time.Now().UTC()
newVersions := []*internal.VersionLog{
&internal.VersionLog{
Name: "testModule",
Version: "v.1.0.0",
CreatedAt: now.Add(-10 * time.Minute),
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule",
Version: "v.1.1.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
&internal.VersionLog{
Name: "testModule/v2",
Version: "v.2.0.0",
CreatedAt: now,
Source: internal.VersionLogProxyIndex,
},
}
if err := db.InsertVersionLogs(newVersions); err != nil {
t.Errorf("db.InsertVersionLogs(newVersions) error: %v", err)
}
}