Skip to content

Add dbt, gitlab, kickstarter, and mattmc3 formatters#2

Merged
gmr merged 13 commits into
mainfrom
feature/additional-formatters
Mar 27, 2026
Merged

Add dbt, gitlab, kickstarter, and mattmc3 formatters#2
gmr merged 13 commits into
mainfrom
feature/additional-formatters

Conversation

@gmr

@gmr gmr commented Mar 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add four new formatting styles: dbt, gitlab, kickstarter, and mattmc3
  • Add _kw() method to base Formatter for keyword casing (enables lowercase styles)
  • Update MozillaFormatter structural keywords to use _kw() for subclass reuse
  • Full mkdocs documentation: CLI reference, Python API, style reference
  • Updated README with all 7 styles and example output

New Styles

Style Base Keywords Indent Distinct feature
dbt Mozilla lowercase 4-space Blank lines between clauses, explicit inner join
gitlab Mozilla UPPERCASE 2-space Blank lines inside CTE bodies
kickstarter Mozilla UPPERCASE 2-space JOIN ON same line, compact CTE chaining
mattmc3 AWeber lowercase river Leading commas, plain join

Test plan

  • 62 fixture-based tests across all 7 styles
  • Verify pgfmt --style dbt produces lowercase output with blank lines
  • Verify pgfmt --style mattmc3 produces leading comma river output
  • Verify existing river/mozilla/aweber tests unchanged

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added four new formatting styles (dbt, gitlab, kickstarter, mattmc3); selectable via CLI and Python API; CLI --check supported.
  • Documentation

    • Expanded docs and site with installation, CLI usage, Python API examples, style catalog, and style-specific examples.
  • Tests

    • Added fixture-based tests and expected outputs covering all new styles.

gmr and others added 5 commits March 26, 2026 19:52
Adds a _kw() method to the base Formatter class that controls keyword
casing. All deparse methods now use self._kw() for SQL keywords,
enabling formatters to output lowercase (dbt) or uppercase (river,
mozilla, aweber) consistently.

The dbt formatter subclasses MozillaFormatter with:
- All keywords and function names lowercase
- Blank lines between query clauses (generous whitespace)
- CTE style: `with` on its own line, blank lines around CTE body
- Explicit join types (inner join, not join)
- 4-space indent, trailing commas, one item per line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitLab: Mozilla-like with 2-space indent, uppercase keywords, blank
lines inside CTE bodies, ON indented under JOIN keyword.

