Add rename_constraint operation (#293)

This commit is contained in:
Alexis Rico 2024-03-01 12:30:09 +01:00 committed by GitHub
parent def08e2bcc
commit 577870c325
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 233 additions and 0 deletions

View File

@ -31,6 +31,7 @@
* [Drop table](#drop-table)
* [Raw SQL](#raw-sql)
* [Rename table](#rename-table)
* [Rename constraint](#rename-constraint)
* [Set replica identity](#set-replica-identity)
## Concepts
@ -686,6 +687,7 @@ See the [examples](../examples) directory for examples of each kind of operation
* [Drop table](#drop-table)
* [Raw SQL](#raw-sql)
* [Rename table](#rename-table)
* [Rename constraint](#rename-constraint)
* [Set replica identity](#set-replica-identity)
### Add column
@ -1112,6 +1114,28 @@ Example **rename table** migrations:
* [04_rename_table.json](../examples/04_rename_table.json)
### Rename constraint
A rename constraint operation renames a constraint.
**rename constraint** operations have this structure:
```json
{
"rename_constraint": {
"table": "table name",
"from": "old constraint name",
"to": "new constraint name"
}
}
```
Example **rename constraint** migrations:
* [33_rename_constraint.json](../examples/33_rename_constraint.json)
### Set replica identity
A set replica identity operation sets the replica identity for a table.

View File

@ -0,0 +1,12 @@
{
"name": "33_rename_check_constraint",
"operations": [
{
"rename_constraint": {
"table": "people",
"from": "name_length",
"to": "name_length_check"
}
}
]
}

View File

@ -137,6 +137,15 @@ func (e ConstraintDoesNotExistError) Error() string {
return fmt.Sprintf("constraint %q on table %q does not exist", e.Constraint, e.Table)
}
type ConstraintAlreadyExistsError struct {
Table string
Constraint string
}
func (e ConstraintAlreadyExistsError) Error() string {
return fmt.Sprintf("constraint %q on table %q already exists", e.Constraint, e.Table)
}
type NoUpSQLAllowedError struct{}
func (e NoUpSQLAllowedError) Error() string {

View File

@ -20,6 +20,7 @@ const (
OpNameAlterColumn OpName = "alter_column"
OpNameCreateIndex OpName = "create_index"
OpNameDropIndex OpName = "drop_index"
OpNameRenameConstraint OpName = "rename_constraint"
OpNameDropConstraint OpName = "drop_constraint"
OpNameSetReplicaIdentity OpName = "set_replica_identity"
OpRawSQLName OpName = "sql"
@ -95,6 +96,9 @@ func (v *Operations) UnmarshalJSON(data []byte) error {
case OpNameDropColumn:
item = &OpDropColumn{}
case OpNameRenameConstraint:
item = &OpRenameConstraint{}
case OpNameDropConstraint:
item = &OpDropConstraint{}
@ -175,6 +179,9 @@ func OperationName(op Operation) OpName {
case *OpDropColumn:
return OpNameDropColumn
case *OpRenameConstraint:
return OpNameRenameConstraint
case *OpDropConstraint:
return OpNameDropConstraint

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/lib/pq"
"github.com/xataio/pgroll/pkg/schema"
)
var _ Operation = (*OpRenameConstraint)(nil)
func (o *OpRenameConstraint) Start(ctx context.Context, conn *sql.DB, stateSchema string, s *schema.Schema, cbs ...CallbackFn) (*schema.Table, error) {
// no-op
return nil, nil
}
func (o *OpRenameConstraint) Complete(ctx context.Context, conn *sql.DB, s *schema.Schema) error {
// rename the constraint in the underlying table
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME CONSTRAINT %s TO %s",
pq.QuoteIdentifier(o.Table),
pq.QuoteIdentifier(o.From),
pq.QuoteIdentifier(o.To)))
return err
}
func (o *OpRenameConstraint) Rollback(ctx context.Context, conn *sql.DB) error {
// no-op
return nil
}
func (o *OpRenameConstraint) Validate(ctx context.Context, s *schema.Schema) error {
table := s.GetTable(o.Table)
if !table.ConstraintExists(o.From) {
return ConstraintDoesNotExistError{Table: o.Table, Constraint: o.From}
}
if table.ConstraintExists(o.To) {
return ConstraintAlreadyExistsError{Table: o.Table, Constraint: o.To}
}
return nil
}

View File

@ -0,0 +1,102 @@
// SPDX-License-Identifier: Apache-2.0
package migrations_test
import (
"database/sql"
"testing"
"github.com/stretchr/testify/assert"
"github.com/xataio/pgroll/pkg/migrations"
)
func TestRenameConstraint(t *testing.T) {
t.Parallel()
addTableMigration := migrations.Migration{
Name: "01_add_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "users",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: ptr(true),
},
{
Name: "username",
Type: "text",
Nullable: ptr(false),
Check: &migrations.CheckConstraint{Constraint: `LENGTH("username") <= 2048`, Name: "users_text_length_username"},
},
},
},
},
}
ExecuteTests(t, TestCases{{
name: "rename constraint",
migrations: []migrations.Migration{
addTableMigration,
{
Name: "02_rename_constraint",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "users",
Column: "username",
Name: ptr("name"),
},
&migrations.OpRenameConstraint{
Table: "users",
From: "users_text_length_username",
To: "users_text_length_name",
},
},
},
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// The column in the underlying table has not been renamed.
ColumnMustExist(t, db, schema, "users", "username")
// Insertions to the new column name in the new version schema should work.
MustInsert(t, db, schema, "02_rename_constraint", "users", map[string]string{"name": "alice"})
// Insertions to the old column name in the old version schema should work.
MustInsert(t, db, schema, "01_add_table", "users", map[string]string{"username": "bob"})
// The check constraint in the underlying table has not been renamed.
CheckConstraintMustExist(t, db, schema, "users", "users_text_length_username")
// The new check constraint in the underlying table has not been created.
CheckConstraintMustNotExist(t, db, schema, "users", "users_text_length_name")
// Data can be read from the view in the new version schema.
rows := MustSelect(t, db, schema, "02_rename_constraint", "users")
assert.Equal(t, []map[string]any{
{"id": 1, "name": "alice"},
{"id": 2, "name": "bob"},
}, rows)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
// no-op
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The column in the underlying table has been renamed.
ColumnMustExist(t, db, schema, "users", "name")
// The check constraint in the underlying table has been renamed.
CheckConstraintMustExist(t, db, schema, "users", "users_text_length_name")
// The old check constraint in the underlying table has been dropped.
CheckConstraintMustNotExist(t, db, schema, "users", "users_text_length_username")
// Data can be read from the view in the new version schema.
rows := MustSelect(t, db, schema, "02_rename_constraint", "users")
assert.Equal(t, []map[string]any{
{"id": 1, "name": "alice"},
{"id": 2, "name": "bob"},
}, rows)
},
}})
}

View File

@ -189,6 +189,18 @@ type OpRawSQL struct {
Up string `json:"up"`
}
// Rename constraint operation
type OpRenameConstraint struct {
// Name of the constraint
From string `json:"from"`
// Name of the table
Table string `json:"table"`
// New name of the constraint
To string `json:"to"`
}
// Rename table operation
type OpRenameTable struct {
// Old name of the table

View File

@ -348,6 +348,26 @@
],
"type": "object"
},
"OpRenameConstraint": {
"additionalProperties": false,
"description": "Rename constraint operation",
"properties": {
"from": {
"description": "Name of the constraint",
"type": "string"
},
"to": {
"description": "New name of the constraint",
"type": "string"
},
"table": {
"description": "Name of the table",
"type": "string"
}
},
"required": ["from", "to", "table"],
"type": "object"
},
"OpRenameTable": {
"additionalProperties": false,
"description": "Rename table operation",