Skip to content

Commit b40621b

Browse files
authored
Add support for foreign key constraints in create_table operation (#597)
This PR adds support for `foreign_key` constraints in table constraint definitions in `create_table`. It includes all settings that are currently available in PostgreSQL 17. At the moment, this option is superior to other foreign key definitions in pgroll because it supports `ON UPDATE` and `MATCH` type settings. As I mentioned in earlier PRs, once this work is finished, I am unifying the constraint options in all operations. Example constraint configuration: ```sql { "name": "provider_fk", "type": "foreign_key", "columns": [ "provider_id", "product_id" ], "deferrable": false, "references": { "table": "provider", "columns": [ "id", "product_id" ], "on_delete": "SET DEFAULT", "on_delete_set_columns": ["id"], "on_update": "CASCADE", "match_type": "SIMPLE" } } ```
1 parent de56b35 commit b40621b

17 files changed

+985
-33
lines changed

docs/operations/create_table.mdx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Each `column` is defined as:
4141
"name": "name of foreign key constraint",
4242
"table": "name of referenced table",
4343
"column": "name of referenced column",
44-
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, RESTRICT, or NO ACTION. Default is NO ACTION",
44+
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, SET DEFAULT, RESTRICT, or NO ACTION. Default is NO ACTION",
4545
}
4646
},
4747
```
@@ -62,6 +62,15 @@ Each `constraint` is defined as:
6262
"deferrable": true|false,
6363
"initially_deferred": true|false,
6464
"no_inherit": true|false,
65+
"references": {
66+
"name": "name of foreign key constraint",
67+
"table": "name of referenced table",
68+
"columns": ["list", "of", "referenced", "columns"],
69+
"on_delete": "ON DELETE behaviour, can be CASCADE, SET NULL, SET DEFAULT, RESTRICT, or NO ACTION. Default is NO ACTION",
70+
"on_delete_set_columns": ["list of FKs to set", "in on delete operation on SET NULL or SET DEFAULT"],
71+
"on_update": "ON UPDATE behaviour, can be CASCADE, SET NULL, SET DEFAULT, RESTRICT, or NO ACTION. Default is NO ACTION",
72+
"match_type": "match type, can be SIMPLE, FULL or PARTIAL. Default is SIMPLE"
73+
},
6574
"index_parameters": {
6675
"tablespace": "index_tablespace",
6776
"storage_parameters": "parameter=value",
@@ -70,7 +79,7 @@ Each `constraint` is defined as:
7079
},
7180
```
7281

73-
Supported constraint types: `unique`, `check`, `primary_key`.
82+
Supported constraint types: `unique`, `check`, `primary_key`, `foreign_key`.
7483

7584
Please note that you can only configure primary keys in `columns` list or `constraints` list, but
7685
not in both places.
@@ -138,4 +147,10 @@ Create a table with different `DEFAULT` values:
138147

139148
Create a table with table level constraints:
140149

141-
<ExampleSnippet example="50_create_table_with_table_constraint.json" language="json" />
150+
<ExampleSnippet example="50_create_table_with_table_constraint.json" language="json" />
151+
152+
### Create a table with table level foreign key constraints
153+
154+
Create a table with table level foreign key constraints:
155+
156+
<ExampleSnippet example="51_create_table_with_table_foreign_key_constraint.json" language="json" />

examples/.ledger

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@
4848
48_drop_tickets_check.json
4949
49_unset_not_null_on_indexed_column.json
5050
50_create_table_with_table_constraint.json
51+
51_create_table_with_table_foreign_key_constraint.json

