Skip to content

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:

pip install execsql2

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:

pip install "execsql2[formatter]"

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

execsql-format [OPTIONS] FILE_OR_DIR [FILE_OR_DIR ...]

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:

  1. Each comment line is replaced with a unique inline marker attached to the next SQL line.
  2. sqlglot formats the complete statement (no fragmentation).
  3. Markers are restored to their original -- comment style and position.
  4. 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:

SELECT
    a
    , b
    , c
FROM t;

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.