Kickstarter: Mozilla-like with 2-space indent, uppercase keywords,
JOIN ON on same line with AND indented below, compact CTE chaining
(), name AS () on closing paren line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lowercase river-style with leading commas. Keywords right-aligned
to form a river, with `, item` prefix notation for continuation
items in SELECT, VALUES, and SET clauses. Uses plain `join` instead
of `inner join`, all keywords lowercase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLI and library usage, all 7 style examples with sample output,
and supported statement types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLI reference: arguments, --check mode, exit codes, examples
- Python API: pgfmt.format() usage, style selection, error handling
- Style reference: all 7 styles with example output
- Updated index with quick start and style comparison table
- Removed supported statements section from README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds four new formatter styles (dbt, gitlab, kickstarter, mattmc3), a keyword-casing hook in the base formatter, extends format dispatch and CLI choices to expose the styles, updates river/aweber CTE/width handling, and adds documentation plus parametrized tests with style-specific fixtures.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/index.md, docs/api.md, docs/cli.md, docs/styles.md, mkdocs.yml
Expanded README and docs with CLI/API usage, style catalog/examples, added CLI/API reference pages and navigation entries.
Core formatter hook & safety
src/pgfmt/formatter.py, src/pgfmt/mozilla.py
Added Formatter._kw() hook and routed many keyword literals through it; converted several static helpers to instance methods and safer lookups; updated set-op keyword handling to instance method.
Dispatcher & CLI
src/pgfmt/__init__.py, src/pgfmt/cli.py
Extended public format() style dispatch to include dbt, gitlab, kickstarter, mattmc3; expanded CLI --style choices accordingly.
New formatter modules
src/pgfmt/dbt.py, src/pgfmt/gitlab.py, src/pgfmt/kickstarter.py, src/pgfmt/mattmc3.py
Added four new formatter classes implementing style-specific formatting rules (keyword casing, join behavior, clause/layout, DML/DDL rendering, and many helper methods).
River / AWeber adjustments
src/pgfmt/river.py, src/pgfmt/aweber.py
Propagated a min_width/keyword-width through select/CTE formatting, added CTE-body helpers, and improved river-aligned CASE rendering.
Tests — fixtures (new styles)
tests/fixtures/dbt/*, tests/fixtures/gitlab/*, tests/fixtures/kickstarter/*, tests/fixtures/mattmc3/*, tests/fixtures/aweber/*
Added SQL input and corresponding .expected outputs covering SELECT/CTE/join/group/order/insert/update cases for each new style.
Tests — test modules
tests/test_dbt.py, tests/test_gitlab.py, tests/test_kickstarter.py, tests/test_mattmc3.py
Added parametrized pytest modules that discover fixture pairs and assert formatted output equals expected for each new style.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add river-style SQL formatter #1: Prior PR introducing core formatter architecture and initial formatter primitives that these new styles and the _kw() integration extend.

Poem

🐰 I hopped through keywords, soft and spry,
dbt, GitLab, Kickstarter, Matt under SQL sky.
I tuck commas, nudge joins, make clauses neat,
from WITH to LIMIT — every line I greet.
Hop, hop — queries trimmed and oh so sweet!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the primary change: adding four new SQL formatting styles (dbt, gitlab, kickstarter, mattmc3) to the pgfmt formatter.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/additional-formatters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
tests/test_dbt.py (1)

7-30: Cover the new lowercase DDL/DML paths too.

The added dbt fixtures here appear to be SELECT-only, but the _kw() rollout in src/pgfmt/formatter.py and src/pgfmt/mozilla.py also changed INSERT/UPDATE/DELETE/CREATE formatting. Please add at least one non-SELECT fixture for a lowercase style; otherwise mixed-case regressions in those paths can ship untested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_dbt.py` around lines 7 - 30, Add at least one lowercase DDL/DML
fixture to the dbt test set and update test_dbt_format/_fixture_cases to include
it: create a fixture .sql in the fixtures/dbt directory containing a lowercase
INSERT/UPDATE/DELETE or CREATE statement (not just SELECT), add the
corresponding .expected file, and ensure _fixture_cases yields that pair so
pgfmt.format(sql, style='dbt') exercises the lowercase DDL/DML paths (the recent
_kw() changes in formatter.py and mozilla.py).
src/pgfmt/gitlab.py (1)

325-354: Unused close variable - same pattern as KickstarterFormatter.

The close variable is initialized to empty string and never modified. Consider inlining or removing it, or adding a comment if reserved for future use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/gitlab.py` around lines 325 - 354, In _deparse_sub_link remove the
unused local variable close (or document it if intentionally reserved); update
all return f-strings in _deparse_sub_link to drop the trailing {close}
interpolation (e.g., change f'(\n{inner}\n{close})' to f'(\n{inner}\n)') and
similarly for the EXISTS_SUBLINK, ANY_SUBLINK and ALL_SUBLINK branches, or
alternatively replace the close declaration with a clear comment explaining its
future purpose.
src/pgfmt/kickstarter.py (2)

91-106: Unnecessary continue statement.

The continue on line 105 is redundant since it's the last statement in the else branch and the loop would naturally proceed to the next iteration.

Suggested fix
             else:
                 self._format_join_item(
                     kw,
                     table,
                     quals,
                     using,
                     lines,
                 )
-                continue
             self._format_item_clauses(quals, using, lines)

Wait - actually looking more closely, the continue is intentional to skip _format_item_clauses for JOIN items since _format_join_item handles the clauses internally. However, this control flow is confusing.

Consider restructuring to make the control flow clearer:

         for kw, table, quals, using in from_items:
             if kw == ',':
                 lines[-1] += ','
                 lines.append(f'{INDENT}{table}')
+                self._format_item_clauses(quals, using, lines)
             elif kw == from_kw:
                 lines.append(f'{from_kw} {table}')
+                self._format_item_clauses(quals, using, lines)
             else:
                 self._format_join_item(
                     kw,
                     table,
                     quals,
                     using,
                     lines,
                 )
-                continue
-            self._format_item_clauses(quals, using, lines)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/kickstarter.py` around lines 91 - 106, The loop in the method
iterates over from_items and uses a confusing continue to avoid calling
_format_item_clauses after handling JOINs in _format_join_item; change the
control flow in the loop so it's explicit: detect JOIN keywords (the values that
trigger _format_join_item) and call self._format_join_item(kw, table, quals,
using, lines) and then skip calling self._format_item_clauses, otherwise handle
the ',' and from_kw branches (updating lines) and then call
self._format_item_clauses(quals, using, lines); remove the redundant continue
and make the branch that handles joins explicitly separate so it's clear that
_format_item_clauses is not invoked for JOIN items (refer to the loop and the
helper methods _format_join_item and _format_item_clauses).

348-351: Unused close variable.

The close variable is initialized to an empty string on line 348 and used in f-strings but never assigned a different value. If this is intentional (for future use or consistency), consider adding a comment. Otherwise, you can inline the empty string or remove it.

Suggested fix (if close will always be empty)
-        close = ''
         match sub_type:
             case 'EXISTS_SUBLINK':
-                return f'{self._kw("EXISTS")} (\n{inner}\n{close})'
+                return f'{self._kw("EXISTS")} (\n{inner}\n)'
             case 'ANY_SUBLINK':
                 test = self.deparse(node.get('testexpr'))
                 op = self._get_operator(node.get('operName', []))
                 if op == '=':
-                    return f'{test} {self._kw("IN")} (\n{inner}\n{close})'
-                return f'{test} {op} {self._kw("ANY")} (\n{inner}\n{close})'
+                    return f'{test} {self._kw("IN")} (\n{inner}\n)'
+                return f'{test} {op} {self._kw("ANY")} (\n{inner}\n)'
             case 'ALL_SUBLINK':
                 test = self.deparse(node.get('testexpr'))
                 op = self._get_operator(node.get('operName', []))
-                return f'{test} {op} {self._kw("ALL")} (\n{inner}\n{close})'
+                return f'{test} {op} {self._kw("ALL")} (\n{inner}\n)'
             case _:
-                return f'(\n{inner}\n{close})'
+                return f'(\n{inner}\n)'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/kickstarter.py` around lines 348 - 351, The variable `close` in
kickstarter.py is defined as an empty string and never reassigned but is
interpolated into the f-string returned for the 'EXISTS_SUBLINK' branch; remove
the unused `close` variable and inline the empty string (or delete it and update
the f-string to not reference `close`) in the match case for sub_type
'EXISTS_SUBLINK' inside the function handling sublinks to eliminate the dead
variable, or if it's intended for future use add a clarifying comment above
`close` explaining why it remains an empty string.
src/pgfmt/mattmc3.py (1)

116-118: Consider using enumerate start parameter or _ for unused index.

The variable _idx suggests it's unused, but it's actually used on line 132. Consider renaming to idx since it's used, or restructure to avoid the underscore prefix convention for unused variables.

Suggested fix
-        for _idx, (kw, table, quals, using, is_qual) in enumerate(
+        for idx, (kw, table, quals, using, is_qual) in enumerate(
             from_items,
         ):
             kw_lower = kw.lower() if kw != ',' else kw
             if not is_qual:
                 self._format_river_from_item(
                     kw,
                     kw_lower,
                     table,
                     quals,
                     using,
                     width,
                     content_pad,
                     lines,
                 )
             else:
-                if _idx > 1:
+                if idx > 1:
                     lines.append('')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/mattmc3.py` around lines 116 - 118, The loop currently declares an
index as `_idx` in the enumerate(...) tuple but that variable is actually used
later; rename `_idx` to `idx` (or otherwise remove the leading underscore) in
the for loop that unpacks `(kw, table, quals, using, is_qual)` so the index is
clearly used — update every reference to `_idx` within the surrounding function
in src/pgfmt/mattmc3.py (the enumerate(...) loop and the usage at the later
line) to `idx`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Line 17: The .gitignore entry "guides/" is too broad and can hide tracked
documentation source files; update the ignore rule to only exclude generated
artifacts (e.g., specific build/output folders or file patterns) or add negation
rules for source content so authors' documentation changes are not silently
ignored—modify the "guides/" line to a narrower pattern (for example target
"guides/build/" or "guides/**/*.html") or add "!guides/**/src/"-style negations
to preserve source files while still ignoring generated outputs.

