sqlnbfmt 0.5.0__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 (89) hide show
  1. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.github/workflows/ci-and-publish.yml +2 -0
  2. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/CHANGELOG.md +36 -0
  3. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/PKG-INFO +26 -7
  4. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/README.md +25 -6
  5. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/config.yaml +5 -1
  6. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/pyproject.toml +4 -2
  7. sqlnbfmt-0.6.0/sqlnbfmt/formatter.py +1534 -0
  8. sqlnbfmt-0.6.0/tests/eval/python_sql_decorators/expected.py +56 -0
  9. sqlnbfmt-0.6.0/tests/eval/python_sql_decorators/input.py +27 -0
  10. sqlnbfmt-0.6.0/tests/test_function_name_matching.py +63 -0
  11. sqlnbfmt-0.6.0/tests/test_live.py +284 -0
  12. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_pyproject_config.py +158 -1
  13. sqlnbfmt-0.6.0/tests/test_round_trip.py +642 -0
  14. sqlnbfmt-0.6.0/tests/test_sql_decorators.py +136 -0
  15. sqlnbfmt-0.6.0/tests/test_string_literals.py +131 -0
  16. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/uv.lock +1 -1
  17. sqlnbfmt-0.5.0/sqlnbfmt/formatter.py +0 -1015
  18. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.github/dependabot.yml +0 -0
  19. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.gitignore +0 -0
  20. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.pre-commit-config.yaml +0 -0
  21. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.pre-commit-hooks.yaml +0 -0
  22. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/.python-version +0 -0
  23. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/LICENSE +0 -0
  24. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/SECURITY.md +0 -0
  25. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/jupyter-config/sqlnbfmt.json +0 -0
  26. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/sqlnbfmt/__init__.py +0 -0
  27. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/sqlnbfmt/jupyterlab_integration.py +0 -0
  28. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/__init__.py +0 -0
  29. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/README.md +0 -0
  30. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/basic_select/expected.ipynb +0 -0
  31. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/basic_select/input.ipynb +0 -0
  32. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/case_expression/expected.ipynb +0 -0
  33. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/case_expression/input.ipynb +0 -0
  34. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/comment_preservation/expected.ipynb +0 -0
  35. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/comment_preservation/input.ipynb +0 -0
  36. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/cte/expected.ipynb +0 -0
  37. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/cte/input.ipynb +0 -0
  38. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/fstring_variables/expected.ipynb +0 -0
  39. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/fstring_variables/input.ipynb +0 -0
  40. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/generate_fixtures.py +0 -0
  41. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/group_by_having/expected.ipynb +0 -0
  42. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/group_by_having/input.ipynb +0 -0
  43. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/idempotency/expected.ipynb +0 -0
  44. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/idempotency/input.ipynb +0 -0
  45. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/in_function_sql/expected.ipynb +0 -0
  46. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/in_function_sql/input.ipynb +0 -0
  47. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/joins/expected.ipynb +0 -0
  48. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/joins/input.ipynb +0 -0
  49. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/magic_commands/expected.ipynb +0 -0
  50. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/magic_commands/input.ipynb +0 -0
  51. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_basic_sql/expected.py +0 -0
  52. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_basic_sql/input.py +0 -0
  53. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_comment_preservation/expected.py +0 -0
  54. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_comment_preservation/input.py +0 -0
  55. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_complex_sql/expected.py +0 -0
  56. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_complex_sql/input.py +0 -0
  57. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_fstring_interpolation/expected.py +0 -0
  58. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_fstring_interpolation/input.py +0 -0
  59. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_idempotency/expected.py +0 -0
  60. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_idempotency/input.py +0 -0
  61. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_cell_mixed/expected.py +0 -0
  62. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_cell_mixed/input.py +0 -0
  63. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_statement/expected.py +0 -0
  64. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_multi_statement/input.py +0 -0
  65. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_non_sql_unchanged/expected.py +0 -0
  66. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_non_sql_unchanged/input.py +0 -0
  67. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_skip_hint/expected.py +0 -0
  68. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/marimo_skip_hint/input.py +0 -0
  69. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/multi_cell_mixed/expected.ipynb +0 -0
  70. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/multi_cell_mixed/input.ipynb +0 -0
  71. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/multi_statement/expected.ipynb +0 -0
  72. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/multi_statement/input.ipynb +0 -0
  73. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/non_sql_unchanged/expected.ipynb +0 -0
  74. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/non_sql_unchanged/input.ipynb +0 -0
  75. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/skip_hint/expected.ipynb +0 -0
  76. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/skip_hint/input.ipynb +0 -0
  77. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/subquery/expected.ipynb +0 -0
  78. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/subquery/input.ipynb +0 -0
  79. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/union/expected.ipynb +0 -0
  80. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/union/input.ipynb +0 -0
  81. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/window_function/expected.ipynb +0 -0
  82. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/eval/window_function/input.ipynb +0 -0
  83. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_call_layout.py +0 -0
  84. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_eval.py +0 -0
  85. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_formatter.py +0 -0
  86. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_jupyterlab_integration.py +0 -0
  87. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_marimo.py +0 -0
  88. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_notebook.ipynb +0 -0
  89. {sqlnbfmt-0.5.0 → sqlnbfmt-0.6.0}/tests/test_notebook_copy.ipynb +0 -0
