sqlalign GitHub

All flags#

Generated from the parser, so it cannot fall behind it. The sections below explain the ones that need more than a line.

FlagDescription
-h, --helpshow this help message and exit
--versionprint the version and exit
--checkdo not write; exit 1 if any file would change
--stdoutwrite the result to stdout instead of the file
--diffwrite nothing; print a unified diff of what would change (exit 1 if anything would)
--dialect postgres, redshift, tsqlthe SQL dialect to parse and print. Overrides a dialect in the config file (default: postgres)
--linesformat only statements overlapping these 1-based, inclusive line numbers; the rest of the file is left byte-identical. Repeatable. Applies to one file (or stdin), since line numbers mean nothing across several
--line-ending auto, lf, crlfline endings to write: auto preserves the file's own (default)
--configuse this config file instead of discovering one
--excludeskip files matching this glob when expanding a directory (repeatable; also settable as exclude in a config file)
--isolatedignore any config file and use the built-in defaults
--show-configprint the effective settings as TOML and exit
--initwrite a commented .sqlalign.toml in the current directory and exit; every setting is commented out, so the file changes nothing until you edit it
--gui(experimental) open the settings panel with a live preview, and exit
--reportprint a coverage summary: how many statements were formatted, and what the rest declined on. Adds output without changing the mode, so on its own it still rewrites; pair it with --check to survey without writing
--max-declinesexit 1 if more than N statements are passed through unformatted (implies --report)
--lintafter formatting, run sqlfluff over the result (needs the optional sqlalign[lint] extra)
--sqlfluff-configlint with this sqlfluff config instead of the one discovered next to each file. For a shared config that lives outside the repository, which discovery cannot reach. Requires --lint
--print-sqlfluff-configprint a .sqlfluff that lets sqlfluff run alongside sqlalign without fighting it, and exit
--no-strict-configwarn on unknown config keys instead of failing
--preset compact, dbt, gitlab, house, river, trailingnamed starting point; config keys and flags layer on top
--widthcolumn the formatter tries to stay inside; 0 turns it off
--blank-lines-between-statementsforce N blank lines between every pair of statements (default: one only between two multi-line statements)
--no-alignemit one space between tokens instead of aligning them into columns (same line structure, no padding)
--no-protect-templatingdo not mask Jinja/dbt template expressions before formatting
--no-format-bodiesleave dollar-quoted ($$) procedure and function bodies untouched
--align-targetscomma-separated alignment targets to keep. Default is every target except table_names, which is opt-in because it pads the FROM/JOIN keyword out to a shared table column. Valid: aliases, case_results, column_aliases, column_constraints, column_types, join_conditions, operators, table_aliases, table_names
--comma-position leading, trailingwhere the separator comma sits in a stacked list (default: leading)
--boolean-operator-position leading, trailingwhere AND/OR sit when a predicate spans lines (default: leading)
--on-placement inline, own_linewhether a JOIN ON rides the table line or drops below it
--select-placement inline, own_linewhether the first select item rides the SELECT line
--select-indentcolumns the select list indents under --select-placement own_line (default: 2)
--body-blank-linesblank lines between the elements of a $$ body (default: 1)
--clause-keyword-align left, riverroot clause keywords flush left, or right-aligned to a river
--river-guttercolumn a river right-aligns clause keywords to (default: 6)
--keyword-case upper, lowercase for keywords, function names and types (default: upper)
--neq-style !=, <>spelling for the not-equal operator (default: !=)
--table-alias-style bare, asprint a table alias as t a (bare) or t AS a
--decimal-style NUMERIC, DECIMALspelling for the NUMERIC/DECIMAL type (default: NUMERIC)
sqlalign [OPTIONS] FILE_OR_DIRECTORY...

Every flag below is the complete set — this page is written against sqlalign --help and nothing is omitted. New to the tool? Start with Getting started; this page is for looking things up. For committing these settings to a config file rather than typing them, see Configuration.

Defaults marked (house) are what you get with no config file and no flags.

Positional arguments#

Argument Default What it does
files … required One or more files or directories. A directory is searched recursively for *.sql, in sorted order so a run is reproducible. At least one path is required, including with --show-config.
-h, --help — Print the usage summary and exit.

A single - reads stdin and writes the result to stdout, which is what an editor's format-on-save runs through a generic external-formatter setting:

cat query.sql | sqlalign -
sqlalign - --dialect tsql < query.sql

--check and --diff still report rather than write when the input is -, so sqlalign --check - is a gate on piped SQL. - cannot be combined with file arguments: stdin is read once, so that has no sensible reading.

Output mode#

By default sqlalign rewrites each file in place and prints nothing. These flags change that.