In `@src/pgfmt/dbt.py`:
- Around line 77-84: The code currently omits trailing commas: update the
SELECT-target formatting so every target line ends with a comma per the
docstring/dbt style; specifically, in the single-target branch (where it emits
f'{select_kw} {distinct}{targets[0]}') append a comma, and in the multi-target
loop change the suffix computation (currently suffix = ',' if i < len(targets) -
1 else '') to always set suffix = ',' so the last item also gets a trailing
comma; keep existing prefixes (distinct and INDENT) intact.

In `@src/pgfmt/formatter.py`:
- Around line 46-48: Several formatter methods still emit hard-coded uppercase
keywords instead of using the central casing hook; update
format_create_table_as(), format_create_function(), _format_create_domain(), and
_format_create_user_mapping() to call self._kw(keyword) for each SQL keyword
(e.g., "CREATE", "TABLE", "AS", "FUNCTION", "DOMAIN", "USER", "MAPPING", etc.)
rather than embedding literal uppercase strings, and ensure any surrounding
punctuation/spaces are preserved when replacing the hard-coded keywords so
lowercase styles consistently apply.

In `@src/pgfmt/gitlab.py`:
- Around line 365-426: Remove the redundant overrides _format_where,
_format_having, and _format_on from the GitLabFormatter class so it uses the
parent MozillaFormatter implementations; locate the methods named _format_where,
_format_having, and _format_on in src/pgfmt/gitlab.py and delete their entire
method bodies (including their signatures) so inheritance handles formatting,
and verify there are no GitLab-specific references that require custom behavior
(e.g., calls to _flatten_bool_expr or _kw) before committing.