@@ -29,6 +29,8 @@ jobs:
29
29
  - run: uv python install 3.12
30
30
  - run: uv sync --all-extras --dev --frozen
31
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
32
34
 
33
35
  security:
34
36
  runs-on: ubuntu-latest
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
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
+
8
44
  ## [0.5.0] - 2026-10-15
9
45
 
10
46
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlnbfmt
3
- Version: 0.5.0
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
@@ -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.5.0
124
+ rev: v0.6.0
125
125
  hooks:
126
126
  - id: sqlnbfmt
127
127
  types: [jupyter]
@@ -169,6 +169,7 @@ Use `--check` in GitHub Actions to enforce formatting:
169
169
  ```toml
170
170
  [tool.sqlnbfmt]
171
171
  dialect = "postgres"
172
+ extend_function_names = ["run_query"] # recognized in addition to the defaults
172
173
 
173
174
  [tool.sqlnbfmt.formatting_options]
174
175
  indent_width = 2
@@ -188,12 +189,25 @@ When `--config` is given, `pyproject.toml` discovery is skipped. Options you don
188
189
  |--------|-------------|---------|
189
190
  | `dialect` | SQL dialect passed to sqlglot (e.g. `postgres`, `mysql`, `bigquery`); `--dialect` overrides it | none (generic SQL) |
190
191
  | `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
191
- | `function_names` | Python functions containing SQL code | `read_sql`, `execute`, `sql` (Marimo's `mo.sql`), etc. |
192
- | `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
193
- | `formatting_options.single_line_threshold` | Maximum length before splitting SQL | 80 |
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 | |
194
196
  | `formatting_options.indent_width` | Number of spaces for indentation | 4 |
195
197
 
196
- `sql_keywords`, `function_names` and `sql_decorators` **replace** the built-in lists rather than extending them. Copy the defaults you still rely on from the [template](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml); for example, keep `sql` in `function_names` or Marimo's `mo.sql()` calls stop being formatted.
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`.
197
211
 
198
212
  ## Examples
199
213
 
@@ -255,7 +269,7 @@ def _(mo):
255
269
 
256
270
  **SQL not being formatted?**
257
271
  - Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
258
- - Check that the function name is in the recognized list (add custom ones via `function_names` in `[tool.sqlnbfmt]` or `--config`)
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)
259
273
 
260
274
  **Comments being modified?**
261
275
  - Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
@@ -283,6 +297,11 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
283
297
  3. Run tests:
284
298
  ```bash
285
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
286
305
  ```
287
306
 
288
307
  4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
@@ -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.5.0
96
+ rev: v0.6.0
97
97
  hooks:
98
98
  - id: sqlnbfmt
99
99
  types: [jupyter]
