Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/operations/create_constraint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Required fields: `name`, `table`, `type`, `up`, `down`.
"table": "name of referenced table",
"columns": "[names of referenced columns]",
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
"on_delete_set_columns": ["list of FKs to set", "in on delete operation on SET NULL or SET DEFAULT"],
"on_update": "ON UPDATE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
"match_type": "match type, can be SIMPLE or FULL. Default is SIMPLE"
},
"up": {
"column1": "up SQL expressions for each column covered by the constraint",
Expand Down
2 changes: 2 additions & 0 deletions docs/operations/create_table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Each `column` is defined as:
"table": "name of referenced table",
"column": "name of referenced column",
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, SET DEFAULT, RESTRICT, or NO ACTION. Default is NO ACTION",
"on_update": "ON UPDATE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
"match_type": "match type, can be SIMPLE or FULL. Default is SIMPLE"
}
},
```
Expand Down
18 changes: 8 additions & 10 deletions pkg/migrations/op_add_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,16 +314,14 @@ func (w ColumnSQLWriter) Write(col Column) (string, error) {
}

if col.References != nil {
onDelete := string(ForeignKeyActionNOACTION)
if col.References.OnDelete != "" {
onDelete = strings.ToUpper(string(col.References.OnDelete))
}

sql += fmt.Sprintf(" CONSTRAINT %s REFERENCES %s(%s) ON DELETE %s",
pq.QuoteIdentifier(col.References.Name),
pq.QuoteIdentifier(col.References.Table),
pq.QuoteIdentifier(col.References.Column),
onDelete)
writer := &ConstraintSQLWriter{Name: col.References.Name}
sql += " " + writer.WriteForeignKey(
col.References.Table,
[]string{col.References.Column},
col.References.OnDelete,
col.References.OnUpdate,
nil,
col.References.MatchType)
}
if col.Check != nil {
sql += fmt.Sprintf(" CONSTRAINT %s CHECK (%s)",
Expand Down
18 changes: 16 additions & 2 deletions pkg/migrations/op_add_column_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,14 @@ func TestAddForeignKeyColumn(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// The foreign key constraint exists on the new table.
ValidatedForeignKeyMustExist(t, db, schema, "orders", "fk_users_id", withOnDeleteCascade())
ValidatedForeignKeyMustExistWithReferentialAction(
t,
db,
schema,
"orders",
"fk_users_id",
migrations.ForeignKeyActionCASCADE,
migrations.ForeignKeyActionNOACTION)

// Inserting a row into the referenced table succeeds.
MustInsert(t, db, schema, "01_create_table", "users", map[string]string{
Expand Down Expand Up @@ -669,7 +676,14 @@ func TestAddForeignKeyColumn(t *testing.T) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The foreign key constraint still exists on the new table
ValidatedForeignKeyMustExist(t, db, schema, "orders", "fk_users_id", withOnDeleteCascade())
ValidatedForeignKeyMustExistWithReferentialAction(
t,
db,
schema,
"orders",
"fk_users_id",
migrations.ForeignKeyActionCASCADE,
migrations.ForeignKeyActionNOACTION)

// Inserting a row into the referenced table succeeds.
MustInsert(t, db, schema, "02_add_column", "users", map[string]string{
Expand Down
18 changes: 16 additions & 2 deletions pkg/migrations/op_change_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,27 @@ func TestChangeColumnType(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// A temporary FK constraint has been created on the temporary column
ValidatedForeignKeyMustExist(t, db, schema, "employees", migrations.DuplicationName("fk_employee_department"), withOnDeleteCascade())
ValidatedForeignKeyMustExistWithReferentialAction(
t,
db,
schema,
"employees",
migrations.DuplicationName("fk_employee_department"),
migrations.ForeignKeyActionCASCADE,
migrations.ForeignKeyActionNOACTION)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The foreign key constraint still exists on the column
ValidatedForeignKeyMustExist(t, db, schema, "employees", "fk_employee_department", withOnDeleteCascade())
ValidatedForeignKeyMustExistWithReferentialAction(
t,
db,
schema,
"employees",
"fk_employee_department",
migrations.ForeignKeyActionCASCADE,
migrations.ForeignKeyActionNOACTION)
},
},
{
Expand Down
57 changes: 37 additions & 20 deletions pkg/migrations/op_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,30 @@ func UniqueConstraintMustExist(t *testing.T, db *sql.DB, schema, table, constrai
}
}

func ValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string, opts ...foreignKeyOnDeleteOpt) {
func ValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
t.Helper()
if !foreignKeyExists(t, db, schema, table, constraint, true, opts...) {
if !foreignKeyExists(t, db, schema, table, constraint, true, migrations.ForeignKeyActionNOACTION, migrations.ForeignKeyActionNOACTION) {
t.Fatalf("Expected validated foreign key %q to exist", constraint)
}
}

func NotValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string, opts ...foreignKeyOnDeleteOpt) {
func ValidatedForeignKeyMustExistWithReferentialAction(t *testing.T, db *sql.DB, schema, table, constraint string, onDelete, onUpdate migrations.ForeignKeyAction) {
t.Helper()
if !foreignKeyExists(t, db, schema, table, constraint, false, opts...) {
if !foreignKeyExists(t, db, schema, table, constraint, true, onDelete, onUpdate) {
t.Fatalf("Expected validated foreign key %q to exist", constraint)
}
}

func NotValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
t.Helper()
if !foreignKeyExists(t, db, schema, table, constraint, false, migrations.ForeignKeyActionNOACTION, migrations.ForeignKeyActionNOACTION) {
t.Fatalf("Expected not validated foreign key %q to exist", constraint)
}
}

func NotValidatedForeignKeyMustExistWithReferentialAction(t *testing.T, db *sql.DB, schema, table, constraint string, onDelete, onUpdate migrations.ForeignKeyAction) {
t.Helper()
if !foreignKeyExists(t, db, schema, table, constraint, false, onDelete, onUpdate) {
t.Fatalf("Expected not validated foreign key %q to exist", constraint)
}
}
Expand Down Expand Up @@ -395,24 +409,26 @@ func uniqueConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint
return exists
}

type foreignKeyOnDeleteOpt func() string

func withOnDeleteCascade() foreignKeyOnDeleteOpt {
return func() string { return "c" }
}

func withOnDeleteSetNull() foreignKeyOnDeleteOpt {
return func() string { return "n" }
func referentialAction(a migrations.ForeignKeyAction) string {
switch a {
case migrations.ForeignKeyActionNOACTION:
return "a"
case migrations.ForeignKeyActionRESTRICT:
return "r"
case migrations.ForeignKeyActionSETNULL:
return "n"
case migrations.ForeignKeyActionSETDEFAULT:
return "d"
case migrations.ForeignKeyActionCASCADE:
return "c"
default:
return "a"
}
}

func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string, validated bool, opts ...foreignKeyOnDeleteOpt) bool {
func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string, validated bool, onDeleteAction, onUpdateAction migrations.ForeignKeyAction) bool {
t.Helper()

confDelType := "a"
for _, opt := range opts {
confDelType = opt()
}

var exists bool
err := db.QueryRow(`
SELECT EXISTS (
Expand All @@ -422,9 +438,10 @@ func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string
AND conname = $2
AND contype = 'f'
AND convalidated = $3
AND confdeltype = $4
AND confdeltype = $4
AND confupdtype = $5
)`,
fmt.Sprintf("%s.%s", schema, table), constraint, validated, confDelType).Scan(&exists)
fmt.Sprintf("%s.%s", schema, table), constraint, validated, referentialAction(onDeleteAction), referentialAction(onUpdateAction)).Scan(&exists)
if err != nil {
t.Fatal(err)
}
Expand Down
41 changes: 24 additions & 17 deletions pkg/migrations/op_create_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ func (o *OpCreateConstraint) Start(ctx context.Context, conn db.DB, latestSchema

switch o.Type {
case OpCreateConstraintTypeUnique:
temporaryColumnNames := make([]string, len(o.Columns))
for i, col := range o.Columns {
temporaryColumnNames[i] = TemporaryName(col)
}
return table, createUniqueIndexConcurrently(ctx, conn, s.Name, o.Name, o.Table, temporaryColumnNames)
return table, createUniqueIndexConcurrently(ctx, conn, s.Name, o.Name, o.Table, temporaryNames(o.Columns))
case OpCreateConstraintTypeCheck:
return table, o.addCheckConstraint(ctx, conn)
case OpCreateConstraintTypeForeignKey:
Expand Down Expand Up @@ -248,24 +244,35 @@ func (o *OpCreateConstraint) addCheckConstraint(ctx context.Context, conn db.DB)
}

func (o *OpCreateConstraint) addForeignKeyConstraint(ctx context.Context, conn db.DB) error {
onDelete := "NO ACTION"
if o.References.OnDelete != "" {
onDelete = strings.ToUpper(string(o.References.OnDelete))
sql := fmt.Sprintf("ALTER TABLE %s ADD ", pq.QuoteIdentifier(o.Table))

writer := &ConstraintSQLWriter{
Name: o.Name,
Columns: temporaryNames(o.Columns),
SkipValidation: true,
}
sql += writer.WriteForeignKey(
o.References.Table,
o.References.Columns,
o.References.OnDelete,
o.References.OnUpdate,
o.References.OnDeleteSetColumns,
o.References.MatchType,
)

_, err := conn.ExecContext(ctx,
fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) ON DELETE %s NOT VALID",
pq.QuoteIdentifier(o.Table),
pq.QuoteIdentifier(o.Name),
strings.Join(quotedTemporaryNames(o.Columns), ","),
pq.QuoteIdentifier(o.References.Table),
strings.Join(quoteColumnNames(o.References.Columns), ","),
onDelete,
))
_, err := conn.ExecContext(ctx, sql)

return err
}

func temporaryNames(columns []string) []string {
names := make([]string, len(columns))
for i, col := range columns {
names[i] = TemporaryName(col)
}
return names
}

func quotedTemporaryNames(columns []string) []string {
names := make([]string, len(columns))
for i, col := range columns {
Expand Down
Loading