Squawk Documentation

website·Indexed 33 days ago

https://squawkhq.com/

A linter for Postgres migrations designed to prevent downtime and data loss. Squawk analyzes SQL migrations for dangerous operations, such as those requiring ACCESS EXCLUSIVE locks, missing timeouts, or non-robust statements, and provides guidance on safe alternatives.

Tokens
14.5K
Snippets
79
Records
104
Agent score
67%

What's inside Squawk

  1. Avoid table rewrites when adding columns with defaults in Postgres

    Adding a column with a default value can trigger a table rewrite, which requires an ACCESS EXCLUSIVE lock and blocks all reads and writes.

    • Postgres 11+: Adding a column with a non-VOLATILE default does not require a table rewrite.
    • Volatile Defaults: Adding a column with a VOLATILE default always causes a table rewrite regardless of the Postgres version.
  2. Prevent database downtime with Squawk

    Squawk helps prevent database downtime by linting schema changes to identify operations that could block reads or writes. It provides three primary ways to integrate linting into the development workflow: using the Squawk GitHub App for pull request linting, using the VSCode extension for real-time editor linting, and running the linter against schema changes.
  3. Maintain correct ordering when adding values to a PostgreSQL ENUM

    When using ALTER TYPE ... ADD VALUE in PostgreSQL, omitting the BEFORE or AFTER keywords appends the new value to the end of the enum type. This can break queries that rely on enum ordering (e.g., using the < or > operators), as new values will be considered 'greater than' all existing values regardless of their logical meaning.

    To ensure a new value is placed correctly within the existing sequence, explicitly specify its position using BEFORE 'existing_value' or AFTER 'existing_value'.

    -- Existing type
    CREATE TYPE status AS ENUM ('draft', 'submitted', 'approved', 'rejected');
    
    -- Query relying on order
    SELECT * FROM papers WHERE status < 'approved';
    
    -- INCORRECT: Appends to the end, making 'pending_review' > 'approved'
    ALTER TYPE status ADD VALUE 'pending_review';
    
    -- CORRECT: Inserts value in the specific logical position
    ALTER TYPE status ADD VALUE 'pending_review' BEFORE 'approved';
    -- OR
    ALTER TYPE status ADD VALUE 'pending_review' AFTER 'submitted';
  4. Run Squawk using Docker

    Run Squawk without a local installation using the official image from the GitHub Container Registry. To lint SQL files in the current working directory, mount the current directory to /data inside the container.
    # Assuming you want to check sql files in the current directory
    docker run --rm -v $(pwd):/data ghcr.io/sbdchd/squawk:latest *.sql
  5. Safely drop a database table

    Dropping a table can break existing clients if the application is still attempting to read or write to it. To safely remove a table, first update your application code to remove all dependencies on the table. Once the table is no longer in use, it can be permanently deleted using the DROP TABLE command. It is strongly recommended to back up the table before deletion to allow for future restoration if needed.
    DROP TABLE mytable;
  6. Write robust, idempotent database migrations

    Non-robust migrations that fail after partially applying may fail again when retried. This is common in migrations that cannot be wrapped in a transaction (e.g., those using CREATE INDEX CONCURRENTLY). To make migrations robust and idempotent, use guards like IF NOT EXISTS or IF EXISTS so that the migration can be run multiple times without erroring if some steps have already been completed.
  7. Prevent integer overflow by using BIGINT and BIGSERIAL

    Using 32-bit integer fields in database schemas can lead to hitting the maximum integer limit. To avoid this, use 64-bit integer fields instead of 32-bit ones.
  8. Add a primary key constraint without blocking reads and writes

    Adding a primary key constraint using a standard ALTER TABLE statement requires an ACCESS EXCLUSIVE lock, which blocks all reads and writes to the table while the index is being built. To avoid this downtime, create the unique index concurrently first, then attach it as a constraint. The ACCESS EXCLUSIVE lock is only held for a brief moment during the metadata update in the second step.
    -- 1. Create the unique index concurrently (allows reads and writes during build)
    CREATE UNIQUE INDEX CONCURRENTLY account_pk_idx ON account (id);
    
    -- 2. Add the constraint using the existing index (blocks reads/writes only briefly for schema update)
    ALTER TABLE account ADD CONSTRAINT account_pk PRIMARY KEY USING INDEX account_pk_idx;
  9. Avoid blocking writes when adding PostgreSQL check constraints

    By default, adding a new check constraint in PostgreSQL requires a full table scan that blocks writes to the table. To prevent this, use the NOT VALID clause when creating the constraint. This allows the constraint to be applied immediately to new rows and updates without blocking writes. Subsequently, call VALIDATE CONSTRAINT to verify existing data; this step still performs a table scan but only requires a SHARE UPDATE EXCLUSIVE lock, allowing concurrent writes to continue.
    -- 1. Add the constraint without validating existing data (non-blocking)
    ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0) NOT VALID;
    
    -- 2. Validate existing data (non-blocking for writes)
    ALTER TABLE accounts VALIDATE CONSTRAINT positive_balance;
  10. Avoid breaking clients when renaming database tables

    Renaming a database table during deployment can cause errors if multiple versions of an application are running simultaneously and the older version still depends on the original table name. To avoid this, consider renaming the object within your ORM (Object Relational Mapper) while keeping the underlying SQL table name unchanged.
  11. Prevent table blocking during PostgreSQL index deletion

    A standard DROP INDEX command acquires an ACCESS EXCLUSIVE lock on the table, which blocks all other reads and writes until the operation completes. To avoid this downtime, use the CONCURRENTLY option. This allows the table to remain accessible while PostgreSQL waits for conflicting transactions to finish.
    -- Avoid this (blocks reads and writes):
    DROP INDEX "app"."email_idx";
    
    -- Use this (allows reads and writes):
    DROP INDEX CONCURRENTLY "app"."email_idx";