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
80 changes: 79 additions & 1 deletion docs/classification.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,87 @@ Backward-compatible migration
> **WARNING**: If there are foreign keys, table creation requires
> `ShareRowExclusiveLock` on the child tables, so use `lock_timeout`
> if the table to create contains foreign keys. `ADD FOREIGN KEY ... NOT VALID`
> does require the same lock, so it doesnt make much sense
> does require the same lock, so it doesn't make much sense
> to create foreign keys separately.

#### Primary Key Requirement

**All newly created tables must have a primary key.** Tables without primary keys
are classified as **RESTRICTED** operations and will cause the linter to fail.

**Valid approaches:**

**Column-level primary key:**

```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT
);
```

**Table-level primary key:**

```sql
CREATE TABLE users (
id INTEGER,
name TEXT,
PRIMARY KEY (id)
);
```

**Named constraint:**

```sql
CREATE TABLE users (
id INTEGER,
name TEXT,
CONSTRAINT pk_users PRIMARY KEY (id)
);
```

**Composite primary key:**

```sql
CREATE TABLE user_roles (
user_id INTEGER,
role_id INTEGER,
PRIMARY KEY (user_id, role_id)
);
```

**UUID primary key:**

```sql
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```

**Why primary keys are required:**

* Enables logical replication
* Improves query performance and indexing
* Provides row uniqueness guarantees
* Required for many database tools and ORMs
* Facilitates data consistency and referential integrity

**Exception cases:**

If you have a legitimate use case for a table without a primary key (such as
temporary tables, log tables, or staging tables), you can ignore this specific
migration using the annotation:

```sql
-- migration-lint:ignore
CREATE TABLE temp_data_load (
raw_data TEXT,
imported_at TIMESTAMPTZ DEFAULT NOW()
);
```

### Drop Table

Backward-incompatible migration
Expand Down
15 changes: 14 additions & 1 deletion migration_lint/sql/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@
),
SegmentLocator(type="create_sequence_statement"),
SegmentLocator(type="alter_sequence_statement"),
SegmentLocator(type="create_table_statement"),
SegmentLocator(
type="create_table_statement",
children=[
KeywordLocator(raw="PRIMARY"),
KeywordLocator(raw="KEY"),
],
),
SegmentLocator(
type="alter_table_statement",
children=[
Expand Down Expand Up @@ -358,6 +364,13 @@
KeywordLocator(raw="IDENTITY"),
],
),
SegmentLocator(
type="create_table_statement",
children=[
KeywordLocator(raw="PRIMARY", inverted=True),
KeywordLocator(raw="KEY", inverted=True),
],
),
]

DATA_MIGRATION_OPERATIONS = [
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "migration-lint"
version = "0.2.12"
version = "0.2.13"
description = "Tool for lint operations in DB migrations SQL"
authors = ["Alexey Nikitenko <alexey.nikitenko@pandadoc.com>"]
readme = "README.md"
Expand Down
45 changes: 43 additions & 2 deletions tests/test_classify_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@
StatementType.BACKWARD_COMPATIBLE,
),
(
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) REFERENCES some_table (id);",
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) "
"REFERENCES some_table (id);",
StatementType.RESTRICTED,
),
(
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) REFERENCES some_table (id) NOT VALID;",
"ALTER TABLE t_name ADD CONSTRAINT name FOREIGN KEY (c_name) "
"REFERENCES some_table (id) NOT VALID;",
StatementType.BACKWARD_COMPATIBLE,
),
("UPDATE t_name SET col=0", StatementType.DATA_MIGRATION),
Expand Down Expand Up @@ -148,6 +150,45 @@
"ALTER TABLE t_name ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY",
StatementType.BACKWARD_COMPATIBLE,
),
# CREATE TABLE PRIMARY KEY tests
(
"CREATE TABLE users (id integer, name text);",
StatementType.RESTRICTED,
),
(
"CREATE TABLE users (id integer PRIMARY KEY, name text);",
StatementType.BACKWARD_COMPATIBLE,
),
(
"CREATE TABLE users (id integer, name text, PRIMARY KEY (id));",
StatementType.BACKWARD_COMPATIBLE,
),
(
"CREATE TABLE users (id serial PRIMARY KEY, name text);",
StatementType.BACKWARD_COMPATIBLE,
),
(
"CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), "
"name text);",
StatementType.BACKWARD_COMPATIBLE,
),
(
"CREATE TABLE users (id integer, name text, "
"CONSTRAINT pk_users PRIMARY KEY (id));",
StatementType.BACKWARD_COMPATIBLE,
),
(
"CREATE TABLE users (id integer NOT NULL, name text UNIQUE);",
StatementType.RESTRICTED,
),
(
"CREATE TABLE log_entries (timestamp timestamptz, message text);",
StatementType.RESTRICTED,
),
(
"CREATE TABLE users (primary_email text, secondary_email text);",
StatementType.RESTRICTED,
),
],
)
def test_classify_migration(statement: str, expected_type: StatementType):
Expand Down