sqlnbfmt 0.4.2__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. sqlnbfmt-0.6.0/.github/dependabot.yml +23 -0
  2. sqlnbfmt-0.6.0/.github/workflows/ci-and-publish.yml +57 -0
  3. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/.pre-commit-config.yaml +3 -3
  4. sqlnbfmt-0.6.0/CHANGELOG.md +201 -0
  5. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/PKG-INFO +75 -37
  6. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/README.md +73 -35
  7. sqlnbfmt-0.6.0/SECURITY.md +13 -0
  8. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/config.yaml +9 -1
  9. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/pyproject.toml +10 -3
  10. sqlnbfmt-0.6.0/sqlnbfmt/__init__.py +29 -0
  11. sqlnbfmt-0.6.0/sqlnbfmt/formatter.py +1534 -0
  12. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/sqlnbfmt/jupyterlab_integration.py +29 -12
  13. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/in_function_sql/expected.ipynb +1 -2
  14. {sqlnbfmt-0.4.2/tests/eval/marimo_idempotency → sqlnbfmt-0.6.0/tests/eval/marimo_basic_sql}/expected.py +11 -11
  15. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_comment_preservation/expected.py +20 -20
  16. sqlnbfmt-0.6.0/tests/eval/marimo_complex_sql/expected.py +63 -0
  17. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_fstring_interpolation/expected.py +19 -19
  18. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_cell_mixed/expected.py +22 -22
  19. sqlnbfmt-0.6.0/tests/eval/marimo_multi_statement/expected.py +34 -0
  20. sqlnbfmt-0.6.0/tests/eval/python_sql_decorators/expected.py +56 -0
  21. sqlnbfmt-0.6.0/tests/eval/python_sql_decorators/input.py +27 -0
  22. sqlnbfmt-0.6.0/tests/test_call_layout.py +151 -0
  23. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/test_formatter.py +22 -4
  24. sqlnbfmt-0.6.0/tests/test_function_name_matching.py +63 -0
  25. sqlnbfmt-0.6.0/tests/test_jupyterlab_integration.py +208 -0
  26. sqlnbfmt-0.6.0/tests/test_live.py +284 -0
  27. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/test_marimo.py +6 -2
  28. sqlnbfmt-0.6.0/tests/test_pyproject_config.py +401 -0
  29. sqlnbfmt-0.6.0/tests/test_round_trip.py +642 -0
  30. sqlnbfmt-0.6.0/tests/test_sql_decorators.py +136 -0
  31. sqlnbfmt-0.6.0/tests/test_string_literals.py +131 -0
  32. sqlnbfmt-0.6.0/uv.lock +497 -0
  33. sqlnbfmt-0.4.2/.github/workflows/ci-and-publish.yml +0 -42
  34. sqlnbfmt-0.4.2/CHANGELOG.md +0 -143
  35. sqlnbfmt-0.4.2/sqlnbfmt/__init__.py +0 -15
  36. sqlnbfmt-0.4.2/sqlnbfmt/formatter.py +0 -832
  37. sqlnbfmt-0.4.2/tests/eval/marimo_complex_sql/expected.py +0 -63
  38. sqlnbfmt-0.4.2/tests/eval/marimo_multi_statement/expected.py +0 -34
  39. sqlnbfmt-0.4.2/tests/test_jupyterlab_integration.py +0 -76
  40. sqlnbfmt-0.4.2/uv.lock +0 -455
  41. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/.gitignore +0 -0
  42. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/.pre-commit-hooks.yaml +0 -0
  43. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/.python-version +0 -0
  44. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/LICENSE +0 -0
  45. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/jupyter-config/sqlnbfmt.json +0 -0
  46. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/__init__.py +0 -0
  47. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/README.md +0 -0
  48. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/basic_select/expected.ipynb +0 -0
  49. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/basic_select/input.ipynb +0 -0
  50. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/case_expression/expected.ipynb +0 -0
  51. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/case_expression/input.ipynb +0 -0
  52. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/comment_preservation/expected.ipynb +0 -0
  53. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/comment_preservation/input.ipynb +0 -0
  54. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/cte/expected.ipynb +0 -0
  55. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/cte/input.ipynb +0 -0
  56. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/fstring_variables/expected.ipynb +0 -0
  57. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/fstring_variables/input.ipynb +0 -0
  58. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/generate_fixtures.py +0 -0
  59. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/group_by_having/expected.ipynb +0 -0
  60. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/group_by_having/input.ipynb +0 -0
  61. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/idempotency/expected.ipynb +0 -0
  62. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/idempotency/input.ipynb +0 -0
  63. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/in_function_sql/input.ipynb +0 -0
  64. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/joins/expected.ipynb +0 -0
  65. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/joins/input.ipynb +0 -0
  66. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/magic_commands/expected.ipynb +0 -0
  67. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/magic_commands/input.ipynb +0 -0
  68. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_basic_sql/input.py +0 -0
  69. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_comment_preservation/input.py +0 -0
  70. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_complex_sql/input.py +0 -0
  71. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_fstring_interpolation/input.py +0 -0
  72. {sqlnbfmt-0.4.2/tests/eval/marimo_basic_sql → sqlnbfmt-0.6.0/tests/eval/marimo_idempotency}/expected.py +0 -0
  73. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_idempotency/input.py +0 -0
  74. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_cell_mixed/input.py +0 -0
  75. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_statement/input.py +0 -0
  76. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_non_sql_unchanged/expected.py +0 -0
  77. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_non_sql_unchanged/input.py +0 -0
  78. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_skip_hint/expected.py +0 -0
  79. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/marimo_skip_hint/input.py +0 -0
  80. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/multi_cell_mixed/expected.ipynb +0 -0
  81. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/multi_cell_mixed/input.ipynb +0 -0
  82. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/multi_statement/expected.ipynb +0 -0
  83. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/multi_statement/input.ipynb +0 -0
  84. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/non_sql_unchanged/expected.ipynb +0 -0
  85. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/non_sql_unchanged/input.ipynb +0 -0
  86. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/skip_hint/expected.ipynb +0 -0
  87. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/skip_hint/input.ipynb +0 -0
  88. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/subquery/expected.ipynb +0 -0
  89. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/subquery/input.ipynb +0 -0
  90. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/union/expected.ipynb +0 -0
  91. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/union/input.ipynb +0 -0
  92. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/window_function/expected.ipynb +0 -0
  93. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/eval/window_function/input.ipynb +0 -0
  94. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/test_eval.py +0 -0
  95. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/test_notebook.ipynb +0 -0
  96. {sqlnbfmt-0.4.2 → sqlnbfmt-0.6.0}/tests/test_notebook_copy.ipynb +0 -0
