jarify 0.1.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 (94) hide show
  1. jarify-0.1.0/.github/workflows/ci.yml +29 -0
  2. jarify-0.1.0/.github/workflows/publish.yml +59 -0
  3. jarify-0.1.0/.gitignore +8 -0
  4. jarify-0.1.0/.python-version +1 -0
  5. jarify-0.1.0/AGENTS.md +46 -0
  6. jarify-0.1.0/LICENSE +21 -0
  7. jarify-0.1.0/PKG-INFO +148 -0
  8. jarify-0.1.0/README.md +123 -0
  9. jarify-0.1.0/docs/sql-style-guide.md +904 -0
  10. jarify-0.1.0/mise.toml +24 -0
  11. jarify-0.1.0/pyproject.toml +56 -0
  12. jarify-0.1.0/src/jarify/__init__.py +3 -0
  13. jarify-0.1.0/src/jarify/cli.py +238 -0
  14. jarify-0.1.0/src/jarify/config.py +84 -0
  15. jarify-0.1.0/src/jarify/formatter.py +147 -0
  16. jarify-0.1.0/src/jarify/generator.py +828 -0
  17. jarify-0.1.0/src/jarify/linter.py +29 -0
  18. jarify-0.1.0/src/jarify/parser.py +98 -0
  19. jarify-0.1.0/src/jarify/rules/__init__.py +62 -0
  20. jarify-0.1.0/src/jarify/rules/base.py +28 -0
  21. jarify-0.1.0/src/jarify/rules/consistent_empty_array.py +65 -0
  22. jarify-0.1.0/src/jarify/rules/cte_naming.py +66 -0
  23. jarify-0.1.0/src/jarify/rules/duckdb_prefer_qualify.py +60 -0
  24. jarify-0.1.0/src/jarify/rules/duckdb_type_style.py +57 -0
  25. jarify-0.1.0/src/jarify/rules/keyword_case.py +32 -0
  26. jarify-0.1.0/src/jarify/rules/no_implicit_cross_join.py +44 -0
  27. jarify-0.1.0/src/jarify/rules/no_select_star.py +43 -0
  28. jarify-0.1.0/src/jarify/rules/no_select_star_in_cte.py +48 -0
  29. jarify-0.1.0/src/jarify/rules/no_unused_cte.py +56 -0
  30. jarify-0.1.0/src/jarify/rules/prefer_group_by_all.py +59 -0
  31. jarify-0.1.0/src/jarify/rules/prefer_using_over_on.py +70 -0
  32. jarify-0.1.0/src/jarify/rules/trailing_commas.py +29 -0
  33. jarify-0.1.0/src/jarify/types.py +29 -0
  34. jarify-0.1.0/tests/__init__.py +0 -0
  35. jarify-0.1.0/tests/conftest.py +17 -0
  36. jarify-0.1.0/tests/fixtures/complex/real_world.expected.sql +78 -0
  37. jarify-0.1.0/tests/fixtures/complex/real_world.input.sql +1 -0
  38. jarify-0.1.0/tests/fixtures/create_table/basic.expected.sql +20 -0
  39. jarify-0.1.0/tests/fixtures/create_table/basic.input.sql +20 -0
  40. jarify-0.1.0/tests/fixtures/create_table/inline_primary_key.expected.sql +7 -0
  41. jarify-0.1.0/tests/fixtures/create_table/inline_primary_key.input.sql +7 -0
  42. jarify-0.1.0/tests/fixtures/create_table/simple_no_constraints.expected.sql +7 -0
  43. jarify-0.1.0/tests/fixtures/create_table/simple_no_constraints.input.sql +2 -0
  44. jarify-0.1.0/tests/fixtures/cte/basic_cte.expected.sql +21 -0
  45. jarify-0.1.0/tests/fixtures/cte/basic_cte.input.sql +1 -0
  46. jarify-0.1.0/tests/fixtures/cte/cte_inline_comment.expected.sql +13 -0
  47. jarify-0.1.0/tests/fixtures/cte/cte_inline_comment.input.sql +6 -0
  48. jarify-0.1.0/tests/fixtures/cte/in_subquery.expected.sql +16 -0
  49. jarify-0.1.0/tests/fixtures/cte/in_subquery.input.sql +3 -0
  50. jarify-0.1.0/tests/fixtures/duckdb/agg_window_uppercase.expected.sql +12 -0
  51. jarify-0.1.0/tests/fixtures/duckdb/agg_window_uppercase.input.sql +11 -0
  52. jarify-0.1.0/tests/fixtures/duckdb/create_macro.expected.sql +15 -0
  53. jarify-0.1.0/tests/fixtures/duckdb/create_macro.input.sql +1 -0
  54. jarify-0.1.0/tests/fixtures/duckdb/distinct_in_aggregates.expected.sql +10 -0
  55. jarify-0.1.0/tests/fixtures/duckdb/distinct_in_aggregates.input.sql +1 -0
  56. jarify-0.1.0/tests/fixtures/duckdb/pivot.expected.sql +5 -0
  57. jarify-0.1.0/tests/fixtures/duckdb/pivot.input.sql +1 -0
  58. jarify-0.1.0/tests/fixtures/duckdb/pivot_order_by.expected.sql +21 -0
  59. jarify-0.1.0/tests/fixtures/duckdb/pivot_order_by.input.sql +1 -0
  60. jarify-0.1.0/tests/fixtures/duckdb/qualify.expected.sql +7 -0
  61. jarify-0.1.0/tests/fixtures/duckdb/qualify.input.sql +1 -0
  62. jarify-0.1.0/tests/fixtures/duckdb/read_parquet.expected.sql +3 -0
  63. jarify-0.1.0/tests/fixtures/duckdb/read_parquet.input.sql +1 -0
  64. jarify-0.1.0/tests/fixtures/duckdb/reserved_keyword_type_cast.expected.sql +21 -0
  65. jarify-0.1.0/tests/fixtures/duckdb/reserved_keyword_type_cast.input.sql +13 -0
  66. jarify-0.1.0/tests/fixtures/duckdb/struct_cast_alignment.expected.sql +8 -0
  67. jarify-0.1.0/tests/fixtures/duckdb/struct_cast_alignment.input.sql +1 -0
  68. jarify-0.1.0/tests/fixtures/duckdb/type_cast.expected.sql +6 -0
  69. jarify-0.1.0/tests/fixtures/duckdb/type_cast.input.sql +1 -0
  70. jarify-0.1.0/tests/fixtures/duckdb/values_cte.expected.sql +25 -0
  71. jarify-0.1.0/tests/fixtures/duckdb/values_cte.input.sql +6 -0
  72. jarify-0.1.0/tests/fixtures/joins/aligned_joins.expected.sql +10 -0
  73. jarify-0.1.0/tests/fixtures/joins/aligned_joins.input.sql +1 -0
  74. jarify-0.1.0/tests/fixtures/joins/bare_join.expected.sql +5 -0
  75. jarify-0.1.0/tests/fixtures/joins/bare_join.input.sql +1 -0
  76. jarify-0.1.0/tests/fixtures/joins/multi_join.expected.sql +6 -0
  77. jarify-0.1.0/tests/fixtures/joins/multi_join.input.sql +1 -0
  78. jarify-0.1.0/tests/fixtures/select/alias_normalization.expected.sql +6 -0
  79. jarify-0.1.0/tests/fixtures/select/alias_normalization.input.sql +1 -0
  80. jarify-0.1.0/tests/fixtures/select/basic.expected.sql +9 -0
  81. jarify-0.1.0/tests/fixtures/select/basic.input.sql +1 -0
  82. jarify-0.1.0/tests/fixtures/select/from_first.expected.sql +11 -0
  83. jarify-0.1.0/tests/fixtures/select/from_first.input.sql +3 -0
  84. jarify-0.1.0/tests/fixtures/select/where_clause.expected.sql +8 -0
  85. jarify-0.1.0/tests/fixtures/select/where_clause.input.sql +1 -0
  86. jarify-0.1.0/tests/fixtures/select/where_eq_alignment.expected.sql +9 -0
  87. jarify-0.1.0/tests/fixtures/select/where_eq_alignment.input.sql +4 -0
  88. jarify-0.1.0/tests/test_config.py +18 -0
  89. jarify-0.1.0/tests/test_duckdb_rules.py +64 -0
  90. jarify-0.1.0/tests/test_fixtures.py +75 -0
  91. jarify-0.1.0/tests/test_formatter.py +226 -0
  92. jarify-0.1.0/tests/test_lint_rules.py +208 -0
  93. jarify-0.1.0/tests/test_parser.py +96 -0
  94. jarify-0.1.0/uv.lock +187 -0
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: blacksmith-2vcpu-ubuntu-2404
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install mise
20
+ uses: jdx/mise-action@v2
21
+
22
+ - name: Install dependencies
23
+ run: uv sync --frozen --all-groups
24
+
25
+ - name: Lint
26
+ run: mise run lint
27
+
28
+ - name: Test
29
+ run: mise run test
@@ -0,0 +1,59 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+ packages: write
11
+ id-token: write # required for OIDC trusted publishing to PyPI
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: blacksmith-2vcpu-ubuntu-2404
16
+
17
+ steps:
18
+ - name: Fetch secrets from Doppler
19
+ uses: dopplerhq/secrets-fetch-action@v2.0.0
20
+ id: doppler
21
+ with:
22
+ doppler-token: ${{ secrets.DOPPLER_TOKEN }}
23
+ doppler-project: shared
24
+ doppler-config: prod
25
+ inject-env-vars: false
26
+
27
+ - name: Generate token
28
+ id: app-token
29
+ uses: actions/create-github-app-token@v2
30
+ with:
31
+ app-id: ${{ steps.doppler.outputs.WORKFLOW_CLIENT_ID }}
32
+ private-key: ${{ steps.doppler.outputs.WORKFLOW_CLIENT_PRIVATE_KEY }}
33
+
34
+ - uses: actions/checkout@v4
35
+ with:
36
+ token: ${{ steps.app-token.outputs.token }}
37
+
38
+ - name: Install mise
39
+ uses: jdx/mise-action@v2
40
+
41
+ - name: Install dependencies
42
+ run: uv sync --frozen
43
+
44
+ - name: Build package
45
+ run: uv build
46
+
47
+ - name: Publish to PyPI
48
+ run: uv publish
49
+ env:
50
+ UV_PUBLISH_TRUSTED_PUBLISHING: true
51
+
52
+ - name: Create GitHub Release
53
+ run: |
54
+ gh release create "${{ github.ref_name }}" \
55
+ --title "${{ github.ref_name }}" \
56
+ --generate-notes \
57
+ dist/*
58
+ env:
59
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ tmp/
@@ -0,0 +1 @@
1
+ 3.13
jarify-0.1.0/AGENTS.md ADDED
@@ -0,0 +1,46 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI agents working in this repository.
4
+
5
+ ## What this project is
6
+
7
+ Jarify is a SQL formatter and linter for DuckDB, built on [sqlglot](https://github.com/tobymao/sqlglot). It parses SQL with the DuckDB dialect and rewrites it through an opinionated, non-configurable style.
8
+
9
+ ## Dev setup
10
+
11
+ Requires [mise](https://mise.jdx.dev) and [uv](https://docs.astral.sh/uv/).
12
+
13
+ ```bash
14
+ uv sync --all-groups
15
+ ```
16
+
17
+ ## Common tasks
18
+
19
+ | Task | Command |
20
+ |------|---------|
21
+ | Run tests | `mise run test` |
22
+ | Run linter | `mise run lint` |
23
+ | Run both | `mise run check` |
24
+ | Format source | `mise run fmt` |
25
+
26
+ Always run `mise run check` before committing and ensure all tests pass.
27
+
28
+ ## Adding or changing a rule
29
+
30
+ 1. **Formatting rules** live in `src/jarify/generator.py` (override a generator method) or `src/jarify/rules/` (subclass `FormatterRule` and implement `apply()`).
31
+ 2. **Lint-only rules** live in `src/jarify/rules/` (implement `check()`, leave `apply()` as a no-op).
32
+ 3. Register new rules in `src/jarify/rules/__init__.py` and add a config knob to `src/jarify/config.py`.
33
+ 4. Add a fixture pair in `tests/fixtures/<category>/` and unit tests in `tests/`.
34
+ 5. **Document every new or changed rule in [`docs/sql-style-guide.md`](docs/sql-style-guide.md)** with a bad/good SQL example.
35
+
36
+ ## Adding fixture tests
37
+
38
+ ```bash
39
+ # 1. Create the input file
40
+ echo "SELECT a, b FROM t" > tests/fixtures/<category>/<name>.input.sql
41
+
42
+ # 2. Generate the expected output
43
+ uv run pytest tests/test_fixtures.py --update-fixtures
44
+
45
+ # 3. Review the generated .expected.sql, then commit both files
46
+ ```
jarify-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 John Allen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jarify-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: jarify
3
+ Version: 0.1.0
4
+ Summary: Bespoke SQL linter and formatter for DuckDB, powered by sqlglot
5
+ Project-URL: Homepage, https://github.com/amfaro/jarify
6
+ Project-URL: Repository, https://github.com/amfaro/jarify
7
+ Project-URL: Issues, https://github.com/amfaro/jarify/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: duckdb,formatter,linter,sql,sqlglot
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: click>=8.3.2
22
+ Requires-Dist: rich>=14.3.3
23
+ Requires-Dist: sqlglot>=30.4.2
24
+ Description-Content-Type: text/markdown
25
+
26
+ # jarify
27
+
28
+ Bespoke SQL linter and formatter for [DuckDB](https://duckdb.org), built on [sqlglot](https://github.com/tobymao/sqlglot).
29
+
30
+ Existing SQL formatters can't be configured to enforce a specific team style, and none of them are DuckDB-aware. Jarify parses SQL with the DuckDB dialect and rewrites it through an opinionated, non-configurable formatter — no style debates, consistent output everywhere.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ uv tool install jarify
36
+ ```
37
+
38
+ Or run one-off without installing:
39
+
40
+ ```bash
41
+ uvx jarify fmt path/to/query.sql
42
+ ```
43
+
44
+ ### Upgrade
45
+
46
+ ```bash
47
+ uv tool upgrade jarify
48
+ ```
49
+
50
+ ### Pin to a specific version
51
+
52
+ ```bash
53
+ uv tool install 'jarify==0.1.0'
54
+ ```
55
+
56
+ ## Commands
57
+
58
+ ### `jarify fmt` — format SQL files
59
+
60
+ ```
61
+ jarify fmt [OPTIONS] [FILES]...
62
+ ```
63
+
64
+ Reads each file, formats it, and writes the result back in place.
65
+
66
+ | Option | Description |
67
+ |--------|-------------|
68
+ | `-` | Read from stdin |
69
+ | `--check` | Exit non-zero if any file would change (useful in CI) |
70
+ | `--diff` | Print a unified diff instead of rewriting files |
71
+ | `--stdin-filename NAME` | Label to use in diff output when reading from stdin |
72
+
73
+ **Exit codes:** `0` = all files already formatted, `1` = files were reformatted, `2` = error.
74
+
75
+ ```bash
76
+ # Format a single file
77
+ jarify fmt query.sql
78
+
79
+ # Check all .sql files without modifying them (CI mode)
80
+ jarify fmt --check **/*.sql
81
+
82
+ # Preview changes as a diff
83
+ jarify fmt --diff query.sql
84
+
85
+ # Pipe from stdin
86
+ cat query.sql | jarify fmt -
87
+ ```
88
+
89
+ ### `jarify lint` — lint SQL files
90
+
91
+ ```
92
+ jarify lint [OPTIONS] [FILES]...
93
+ ```
94
+
95
+ Reports style and semantic violations. Does not modify files.
96
+
97
+ ```bash
98
+ jarify lint query.sql
99
+ ```
100
+
101
+ ### `jarify init` — create a config file
102
+
103
+ ```bash
104
+ jarify init
105
+ ```
106
+
107
+ Writes a `jarify.toml` in the current directory. Config is internal to the tool — this is primarily useful for future per-project rule overrides.
108
+
109
+ ### `jarify show-config` — inspect active config
110
+
111
+ ```bash
112
+ jarify show-config
113
+ ```
114
+
115
+ Prints the effective configuration (syntax-highlighted TOML).
116
+
117
+ ## Style and lint rules
118
+
119
+ Jarify enforces a single, opinionated style. There are no knobs to turn. See the **[SQL Style Guide](docs/sql-style-guide.md)** for the complete rule reference with bad/good examples for every formatting and lint rule.
120
+
121
+ ## Development
122
+
123
+ Requires [mise](https://mise.jdx.dev) and [uv](https://docs.astral.sh/uv/).
124
+
125
+ ```bash
126
+ git clone https://github.com/amfaro/jarify.git
127
+ cd jarify
128
+ uv sync --all-groups
129
+
130
+ mise run test # run tests
131
+ mise run lint # ruff check
132
+ mise run check # lint + test
133
+ ```
134
+
135
+ ### Adding a new fixture test
136
+
137
+ 1. Create `tests/fixtures/<category>/<name>.input.sql` with the raw SQL
138
+ 2. Run `uv run pytest tests/test_fixtures.py --update-fixtures` to generate the expected output
139
+ 3. Review `tests/fixtures/<category>/<name>.expected.sql` and commit both files
140
+
141
+ ### Releases
142
+
143
+ Push a `v*` tag to trigger the publish workflow. GitHub Actions builds the wheel and sdist, publishes to PyPI via OIDC trusted publishing, attaches artifacts to a GitHub Release, and generates release notes automatically.
144
+
145
+ ```bash
146
+ git tag v0.1.0
147
+ git push origin v0.1.0
148
+ ```
jarify-0.1.0/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # jarify
2
+
3
+ Bespoke SQL linter and formatter for [DuckDB](https://duckdb.org), built on [sqlglot](https://github.com/tobymao/sqlglot).
4
+
5
+ Existing SQL formatters can't be configured to enforce a specific team style, and none of them are DuckDB-aware. Jarify parses SQL with the DuckDB dialect and rewrites it through an opinionated, non-configurable formatter — no style debates, consistent output everywhere.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ uv tool install jarify
11
+ ```
12
+
13
+ Or run one-off without installing:
14
+
15
+ ```bash
16
+ uvx jarify fmt path/to/query.sql
17
+ ```
18
+
19
+ ### Upgrade
20
+
21
+ ```bash
22
+ uv tool upgrade jarify
23
+ ```
24
+
25
+ ### Pin to a specific version
26
+
27
+ ```bash
28
+ uv tool install 'jarify==0.1.0'
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `jarify fmt` — format SQL files
34
+
35
+ ```
36
+ jarify fmt [OPTIONS] [FILES]...
37
+ ```
38
+
39
+ Reads each file, formats it, and writes the result back in place.
40
+
41
+ | Option | Description |
42
+ |--------|-------------|
43
+ | `-` | Read from stdin |
44
+ | `--check` | Exit non-zero if any file would change (useful in CI) |
45
+ | `--diff` | Print a unified diff instead of rewriting files |
46
+ | `--stdin-filename NAME` | Label to use in diff output when reading from stdin |
47
+
48
+ **Exit codes:** `0` = all files already formatted, `1` = files were reformatted, `2` = error.
49
+
50
+ ```bash
51
+ # Format a single file
52
+ jarify fmt query.sql
53
+
54
+ # Check all .sql files without modifying them (CI mode)
55
+ jarify fmt --check **/*.sql
56
+
57
+ # Preview changes as a diff
58
+ jarify fmt --diff query.sql
59
+
60
+ # Pipe from stdin
61
+ cat query.sql | jarify fmt -
62
+ ```
63
+
64
+ ### `jarify lint` — lint SQL files
65
+
66
+ ```
67
+ jarify lint [OPTIONS] [FILES]...
68
+ ```
69
+
70
+ Reports style and semantic violations. Does not modify files.
71
+
72
+ ```bash
73
+ jarify lint query.sql
74
+ ```
75
+
76
+ ### `jarify init` — create a config file
77
+
78
+ ```bash
79
+ jarify init
80
+ ```
81
+
82
+ Writes a `jarify.toml` in the current directory. Config is internal to the tool — this is primarily useful for future per-project rule overrides.
83
+
84
+ ### `jarify show-config` — inspect active config
85
+
86
+ ```bash
87
+ jarify show-config
88
+ ```
89
+
90
+ Prints the effective configuration (syntax-highlighted TOML).
91
+
92
+ ## Style and lint rules
93
+
94
+ Jarify enforces a single, opinionated style. There are no knobs to turn. See the **[SQL Style Guide](docs/sql-style-guide.md)** for the complete rule reference with bad/good examples for every formatting and lint rule.
95
+
96
+ ## Development
97
+
98
+ Requires [mise](https://mise.jdx.dev) and [uv](https://docs.astral.sh/uv/).
99
+
100
+ ```bash
101
+ git clone https://github.com/amfaro/jarify.git
102
+ cd jarify
103
+ uv sync --all-groups
104
+
105
+ mise run test # run tests
106
+ mise run lint # ruff check
107
+ mise run check # lint + test
108
+ ```
109
+
110
+ ### Adding a new fixture test
111
+
112
+ 1. Create `tests/fixtures/<category>/<name>.input.sql` with the raw SQL
113
+ 2. Run `uv run pytest tests/test_fixtures.py --update-fixtures` to generate the expected output
114
+ 3. Review `tests/fixtures/<category>/<name>.expected.sql` and commit both files
115
+
116
+ ### Releases
117
+
118
+ Push a `v*` tag to trigger the publish workflow. GitHub Actions builds the wheel and sdist, publishes to PyPI via OIDC trusted publishing, attaches artifacts to a GitHub Release, and generates release notes automatically.
119
+
120
+ ```bash
121
+ git tag v0.1.0
122
+ git push origin v0.1.0
123
+ ```