Implement 'rename column' migrations (#52)

Add support for **rename column** migrations. A rename column migration
looks like:

```json
{
  "name": "13_rename_column",
  "operations": [
    {
      "rename_column": {
        "table": "employees",
        "from": "role",
        "to": "jobTitle"
      }
    }
  ]
}
```

* On `Start`, the view in the new version schema aliases the renamed
column to its new name. The column in the underlying table is not
renamed.
* `Rollback` is a no-op.
* `Complete` renames the column in the underlying table.
This commit is contained in:
Andrew Farries 2023-08-18 06:49:27 +01:00 committed by GitHub
parent 9d6ad24fb1
commit 6448afe956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 189 additions and 7 deletions

View File

@ -0,0 +1,21 @@
{
"name": "12_create_employees_table",
"operations": [
{
"create_table": {
"name": "employees",
"columns": [
{
"name": "id",
"type": "serial",
"pk": true
},
{
"name": "role",
"type": "varchar(255)"
}
]
}
}
]
}

View File

@ -0,0 +1,12 @@
{
"name": "13_rename_column",
"operations": [
{
"rename_column": {
"table": "employees",
"from": "role",
"to": "jobTitle"
}
}
]
}

View File

@ -11,13 +11,14 @@ import (
type OpName string
const (
OpNameCreateTable OpName = "create_table"
OpNameRenameTable OpName = "rename_table"
OpNameDropTable OpName = "drop_table"
OpNameAddColumn OpName = "add_column"
OpNameDropColumn OpName = "drop_column"
OpNameCreateIndex OpName = "create_index"
OpNameDropIndex OpName = "drop_index"
OpNameCreateTable OpName = "create_table"
OpNameRenameTable OpName = "rename_table"
OpNameDropTable OpName = "drop_table"
OpNameAddColumn OpName = "add_column"
OpNameDropColumn OpName = "drop_column"
OpNameCreateIndex OpName = "create_index"
OpNameDropIndex OpName = "drop_index"
OpNameRenameColumn OpName = "rename_column"
)
func TemporaryName(name string) string {
@ -88,6 +89,9 @@ func (v *Operations) UnmarshalJSON(data []byte) error {
case OpNameDropColumn:
item = &OpDropColumn{}
case OpNameRenameColumn:
item = &OpRenameColumn{}
case OpNameCreateIndex:
item = &OpCreateIndex{}
@ -141,6 +145,9 @@ func (v Operations) MarshalJSON() ([]byte, error) {
case *OpDropColumn:
opName = OpNameDropColumn
case *OpRenameColumn:
opName = OpNameRenameColumn
case *OpCreateIndex:
opName = OpNameCreateIndex

View File

@ -0,0 +1,56 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/lib/pq"
"pg-roll/pkg/schema"
)
type OpRenameColumn struct {
Table string `json:"table"`
From string `json:"from"`
To string `json:"to"`
}
var _ Operation = (*OpRenameColumn)(nil)
func (o *OpRenameColumn) Start(ctx context.Context, conn *sql.DB, schemaName string, stateSchema string, s *schema.Schema) error {
table := s.GetTable(o.Table)
table.RenameColumn(o.From, o.To)
return nil
}
func (o *OpRenameColumn) Complete(ctx context.Context, conn *sql.DB) error {
// rename the column in the underlying table
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s",
pq.QuoteIdentifier(o.Table),
pq.QuoteIdentifier(o.From),
pq.QuoteIdentifier(o.To)))
return err
}
func (o *OpRenameColumn) Rollback(ctx context.Context, conn *sql.DB) error {
// no-op
return nil
}
func (o *OpRenameColumn) Validate(ctx context.Context, s *schema.Schema) error {
table := s.GetTable(o.Table)
if table == nil {
return TableDoesNotExistError{Name: o.Table}
}
if table.GetColumn(o.From) == nil {
return ColumnDoesNotExistError{Table: o.Table, Name: o.From}
}
if table.GetColumn(o.To) != nil {
return ColumnAlreadyExistsError{Table: o.Table, Name: o.From}
}
return nil
}

View File

@ -0,0 +1,81 @@
package migrations_test
import (
"database/sql"
"testing"
"github.com/stretchr/testify/assert"
"pg-roll/pkg/migrations"
)
func TestRenameColumn(t *testing.T) {
t.Parallel()
ExecuteTests(t, TestCases{{
name: "rename column",
migrations: []migrations.Migration{
{
Name: "01_add_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "users",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
PrimaryKey: true,
},
{
Name: "username",
Type: "varchar(255)",
Nullable: false,
},
},
},
},
},
{
Name: "02_rename_column",
Operations: migrations.Operations{
&migrations.OpRenameColumn{
Table: "users",
From: "username",
To: "name",
},
},
},
},
afterStart: func(t *testing.T, db *sql.DB) {
// The column in the underlying table has not been renamed.
ColumnMustExist(t, db, "public", "users", "username")
// Insertions to the new column name in the new version schema should work.
MustInsert(t, db, "public", "02_rename_column", "users", map[string]string{"name": "alice"})
// Insertions to the old column name in the old version schema should work.
MustInsert(t, db, "public", "01_add_table", "users", map[string]string{"username": "bob"})
// Data can be read from the view in the new version schema.
rows := MustSelect(t, db, "public", "02_rename_column", "users")
assert.Equal(t, []map[string]any{
{"id": 1, "name": "alice"},
{"id": 2, "name": "bob"},
}, rows)
},
afterRollback: func(t *testing.T, db *sql.DB) {
// no-op
},
afterComplete: func(t *testing.T, db *sql.DB) {
// The column in the underlying table has been renamed.
ColumnMustExist(t, db, "public", "users", "name")
// Data can be read from the view in the new version schema.
rows := MustSelect(t, db, "public", "02_rename_column", "users")
assert.Equal(t, []map[string]any{
{"id": 1, "name": "alice"},
{"id": 2, "name": "bob"},
}, rows)
},
}})
}

View File

@ -114,3 +114,8 @@ func (t *Table) AddColumn(name string, c Column) {
func (t *Table) RemoveColumn(column string) {
delete(t.Columns, column)
}
func (t *Table) RenameColumn(from, to string) {
t.Columns[to] = t.Columns[from]
delete(t.Columns, from)
}