Flag Default What it does
--check off Write nothing. Print would reformat <path> for each file that is not already formatted. Exit 1 if any is.
--stdout off Write the formatted result to stdout instead of rewriting the file. Exit 0.
--diff off Write nothing. Print a unified diff of what would change. Exit 1 if anything would.
--lines START:END off Format only statements overlapping these 1-based, inclusive lines; the rest of the file comes back byte-identical. Repeatable. A bare --lines 12 is that one line. Applies to a single file or -.
--line-ending {auto,lf,crlf} auto Line endings to write. auto preserves each file's own — a CRLF file stays CRLF, and an already-formatted CRLF file does not report a spurious diff.

--check, --stdout and --diff are mutually exclusive. Passing two is an argument error, not a silent precedence rule:

sqlalign: error: argument --stdout: not allowed with argument --check

A file with lone \r (classic-Mac) line endings is not a shape sqlalign models; it is passed through untouched with a warning.

File selection#

Flag Default What it does
--exclude GLOB none Skip files matching this glob when expanding a directory. Repeatable. Matched against the path relative to the directory you named (posix separators, fnmatch semantics) and against the bare filename. Also settable as exclude in a config file.

A file you name explicitly on the command line is never excluded — naming it is a clearer signal of intent than a pattern in a config file.

Exclusions are resolved per directory argument, before per-file style resolution, from --exclude plus the exclude key of the config discovered at that directory.

Configuration#

sqlalign reads a .sqlalign.toml, or a [tool.sqlalign] table in a pyproject.toml, discovered by walking up from each file being formatted. Precedence is built-in defaults → preset → config file → command-line flags.

Flag Default What it does
--config PATH discovered Use this config file instead of discovering one.
--isolated off Ignore any config file and use the built-in defaults.
--show-config off Print the effective settings as TOML and exit 0. Formats nothing. The first line is the config file they came from, or # built-in defaults (no config file found).
--no-strict-config off Warn on unknown config keys instead of failing. By default an unknown key is a hard error (exit 2) — a typo in a committed config would otherwise mean a team believes it has a setting it does not have.
--dialect {postgres,redshift,tsql} postgres Dialect to parse and emit. CLI only — there is no dialect config key.

--show-config resolves the config against the first path you give it, so in a repo with per-directory configs, ask about the directory you care about. Its output is valid TOML you can paste straight into a config file:

sqlalign --show-config .
# built-in defaults (no config file found)
width = 100
align = true
align_targets = ["aliases", "case_results", "column_aliases", "column_constraints", "column_types", "join_conditions", "operators", "table_aliases"]
comma_position = "leading"
boolean_operator_position = "leading"
on_placement = "inline"
select_placement = "inline"
select_indent = 2
clause_keyword_align = "left"
river_gutter = 6
format_dollar_bodies = true
neq_style = "!="
decimal_style = "NUMERIC"
table_alias_style = "bare"
keyword_case = "upper"
protect_templating = true
# blank_lines_between_statements is unset: one blank line between two
# multi-line statements, none otherwise. Set an integer to force a count.

Valid config keys#

Every key below is accepted in .sqlalign.toml or [tool.sqlalign]. Anything else is an error unless you pass --no-strict-config.

align · align_targets · blank_lines_between_statements · boolean_operator_position · clause_keyword_align · comma_position · decimal_style · dialect · exclude · format_dollar_bodies · keyword_case · neq_style · on_placement · preset · protect_templating · river_gutter · select_indent · select_placement · table_alias_style · width

Formatting a selection#

$ sqlalign --lines 40:58 models/orders.sql        # just those lines
$ sqlalign --lines 12 --lines 40:58 orders.sql    # two selections
$ sqlalign --diff --lines 40:58 orders.sql        # review before writing

The unit is the statement, not the line. Half a statement does not parse, so a range starting or ending inside one formats that statement entire — which is also what a dragged editor selection means. A range that covers only blank lines selects nothing and the file is left alone.

Everything outside the range is byte-identical, including the blank lines around it. Asking for line 12 is not asking for the spacing at line 40 to be normalised.

The use this exists for is adopting sqlalign on a repository nobody wants to reformat in one commit: format the lines your change touches, leave the rest, and the review stays about your change. For an editor filter that hands over just the selected text — a visual selection piped through :! in vim — use stdin instead: sqlalign - formats what it is given and needs no line numbers.

--line-ending is the only setting with no config key. Three of the four --no-* flags map onto the positive keys: --no-align is align = false, --no-protect-templating is protect_templating = false, and --no-format-bodies is format_dollar_bodies = false. The fourth, --no-strict-config, is not a style setting at all — it changes how the config file itself is read.

