Skip to content

Commit dcbadd1

Browse files
committed
feat: ship the sqlquack command with pip install sqlquack
pip install sqlquack installed only the Python module. A new sqlquack-cli distribution ships the native sqlquack executable, and sqlquack depends on it at the same version. Both wheels compile DuckDB in, so they need no system DuckDB, and carry the licence and third-party notices, now including DuckDB's; make distribution-notices checks the shipped copies match the root files. Distributions builds Linux, macOS and Windows wheels on pull requests that touch packaging and smoke tests each one with the quick start and the README's commands and Python example. Release Please and Publish follow SQLBuild's and stay idle until the SQLQUACK_RELEASES repository variable is enabled at launch. Rebuilt on current main from feat/pip-console-command.
1 parent 19c51fa commit dcbadd1

22 files changed

Lines changed: 1472 additions & 5 deletions
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
name: Build Distributions
2+
3+
# The single owner of the wheel platform matrix, the maturin settings and the distribution smoke
4+
# tests. Distributions builds the wheels for pull requests that touch packaging, Release
5+
# Distributions builds and keeps every distribution for a release pull request, and Publish either
6+
# smoke tests and reuses those distributions or builds them here from the release tag.
7+
on:
8+
workflow_call:
9+
inputs:
10+
ref:
11+
description: "Commit, tag or branch to build; empty builds the triggering commit"
12+
required: false
13+
type: string
14+
default: ""
15+
sdist:
16+
description: "Also build the source distributions"
17+
required: false
18+
type: boolean
19+
default: true
20+
upload:
21+
description: "Upload the distributions and a manifest recording their source commit and tree"
22+
required: false
23+
type: boolean
24+
default: false
25+
prebuilt-run-id:
26+
description: >-
27+
Release Distributions run whose distributions are smoke tested and uploaded instead of
28+
building; its manifest must record the tree of ref. The caller grants actions: read.
29+
required: false
30+
type: string
31+
default: ""
32+
retention-days:
33+
description: "Retention of uploaded artifacts in days; 0 uses the repository default"
34+
required: false
35+
type: number
36+
default: 0
37+
outputs:
38+
sha:
39+
description: "Commit the distributions were built from"
40+
value: ${{ jobs.source.outputs.sha }}
41+
tree:
42+
description: "Tree of the commit the distributions were built from"
43+
value: ${{ jobs.source.outputs.tree }}
44+
45+
jobs:
46+
source:
47+
name: Resolve source
48+
runs-on: ubuntu-24.04
49+
outputs:
50+
sha: ${{ steps.source.outputs.sha }}
51+
tree: ${{ steps.source.outputs.tree }}
52+
steps:
53+
- uses: actions/checkout@v4
54+
with:
55+
ref: ${{ inputs.ref }}
56+
- name: Pin the source commit
57+
id: source
58+
run: |
59+
sha="$(git rev-parse HEAD)"
60+
tree="$(git rev-parse 'HEAD^{tree}')"
61+
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
62+
echo "tree=${tree}" >> "${GITHUB_OUTPUT}"
63+
echo "Distributions for commit ${sha} with tree ${tree}" >> "${GITHUB_STEP_SUMMARY}"
64+
- name: Download the prebuilt manifest
65+
if: inputs.prebuilt-run-id != ''
66+
uses: actions/download-artifact@v4
67+
with:
68+
name: release-manifest
69+
path: ${{ runner.temp }}/prebuilt
70+
run-id: ${{ inputs.prebuilt-run-id }}
71+
github-token: ${{ github.token }}
72+
- name: Require prebuilt distributions from this exact tree
73+
if: inputs.prebuilt-run-id != ''
74+
env:
75+
MANIFEST: ${{ runner.temp }}/prebuilt/release-manifest.json
76+
PREBUILT_RUN_ID: ${{ inputs.prebuilt-run-id }}
77+
TREE: ${{ steps.source.outputs.tree }}
78+
run: |
79+
recorded="$(jq -r '.tree // empty' "${MANIFEST}")"
80+
if [[ "${recorded}" != "${TREE}" ]]; then
81+
echo "Run ${PREBUILT_RUN_ID} built tree ${recorded:-<none>}, not ${TREE}" >&2
82+
exit 1
83+
fi
84+
echo "Reusing distributions of run ${PREBUILT_RUN_ID}, built from tree ${TREE}" \
85+
>> "${GITHUB_STEP_SUMMARY}"
86+
87+
wheels:
88+
name: Build wheels (${{ matrix.platform.name }})
89+
needs: source
90+
runs-on: ${{ matrix.platform.runner }}
91+
timeout-minutes: 90
92+
strategy:
93+
fail-fast: false
94+
matrix:
95+
platform:
96+
- name: linux-x86_64
97+
runner: ubuntu-24.04
98+
target: x86_64
99+
- name: linux-aarch64
100+
runner: ubuntu-24.04-arm
101+
target: aarch64
102+
# DuckDB's C++ needs aligned deallocation, available from macOS 10.13.
103+
- name: macos-x86_64
104+
runner: macos-15-intel
105+
target: x86_64
106+
macos-target: "10.15"
107+
- name: macos-arm64
108+
runner: macos-15
109+
target: aarch64
110+
macos-target: "11.0"
111+
- name: windows-x86_64
112+
runner: windows-2025
113+
target: x86_64
114+
steps:
115+
- uses: actions/checkout@v4
116+
if: inputs.prebuilt-run-id == ''
117+
with:
118+
ref: ${{ needs.source.outputs.sha }}
119+
- uses: actions/setup-python@v5
120+
with:
121+
python-version: "3.12"
122+
# Both wheels compile DuckDB in (the `bundled` feature), so neither needs a system DuckDB.
123+
- name: Set the oldest macOS the wheels support
124+
if: matrix.platform.macos-target
125+
shell: bash
126+
run: echo "MACOSX_DEPLOYMENT_TARGET=${{ matrix.platform.macos-target }}" >> "$GITHUB_ENV"
127+
- uses: PyO3/maturin-action@v1
128+
if: inputs.prebuilt-run-id == ''
129+
with:
130+
command: build
131+
args: --release --locked --out ../dist
132+
working-directory: python
133+
target: ${{ matrix.platform.target }}
134+
manylinux: "2_28"
135+
- uses: PyO3/maturin-action@v1
136+
if: inputs.prebuilt-run-id == ''
137+
with:
138+
command: build
139+
args: --release --locked --out ../../dist
140+
working-directory: crates/sqlquack
141+
target: ${{ matrix.platform.target }}
142+
manylinux: "2_28"
143+
- name: Download prebuilt wheels
144+
if: inputs.prebuilt-run-id != ''
145+
uses: actions/download-artifact@v4
146+
with:
147+
name: dist-wheel-${{ matrix.platform.name }}
148+
path: dist
149+
run-id: ${{ inputs.prebuilt-run-id }}
150+
github-token: ${{ github.token }}
151+
- name: Smoke test the installed wheels as the site's quick start runs them
152+
shell: bash
153+
run: |
154+
python -m venv "${RUNNER_TEMP}/wheel-smoke"
155+
if [[ -x "${RUNNER_TEMP}/wheel-smoke/Scripts/python.exe" ]]; then
156+
bin="${RUNNER_TEMP}/wheel-smoke/Scripts"
157+
else
158+
bin="${RUNNER_TEMP}/wheel-smoke/bin"
159+
fi
160+
# Only the wheels just built: sqlquack-cli must resolve from dist, never from an index.
161+
"${bin}/python" -m pip install --no-index --find-links dist sqlquack
162+
cd "${RUNNER_TEMP}"
163+
"${bin}/sqlquack" --version
164+
"${bin}/python" -c "import sqlquack.api"
165+
set +e
166+
check="$(echo "SELECT 1 QUALIFY TRUE" | "${bin}/sqlquack" check 2>&1)"
167+
status=$?
168+
set -e
169+
echo "${check}"
170+
if [[ ${status} -ne 1 ]] || ! grep -q 'error\[SQ0203\]' <<< "${check}"; then
171+
echo "check should refuse QUALIFY without a window function with SQ0203 and exit 1" >&2
172+
exit 1
173+
fi
174+
converted="$(echo "SELECT 1 AS total" | "${bin}/sqlquack" convert --builtin-policy)"
175+
echo "${converted}"
176+
grep -qx 'SELECT CAST(1 AS DECIMAL(1,0)) AS "TOTAL"' <<< "${converted}"
177+
# The README's file commands, with no sqlquack.toml in the directory: the built-in policy.
178+
mkdir readme-smoke && cd readme-smoke
179+
printf "SELECT customer_id, SUM(total) AS revenue\nFROM orders\nGROUP BY customer_id\n" > query.sql
180+
printf '{"ORDERS": [["CUSTOMER_ID", "NUMBER(38,0)"], ["TOTAL", "NUMBER(12,2)"]]}\n' > schema.json
181+
"${bin}/sqlquack" check query.sql
182+
converted="$("${bin}/sqlquack" convert query.sql --schema schema.json)"
183+
echo "${converted}"
184+
grep -q 'CAST(sum("ORDERS"."TOTAL") AS DECIMAL(24,2)) AS "REVENUE"' <<< "${converted}"
185+
# The README's Python example.
186+
"${bin}/python" -c 'import sqlquack
187+
result = sqlquack.convert(sql="SELECT 1 AS total", options=sqlquack.ConvertOptions(builtin_policy=True))
188+
assert result.sql == "SELECT CAST(1 AS DECIMAL(1,0)) AS \"TOTAL\"", result.sql'
189+
- uses: actions/upload-artifact@v4
190+
if: inputs.upload
191+
with:
192+
name: dist-wheel-${{ matrix.platform.name }}
193+
path: dist
194+
if-no-files-found: error
195+
retention-days: ${{ inputs.retention-days }}
196+
197+
sdist:
198+
name: Build sdists
199+
needs: source
200+
if: inputs.sdist
201+
runs-on: ubuntu-24.04
202+
steps:
203+
- uses: actions/checkout@v4
204+
if: inputs.prebuilt-run-id == ''
205+
with:
206+
ref: ${{ needs.source.outputs.sha }}
207+
- uses: PyO3/maturin-action@v1
208+
if: inputs.prebuilt-run-id == ''
209+
with:
210+
command: sdist
211+
args: --out ../dist
212+
working-directory: python
213+
- uses: PyO3/maturin-action@v1
214+
if: inputs.prebuilt-run-id == ''
215+
with:
216+
command: sdist
217+
args: --out ../../dist
218+
working-directory: crates/sqlquack
219+
- name: Download prebuilt source distributions
220+
if: inputs.prebuilt-run-id != ''
221+
uses: actions/download-artifact@v4
222+
with:
223+
name: dist-sdist
224+
path: dist
225+
run-id: ${{ inputs.prebuilt-run-id }}
226+
github-token: ${{ github.token }}
227+
- name: Inspect source distribution contents
228+
run: |
229+
for archive in dist/*.tar.gz; do
230+
tar -tzf "$archive" > "${RUNNER_TEMP}/archive-contents.txt"
231+
if grep -E '(__pycache__/|\.pyc$|\.tmp$)' "${RUNNER_TEMP}/archive-contents.txt"; then
232+
echo "Source distribution contains generated or temporary script files"
233+
exit 1
234+
fi
235+
done
236+
- uses: actions/upload-artifact@v4
237+
if: inputs.upload
238+
with:
239+
name: dist-sdist
240+
path: dist
241+
if-no-files-found: error
242+
retention-days: ${{ inputs.retention-days }}
243+
244+
manifest:
245+
name: Record distribution source
246+
needs: [source, wheels, sdist]
247+
if: >-
248+
${{ !cancelled() && inputs.upload && needs.wheels.result == 'success'
249+
&& (needs.sdist.result == 'success' || needs.sdist.result == 'skipped') }}
250+
runs-on: ubuntu-24.04
251+
steps:
252+
- name: Write the manifest
253+
env:
254+
SHA: ${{ needs.source.outputs.sha }}
255+
TREE: ${{ needs.source.outputs.tree }}
256+
run: |
257+
jq -n --arg sha "${SHA}" --arg tree "${TREE}" --arg run_id "${GITHUB_RUN_ID}" \
258+
'{sha: $sha, tree: $tree, run_id: $run_id}' \
259+
> "${RUNNER_TEMP}/release-manifest.json"
260+
cat "${RUNNER_TEMP}/release-manifest.json"
261+
- uses: actions/upload-artifact@v4
262+
with:
263+
name: release-manifest
264+
path: ${{ runner.temp }}/release-manifest.json
265+
if-no-files-found: error
266+
retention-days: ${{ inputs.retention-days }}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Distributions
2+
3+
# Builds and smoke tests every wheel and sdist when a pull request changes what goes into them.
4+
on:
5+
pull_request:
6+
paths:
7+
- "python/**"
8+
- "crates/sqlquack/pyproject.toml"
9+
- "crates/sqlquack-python/**"
10+
- "Cargo.toml"
11+
- "Cargo.lock"
12+
- "rust-toolchain.toml"
13+
- ".github/workflows/build-distributions.yml"
14+
- ".github/workflows/distributions.yml"
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: distributions-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
distributions:
26+
name: Distributions
27+
uses: ./.github/workflows/build-distributions.yml

0 commit comments

Comments
 (0)