examples/50_create_table_with_table_constraint.json

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"operations": [
44
{
55
"create_table": {
6-
"name": "phonebook",
6+
"name": "telephone_providers",
77
"columns": [
88
{
99
"name": "id",
@@ -14,33 +14,28 @@
1414
"type": "varchar(255)"
1515
},
1616
{
17-
"name": "city",
17+
"name": "tax_id",
1818
"type": "varchar(255)"
1919
},
2020
{
21-
"name": "phone",
21+
"name": "headquarters",
2222
"type": "varchar(255)"
2323
}
2424
],
2525
"constraints": [
2626
{
27-
"name": "phonebook_pk",
27+
"name": "provider_pk",
2828
"type": "primary_key",
2929
"columns": [
3030
"id"
3131
]
3232
},
3333
{
34-
"name": "unique_numbers",
34+
"name": "unique_tax_id",
3535
"type": "unique",
3636
"columns": [
37-
"phone"
38-
],
39-
"index_parameters": {
40-
"include_columns": [
41-
"name"
42-
]
43-
}
37+
"tax_id"
38+
]
4439
},
4540
{
4641
"name": "name_must_be_present",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"name": "51_create_table_with_table_foreign_key_constraint",
3+
"operations": [
4+
{
5+
"create_table": {
6+
"name": "phonebook",
7+
"columns": [
8+
{
9+
"name": "id",
10+
"type": "serial"
11+
},
12+
{
13+
"name": "provider_id",
14+
"type": "serial"
15+
},
16+
{
17+
"name": "name",
18+
"type": "varchar(255)"
19+
},
20+
{
21+
"name": "city",
22+
"type": "varchar(255)"
23+
},
24+
{
25+
"name": "phone",
26+
"type": "varchar(255)"
27+
}
28+
],
29+
"constraints": [
30+
{
31+
"name": "phonebook_pk",
32+
"type": "primary_key",
33+
"columns": [
34+
"id"
35+
]
36+
},
37+
{
38+
"name": "provider_fk",
39+
"type": "foreign_key",
40+
"columns": [
41+
"provider_id"
42+
],
43+
"deferrable": false,
44+
"references": {
45+
"table": "telephone_providers",
46+
"columns": [
47+
"id"
48+
],
49+
"on_delete": "CASCADE",
50+
"on_update": "CASCADE",
51+
"match_type": "SIMPLE"
52+
}
53+
},
54+
{
55+
"name": "unique_numbers",
56+
"type": "unique",
57+
"columns": [
58+
"phone"
59+
],
60+
"index_parameters": {
61+
"include_columns": [
62+
"name"
63+
]
64+
}
65+
},
66+
{
67+
"name": "name_must_be_present",
68+
"type": "check",
69+
"check": "length(name) > 0"
70+
}
71+
]
72+
}
73+
}
74+
]
75+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
This is an invalid 'create_table' migration.
2+
Foreign key constraints must have references configured
3+
4+
-- create_table.json --
5+
{
6+
"name": "migration_name",
7+
"operations": [
8+
{
9+
"create_table": {
10+
"name": "posts",
11+
"columns": [
12+
{
13+
"name": "title",
14+
"type": "varchar(255)"
15+
},
16+
{
17+
"name": "user_id",
18+
"type": "integer",
19+
"nullable": true
20+
}
21+
],
22+
"constraints": [
23+
{
24+
"name": "my_invalid_fk",
25+
"type": "foreign_key",
26+
"columns": ["title"]
27+
}
28+
]
29+
}
30+
}
31+
]
32+
}
33+
34+
-- valid --
35+
false
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
This is an invalid 'create_table' migration.
2+
Foreign key constraints must have referenced table configured in references.table
3+
4+
-- create_table.json --
5+
{
6+
"name": "migration_name",
7+
"operations": [
8+
{
9+
"create_table": {
10+
"name": "posts",
11+
"columns": [
12+
{
13+
"name": "title",
14+
"type": "varchar(255)"
15+
},
16+
{
17+
"name": "user_id",
18+
"type": "integer",
19+
"nullable": true
20+
}
21+
],
22+
"constraints": [
23+
{
24+
"name": "my_invalid_fk",
25+
"type": "foreign_key",
26+
"columns": ["title"],
27+
"references": {
28+
"columns": [
29+
"referenced"
30+
]
31+
}
32+
}
33+
]
34+
}
35+
}
36+
]
37+
}
38+
39+
-- valid --
40+
false
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
This is an invalid 'create_table' migration.
2+
Foreign key constraints must have columns configured
3+
4+
-- create_table.json --
5+
{
6+
"name": "migration_name",
7+
"operations": [
8+
{
9+
"create_table": {
10+
"name": "posts",
11+
"columns": [
12+
{
13+
"name": "title",
14+
"type": "varchar(255)"
15+
},
16+
{
17+
"name": "user_id",
18+
"type": "integer",
19+
"nullable": true
20+
}
21+
],
22+
"constraints": [
23+
{
24+
"name": "my_invalid_fk",
25+
"type": "foreign_key",
26+
"references": {
27+
"columns": ["title"],
28+
"table": "referenced",
29+
"on_delete": "CASCADE",
30+
"on_update": "SET NULL"
31+
}
32+
}
33+
]
34+
}
35+
}
36+
]
37+
}
38+
39+
-- valid --
40+
false

pkg/migrations/errors.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,30 @@ func (e InvalidOnDeleteSettingError) Error() string {
208208
)
209209
}
210210

211+
type UnexpectedOnDeleteSetColumnError struct {
212+
Name string
213+
}
214+
215+
func (e UnexpectedOnDeleteSetColumnError) Error() string {
216+
return fmt.Sprintf("if on_delete_set_columns is set in foreign key %q, on_delete setting must be one of: %q, %q",
217+
e.Name,
218+
ForeignKeyReferenceOnDeleteSETDEFAULT,
219+
ForeignKeyReferenceOnDeleteSETNULL,
220+
)
221+
}
222+
223+
type InvalidOnDeleteSetColumnError struct {
224+
Name string
225+
Column string
226+
}
227+
228+
func (e InvalidOnDeleteSetColumnError) Error() string {
229+
return fmt.Sprintf("invalid on_delete_set_columns setting: column %q is not part of the foreign key %q",
230+
e.Column,
231+
e.Name,
232+
)
233+
}
234+
211235
type AlterColumnNoChangesError struct {
212236
Table string
213237
Column string

0 commit comments

Comments
 (0)