Skip to content

Commit

Permalink
Allow setting the ON DELETE behaviour of foreign key constraints (#297
Browse files Browse the repository at this point in the history
)

Add support for setting the `ON DELETE` behaviour of a foreign key
constraint.

An example migration that uses the behaviour is:

```json
{
  "name": "21_add_foreign_key_constraint",
  "operations": [
    {
      "alter_column": {
        "table": "posts",
        "column": "user_id",
        "references": {
          "name": "fk_users_id",
          "table": "users",
          "column": "id",
          "on_delete": "CASCADE"
        },
        "up": "(SELECT CASE WHEN EXISTS (SELECT 1 FROM users WHERE users.id = user_id) THEN user_id ELSE NULL END)",
        "down": "user_id"
      }
    }
  ]
}
```

The valid options for `on_delete` are `CASCADE`, `SET NULL`, `RESTRICT`,
or `NO ACTION`. If the field is omitted, the default is `NO ACTION`,

Fixes #221
  • Loading branch information
andrew-farries authored Mar 1, 2024
1 parent 431b951 commit def08e2
Show file tree
Hide file tree
Showing 8 changed files with 578 additions and 9 deletions.
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,8 @@ Add foreign key operations add a foreign key constraint to a column.
"references": {
"name": "name of foreign key reference",
"table": "name of referenced table",
"column": "name of referenced column"
"column": "name of referenced column",
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
},
"up": "SQL expression",
"down": "SQL expression"
Expand Down
3 changes: 2 additions & 1 deletion examples/21_add_foreign_key_constraint.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"references": {
"name": "fk_users_id",
"table": "users",
"column": "id"
"column": "id",
"on_delete": "CASCADE"
},
"up": "(SELECT CASE WHEN EXISTS (SELECT 1 FROM users WHERE users.id = user_id) THEN user_id ELSE NULL END)",
"down": "user_id"
Expand Down
19 changes: 19 additions & 0 deletions pkg/migrations/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,22 @@ type InvalidReplicaIdentityError struct {
func (e InvalidReplicaIdentityError) Error() string {
return fmt.Sprintf("replica identity on table %q must be one of 'NOTHING', 'DEFAULT', 'INDEX' or 'FULL', found %q", e.Table, e.Identity)
}

type InvalidOnDeleteSettingError struct {
Table string
Column string
Setting string
}

func (e InvalidOnDeleteSettingError) Error() string {
return fmt.Sprintf("foreign key on_delete setting on column %q, table %q must be one of: %q, %q, %q, %q or %q, not %q",
e.Column,
e.Table,
ForeignKeyReferenceOnDeleteNOACTION,
ForeignKeyReferenceOnDeleteRESTRICT,
ForeignKeyReferenceOnDeleteSETDEFAULT,
ForeignKeyReferenceOnDeleteSETNULL,
ForeignKeyReferenceOnDeleteCASCADE,
e.Setting,
)
}
18 changes: 18 additions & 0 deletions pkg/migrations/op_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,24 @@ func MustDelete(t *testing.T, db *sql.DB, schema, version, table string, record
}
}

func MustNotDelete(t *testing.T, db *sql.DB, schema, version, table string, record map[string]string, errorCode string) {
t.Helper()

err := delete(t, db, schema, version, table, record)
if err == nil {
t.Fatal("Expected DELETE to fail")
}

var pqErr *pq.Error
if ok := errors.As(err, &pqErr); ok {
if pqErr.Code.Name() != errorCode {
t.Fatalf("Expected DELETE to fail with %q, got %q", errorCode, pqErr.Code.Name())
}
} else {
t.Fatalf("DELETE failed with unknown error: %v", err)
}
}

func delete(t *testing.T, db *sql.DB, schema, version, table string, record map[string]string) error {
t.Helper()
versionSchema := roll.VersionedSchemaName(schema, version)
Expand Down
34 changes: 27 additions & 7 deletions pkg/migrations/op_set_fk.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/lib/pq"
"github.com/xataio/pgroll/pkg/schema"
Expand Down Expand Up @@ -160,19 +161,38 @@ func (o *OpSetForeignKey) Validate(ctx context.Context, s *schema.Schema) error
return FieldRequiredError{Name: "down"}
}

switch strings.ToUpper(string(o.References.OnDelete)) {
case string(ForeignKeyReferenceOnDeleteNOACTION):
case string(ForeignKeyReferenceOnDeleteRESTRICT):
case string(ForeignKeyReferenceOnDeleteSETDEFAULT):
case string(ForeignKeyReferenceOnDeleteSETNULL):
case string(ForeignKeyReferenceOnDeleteCASCADE):
case "":
break
default:
return InvalidOnDeleteSettingError{Table: o.Table, Column: o.Column, Setting: string(o.References.OnDelete)}
}

return nil
}

func (o *OpSetForeignKey) addForeignKeyConstraint(ctx context.Context, conn *sql.DB) error {
tempColumnName := TemporaryName(o.Column)

_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID",
pq.QuoteIdentifier(o.Table),
pq.QuoteIdentifier(o.References.Name),
pq.QuoteIdentifier(tempColumnName),
pq.QuoteIdentifier(o.References.Table),
pq.QuoteIdentifier(o.References.Column),
))
onDelete := "NO ACTION"
if o.References.OnDelete != "" {
onDelete = strings.ToUpper(string(o.References.OnDelete))
}

_, 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.References.Name),
pq.QuoteIdentifier(tempColumnName),
pq.QuoteIdentifier(o.References.Table),
pq.QuoteIdentifier(o.References.Column),
onDelete,
))

return err
}
Loading

0 comments on commit def08e2

Please sign in to comment.