Configuration Reference
Project Configuration (dlab.json)
dfmt setup writes dlab.json at your project root, and dfmt options --set changes one option in it. dfmt finds it by walking up from the file it formats.
{
"dialect": "postgres",
"include": [
"**.sql",
"**.pgsql",
"**.pg"
],
"exclude": [],
"dfmt": {
"keywordCase": "preserve",
"identifierCase": "preserve",
"functionCase": "like_keyword",
"dataTypeCase": "like_keyword",
"indentWidth": 4,
"maxLineWidth": 80,
"commaStyle": "trailing",
"andOrPosition": "before",
"clauseAlignment": "left",
"joinPlacement": "clause",
"routineBodies": "all",
"linesBetweenStatements": 1,
"preserveExtraBlankLinesBetweenStatements": false,
"collapseShortStatements": true,
"align": "",
"blankLines": "keep_one",
"blankLineBetweenCtes": true,
"blankLineBetweenCaseArms": true,
"listLayout": "one_per_line",
"clarifyPrecedence": false,
"cteNamesOnNewLine": true,
"preserveBreaks": "off",
"statementTerminator": ""
}
}
Precedence (lowest to highest): defaults → dlab.json "dfmt" → dlab.json dialect section. No option is a command-line flag: what a project chooses holds for every run, everyone's and CI's.
Per-dialect options
A key under "dfmt" named after a dialect is a section of options for that dialect only. It overrides the entries beside it when that dialect is formatted and is ignored otherwise, so one dlab.json can carry sections for every dialect a repository holds. Here Oracle files format to 80 columns and everything else to 100:
{
"dialect": "snowflake",
"dfmt": {
"maxLineWidth": 100,
"keywordCase": "lower",
"oracle": { "maxLineWidth": 80 },
"snowflake": { "keywordCase": "upper" }
}
}
The section name is any dialect name dfmt knows, aliases included (a timescaledb section is a postgres section). A section for a dialect dfmt does not know, or an option inside a section that is not an option, is an error with the same suggestion a mistyped key gets at the top level. Sections do not nest, and a dialect may have only one.
CLI usage:
dfmt setup # Set the project up, or tune it, on your own files
dfmt setup --dialect postgres # Create a one-dialect dlab.json without asking
dfmt options --set keywordCase=upper # Record one option
dfmt . # Dry run: report what would change, write nothing
dfmt diff . # Show what would change
dfmt write . # Format in place
dfmt check . # Exit 1 if anything needs formatting
Formatting Options
Casing
keywordCase
Type: upper | lower | preserve
Default: preserve
Description: Controls the casing of SQL keywords (SELECT, FROM, WHERE, JOIN, etc.). preserve (default) leaves them as written. Function names and data types follow it unless functionCase or dataTypeCase says otherwise; names never do.
identifierCase
Type: upper | lower | preserve
Default: preserve
Description: Controls the casing of unquoted names (tables, columns, aliases). preserve (default) is recommended: a name is an object in your database, and re-casing it can rename a result column (DuckDB, Spark, Trino and SQL Server keep the spelling you wrote) or miss a JSON key. When set, it is applied only where the engine compares every kind of name without regard to case, and only to the letters it folds. It is ignored, with a note that names the reason, on an engine that compares any kind of name exactly as written — BigQuery, ClickHouse, and the MySQL-family engines except TiDB, which is measured insensitive at every role — and on an engine whose rules nobody has measured. Quoted names are never re-cased.
functionCase
Type: like_keyword | upper | lower | preserve
Default: like_keyword
Description: Controls the casing of function names (count, sum, coalesce, current_date). like_keyword (default) cases them as keywordCase does; upper, lower or preserve cases them on their own. A quoted function name is never re-cased. Ignored on ClickHouse, where toString is a function and tostring is not, because re-casing there would change the query rather than its presentation.
dataTypeCase
Type: like_keyword | upper | lower | preserve
Default: like_keyword
Description: Controls the casing of data type names (INT, VARCHAR, BOOLEAN, NUMERIC, etc.), including nested types and typed literals such as DATE '2020-01-01'. like_keyword (default) cases them as keywordCase does; upper, lower or preserve cases them on their own. Ignored on ClickHouse, where CAST(1 AS uint8) is an unknown data type family and UInt8 is the type.
Width and indentation
indentWidth
Type: integer (1-8)
Default: 4
Description: Number of spaces per indentation level (used for subqueries, CASE branches, continuation lines).
maxLineWidth
Type: integer (0-200)
Default: 80
Description: Target maximum line width. Constructs that do not fit are broken across lines; 0 turns width-based breaking off, so no line is ever too wide and nothing breaks for width alone. It does NOT keep the line breaks you wrote — the formatter still lays every clause out on its own line, and the one-line collapse of short queries goes off with the width it was measured against. preserveBreaks is the option that keeps an author's breaks.
Layout
clauseAlignment
Type: left | right
Default: left
Description: Where a query's clause keywords stand once its clauses come apart. LEFT: every keyword begins at the query's column. RIGHT: every keyword ENDS at one column, set by the widest of them (the "river" of The Art of PostgreSQL), and the bodies begin one column right of it. A query that fits on one line keeps it either way.
joinPlacement
Type: clause | from_list
Default: clause
Description: Where a join stands once the query comes apart. CLAUSE: a join is a clause of the query, beside FROM and WHERE. FROM_LIST: a join continues the FROM it belongs to, one per line in the column FROM's body takes (under clauseAlignment: right, one column right of the river).
routineBodies
Type: all | sql | as_written
Default: all
Description: Which routine bodies are read and laid out, each in the language its routine declares. ALL: SQL and PL/pgSQL bodies. SQL: SQL bodies only, a PL/pgSQL body is kept as written. AS_WRITTEN: every body is kept as written.
align
Type: any of aliases | case_arms | assignments | comparisons | named_arguments | column_definitions, comma-separated, or all
Default: ``
Description: Which CONSTRUCTS line up in columns down their sibling rows — any number of them, or "all" for every one there is now and later. Empty aligns nothing. A construct lines up only where two of its rows each start a line of their own, so nothing happens until maxLineWidth has broken the list apart. Independent of preserveBreaks: breaks_and_alignment, which keeps alignment the author wrote for any construct, whatever this names.
listLayout
Type: one_per_line | packed
Default: one_per_line
Description: Controls how items in a broken comma-separated list arrange on lines. ONE_PER_LINE: all-or-nothing — a list fits on one line or breaks one item per line. PACKED: paragraph-wrap, pack items greedily onto lines.
preserveBreaks
Type: off | breaks_only | breaks_and_alignment
Default: off
Description: Controls how the formatter reads line breaks in the user's original SQL. off: pure width-only formatting. breaks_only: preserve newlines between list items. breaks_and_alignment: breaks_only, and the author's alignment stays where they padded it: sibling rows whose markers (AS, =, a CASE's THEN) the author lined up stay lined up, and a table's column definitions whose names and types the author lined up keep those two columns aligned, every row of the group padded; a blank line or a comment between two columns starts a new group. A row that padding would push past maxLineWidth drops the padding that does not fit.
Separators
commaStyle
Type: trailing | leading
Default: trailing
Description: Where the comma sits once a list has broken: at the end of the line (trailing) or at the head of the next one (leading). Leading indents the first item by the comma's width, so every item's text starts in one column.
andOrPosition
Type: before | after
Default: before
Description: Controls where AND/OR operators are placed relative to line breaks. BEFORE: AND/OR starts a new line. AFTER: AND/OR ends the line, continuation on next line.
Blank lines
linesBetweenStatements
Type: integer (0-5)
Default: 1
Description: Number of blank lines between statements when formatting multiple statements. 0 means just a newline, 1 means one blank line, etc.
preserveExtraBlankLinesBetweenStatements
Type: true | false
Default: false
Description: When enabled, gaps between top-level statements that exceed linesBetweenStatements blank lines in the source are preserved (capped by the source count). When disabled, every gap collapses to exactly linesBetweenStatements blank lines.
blankLines
Type: keep_one | keep_all | none
Default: keep_one
Description: What happens to the blank lines you wrote. keep_one (default): a blank line is grouping — it survives, collapsed to one per gap, and is never affected by preserveBreaks. keep_all: a blank run survives with its count — three empty lines stay three. none: your blank lines are discarded; only blanks other options ask for (linesBetweenStatements, blankLineBetweenCtes, blankLineBetweenCaseArms) appear.
blankLineBetweenCtes
Type: true | false
Default: true
Description: Writes a blank line between the CTEs of a WITH clause, once that clause has broken over several lines — a WITH short enough to fit on one line stays on one line. On by default. What it controls is only the ADDING: an empty line you wrote between two CTEs yourself is kept by blankLines either way, and a line this wrote is one of yours the next time the file is read, so switching it off later does not take it out again. Forced off inside a client-script submission whatever it is set to, because there a blank line ends the statement.
blankLineBetweenCaseArms
Type: true | false
Default: true
Description: Writes a blank line between every arm of a CASE once two or more of its arms each take more than one line, so a reader can see where one arm ends and the next begins; a CASE whose arms fit on a line each is left alone. On by default. Like blankLineBetweenCtes it controls only the ADDING: an empty line you wrote between two arms is kept by blankLines either way, and a line this wrote is one of yours the next time the file is read. Forced off inside a client-script submission whatever it is set to, because there a blank line ends the statement.
Structure
collapseShortStatements
Type: true | false
Default: true
Description: When enabled, a query that fits within maxLineWidth stays on one line — a whole statement, and a subquery, derived table or CTE body alike. When disabled, every query takes a line per clause, and a bracketed one always opens.
clarifyPrecedence
Type: true | false
Default: false
Description: Parenthesises AND groups inside OR, so SQL's precedence is visible rather than remembered: a OR b AND c → a OR (b AND c). Off by default, because it is the one option that adds tokens you did not write, and it is switched off entirely for a file whose parse recovered. Set operations are left alone: wrapping their operands moved a union's ORDER BY and LIMIT inside the last one, which changed what the query returned.
cteNamesOnNewLine
Type: true | false
Default: true
Description: When true (default), CTE names start on a new indented line after WITH.
statementTerminator
Type: string
Default: ``
Description: What ends a statement in this project's script files, when the client they are run with was told something other than its own default. db2 -td! makes it !; mysql --delimiter=// makes it //; sqlcmd -c cmd_end replaces GO. That instruction is in the invocation, not in the file, so the formatter cannot find it out for itself — without it a script whose statements end in ! reads as one enormous statement. Leave it out to use the client's own default. A file that carries its own --#SET TERMINATOR or DELIMITER still overrides this from the point it appears. Set it under a dialect section of dlab.json when a project holds more than one dialect, since a terminator is a fact about one client. A dialect whose client has no such option refuses the setting rather than ignoring it.
File Selection
include
Type: array of glob patterns
Default: the dialect's own source suffixes — ["**.sql"] for most, and more where a dialect's tooling names its files differently: Oracle adds **.pkb, **.pks and the rest of the PL/SQL family, Hive adds **.hql, Db2 adds **.db2, PostgreSQL adds **.pgsql and **.pg, MySQL and MariaDB add **.mysql. dfmt setup --dialect <dialect> writes the list out, so the file always says which one you got.
Description: Glob patterns for files to format. Files must match at least one include pattern.
exclude
Type: array of glob patterns
Default: []
Description: Glob patterns for files to skip. Exclude takes precedence over include.
Common patterns
| Pattern | Matches |
|---|---|
**.sql | All .sql files recursively |
models/**.sql | SQL files under models/ |
target/** | Files in any target directory |
vendor/** | Files in vendor/ |