@@ -0,0 +1,23 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "monday"
8
+ labels:
9
+ - "dependencies"
10
+
11
+ - package-ecosystem: "pip"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "weekly"
15
+ day: "monday"
16
+ open-pull-requests-limit: 10
17
+ labels:
18
+ - "dependencies"
19
+ groups:
20
+ minor-and-patch:
21
+ update-types:
22
+ - "minor"
23
+ - "patch"
@@ -0,0 +1,57 @@
1
+ name: Python CI and Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ release:
8
+ types: [created]
9
+ workflow_dispatch:
10
+ inputs:
11
+ run_publish:
12
+ description: 'Set to true to run the publish job'
13
+ required: false
14
+ default: 'false'
15
+
16
+ permissions: {}
17
+
18
+ jobs:
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
26
+ with:
27
+ enable-cache: true
28
+ cache-dependency-glob: "pyproject.toml"
29
+ - run: uv python install 3.12
30
+ - run: uv sync --all-extras --dev --frozen
31
+ - run: uv run pytest
32
+ # Also the oldest sqlglot that pyproject.toml allows (keep in sync)
33
+ - run: uv run --isolated --with "sqlglot==25.27.0" pytest
34
+
35
+ security:
36
+ runs-on: ubuntu-latest
37
+ permissions:
38
+ contents: read
39
+ steps:
40
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
41
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
42
+ - run: uvx uv-secure uv.lock
43
+
44
+ publish:
45
+ needs: [test, security]
46
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true')
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ id-token: write
50
+ steps:
51
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
52
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
53
+ with:
54
+ enable-cache: true
55
+ - run: uv python install 3.12
56
+ - run: uv build --no-sources
57
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -1,9 +1,9 @@
1
1
  repos:
2
2
  # Linting + formatting (Astral)
3
3
  - repo: https://github.com/astral-sh/ruff-pre-commit
4
- rev: v0.15.6
4
+ rev: v0.17.1
5
5
  hooks:
6
- - id: ruff
6
+ - id: ruff-check
7
7
  args: [--fix]
8
8
  - id: ruff-format
9
9
 
@@ -24,7 +24,7 @@ repos:
24
24
 
25
25
  # Validate pyproject.toml against PEP 621
26
26
  - repo: https://github.com/abravalheri/validate-pyproject
27
- rev: v0.25
27
+ rev: '0.26'
28
28
  hooks:
29
29
  - id: validate-pyproject
30
30
 
