Skip to content

Commit 940f74d

Browse files
author
Vladyslav Fomenko
committed
Add primary keys requirement rule
1 parent 595a502 commit 940f74d

4 files changed

Lines changed: 137 additions & 5 deletions

File tree

docs/classification.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,87 @@ Backward-compatible migration
125125
> **WARNING**: If there are foreign keys, table creation requires
126126
> `ShareRowExclusiveLock` on the child tables, so use `lock_timeout`
127127
> if the table to create contains foreign keys. `ADD FOREIGN KEY ... NOT VALID`
128-
> does require the same lock, so it doesnt make much sense
128+
> does require the same lock, so it doesn't make much sense
129129
> to create foreign keys separately.
130130
131+
#### Primary Key Requirement
132+
133+
**All newly created tables must have a primary key.** Tables without primary keys
134+
are classified as **RESTRICTED** operations and will cause the linter to fail.
135+
136+
**Valid approaches:**
137+
138+
**Column-level primary key:**
139+
140+
```sql
141+
CREATE TABLE users (
142+
id SERIAL PRIMARY KEY,
143+
name TEXT
144+
);
145+
```
146+
147+
**Table-level primary key:**
148+
149+
```sql
150+
CREATE TABLE users (
151+
id INTEGER,
152+
name TEXT,
153+
PRIMARY KEY (id)
154+
);
155+
```
156+
157+
**Named constraint:**
158+
159+
```sql
160+
CREATE TABLE users (
161+
id INTEGER,
162+
name TEXT,
163+
CONSTRAINT pk_users PRIMARY KEY (id)
164+
);
165+
```
166+
167+
**Composite primary key:**
168+
169+
```sql
170+
CREATE TABLE user_roles (
171+
user_id INTEGER,
172+
role_id INTEGER,
173+
PRIMARY KEY (user_id, role_id)
174+
);
175+
```
176+
177+
**UUID primary key:**
178+
179+
```sql
180+
CREATE TABLE sessions (
181+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
182+
user_id INTEGER,
183+
created_at TIMESTAMPTZ DEFAULT NOW()
184+
);
185+
```
186+
187+
**Why primary keys are required:**
188+
189+
* Enables logical replication
190+
* Improves query performance and indexing
191+
* Provides row uniqueness guarantees
192+
* Required for many database tools and ORMs
193+
* Facilitates data consistency and referential integrity
194+
195+
**Exception cases:**
196+
197+
If you have a legitimate use case for a table without a primary key (such as
198+
temporary tables, log tables, or staging tables), you can ignore this specific
199+
migration using the annotation:
200+
201+
```sql
202+
-- migration-lint:ignore
203+
CREATE TABLE temp_data_load (
204+
raw_data TEXT,
205+
imported_at TIMESTAMPTZ DEFAULT NOW()
206+
);
207+
```
208+
131209
### Drop Table
132210

133211
Backward-incompatible migration

migration_lint/sql/rules.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@
7676
),
7777
SegmentLocator(type="create_sequence_statement"),
7878
SegmentLocator(type="alter_sequence_statement"),
79-
SegmentLocator(type="create_table_statement"),
79+
SegmentLocator(
80+
type="create_table_statement",
81+
children=[
82+
KeywordLocator(raw="PRIMARY"),
83+
KeywordLocator(raw="KEY"),
84+
],
85+
),
8086
SegmentLocator(
8187
type="alter_table_statement",
8288
children=[
@@ -358,6 +364,13 @@
358364
KeywordLocator(raw="IDENTITY"),
359365
],
360366
),
367+
SegmentLocator(
368+
type="create_table_statement",
369+
children=[
370+
KeywordLocator(raw="PRIMARY", inverted=True),
371+
KeywordLocator(raw="KEY", inverted=True),
372+
],
373+
),
361374
]
362375

363376
DATA_MIGRATION_OPERATIONS = [

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "migration-lint"
3-
version = "0.2.12"
3+
version = "0.2.13"
44
description = "Tool for lint operations in DB migrations SQL"
55
authors = ["Alexey Nikitenko <alexey.nikitenko@pandadoc.com>"]
66
readme = "README.md"

tests/test_classify_statement.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@
7373
StatementType.BACKWARD_COMPATIBLE,
7474
),
7575
(
76-
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) REFERENCES some_table (id);",
76+
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) "
77+
"REFERENCES some_table (id);",
7778
StatementType.RESTRICTED,
7879
),
7980
(
80-
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) REFERENCES some_table (id) NOT VALID;",
81+
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) "
82+
"REFERENCES some_table (id) NOT VALID;",
8183
StatementType.BACKWARD_COMPATIBLE,
8284
),
8385
("UPDATE t_name SET col=0", StatementType.DATA_MIGRATION),
@@ -148,6 +150,45 @@
148150
"ALTER TABLE t_name ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY",
149151
StatementType.BACKWARD_COMPATIBLE,
150152
),
153+
# CREATE TABLE PRIMARY KEY tests
154+
(
155+
"CREATE TABLE users (id integer, name text);",
156+
StatementType.RESTRICTED,
157+
),
158+
(
159+
"CREATE TABLE users (id integer PRIMARY KEY, name text);",
160+
StatementType.BACKWARD_COMPATIBLE,
161+
),
162+
(
163+
"CREATE TABLE users (id integer, name text, PRIMARY KEY (id));",
164+
StatementType.BACKWARD_COMPATIBLE,
165+
),
166+
(
167+
"CREATE TABLE users (id serial PRIMARY KEY, name text);",
168+
StatementType.BACKWARD_COMPATIBLE,
169+
),
170+
(
171+
"CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), "
172+
"name text);",
173+
StatementType.BACKWARD_COMPATIBLE,
174+
),
175+
(
176+
"CREATE TABLE users (id integer, name text, "
177+
"CONSTRAINT pk_users PRIMARY KEY (id));",
178+
StatementType.BACKWARD_COMPATIBLE,
179+
),
180+
(
181+
"CREATE TABLE users (id integer NOT NULL, name text UNIQUE);",
182+
StatementType.RESTRICTED,
183+
),
184+
(
185+
"CREATE TABLE log_entries (timestamp timestamptz, message text);",
186+
StatementType.RESTRICTED,
187+
),
188+
(
189+
"CREATE TABLE users (primary_email text, secondary_email text);",
190+
StatementType.RESTRICTED,
191+
),
151192
],
152193
)
153194
def test_classify_migration(statement: str, expected_type: StatementType):

0 commit comments

Comments
 (0)