From 1e283e7cd69e26ad6ff3d157e7eaa347eeabd0dd Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Tue, 30 Sep 2014 15:13:36 -0800 Subject: [PATCH] Working in PG datastore. --- datastore/datastore.go | 24 +++++++++++++++ datastore/datastore_test.go | 9 ++++++ datastore/db.go | 53 +++++++++++++++++++++++++++++++++ datastore/db_test.go | 26 +++++++++++++++++ datastore/users.go | 34 ++++++++++++++++++++++ datastore/users_test.go | 58 +++++++++++++++++++++++++++++++++++++ models/users.go | 5 ++++ test.sh | 2 +- 8 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 datastore/datastore.go create mode 100644 datastore/datastore_test.go create mode 100644 datastore/db.go create mode 100644 datastore/db_test.go create mode 100644 datastore/users.go create mode 100644 datastore/users_test.go diff --git a/datastore/datastore.go b/datastore/datastore.go new file mode 100644 index 0000000..3a8e927 --- /dev/null +++ b/datastore/datastore.go @@ -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 +} diff --git a/datastore/datastore_test.go b/datastore/datastore_test.go new file mode 100644 index 0000000..1e952a3 --- /dev/null +++ b/datastore/datastore_test.go @@ -0,0 +1,9 @@ +package datastore + +import "time" + +func normalizeTime(t ...*time.Time) { + for _, v := range t { + *v = v.In(time.UTC) + } +} diff --git a/datastore/db.go b/datastore/db.go new file mode 100644 index 0000000..89ede16 --- /dev/null +++ b/datastore/db.go @@ -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() +} diff --git a/datastore/db_test.go b/datastore/db_test.go new file mode 100644 index 0000000..c8103e0 --- /dev/null +++ b/datastore/db_test.go @@ -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() +} diff --git a/datastore/users.go b/datastore/users.go new file mode 100644 index 0000000..0d4049a --- /dev/null +++ b/datastore/users.go @@ -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 +} diff --git a/datastore/users_test.go b/datastore/users_test.go new file mode 100644 index 0000000..eaff1e0 --- /dev/null +++ b/datastore/users_test.go @@ -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) + } +} diff --git a/models/users.go b/models/users.go index 8f08338..e2253fa 100644 --- a/models/users.go +++ b/models/users.go @@ -1,6 +1,7 @@ package models import ( + "errors" "strconv" "time" @@ -25,6 +26,10 @@ type UsersService interface { List(opt *UserListOptions) ([]*User, error) } +var ( + ErrUserNotFound = errors.New("user not found") +) + type usersService struct { client *Client } diff --git a/test.sh b/test.sh index 7a8971c..4a770db 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -go test ./... +PGTZ=UTC PGSSLMODE=disable go test ./...