# ARGUS-A11: Destructive Schema Migrations & Zero-Downtime Violations > **Rule Code:** `ARGUS-A11` > **Identifier:** `DESTRUCTIVE_MIGRATION` > **Severity:** `CRITICAL` (Rolling Deployment Crash & Full Table Rewrite Blocker) > **Category:** `Schema Evolution, Zero-Downtime DDL & Deployment Safety` > **Target Standards:** CWE-400 (Uncontrolled Resource Consumption), Operational Zero-Downtime Standards, OWASP ASVS v4.0.3/v5.0 §V1.4.3 --- ## 1. Overview & Core Invariant Database schema evolution across migration files (`db/migrations/`) **must adhere to the Expand and Contract (Parallel Run) zero-downtime paradigm**. Directly destructive DDL operations are strictly prohibited in `.up.sql` migration scripts: - **`DROP COLUMN`** - **`RENAME COLUMN`** - **`RENAME TABLE`** - **`ALTER COLUMN TYPE`** (in-place column type modification) - **`ADD COLUMN ... NOT NULL`** without a `DEFAULT` clause - **`TRUNCATE TABLE`** All breaking schema changes must be split into non-destructive expand phases, application transition phases (dual-write and backfill), and contract phases across separated release cycles. --- ## 2. Technical Grounding & PostgreSQL Engine Realities ### 2.1. Full Table Heap Rewrite & Lock Starvation In PostgreSQL: - Executing `ALTER COLUMN TYPE` (e.g. converting `VARCHAR(50)` to `INT`, or `INT` to `BIGINT`) or adding a `NOT NULL` column without a default value **forces PostgreSQL to rewrite every physical table tuple on disk (_full heap rewrite_)** under an **`ACCESS EXCLUSIVE`** lock. - On large production tables, this operation holds exclusive locks for minutes to hours. During this lock window, **all application reads (`SELECT`) and writes (`INSERT`/`UPDATE`) are queued and blocked completely**. ### 2.2. Rolling Deployment Crashes (SQLSTATE 42703) In modern rolling deployment architectures (Kubernetes): 1. **Active Pre-Migration Pods:** Pods running Version 1 (V1) remain active serving traffic while the CI/CD pipeline runs the migration for Version 2 (V2). 2. **Prepared Statement Cache:** Driver connections on V1 maintain cached generic prepared statements expecting column `phone` (e.g., `SELECT id, name, phone FROM users`). 3. **Immediate Crash on Column Drop:** When migration V2 executes `ALTER TABLE users DROP COLUMN phone;`, V1 pods immediately throw runtime fatal errors: ``` ERROR: column "phone" of relation "users" does not exist (SQLSTATE 42703) ``` This triggers widespread service outages before V2 pods finish spinning up. ```mermaid flowchart TD subgraph VULNERABLE ["Destructive Single-Step Drop (OUTAGE)"] direction TB V1["Active Pod V1: Serving traffic with SELECT id, name, phone"] --> PG["PostgreSQL"] Deploy["CI/CD Executes: ALTER TABLE users DROP COLUMN phone"] --> PG PG --> Crash["Pod V1 Throws SQLSTATE 42703 (Column Not Found)"] Crash --> Outage["Immediate Production Outage (CWE-400)"] end subgraph EXPAND_CONTRACT ["5-Phase Expand-and-Contract Protocol (COMPLIANT)"] direction TB P1["Phase 1 (Expand): Add new column (nullable or with default) in Release N"] P2["Phase 2 (Dual-Write): Deploy App Release N (writes both old & new columns)"] P3["Phase 3 (Backfill): Background worker backfills legacy rows"] P4["Phase 4 (Read-Switch): Deploy App Release N+1 (reads exclusively from new column)"] P5["Phase 5 (Contract): Drop old column in Release N+2 with -- argus:contract tag"] P1 --> P2 --> P3 --> P4 --> P5 end ``` --- ## 3. How Argus Detects Violations (Static Analysis Architecture) Argus inspects all `.up.sql` migration files using pure PostgreSQL AST parsing: ```mermaid flowchart LR Scan["Scan .up.sql Migrations
(Exclude .down.sql)"] --> Parse["ddl_ast_walker.go:
pg_query_go AST Inspection"] Parse --> CheckDrop{"Drop Table / Drop Column?"} Parse --> CheckRename{"Rename Table / Column?"} Parse --> CheckAlter{"Alter Column Type?"} Parse --> CheckNotNull{"ADD COLUMN NOT NULL
Without DEFAULT?"} CheckDrop -->|Yes| TagCheck{"Has -- argus:contract Tag?"} CheckRename -->|Yes| Report["Report CRITICAL Violation:
Destructive Migration Operation"] CheckAlter -->|Yes| Report CheckNotNull -->|Yes| Report TagCheck -->|No| Report TagCheck -->|Yes| Pass["Pass (Verified Contract Phase)"] CheckDrop -->|No| Pass ``` 1. **AST Statement Inspection (`ddl_ast_walker.go`):** Identifies `DropStmt`, `TruncateStmt`, `RenameStmt`, and `AlterTableCmd` (`AT_DropColumn`, `AT_AlterColumnType`, `AT_AddColumn` with `CONSTR_NOTNULL` and no `CONSTR_DEFAULT`). 2. **Contract Phase Tag Verification (`contract_tag.go`):** Validates `-- argus:contract ` comments immediately preceding legitimate contract-phase drop statements. 3. **Standalone Scanner (`standalone_runner.go`):** Provides independent migration directory validation capable of running in pre-commit hooks. --- ## 4. Vulnerability & Risk Taxonomy | Failure Mode | Technical Impact | Risk Severity | | :--------------------------------------- | :------------------------------------------------------------------------------------------- | :------------ | | **`DROP COLUMN` in Single Release** | Breaks generic prepared statements on active rolling-deployment pods with SQLSTATE `42703`. | **CRITICAL** | | **`ALTER COLUMN TYPE` In-Place** | Triggers full table heap rewrite under `ACCESS EXCLUSIVE` lock, freezing production traffic. | **CRITICAL** | | **`ADD COLUMN ... NOT NULL` No Default** | Table modification fails immediately on existing tables and locks writes. | **HIGH** | | **`TRUNCATE TABLE` in Up Migration** | Irrevocably purges live application data during schema deployment. | **CRITICAL** | --- ## 5. Non-Compliant Code Patterns (Bad Examples) ### Example 1: Direct Column Deletion in Up Migration ```sql -- VIOLATION: Breaks active application pods executing prepared queries -- 000042_remove_legacy_phone.up.sql ALTER TABLE users DROP COLUMN phone; ``` ### Example 2: In-Place Column Type Alteration ```sql -- VIOLATION: Forces full table rewrite under AccessExclusiveLock -- 000043_upgrade_balance_type.up.sql ALTER TABLE accounts ALTER COLUMN balance TYPE BIGINT; ``` ### Example 3: Adding NOT NULL Without Default ```sql -- VIOLATION: Fails on non-empty tables and locks relation -- 000044_add_user_email.up.sql ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL; ``` --- ## 6. Compliant Implementation Patterns (Good Examples) ### Solution 1: Safe Expand Step (Column Addition with Default) ```sql -- COMPLIANT: Instant metadata update in PostgreSQL without heap rewrite -- 000042_add_user_email_expand.up.sql ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT ''; ``` ### Solution 2: Verified Contract Step with Annotation ```sql -- COMPLIANT: Verified contract phase drop after all old pods have been retired -- 000045_cleanup_legacy_phone.up.sql -- argus:contract release_v2_cleanup ALTER TABLE users DROP COLUMN phone; ``` ### Solution 3: Safe Table Creation ```sql -- COMPLIANT: Standard table initialization -- 000001_init_schema.up.sql CREATE TABLE users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` --- ## 7. How to Suppress (Ignore Directives) For one-off temporary tables or pre-production test harness scripts: ```sql -- argus:ignore-a11 temporary staging test table cleanup DROP TABLE temp_audit_staging; ``` Alternatively, use the canonical identifier alias: ```sql -- argus:ignore DESTRUCTIVE_MIGRATION sandbox environment reset script TRUNCATE TABLE sandbox_mock_data; ``` --- ## 8. Configuration Reference (`.argus.yaml`) Enable or configure migration directory paths in `.argus.yaml`: ```yaml rules: ARGUS-A11: enabled: true ```