Skip to content

Commit

Permalink
refactor: replace GORM with pgx + go-migrate + squirrel
Browse files Browse the repository at this point in the history
  • Loading branch information
mgjules committed Jun 9, 2022
1 parent 8f87289 commit ae2e03d
Show file tree
Hide file tree
Showing 10 changed files with 1,635 additions and 45 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# Dependency directories (remove the comment below to include it)
# vendor/

*.http

# Kreya
*.krproj
transport/DeckService
Expand Down
2 changes: 1 addition & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ var serve = &cli.Command{

storageURI := c.String("storage-uri")

repository, err := repo.NewRepository(storageURI, log)
repository, err := repo.NewRepository(c.Context, storageURI, log)
if err != nil {
return fmt.Errorf("new repository: %w", err)
}
Expand Down
13 changes: 9 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ module github.com/mgjules/deckr
go 1.18

require (
github.com/Masterminds/squirrel v1.5.3
github.com/gin-contrib/zap v0.0.2
github.com/gin-gonic/gin v1.7.7
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3
github.com/imdario/mergo v0.3.12
github.com/jackc/pgx/v4 v4.16.1
github.com/json-iterator/go v1.1.12
github.com/lib/pq v1.10.4
github.com/magefile/mage v1.13.0
github.com/onsi/ginkgo/v2 v2.1.4
github.com/onsi/gomega v1.19.0
Expand All @@ -21,7 +23,6 @@ require (
google.golang.org/grpc v1.46.2
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
google.golang.org/protobuf v1.28.0
gorm.io/driver/postgres v1.3.5
gorm.io/gorm v1.23.5
)

Expand All @@ -41,23 +42,27 @@ require (
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/swaggo/swag v1.8.1 // indirect
Expand Down
1,529 changes: 1,523 additions & 6 deletions go.sum

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion repo/inmemory/inmemory.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Repository struct {
}

// NewRepository creates a new in-memory repository.
func NewRepository(log *logger.Logger) *Repository {
func NewRepository(_ context.Context, log *logger.Logger) *Repository {
return &Repository{
log: log,
items: make(map[string]*Deck),
Expand Down
7 changes: 3 additions & 4 deletions repo/postgres/deck.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ package postgres
import (
"fmt"

"github.com/lib/pq"
"github.com/mgjules/deckr/deck"
"gorm.io/gorm"
)

// Deck represents a deck of cards.
type Deck struct {
gorm.Model
ID string `gorm:"primaryKey"`
ID string
Shuffled bool
Composition string `gorm:"type:varchar(64)"`
Codes pq.StringArray `gorm:"type:varchar(3)[]"`
Composition string
Codes []string
}

// DomainDeckToDeck transforms a domain deck to a repo deck.
Expand Down
1 change: 1 addition & 0 deletions repo/postgres/migrations/001_create_decks_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS decks;
8 changes: 8 additions & 0 deletions repo/postgres/migrations/001_create_decks_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE decks (
id VARCHAR(36) PRIMARY KEY,
shuffled BOOLEAN NOT NULL DEFAULT FALSE,
composition VARCHAR(64) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE,
codes VARCHAR(3)[]
);
110 changes: 84 additions & 26 deletions repo/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,84 @@ package postgres

import (
"context"
"database/sql"
"embed"
"errors"
"fmt"

sq "github.com/Masterminds/squirrel"
"github.com/golang-migrate/migrate/v4"
mpgx "github.com/golang-migrate/migrate/v4/database/pgx"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/stdlib"
"github.com/mgjules/deckr/deck"
"github.com/mgjules/deckr/logger"
"github.com/mgjules/deckr/repo/errs"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

//go:embed migrations/*.sql
var migrations embed.FS

const (
table = "decks"
version = 1
)

// Repository is a PostgreSQL implementation of the deckr.Repository interface.
type Repository struct {
log *logger.Logger
db *gorm.DB
pgx *pgx.Conn
db *sql.DB
sb sq.StatementBuilderType
}

// NewRepository creates a new PostgreSQL repository.
func NewRepository(uri string, log *logger.Logger) (*Repository, error) {
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: uri,
}))
func NewRepository(ctx context.Context, uri string, log *logger.Logger) (*Repository, error) {
db, err := sql.Open("pgx", uri)
if err != nil {
return nil, fmt.Errorf("open postgres db: %w", err)
}

if err = db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("ping postgres db: %w", err)
}

conn, err := stdlib.AcquireConn(db)
if err != nil {
return nil, fmt.Errorf("open postgres connection: %w", err)
return nil, fmt.Errorf("acquire pgx connection: %w", err)
}

return &Repository{
log: log,
pgx: conn,
db: db,
sb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}, nil
}

// Get returns the deck with the given id.
func (r *Repository) Get(_ context.Context, id string) (*deck.Deck, error) {
func (r *Repository) Get(ctx context.Context, id string) (*deck.Deck, error) {
saved := Deck{
ID: id,
}

if err := r.db.First(&saved).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("deck '%s': %w", id, errs.ErrDeckNotFound)
sql, args, err := r.sb.Select("id", "shuffled", "composition", "codes").
From(table).
Where(sq.Eq{"id": id}).
Limit(1).
ToSql()
if err != nil {
return nil, fmt.Errorf("build select sql for deck '%s': %w", id, err)
}

if err = r.pgx.QueryRow(ctx, sql, args...).
Scan(&saved.ID, &saved.Shuffled, &saved.Composition, &saved.Codes); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("query row for deck '%s': %w", id, errs.ErrDeckNotFound)
}

return nil, fmt.Errorf("deck '%s': %w", id, err)
return nil, fmt.Errorf("query row for deck '%s': %w", id, err)
}

d, err := DeckToDomainDeck(&saved)
Expand All @@ -58,22 +93,50 @@ func (r *Repository) Get(_ context.Context, id string) (*deck.Deck, error) {
}

// Save saves the given deck.
func (r *Repository) Save(_ context.Context, d *deck.Deck) error {
func (r *Repository) Save(ctx context.Context, d *deck.Deck) error {
save := DomainDeckToDeck(d)

if err := r.db.Save(save).Error; err != nil {
return fmt.Errorf("save deck '%s': %w", save.ID, err)
sql, args, err := r.sb.Insert(table).
Columns("id", "shuffled", "composition", "codes").
Values(save.ID, save.Shuffled, save.Composition, save.Codes).
Suffix(`ON CONFLICT (id) DO
UPDATE SET
shuffled = EXCLUDED.shuffled,
codes = EXCLUDED.codes,
updated_at = NOW()`).
ToSql()
if err != nil {
return fmt.Errorf("build upsert sql for deck '%s': %w", d.ID(), err)
}

if _, err := r.pgx.Exec(ctx, sql, args...); err != nil {
return fmt.Errorf("save deck '%s': %w", d.ID(), err)
}

r.log.Debugf("saved deck '%s'", save.ID)

return nil
}

// Migrate migratess the deck model.
// Migrate migrates the deck model.
func (r *Repository) Migrate(_ context.Context) error {
if err := r.db.AutoMigrate(&Deck{}); err != nil {
return fmt.Errorf("migrate deck model: %w", err)
d, err := iofs.New(migrations, "migrations")
if err != nil {
return fmt.Errorf("iofs: %w", err)
}

driver, err := mpgx.WithInstance(r.db, &mpgx.Config{})
if err != nil {
return fmt.Errorf("retrieve database.Driver instance: %w", err)
}

m, err := migrate.NewWithInstance("iofs", d, "postgres", driver)
if err != nil {
return fmt.Errorf("new migrate instance: %w", err)
}

if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("migrate up: %w", err)
}

r.log.Debug("migrated deck model")
Expand All @@ -82,11 +145,6 @@ func (r *Repository) Migrate(_ context.Context) error {
}

// Close closes any external connection in the repository.
func (r *Repository) Close(_ context.Context) error {
db, err := r.db.DB()
if err != nil {
return fmt.Errorf("get db instance: %w", err)
}

return db.Close()
func (r *Repository) Close(ctx context.Context) error {
return r.pgx.Close(ctx)
}
6 changes: 3 additions & 3 deletions repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ type Repository interface {
}

// NewRepository returns a new repository.
func NewRepository(uri string, log *logger.Logger) (Repository, error) {
func NewRepository(ctx context.Context, uri string, log *logger.Logger) (Repository, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("invalid repository URI: %w", err)
}

switch u.Scheme {
case "inmemory":
return inmemory.NewRepository(log), nil
return inmemory.NewRepository(ctx, log), nil
case "postgresql":
repo, err := postgres.NewRepository(uri, log)
repo, err := postgres.NewRepository(ctx, uri, log)
if err != nil {
return nil, fmt.Errorf("new postgres repository: %w", err)
}
Expand Down

0 comments on commit ae2e03d

Please sign in to comment.