In `@src/pgfmt/mozilla.py`:
- Around line 237-241: format_create_table() currently only uses self._kw() for
the initial CREATE prefix, leaving subsequent DDL tokens like WITH, SERVER and
OPTIONS hard-coded and causing mixed-case output for Mozilla-derived lowercase
styles; update the branches that emit those tokens (the blocks that append
"WITH", "SERVER", "OPTIONS" and any storage option keys) to call
self._kw('WITH'), self._kw('SERVER'), self._kw('OPTIONS') (and any other DDL
keywords emitted in that method) and ensure any keyword strings are routed
through _kw() so the entire CREATE TABLE / CREATE FOREIGN TABLE output follows
the configured keyword casing, using the existing foreign_server flag and
surrounding logic to locate the correct places to change.

---

Nitpick comments:
In `@src/pgfmt/gitlab.py`:
- Around line 325-354: In _deparse_sub_link remove the unused local variable
close (or document it if intentionally reserved); update all return f-strings in
_deparse_sub_link to drop the trailing {close} interpolation (e.g., change
f'(\n{inner}\n{close})' to f'(\n{inner}\n)') and similarly for the
EXISTS_SUBLINK, ANY_SUBLINK and ALL_SUBLINK branches, or alternatively replace
the close declaration with a clear comment explaining its future purpose.

In `@src/pgfmt/kickstarter.py`:
- Around line 91-106: The loop in the method iterates over from_items and uses a
confusing continue to avoid calling _format_item_clauses after handling JOINs in
_format_join_item; change the control flow in the loop so it's explicit: detect
JOIN keywords (the values that trigger _format_join_item) and call
self._format_join_item(kw, table, quals, using, lines) and then skip calling
self._format_item_clauses, otherwise handle the ',' and from_kw branches
(updating lines) and then call self._format_item_clauses(quals, using, lines);
remove the redundant continue and make the branch that handles joins explicitly
separate so it's clear that _format_item_clauses is not invoked for JOIN items
(refer to the loop and the helper methods _format_join_item and
_format_item_clauses).
- Around line 348-351: The variable `close` in kickstarter.py is defined as an
empty string and never reassigned but is interpolated into the f-string returned
for the 'EXISTS_SUBLINK' branch; remove the unused `close` variable and inline
the empty string (or delete it and update the f-string to not reference `close`)
in the match case for sub_type 'EXISTS_SUBLINK' inside the function handling
sublinks to eliminate the dead variable, or if it's intended for future use add
a clarifying comment above `close` explaining why it remains an empty string.

