# Squawk: a Postgres linter that catches migrations which lock the table

> Squawk is a Rust linter for PostgreSQL migrations and SQL that warns about statements which take heavy locks, and it ships as an npm package, a PyPI package, a Docker image, a VS Code extension and a WASM playground. Rules can be excluded by flag, config file or comment.

**sbdchd/squawk** — 🐘 Linter for Postgres migrations & SQL

- Repository: https://github.com/sbdchd/squawk
- Website: https://squawkhq.com
- Stars: 1,229 · Forks: 74
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/sbdchd-squawk

## The problem is downtime, not style

Squawk is a linter for Postgres migrations and SQL, and the reason it exists is stated in two lines: to prevent unexpected downtime caused by database migrations and to encourage best practices around Postgres schemas and SQL.

That framing is what separates it from a general SQL style checker. The rules are about what a statement does to a live table, and the example output in the README makes that concrete.

The `prefer-bigint-over-int` rule warns that using 32-bit integer fields can result in hitting the max `int` limit. The `prefer-identity` rule warns that serial types make schema, dependency and permission management difficult, and shows the replacement as an `integer generated by default as identity` column. The `prefer-text-field` rule is the one that matters most for a busy table, since changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock that will prevent all reads and writes to the table, and the fix is to use a `TEXT` field with a `CHECK` constraint.

The other two in the example are about locks during schema changes. `require-concurrent-index-creation` points out that during normal index creation, table updates are blocked even though reads are still allowed, and suggests using `concurrently`. `constraint-missing-not-valid` notes that new constraints require a table scan by default and block writes while that scan occurs, with the fix being `NOT VALID` followed by a later `VALIDATE CONSTRAINT` call. And `disallowed-unique-constraint` says that adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes while the index is built, suggesting creating an index `CONCURRENTLY` and then creating the constraint using the index.

Every one of these is a statement that works fine in a test database and takes a table offline in production. That is the whole point.

## Four ways to run it

Squawk is distributed in more shapes than most linters, and each one exists because of a different constraint.

The npm package is the primary distribution, and the Rust binary is shipped inside it as a platform-specific optional dependency:

```shell
npm install -g squawk-cli
```

Those optional dependencies are named per platform, `@squawk-cli/darwin-arm64`, `@squawk-cli/linux-x64-musl` and so on, each pinned to the same version as the package, so the npm install pulls the right compiled binary for your machine rather than compiling anything.

There is also a PyPI package:

```shell
pip install squawk-cli
```

Which is the route to take in a Python environment where installing a global npm package is not wanted.

The Docker image is published on GitHub Container Registry, and the invocation is short:

```shell
docker run --rm -v $(pwd):/data ghcr.io/sbdchd/squawk:latest *.sql
```

The volume mount is what makes that work, since it gives the container your files as the working directory.

And there are two in-browser options. The playground at play.squawkhq.com is a WASM build, so you can check SQL locally in the browser without sending it anywhere, and there is a VS Code extension so the check happens as you type. The playground is the more interesting of the two for a security-minded team, because a WASM build means the SQL never leaves the browser.

For continuous integration there is a GitHub Action, squawk-action, and a GitHub app for a more integrated setup.

## Turning rules off, three ways

A linter that cannot be silenced is a linter teams route around, and Squawk gives three mechanisms for turning rules off, which is the sign of a tool that expects to be used on an existing codebase.

The first is the command line flag. Individual rules can be disabled via `--exclude`:

```shell
squawk --exclude=adding-field-with-default,disallowed-unique-constraint example.sql
```

The second is a comment in the SQL itself, which is the one that scales best, because the decision is recorded next to the statement it applies to:

```sql
-- squawk-ignore ban-drop-column
alter table t drop column c cascade;
```

Multiple rules can be ignored with a comma-separated list, and to ignore a rule for the entire file there is `squawk-ignore-file`:

```sql
-- squawk-ignore-file ban-drop-column
alter table t drop column c cascade;
-- also ignored!
alter table t drop column d cascade;
```

Leaving the rule names off the `squawk-ignore-file` comment ignores all rules for that file.

The third is a configuration file. By default Squawk traverses up from the current directory to find a `.squawk.toml` file, and you can point at a different one with `-c` or `--config`:

```shell
squawk --config=~/.squawk.toml example.sql
```

The `--exclude` flag always takes priority over the configuration file, which is the behaviour you want for a one-off override in a pipeline.

The `--include` flag works the other way, enabling opt-in rules that are disabled by default, with an example of `--include=require-table-schema`. Rules listed in `--exclude` take precedence over `--include`, so an exclusion always wins.

## The options that reflect a real CI environment

The help output is where the design decisions show, and a few options are worth calling out.

`--pg-version` lets you specify the Postgres version, for example `--pg-version=13.0`. That matters because some rules depend on the server version, and checking SQL written for Postgres 16 against a Postgres 13 target produces warnings that do not apply to you.