@@ -141,6 +141,7 @@ Use `--check` in GitHub Actions to enforce formatting:
141
141
  ```toml
142
142
  [tool.sqlnbfmt]
143
143
  dialect = "postgres"
144
+ extend_function_names = ["run_query"] # recognized in addition to the defaults
144
145
 
145
146
  [tool.sqlnbfmt.formatting_options]
146
147
  indent_width = 2
@@ -160,12 +161,25 @@ When `--config` is given, `pyproject.toml` discovery is skipped. Options you don
160
161
  |--------|-------------|---------|
161
162
  | `dialect` | SQL dialect passed to sqlglot (e.g. `postgres`, `mysql`, `bigquery`); `--dialect` overrides it | none (generic SQL) |
162
163
  | `sql_keywords` | SQL keywords to recognize and format | Common SQL keywords |
163
- | `function_names` | Python functions containing SQL code | `read_sql`, `execute`, `sql` (Marimo's `mo.sql`), etc. |
164
- | `sql_decorators` | Decorators indicating SQL code | `query`, `sql_query`, etc. |
165
- | `formatting_options.single_line_threshold` | Maximum length before splitting SQL | 80 |
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 | |
166
168
  | `formatting_options.indent_width` | Number of spaces for indentation | 4 |
167
169
 
168
- `sql_keywords`, `function_names` and `sql_decorators` **replace** the built-in lists rather than extending them. Copy the defaults you still rely on from the [template](https://github.com/flyersworder/sqlnbfmt/blob/main/config.yaml); for example, keep `sql` in `function_names` or Marimo's `mo.sql()` calls stop being formatted.
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`.
169
183
 
170
184
  ## Examples
171
185
 
@@ -227,7 +241,7 @@ def _(mo):
227
241
 
228
242
  **SQL not being formatted?**
229
243
  - Ensure the string contains at least 2 SQL keywords or a recognizable pattern like `SELECT...FROM`
230
- - Check that the function name is in the recognized list (add custom ones via `function_names` in `[tool.sqlnbfmt]` or `--config`)
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)
231
245
 
232
246
  **Comments being modified?**
233
247
  - Python comments (`#`) are preserved. SQL comments (`--`) inside strings are converted to `/* */` block comments by sqlglot.
@@ -255,6 +269,11 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
255
269
  3. Run tests:
256
270
  ```bash
257
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
258
277
  ```
259
278
 
260
279
  4. Add eval cases: see `tests/eval/generate_fixtures.py` for examples. Run `python tests/eval/generate_fixtures.py` to regenerate fixtures.
@@ -28,6 +28,11 @@ sql_keywords:
28
28
  - OFFSET
29
29
  - DISTINCT
30
30
 
31
+ # To add to the defaults instead of replacing them, use extend_function_names
32
+ # (and extend_sql_keywords / extend_sql_decorators), e.g.:
33
+ # extend_function_names:
34
+ # - run_query
35
+
31
36
  function_names:
32
37
  - read_sql
33
38
  - read_sql_query
@@ -49,5 +54,4 @@ sql_decorators:
49
54
 
50
55
  # Formatting options
51
56
  formatting_options:
52
- single_line_threshold: 80 # Maximum length for single-line SQL
53
57
  indent_width: 4 # Number of spaces for indentation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlnbfmt"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "A tool to format SQL code in Jupyter notebooks and Marimo notebooks."
5
5
  authors = [
6
6
  { name = "Qing", email = "qingye779@gmail.com" }
@@ -49,8 +49,10 @@ build-backend = "hatchling.build"
49
49
 
50
50
  [tool.pytest.ini_options]
51
51
  markers = [
52
- "integration: mark a test as an integration test."
52
+ "integration: mark a test as an integration test.",
53
+ "live: end-to-end tests against the built wheel; slow, need uv, git and network (run with `pytest -m live`)",
53
54
  ]
55
+ addopts = "-m 'not live'"
54
56
 
55
57
  [tool.hatch.build.targets.wheel.shared-data]
56
58
  "jupyter-config/sqlnbfmt.json" = "etc/jupyter/jupyter_server_config.d/sqlnbfmt.json"