In `@src/pgfmt/mattmc3.py`:
- Around line 116-118: The loop currently declares an index as `_idx` in the
enumerate(...) tuple but that variable is actually used later; rename `_idx` to
`idx` (or otherwise remove the leading underscore) in the for loop that unpacks
`(kw, table, quals, using, is_qual)` so the index is clearly used — update every
reference to `_idx` within the surrounding function in src/pgfmt/mattmc3.py (the
enumerate(...) loop and the usage at the later line) to `idx`.

In `@tests/test_dbt.py`:
- Around line 7-30: Add at least one lowercase DDL/DML fixture to the dbt test
set and update test_dbt_format/_fixture_cases to include it: create a fixture
.sql in the fixtures/dbt directory containing a lowercase INSERT/UPDATE/DELETE
or CREATE statement (not just SELECT), add the corresponding .expected file, and
ensure _fixture_cases yields that pair so pgfmt.format(sql, style='dbt')
exercises the lowercase DDL/DML paths (the recent _kw() changes in formatter.py
and mozilla.py).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5df99748-b531-4c88-92b9-8eaf75453809

📥 Commits

Reviewing files that changed from the base of the PR and between 9132a75 and 842bf03.

📒 Files selected for processing (53)
  • .gitignore
  • README.md
  • docs/api.md
  • docs/cli.md
  • docs/index.md
  • docs/styles.md
  • mkdocs.yml
  • src/pgfmt/__init__.py
  • src/pgfmt/cli.py
  • src/pgfmt/dbt.py
  • src/pgfmt/formatter.py
  • src/pgfmt/gitlab.py
  • src/pgfmt/kickstarter.py
  • src/pgfmt/mattmc3.py
  • src/pgfmt/mozilla.py
  • tests/fixtures/dbt/select_cte.expected
  • tests/fixtures/dbt/select_cte.sql
  • tests/fixtures/dbt/select_group_order.expected
  • tests/fixtures/dbt/select_group_order.sql
  • tests/fixtures/dbt/select_join.expected
  • tests/fixtures/dbt/select_join.sql
  • tests/fixtures/dbt/select_simple.expected
  • tests/fixtures/dbt/select_simple.sql
  • tests/fixtures/gitlab/select_cte.expected
  • tests/fixtures/gitlab/select_cte.sql
  • tests/fixtures/gitlab/select_group_order.expected
  • tests/fixtures/gitlab/select_group_order.sql
  • tests/fixtures/gitlab/select_join.expected
  • tests/fixtures/gitlab/select_join.sql
  • tests/fixtures/gitlab/select_simple.expected
  • tests/fixtures/gitlab/select_simple.sql
  • tests/fixtures/kickstarter/select_cte.expected
  • tests/fixtures/kickstarter/select_cte.sql
  • tests/fixtures/kickstarter/select_join.expected
  • tests/fixtures/kickstarter/select_join.sql
  • tests/fixtures/kickstarter/select_simple.expected
  • tests/fixtures/kickstarter/select_simple.sql
  • tests/fixtures/kickstarter/select_where.expected
  • tests/fixtures/kickstarter/select_where.sql
  • tests/fixtures/mattmc3/insert_values.expected
  • tests/fixtures/mattmc3/insert_values.sql
  • tests/fixtures/mattmc3/select_join.expected
  • tests/fixtures/mattmc3/select_join.sql
  • tests/fixtures/mattmc3/select_or.expected
  • tests/fixtures/mattmc3/select_or.sql
  • tests/fixtures/mattmc3/select_simple.expected
  • tests/fixtures/mattmc3/select_simple.sql
  • tests/fixtures/mattmc3/update_multi.expected
  • tests/fixtures/mattmc3/update_multi.sql
  • tests/test_dbt.py
  • tests/test_gitlab.py
  • tests/test_kickstarter.py
  • tests/test_mattmc3.py