`--assume-in-transaction` assumes that a transaction will wrap each SQL file when run by a migration tool, with `--no-assume-in-transaction` to override a config file that sets it. Whether a migration runs inside a transaction changes what a failing statement does, and a lint that assumes the wrong answer gives advice that does not hold in your setup.

The `--reporter` option takes `tty`, `gcc`, `json` or `gitlab`. The `gitlab` reporter is the one that shows the tool was built with a specific CI system in mind, since it emits findings in the format GitLab's code quality reports expect, and the `json` reporter is what you would use to build a custom dashboard.

`--exclude-path` takes patterns to skip, and the examples are given as both a concrete filename and a glob like `--exclude-path='*user_ids.sql'`. `--no-error-on-unmatched-pattern` stops the tool failing when a path pattern matched nothing, which is the flag you need when your path list comes from a glob and one branch of it does not exist.

There are two subcommands beyond the default lint. `squawk server` runs the language server, which is what the VS Code extension talks to, and `squawk upload-to-github` comments on a pull request with the results.

For debugging the parser itself, `--debug` takes `lex`, `parse` or `ast` as a format, and `--stdin-filepath` sets the path used in reporting when the input comes from stdin.

## The Rust workspace and the packaging split

The Rust side is a Cargo workspace with members under crates/, on edition 2024 with a rust-version floor of 1.99, and the licence is Apache-2.0 OR MIT.

The dependency list shows a compiler rather than a wrapper. `rowan` and `salsa` are there, which are the crates for lossless syntax trees and incremental computation, and `ungrammar` is a grammar description language, which is how a SQL grammar is usually defined for a parser generator. `gen-lsp-types` and `lsp-server` are for the language server, and `wasm-bindgen`, `web-sys` and `serde-wasm-bindgen` are for the playground build, with `wasm-bindgen-test` for testing it.

The reporting side uses `annotate-snippets` for the pretty output, `tabled` for the tables, `tiny_pretty` for the small formatting, `clap` with derive for the CLI, and `insta` for snapshot testing, which is a good sign for a linter whose output is a large part of its contract.

The npm side is a thin wrapper rather than a JavaScript port. The package `squawk-cli` declares a `bin` entry pointing at `js/bin/squawk`, and the `files` array contains only `js/bin/squawk` and `js/index.js`, with the real work happening in the platform-specific optional dependency. The dev dependencies there are modest and pinned to older major versions, and the package uses volta to pin Node 20.19.0 and pnpm 9.15.4, with `packageManager` set to pnpm 9.15.4.

There are a couple of other signs of a project that has been running for a while. The `postgres/` directory is presumably a real Postgres instance for differential testing, and `.kodiak.toml` is the configuration for Kodiak, the GitHub app that automates pull request chores.

The Dockerfile is a two-stage build, compiling in `rust:1-slim-bookworm` and running in `debian:bookworm-slim` as a non-root `squawk` user, with the binary copied to `/usr/local/bin/squawk` and the working directory set to `/data` so the volume mount lines up with the documented invocation.

## Conclusion

Squawk fits a team running PostgreSQL in production that wants a fast static check on migration files before they reach a database, and that can afford to write its own rules for the conventions it has not yet encoded. It does not fit a team wanting deep semantic analysis, since it reads SQL as text and does not know your data, and its value depends entirely on the rule set. Before adopting it, run `squawk` over an existing migrations directory to see which of your past migrations would now fail, and read the rules documentation at squawkhq.com/docs/rules before deciding which rules to exclude.

## FAQ

### What is Squawk?

It is a linter for PostgreSQL migrations and SQL, written in Rust, whose purpose is to prevent unexpected downtime caused by database migrations and encourage best practices around Postgres schemas and SQL. The project is licensed Apache-2.0 OR MIT.

### How do I install Squawk?

With `npm install -g squawk-cli` or `pip install squawk-cli`, or by downloading a binary from the releases page. There is also a Docker image at ghcr.io/sbdchd/squawk, a WASM playground at play.squawkhq.com and a VS Code extension.

### How do I disable a Squawk rule?

With the `--exclude` flag on the command line, with a `-- squawk-ignore` or `-- squawk-ignore-file` comment inside the SQL, or in a `.squawk.toml` configuration file that Squawk finds by traversing up from the current directory. The `--exclude` flag takes priority over the config file.

### What does the require-concurrent-index-creation rule warn about?

That during normal index creation, table updates are blocked even though reads are still allowed. The suggested fix is to use `concurrently` to avoid blocking writes.

### Can Squawk report findings in a format other than the terminal output?

Yes. The `--reporter` option takes tty, gcc, json or gitlab, and there is a `squawk upload-to-github` subcommand that comments on a pull request with the results, plus a GitHub Action and a GitHub app for CI integration.

## Sources

- [License: Apache-2.0](https://github.com/sbdchd/squawk/blob/master/LICENSE)
- [Project website](https://squawkhq.com)
- [README](https://github.com/sbdchd/squawk/blob/master/README.md)
- [Releases](https://github.com/sbdchd/squawk/releases)
- [sbdchd/squawk on GitHub](https://github.com/sbdchd/squawk)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sbdchd-squawk
