execsql-format¶
execsql-format is a code formatter for execsql script files. It normalizes metacommand indentation, uppercases metacommand keywords, and optionally reformats SQL statements. Run it before committing scripts, in CI, or any time you want consistent formatting across a codebase.
Installation¶
The execsql-format command is installed automatically with the execsql2 package and is available on your PATH after install:
The metacommand-indentation and keyword-casing passes work out of the box. SQL reformatting (the optional sqlglot pass) requires the [formatter] extra, as of execsql2 2.19.0:
Without the extra, execsql-format works in --no-sql mode (metacommand indentation and keyword casing only); invoking the SQL pass without [formatter] installed raises ModuleNotFoundError: No module named 'sqlglot'.
Usage¶
Pass one or more files or directories. Directories are searched recursively for *.sql files.
By default, formatted output is written to stdout. Use --in-place to overwrite files, or --check to report which files need changes without modifying them.
Options¶
| Option | Default | Description |
|---|---|---|
FILE_OR_DIR |
required | One or more files or directories to format. Directories are searched recursively for *.sql files. |
--check |
off | Exit with code 1 if any file would be reformatted. Does not write any changes. Useful in CI. |
-i, --in-place |
off | Modify files in place instead of writing to stdout. |
--no-sql |
off | Skip SQL reformatting via sqlglot. Only normalizes metacommand indentation and keyword casing. |
--indent N |
4 |
Spaces per indent level. Controls both metacommand block depth and SQL indentation (columns, subqueries, etc). |
--leading-comma |
off | Place commas at the start of lines instead of the end (e.g. , col2 instead of col1,). |
What Gets Formatted¶
Metacommand keyword casing¶
All metacommand keywords are uppercased. Arguments after the keyword are preserved exactly as written.
-- before
-- !x! if(!!myvar!! = "yes")
-- !x! sub_add mykey myvalue
-- after
-- !x! IF(!!myvar!! = "yes")
-- !x! SUB_ADD mykey myvalue
Metacommand indentation¶
Metacommands that open a block (IF, LOOP, BEGIN SCRIPT, BEGIN BATCH, BEGIN SQL, CREATE SCRIPT) increase the indent level for everything that follows. Their matching close keywords (ENDIF, END LOOP, END SCRIPT, END BATCH, END SQL) are dedented back to the opening level.
ELSE and ELSEIF pivot at the same depth as their IF. ANDIF and ORIF are emitted at one level above the current depth without changing the depth counter.
-- !x! IF(!!status!! = "active")
-- !x! SUB_ADD result "found"
-- !x! ELSE
-- !x! SUB_ADD result "not found"
-- !x! ENDIF
SQL block formatting¶
SQL statements between metacommands are re-indented to match the current block depth and reformatted using sqlglot in PostgreSQL dialect with pretty-printing enabled.
The --indent flag controls SQL indentation in addition to metacommand depth. For example, --indent 4 (the default) produces 4-space indented column lists, subqueries, and CASE branches. --indent 2 gives a more compact style.
Comment handling¶
Comments interleaved within SQL statements (e.g. -- comments between SELECT columns, or inside CASE expressions) are preserved through formatting using a marker-based round-trip:
- Each comment line is replaced with a unique inline marker attached to the next SQL line.
- sqlglot formats the complete statement (no fragmentation).
- Markers are restored to their original
--comment style and position. - Comments that sqlglot's AST drops (e.g. inside CASE WHEN) are detected and re-inserted at the best matching position.
Block comments (/* */) that contain -- !x! metacommand markers (e.g. commented-out code blocks) are recognized and passed through without metacommand processing.
Variable preservation¶
execsql substitution variables (!!varname!!, !{varname}!) are replaced with valid SQL identifiers before formatting, then restored afterward, so the formatter does not corrupt them — including in schema-qualified names (!!staging!!.!!table!!), CASE expressions, JOIN conditions, and string concatenation.
Fallback behavior¶
If sqlglot cannot parse a SQL statement, or if safety checks detect that formatting would corrupt the SQL (e.g. statement count changes, significant content loss), the original text is preserved unchanged.
Use --no-sql to skip SQL reformatting entirely and only normalize metacommands.
Examples¶
execsql-format myscript.sql # Preview to stdout
execsql-format --in-place myscript.sql # Rewrite in place
execsql-format --in-place scripts/ # Recurse into a directory
execsql-format --check scripts/ # Exit 1 if any file would change (for CI)
execsql-format --indent 2 --in-place myscript.sql # Two-space indent
execsql-format --leading-comma --in-place myscript.sql # Commas at line start
execsql-format --no-sql --in-place myscript.sql # Only re-indent metacommands; leave SQL alone
--leading-comma produces output like:
Before and After Example¶
The following script has inconsistent metacommand casing, no indentation inside the IF block, and unformatted SQL.
Before:
-- !x! sub schema "public"
-- !x! if(equal(!!schema!!, "public"))
-- !x! write "Checking public schema..."
select id,name,created_at from users where active = true order by name;
-- !x! endif
After (execsql-format myscript.sql):
-- !x! SUB schema "public"
-- !x! IF(EQUAL(!!schema!!, "public"))
-- !x! WRITE "Checking public schema..."
SELECT
id,
name,
created_at
FROM users
WHERE
active = TRUE
ORDER BY
name;
-- !x! ENDIF
Pre-commit Hook¶
execsql-format can be used as a pre-commit hook. Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/geocoug/execsql
rev: v2.17.0
hooks:
- id: execsql-format
args: [--in-place]
The hook runs on *.sql files. Pass any CLI flags via args — e.g. [--check] for a CI-style check-only run, or [--in-place, --indent, "2"] to combine in-place rewriting with a custom indent width. Run pre-commit autoupdate periodically to bump the rev.
Exit Codes¶
| Code | Meaning |
|---|---|
0 |
Success. All files formatted (or already up to date in --check mode). |
1 |
One or more files would be reformatted (--check mode), a file could not be read, or no .sql files were found. |