Working in PG datastore.

This commit is contained in:
Matthew Dillon 2014-09-30 15:13:36 -08:00
parent 76d5264474
commit 1e283e7cd6
8 changed files with 210 additions and 1 deletions

24
datastore/datastore.go Normal file
View 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
}

View 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
View 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
View 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
View 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
View 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)
}
}

View file

@ -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
} }

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
go test ./... PGTZ=UTC PGSSLMODE=disable go test ./...