Skip to content

Commit

Permalink
Implement the set unique operation
Browse files Browse the repository at this point in the history
  • Loading branch information
andrew-farries committed Aug 18, 2023
1 parent 9de1261 commit 5d64ac4
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 6 deletions.
7 changes: 7 additions & 0 deletions pkg/migrations/op_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
OpNameCreateIndex OpName = "create_index"
OpNameDropIndex OpName = "drop_index"
OpNameRenameColumn OpName = "rename_column"
OpNameSetUnique OpName = "set_unique"
)

func TemporaryName(name string) string {
Expand Down Expand Up @@ -98,6 +99,9 @@ func (v *Operations) UnmarshalJSON(data []byte) error {
case OpNameDropIndex:
item = &OpDropIndex{}

case OpNameSetUnique:
item = &OpSetUnique{}

default:
return fmt.Errorf("unknown migration type: %v", opName)
}
Expand Down Expand Up @@ -154,6 +158,9 @@ func (v Operations) MarshalJSON() ([]byte, error) {
case *OpDropIndex:
opName = OpNameDropIndex

case *OpSetUnique:
opName = OpNameSetUnique

default:
panic(fmt.Errorf("unknown operation for %T", op))
}
Expand Down
65 changes: 65 additions & 0 deletions pkg/migrations/op_set_unique.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package migrations

import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/lib/pq"

"pg-roll/pkg/schema"
)

type OpSetUnique struct {
Name string `json:"name"`
Table string `json:"table"`
Columns []string `json:"columns"`
}

var _ Operation = (*OpSetUnique)(nil)

func (o *OpSetUnique) Start(ctx context.Context, conn *sql.DB, schemaName string, stateSchema string, s *schema.Schema) error {
// create unique index concurrently
_, err := conn.ExecContext(ctx, fmt.Sprintf("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS %s ON %s (%s)",
pq.QuoteIdentifier(o.Name),
pq.QuoteIdentifier(o.Table),
strings.Join(quoteColumnNames(o.Columns), ", ")))
return err
}

func (o *OpSetUnique) Complete(ctx context.Context, conn *sql.DB) error {
// create a unique constraint using the unique index
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE IF EXISTS %s ADD CONSTRAINT %s UNIQUE USING INDEX %s",
pq.QuoteIdentifier(o.Table),
pq.QuoteIdentifier(o.Name),
pq.QuoteIdentifier(o.Name)))

return err
}

func (o *OpSetUnique) Rollback(ctx context.Context, conn *sql.DB) error {
// drop the index concurrently
_, err := conn.ExecContext(ctx, fmt.Sprintf("DROP INDEX CONCURRENTLY IF EXISTS %s", o.Name))

return err
}

func (o *OpSetUnique) Validate(ctx context.Context, s *schema.Schema) error {
if o.Name == "" {
return NameRequiredError{}
}

table := s.GetTable(o.Table)
if table == nil {
return TableDoesNotExistError{Name: o.Table}
}

for _, column := range o.Columns {
if table.GetColumn(column) == nil {
return ColumnDoesNotExistError{Table: o.Table, Name: column}
}
}

return nil
}
10 changes: 4 additions & 6 deletions pkg/migrations/op_set_unique_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestSetColumnsUnique(t *testing.T) {
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpSetUnique{
Name: "reviews_username_product_unique",
Table: "reviews",
Columns: []string{"username", "product"},
},
Expand All @@ -55,8 +56,7 @@ func TestSetColumnsUnique(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB) {
// The unique index has been created on the underlying table.
idxName := migrations.IndexName("reviews", []string{"username", "product"})
IndexMustExist(t, db, "public", "reviews", idxName)
IndexMustExist(t, db, "public", "reviews", "reviews_username_product_unique")

// Inserting values into the old schema that violate uniqueness should fail.
MustInsert(t, db, "public", "01_add_table", "reviews", map[string]string{
Expand All @@ -76,13 +76,11 @@ func TestSetColumnsUnique(t *testing.T) {
},
afterRollback: func(t *testing.T, db *sql.DB) {
// The unique index has been dropped from the the underlying table.
idxName := migrations.IndexName("reviews", []string{"username", "product"})
IndexMustNotExist(t, db, "public", "reviews", idxName)
IndexMustNotExist(t, db, "public", "reviews", "reviews_username_product_unique")
},
afterComplete: func(t *testing.T, db *sql.DB) {
// The unique constraint has been created on the underlying table.
constraintName := migrations.UniqueConstraintName("reviews", []string{"username", "product"})
ConstraintMustExist(t, db, "public", "reviews", constraintName)
ConstraintMustExist(t, db, "public", "reviews", "reviews_username_product_unique")

// Inserting values into the new schema that violate uniqueness should fail.
MustInsert(t, db, "public", "02_set_unique", "reviews", map[string]string{
Expand Down

0 comments on commit 5d64ac4

Please sign in to comment.