Comment thread .gitignore Outdated
Comment thread src/pgfmt/dbt.py
Comment thread src/pgfmt/formatter.py
Comment thread src/pgfmt/gitlab.py Outdated
Comment thread src/pgfmt/mozilla.py
- Narrow .gitignore to only ignore guides/sqlfmt.html instead of the
  entire guides/ directory
- Replace hardcoded uppercase keywords with self._kw() calls in
  format_create_function(), _format_create_domain(),
  _format_create_user_mapping(), and format_create_table_as()
- Remove redundant _format_where, _format_having, and _format_on
  overrides in GitLabFormatter that were identical to the inherited
  MozillaFormatter methods
- Replace hardcoded WITH, SERVER, and OPTIONS keywords with self._kw()
  in mozilla.py and gitlab.py format_create_table()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/pgfmt/gitlab.py (1)

325-353: Remove the unused close variable.

The close variable on line 334 is initialized to an empty string and never modified. It's used in the f-strings but adds no value. Consider removing it for clarity.

♻️ Suggested simplification
     def _deparse_sub_link(self, node: dict) -> str:
         sub_type = node.get('subLinkType', '')
         subselect = node['subselect']
         inner_node = subselect.get('SelectStmt', subselect)
         inner = self.format_select(
             inner_node,
             indent=len(INDENT),
         )

-        close = ''
         match sub_type:
             case 'EXISTS_SUBLINK':
-                return f'{self._kw("EXISTS")} (\n{inner}\n{close})'
+                return f'{self._kw("EXISTS")} (\n{inner}\n)'
             case 'ANY_SUBLINK':
                 test = self.deparse(node.get('testexpr'))
                 op = self._get_operator(
                     node.get('operName', []),
                 )
                 if op == '=':
-                    return f'{test} {self._kw("IN")} (\n{inner}\n{close})'
-                return f'{test} {op} {self._kw("ANY")} (\n{inner}\n{close})'
+                    return f'{test} {self._kw("IN")} (\n{inner}\n)'
+                return f'{test} {op} {self._kw("ANY")} (\n{inner}\n)'
             case 'ALL_SUBLINK':
                 test = self.deparse(node.get('testexpr'))
                 op = self._get_operator(
                     node.get('operName', []),
                 )
-                return f'{test} {op} {self._kw("ALL")} (\n{inner}\n{close})'
+                return f'{test} {op} {self._kw("ALL")} (\n{inner}\n)'
             case _:
-                return f'(\n{inner}\n{close})'
+                return f'(\n{inner}\n)'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/gitlab.py` around lines 325 - 353, In _deparse_sub_link remove the
unused local variable close (declared on the line initializing close = '') and
update the f-strings in the match cases to omit {close}; keep the rest of the
logic identical—this affects the function _deparse_sub_link and its returned
string constructions that currently include {close} alongside inner and the
surrounding parentheses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pgfmt/gitlab.py`:
- Around line 325-353: In _deparse_sub_link remove the unused local variable
close (declared on the line initializing close = '') and update the f-strings in
the match cases to omit {close}; keep the rest of the logic identical—this
affects the function _deparse_sub_link and its returned string constructions
that currently include {close} alongside inner and the surrounding parentheses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f08f28c-a98c-42e4-9583-a94a7e0b9bdd

📥 Commits

Reviewing files that changed from the base of the PR and between 842bf03 and 10a2eeb.

📒 Files selected for processing (3)
  • src/pgfmt/formatter.py
  • src/pgfmt/gitlab.py
  • src/pgfmt/mozilla.py
✅ Files skipped from review due to trivial changes (1)
  • src/pgfmt/mozilla.py

pgparse represents zero values as empty dicts ({"ival": {}} for 0,
{"fval": {}} for 0.0), similar to FALSE booleans. Use .get() with
defaults to handle missing inner keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/pgfmt/formatter.py (1)

734-736: Boolean values in storage options bypass _kw() casing.

Unlike other boolean handling (e.g., lines 360-363), the 'true'/'false' literals here are hardcoded lowercase and don't use self._kw(). If this is intentional (PostgreSQL storage options may require lowercase), consider adding a brief comment. Otherwise, wrap with _kw() for consistency.

