Working in PG datastore.
This commit is contained in:
parent
76d5264474
commit
1e283e7cd6
8 changed files with 210 additions and 1 deletions
24
datastore/datastore.go
Normal file
24
datastore/datastore.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmoiron/modl"
|
||||||
|
"github.com/thermokarst/bactdb/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A datastore access point (in PostgreSQL)
|
||||||
|
type Datastore struct {
|
||||||
|
Users models.UsersService
|
||||||
|
dbh modl.SqlExecutor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatastore creates a new client for accessing the datastore (in PostgreSQL).
|
||||||
|
// If dbh is nil, it uses the global DB handle.
|
||||||
|
func NewDatastore(dbh modl.SqlExecutor) *Datastore {
|
||||||
|
if dbh == nil {
|
||||||
|
dbh = DBH
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &Datastore{dbh: dbh}
|
||||||
|
d.Users = &usersStore{d}
|
||||||
|
return d
|
||||||
|
}
|
9
datastore/datastore_test.go
Normal file
9
datastore/datastore_test.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func normalizeTime(t ...*time.Time) {
|
||||||
|
for _, v := range t {
|
||||||
|
*v = v.In(time.UTC)
|
||||||
|
}
|
||||||
|
}
|
53
datastore/db.go
Normal file
53
datastore/db.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jmoiron/modl"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is the global database
|
||||||
|
var DB = &modl.DbMap{Dialect: modl.PostgresDialect{}}
|
||||||
|
|
||||||
|
// DBH is a modl.SqlExecutor interface to DB, the global database. It is better to
|
||||||
|
// use DBH instead of DB because it prevents you from calling methods that could
|
||||||
|
// not later be wrapped in a transaction.
|
||||||
|
var DBH modl.SqlExecutor = DB
|
||||||
|
|
||||||
|
var connectOnce sync.Once
|
||||||
|
|
||||||
|
// Connect connects to the PostgreSQL database specified by the PG* environment
|
||||||
|
// variables. It calls log.Fatal if it encounters an error.
|
||||||
|
func Connect() {
|
||||||
|
connectOnce.Do(func() {
|
||||||
|
var err error
|
||||||
|
DB.Dbx, err = sqlx.Open("postgres", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error connecting to PostgreSQL database (using PG* environment variables): ", err)
|
||||||
|
}
|
||||||
|
DB.Db = DB.Dbx.DB
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var createSQL []string
|
||||||
|
|
||||||
|
// Create the database schema. It calls log.Fatal if it encounters an error.
|
||||||
|
func Create() {
|
||||||
|
if err := DB.CreateTablesIfNotExists(); err != nil {
|
||||||
|
log.Fatal("Error creating tables: ", err)
|
||||||
|
}
|
||||||
|
for _, query := range createSQL {
|
||||||
|
if _, err := DB.Exec(query); err != nil {
|
||||||
|
log.Fatalf("Error running query %q: %s", query, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the database schema
|
||||||
|
func Drop() {
|
||||||
|
// TODO(mrd): raise errors.
|
||||||
|
DB.DropTables()
|
||||||
|
}
|
26
datastore/db_test.go
Normal file
26
datastore/db_test.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Make sure we don't run the tests on the main DB (will destroy the data)
|
||||||
|
dbname := os.Getenv("PGDATABASE")
|
||||||
|
if dbname == "" {
|
||||||
|
dbname = "bactdbtest"
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(dbname, "test") {
|
||||||
|
dbname += "test"
|
||||||
|
}
|
||||||
|
if err := os.Setenv("PGDATABASE", dbname); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset DB
|
||||||
|
Connect()
|
||||||
|
Drop()
|
||||||
|
Create()
|
||||||
|
}
|
34
datastore/users.go
Normal file
34
datastore/users.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import "github.com/thermokarst/bactdb/models"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DB.AddTableWithName(models.User{}, "users").SetKeys(false, "Id")
|
||||||
|
createSQL = append(createSQL,
|
||||||
|
`CREATE UNIQUE INDEX username_idx ON users (username);`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type usersStore struct {
|
||||||
|
*Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *usersStore) Get(id int64) (*models.User, error) {
|
||||||
|
var users []*models.User
|
||||||
|
if err := s.dbh.Select(&users, `SELECT * FROM users WHERE id=$1;`, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, models.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return users[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *usersStore) List(opt *models.UserListOptions) ([]*models.User, error) {
|
||||||
|
var users []*models.User
|
||||||
|
err := s.dbh.Select(&users, `SELECT * FROM users LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
58
datastore/users_test.go
Normal file
58
datastore/users_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/thermokarst/bactdb/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsersStore_Get_db(t *testing.T) {
|
||||||
|
want := &models.User{Id: 1, UserName: "Test User"}
|
||||||
|
|
||||||
|
tx, _ := DB.Begin()
|
||||||
|
defer tx.Rollback()
|
||||||
|
// Test on a clean database
|
||||||
|
tx.Exec(`DELETE FROM users;`)
|
||||||
|
if err := tx.Insert(want); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := NewDatastore(tx)
|
||||||
|
user, err := d.Users.Get(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
|
||||||
|
if !reflect.DeepEqual(user, want) {
|
||||||
|
t.Errorf("got user %+v, want %+v", user, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsersStore_List_db(t *testing.T) {
|
||||||
|
want := []*models.User{{Id: 1, UserName: "Test User"}}
|
||||||
|
|
||||||
|
// tx := DBH
|
||||||
|
tx, _ := DB.Begin()
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Test on a clean database
|
||||||
|
tx.Exec(`DELETE FROM users;`)
|
||||||
|
if err := tx.Insert(want[0]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := NewDatastore(tx)
|
||||||
|
users, err := d.Users.List(&models.UserListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range want {
|
||||||
|
normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(users, want) {
|
||||||
|
t.Errorf("got users %+v, want %+v", users, want)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,6 +26,10 @@ type UsersService interface {
|
||||||
List(opt *UserListOptions) ([]*User, error)
|
List(opt *UserListOptions) ([]*User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
)
|
||||||
|
|
||||||
type usersService struct {
|
type usersService struct {
|
||||||
client *Client
|
client *Client
|
||||||
}
|
}
|
||||||
|
|
2
test.sh
2
test.sh
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
go test ./...
|
PGTZ=UTC PGSSLMODE=disable go test ./...
|
||||||
|
|
||||||
|
|
Reference in a new issue