web/book/src/project/contributing/development.md
We can set up a local development environment sufficient for navigating, editing, and testing PRQL's compiler code in two minutes:
Install
rustup & cargo.
[Optional but highly recommended] Install cargo-insta, our testing
framework:
cargo install cargo-insta
That's it! Running the unit tests for the prqlc crate after cloning the repo
should complete successfully:
cargo test --package prqlc --lib
...or, to run tests and update the test snapshots:
cargo insta test --accept --package prqlc --lib
There's more context on our tests in How we test below.
That's sufficient for making an initial contribution to the compiler.
[!NOTE] We really care about this process being easy, both because the project benefits from more contributors like you, and to reciprocate your future contribution. If something isn't easy, please let us know in a GitHub Issue. We'll enthusiastically help you, and use your feedback to improve the scripts & instructions.
For more advanced development; for example compiling for wasm or previewing the website, we have two options:
task[!NOTE] This is tested on macOS, should work on amd64 Linux, but won't work on others (include Windows), since it relies on
brew.
Then run the setup-dev task. This runs commands from our
Taskfile.yaml,
installing dependencies with cargo, brew, npm & uv, and suggests some
VS Code extensions.
task setup-dev
We'll need cargo-insta, to update snapshot tests:
cargo install cargo-insta
We'll need Python, which most systems will have already. The easiest way to check is to try running the full tests:
cargo test
...and if that doesn't complete successfully, ensure we have Python >= 3.7, to
compile prqlc-python.
For more involved contributions, such as building the website, playground, book, or some release artifacts, we'll need some additional tools. But we won't need those immediately, and the error messages on what's missing should be clear when we attempt those things. When we hit them, the Taskfile.yaml will be a good source to copy & paste instructions from.
This project has a devcontainer.json file and a pre-built dev container base Docker image. Learn more about Dev Containers at https://containers.dev/
Currently, the tools for Rust are already installed in the pre-built image, and, Node.js, Python and others are configured to be installed when build the container.
While there are a variety of tools that support Dev Containers, the focus here is on developing with VS Code in a container by GitHub Codespaces or VS Code Dev Containers extension.
To use a Dev Container on a local computer with VS Code, install the VS Code Dev Containers extension and its system requirements. Then refer to the links above to get started.
[!NOTE] This is used by a member of the core team on Linux, but doesn't currently work on Mac. We're open to contributions to improve support.
A nix flake flake.nix provides 3 development
environments:
To load the shell:
Install nix (the package manager). (only first time)
Enable flakes, which are a (pretty stable) experimental feature of nix. (only first time)
For non-NixOS users:
mkdir -p ~/.config/nix/
tee 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
For NixOS users, follow instructions here.
Run:
nix develop
To use the "web" or "full" shell, run:
nix develop .#web
Optionally, you can install direnv, to automatically load
the shell when you enter this repo. The easiest way is to also install
direnv-nix and configure your
.envrc with:
# .envrc
use flake .#full
We're similar to most projects on GitHub — open a Pull Request with a suggested change!
CHANGELOG.md,
with {message}, ({@contributor, #X}) where X is the PR number.
We're very keen on contributions to improve our documentation.
This includes our docs in the book, on the website, in our code, or in a Readme. We also appreciate issues pointing out that our documentation was confusing, incorrect, or stale — if it's confusing for you, it's probably confusing for others.
Some principles for ensuring our docs remain maintainable:
--help message, rather than write a paragraph in the Readme
explaining the CLI.If something doesn't fit into one of these categories, there are still lots of ways of getting the word out there — a blog post / gist / etc. Let us know and we're happy to link to it / tweet it.
We use a pyramid of tests — we have fast, focused tests at the bottom of the pyramid, which give us low latency feedback when developing, and then slower, broader tests which ensure that we don't miss anything as PRQL develops{{footnote: Our approach is very consistent with @matklad's advice, in his excellent blog post How to Test.}}.
<!-- markdownlint-disable MD053 --> <!-- prettier-ignore -->[!NOTE] If you're making your first contribution, you don't need to engage with all this — it's fine to just make a change and push the results; the tests that run in GitHub will point you towards any errors, which can be then be run locally if needed. We're always around to help out.
Our tests, from the bottom of the pyramid to the top:
Static checks
— we run a few static checks to ensure the code stays healthy and consistent.
They're defined in
.pre-commit-config.yaml,
using pre-commit. They can be run locally with
task lint
# or
pre-commit run -a
The tests fix most of the issues they find themselves. Most of them also run on GitHub on every commit; any changes they make are added onto the branch automatically in an additional commit.
Unit tests & inline insta snapshots — we rely on unit tests to rapidly check that our code basically works. We extensively use Insta, a snapshot testing tool which writes out the values generated by our code, making it fast & simple to write and modify tests{{footnote: Here's an example of an insta test — note that only the initial line of each test is written by us; the remainder is filled in by insta.}}
These are the fastest tests which run our code; they're designed to run on
every save while you're developing. We include a task which does this:
task prqlc:test
# or
cargo insta test --accept --package prqlc --lib
Documentation — we compile all examples from our documentation in the Website, README, and PRQL Book, to test that they produce the SQL we expect, and that changes to our code don't cause any unexpected regressions. These are included in:
cargo insta test --accept
Database integration tests — we run tests with example queries against databases with actual data to ensure we're producing correct SQL across our supported dialects. The in-process tests can be run locally with:
task prqlc:test-all
# or
cargo insta test --accept --features=default,test-dbs
More details on running with external databases are in the Readme.
[!NOTE] Integration tests use DuckDB, and so require a clang compiler to compile
duckdb-rs. Most development systems will have one, but if the test command fails, install a clang compiler with:
- On macOS, install xcode with
xcode-select --install- On Debian Linux,
apt-get update && apt-get install clang- On Windows,
duckdb-rsisn't supported, so these tests are excluded
GitHub Actions on every commit — we run tests relevant to a PR's changes in CI — for example changes to docs will attempt to build docs, changes to a binding will run that binding's tests. The vast majority of changes trigger tests which run in less than five minutes, and we should be reassessing their scope if they take longer than that. Once these pass, a pull request can be merged.
GitHub Actions on merge — we run a wider set tests on every merge to main. This includes testing across OSs, all our language bindings, a measure of test code coverage, and some performance benchmarks.
If these tests fail after merging, we should revert the commit before fixing the test and then re-reverting.
Most of these will run locally with:
task test-all
GitHub Actions nightly — every night, we run tests that take longer, are less likely to fail, or are unrelated to code changes — such as security checks, bindings' tests on multiple OSs, or expensive timing benchmarks.
We can run these tests before a merge by adding a label pr-nightly to the
PR.
The goal of our tests is to allow us to make changes quickly. If they're making it more difficult to make changes, or there are missing tests that would offer the confidence to make changes faster, please raise an issue.
The website is published together with the book and the playground, and is
automatically built and released on any push to the web branch.
The web branch points to the latest release plus any website-specific fixes.
That way, the compiler behavior in the playground matches the latest release
while allowing us to fix mistakes in the docs with a tighter loop than every
release.
Fixes to the playground, book, or website should have a pr-backport-web label
added to their PR — a bot will then open & merge another PR onto the web
branch once the initial branch merges.
The website components will run locally with:
# Run the main website
task web:run-website
# Run the PRQL online book
task web:run-book
# Run the PRQL playground
task web:run-playground
We have a number of language bindings, as documented at https://prql-lang.org/book/project/bindings/index.html. Some of these are within our monorepo, some are in separate repos. Here's a provisional framework for when we use the main prql repo vs separate repos for bindings:
| Factor | Rationale | Example |
|---|---|---|
| Does someone want to sign up to maintain a repo? | A different repo is harder for the core team to maintain | tree-sitter-prql is well maintained |
| Can it change independently from the compiler? | If it's in a different repo, it can't be changed in lockstep with the compiler | prql-vscode is fine to change "behind" the language |
| Would a separate repo invite new contributors? | A monorepo with all the rust code can be less inviting for those familiar with other langs | prql-vscode had some JS-only contributors |
| Is there an convention for a stand-alone repo? | A small number of ecosystems require a separate repo | homebrew-prql needs to be named that way for a Homebrew tap |
Currently we release in a semi-automated way:
PR & merge an updated Changelog. GitHub will produce a draft version at https://github.com/PRQL/prql/releases/new, including "New Contributors".
Use this script to generate a line introducing the enumerated changes:
echo "It has $(git rev-list --count $(git rev-list --tags --max-count=1)..) commits from $(git shortlog --summary $(git rev-list --tags --max-count=1).. | wc -l | tr -d '[:space:]') contributors. Selected changes:"
When a fix closes an issue reported by someone other than the PR author,
thank them in the changelog entry, e.g.
(@pr-author, #5639; reported by @issue-reporter).
If the current version is correct, then skip ahead. But if the version needs
to be changed — for example, we had planned on a patch release, but instead
require a minor release — then run
cargo release version $version -x && cargo release replace -x && task prqlc:test-all
to bump the version, and PR the resulting commit.
Ensure all changes intended for the release are merged to main. Then create
the release (which creates the tag on the latest commit on main):
Web UI: Go to Draft a new release{{footnote: Only maintainers have access to this page.}}, copy the changelog entry into the release description{{footnote: Unfortunately GitHub's markdown parser interprets linebreaks as newlines. I haven't found a better way of editing the markdown to look reasonable than manually editing the text or asking LLM to help.}}, enter the tag to be created, and hit "Publish".
CLI:
gh release create $version --title "$version" --notes "$(cat <<'EOF'
<paste changelog entry here>
EOF
)"
From there, both the tag and release is created and all packages are published automatically based on our release workflow.
Run
cargo release patch --no-publish --no-push --execute --no-verify --no-confirm --no-tag && task prqlc:test-all
to bump the versions and add a new Changelog section; then PR the resulting
commit. Note this currently contains task prqlc:test-all to update snapshot
tests which contain the version.
Check whether there are milestones that need to be pushed out.
Review the Current Status on the README.md to ensure it reflects the project state.
We may make this more automated in future; e.g. automatic changelog creation.