♻️ Suggested fix if consistency is desired
         elif arg and 'Boolean' in arg:
-            val = 'true' if arg['Boolean'].get('boolval') else 'false'
+            val = self._kw('TRUE') if arg['Boolean'].get('boolval') else self._kw('FALSE')
             parts.append(f'{name}={val}')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pgfmt/formatter.py` around lines 734 - 736, The boolean storage-option
branch in formatter.py constructs literal 'true'/'false' without using the
formatter's keyword-casing helper; update the branch in the block handling arg
with 'Boolean' so val is produced via self._kw('true') or self._kw('false')
(i.e., replace the hardcoded lowercase literals with calls to self._kw()) before
building parts.append(f'{name}={val}'); if lowercase is intentional, instead add
a short comment above this branch explaining why casing is forced rather than
using _kw().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pgfmt/formatter.py`:
- Around line 734-736: The boolean storage-option branch in formatter.py
constructs literal 'true'/'false' without using the formatter's keyword-casing
helper; update the branch in the block handling arg with 'Boolean' so val is
produced via self._kw('true') or self._kw('false') (i.e., replace the hardcoded
lowercase literals with calls to self._kw()) before building
parts.append(f'{name}={val}'); if lowercase is intentional, instead add a short
comment above this branch explaining why casing is forced rather than using
_kw().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 042ffdf0-5161-4a9f-b054-48ec5adce1e9

📥 Commits

Reviewing files that changed from the base of the PR and between 10a2eeb and 9f6f9fb.

📒 Files selected for processing (1)
  • src/pgfmt/formatter.py

gmr and others added 5 commits March 27, 2026 11:18
AWeber formatter now pre-computes the max river width across all CTEs
and the outer SELECT, ensuring consistent keyword alignment throughout
the entire statement. Added min_width parameter to format_select and
_format_with_clause to propagate the unified width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CASE expressions now render with WHEN/ELSE aligned and END offset
to match the river position. Both `CASE WHEN` and `CASE expr WHEN`
forms are handled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three new fixtures derived from production queries:
- Multi-CTE with UNION and final aggregated JOIN
- CASE expression with multiple LEFT JOINs
- Nested CTE-in-CTE with RANK() window function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nested CTEs (WITH inside a CTE body) now indent their WITH header
right-aligned to the outer river width, with the nested body
indented to the content column. The nested body uses its own river
width for internal alignment.

Added _format_cte_body hook to base RiverFormatter for subclass
override. AWeber overrides it to detect and indent nested CTEs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a CASE expression's THEN or ELSE result is multi-line (e.g.,
an EXISTS subquery), continuation lines are now indented to align
after the THEN/ELSE keyword using _indent_continuation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pgfmt/aweber.py`:
- Around line 12-30: In format_select, when a WITH clause is present you must
recompute the river width to account for nested CTEs that can introduce a wider
river keyword; replace the current conditional so you compute width =
max(min_width, self._unified_river_width(node)) (call _unified_river_width even
when min_width != 0) and pass that width into super().format_select (use the
withClause check and the format_select signature already present). Also apply
the same change to the other similar branches handling WITH/nested scopes in
this file so nested scopes always consider the locally computed unified river
width.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07406268-e775-43fb-9ecf-6a291ecf9f72

📥 Commits

Reviewing files that changed from the base of the PR and between 1611dea and f5f437a.

📒 Files selected for processing (1)
  • src/pgfmt/aweber.py

Comment thread src/pgfmt/aweber.py
- In format_select, always compute max(min_width, _unified_river_width)
  when a WITH clause is present, instead of only when min_width == 0
- In _format_cte_body, compute scope_width as max(min_width,
  _unified_river_width) so the nested WITH block layout uses the
  correct alignment column even when called with an inherited width
- Pass scope_width (not the inherited min_width) to the outer query
  format_select call inside _format_cte_body

Co-authored-by: Claude <noreply@anthropic.com>
@gmr gmr merged commit 73d3412 into main Mar 27, 2026
5 checks passed
@gmr gmr deleted the feature/additional-formatters branch March 27, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant