# ARGUS-A13: Missing or Empty Rollback Down Migrations
> **Rule Code:** `ARGUS-A13`
> **Identifier:** `MISSING_DOWN_MIGRATION`
> **Severity:** `HIGH / CRITICAL` (Failed Automated Rollback & Half-Migrated State Blocker)
> **Category:** `Schema Evolution, Incident Recovery & Deployment Safety`
> **Target Standards:** CWE-1033 (Incomplete Component Recovery), Operational Zero-Downtime Standards, OWASP ASVS v4.0.3/v5.0 §V1.4.3
---
## 1. Overview & Core Invariant
Every forward database schema migration file (`.up.sql`) **must have a corresponding, non-empty, and executable reverse rollback migration file (`.down.sql`)**.
Automated deployment orchestrators and CI/CD pipelines require instantaneous rollback capabilities if application health checks fail during release deployment. Rollback scripts must never be omitted, left as 0-byte placeholders, or contain only whitespace and comments.
---
## 2. Technical Grounding & PostgreSQL Engine Realities
### 2.1. Transactional DDL & Post-Commit Failures
PostgreSQL supports transactional DDL (`BEGIN ... DDL ... COMMIT`), allowing multi-statement migrations to roll back if a syntax or constraint error occurs during execution. However, once a migration commits successfully and the subsequent application startup fails (e.g. environment variable misconfiguration or startup panic), transactional DDL cannot revert committed catalog modifications. Reversion requires executing the corresponding `.down.sql` script.
### 2.2. The Half-Migrated Production Disaster (CWE-1033)
When deployment automation triggers a rollback to the previous release:
1. **Unpaired Migration:** Migration `000025_add_documents.up.sql` was applied to the database.
2. **Application Startup Failure:** The new application version fails post-deployment health checks.
3. **Rollback Attempt:** CI/CD initiates an automated rollback to version `000024`, looking for `000025_add_documents.down.sql`.
4. **Catastrophic Outage:** Because the `.down.sql` file is missing or empty, the automated rollback fails. The database remains stuck in a half-migrated state where old application pods fail to run against the new schema, triggering extended downtime.
```mermaid
flowchart TD
subgraph OUTAGE ["Missing Down Migration Disaster (CWE-1033)"]
direction TB
UpApply["1. CI/CD Applies: 000025_add_docs.up.sql (COMMITTED)"] --> AppFail["2. New App Pod Crashes on Startup"]
AppFail --> Rollback["3. CI/CD Triggers Auto-Rollback to V24"]
Rollback --> Missing["4. 000025_add_docs.down.sql NOT FOUND / EMPTY!"]
Missing --> HalfMigrated["5. Database Stuck in Half-Migrated State
Old Pods Cannot Serve V25 Schema -> OUTAGE!"]
end
subgraph RECOVERY ["Deterministic Symmetric Rollback (COMPLIANT)"]
direction TB
U["1. CI/CD Applies: 000025_add_docs.up.sql"] --> F["2. New App Pod Fails Health Check"]
F --> R["3. CI/CD Executes: 000025_add_docs.down.sql"]
R --> Clean["4. Schema Cleanly Reverted to V24"]
Clean --> OldPods["5. Old Pods Continue Serving Traffic Seamlessly"]
end
```
### 2.3. Irreversible Data Migrations Protocol
For historical migrations involving lossy or mathematically irreversible data transformations:
- A physical `.down.sql` file is **still mandatory**.
- The file must contain a no-op statement (`SELECT 1;`) accompanied by an approved ADR reference and suppression directive:
```sql
-- argus:ignore ARGUS-A13 ADR-0042 irreversible historical data migration
SELECT 1;
```
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus evaluates migration directory pairings and AST statement content:
```mermaid
flowchart LR
Scan["Scan Migration Directory
(db/migrations)"] --> PairCheck{"For Each .up.sql:
Does .down.sql Exist?"}
PairCheck -->|No| ReportMissing["Report CRITICAL Violation:
Missing .down.sql File"]
PairCheck -->|Yes| ReadDown["symmetry_ast.go:
Parse .down.sql AST"]
ReadDown --> EmptyCheck{"Is 0 Bytes or
No Executable SQL?"}
EmptyCheck -->|Yes| TagCheck{"Has Valid ADR
Ignore Directive?"}
TagCheck -->|No| ReportEmpty["Report HIGH Violation:
Empty / Invalid .down.sql"]
TagCheck -->|Yes| Pass["Pass (Verified ADR Exemption)"]
EmptyCheck -->|No| Pass["Pass (Valid Symmetric Pair)"]
```
1. **Pairing Matcher (`pair_matcher.go`):** Validates 1-to-1 filesystem mapping between `NNNN_name.up.sql` and `NNNN_name.down.sql`.
2. **AST Statement Validator (`symmetry_ast.go`):** Ensures `.down.sql` contains executable SQL statements using `pg_query_go`.
3. **Standalone Runner (`standalone_scanner.go`):** Independent directory auditor capable of running in CI/CD pre-commit hooks.
---
## 4. Vulnerability & Risk Taxonomy
| Failure Mode | Technical Impact | Risk Severity |
| :---------------------------- | :--------------------------------------------------------------------------------------------- | :------------ |
| **Missing `.down.sql` File** | Prevents automated rollback during failed deployments, leaving cluster in half-migrated state. | **CRITICAL** |
| **0-Byte Empty `.down.sql`** | Bypasses superficial file existence checks without providing rollback capability. | **HIGH** |
| **Comments-Only `.down.sql`** | Contains no executable SQL statements to undo schema changes. | **HIGH** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Missing Down Migration File
```
db/migrations/
├── 000001_init.up.sql
├── 000001_init.down.sql
├── 000002_add_orders.up.sql <-- Missing 000002_add_orders.down.sql!
```
### Example 2: Empty 0-Byte Down Migration
```sql
-- 000003_add_coupons.down.sql
-- (File is empty: 0 bytes)
```
### Example 3: Comments-Only Without Executable DDL
```sql
-- 000004_drop_temp_index.down.sql
-- TODO: Add rollback later when time permits
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Complete Symmetric Table Pair
```sql
-- 000001_create_accounts.up.sql
CREATE TABLE accounts (
id UUID PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0
);
-- 000001_create_accounts.down.sql
DROP TABLE IF EXISTS accounts;
```
### Solution 2: Symmetric Column Addition Pair
```sql
-- 000002_add_account_status.up.sql
ALTER TABLE accounts ADD COLUMN status VARCHAR(32) NOT NULL DEFAULT 'active';
-- 000002_add_account_status.down.sql
ALTER TABLE accounts DROP COLUMN IF EXISTS status;
```
### Solution 3: Irreversible Data Migration with ADR Reference
```sql
-- 000003_backfill_payout_hash.down.sql
-- argus:ignore-a13 ADR-0089 hash backfill is computationally irreversible
SELECT 1;
```
---
## 7. How to Suppress (Ignore Directives)
For approved irreversible data backfills documented in Architecture Decision Records:
```sql
-- argus:ignore-a13 ADR-0042 lossy data transformation irreversible
SELECT 1;
```
Alternatively, use the canonical identifier alias:
```sql
-- argus:ignore MISSING_DOWN_MIGRATION ADR-0042 irreversible data migration
SELECT 1;
```
---
## 8. Configuration Reference (`.argus.yaml`)
Enable or configure this rule in `.argus.yaml`:
```yaml
rules:
ARGUS-A13:
enabled: true
```