@@ -0,0 +1,201 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.6.0] - 2026-10-16
9
+
10
+ **Upgrading is recommended.** Earlier versions could change what code means: query parameters were rewritten with the postgres, duckdb and bigquery dialects, non-ASCII text could cut a rewrite in the wrong place, f-string fields lost their quotes, and `%sql` line magics deleted the rest of the cell. sqlnbfmt now checks every rewrite and leaves a string unchanged, with a warning, when it can't format it safely.
11
+
12
+ ### Added
13
+
14
+ - `extend_sql_keywords`, `extend_function_names` and `extend_sql_decorators` config options, which add entries to the built-in lists instead of replacing them, so adding one function name no longer drops defaults like `sql` (Marimo's `mo.sql()`) ([#26](https://github.com/flyersworder/sqlnbfmt/issues/26))
15
+ - The unknown-option warning suggests the intended option for kebab-case keys (e.g. `extend-function-names`, `indent-width`), at the top level and under `formatting_options`
16
+
17
+ ### Deprecated
18
+
19
+ - `formatting_options.single_line_threshold`: it was documented as the maximum length before splitting SQL, but no version ever read it; line breaks are decided by sqlglot. Setting it now logs a warning, and it will be removed in a future release
20
+
21
+ ### Fixed
22
+
23
+ - **Query parameters were corrupted with the postgres, duckdb and bigquery dialects.** `%s`/`?` parameters became `%(AUTO_PLACEHOLDER_0)s`, `$AUTO_PLACEHOLDER_0` or `@AUTO_PLACEHOLDER_0`, f-string fields became `%({table})s`, `${table}` or `@{table}`, and user-written `:name` parameters were converted to the dialect's own style. All parameter styles (`%s`, `?`, `%(name)s`, `:name`, `:1`, `$1`) and f-string fields now pass through unchanged; `?` or `%s` inside SQL string literals and comments is left as text, using sqlglot's tokenizer for the dialect to tell them apart (so `'\'` in postgres, `$$…$$` bodies and MySQL/BigQuery `#` comments are read correctly). Queries with 11 or more parameters were also mangled (`_1` matched inside `_10`)
24
+ - **Formatting could move parameters or rewrap fields.** Positional parameters bind by order, but `… OFFSET %s LIMIT %s` was reordered to `LIMIT %s OFFSET %s` (and T-SQL `LIMIT ?` to `TOP (?)`), a field holding a clause, as in `f"SELECT * FROM t {where_clause}"`, became `FROM t AS {where_clause}`, and `SELECT {distinct} a` became `SELECT {distinct} AS a`. Parameters and fields could also be pulled into a string (`INTERVAL %s DAY` → `INTERVAL '%s DAY'`) or wrapped in parentheses (`VALUES %s` → `VALUES (%s)`, which breaks psycopg2's `execute_values`), and the quotes around a field could be rewritten (`"{name}"` → `'{name}'` in MySQL, `$${body}$$` → `'{body}'`), which breaks when the value contains a quote. A `%s` inside a SQL comment, which drivers fill in too, could be moved or dropped along with the comment, shifting the values of the parameters after it. Positional parameters must now keep their order (comments included), fields and parameters must stay outside strings without added parentheses, a string holding a field must be written exactly as before, and one may only become an alias, or be given one, if the original wrote `AS` there; otherwise the string is left unformatted with a warning that says why. Harmless rewrites (comments, a trailing `;`, `%s::int` → `CAST(%s AS INT)`) are still formatted
25
+ - **Multi-line SQL string literals changed when re-indented.** In SQL passed to a call, the continuation lines of a string such as `'multi\nline'`, a quoted name (`` `x\ny` ``, `[x\ny]`) or a `$$…$$` function body were indented too, changing its value; only lines outside SQL strings are indented now, following the dialect's quoting rules
26
+ - **Magic cells lost code.** A `%sql` line magic deleted every line after it, `%%sql` arguments were dropped (`%%sql df <<` lost the assignment, `%%sql postgresql://…` the connection), a `%%sql` cell with lone `\r` line breaks lost its SQL, and a line magic collapsed spaces inside strings (`'a b'` → `'a b'`). Line breaks around the magic are now kept as written
27
+ - **Multi-line block comments never settled.** In SQL passed to a call, `/* … */` comments spanning lines were indented further on every run, so `--check` always failed
28
+ - `%%` (an escaped `%` in pyformat SQL) followed by `s` was read as a `%s` parameter
29
+ - **Non-ASCII text could break the file.** AST column offsets count UTF-8 bytes, but they were used as character positions, so with `é`, `日本語` or emoji on the same line, a rewrite was cut at the wrong place (e.g. deleting the closing `)` of `cursor.execute("… 'é' …")`). A lone `\r` line break could crash the run; line breaks are now counted like Python does, and an unexpected error skips the cell or file with a warning instead of aborting
30
+ - **F-string fields lost their quotes or format.** `b = '{y}'` became `b = {y}`, conversions and format specs (`{x!r}`, `{y:>3}`) were dropped, and field expressions were rewritten (`{d["k"]}` became `{d['k']}`). Fields are now copied from the source verbatim. F-strings containing backslashes or literal braces are now formatted correctly instead of being skipped
31
+ - **Re-parse check:** formatted SQL is parsed again and must give the same syntax tree as the original (ignoring quoting of names), so a printing bug in sqlglot can't change a query. Before sqlglot 30.23, for example, the second line of a multi-line quoted name was indented
32
+ - **Whole-file safety net:** after all rewrites, the edited source is parsed again and every string literal is compared with the original (untouched ones) or its intended value (rewritten ones). A rewrite that fails the check is dropped and reported with its line number (the others are still applied), with a request for a bug report. Strings that are skipped because they can't be written back safely are now reported as warnings with their line number
33
+ - **`sql_decorators` now works.** The option was parsed and documented but never used by the formatter in any version ([#30](https://github.com/flyersworder/sqlnbfmt/issues/30)). SQL in decorator arguments (`@query("SELECT ...")`) and string literals returned from decorated functions (`@query` above `def f(): return "SELECT ..."`) are now formatted. Decorator names match like function names, and docstrings and returns of nested functions are left alone
34
+ - **Rewritten SQL strings could corrupt the code.** When writing formatted SQL back, sqlnbfmt now keeps the value of the string exactly. In particular:
35
+ - backslash escapes are preserved (`'\\n'` used to become a real newline)
36
+ - the raw `r` prefix is kept (dropping it turned `\_` into an invalid escape)
37
+ - quotes are chosen so they don't clash with the SQL (`cursor.execute('delete from "users"')` produced broken Python, as did `"""` inside the SQL)
38
+ - literal `{{ }}` braces in f-strings are no longer turned into placeholders
39
+
40
+ Each rewritten literal is parsed back and checked; a string that can't be written back faithfully is left unchanged
41
+ - `sql_keywords` entries are now case-insensitive. SQL detection compares against upper-cased code, so a lowercase entry such as `"merge"` was silently never matched
42
+ - Blank entries in `sql_keywords`, `function_names` and `sql_decorators` (and their `extend_*` forms) are rejected. Function names match as substrings, so `""` matched every call
43
+
44
+ ## [0.5.0] - 2026-10-15
45
+
46
+ ### Added
47
+
48
+ - **`pyproject.toml` configuration**: set options in a `[tool.sqlnbfmt]` table instead of a separate YAML file. Without `--config`, sqlnbfmt uses the nearest `pyproject.toml` that has the table, searching up to the repository root. The JupyterLab integration uses the same discovery ([#22](https://github.com/flyersworder/sqlnbfmt/issues/22))
49
+ - `--config` accepts a `.toml` path as well as YAML
50
+ - `dialect` config option (in `[tool.sqlnbfmt]` or YAML), so the dialect no longer has to be passed as `--dialect` on every run; `--dialect` still overrides it
51
+
52
+ ### Changed
53
+
54
+ - Config values are now type-checked: a wrong type (e.g. a string where a list is expected) is reported as an error naming the file. Previously such values were silently mis-parsed or crashed with a traceback
55
+ - Unknown config options now produce a warning instead of being silently ignored
56
+
57
+ ### Fixed
58
+
59
+ - SQL passed to a function call is now indented relative to the line the call is on, and a closing `)` or remaining arguments no longer land at column 0. This was most visible in Marimo cells, where every `mo.sql(...)` sits inside a function. It had regressed in 0.3.0, when the `black` post-processing pass was removed. Code already formatted by an earlier version keeps its layout until its SQL changes
60
+ - The Jupyter server extension no longer overrides a formatter registered in `jupyter_server_config.py` (e.g. `register(dialect="postgres")`), and it now logs configuration errors instead of silently not registering
61
+ - An unknown dialect (e.g. `--dialect postgress`) is now rejected with a suggestion. Previously every SQL string silently failed to parse, so `--check` reported all files as properly formatted
62
+ - The `config.yaml` template was missing the `sql` function name (default since 0.4.0). Because config lists replace the defaults, copying the template silently disabled Marimo `mo.sql()` formatting. The README now states the replace semantics explicitly
63
+ - Upgraded locked dependencies, including sqlglot 29 → 30 (formatting output unchanged across the eval suite)
64
+ - A skipped Jupyter extension registration is now logged at debug level instead of being silently swallowed
65
+
66
+ ## [0.4.2] - 2026-03-24
67
+
68
+ ### Added
69
+
70
+ - **Jupyter server extension**: `pip install sqlnbfmt` now auto-registers with jupyterlab-code-formatter — no manual config needed
71
+
72
+ ### Changed
73
+
74
+ - Switched build system from setuptools to hatchling
75
+ - Updated JupyterLab docs to use `jupyter_server_config.py` (modern) instead of `jupyter_notebook_config.py` (legacy)
76
+
77
+ ## [0.4.0] - 2026-03-24
78
+
79
+ ### Added
80
+
81
+ - **Marimo notebook support**: format SQL in Marimo `.py` notebooks via `mo.sql()` calls — same `sqlnbfmt` command, auto-detected by file extension
82
+ - **Python file support**: `sqlnbfmt` now accepts `.py` files alongside `.ipynb` — works on any Python file containing SQL strings
83
+ - **JupyterLab integration**: `sqlnbfmt.jupyterlab_integration.register()` for use with `jupyterlab-code-formatter`
84
+ - `format_cell_source()` public API for formatting a single cell's source code
85
+ - `_format_python_source()`, `process_python_file()`, `diff_python_file()` public API for programmatic use
86
+ - `sql` added to default recognized function names (matches `mo.sql()`, etc.)
87
+ - `sqlnbfmt-py` and `sqlnbfmt-py-check` pre-commit hooks for Python files
88
+ - Marimo eval fixtures: `marimo_basic_sql`, `marimo_fstring_interpolation`, `marimo_complex_sql`, `marimo_multi_cell_mixed`, `marimo_comment_preservation`, `marimo_multi_statement`, `marimo_skip_hint`, `marimo_non_sql_unchanged`, `marimo_idempotency`
89
+
90
+ ### Fixed
91
+
92
+ - Idempotency: already-formatted SQL in triple-quoted strings (with leading whitespace/indentation) is no longer re-formatted on every run
93
+
94
+ ### Changed
95
+
96
+ - CLI description and help text updated to reflect multi-format support
97
+ - Project description and keywords updated for Marimo discoverability
98
+
99
+ ## [0.3.2] - 2026-03-16
100
+
101
+ ### Fixed
102
+
103
+ - Multiple SQL statements in a single cell are no longer concatenated on one line — each statement now starts on its own line after the `;` separator ([#3](https://github.com/flyersworder/sqlnbfmt/issues/3))
104
+
105
+ ### Changed
106
+
107
+ - SQL parsing switched from `sqlglot.parse_one()` to `sqlglot.parse()` to correctly handle multi-statement inputs
108
+
109
+ ### Added
110
+
111
+ - `multi_statement` eval fixture case covering `DROP TABLE` + `CREATE TABLE` sequences
112
+
113
+ ## [0.3.1] - 2026-03-15
114
+
115
+ ### Fixed
116
+
117
+ - `diff_notebook()` no longer duplicates cell-processing logic — extracted shared `_format_cells()` helper to prevent future divergence
118
+ - `--check --diff` combined mode no longer double-counts notebooks or processes them twice
119
+ - Skip hint (`# sqlnbfmt: skip`) no longer triggers on string literals containing the directive — only dedicated comment lines are recognised
120
+ - `load_config()` with a partial config file (missing keys) now falls back to built-in defaults instead of empty sets
121
+ - `load_config()` with an empty YAML file no longer raises `AttributeError`
122
+ - In-function SQL indentation now respects `indent_width` config instead of hardcoded 4 spaces
123
+
124
+ ### Added
125
+
126
+ - `main()` integration tests for `--diff`, `--check`, and `--check --diff` exit code behavior
127
+ - Docstring on `_has_skip_hint()` clarifying that inline trailing comments are intentionally ignored
128
+
129
+ ## [0.3.0] - 2026-03-15
130
+
131
+ ### Added
132
+
133
+ - **Zero-config defaults**: sqlnbfmt now works out of the box without a `config.yaml` file — built-in defaults for SQL keywords, function names, and decorators
134
+ - **`--check` mode**: verify formatting without modifying files; exits with code 1 if changes are needed (CI-friendly)
135
+ - **`--diff` mode**: print a unified diff of formatting changes without modifying files
136
+ - **Skip hints**: add `# sqlnbfmt: skip` to any cell to skip formatting
137
+ - **`sqlnbfmt-check` pre-commit hook**: a read-only hook variant for CI pipelines
138
+ - Cell index in all warning/debug messages for easier debugging
139
+ - PyPI keywords and project URLs in package metadata
140
+ - `skip_hint` eval fixture case
141
+
142
+ ### Changed
143
+
144
+ - `--config` argument is now optional (defaults to built-in settings instead of requiring `config.yaml`)
145
+ - `load_config()` accepts `None` to return defaults; passing a missing path still raises `FileNotFoundError`
146
+ - Version bumped to 0.3.0
147
+
148
+ ## [0.2.0] - 2026-03-15
149
+
150
+ ### Fixed
151
+
152
+ - Python comments in code cells are no longer stripped during formatting ([#2](https://github.com/flyersworder/sqlnbfmt/issues/2))
153
+
154
+ ### Changed
155
+
156
+ - Replaced AST-mutation approach (`ast.NodeTransformer` + `astor` + `black`) with surgical string replacement (`ast.NodeVisitor` + offset-based text splicing), preserving all non-SQL content exactly
157
+ - Removed `astor` and `black` from dependencies — fewer install requirements
158
+ - CI publish now uses PyPI Trusted Publishers (OIDC) instead of API tokens
159
+ - Migrated `tool.uv.dev-dependencies` to `dependency-groups.dev` (PEP 735)
160
+ - Updated `actions/checkout` from v3 to v4
161
+
162
+ ### Added
163
+
164
+ - Python 3.13 classifier
165
+ - Comment-preservation test case
166
+
167
+ ### Fixed
168
+
169
+ - Typo in config.yaml: `exeucte_query` → `execute_query`
170
+
171
+ ## [0.1.9] - 2025-01-01
172
+
173
+ ### Changed
174
+
175
+ - Updated dependencies (attrs, black, referencing, sqlglot, typing-extensions)
176
+
177
+ ## [0.1.8] - 2024-12-01
178
+
179
+ ### Fixed
180
+
181
+ - First-line indentation error for in-function SQL
182
+ - New-line handling issues in formatted output
183
+ - Introduced `black` formatter to resolve in-function SQL formatting edge cases
184
+
185
+ ## [0.1.7] - 2024-11-01
186
+
187
+ ### Added
188
+
189
+ - Pre-commit hook support
190
+ - F-string SQL formatting (with and without variables)
191
+ - In-function SQL formatting (e.g., `pd.read_sql(...)`)
192
+
193
+ ## [0.1.0] - 2024-10-01
194
+
195
+ ### Added
196
+
197
+ - Initial release
198
+ - SQL formatting in Jupyter notebook code cells using sqlglot
199
+ - Support for `%sql` / `%%sql` magic commands
200
+ - Configurable SQL keywords, function names, and formatting options
201
+ - GitHub Actions CI pipeline
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sqlnbfmt
3
- Version: 0.4.2
3
+ Version: 0.6.0
4
4
  Summary: A tool to format SQL code in Jupyter notebooks and Marimo notebooks.
5
5
  Project-URL: Homepage, https://github.com/flyersworder/sqlnbfmt
6
6
  Project-URL: Repository, https://github.com/flyersworder/sqlnbfmt
@@ -104,7 +104,7 @@ pip install sqlnbfmt jupyterlab-code-formatter
104
104
 
105
105
  That's it — sqlnbfmt auto-registers as a Jupyter server extension. In JupyterLab, use **Edit > Format Cell** or configure format-on-save in Settings > Code Formatter, and select "Apply SQL Notebook Formatter".
106
106
 
107
- To customize the dialect, add this to `~/.jupyter/jupyter_server_config.py`:
107
+ The extension uses `[tool.sqlnbfmt]` (including `dialect`) from the nearest `pyproject.toml` of the directory you start Jupyter in. To set the dialect explicitly instead, add this to `~/.jupyter/jupyter_server_config.py`:
108
108
  ```python
109
109
  from sqlnbfmt.jupyterlab_integration import register
110
110
  register(dialect="postgres")
@@ -121,7 +121,7 @@ pip install pre-commit
121
121
  ```yaml
122
122
  repos:
123
123
  - repo: https://github.com/flyersworder/sqlnbfmt
124
- rev: v0.4.0
124
+ rev: v0.6.0
125
125
  hooks:
126
126
  - id: sqlnbfmt
127
127
  types: [jupyter]
@@ -133,7 +133,7 @@ To also format Marimo notebooks (Python files), add the Python hook:
133
133
  files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
134
134
  ```
135
135
 
136
- All arguments are optional. To specify a dialect or custom config:
136
+ All arguments are optional. Settings in `[tool.sqlnbfmt]` in your `pyproject.toml` are picked up automatically (see [Configuration](#configuration)); alternatively pass them as arguments:
137
137
  ```yaml
138
138
  args: [--dialect, postgres, --config, config.yaml]
139
139
  ```
@@ -162,19 +162,52 @@ Use `--check` in GitHub Actions to enforce formatting:
162
162
 
163
163
  ## Configuration
164
164
 
165
- `sqlnbfmt` works without any configuration file. A `config.yaml` is only needed to override defaults.
165
+ `sqlnbfmt` works without any configuration. To override defaults, use a `[tool.sqlnbfmt]` table in `pyproject.toml` or a separate YAML file.
166
166
 
167
- Create a `config.yaml` file to customize formatting behavior. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template.
167
+ ### pyproject.toml
168
+
169
+ ```toml
170
+ [tool.sqlnbfmt]
171
+ dialect = "postgres"
172
+ extend_function_names = ["run_query"] # recognized in addition to the defaults
173
+
174
+ [tool.sqlnbfmt.formatting_options]
175
+ indent_width = 2
176
+ ```
177
+
178
+ `sqlnbfmt` searches the current directory and its parents for the nearest `pyproject.toml` with a `[tool.sqlnbfmt]` table, stopping at the repository root (the directory containing `.git`). A `pyproject.toml` without that table is skipped, so a sub-package's file doesn't hide the repository-level configuration. The JupyterLab integration does the same, starting from the Jupyter server's working directory.
179
+
180
+ ### YAML file
181
+
182
+ Pass a YAML file with `--config config.yaml`. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template. `--config` also accepts a `.toml` path, which is read from its `[tool.sqlnbfmt]` table.
183
+
184
+ When `--config` is given, `pyproject.toml` discovery is skipped. Options you don't set keep their defaults, and unknown option names produce a warning.
168
185
 
169
186
  ### Configuration Options
170
187
 
171
188
  | Option | Description | Default |
172
189
  |--------|-------------|---------|
190
+ | `dialect` | SQL dialect passed to sqlglot (e.g. `postgres`, `mysql`, `bigquery`); `--dialect` overrides it | none (generic SQL) |
173
191
  | `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
174
- | `function_names` | Python functions containing SQL code | `read_sql`, `execute`, etc. |
175
- | `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
176
- | `single_line_threshold` | Maximum length before splitting SQL | 80 |
177
- | `indent_width` | Number of spaces for indentation | 4 |
192
+ | `function_names` | Python functions containing SQL code (see [matching](#how-function-and-decorator-names-match)) | `read_sql`, `execute`, `sql` (Marimo's `mo.sql`), etc. |
193
+ | `sql_decorators` | Decorators whose SQL is formatted: string arguments of `@query("SELECT ...")`, and strings returned by a function decorated with `@query` | `query`, `db_query`, `sql_query`, `sql_decorator` |
194
+ | `extend_sql_keywords`, `extend_function_names`, `extend_sql_decorators` | Entries added to the corresponding list instead of replacing it | none |
195
+ | `formatting_options.single_line_threshold` | **Deprecated:** never had any effect and will be removed. Line breaks are decided by sqlglot | |
196
+ | `formatting_options.indent_width` | Number of spaces for indentation | 4 |
197
+
198
+ To add to the built-in lists, use the `extend_*` options. `sql_keywords`, `function_names` and `sql_decorators` **replace** the built-in lists, so setting `function_names = ["run_query"]` drops `sql`, and Marimo's `mo.sql()` calls stop being formatted. If you do replace a list, copy the defaults you still rely on from the [template](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml). You can set both: the `extend_*` entries are added to your replacement list.
199
+
200
+ ### How function and decorator names match
201
+
202
+ A call counts as SQL-bearing when a configured name appears **anywhere** in its full dotted name. Matching is case-sensitive. This is deliberately permissive, so that wrappers and variants are covered without configuration:
203
+
204
+ - `execute` matches `cursor.execute`, `cursor.executemany` and `execute_sql`
205
+ - `sql` matches `mo.sql`, `spark.sql`, pandasql's `sqldf`/`pysqldf` and `sqlalchemy.text`
206
+ - `sql` does **not** match `runSQLQuery`, because matching is case-sensitive; add `SQL` if you need it
207
+
208
+ Decorator names in `sql_decorators` match the same way, against the decorator's dotted name (`query` matches `@query`, `@db_query` and `@app.query(...)`).
209
+
210
+ A matched call's string arguments are only reformatted if they look like SQL. Still, prefer specific names: `run` would also match `rerun(...)` and `subprocess.run(...)`, so use `run_query` rather than `run`.
178
211
 
179
212
  ## Examples
180
213
 
@@ -187,20 +220,20 @@ execute_sql("""SELECT a.col1, b.col2 FROM table_a a JOIN table_b b ON a.id = b.a
187
220
 
188
221
  After formatting:
189
222
  ```python
190
- execute_sql("""
191
- SELECT
192
- a.col1,
193
- b.col2
194
- FROM
195
- table_a AS a
196
- JOIN
197
- table_b AS b
198
- ON a.id = b.a_id
199
- WHERE
200
- a.status = 'active'
201
- ORDER BY
202
- a.created_at DESC
203
- """)
223
+ execute_sql(
224
+ """
225
+ SELECT
226
+ a.col1,
227
+ b.col2
228
+ FROM table_a AS a
229
+ JOIN table_b AS b
230
+ ON a.id = b.a_id
231
+ WHERE
232
+ a.status = 'active'
233
+ ORDER BY
234
+ a.created_at DESC
235
+ """
236
+ )
204
237
  ```
205
238
 
206
239
  ### Marimo Notebook
@@ -218,17 +251,17 @@ After formatting:
218
251
  @app.cell
219
252
  def _(mo):
220
253
  _df = mo.sql(
221
- f"""
222
- SELECT
223
- id,
224
- name
225
- FROM users
226
- WHERE
227
- active = 1
228
- ORDER BY
229
- name
230
- """
231
- )
254
+ f"""
255
+ SELECT
256
+ id,
257
+ name
258
+ FROM users
259
+ WHERE
260
+ active = 1
261
+ ORDER BY
262
+ name
263
+ """
264
+ )
232
265
  return
233
266
  ```
234
267
 
@@ -236,7 +269,7 @@ def _(mo):
236
269
 
237
270
  **SQL not being formatted?**
238
271
  - Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
239
- - Check that the function name is in the recognized list (use `--config` to add custom ones)
272
+ - Check that the function name is in the recognized list (add custom ones via `extend_function_names` in `[tool.sqlnbfmt]` or your `--config` file)
240
273
 
241
274
  **Comments being modified?**
242
275
  - Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
@@ -264,6 +297,11 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
264
297
  3. Run tests:
265
298
  ```bash
266
299
  pytest
300
+ ```
301
+
302
+ Before a release, also run the live end-to-end tests. They build the wheel, install it into a fresh virtualenv with Jupyter Server and jupyterlab-code-formatter, and exercise the CLI, the pre-commit hooks and the Jupyter extension against a scratch project. They need `uv`, `git` and network access, so the default `pytest` run skips them:
303
+ ```bash
304
+ uv run pytest -m live
267
305
  ```
268
306
 
269
307
  4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
@@ -76,7 +76,7 @@ pip install sqlnbfmt jupyterlab-code-formatter
76
76
 
77
77
  That's it — sqlnbfmt auto-registers as a Jupyter server extension. In JupyterLab, use **Edit > Format Cell** or configure format-on-save in Settings > Code Formatter, and select "Apply SQL Notebook Formatter".
78
78
 
79
- To customize the dialect, add this to `~/.jupyter/jupyter_server_config.py`:
79
+ The extension uses `[tool.sqlnbfmt]` (including `dialect`) from the nearest `pyproject.toml` of the directory you start Jupyter in. To set the dialect explicitly instead, add this to `~/.jupyter/jupyter_server_config.py`:
80
80
  ```python
81
81
  from sqlnbfmt.jupyterlab_integration import register
82
82
  register(dialect="postgres")
@@ -93,7 +93,7 @@ pip install pre-commit
93
93
  ```yaml
94
94
  repos:
95
95
  - repo: https://github.com/flyersworder/sqlnbfmt
96
- rev: v0.4.0
96
+ rev: v0.6.0
97
97
  hooks:
98
98
  - id: sqlnbfmt
99
99
  types: [jupyter]
@@ -105,7 +105,7 @@ To also format Marimo notebooks (Python files), add the Python hook:
105
105
  files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
106
106
  ```
107
107
 
108
- All arguments are optional. To specify a dialect or custom config:
108
+ All arguments are optional. Settings in `[tool.sqlnbfmt]` in your `pyproject.toml` are picked up automatically (see [Configuration](#configuration)); alternatively pass them as arguments:
109
109
  ```yaml
110
110
  args: [--dialect, postgres, --config, config.yaml]
111
111
  ```
@@ -134,19 +134,52 @@ Use `--check` in GitHub Actions to enforce formatting:
134
134
 
135
135
  ## Configuration
136
136
 
137
- `sqlnbfmt` works without any configuration file. A `config.yaml` is only needed to override defaults.
137
+ `sqlnbfmt` works without any configuration. To override defaults, use a `[tool.sqlnbfmt]` table in `pyproject.toml` or a separate YAML file.
138
138
 
139
- Create a `config.yaml` file to customize formatting behavior. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template.
139
+ ### pyproject.toml
140
+
141
+ ```toml
142
+ [tool.sqlnbfmt]
143
+ dialect = "postgres"
144
+ extend_function_names = ["run_query"] # recognized in addition to the defaults
145
+
146
+ [tool.sqlnbfmt.formatting_options]
147
+ indent_width = 2
148
+ ```
149
+
150
+ `sqlnbfmt` searches the current directory and its parents for the nearest `pyproject.toml` with a `[tool.sqlnbfmt]` table, stopping at the repository root (the directory containing `.git`). A `pyproject.toml` without that table is skipped, so a sub-package's file doesn't hide the repository-level configuration. The JupyterLab integration does the same, starting from the Jupyter server's working directory.
151
+
152
+ ### YAML file
153
+
154
+ Pass a YAML file with `--config config.yaml`. [Here](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml) is a template. `--config` also accepts a `.toml` path, which is read from its `[tool.sqlnbfmt]` table.
155
+
156
+ When `--config` is given, `pyproject.toml` discovery is skipped. Options you don't set keep their defaults, and unknown option names produce a warning.
140
157
 
141
158
  ### Configuration Options
142
159
 
143
160
  | Option | Description | Default |
144
161
  |--------|-------------|---------|
162
+ | `dialect` | SQL dialect passed to sqlglot (e.g. `postgres`, `mysql`, `bigquery`); `--dialect` overrides it | none (generic SQL) |
145
163
  | `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
146
- | `function_names` | Python functions containing SQL code | `read_sql`, `execute`, etc. |
147
- | `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
148
- | `single_line_threshold` | Maximum length before splitting SQL | 80 |
149
- | `indent_width` | Number of spaces for indentation | 4 |
164
+ | `function_names` | Python functions containing SQL code (see [matching](#how-function-and-decorator-names-match)) | `read_sql`, `execute`, `sql` (Marimo's `mo.sql`), etc. |
165
+ | `sql_decorators` | Decorators whose SQL is formatted: string arguments of `@query("SELECT ...")`, and strings returned by a function decorated with `@query` | `query`, `db_query`, `sql_query`, `sql_decorator` |
166
+ | `extend_sql_keywords`, `extend_function_names`, `extend_sql_decorators` | Entries added to the corresponding list instead of replacing it | none |
167
+ | `formatting_options.single_line_threshold` | **Deprecated:** never had any effect and will be removed. Line breaks are decided by sqlglot | |
168
+ | `formatting_options.indent_width` | Number of spaces for indentation | 4 |
169
+
170
+ To add to the built-in lists, use the `extend_*` options. `sql_keywords`, `function_names` and `sql_decorators` **replace** the built-in lists, so setting `function_names = ["run_query"]` drops `sql`, and Marimo's `mo.sql()` calls stop being formatted. If you do replace a list, copy the defaults you still rely on from the [template](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml). You can set both: the `extend_*` entries are added to your replacement list.
171
+
172
+ ### How function and decorator names match
173
+
174
+ A call counts as SQL-bearing when a configured name appears **anywhere** in its full dotted name. Matching is case-sensitive. This is deliberately permissive, so that wrappers and variants are covered without configuration:
175
+
176
+ - `execute` matches `cursor.execute`, `cursor.executemany` and `execute_sql`
177
+ - `sql` matches `mo.sql`, `spark.sql`, pandasql's `sqldf`/`pysqldf` and `sqlalchemy.text`
178
+ - `sql` does **not** match `runSQLQuery`, because matching is case-sensitive; add `SQL` if you need it
179
+
180
+ Decorator names in `sql_decorators` match the same way, against the decorator's dotted name (`query` matches `@query`, `@db_query` and `@app.query(...)`).
181
+
182
+ A matched call's string arguments are only reformatted if they look like SQL. Still, prefer specific names: `run` would also match `rerun(...)` and `subprocess.run(...)`, so use `run_query` rather than `run`.
150
183
 
151
184
  ## Examples
152
185
 
@@ -159,20 +192,20 @@ execute_sql("""SELECT a.col1, b.col2 FROM table_a a JOIN table_b b ON a.id = b.a
159
192
 
160
193
  After formatting:
161
194
  ```python
162
- execute_sql("""
163
- SELECT
164
- a.col1,
165
- b.col2
166
- FROM
167
- table_a AS a
168
- JOIN
169
- table_b AS b
170
- ON a.id = b.a_id
171
- WHERE
172
- a.status = 'active'
173
- ORDER BY
174
- a.created_at DESC
175
- """)
195
+ execute_sql(
196
+ """
197
+ SELECT
198
+ a.col1,
199
+ b.col2
200
+ FROM table_a AS a
201
+ JOIN table_b AS b
202
+ ON a.id = b.a_id
203
+ WHERE
204
+ a.status = 'active'
205
+ ORDER BY
206
+ a.created_at DESC
207
+ """
208
+ )
176
209
  ```
177
210
 
178
211
  ### Marimo Notebook
@@ -190,17 +223,17 @@ After formatting:
190
223
  @app.cell
191
224
  def _(mo):
192
225
  _df = mo.sql(
193
- f"""
194
- SELECT
195
- id,
196
- name
197
- FROM users
198
- WHERE
199
- active = 1
200
- ORDER BY
201
- name
202
- """
203
- )
226
+ f"""
227
+ SELECT
228
+ id,
229
+ name
230
+ FROM users
231
+ WHERE
232
+ active = 1
233
+ ORDER BY
234
+ name
235
+ """
236
+ )
204
237
  return
205
238
  ```
206
239
 
@@ -208,7 +241,7 @@ def _(mo):
208
241
 
209
242
  **SQL not being formatted?**
210
243
  - Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
211
- - Check that the function name is in the recognized list (use `--config` to add custom ones)
244
+ - Check that the function name is in the recognized list (add custom ones via `extend_function_names` in `[tool.sqlnbfmt]` or your `--config` file)
212
245
 
213
246
  **Comments being modified?**
214
247
  - Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
@@ -236,6 +269,11 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
236
269
  3. Run tests:
237
270
  ```bash
238
271
  pytest
272
+ ```
273
+
274
+ Before a release, also run the live end-to-end tests. They build the wheel, install it into a fresh virtualenv with Jupyter Server and jupyterlab-code-formatter, and exercise the CLI, the pre-commit hooks and the Jupyter extension against a scratch project. They need `uv`, `git` and network access, so the default `pytest` run skips them:
275
+ ```bash
276
+ uv run pytest -m live
239
277
  ```
240
278
 
241
279
  4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.