Skip to content

Commit 09e2fd4

Browse files
authored
Expose foreign key constraint options in inline FK definition and in create_constraint (#653)
Now more foreign key options are supported: * `match_type` in column definitions and in `create_constraint` * `on_update` in column definitions and in `create_constraint` * `on_delete_set_columns` in `create_constraint` * `on_update` in inline FK definitions in a column's `references` * `match_type` in inline FK definction in a column's `references` Extracted from #628. The only remaining part of that PR is adding support in `sql2pgroll`. 🎉
1 parent c0dc021 commit 09e2fd4

15 files changed

+475
-78
lines changed

docs/operations/create_constraint.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Required fields: `name`, `table`, `type`, `up`, `down`.
2222
"table": "name of referenced table",
2323
"columns": "[names of referenced columns]",
2424
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
25+
"on_delete_set_columns": ["list of FKs to set", "in on delete operation on SET NULL or SET DEFAULT"],
26+
"on_update": "ON UPDATE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
27+
"match_type": "match type, can be SIMPLE or FULL. Default is SIMPLE"
2528
},
2629
"up": {
2730
"column1": "up SQL expressions for each column covered by the constraint",

docs/operations/create_table.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Each `column` is defined as:
4242
"table": "name of referenced table",
4343
"column": "name of referenced column",
4444
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, SET DEFAULT, RESTRICT, or NO ACTION. Default is NO ACTION",
45+
"on_update": "ON UPDATE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
46+
"match_type": "match type, can be SIMPLE or FULL. Default is SIMPLE"
4547
}
4648
},
4749
```

pkg/migrations/op_add_column.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,14 @@ func (w ColumnSQLWriter) Write(col Column) (string, error) {
314314
}
315315

316316
if col.References != nil {
317-
onDelete := string(ForeignKeyActionNOACTION)
318-
if col.References.OnDelete != "" {
319-
onDelete = strings.ToUpper(string(col.References.OnDelete))
320-
}
321-
322-
sql += fmt.Sprintf(" CONSTRAINT %s REFERENCES %s(%s) ON DELETE %s",
323-
pq.QuoteIdentifier(col.References.Name),
324-
pq.QuoteIdentifier(col.References.Table),
325-
pq.QuoteIdentifier(col.References.Column),
326-
onDelete)
317+
writer := &ConstraintSQLWriter{Name: col.References.Name}
318+
sql += " " + writer.WriteForeignKey(
319+
col.References.Table,
320+
[]string{col.References.Column},
321+
col.References.OnDelete,
322+
col.References.OnUpdate,
323+
nil,
324+
col.References.MatchType)
327325
}
328326
if col.Check != nil {
329327
sql += fmt.Sprintf(" CONSTRAINT %s CHECK (%s)",

pkg/migrations/op_add_column_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,14 @@ func TestAddForeignKeyColumn(t *testing.T) {
636636
},
637637
afterStart: func(t *testing.T, db *sql.DB, schema string) {
638638
// The foreign key constraint exists on the new table.
639-
ValidatedForeignKeyMustExist(t, db, schema, "orders", "fk_users_id", withOnDeleteCascade())
639+
ValidatedForeignKeyMustExistWithReferentialAction(
640+
t,
641+
db,
642+
schema,
643+
"orders",
644+
"fk_users_id",
645+
migrations.ForeignKeyActionCASCADE,
646+
migrations.ForeignKeyActionNOACTION)
640647

641648
// Inserting a row into the referenced table succeeds.
642649
MustInsert(t, db, schema, "01_create_table", "users", map[string]string{
@@ -669,7 +676,14 @@ func TestAddForeignKeyColumn(t *testing.T) {
669676
},
670677
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
671678
// The foreign key constraint still exists on the new table
672-
ValidatedForeignKeyMustExist(t, db, schema, "orders", "fk_users_id", withOnDeleteCascade())
679+
ValidatedForeignKeyMustExistWithReferentialAction(
680+
t,
681+
db,
682+
schema,
683+
"orders",
684+
"fk_users_id",
685+
migrations.ForeignKeyActionCASCADE,
686+
migrations.ForeignKeyActionNOACTION)
673687

674688
// Inserting a row into the referenced table succeeds.
675689
MustInsert(t, db, schema, "02_add_column", "users", map[string]string{

pkg/migrations/op_change_type_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,27 @@ func TestChangeColumnType(t *testing.T) {
207207
},
208208
afterStart: func(t *testing.T, db *sql.DB, schema string) {
209209
// A temporary FK constraint has been created on the temporary column
210-
ValidatedForeignKeyMustExist(t, db, schema, "employees", migrations.DuplicationName("fk_employee_department"), withOnDeleteCascade())
210+
ValidatedForeignKeyMustExistWithReferentialAction(
211+
t,
212+
db,
213+
schema,
214+
"employees",
215+
migrations.DuplicationName("fk_employee_department"),
216+
migrations.ForeignKeyActionCASCADE,
217+
migrations.ForeignKeyActionNOACTION)
211218
},
212219
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
213220
},
214221
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
215222
// The foreign key constraint still exists on the column
216-
ValidatedForeignKeyMustExist(t, db, schema, "employees", "fk_employee_department", withOnDeleteCascade())
223+
ValidatedForeignKeyMustExistWithReferentialAction(
224+
t,
225+
db,
226+
schema,
227+
"employees",
228+
"fk_employee_department",
229+
migrations.ForeignKeyActionCASCADE,
230+
migrations.ForeignKeyActionNOACTION)
217231
},
218232
},
219233
{

pkg/migrations/op_common_test.go

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,30 @@ func UniqueConstraintMustExist(t *testing.T, db *sql.DB, schema, table, constrai
244244
}
245245
}
246246

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

254-
func NotValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string, opts ...foreignKeyOnDeleteOpt) {
254+
func ValidatedForeignKeyMustExistWithReferentialAction(t *testing.T, db *sql.DB, schema, table, constraint string, onDelete, onUpdate migrations.ForeignKeyAction) {
255255
t.Helper()
256-
if !foreignKeyExists(t, db, schema, table, constraint, false, opts...) {
256+
if !foreignKeyExists(t, db, schema, table, constraint, true, onDelete, onUpdate) {
257+
t.Fatalf("Expected validated foreign key %q to exist", constraint)
258+
}
259+
}
260+
261+
func NotValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
262+
t.Helper()
263+
if !foreignKeyExists(t, db, schema, table, constraint, false, migrations.ForeignKeyActionNOACTION, migrations.ForeignKeyActionNOACTION) {
264+
t.Fatalf("Expected not validated foreign key %q to exist", constraint)
265+
}
266+
}
267+
268+
func NotValidatedForeignKeyMustExistWithReferentialAction(t *testing.T, db *sql.DB, schema, table, constraint string, onDelete, onUpdate migrations.ForeignKeyAction) {
269+
t.Helper()
270+
if !foreignKeyExists(t, db, schema, table, constraint, false, onDelete, onUpdate) {
257271
t.Fatalf("Expected not validated foreign key %q to exist", constraint)
258272
}
259273
}
@@ -395,24 +409,26 @@ func uniqueConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint
395409
return exists
396410
}
397411

398-
type foreignKeyOnDeleteOpt func() string
399-
400-
func withOnDeleteCascade() foreignKeyOnDeleteOpt {
401-
return func() string { return "c" }
402-
}
403-
404-
func withOnDeleteSetNull() foreignKeyOnDeleteOpt {
405-
return func() string { return "n" }
412+
func referentialAction(a migrations.ForeignKeyAction) string {
413+
switch a {
414+
case migrations.ForeignKeyActionNOACTION:
415+
return "a"
416+
case migrations.ForeignKeyActionRESTRICT:
417+
return "r"
418+
case migrations.ForeignKeyActionSETNULL:
419+
return "n"
420+
case migrations.ForeignKeyActionSETDEFAULT:
421+
return "d"
422+
case migrations.ForeignKeyActionCASCADE:
423+
return "c"
424+
default:
425+
return "a"
426+
}
406427
}
407428

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

411-
confDelType := "a"
412-
for _, opt := range opts {
413-
confDelType = opt()
414-
}
415-
416432
var exists bool
417433
err := db.QueryRow(`
418434
SELECT EXISTS (
@@ -422,9 +438,10 @@ func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string
422438
AND conname = $2
423439
AND contype = 'f'
424440
AND convalidated = $3
425-
AND confdeltype = $4
441+
AND confdeltype = $4
442+
AND confupdtype = $5
426443
)`,
427-
fmt.Sprintf("%s.%s", schema, table), constraint, validated, confDelType).Scan(&exists)
444+
fmt.Sprintf("%s.%s", schema, table), constraint, validated, referentialAction(onDeleteAction), referentialAction(onUpdateAction)).Scan(&exists)
428445
if err != nil {
429446
t.Fatal(err)
430447
}

pkg/migrations/op_create_constraint.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,7 @@ func (o *OpCreateConstraint) Start(ctx context.Context, conn db.DB, latestSchema
6868

6969
switch o.Type {
7070
case OpCreateConstraintTypeUnique:
71-
temporaryColumnNames := make([]string, len(o.Columns))
72-
for i, col := range o.Columns {
73-
temporaryColumnNames[i] = TemporaryName(col)
74-
}
75-
return table, createUniqueIndexConcurrently(ctx, conn, s.Name, o.Name, o.Table, temporaryColumnNames)
71+
return table, createUniqueIndexConcurrently(ctx, conn, s.Name, o.Name, o.Table, temporaryNames(o.Columns))
7672
case OpCreateConstraintTypeCheck:
7773
return table, o.addCheckConstraint(ctx, conn)
7874
case OpCreateConstraintTypeForeignKey:
@@ -248,24 +244,35 @@ func (o *OpCreateConstraint) addCheckConstraint(ctx context.Context, conn db.DB)
248244
}
249245

250246
func (o *OpCreateConstraint) addForeignKeyConstraint(ctx context.Context, conn db.DB) error {
251-
onDelete := "NO ACTION"
252-
if o.References.OnDelete != "" {
253-
onDelete = strings.ToUpper(string(o.References.OnDelete))
247+
sql := fmt.Sprintf("ALTER TABLE %s ADD ", pq.QuoteIdentifier(o.Table))
248+
249+
writer := &ConstraintSQLWriter{
250+
Name: o.Name,
251+
Columns: temporaryNames(o.Columns),
252+
SkipValidation: true,
254253
}
254+
sql += writer.WriteForeignKey(
255+
o.References.Table,
256+
o.References.Columns,
257+
o.References.OnDelete,
258+
o.References.OnUpdate,
259+
o.References.OnDeleteSetColumns,
260+
o.References.MatchType,
261+
)
255262

256-
_, err := conn.ExecContext(ctx,
257-
fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) ON DELETE %s NOT VALID",
258-
pq.QuoteIdentifier(o.Table),
259-
pq.QuoteIdentifier(o.Name),
260-
strings.Join(quotedTemporaryNames(o.Columns), ","),
261-
pq.QuoteIdentifier(o.References.Table),
262-
strings.Join(quoteColumnNames(o.References.Columns), ","),
263-
onDelete,
264-
))
263+
_, err := conn.ExecContext(ctx, sql)
265264

266265
return err
267266
}
268267

268+
func temporaryNames(columns []string) []string {
269+
names := make([]string, len(columns))
270+
for i, col := range columns {
271+
names[i] = TemporaryName(col)
272+
}
273+
return names
274+
}
275+
269276
func quotedTemporaryNames(columns []string) []string {
270277
names := make([]string, len(columns))
271278
for i, col := range columns {

0 commit comments

Comments
 (0)