Style#

Flag Default What it does
--preset {compact,dbt,gitlab,house,river,trailing} none (the house defaults) Named starting point. Sets a base that config keys and flags then layer on top of, so --preset compact --comma-position trailing means both. Not passing it leaves any preset key in your config file in force.
--width WIDTH 100 Target line width for wrapping decisions. Not a hard cap: a construct anchored deep in an indent gets a floor of anchor + 60, plus 5 characters of grace, so alignment is never sacrificed to shave two columns.
--blank-lines-between-statements N unset Force N blank lines between every pair of statements. Unset is the house rule: exactly one blank line between two multi-line statements and none otherwise, so a run of one-line GRANTs stays a block. 0 removes them all.
--no-align off (aligned) Emit one space between tokens instead of padding them into columns. Same line structure, no padding — this is what 9 of 10 published SQL style guides call for.
--align-targets a,b,… all but table_names Comma-separated alignment columns to keep. Anything left out collapses to a single space. See the table below.
--comma-position {leading,trailing} leading (house) Where the separator comma sits in a stacked list — select items, GROUP BY/ORDER BY terms, INSERT columns, UPDATE SET assignments, CREATE TABLE columns, window terms.
--boolean-operator-position {leading,trailing} leading (house) Where AND/OR sit when a predicate spans lines. The condition column is identical either way — only the operator moves.
--on-placement {inline,own_line} inline (house) Whether a JOIN's ON rides the table line or drops below it. own_line retires the FROM-block-wide ON column — there is no longer an ON after each alias to align — so join_conditions has nothing to act on.
--keyword-case {upper,lower} upper (house) Case for keywords, function names and type names. Your identifiers and string literals are never touched.
--neq-style {!=,<>} != Spelling for the not-equal operator.
--decimal-style {NUMERIC,DECIMAL} NUMERIC Spelling for the NUMERIC/DECIMAL type.
--no-protect-templating off (protected) Do not mask Jinja/dbt template expressions ({{ … }}, {% … %}, {# … #}) before formatting. With protection on, each expression becomes a same-width placeholder, so alignment is computed against the real text width.
--no-format-bodies off (formatted) Leave dollar-quoted ($$) procedure and function bodies untouched. On by default; off passes the whole CREATE FUNCTION/PROCEDURE through byte-identical.

--neq-style and --decimal-style exist because sqlglot's parser collapses each of those pairs into a single AST node, so a spelling has to be chosen when printing. They are the only two places sqlalign picks for you.

Presets#

Preset Sets
house nothing — the built-in columnar default: aligned, leading separators, ON inline, uppercase keywords
compact align = false
trailing comma_position = "trailing", boolean_operator_position = "trailing"
dbt keyword_case = "lower", comma_position = "trailing", align = false

dbt ships with its one remaining deviation stated: dbt indents nested blocks 4, sqlalign indents a CTE body 2.

Alignment targets#

Target Aligns
aliases AS x in a select list, and table aliases in FROM/JOIN
operators =, !=, <, LIKE, IS … in WHERE/ON/HAVING/CASE
join_conditions the ON/AND column across a whole FROM block
case_results THEN in a short-form CASE
column_types column types in CREATE TABLE
column_constraints NOT NULL/DEFAULT, and Redshift ENCODE
column_aliases the alias column alone, inside a select list
table_aliases the alias column alone, across a FROM/JOIN block
table_names table names padded to a shared column — opt-in, not in the default set

An unknown name is an error, not a silent no-op:

sqlalign: unknown align_targets ['alias']; valid: ['aliases', 'case_results', 'column_aliases', 'column_constraints', 'column_types', 'join_conditions', 'operators', 'table_aliases', 'table_names']

--no-align is the shorthand for switching every target off.

Exit codes#

Code Meaning
0 Success, including --check/--diff finding nothing to change, and including statements that passed through untouched with a warning.
1 Something a gate should fail on: --check or --diff found a file that would change, --max-declines was exceeded, or --lint reported findings.
2 Unreadable file, invalid config file, invalid argument, or an unexpected engine error on one file.
141 The reader closed the pipe (sqlalign - | head). The shell's spelling of death-by-SIGPIPE; not an error.

Per-file failures do not abort the run: the file is reported on stderr and skipped, the rest still process, and the worst code seen is returned.

sqlalign --check nope.sql still.sql
# sqlalign: [Errno 2] No such file or directory: 'nope.sql'
# would reformat still.sql
# exit 2

Worked invocations#

Each block below is real output. The input for the style examples is tests/fixtures/input/13.sql; the default rendering of it is in the README.

Adopt the line structure without the padding#

sqlalign --stdout --no-align 13.sql
-- #13: multi-character aliases (mixed lengths)
SELECT cust.customer_id
     , cust.email
     , ord.order_id
     , ord.total
     , line_items.product_id
     , line_items.quantity
     , addr.city
FROM customers cust
INNER JOIN orders ord ON ord.customer_id = cust.customer_id
LEFT JOIN order_line_items line_items ON line_items.order_id = ord.order_id
LEFT JOIN shipping_addresses addr ON addr.order_id = ord.order_id
  AND addr.address_type = 'shipping'
WHERE ord.order_date >= '2026-07-01'
  AND cust.segment = 'enterprise';

Keep the alignment, move the separators to end of line#

sqlalign --stdout --preset trailing 13.sql
-- #13: multi-character aliases (mixed lengths)
SELECT cust.customer_id,
       cust.email,
       ord.order_id,
       ord.total,
       line_items.product_id,
       line_items.quantity,
       addr.city
FROM customers               cust
INNER JOIN orders            ord        ON ord.customer_id     = cust.customer_id
LEFT JOIN order_line_items   line_items ON line_items.order_id = ord.order_id
LEFT JOIN shipping_addresses addr       ON addr.order_id       = ord.order_id AND
                                           addr.address_type   = 'shipping'
WHERE ord.order_date >= '2026-07-01' AND
      cust.segment    = 'enterprise';

Drop ON to its own line#

Note what this costs: the ON conditions are now justified within each join instead of into one column spanning the whole FROM block.

sqlalign --stdout --on-placement own_line 13.sql
-- #13: multi-character aliases (mixed lengths)
SELECT cust.customer_id
     , cust.email
     , ord.order_id
     , ord.total
     , line_items.product_id
     , line_items.quantity
     , addr.city
FROM customers               cust
INNER JOIN orders            ord
   ON ord.customer_id     = cust.customer_id
LEFT JOIN order_line_items   line_items
   ON line_items.order_id = ord.order_id
LEFT JOIN shipping_addresses addr
   ON addr.order_id       = ord.order_id
  AND addr.address_type   = 'shipping'
WHERE ord.order_date >= '2026-07-01'
  AND cust.segment    = 'enterprise';

Lowercase everything#

sqlalign --stdout --keyword-case lower 13.sql
-- #13: multi-character aliases (mixed lengths)
select cust.customer_id
     , cust.email
     , ord.order_id
     , ord.total
     , line_items.product_id
     , line_items.quantity
     , addr.city
from customers               cust
inner join orders            ord        on ord.customer_id     = cust.customer_id
left join order_line_items   line_items on line_items.order_id = ord.order_id
left join shipping_addresses addr       on addr.order_id       = ord.order_id
                                       and addr.address_type   = 'shipping'
where ord.order_date >= '2026-07-01'
  and cust.segment    = 'enterprise';

Identifiers and string literals are untouched — the pass is AST-driven.

Format a dbt model#

--preset dbt is keyword_case = "lower" + trailing commas + no padding. Jinja survives because it is masked with same-width placeholders before parsing.

sqlalign --stdout --preset dbt orders.sql
select o.id,
       o.total,
       c.email
from {{ ref('orders') }} o
join {{ ref('customers') }} c on c.id = o.customer_id
where o.status = 'complete';

Give a long window function room#

--width moves the wrapping threshold. Same query, two widths:

sqlalign --stdout wide.sql            # default width 100
SELECT customer_id
     , SUM(total) OVER (PARTITION BY customer_id ORDER BY order_date
                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM orders;
sqlalign --stdout --width 200 wide.sql
SELECT customer_id
     , SUM(total) OVER (PARTITION BY customer_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM orders;

Lowering --width below the default often changes nothing, because the anchor + 60 floor keeps a deeply-indented construct readable regardless.

Space out a migration script#

sqlalign --stdout --blank-lines-between-statements 1 multi.sql
TRUNCATE TABLE staging_orders;

TRUNCATE TABLE staging_customers;

SELECT a
     , b
FROM t
WHERE a = 1
  AND b = 2;

SELECT c
FROM u;

With the flag unset, the two TRUNCATEs stay packed together and only the multi-line statements get air:

TRUNCATE TABLE staging_orders;
TRUNCATE TABLE staging_customers;
SELECT a
     , b
FROM t
WHERE a = 1
  AND b = 2;

SELECT c
FROM u;

Gate a repository in CI#

sqlalign --check --exclude 'vendor/*' --exclude '*.gen.sql' .
would reformat models/marts/rollup.sql
would reformat models/users.sql

Exit 1. Swap --check for --diff when you want the log to show the change rather than only the filename.